mirror of https://github.com/sipwise/rtpengine.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1708 lines
35 KiB
1708 lines
35 KiB
#include <locale.h>
|
|
#include <ncurses.h>
|
|
#include <pthread.h>
|
|
#include <stdbool.h>
|
|
#include <unistd.h>
|
|
#include <sys/time.h>
|
|
#include <sys/timerfd.h>
|
|
#include <sys/resource.h>
|
|
#include <fcntl.h>
|
|
#include <stdarg.h>
|
|
#include <libavformat/avformat.h>
|
|
|
|
#include "auxlib.h"
|
|
#include "codeclib.h"
|
|
#include "poller.h"
|
|
#include "ssllib.h"
|
|
#include "obj.h"
|
|
#include "fix_frame_channel_layout.h"
|
|
|
|
|
|
|
|
#define COMM_SIZE 25 // including null byte
|
|
#define COMM_SIZE_SCAN "24" // COMM_SIZE without null byte as string
|
|
|
|
|
|
|
|
struct testparams {
|
|
const char *name;
|
|
const char *file;
|
|
GPtrArray *fixture; // not a reference
|
|
void (*read_fn)(GPtrArray *, struct testparams *);
|
|
int clock_rate;
|
|
int channels;
|
|
enum AVCodecID codec_id;
|
|
};
|
|
|
|
struct stream {
|
|
struct obj obj;
|
|
int timer_fd;
|
|
int output_fd;
|
|
char *type;
|
|
|
|
mutex_t lock;
|
|
unsigned long long input_ts;
|
|
unsigned long long output_ts;
|
|
decoder_t *decoder;
|
|
encoder_t *encoder;
|
|
struct testparams in_params;
|
|
struct testparams out_params;
|
|
uint fixture_idx;
|
|
|
|
uint dump_count;
|
|
AVFormatContext *fmtctx;
|
|
AVStream *avst;
|
|
};
|
|
|
|
struct stats {
|
|
long long iv; // last interval, or sum of all intervals
|
|
long long ucpu; // CPU time, or sum of all user CPU times
|
|
long long scpu; // CPU time, or sum of all system CPU times
|
|
long long comput; // compute real time, or sum of all compute times
|
|
|
|
bool blocked; // copied from worker->blocked
|
|
};
|
|
|
|
struct stats_sample {
|
|
struct timeval tv; // last time stats were sampled
|
|
struct stats stats; // last sampled stats
|
|
};
|
|
|
|
struct worker {
|
|
pthread_t thr;
|
|
pid_t pid;
|
|
|
|
bool blocked; // not locked, not critical. set by worker, cleared by output
|
|
|
|
struct stats_sample sample; // owned by output thread
|
|
|
|
mutex_t comput_lock;
|
|
long long comput; // sum of time spent computing in us, reset to 0 at reading
|
|
};
|
|
|
|
struct other_thread {
|
|
char comm[COMM_SIZE];
|
|
struct stats_sample sample; // owned by output thread
|
|
struct stats stats; // temp storage, owned by output thread
|
|
};
|
|
|
|
|
|
typedef void render_fn(const struct stats *stats, int line, int x, int breadth, int width,
|
|
int color,
|
|
const char *titlefmt, ...);
|
|
|
|
|
|
|
|
static struct rtpengine_common_config rtpe_common_config = {
|
|
.log_levels = {
|
|
[log_level_index_ffmpeg] = 6,
|
|
[log_level_index_internals] = 6,
|
|
[log_level_index_core] = 6,
|
|
},
|
|
};
|
|
|
|
|
|
static void fixture_read_avio(GPtrArray *, struct testparams *);
|
|
static void fixture_read_raw(GPtrArray *, struct testparams *);
|
|
|
|
|
|
static const struct testparams testparams[] = {
|
|
{
|
|
.name = "PCMA",
|
|
.file = "pcma.1.8k.raw",
|
|
.read_fn = fixture_read_raw,
|
|
.clock_rate = 8000,
|
|
.channels = 1,
|
|
.codec_id = AV_CODEC_ID_PCM_ALAW,
|
|
},
|
|
{
|
|
.name = "PCMU",
|
|
.file = "pcmu.1.8k.raw",
|
|
.read_fn = fixture_read_raw,
|
|
.clock_rate = 8000,
|
|
.channels = 1,
|
|
.codec_id = AV_CODEC_ID_PCM_MULAW,
|
|
},
|
|
{
|
|
.name = "opus",
|
|
.file = "opus.1.8k.11k.speech.ogg",
|
|
.read_fn = fixture_read_avio,
|
|
.clock_rate = 48000,
|
|
.channels = 2,
|
|
.codec_id = AV_CODEC_ID_OPUS,
|
|
},
|
|
};
|
|
|
|
|
|
// settings
|
|
static char *source_codec = "PCMA";
|
|
static char *dest_codec = "opus";
|
|
static int init_threads = 0;
|
|
static bool bidirectional = false;
|
|
static int max_cpu = 0;
|
|
static bool system_cpu;
|
|
|
|
|
|
#define BLOCKED_COLOR 1
|
|
#define SUMMARY_COLOR 2
|
|
#define CPU_COLOR 3
|
|
#define THREAD_COLOR 4
|
|
|
|
|
|
static long ticks_per_sec;
|
|
static uint num_cpus;
|
|
static struct stats_sample *cpu_stats;
|
|
|
|
static struct poller *rtpe_poller;
|
|
|
|
static codec_def_t *decoder_def;
|
|
static codec_def_t *encoder_def;
|
|
|
|
static struct testparams in_params;
|
|
static struct testparams out_params;
|
|
|
|
static mutex_t workers_lock = MUTEX_STATIC_INIT;
|
|
static GQueue workers = G_QUEUE_INIT;
|
|
|
|
static __thread struct worker *worker_self;
|
|
|
|
static mutex_t streams_lock = MUTEX_STATIC_INIT;
|
|
static GPtrArray *streams;
|
|
static GHashTable *stream_types;
|
|
|
|
static mutex_t other_threads_lock = MUTEX_STATIC_INIT;
|
|
static GHashTable *other_threads;
|
|
static GHashTable *worker_threads;
|
|
|
|
static mutex_t curses_lock = MUTEX_STATIC_INIT;
|
|
static WINDOW *popup;
|
|
|
|
static long long ptime = 20000; // us XXX
|
|
|
|
|
|
|
|
static render_fn usage_bar;
|
|
static render_fn time_bar;
|
|
|
|
static render_fn *output_fn = usage_bar; // startup default
|
|
static bool do_cpu_stats = false;
|
|
static bool do_thread_stats = false;
|
|
|
|
|
|
|
|
|
|
static pthread_t thread_new(const char *name, void *(*fn)(void *), void *p) {
|
|
pthread_t ret;
|
|
int s = pthread_create(&ret, NULL, fn, p);
|
|
if (s != 0)
|
|
abort();
|
|
#ifdef __GLIBC__
|
|
if (name)
|
|
pthread_setname_np(ret, name);
|
|
#endif
|
|
return ret;
|
|
}
|
|
|
|
|
|
static inline long long us_ticks_scale(long long val) {
|
|
return val * ticks_per_sec / 1000000;
|
|
}
|
|
|
|
|
|
// stream is locked
|
|
static void dump_close(struct stream *s) {
|
|
if (!s->fmtctx)
|
|
return;
|
|
|
|
av_write_trailer(s->fmtctx);
|
|
avio_closep(&s->fmtctx->pb);
|
|
avformat_free_context(s->fmtctx);
|
|
s->fmtctx = NULL;
|
|
}
|
|
|
|
// stream is locked
|
|
static int got_packet_pkt(struct stream *s, AVPacket *pkt) {
|
|
pkt->pts = pkt->dts = s->output_ts;
|
|
s->output_ts += pkt->duration;
|
|
|
|
ssize_t ret = write(s->output_fd, pkt->data, pkt->size);
|
|
(void)ret;
|
|
|
|
if (s->fmtctx) {
|
|
// mkv uses millisecond timestamps
|
|
pkt->pts = pkt->dts = av_rescale(pkt->pts, 1000, out_params.clock_rate);
|
|
av_write_frame(s->fmtctx, pkt);
|
|
avio_flush(s->fmtctx->pb);
|
|
|
|
s->dump_count++;
|
|
if (s->dump_count >= s->in_params.fixture->len) {
|
|
// not technically correct as input frames may not match output frames
|
|
dump_close(s);
|
|
}
|
|
}
|
|
|
|
av_packet_unref(pkt);
|
|
|
|
return 0;
|
|
}
|
|
|
|
// stream is locked
|
|
static int got_packet(encoder_t *encoder, void *p1, void *p2) {
|
|
AVPacket *pkt = encoder->avpkt;
|
|
pkt->pts = fraction_multl(pkt->pts, &encoder->clockrate_fact);
|
|
return got_packet_pkt(p1, pkt);
|
|
}
|
|
|
|
static int got_frame(decoder_t *decoder, AVFrame *frame, void *p1, void *b) {
|
|
struct stream *s = p1;
|
|
encoder_input_fifo(s->encoder, frame, got_packet, s, NULL);
|
|
av_frame_free(&frame);
|
|
return 0;
|
|
}
|
|
|
|
|
|
static void *worker(void *p) {
|
|
thread_cancel_disable();
|
|
worker_self = p;
|
|
worker_self->pid = gettid();
|
|
{
|
|
LOCK(&other_threads_lock);
|
|
g_hash_table_insert(worker_threads, GINT_TO_POINTER(worker_self->pid), NULL);
|
|
}
|
|
poller_loop2(rtpe_poller);
|
|
return NULL;
|
|
}
|
|
|
|
static void readable(int fd, void *o, uintptr_t x) {
|
|
struct stream *s = o;
|
|
|
|
struct timeval start;
|
|
gettimeofday(&start, NULL);
|
|
|
|
LOCK(&s->lock);
|
|
|
|
static const uint64_t max_iters = 10; // hard upper limit for iterations
|
|
uint64_t total_iters = 0;
|
|
|
|
while (true) {
|
|
uint64_t exp;
|
|
ssize_t ret = read(fd, &exp, sizeof(exp));
|
|
if (ret != sizeof(exp)) {
|
|
if (ret == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
|
|
break;
|
|
abort();
|
|
}
|
|
|
|
// workers falling behind timer?
|
|
if (exp >= 2) {
|
|
worker_self->blocked = true;
|
|
if (exp > max_iters)
|
|
exp = max_iters;
|
|
}
|
|
if (total_iters++ > max_iters)
|
|
break; // bail
|
|
|
|
while (exp) {
|
|
AVPacket *data = s->in_params.fixture->pdata[s->fixture_idx++];
|
|
if (s->fixture_idx >= s->in_params.fixture->len)
|
|
s->fixture_idx = 0;
|
|
|
|
str frame;
|
|
str_init_len(&frame, (char *) data->data, data->size);
|
|
decoder_input_data(s->decoder, &frame, s->input_ts, got_frame, s, NULL);
|
|
|
|
s->input_ts += data->duration;
|
|
|
|
exp--;
|
|
}
|
|
}
|
|
|
|
struct timeval end;
|
|
gettimeofday(&end, NULL);
|
|
|
|
LOCK(&worker_self->comput_lock);
|
|
worker_self->comput += timeval_diff(&end, &start);
|
|
}
|
|
|
|
static void closed(int fd, void *o, uintptr_t x) {
|
|
abort();
|
|
}
|
|
|
|
|
|
static void new_threads(uint num) {
|
|
while (num--) {
|
|
struct worker *w = g_slice_alloc0(sizeof(*w));
|
|
|
|
mutex_init(&w->comput_lock);
|
|
|
|
w->thr = thread_new("worker", worker, w);
|
|
|
|
LOCK(&workers_lock);
|
|
g_queue_push_tail(&workers, w);
|
|
}
|
|
}
|
|
static void kill_threads(uint num) {
|
|
GQueue to_join = G_QUEUE_INIT;
|
|
|
|
struct worker *w;
|
|
|
|
while (num--) {
|
|
{
|
|
LOCK(&workers_lock);
|
|
w = g_queue_pop_tail(&workers);
|
|
if (!w)
|
|
break;
|
|
}
|
|
|
|
pthread_cancel(w->thr);
|
|
g_queue_push_tail(&to_join, w);
|
|
}
|
|
|
|
while ((w = g_queue_pop_head(&to_join))) {
|
|
pthread_join(w->thr, NULL);
|
|
g_slice_free1(sizeof(*w), w);
|
|
}
|
|
}
|
|
|
|
|
|
static void stream_free(void *p) {
|
|
struct stream *s = p;
|
|
close(s->timer_fd);
|
|
close(s->output_fd);
|
|
dump_close(s);
|
|
if (s->encoder)
|
|
encoder_free(s->encoder);
|
|
if (s->decoder)
|
|
decoder_close(s->decoder);
|
|
g_free(s->type);
|
|
}
|
|
|
|
|
|
static void new_stream_params(
|
|
const codec_def_t *in_def,
|
|
const struct testparams *inprm,
|
|
const codec_def_t *out_def,
|
|
const struct testparams *outprm
|
|
) {
|
|
struct stream *s = obj_alloc0("stream", sizeof(*s), stream_free);
|
|
|
|
// create timerfd
|
|
s->timer_fd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK);
|
|
if (s->timer_fd == -1)
|
|
abort();
|
|
|
|
// create dummy output fd
|
|
s->output_fd = open("/dev/null", O_WRONLY);
|
|
if (s->output_fd == -1)
|
|
abort();
|
|
|
|
// init contents
|
|
s->in_params = *inprm;
|
|
s->out_params = *outprm;
|
|
s->fixture_idx = ssl_random() % s->in_params.fixture->len;
|
|
mutex_init(&s->lock);
|
|
s->type = g_strdup_printf("%s -> %s", inprm->name, outprm->name);
|
|
|
|
// create decoder and encoder
|
|
|
|
format_t dec_format = {
|
|
.clockrate = inprm->clock_rate,
|
|
.channels = inprm->channels,
|
|
.format = -1,
|
|
};
|
|
|
|
format_t enc_format = {
|
|
.clockrate = outprm->clock_rate,
|
|
.channels = outprm->channels,
|
|
.format = -1,
|
|
};
|
|
|
|
int bitrate = encoder_def->default_bitrate;
|
|
|
|
format_t actual_enc_format;
|
|
|
|
s->encoder = encoder_new();
|
|
int res = encoder_config_fmtp(s->encoder, out_def, bitrate, 20, &dec_format, &enc_format,
|
|
&actual_enc_format,
|
|
NULL, NULL, NULL);
|
|
assert(res == 0); // XXX
|
|
|
|
s->decoder = decoder_new_fmtp(in_def, dec_format.clockrate, dec_format.channels, 20,
|
|
&actual_enc_format, NULL, NULL, NULL); // XXX
|
|
assert(s->decoder != NULL); // XXX
|
|
|
|
// arm timer
|
|
struct itimerspec timer = {
|
|
.it_interval = {
|
|
.tv_sec = 0,
|
|
.tv_nsec = ptime * 1000,
|
|
},
|
|
.it_value = {
|
|
.tv_sec = 0,
|
|
(ssl_random() % ptime) * 1000,
|
|
},
|
|
};
|
|
res = timerfd_settime(s->timer_fd, 0, &timer, NULL);
|
|
if (res != 0)
|
|
abort();
|
|
|
|
struct poller_item pi = {
|
|
.fd = s->timer_fd,
|
|
.obj = &s->obj,
|
|
.readable = readable,
|
|
.closed = closed,
|
|
};
|
|
|
|
res = poller_add_item(rtpe_poller, &pi);
|
|
assert(res == 0);
|
|
|
|
LOCK(&streams_lock);
|
|
g_ptr_array_add(streams, s);
|
|
uint *count = g_hash_table_lookup(stream_types, s->type);
|
|
if (!count) {
|
|
count = g_malloc0(sizeof(*count));
|
|
g_hash_table_insert(stream_types, g_strdup(s->type), count);
|
|
}
|
|
(*count)++;
|
|
}
|
|
|
|
|
|
static void new_stream(void) {
|
|
new_stream_params(decoder_def, &in_params, encoder_def, &out_params);
|
|
if (bidirectional)
|
|
new_stream_params(encoder_def, &out_params, decoder_def, &in_params);
|
|
}
|
|
|
|
|
|
static void new_streams(uint num) {
|
|
while (num--)
|
|
new_stream();
|
|
}
|
|
|
|
|
|
static void del_stream(void) {
|
|
struct stream *s = NULL;
|
|
{
|
|
LOCK(&streams_lock);
|
|
if (streams->len) {
|
|
s = streams->pdata[streams->len - 1];
|
|
g_ptr_array_set_size(streams, streams->len - 1);
|
|
uint *count = g_hash_table_lookup(stream_types, s->type);
|
|
if (count) {
|
|
(*count)--;
|
|
if (!*count)
|
|
g_hash_table_remove(stream_types, s->type);
|
|
}
|
|
}
|
|
}
|
|
if (!s)
|
|
return;
|
|
|
|
poller_del_item(rtpe_poller, s->timer_fd);
|
|
|
|
obj_put(s);
|
|
}
|
|
|
|
static void del_streams_raw(uint num) {
|
|
while (num--)
|
|
del_stream();
|
|
}
|
|
|
|
|
|
static void del_streams(uint num) {
|
|
if (bidirectional)
|
|
num *= 2;
|
|
del_streams_raw(num);
|
|
}
|
|
|
|
|
|
static void set_streams(uint num) {
|
|
if (bidirectional)
|
|
num *= 2;
|
|
|
|
while (true) {
|
|
uint cur;
|
|
{
|
|
LOCK(&streams_lock);
|
|
cur = streams->len;
|
|
}
|
|
if (cur == num)
|
|
break;
|
|
if (cur < num)
|
|
new_stream();
|
|
else
|
|
del_stream();
|
|
}
|
|
}
|
|
|
|
|
|
// curses_lock must be held
|
|
static void kill_popup(void) {
|
|
if (!popup)
|
|
return;
|
|
delwin(popup);
|
|
popup = NULL;
|
|
}
|
|
|
|
|
|
// curses_lock must be held
|
|
static void refresh_all(void) {
|
|
wnoutrefresh(stdscr);
|
|
if (popup) {
|
|
touchwin(popup);
|
|
wnoutrefresh(popup);
|
|
}
|
|
doupdate();
|
|
}
|
|
|
|
|
|
__attribute__((format(printf,1,2)))
|
|
static void show_popup(const char *fmt, ...) {
|
|
va_list va;
|
|
va_start(va, fmt);
|
|
char *s = g_strdup_vprintf(fmt, va);
|
|
va_end(va);
|
|
|
|
// split into lines and get max line length
|
|
GQueue lines = G_QUEUE_INIT;
|
|
str st = STR_INIT(s);
|
|
str token;
|
|
uint llen = 0;
|
|
while (str_token_sep(&token, &st, '\n') == 0) {
|
|
g_queue_push_tail(&lines, str_dup(&token));
|
|
llen = MAX(token.len, llen);
|
|
}
|
|
|
|
// render window
|
|
|
|
{
|
|
LOCK(&curses_lock);
|
|
|
|
kill_popup();
|
|
|
|
int maxx, maxy;
|
|
getmaxyx(stdscr, maxy, maxx);
|
|
|
|
int w = llen + 4, h = lines.length + 2;
|
|
|
|
popup = newwin(h, w, (maxy - h) / 2, (maxx - w) / 2);
|
|
box(popup, 0, 0);
|
|
|
|
int linenum = 1;
|
|
while (lines.length) {
|
|
str *line = g_queue_pop_head(&lines);
|
|
mvwprintw(popup, linenum++, 2, "%s", line->s);
|
|
g_free(line);
|
|
}
|
|
|
|
refresh_all();
|
|
}
|
|
|
|
// wait for key and then kill it
|
|
getch();
|
|
|
|
{
|
|
LOCK(&curses_lock);
|
|
kill_popup();
|
|
refresh_all();
|
|
}
|
|
}
|
|
|
|
|
|
// streams_lock must be held
|
|
// returns g_malloc'd string
|
|
static char *start_dump_stream(struct stream *s, const char *suffix) {
|
|
char *msg = NULL;
|
|
const char *err = NULL;
|
|
|
|
{
|
|
LOCK(&s->lock);
|
|
|
|
err = "Stream is already dumping";
|
|
if (s->fmtctx)
|
|
goto out;
|
|
|
|
s->dump_count = 0;
|
|
|
|
err = "Failed to allocate AVFormat";
|
|
s->fmtctx = avformat_alloc_context();
|
|
if (!s->fmtctx)
|
|
goto out;
|
|
AUTO_CLEANUP(char *fn, free_gbuf)
|
|
= g_strdup_printf("stream-dump-%llu%s.mkv",
|
|
(long long unsigned) time(NULL),
|
|
suffix ?: "");
|
|
s->fmtctx->oformat = av_guess_format(NULL, fn, NULL);
|
|
err = "Output format unknown to ffmpeg";
|
|
if (!s->fmtctx->oformat)
|
|
goto out;
|
|
err = "Failed to add output audio stream";
|
|
s->avst = avformat_new_stream(s->fmtctx, avcodec_find_encoder(s->out_params.codec_id));
|
|
if (!s->avst)
|
|
goto out;
|
|
|
|
s->avst->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
|
|
s->avst->codecpar->codec_id = s->out_params.codec_id;
|
|
DEF_CH_LAYOUT(&s->avst->codecpar->ch_layout, s->out_params.channels);
|
|
s->avst->codecpar->sample_rate = s->out_params.clock_rate;
|
|
s->avst->time_base = (AVRational) {1, s->out_params.clock_rate}; // XXX ???
|
|
|
|
int ret = avio_open(&s->fmtctx->pb, fn, AVIO_FLAG_WRITE);
|
|
msg = g_strdup_printf("Failed to open output file '%s'", fn);
|
|
if (ret < 0)
|
|
goto out;
|
|
err = "Failed to write file header";
|
|
ret = avformat_write_header(s->fmtctx, NULL);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
msg = g_strdup_printf("Started dumping to file '%s'", fn);
|
|
err = NULL;
|
|
}
|
|
|
|
out:
|
|
if (err) {
|
|
g_free(msg);
|
|
return g_strdup(err);
|
|
}
|
|
|
|
return msg;
|
|
}
|
|
|
|
|
|
static void start_dump(void) {
|
|
char *msg1 = NULL, *msg2 = NULL;
|
|
const char *err = NULL;
|
|
uint idx;
|
|
|
|
{
|
|
LOCK(&streams_lock);
|
|
err = "No active streams to dump";
|
|
if (!streams->len)
|
|
goto out;
|
|
|
|
uint len = streams->len;
|
|
if (bidirectional) {
|
|
assert((len % 2) == 0); // must be an even number
|
|
len /= 2;
|
|
}
|
|
|
|
err = NULL;
|
|
|
|
idx = ssl_random() % len;
|
|
if (!bidirectional)
|
|
msg1 = start_dump_stream(streams->pdata[idx], NULL);
|
|
else {
|
|
msg1 = start_dump_stream(streams->pdata[idx], "-fwd");
|
|
msg2 = start_dump_stream(streams->pdata[idx + 1], "-rev");
|
|
}
|
|
}
|
|
|
|
out:
|
|
if (err)
|
|
show_popup("%s", err);
|
|
else {
|
|
assert(msg1 != NULL);
|
|
if (msg2)
|
|
show_popup("Stream %u: %s\nStream %u: %s", idx, msg1, idx + 1, msg2);
|
|
else
|
|
show_popup("Stream %u: %s", idx, msg1);
|
|
}
|
|
|
|
g_free(msg1);
|
|
g_free(msg2);
|
|
}
|
|
|
|
|
|
static void show_help(void) {
|
|
show_popup(
|
|
"ESC exit 1 CPU usage\n"
|
|
"[ ] -/+ 1 thread 2 realtime\n"
|
|
"{ } -/+ 10 threads c CPU stats\n"
|
|
"q w e +1/10/100 streams t other threads\n"
|
|
"a s d -1/10/100 streams o start dump\n"
|
|
" l show log"
|
|
);
|
|
}
|
|
|
|
|
|
static void show_log(void) {
|
|
int h, w;
|
|
|
|
{
|
|
LOCK(&curses_lock);
|
|
|
|
kill_popup();
|
|
|
|
int maxx, maxy;
|
|
getmaxyx(stdscr, maxy, maxx);
|
|
|
|
w = maxx - 4;
|
|
h = maxy - 4;
|
|
|
|
popup = newwin(h, w, (maxy - h) / 2, (maxx - w) / 2);
|
|
box(popup, 0, 0);
|
|
|
|
GQueue *log = get_log_lines(h - 2, 0);
|
|
char *line;
|
|
int y = 1;
|
|
while ((line = g_queue_pop_head(log))) {
|
|
mvwprintw(popup, y++, 2, "%.*s", w - 2, line);
|
|
g_free(line);
|
|
}
|
|
|
|
refresh_all();
|
|
}
|
|
|
|
getch();
|
|
|
|
{
|
|
LOCK(&curses_lock);
|
|
kill_popup();
|
|
refresh_all();
|
|
}
|
|
}
|
|
|
|
|
|
static void *do_input(void *p) {
|
|
while (true) {
|
|
thread_cancel_enable();
|
|
int ch = getch();
|
|
thread_cancel_disable();
|
|
|
|
switch (ch) {
|
|
case 27: // escape
|
|
return NULL;
|
|
|
|
case ']':
|
|
new_threads(1);
|
|
break;
|
|
|
|
case '[':
|
|
kill_threads(1);
|
|
break;
|
|
|
|
case '}':
|
|
new_threads(10);
|
|
break;
|
|
|
|
case '{':
|
|
kill_threads(10);
|
|
break;
|
|
|
|
case 'q':
|
|
new_streams(1);
|
|
break;
|
|
|
|
case 'w':
|
|
new_streams(10);
|
|
break;
|
|
|
|
case 'e':
|
|
new_streams(100);
|
|
break;
|
|
|
|
case 'a':
|
|
del_streams(1);
|
|
break;
|
|
|
|
case 's':
|
|
del_streams(10);
|
|
break;
|
|
|
|
case 'd':
|
|
del_streams(100);
|
|
break;
|
|
|
|
case '1':
|
|
output_fn = usage_bar;
|
|
break;
|
|
|
|
case '2':
|
|
output_fn = time_bar;
|
|
break;
|
|
|
|
case 'o':
|
|
start_dump();
|
|
break;
|
|
|
|
case 'c':
|
|
do_cpu_stats = !do_cpu_stats;
|
|
break;
|
|
|
|
case 't':
|
|
do_thread_stats = !do_thread_stats;
|
|
break;
|
|
|
|
case KEY_F(1):
|
|
case 'h':
|
|
case 'H':
|
|
case '?':
|
|
show_help();
|
|
break;
|
|
|
|
case 'l':
|
|
show_log();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
static void head_tail_bar(int x, int line, int *width, int head, int tail, int *col0, int *colt) {
|
|
*col0 = x + head;
|
|
*width -= tail;
|
|
*colt = x + *width;
|
|
*width -= head;
|
|
}
|
|
|
|
|
|
static void usage_bar(const struct stats *stats, int line, int x, int breadth, int width, int color,
|
|
const char *titlefmt, ...)
|
|
{
|
|
static const int head = 13;
|
|
static const int tail = 15;
|
|
|
|
int col0, colt;
|
|
head_tail_bar(x, line, &width, head, tail, &col0, &colt);
|
|
|
|
int uw = stats->iv ? stats->ucpu * width / stats->iv : 0;
|
|
int sw = stats->iv ? stats->scpu * width / stats->iv : 0;
|
|
int iw = MAX(width - uw - sw, 0);
|
|
|
|
int up = stats->iv ? stats->ucpu * 100 / stats->iv : 0;
|
|
int sp = stats->iv ? stats->scpu * 100 / stats->iv : 0;
|
|
int ip = MAX(100 - up - sp, 0);
|
|
|
|
va_list vp;
|
|
va_start(vp, titlefmt);
|
|
|
|
move(line, x);
|
|
vw_printw(stdscr, titlefmt, vp);
|
|
|
|
va_end(vp);
|
|
|
|
int extra_bits = 0;
|
|
if (stats->blocked)
|
|
extra_bits |= COLOR_PAIR(BLOCKED_COLOR);
|
|
else if (color)
|
|
extra_bits |= COLOR_PAIR(color);
|
|
|
|
for (int br = 0; br < breadth; br++) {
|
|
move(line + br, col0);
|
|
|
|
attron(A_BOLD | extra_bits);
|
|
for (int i = 0; i < uw; i++)
|
|
addstr("▓");
|
|
attroff(A_BOLD | extra_bits);
|
|
|
|
attron(A_DIM | extra_bits);
|
|
for (int i = 0; i < sw; i++)
|
|
addstr("░");
|
|
attroff(A_DIM | extra_bits);
|
|
|
|
attron(A_DIM | extra_bits);
|
|
for (int i = 0; i < iw; i++)
|
|
addstr("·");
|
|
attroff(A_DIM | extra_bits);
|
|
}
|
|
|
|
mvprintw(line, colt, "%3u%%/%3u%%/%3u%%", up, sp, ip);
|
|
}
|
|
|
|
static void time_bar(const struct stats *stats, int line, int x, int breadth, int width, int color,
|
|
const char *titlefmt, ...)
|
|
{
|
|
static const int head = 13;
|
|
static const int tail = 10;
|
|
|
|
int col0, colt;
|
|
head_tail_bar(x, line, &width, head, tail, &col0, &colt);
|
|
|
|
long long comput = us_ticks_scale(stats->comput);
|
|
|
|
int uw = stats->iv ? comput * width / stats->iv : 0;
|
|
int iw = MAX(width - uw, 0);
|
|
|
|
int up = stats->iv ? comput * 100 / stats->iv : 0;
|
|
int ip = MAX(100 - up, 0);
|
|
|
|
va_list vp;
|
|
va_start(vp, titlefmt);
|
|
|
|
move(line, x);
|
|
vw_printw(stdscr, titlefmt, vp);
|
|
|
|
va_end(vp);
|
|
|
|
int extra_bits = 0;
|
|
if (stats->blocked)
|
|
extra_bits |= COLOR_PAIR(BLOCKED_COLOR);
|
|
else if (color)
|
|
extra_bits |= COLOR_PAIR(color);
|
|
|
|
for (int br = 0; br < breadth; br++) {
|
|
move(line + br, col0);
|
|
|
|
attron(A_BOLD | extra_bits);
|
|
for (int i = 0; i < uw; i++)
|
|
addstr("▓");
|
|
attroff(A_BOLD | extra_bits);
|
|
|
|
attron(A_DIM | extra_bits);
|
|
for (int i = 0; i < iw; i++)
|
|
addstr("·");
|
|
attroff(A_DIM | extra_bits);
|
|
}
|
|
|
|
mvprintw(line, colt, "%3u%%/%3u%%", up, ip);
|
|
}
|
|
|
|
|
|
static void fclose_p(FILE **p) {
|
|
if (!*p)
|
|
return;
|
|
fclose(*p);
|
|
}
|
|
|
|
static void closedir_p(DIR **p) {
|
|
if (!*p)
|
|
return;
|
|
closedir(*p);
|
|
}
|
|
|
|
|
|
static bool thread_collect(pid_t pid, struct stats *outp, struct stats_sample *sample,
|
|
char comm_out[COMM_SIZE])
|
|
{
|
|
if (!pid)
|
|
return false;
|
|
|
|
AUTO_CLEANUP(char *fn, free_gbuf)
|
|
= g_strdup_printf("/proc/%i/task/%i/stat", (int) pid, (int) pid);
|
|
AUTO_CLEANUP(FILE *fp, fclose_p) = fopen(fn, "r");
|
|
if (!fp)
|
|
return false;
|
|
|
|
struct timeval now;
|
|
gettimeofday(&now, NULL);
|
|
|
|
long long utime, stime;
|
|
char comm[COMM_SIZE];
|
|
int rets = fscanf(fp, "%*d (%" COMM_SIZE_SCAN "[^)]) %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lld %lld ",
|
|
comm, &utime, &stime);
|
|
if (rets != 3)
|
|
return false;
|
|
|
|
if (sample->tv.tv_sec) {
|
|
outp->iv = us_ticks_scale(timeval_diff(&now, &sample->tv));
|
|
outp->ucpu = utime - sample->stats.ucpu;
|
|
outp->scpu = stime - sample->stats.scpu;
|
|
}
|
|
|
|
sample->tv = now;
|
|
sample->stats.ucpu = utime;
|
|
sample->stats.scpu = stime;
|
|
|
|
if (comm_out)
|
|
strcpy(comm_out, comm);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
// worker_lock is held
|
|
static void worker_collect(struct worker *w, struct stats *outp) {
|
|
if (!thread_collect(w->pid, outp, &w->sample, NULL))
|
|
return;
|
|
|
|
outp->blocked = w->blocked;
|
|
w->blocked = false;
|
|
|
|
LOCK(&w->comput_lock);
|
|
outp->comput = w->comput;
|
|
w->comput = 0;
|
|
}
|
|
|
|
|
|
static void bar_grid(uint num, int maxx, int *height, int *width) {
|
|
if (maxx < 104) {
|
|
*height = num;
|
|
*width = maxx;
|
|
return;
|
|
}
|
|
|
|
*height = (num + 1) / 2;
|
|
*width = maxx / 2 - 1;
|
|
}
|
|
|
|
static void grid_line(int *y, int *x, int starty, int height, int maxx) {
|
|
if (*y > height + starty - 1) {
|
|
*y = starty;
|
|
*x = (maxx + 1) / 2 + 1;
|
|
}
|
|
(*y)++;
|
|
}
|
|
|
|
|
|
static void workers_totals(const struct stats *stats, struct stats *totals) {
|
|
if (!totals)
|
|
return;
|
|
|
|
totals->iv += stats->iv;
|
|
totals->ucpu += stats->ucpu;
|
|
totals->scpu += stats->scpu;
|
|
totals->comput += stats->comput;
|
|
}
|
|
|
|
|
|
// worker_lock is held
|
|
static void worker_stats(struct worker *w, int idx, int starty, int height, int width,
|
|
int breadth,
|
|
int *y,
|
|
int *x,
|
|
int maxy,
|
|
int maxx,
|
|
struct stats *totals)
|
|
{
|
|
struct stats stats;
|
|
|
|
worker_collect(w, &stats);
|
|
workers_totals(&stats, totals);
|
|
|
|
grid_line(y, x, starty, height, maxx);
|
|
|
|
if (*y < maxy)
|
|
output_fn(&stats, *y, *x, breadth, width, 0, "Thread %2u:", idx);
|
|
}
|
|
|
|
|
|
static bool cpu_collect(GQueue *outp, struct stats *totals) {
|
|
AUTO_CLEANUP(FILE *fp, fclose_p) = fopen("/proc/stat", "r");
|
|
if (!fp)
|
|
return false;
|
|
|
|
while (!feof(fp)) {
|
|
struct timeval now;
|
|
gettimeofday(&now, NULL);
|
|
|
|
char cpu[7];
|
|
long long utime, nice, stime;
|
|
int rets = fscanf(fp, "%6s %lld %lld %lld ", cpu, &utime, &nice, &stime);
|
|
if (rets != 4)
|
|
continue;
|
|
utime += nice;
|
|
uint idx;
|
|
rets = sscanf(cpu, "cpu%u", &idx);
|
|
if (rets != 1)
|
|
continue;
|
|
if (idx >= num_cpus)
|
|
break;
|
|
|
|
struct stats stats = {0};
|
|
|
|
if (cpu_stats[idx].tv.tv_sec) {
|
|
stats.iv = us_ticks_scale(timeval_diff(&now, &cpu_stats[idx].tv));
|
|
stats.ucpu = utime - cpu_stats[idx].stats.ucpu;
|
|
stats.scpu = stime - cpu_stats[idx].stats.scpu;
|
|
}
|
|
|
|
cpu_stats[idx].tv = now;
|
|
cpu_stats[idx].stats.ucpu = utime;
|
|
cpu_stats[idx].stats.scpu = stime;
|
|
|
|
if (totals) {
|
|
totals->iv += stats.iv;
|
|
totals->ucpu += stats.ucpu;
|
|
totals->scpu += stats.scpu;
|
|
}
|
|
|
|
if (outp) {
|
|
struct stats *sp = g_slice_alloc(sizeof(*sp));
|
|
*sp = stats;
|
|
g_queue_push_tail(outp, sp);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
static void stats_queue_free(GQueue **q) {
|
|
if (!*q)
|
|
return;
|
|
struct stats *sp;
|
|
while ((sp = g_queue_pop_head(*q)))
|
|
g_slice_free1(sizeof(*sp), sp);
|
|
g_queue_free(*q);
|
|
}
|
|
|
|
|
|
static int cpu_collect_stats(const bool do_output, int starty, int maxy, int maxx, struct stats *totals) {
|
|
AUTO_CLEANUP(GQueue *stats, stats_queue_free) = do_output ? g_queue_new() : NULL;
|
|
|
|
if (!cpu_collect(stats, totals))
|
|
return starty;
|
|
|
|
if (!do_output)
|
|
return starty;
|
|
|
|
int height, width;
|
|
bar_grid(num_cpus, maxx, &height, &width);
|
|
|
|
int y = starty;
|
|
int x = 0;
|
|
|
|
uint idx = 0;
|
|
for (GList *l = stats->head; l; l = l->next) {
|
|
struct stats *sp = l->data;
|
|
grid_line(&y, &x, starty, height, maxx);
|
|
if (y < maxy)
|
|
usage_bar(sp, y, x, 1, width, CPU_COLOR, "CPU %2u:", idx);
|
|
idx++;
|
|
}
|
|
|
|
return starty + height;
|
|
}
|
|
|
|
|
|
static void other_thread_free(struct other_thread *thr) {
|
|
g_slice_free1(sizeof(*thr), thr);
|
|
}
|
|
|
|
static int pid_compare(const void *a, const void *b) {
|
|
pid_t A = GPOINTER_TO_INT(a);
|
|
pid_t B = GPOINTER_TO_INT(b);
|
|
if (A < B)
|
|
return -1;
|
|
if (B < A)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
static int other_threads_collect(const bool do_output, int starty, int maxy, int maxx,
|
|
struct stats *totals)
|
|
{
|
|
AUTO_CLEANUP(char *dn, free_gbuf) = g_strdup_printf("/proc/%u/task", getpid());
|
|
AUTO_CLEANUP(DIR *dp, closedir_p) = opendir(dn);
|
|
if (!dp)
|
|
return starty;
|
|
|
|
int y = starty;
|
|
|
|
LOCK(&other_threads_lock);
|
|
|
|
// track which threads we should delete
|
|
GHashTable *tracker = g_hash_table_new(g_direct_hash, g_direct_equal);
|
|
// and a sorted list for output
|
|
GTree *tree = g_tree_new(pid_compare);
|
|
|
|
GHashTableIter iter;
|
|
g_hash_table_iter_init(&iter, other_threads);
|
|
void *p;
|
|
while (g_hash_table_iter_next(&iter, &p, NULL))
|
|
g_hash_table_insert(tracker, p, NULL);
|
|
|
|
struct dirent *ent;
|
|
while ((ent = readdir(dp))) {
|
|
if (ent->d_name[0] == '.')
|
|
continue;
|
|
pid_t pid = strtol(ent->d_name, NULL, 10);
|
|
if (!pid)
|
|
continue;
|
|
|
|
// skip excluded threads (workers)
|
|
if (g_hash_table_contains(worker_threads, GINT_TO_POINTER(pid)))
|
|
continue;
|
|
|
|
// skip PIDs already seen. (shouldn't happen)
|
|
if (g_tree_lookup_extended(tree, GINT_TO_POINTER(pid), NULL, NULL))
|
|
continue;
|
|
|
|
// object already exists?
|
|
struct other_thread *thr = g_hash_table_lookup(other_threads, GINT_TO_POINTER(pid));
|
|
if (!thr) {
|
|
thr = g_slice_alloc0(sizeof(*thr));
|
|
g_hash_table_insert(other_threads, GINT_TO_POINTER(pid), thr);
|
|
}
|
|
|
|
// collect stats
|
|
if (!thread_collect(pid, &thr->stats, &thr->sample, thr->comm))
|
|
continue;
|
|
|
|
// track as active thread
|
|
g_hash_table_remove(tracker, GINT_TO_POINTER(pid));
|
|
g_tree_insert(tree, GINT_TO_POINTER(pid), thr);
|
|
}
|
|
|
|
// finally delete leftovers
|
|
g_hash_table_iter_init(&iter, tracker);
|
|
while (g_hash_table_iter_next(&iter, &p, NULL))
|
|
g_hash_table_remove(other_threads, p);
|
|
|
|
int height = 0;
|
|
|
|
if (do_output) {
|
|
// output based on sorted list
|
|
GQueue threads = G_QUEUE_INIT;
|
|
g_tree_get_values(&threads, tree);
|
|
|
|
int width;
|
|
bar_grid(threads.length, maxx, &height, &width);
|
|
|
|
int x = 0;
|
|
|
|
struct other_thread *thr;
|
|
while ((thr = g_queue_pop_head(&threads))) {
|
|
grid_line(&y, &x, starty, height, maxx);
|
|
if (y < maxy)
|
|
usage_bar(&thr->stats, y, x, 1, width, THREAD_COLOR, "%s:", thr->comm);
|
|
}
|
|
}
|
|
|
|
g_hash_table_destroy(tracker);
|
|
g_tree_destroy(tree);
|
|
|
|
return starty + height;
|
|
}
|
|
|
|
|
|
static void *do_stats(void *p) {
|
|
thread_cancel_disable();
|
|
|
|
while (true) {
|
|
{
|
|
// init
|
|
LOCK(&curses_lock);
|
|
|
|
erase();
|
|
|
|
int maxx, maxy;
|
|
getmaxyx(stdscr, maxy, maxx);
|
|
|
|
// top line summary
|
|
mvprintw(0, 0, "Threads: %u | ", workers.length);
|
|
|
|
{
|
|
LOCK(&streams_lock);
|
|
addstr("Streams: ");
|
|
if (streams->len == 0)
|
|
addstr("0");
|
|
else {
|
|
GHashTableIter iter;
|
|
g_hash_table_iter_init(&iter, stream_types);
|
|
char *t;
|
|
uint *n;
|
|
while (g_hash_table_iter_next(&iter, (gpointer *) &t, (gpointer *) &n))
|
|
printw("%s: %u | ", t, *n);
|
|
if (g_hash_table_size(stream_types) > 1) {
|
|
if (bidirectional)
|
|
printw("Total: %u streams (%u bidirectional calls)",
|
|
streams->len, streams->len / 2);
|
|
else
|
|
printw("Total: %u unidirectional streams",
|
|
streams->len);
|
|
}
|
|
else {
|
|
int y, x;
|
|
getyx(stdscr, y, x);
|
|
mvaddstr(y, x - 2, " ");
|
|
}
|
|
}
|
|
}
|
|
|
|
// CPU stats, collect and display if enabled
|
|
int line = 2;
|
|
|
|
int totals_line = line;
|
|
struct stats cpu_totals = {0};
|
|
|
|
line = cpu_collect_stats(do_cpu_stats, line, maxy, maxx, &cpu_totals);
|
|
|
|
if (do_cpu_stats) {
|
|
output_fn(&cpu_totals, totals_line, 0, 1, maxx, SUMMARY_COLOR, "CPUs:");
|
|
line += 2;
|
|
}
|
|
|
|
// other threads stats, collect and display if enabled
|
|
totals_line = line;
|
|
struct stats thread_totals = {0};
|
|
line = other_threads_collect(do_thread_stats, line, maxy, maxx, &thread_totals);
|
|
|
|
if (do_thread_stats) {
|
|
output_fn(&thread_totals, totals_line, 0, 1, maxx, SUMMARY_COLOR, "Threads:");
|
|
line += 2;
|
|
}
|
|
|
|
// worker thread stats
|
|
totals_line = line;
|
|
struct stats worker_totals = {0};
|
|
|
|
int breadth = 1;
|
|
int double_thresh = (maxy - line) / 3;
|
|
|
|
{
|
|
LOCK(&workers_lock);
|
|
uint idx = 0;
|
|
|
|
int height = maxy - line;
|
|
int width = maxx;
|
|
int inc = 0;
|
|
|
|
if (workers.length < double_thresh) {
|
|
inc = 2;
|
|
breadth = 2;
|
|
line += 2;
|
|
}
|
|
else if (workers.length < height)
|
|
inc = 0;
|
|
else
|
|
bar_grid(workers.length, maxx, &height, &width);
|
|
|
|
int y = line;
|
|
int x = 0;
|
|
|
|
for (GList *l = workers.head; l; l = l->next) {
|
|
struct worker *w = l->data;
|
|
worker_stats(w, idx, line, height, width, breadth, &y, &x, maxy, maxx,
|
|
&worker_totals);
|
|
idx++;
|
|
y += inc;
|
|
}
|
|
}
|
|
|
|
output_fn(&worker_totals, totals_line, 0, breadth, maxx, SUMMARY_COLOR, "Threads:");
|
|
|
|
refresh_all();
|
|
}
|
|
|
|
thread_cancel_enable();
|
|
usleep(500000);
|
|
thread_cancel_disable();
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static char *fixture_path_file(const char *base_fn) {
|
|
if (base_fn[0] == '/')
|
|
return g_strdup(base_fn);
|
|
return g_strdup_printf("%s/%s", FIXTURES_PATH, base_fn);
|
|
}
|
|
|
|
static void fixture_read_avio(GPtrArray *fixture, struct testparams *prm) {
|
|
AVFormatContext *fctx = NULL;
|
|
AUTO_CLEANUP(char *fn, free_gbuf) = fixture_path_file(prm->file);
|
|
int ret = avformat_open_input(&fctx, fn, NULL, NULL);
|
|
if (ret < 0)
|
|
die("Failed to open input fixture");
|
|
|
|
avformat_find_stream_info(fctx, NULL);
|
|
|
|
AVStream *avst = fctx->streams[0];
|
|
if (!avst)
|
|
die("No streams found in input fixture");
|
|
|
|
while (true) {
|
|
AVPacket *pkt = av_packet_alloc();
|
|
if (!pkt)
|
|
die("Failed to allocate AVPacket");
|
|
ret = av_read_frame(fctx, pkt);
|
|
if (ret < 0) {
|
|
if (ret == AVERROR_EOF)
|
|
break;
|
|
die("Read error while reading input fixture");
|
|
}
|
|
g_ptr_array_add(fixture, pkt);
|
|
}
|
|
|
|
avformat_close_input(&fctx);
|
|
}
|
|
|
|
|
|
static void fixture_read_raw(GPtrArray *fixture, struct testparams *prm) {
|
|
AUTO_CLEANUP(char *fn, free_gbuf) = fixture_path_file(prm->file);
|
|
FILE *fp = fopen(fn, "r");
|
|
if (!fp)
|
|
die("Failed to open input fixture");
|
|
|
|
while (true) {
|
|
AVPacket *pkt = av_packet_alloc();
|
|
if (!pkt)
|
|
die("Failed to allocate AVPacket");
|
|
void *buf = av_malloc(160); // XXX
|
|
if (!buf)
|
|
die("Out of memory");
|
|
size_t ret = fread(buf, 160, 1, fp); // XXX
|
|
if (ret != 1) {
|
|
if (feof(fp))
|
|
break;
|
|
die("Read error while reading input fixture");
|
|
}
|
|
pkt->duration = 160; // XXX
|
|
av_packet_from_data(pkt, buf, 160); // XXX
|
|
g_ptr_array_add(fixture, pkt);
|
|
}
|
|
|
|
fclose(fp);
|
|
}
|
|
|
|
|
|
static void load_fixture(struct testparams *prm) {
|
|
prm->fixture = g_ptr_array_new();
|
|
prm->read_fn(prm->fixture, prm);
|
|
}
|
|
|
|
|
|
static void free_fixture(GPtrArray *fixture) {
|
|
if (!fixture)
|
|
return;
|
|
for (uint idx = 0; idx < fixture->len; idx++)
|
|
av_packet_free((AVPacket **) &fixture->pdata[idx]);
|
|
g_ptr_array_free(fixture, TRUE);
|
|
}
|
|
|
|
|
|
static void options(int *argc, char ***argv) {
|
|
GOptionEntry e[] = {
|
|
{
|
|
.long_name = "source",
|
|
.short_name = 's',
|
|
.arg = G_OPTION_ARG_STRING,
|
|
.arg_data = &source_codec,
|
|
.description = "Source (input) codec",
|
|
.arg_description = "PCMA|opus",
|
|
},
|
|
{
|
|
.long_name = "dest",
|
|
.short_name = 'd',
|
|
.arg = G_OPTION_ARG_STRING,
|
|
.arg_data = &dest_codec,
|
|
.description = "Destination (output) codec",
|
|
.arg_description = "opus|PCMA",
|
|
},
|
|
{
|
|
.long_name = "threads",
|
|
.short_name = 't',
|
|
.arg = G_OPTION_ARG_INT,
|
|
.arg_data = &init_threads,
|
|
.description = "initial number of worker threads",
|
|
.arg_description = "INT",
|
|
},
|
|
{
|
|
.long_name = "bidirectional",
|
|
.short_name = 'b',
|
|
.arg = G_OPTION_ARG_NONE,
|
|
.arg_data = &bidirectional,
|
|
.description = "Create transcoding streams both ways",
|
|
},
|
|
{
|
|
.long_name = "max-cpu",
|
|
.short_name = 'm',
|
|
.arg = G_OPTION_ARG_INT,
|
|
.arg_data = &max_cpu,
|
|
.description = "Automated test up to x% CPU",
|
|
},
|
|
{
|
|
.long_name = "system-cpu",
|
|
.short_name = 's',
|
|
.arg = G_OPTION_ARG_NONE,
|
|
.arg_data = &system_cpu,
|
|
.description = "Consider system CPU usage for automated tests",
|
|
},
|
|
{ NULL, }
|
|
};
|
|
|
|
config_load(argc, argv, e, " - rtpengine performance tester",
|
|
"/etc/rtpengine/rtpengine-perftest.conf", "rtpengine-perftest", &rtpe_common_config);
|
|
|
|
if (init_threads <= 0)
|
|
init_threads = num_cpus;
|
|
if (init_threads <= 0)
|
|
init_threads = 1;
|
|
|
|
if (max_cpu > 100 || max_cpu < 0)
|
|
die("Invalid `max-cpu` number given");
|
|
}
|
|
|
|
|
|
static bool find_params(struct testparams *prm, const char *name) {
|
|
const struct testparams *p;
|
|
for (int i = 0; i < G_N_ELEMENTS(testparams); i++) {
|
|
p = &testparams[i];
|
|
if (strcmp(p->name, name))
|
|
continue;
|
|
*prm = *p;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
static void interactive(void) {
|
|
initscr();
|
|
start_color();
|
|
cbreak();
|
|
noecho();
|
|
keypad(stdscr, TRUE);
|
|
curs_set(0);
|
|
set_escdelay(25);
|
|
init_pair(BLOCKED_COLOR, COLOR_RED, COLOR_BLACK);
|
|
init_pair(SUMMARY_COLOR, COLOR_GREEN, COLOR_BLACK);
|
|
init_pair(CPU_COLOR, COLOR_BLUE, COLOR_BLACK);
|
|
init_pair(THREAD_COLOR, COLOR_CYAN, COLOR_BLACK);
|
|
|
|
refresh();
|
|
|
|
new_threads(init_threads);
|
|
|
|
pthread_t input_thread = thread_new("term input", do_input, NULL);
|
|
pthread_t stats_thread = thread_new("stats", do_stats, NULL);
|
|
|
|
// input handler is in control, wait for shutdown
|
|
pthread_join(input_thread, NULL);
|
|
|
|
pthread_cancel(stats_thread);
|
|
pthread_join(stats_thread, NULL);
|
|
}
|
|
|
|
|
|
static void delay_measure_workers(uint milliseconds, struct stats *totals) {
|
|
usleep(milliseconds * 1000);
|
|
|
|
LOCK(&workers_lock);
|
|
for (GList *l = workers.head; l; l = l->next) {
|
|
struct worker *w = l->data;
|
|
struct stats stats;
|
|
worker_collect(w, &stats);
|
|
workers_totals(&stats, totals);
|
|
}
|
|
}
|
|
|
|
|
|
static void max_cpu_test(void) {
|
|
int max_cpu_scaled = max_cpu * 100000;
|
|
|
|
new_threads(init_threads);
|
|
|
|
uint test_num = 1;
|
|
set_streams(test_num);
|
|
|
|
while (true) {
|
|
// initial break-in
|
|
delay_measure_workers(200, NULL);
|
|
cpu_collect(NULL, NULL);
|
|
|
|
// measure
|
|
struct stats totals = {0};
|
|
if (!system_cpu)
|
|
delay_measure_workers(500, &totals);
|
|
else {
|
|
delay_measure_workers(500, NULL);
|
|
cpu_collect(NULL, &totals);
|
|
}
|
|
|
|
// CPU% x 100000
|
|
int cpu = (totals.scpu + totals.ucpu) * 100 * 100000 / totals.iv;
|
|
|
|
// CPU load per stream (% x 100000)
|
|
int cps = cpu / test_num;
|
|
|
|
if (cps == 0) {
|
|
// not enough to have a useful value - double it
|
|
test_num *= 2;
|
|
}
|
|
else {
|
|
// predicted # streams for target CPU usage
|
|
int target = max_cpu_scaled / cps;
|
|
|
|
// how close are we to the target?
|
|
int acc = test_num * 100 / target;
|
|
|
|
if (acc >= 99 && acc <= 101) {
|
|
printf("%.1f%% CPU usage doing %s %s %s with %u %s on %u threads\n",
|
|
(float) cpu / 100000.0,
|
|
in_params.name,
|
|
bidirectional ? "<>" : "->",
|
|
out_params.name,
|
|
test_num,
|
|
bidirectional
|
|
? "bidirectional calls"
|
|
: "unidirectional streams",
|
|
workers.length);
|
|
break;
|
|
}
|
|
|
|
// scale to 50..100
|
|
int factor = 50 + acc / 2;
|
|
|
|
test_num = target * factor / 100;
|
|
}
|
|
|
|
set_streams(test_num);
|
|
}
|
|
}
|
|
|
|
|
|
int main(int argc, char **argv) {
|
|
setlocale(LC_ALL, "");
|
|
|
|
ticks_per_sec = sysconf(_SC_CLK_TCK);
|
|
num_cpus = num_cpu_cores(1);
|
|
cpu_stats = g_malloc0(sizeof(*cpu_stats) * num_cpus);
|
|
other_threads = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL,
|
|
(GDestroyNotify) other_thread_free);
|
|
worker_threads = g_hash_table_new(g_direct_hash, g_direct_equal);
|
|
|
|
options(&argc, &argv);
|
|
codeclib_init(0);
|
|
rtpe_ssl_init();
|
|
resources();
|
|
|
|
streams = g_ptr_array_new();
|
|
stream_types = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
|
|
|
rtpe_poller = poller_new();
|
|
if (!rtpe_poller)
|
|
die("Failed to create poller");
|
|
|
|
decoder_def = codec_find(&STR_INIT(source_codec), MT_AUDIO);
|
|
if (!decoder_def)
|
|
die("Codec definition for source codec not found");
|
|
encoder_def = codec_find(&STR_INIT(dest_codec), MT_AUDIO);
|
|
if (!encoder_def)
|
|
die("Codec definition for destination codec not found");
|
|
|
|
if (!find_params(&in_params, source_codec))
|
|
die("Definition for input fixture not found");
|
|
if (!find_params(&out_params, dest_codec))
|
|
die("Definition for output fixture not found");
|
|
|
|
load_fixture(&in_params);
|
|
if (bidirectional)
|
|
load_fixture(&out_params);
|
|
|
|
if (max_cpu)
|
|
max_cpu_test();
|
|
else
|
|
interactive();
|
|
|
|
kill_threads(workers.length);
|
|
del_streams_raw(streams->len);
|
|
g_ptr_array_free(streams, TRUE);
|
|
g_hash_table_destroy(stream_types);
|
|
g_hash_table_destroy(other_threads);
|
|
g_hash_table_destroy(worker_threads);
|
|
|
|
free_fixture(in_params.fixture);
|
|
free_fixture(out_params.fixture);
|
|
|
|
log_clear();
|
|
|
|
endwin();
|
|
|
|
return 0;
|
|
}
|