diff options
Diffstat (limited to 'src/tools/pw-top.c')
-rw-r--r-- | src/tools/pw-top.c | 862 |
1 files changed, 862 insertions, 0 deletions
diff --git a/src/tools/pw-top.c b/src/tools/pw-top.c new file mode 100644 index 0000000..a20bdf9 --- /dev/null +++ b/src/tools/pw-top.c @@ -0,0 +1,862 @@ +/* PipeWire + * + * Copyright © 2020 Wim Taymans + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice (including the next + * paragraph) shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include <stdio.h> +#include <signal.h> +#include <getopt.h> +#include <locale.h> +#include <ncurses.h> + +#include <spa/utils/result.h> +#include <spa/utils/string.h> +#include <spa/pod/parser.h> +#include <spa/debug/types.h> +#include <spa/param/format-utils.h> +#include <spa/param/audio/format-utils.h> +#include <spa/param/video/format-utils.h> + +#include <pipewire/impl.h> +#include <pipewire/extensions/profiler.h> + +#define MAX_FORMAT 16 +#define MAX_NAME 128 + +struct driver { + int64_t count; + float cpu_load[3]; + struct spa_io_clock clock; + uint32_t xrun_count; +}; + +struct measurement { + int32_t index; + int32_t status; + int64_t quantum; + int64_t prev_signal; + int64_t signal; + int64_t awake; + int64_t finish; + struct spa_fraction latency; +}; + +struct node { + struct spa_list link; + struct data *data; + uint32_t id; + char name[MAX_NAME+1]; + enum pw_node_state state; + struct measurement measurement; + struct driver info; + struct node *driver; + uint32_t errors; + int32_t last_error_status; + uint32_t generation; + char format[MAX_FORMAT+1]; + struct pw_proxy *proxy; + struct spa_hook proxy_listener; + unsigned int inactive:1; + struct spa_hook object_listener; +}; + +struct data { + struct pw_main_loop *loop; + struct pw_context *context; + + struct pw_core *core; + struct spa_hook core_listener; + + struct pw_registry *registry; + struct spa_hook registry_listener; + + struct pw_proxy *profiler; + struct spa_hook profiler_listener; + int check_profiler; + + struct spa_source *timer; + + int n_nodes; + struct spa_list node_list; + uint32_t generation; + unsigned pending_refresh:1; + + WINDOW *win; +}; + +struct point { + struct node *driver; + struct driver info; +}; + +static int process_info(struct data *d, const struct spa_pod *pod, struct driver *info) +{ + return spa_pod_parse_struct(pod, + SPA_POD_Long(&info->count), + SPA_POD_Float(&info->cpu_load[0]), + SPA_POD_Float(&info->cpu_load[1]), + SPA_POD_Float(&info->cpu_load[2]), + SPA_POD_Int(&info->xrun_count)); +} + +static int process_clock(struct data *d, const struct spa_pod *pod, struct driver *info) +{ + return spa_pod_parse_struct(pod, + SPA_POD_Int(&info->clock.flags), + SPA_POD_Int(&info->clock.id), + SPA_POD_Stringn(info->clock.name, sizeof(info->clock.name)), + SPA_POD_Long(&info->clock.nsec), + SPA_POD_Fraction(&info->clock.rate), + SPA_POD_Long(&info->clock.position), + SPA_POD_Long(&info->clock.duration), + SPA_POD_Long(&info->clock.delay), + SPA_POD_Double(&info->clock.rate_diff), + SPA_POD_Long(&info->clock.next_nsec)); +} + +static struct node *find_node(struct data *d, uint32_t id) +{ + struct node *n; + spa_list_for_each(n, &d->node_list, link) { + if (n->id == id) + return n; + } + return NULL; +} + +static void on_node_removed(void *data) +{ + struct node *n = data; + pw_proxy_destroy(n->proxy); +} + +static void on_node_destroy(void *data) +{ + struct node *n = data; + n->proxy = NULL; + spa_hook_remove(&n->proxy_listener); + spa_hook_remove(&n->object_listener); +} + +static const struct pw_proxy_events proxy_events = { + PW_VERSION_PROXY_EVENTS, + .removed = on_node_removed, + .destroy = on_node_destroy, +}; + +static void do_refresh(struct data *d); + +static void node_info(void *data, const struct pw_node_info *info) +{ + struct node *n = data; + + if (n->state != info->state) { + n->state = info->state; + do_refresh(n->data); + } +} + +static void node_param(void *data, int seq, + uint32_t id, uint32_t index, uint32_t next, + const struct spa_pod *param) +{ + struct node *n = data; + + if (param == NULL) { + spa_zero(n->format); + goto done; + } + + switch (id) { + case SPA_PARAM_Format: + { + uint32_t media_type, media_subtype; + + spa_format_parse(param, &media_type, &media_subtype); + + switch(media_type) { + case SPA_MEDIA_TYPE_audio: + switch(media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + { + struct spa_audio_info_raw info = { 0 }; + if (spa_format_audio_raw_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "%6.6s %d %d", + spa_debug_type_find_short_name( + spa_type_audio_format, info.format), + info.channels, info.rate); + } + break; + } + case SPA_MEDIA_SUBTYPE_dsd: + { + struct spa_audio_info_dsd info = { 0 }; + if (spa_format_audio_dsd_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "DSD%d %d ", + 8 * info.rate / 44100, info.channels); + + } + break; + } + case SPA_MEDIA_SUBTYPE_iec958: + { + struct spa_audio_info_iec958 info = { 0 }; + if (spa_format_audio_iec958_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "IEC958 %s %d", + spa_debug_type_find_short_name( + spa_type_audio_iec958_codec, info.codec), + info.rate); + + } + break; + } + } + break; + case SPA_MEDIA_TYPE_video: + switch(media_subtype) { + case SPA_MEDIA_SUBTYPE_raw: + { + struct spa_video_info_raw info = { 0 }; + if (spa_format_video_raw_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "%6.6s %dx%d", + spa_debug_type_find_short_name(spa_type_video_format, info.format), + info.size.width, info.size.height); + } + break; + } + case SPA_MEDIA_SUBTYPE_mjpg: + { + struct spa_video_info_mjpg info = { 0 }; + if (spa_format_video_mjpg_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "MJPG %dx%d", + info.size.width, info.size.height); + } + break; + } + case SPA_MEDIA_SUBTYPE_h264: + { + struct spa_video_info_h264 info = { 0 }; + if (spa_format_video_h264_parse(param, &info) >= 0) { + snprintf(n->format, sizeof(n->format), "H264 %dx%d", + info.size.width, info.size.height); + } + break; + } + } + break; + case SPA_MEDIA_TYPE_application: + switch(media_subtype) { + case SPA_MEDIA_SUBTYPE_control: + snprintf(n->format, sizeof(n->format), "%s", "CONTROL"); + break; + } + break; + } + break; + } + default: + break; + } +done: + do_refresh(n->data); +} + +static const struct pw_node_events node_events = { + PW_VERSION_NODE, + .info = node_info, + .param = node_param, +}; + +static struct node *add_node(struct data *d, uint32_t id, const char *name) +{ + struct node *n; + + if ((n = calloc(1, sizeof(*n))) == NULL) + return NULL; + + if (name) + strncpy(n->name, name, MAX_NAME); + else + snprintf(n->name, sizeof(n->name), "%u", id); + n->data = d; + n->id = id; + n->driver = n; + n->proxy = pw_registry_bind(d->registry, id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0); + if (n->proxy) { + uint32_t ids[1] = { SPA_PARAM_Format }; + + pw_proxy_add_listener(n->proxy, + &n->proxy_listener, &proxy_events, n); + pw_proxy_add_object_listener(n->proxy, + &n->object_listener, &node_events, n); + + pw_node_subscribe_params((struct pw_node*)n->proxy, + ids, 1); + } + spa_list_append(&d->node_list, &n->link); + d->n_nodes++; + d->pending_refresh = true; + + return n; +} + +static void remove_node(struct data *d, struct node *n) +{ + if (n->proxy) + pw_proxy_destroy(n->proxy); + spa_list_remove(&n->link); + d->n_nodes--; + d->pending_refresh = true; + free(n); +} + +static int process_driver_block(struct data *d, const struct spa_pod *pod, struct point *point) +{ + char *name = NULL; + uint32_t id = 0; + struct measurement m; + struct node *n; + int res; + + spa_zero(m); + if ((res = spa_pod_parse_struct(pod, + SPA_POD_Int(&id), + SPA_POD_String(&name), + SPA_POD_Long(&m.prev_signal), + SPA_POD_Long(&m.signal), + SPA_POD_Long(&m.awake), + SPA_POD_Long(&m.finish), + SPA_POD_Int(&m.status), + SPA_POD_Fraction(&m.latency))) < 0) + return res; + + if ((n = find_node(d, id)) == NULL) + return -ENOENT; + + n->driver = n; + n->measurement = m; + n->info = point->info; + point->driver = n; + n->generation = d->generation; + + if (m.status != 3) { + n->errors++; + if (n->last_error_status == -1) + n->last_error_status = m.status; + } + return 0; +} + +static int process_follower_block(struct data *d, const struct spa_pod *pod, struct point *point) +{ + uint32_t id = 0; + const char *name = NULL; + struct measurement m; + struct node *n; + int res; + + spa_zero(m); + if ((res = spa_pod_parse_struct(pod, + SPA_POD_Int(&id), + SPA_POD_String(&name), + SPA_POD_Long(&m.prev_signal), + SPA_POD_Long(&m.signal), + SPA_POD_Long(&m.awake), + SPA_POD_Long(&m.finish), + SPA_POD_Int(&m.status), + SPA_POD_Fraction(&m.latency))) < 0) + return res; + + if ((n = find_node(d, id)) == NULL) + return -ENOENT; + + n->measurement = m; + if (n->driver != point->driver) { + n->driver = point->driver; + d->pending_refresh = true; + } + n->generation = d->generation; + if (m.status != 3) { + n->errors++; + if (n->last_error_status == -1) + n->last_error_status = m.status; + } + return 0; +} + +static const char *print_time(char *buf, bool active, size_t len, uint64_t val) +{ + if (val == (uint64_t)-1 || !active) + snprintf(buf, len, " --- "); + else if (val == (uint64_t)-2) + snprintf(buf, len, " +++ "); + else if (val < 1000000llu) + snprintf(buf, len, "%5.1fus", val/1000.f); + else if (val < 1000000000llu) + snprintf(buf, len, "%5.1fms", val/1000000.f); + else + snprintf(buf, len, "%5.1fs", val/1000000000.f); + return buf; +} + +static const char *print_perc(char *buf, bool active, size_t len, uint64_t val, float quantum) +{ + if (val == (uint64_t)-1 || !active) { + snprintf(buf, len, " --- "); + } else if (val == (uint64_t)-2) { + snprintf(buf, len, " +++ "); + } else { + float frac = val / 1000000000.f; + snprintf(buf, len, "%5.2f", quantum == 0.0f ? 0.0f : frac/quantum); + } + return buf; +} + +static const char *state_as_string(enum pw_node_state state) +{ + switch (state) { + case PW_NODE_STATE_ERROR: + return "E"; + case PW_NODE_STATE_CREATING: + return "C"; + case PW_NODE_STATE_SUSPENDED: + return "S"; + case PW_NODE_STATE_IDLE: + return "I"; + case PW_NODE_STATE_RUNNING: + return "R"; + } + return "!"; +} + +static void print_node(struct data *d, struct driver *i, struct node *n, int y) +{ + char buf1[64]; + char buf2[64]; + char buf3[64]; + char buf4[64]; + uint64_t waiting, busy; + float quantum; + struct spa_fraction frac; + bool active; + + active = n->state == PW_NODE_STATE_RUNNING || n->state == PW_NODE_STATE_IDLE; + + if (!active) + frac = SPA_FRACTION(0, 0); + else if (n->driver == n) + frac = SPA_FRACTION((uint32_t)(i->clock.duration * i->clock.rate.num), i->clock.rate.denom); + else + frac = SPA_FRACTION(n->measurement.latency.num, n->measurement.latency.denom); + + if (i->clock.rate.denom) + quantum = (float)i->clock.duration * i->clock.rate.num / (float)i->clock.rate.denom; + else + quantum = 0.0; + + if (n->measurement.awake >= n->measurement.signal) + waiting = n->measurement.awake - n->measurement.signal; + else if (n->measurement.signal > n->measurement.prev_signal) + waiting = -2; + else + waiting = -1; + + if (n->measurement.finish >= n->measurement.awake) + busy = n->measurement.finish - n->measurement.awake; + else if (n->measurement.awake > n->measurement.prev_signal) + busy = -2; + else + busy = -1; + + mvwprintw(d->win, y, 0, "%s %4.1u %6.1u %6.1u %s %s %s %s %3.1u %16.16s %s%s", + state_as_string(n->state), + n->id, + frac.num, frac.denom, + print_time(buf1, active, 64, waiting), + print_time(buf2, active, 64, busy), + print_perc(buf3, active, 64, waiting, quantum), + print_perc(buf4, active, 64, busy, quantum), + i->xrun_count + n->errors, + active ? n->format : "", + n->driver == n ? "" : " + ", + n->name); +} + +static void clear_node(struct node *n) +{ + n->driver = n; + spa_zero(n->measurement); + spa_zero(n->info); + n->errors = 0; + n->last_error_status = 0; +} + +static void do_refresh(struct data *d) +{ + struct node *n, *t, *f; + int y = 1; + + wclear(d->win); + wattron(d->win, A_REVERSE); + wprintw(d->win, "%-*.*s", COLS, COLS, "S ID QUANT RATE WAIT BUSY W/Q B/Q ERR FORMAT NAME "); + wattroff(d->win, A_REVERSE); + wprintw(d->win, "\n"); + + spa_list_for_each_safe(n, t, &d->node_list, link) { + if (n->driver != n) + continue; + + print_node(d, &n->info, n, y++); + if(y > LINES) + break; + + spa_list_for_each(f, &d->node_list, link) { + if (d->generation > f->generation + 22) + clear_node(f); + + if (f->driver != n || f == n) + continue; + + print_node(d, &n->info, f, y++); + if(y > LINES) + break; + + } + } + + // Clear from last line to the end of the window to hide text wrapping from the last node + wmove(d->win, y, 0); + wclrtobot(d->win); + + wrefresh(d->win); + d->pending_refresh = false; +} + +static void do_timeout(void *data, uint64_t expirations) +{ + struct data *d = data; + d->generation++; + do_refresh(d); +} + +static void profiler_profile(void *data, const struct spa_pod *pod) +{ + struct data *d = data; + struct spa_pod *o; + struct spa_pod_prop *p; + struct point point; + + SPA_POD_STRUCT_FOREACH(pod, o) { + int res = 0; + if (!spa_pod_is_object_type(o, SPA_TYPE_OBJECT_Profiler)) + continue; + + spa_zero(point); + SPA_POD_OBJECT_FOREACH((struct spa_pod_object*)o, p) { + switch(p->key) { + case SPA_PROFILER_info: + res = process_info(d, &p->value, &point.info); + break; + case SPA_PROFILER_clock: + res = process_clock(d, &p->value, &point.info); + break; + case SPA_PROFILER_driverBlock: + res = process_driver_block(d, &p->value, &point); + break; + case SPA_PROFILER_followerBlock: + process_follower_block(d, &p->value, &point); + break; + default: + break; + } + if (res < 0) + break; + } + if (res < 0) + continue; + } + if (d->pending_refresh) + do_refresh(d); +} + +static const struct pw_profiler_events profiler_events = { + PW_VERSION_PROFILER_EVENTS, + .profile = profiler_profile, +}; + +static void registry_event_global(void *data, uint32_t id, + uint32_t permissions, const char *type, uint32_t version, + const struct spa_dict *props) +{ + struct data *d = data; + struct pw_proxy *proxy; + + if (spa_streq(type, PW_TYPE_INTERFACE_Node)) { + const char *str; + + if ((str = spa_dict_lookup(props, PW_KEY_NODE_NAME)) == NULL && + (str = spa_dict_lookup(props, PW_KEY_NODE_DESCRIPTION)) == NULL) { + str = spa_dict_lookup(props, PW_KEY_APP_NAME); + } + + if (add_node(d, id, str) == NULL) { + pw_log_warn("can add node %u: %m", id); + } + } else if (spa_streq(type, PW_TYPE_INTERFACE_Profiler)) { + if (d->profiler != NULL) { + printf("Ignoring profiler %d: already attached\n", id); + return; + } + + proxy = pw_registry_bind(d->registry, id, type, PW_VERSION_PROFILER, 0); + if (proxy == NULL) + goto error_proxy; + + d->profiler = proxy; + pw_proxy_add_object_listener(proxy, &d->profiler_listener, &profiler_events, d); + } + if (d->pending_refresh) + do_refresh(d); + return; + +error_proxy: + pw_log_error("failed to create proxy: %m"); + return; +} + +static void registry_event_global_remove(void *data, uint32_t id) +{ + struct data *d = data; + struct node *n; + if ((n = find_node(d, id)) != NULL) + remove_node(d, n); + if (d->pending_refresh) + do_refresh(d); +} + +static const struct pw_registry_events registry_events = { + PW_VERSION_REGISTRY_EVENTS, + .global = registry_event_global, + .global_remove = registry_event_global_remove, +}; + +static void on_core_error(void *_data, uint32_t id, int seq, int res, const char *message) +{ + struct data *data = _data; + + if (id == PW_ID_CORE) { + switch (res) { + case -EPIPE: + pw_main_loop_quit(data->loop); + break; + default: + pw_log_error("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + break; + } + } else { + pw_log_info("error id:%u seq:%d res:%d (%s): %s", + id, seq, res, spa_strerror(res), message); + } +} + +static void on_core_done(void *_data, uint32_t id, int seq) +{ + struct data *d = _data; + + if (seq == d->check_profiler) { + if (d->profiler == NULL) { + pw_log_error("no Profiler Interface found, please load one in the server"); + pw_main_loop_quit(d->loop); + } else { + do_refresh(d); + } + } +} + +static const struct pw_core_events core_events = { + PW_VERSION_CORE_EVENTS, + .error = on_core_error, + .done = on_core_done, +}; + +static void do_quit(void *data, int signal_number) +{ + struct data *d = data; + pw_main_loop_quit(d->loop); +} + +static void show_help(const char *name, bool error) +{ + fprintf(error ? stderr : stdout, "%s [options]\n" + " -h, --help Show this help\n" + " --version Show version\n" + " -r, --remote Remote daemon name\n", + name); +} + +static void terminal_start(void) +{ + initscr(); + cbreak(); + noecho(); + refresh(); +} + +static void terminal_stop(void) +{ + endwin(); +} + +static void do_handle_io(void *data, int fd, uint32_t mask) +{ + struct data *d = data; + + if (mask & SPA_IO_IN) { + int ch = getch(); + + switch(ch) { + case 'q': + pw_main_loop_quit(d->loop); + break; + default: + do_refresh(d); + break; + } + } +} + +int main(int argc, char *argv[]) +{ + struct data data = { 0 }; + struct pw_loop *l; + const char *opt_remote = NULL; + static const struct option long_options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "remote", required_argument, NULL, 'r' }, + { NULL, 0, NULL, 0} + }; + int c; + struct timespec value, interval; + struct node *n; + + setlocale(LC_ALL, ""); + pw_init(&argc, &argv); + + spa_list_init(&data.node_list); + + while ((c = getopt_long(argc, argv, "hVr:o:", long_options, NULL)) != -1) { + switch (c) { + case 'h': + show_help(argv[0], false); + return 0; + case 'V': + printf("%s\n" + "Compiled with libpipewire %s\n" + "Linked with libpipewire %s\n", + argv[0], + pw_get_headers_version(), + pw_get_library_version()); + return 0; + case 'r': + opt_remote = optarg; + break; + default: + show_help(argv[0], true); + return -1; + } + } + + data.loop = pw_main_loop_new(NULL); + if (data.loop == NULL) { + fprintf(stderr, "Can't create data loop: %m\n"); + return -1; + } + + l = pw_main_loop_get_loop(data.loop); + pw_loop_add_signal(l, SIGINT, do_quit, &data); + pw_loop_add_signal(l, SIGTERM, do_quit, &data); + + data.context = pw_context_new(l, NULL, 0); + if (data.context == NULL) { + fprintf(stderr, "Can't create context: %m\n"); + return -1; + } + + pw_context_load_module(data.context, PW_EXTENSION_MODULE_PROFILER, NULL, NULL); + + data.core = pw_context_connect(data.context, + pw_properties_new( + PW_KEY_REMOTE_NAME, opt_remote, + NULL), + 0); + if (data.core == NULL) { + fprintf(stderr, "Can't connect: %m\n"); + return -1; + } + + pw_core_add_listener(data.core, + &data.core_listener, + &core_events, &data); + data.registry = pw_core_get_registry(data.core, + PW_VERSION_REGISTRY, 0); + pw_registry_add_listener(data.registry, + &data.registry_listener, + ®istry_events, &data); + + data.check_profiler = pw_core_sync(data.core, 0, 0); + + terminal_start(); + + data.win = newwin(LINES, COLS, 0, 0); + + data.timer = pw_loop_add_timer(l, do_timeout, &data); + value.tv_sec = 1; + value.tv_nsec = 0; + interval.tv_sec = 1; + interval.tv_nsec = 0; + pw_loop_update_timer(l, data.timer, &value, &interval, false); + + pw_loop_add_io(l, fileno(stdin), SPA_IO_IN, false, do_handle_io, &data); + + pw_main_loop_run(data.loop); + + terminal_stop(); + + spa_list_consume(n, &data.node_list, link) + remove_node(&data, n); + + if (data.profiler) { + spa_hook_remove(&data.profiler_listener); + pw_proxy_destroy((struct pw_proxy*)data.profiler); + } + spa_hook_remove(&data.registry_listener); + pw_proxy_destroy((struct pw_proxy*)data.registry); + spa_hook_remove(&data.core_listener); + pw_context_destroy(data.context); + pw_main_loop_destroy(data.loop); + + pw_deinit(); + + return 0; +} |