From fde9f5868ceb4e6c63cedc3b070bbf41f81944c1 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Fri, 25 Oct 2024 10:49:15 -0700 Subject: [PATCH 1/6] libsysprof: add muxer GSource This allows copying events from a capture stream transparently into the destination. No processing of the stream is performed, but that may change in the future to accomidate JIT/Counter translations. Internal only as support for upcoming live unwinding via external process. --- src/libsysprof/meson.build | 1 + src/libsysprof/sysprof-muxer-source.c | 173 ++++++++++++++++++++++++++ src/libsysprof/sysprof-muxer-source.h | 33 +++++ 3 files changed, 207 insertions(+) create mode 100644 src/libsysprof/sysprof-muxer-source.c create mode 100644 src/libsysprof/sysprof-muxer-source.h diff --git a/src/libsysprof/meson.build b/src/libsysprof/meson.build index 549042de..f3fa0850 100644 --- a/src/libsysprof/meson.build +++ b/src/libsysprof/meson.build @@ -147,6 +147,7 @@ libsysprof_private_sources = [ 'sysprof-mount-device.c', 'sysprof-mount-namespace.c', 'sysprof-mount.c', + 'sysprof-muxer-source.c', 'sysprof-perf-event-stream.c', 'sysprof-podman.c', 'sysprof-process-info.c', diff --git a/src/libsysprof/sysprof-muxer-source.c b/src/libsysprof/sysprof-muxer-source.c new file mode 100644 index 00000000..c037a38f --- /dev/null +++ b/src/libsysprof/sysprof-muxer-source.c @@ -0,0 +1,173 @@ +/* + * sysprof-muxer-source.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include "sysprof-muxer-source.h" + +#define DEFAULT_BUFFER_SIZE (4096*16) + +typedef struct _SysprofMuxerSource +{ + GSource gsource; + int capture_fd; + SysprofCaptureWriter *writer; + struct { + guint8 *allocation; + guint8 *begin; + guint8 *end; + guint8 *capacity; + gsize to_skip; + } buffer; +} SysprofMuxerSource; + +static gboolean +sysprof_muxer_source_size (SysprofMuxerSource *source) +{ + return source->buffer.end - source->buffer.begin; +} + +static gboolean +sysprof_muxer_source_dispatch (GSource *gsource, + GSourceFunc callback, + gpointer user_data) +{ + SysprofMuxerSource *source = (SysprofMuxerSource *)gsource; + gssize n_read; + + g_assert (source != NULL); + g_assert (source->writer != NULL); + + /* Try to read the next chunk */ + n_read = read (source->capture_fd, source->buffer.end, source->buffer.capacity - source->buffer.end); + + if (n_read > 0) + { + const SysprofCaptureFrame *frame; + + /* Advance tail to what was filled */ + source->buffer.end += n_read; + + /* Get to next alignment */ + if (source->buffer.to_skip) + { + gsize amount = MIN (source->buffer.to_skip, source->buffer.end - source->buffer.begin); + source->buffer.begin += amount; + source->buffer.to_skip -= amount; + } + + /* If there is enough to read the frame header, try to read and dispatch + * it in raw form. We assume we're the same endianness here because this + * is coming from the same host (live-unwinder currently). + */ + while (sysprof_muxer_source_size (source) >= sizeof *frame) + { + frame = (const SysprofCaptureFrame *)source->buffer.begin; + + if (frame->len <= sysprof_muxer_source_size (source)) + { + source->buffer.begin += frame->len; + + if (frame->len % sizeof (guint64) != 0) + source->buffer.to_skip = sizeof (guint64) - (frame->len % sizeof (guint64)); + + /* TODO: Technically for counters/JIT map we need to translate them. */ + + _sysprof_capture_writer_add_raw (source->writer, frame); + } + + if (source->buffer.to_skip > 0 && + source->buffer.to_skip <= sysprof_muxer_source_size (source)) + { + source->buffer.begin += source->buffer.to_skip; + source->buffer.to_skip = 0; + continue; + } + + break; + } + + /* Move anything left to the head of the buffer so we can + * fill in the entire next frame of data. + */ + if (source->buffer.begin < source->buffer.end) + { + /* TODO: Should we adjust for alignment here? */ + + memmove (source->buffer.allocation, + source->buffer.begin, + source->buffer.end - source->buffer.begin); + source->buffer.end = source->buffer.allocation + (source->buffer.end - source->buffer.begin); + source->buffer.begin = source->buffer.allocation; + } + else + { + source->buffer.end = source->buffer.allocation; + source->buffer.begin = source->buffer.allocation; + } + } + + + return G_SOURCE_CONTINUE; +} + +static void +sysprof_muxer_source_finalize (GSource *gsource) +{ + SysprofMuxerSource *source = (SysprofMuxerSource *)gsource; + + g_clear_fd (&source->capture_fd, NULL); + g_clear_pointer (&source->writer, sysprof_capture_writer_unref); + g_clear_pointer (&source->buffer.begin, g_free); +} + +static const GSourceFuncs source_funcs = { + .dispatch = sysprof_muxer_source_dispatch, + .finalize = sysprof_muxer_source_finalize, +}; + +GSource * +sysprof_muxer_source_new (int capture_fd, + SysprofCaptureWriter *writer) +{ + SysprofMuxerSource *source; + + g_return_val_if_fail (capture_fd > -1, NULL); + g_return_val_if_fail (writer != NULL, NULL); + + source = (SysprofMuxerSource *)g_source_new ((GSourceFuncs *)&source_funcs, sizeof (SysprofMuxerSource)); + source->capture_fd = capture_fd; + source->writer = sysprof_capture_writer_ref (writer); + source->buffer.allocation = g_malloc (DEFAULT_BUFFER_SIZE); + source->buffer.begin = source->buffer.allocation; + source->buffer.end = source->buffer.allocation; + source->buffer.capacity = source->buffer.allocation + DEFAULT_BUFFER_SIZE; + source->buffer.to_skip = sizeof (SysprofCaptureFileHeader); + + g_unix_set_fd_nonblocking (capture_fd, TRUE, NULL); + + g_source_add_unix_fd ((GSource *)source, capture_fd, G_IO_IN); + + return (GSource *)source; +} diff --git a/src/libsysprof/sysprof-muxer-source.h b/src/libsysprof/sysprof-muxer-source.h new file mode 100644 index 00000000..9b9a94e1 --- /dev/null +++ b/src/libsysprof/sysprof-muxer-source.h @@ -0,0 +1,33 @@ +/* + * sysprof-muxer-source.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include + +G_BEGIN_DECLS + +GSource *sysprof_muxer_source_new (int capture_fd, + SysprofCaptureWriter *writer); + +G_END_DECLS From 39b96f47f57070155d05fa3a6eb1820df17980af Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:39:23 -0800 Subject: [PATCH 2/6] libsysprof: add support for stack/regs options in attr This requires a coordinating sysprofd that knows how to handle reading the new attributes. Setting these fields will allow snapshotting the contents of the stack and registers to do offline unwinding. Also make the conversion to GVariant available outside the module so that we can consume it for live unwinding. --- .../sysprof-perf-event-stream-private.h | 25 ++++++++++--------- src/libsysprof/sysprof-perf-event-stream.c | 10 +++++--- src/sysprofd/helpers.c | 16 ++++++++++++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/libsysprof/sysprof-perf-event-stream-private.h b/src/libsysprof/sysprof-perf-event-stream-private.h index 4ac0fd1e..7e3d9deb 100644 --- a/src/libsysprof/sysprof-perf-event-stream-private.h +++ b/src/libsysprof/sysprof-perf-event-stream-private.h @@ -165,17 +165,18 @@ typedef void (*SysprofPerfEventCallback) (const SysprofPerfEvent *event, G_DECLARE_FINAL_TYPE (SysprofPerfEventStream, sysprof_perf_event_stream, SYSPROF, PERF_EVENT_STREAM, GObject) -DexFuture *sysprof_perf_event_stream_new (GDBusConnection *connection, - struct perf_event_attr *attr, - int cpu, - int group_fd, - guint64 flags, - SysprofPerfEventCallback callback, - gpointer callback_data, - GDestroyNotify callback_data_destroy); -gboolean sysprof_perf_event_stream_enable (SysprofPerfEventStream *self, - GError **error); -gboolean sysprof_perf_event_stream_disable (SysprofPerfEventStream *self, - GError **error); +DexFuture *sysprof_perf_event_stream_new (GDBusConnection *connection, + struct perf_event_attr *attr, + int cpu, + int group_fd, + guint64 flags, + SysprofPerfEventCallback callback, + gpointer callback_data, + GDestroyNotify callback_data_destroy); +gboolean sysprof_perf_event_stream_enable (SysprofPerfEventStream *self, + GError **error); +gboolean sysprof_perf_event_stream_disable (SysprofPerfEventStream *self, + GError **error); +GVariant *_sysprof_perf_event_attr_to_variant (const struct perf_event_attr *attr); G_END_DECLS diff --git a/src/libsysprof/sysprof-perf-event-stream.c b/src/libsysprof/sysprof-perf-event-stream.c index a7bf8d88..c40182ad 100644 --- a/src/libsysprof/sysprof-perf-event-stream.c +++ b/src/libsysprof/sysprof-perf-event-stream.c @@ -109,8 +109,8 @@ G_DEFINE_FINAL_TYPE (SysprofPerfEventStream, sysprof_perf_event_stream, G_TYPE_O static GParamSpec *properties [N_PROPS]; -static GVariant * -build_options_dict (const struct perf_event_attr *attr) +GVariant * +_sysprof_perf_event_attr_to_variant (const struct perf_event_attr *attr) { return g_variant_take_ref ( g_variant_new_parsed ("[" @@ -130,6 +130,8 @@ build_options_dict (const struct perf_event_attr *attr) "{'sample_period', <%t>}," "{'sample_type', <%t>}," "{'task', <%b>}," + "{'sample_stack_user', <%u>}," + "{'sample_regs_user', <%t>}," "{'type', <%u>}" "]", (gboolean)!!attr->comm, @@ -148,6 +150,8 @@ build_options_dict (const struct perf_event_attr *attr) (guint64)attr->sample_period, (guint64)attr->sample_type, (gboolean)!!attr->task, + (guint32)attr->sample_stack_user, + (guint64)attr->sample_regs_user, (guint32)attr->type)); } @@ -513,7 +517,7 @@ sysprof_perf_event_stream_new (GDBusConnection *connection, group_fd_handle = g_unix_fd_list_append (fd_list, group_fd, NULL); } - options = build_options_dict (attr); + options = _sysprof_perf_event_attr_to_variant (attr); g_dbus_connection_call_with_unix_fd_list (connection, "org.gnome.Sysprof3", diff --git a/src/sysprofd/helpers.c b/src/sysprofd/helpers.c index 2aebc417..7e5df34a 100644 --- a/src/sysprofd/helpers.c +++ b/src/sysprofd/helpers.c @@ -127,6 +127,8 @@ helpers_perf_event_open (GVariant *options, guint64 sample_period = 0; guint64 sample_type = 0; guint64 config = 0; + guint64 sample_regs_user = 0; + guint sample_stack_user = 0; int clockid = CLOCK_MONOTONIC; int comm = 0; int mmap_ = 0; @@ -236,6 +238,18 @@ helpers_perf_event_open (GVariant *options, goto bad_arg; use_clockid = g_variant_get_boolean (value); } + else if (strcmp (key, "sample_stack_user") == 0) + { + if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32)) + goto bad_arg; + sample_stack_user = g_variant_get_uint32 (value); + } + else if (strcmp (key, "sample_regs_user") == 0) + { + if (!g_variant_is_of_type (value, G_VARIANT_TYPE_UINT64)) + goto bad_arg; + sample_regs_user = g_variant_get_uint64 (value); + } continue; @@ -257,6 +271,8 @@ helpers_perf_event_open (GVariant *options, attr.task = !!task; attr.type = type; attr.wakeup_events = wakeup_events; + attr.sample_regs_user = sample_regs_user; + attr.sample_stack_user = sample_stack_user; #ifdef HAVE_PERF_CLOCKID if (!use_clockid || clockid < 0) From 1bd79af439eb4eb2ffbac7414b2a26a4bfa11ad3 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:41:44 -0800 Subject: [PATCH 3/6] sysprofd: add support for unwinding without frame pointers This provides a new sysprof-live-unwinder subprocess that runs as root to allow accessing all processes on the system via /proc/$pid/. It is spawned by sysprofd with various perf event FDs and a FD to write captures to. Ideally the capture_fd is something that will naturally error if the client application crashes (such as a socketpair() having the peer close). This is not enforced but encouraged. Additionally, an event_fd is used to allow the client application to signal the live-unwinder to exit. Unwinding is performed by looking at the modules loaded into the target pid and using libdwfl to access DWARF/CFI/etc state machinery. Stack data does not touch the disk as it exists in a mmap buffer from perf and is then translated into a callchain and sent to the Sysprof client. Unwinding occurs as normal post-mortem though is improved through the use of debuginfod to locate the appropriate symbols. --- meson.build | 3 +- src/meson.build | 3 + src/sysprof-live-unwinder/main.c | 752 ++++++++++++++++++ src/sysprof-live-unwinder/meson.build | 19 + .../sysprof-live-process.c | 505 ++++++++++++ .../sysprof-live-process.h | 50 ++ .../sysprof-live-unwinder.c | 427 ++++++++++ .../sysprof-live-unwinder.h | 79 ++ src/sysprof-live-unwinder/tests/meson.build | 35 + .../tests/test-live-unwinder.c | 391 +++++++++ src/sysprofd/ipc-unwinder-impl.c | 247 ++++++ src/sysprofd/ipc-unwinder-impl.h | 34 + src/sysprofd/meson.build | 10 +- src/sysprofd/org.gnome.Sysprof3.Unwinder.xml | 23 + src/sysprofd/sysprofd.c | 6 +- 15 files changed, 2580 insertions(+), 4 deletions(-) create mode 100644 src/sysprof-live-unwinder/main.c create mode 100644 src/sysprof-live-unwinder/meson.build create mode 100644 src/sysprof-live-unwinder/sysprof-live-process.c create mode 100644 src/sysprof-live-unwinder/sysprof-live-process.h create mode 100644 src/sysprof-live-unwinder/sysprof-live-unwinder.c create mode 100644 src/sysprof-live-unwinder/sysprof-live-unwinder.h create mode 100644 src/sysprof-live-unwinder/tests/meson.build create mode 100644 src/sysprof-live-unwinder/tests/test-live-unwinder.c create mode 100644 src/sysprofd/ipc-unwinder-impl.c create mode 100644 src/sysprofd/ipc-unwinder-impl.h create mode 100644 src/sysprofd/org.gnome.Sysprof3.Unwinder.xml diff --git a/meson.build b/meson.build index c6a67f68..a9265c0e 100644 --- a/meson.build +++ b/meson.build @@ -39,12 +39,13 @@ need_glib = (need_gtk or get_option('tools') or get_option('tests')) need_libsysprof = (need_gtk or + get_option('sysprofd') == 'bundled' or get_option('libsysprof') or get_option('examples') or get_option('tools') or get_option('tests')) -dex_req = '0.6' +dex_req = '0.9' glib_req = '2.76.0' gtk_req = '4.15' polkit_req = '0.105' diff --git a/src/meson.build b/src/meson.build index e221636a..f0e39890 100644 --- a/src/meson.build +++ b/src/meson.build @@ -11,6 +11,8 @@ sysprof_version_conf.set('MINOR_VERSION', sysprof_version[1]) sysprof_version_conf.set('MICRO_VERSION', 0) sysprof_version_conf.set('VERSION', meson.project_version()) +pkglibexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) + subdir('libsysprof-capture') if need_libsysprof @@ -20,6 +22,7 @@ endif if get_option('sysprofd') == 'bundled' subdir('sysprofd') + subdir('sysprof-live-unwinder') endif if get_option('gtk') diff --git a/src/sysprof-live-unwinder/main.c b/src/sysprof-live-unwinder/main.c new file mode 100644 index 00000000..9e2733ae --- /dev/null +++ b/src/sysprof-live-unwinder/main.c @@ -0,0 +1,752 @@ +/* + * main.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include + +#include +#include + +#include +#include + +#include + +#include "sysprof-live-unwinder.h" +#include "sysprof-perf-event-stream-private.h" + +#define CAPTURE_BUFFER_SIZE (4096*16) +#define N_PAGES 32 + +#define DUMP_BYTES(_n, _b, _l) \ + G_STMT_START { \ + GString *str, *astr; \ + gsize _i; \ + g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + " %s = %p [%d]", #_n, _b, (gint)_l); \ + str = g_string_sized_new (80); \ + astr = g_string_sized_new (16); \ + for (_i = 0; _i < _l; _i++) \ + { \ + if ((_i % 16) == 0) \ + g_string_append_printf (str, "%06x: ", (guint)_i); \ + g_string_append_printf (str, " %02x", _b[_i]); \ + \ + if (g_ascii_isprint(_b[_i])) \ + g_string_append_printf (astr, " %c", _b[_i]); \ + else \ + g_string_append (astr, " ."); \ + \ + if ((_i % 16) == 15) \ + { \ + g_log (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + "%s %s", str->str, astr->str); \ + str->str[0] = str->len = 0; \ + astr->str[0] = astr->len = 0; \ + } \ + else if ((_i % 16) == 7) \ + { \ + g_string_append (str, " "); \ + g_string_append (astr, " "); \ + } \ + } \ + \ + if (_i != 16) \ + g_log (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG, \ + "%-56s %s", str->str, astr->str); \ + \ + g_string_free (str, TRUE); \ + g_string_free (astr, TRUE); \ + } G_STMT_END + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofCaptureWriter, sysprof_capture_writer_unref) + +typedef struct _PerfSource +{ + GSource gsource; + + SysprofLiveUnwinder *unwinder; + SysprofCaptureWriter *writer; + + guint64 map_size; + struct perf_event_mmap_page *map; + guint8 *map_data; + const guint8 *map_data_end; + guint64 map_data_size; + guint64 tail; + + guint8 *buffer; + + int cpu; + GPid self_pid; + + guint stack_size; +} PerfSource; + +typedef struct _PerfFDArg +{ + int fd; + int cpu; +} PerfFDArg; + +SYSPROF_ALIGNED_BEGIN(8); +typedef struct _StackRegs +{ + guint64 abi; + guint64 registers[0]; +} StackRegs +SYSPROF_ALIGNED_END(8); + +SYSPROF_ALIGNED_BEGIN(8); +typedef struct _StackUser +{ + guint64 size; + guint8 data[0]; +} StackUser +SYSPROF_ALIGNED_END(8); + +static GArray *all_perf_fds; + +static inline void +realign (gsize *pos, + gsize align) +{ + *pos = (*pos + align - 1) & ~(align - 1); +} + +static void +handle_event (PerfSource *source, + const SysprofPerfEvent *event) +{ + gsize offset; + gint64 time; + + g_assert (source != NULL); + g_assert (event != NULL); + + switch (event->header.type) + { + case PERF_RECORD_COMM: + offset = strlen (event->comm.comm) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->comm.comm + offset, sizeof time); + + if (event->comm.pid == event->comm.tid) + { + sysprof_live_unwinder_seen_process (source->unwinder, + time, + source->cpu, + event->comm.pid, + event->comm.comm); + } + + break; + + case PERF_RECORD_EXIT: + /* Ignore fork exits for now */ + if (event->exit.tid != event->exit.pid) + break; + + sysprof_live_unwinder_process_exited (source->unwinder, + event->exit.time, + source->cpu, + event->exit.pid); + + break; + + case PERF_RECORD_FORK: + sysprof_live_unwinder_process_forked (source->unwinder, + event->fork.time, + source->cpu, + event->fork.ptid, + event->fork.tid); + break; + + case PERF_RECORD_LOST: + { + char message[64]; + g_snprintf (message, sizeof message, + "Lost %"G_GUINT64_FORMAT" samples", + event->lost.lost); + sysprof_capture_writer_add_log (source->writer, + SYSPROF_CAPTURE_CURRENT_TIME, + source->cpu, + -1, + G_LOG_LEVEL_CRITICAL, "Sampler", message); + break; + } + + case PERF_RECORD_MMAP: + offset = strlen (event->mmap.filename) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->mmap.filename + offset, sizeof time); + + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap.pid, + event->mmap.addr, + event->mmap.addr + event->mmap.len, + event->mmap.pgoff, + 0, + event->mmap.filename, + NULL); + + break; + + case PERF_RECORD_MMAP2: + offset = strlen (event->mmap2.filename) + 1; + realign (&offset, sizeof (guint64)); + offset += sizeof (GPid) + sizeof (GPid); + memcpy (&time, event->mmap2.filename + offset, sizeof time); + + if ((event->header.misc & PERF_RECORD_MISC_MMAP_BUILD_ID) != 0) + { + char build_id[G_N_ELEMENTS (event->mmap2.build_id) * 2 + 1]; + guint len = MIN (G_N_ELEMENTS (event->mmap2.build_id), event->mmap2.build_id_size); + + for (guint i = 0; i < len; i++) + g_snprintf (&build_id[len*2], 3, "%02x", event->mmap2.build_id[i]); + build_id[len*2] = 0; + + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap2.pid, + event->mmap2.addr, + event->mmap2.addr + event->mmap2.len, + event->mmap2.pgoff, + event->mmap2.ino, + event->mmap2.filename, + build_id); + } + else + { + sysprof_live_unwinder_track_mmap (source->unwinder, + time, + source->cpu, + event->mmap2.pid, + event->mmap2.addr, + event->mmap2.addr + event->mmap2.len, + event->mmap2.pgoff, + event->mmap2.ino, + event->mmap2.filename, + NULL); + } + break; + + case PERF_RECORD_READ: + break; + + case PERF_RECORD_SAMPLE: + { + const guint8 *endptr = (const guint8 *)event + event->header.size; + const guint64 *ips = event->callchain.ips; + int n_ips = event->callchain.n_ips; + guint64 trace[3]; + + /* We always expect PERF_RECORD_SAMPLE to contain a callchain because + * we need that even if we sample the stack for user-space unwinding. + * Otherwise we lose the blended stack trace. + */ + if (n_ips == 0) + { + if (event->callchain.header.misc & PERF_RECORD_MISC_KERNEL) + { + trace[0] = PERF_CONTEXT_KERNEL; + trace[1] = event->callchain.ip; + trace[2] = PERF_CONTEXT_USER; + + ips = trace; + n_ips = 3; + } + else + { + trace[0] = PERF_CONTEXT_USER; + trace[1] = event->callchain.ip; + + ips = trace; + n_ips = 2; + } + } + + if (source->stack_size && source->stack_size < event->header.size) + { + guint64 dyn_size = *((const guint64 *)endptr - 1); + const StackUser *stack_user = (const StackUser *)(endptr - sizeof (guint64) - source->stack_size - sizeof (StackUser)); + const StackRegs *stack_regs = (const StackRegs *)&event->callchain.ips[event->callchain.n_ips]; + guint n_regs = ((const guint8 *)stack_user - (const guint8 *)stack_regs->registers) / sizeof (guint64); + +#if 0 + g_print ("n_ips=%u stack_size=%ld dyn_size=%ld abi=%ld n_regs=%d\n", + n_ips, stack_user->size, dyn_size, stack_regs->abi, n_regs); +#endif + + sysprof_live_unwinder_process_sampled_with_stack (source->unwinder, + event->callchain.time, + source->cpu, + event->callchain.pid, + event->callchain.tid, + ips, + n_ips, + stack_user->data, + stack_user->size, + dyn_size, + stack_regs->abi, + stack_regs->registers, + n_regs); + + break; + } + + sysprof_live_unwinder_process_sampled (source->unwinder, + event->callchain.time, + source->cpu, + event->callchain.pid, + event->callchain.tid, + ips, + n_ips); + + break; + } + + case PERF_RECORD_THROTTLE: + case PERF_RECORD_UNTHROTTLE: + default: + break; + } +} + +static gboolean +perf_source_prepare (GSource *gsource, + int *timeout) +{ + *timeout = 50; + return FALSE; +} + +static gboolean +perf_source_check (GSource *gsource) +{ + PerfSource *source = (PerfSource *)gsource; + guint64 head; + guint64 tail; + + atomic_thread_fence (memory_order_acquire); + + tail = source->tail; + head = source->map->data_head; + + if (head < tail) + tail = head; + + return (head - tail) >= sizeof (struct perf_event_header); +} + +static gboolean +perf_source_dispatch (GSource *gsource, + GSourceFunc callback, + gpointer user_data) +{ + PerfSource *source = (PerfSource *)gsource; + guint64 n_bytes = source->map_data_size; + guint64 mask = n_bytes - 1; + guint64 head; + guint64 tail; + guint us = 0; + guint them = 0; + + g_assert (source != NULL); + + tail = source->tail; + head = source->map->data_head; + + atomic_thread_fence (memory_order_acquire); + + if (head < tail) + tail = head; + + while ((head - tail) >= sizeof (struct perf_event_header)) + { + const SysprofPerfEvent *event; + struct perf_event_header *header; + gboolean is_self = FALSE; + + /* Note that: + * + * - perf events are a multiple of 64 bits + * - the perf event header is 64 bits + * - the data area is a multiple of 64 bits + * + * which means there will always be space for one header, which means we + * can safely dereference the size field. + */ + header = (struct perf_event_header *)(gpointer)(source->map_data + (tail & mask)); + + if (header->size > head - tail) + { + /* The kernel did not generate a complete event. + * I don't think that can happen, but we may as well + * be paranoid. + */ + g_warn_if_reached (); + break; + } + + if (source->map_data + (tail & mask) + header->size > source->map_data_end) + { + guint8 *b = source->buffer; + gint n_before; + gint n_after; + + n_after = (tail & mask) + header->size - n_bytes; + n_before = header->size - n_after; + + memcpy (b, source->map_data + (tail & mask), n_before); + memcpy (b + n_before, source->map_data, n_after); + + header = (struct perf_event_header *)(gpointer)b; + } + + event = (SysprofPerfEvent *)header; + + switch (event->header.type) + { + default: + case PERF_RECORD_COMM: + case PERF_RECORD_EXIT: + case PERF_RECORD_FORK: + case PERF_RECORD_MMAP: + case PERF_RECORD_MMAP2: + break; + + case PERF_RECORD_SAMPLE: + is_self = event->callchain.pid == source->self_pid; + break; + + case PERF_RECORD_READ: + case PERF_RECORD_THROTTLE: + case PERF_RECORD_UNTHROTTLE: + goto skip_callback; + + case PERF_RECORD_LOST: + break; + } + + handle_event (source, event); + + us += is_self; + them += !is_self; + + skip_callback: + tail += header->size; + } + + source->tail = tail; + + atomic_thread_fence (memory_order_seq_cst); + + source->map->data_tail = tail; + + sysprof_capture_writer_flush (source->writer); + + return G_SOURCE_CONTINUE; +} + +static void +perf_source_finalize (GSource *gsource) +{ + PerfSource *source = (PerfSource *)gsource; + + if (source->map != NULL && + (gpointer)source->map != MAP_FAILED) + munmap ((gpointer)source->map, source->map_size); + + g_clear_pointer (&source->buffer, g_free); + g_clear_pointer (&source->writer, sysprof_capture_writer_unref); + g_clear_object (&source->unwinder); + + source->map = NULL; + source->map_data = NULL; + source->map_data_end = NULL; + source->map_data_size = 0; + source->map_size = 0; + source->tail = 0; +} + +static const GSourceFuncs source_funcs = { + .prepare = perf_source_prepare, + .check = perf_source_check, + .dispatch = perf_source_dispatch, + .finalize = perf_source_finalize, +}; + +static gboolean +perf_source_init (PerfSource *source, + int fd, + SysprofLiveUnwinder *unwinder, + SysprofCaptureWriter *writer, + int cpu, + int stack_size, + GError **error) +{ + gsize map_size; + guint8 *map; + + g_assert (source != NULL); + g_assert (writer != NULL); + g_assert (SYSPROF_IS_LIVE_UNWINDER (unwinder)); + g_assert (fd > STDERR_FILENO); + + map_size = N_PAGES * sysprof_getpagesize () + sysprof_getpagesize (); + map = mmap (NULL, map_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0); + + if ((gpointer)map == MAP_FAILED) + { + int errsv = errno; + g_set_error_literal (error, + G_FILE_ERROR, + g_file_error_from_errno (errsv), + g_strerror (errsv)); + return FALSE; + } + + source->writer = sysprof_capture_writer_ref (writer); + source->unwinder = g_object_ref (unwinder); + source->buffer = g_malloc (map_size); + source->map_size = map_size; + source->map = (gpointer)map; + source->map_data = map + sysprof_getpagesize (); + source->map_data_size = N_PAGES * sysprof_getpagesize (); + source->map_data_end = source->map_data + source->map_data_size; + source->tail = 0; + source->self_pid = getpid (); + source->cpu = cpu; + source->stack_size = stack_size; + + g_source_add_unix_fd ((GSource *)source, fd, G_IO_IN); + + return TRUE; +} + +static GSource * +perf_source_new (int perf_fd, + SysprofLiveUnwinder *unwinder, + SysprofCaptureWriter *writer, + int cpu, + int stack_size, + GError **error) +{ + GSource *source; + + if (perf_fd <= STDERR_FILENO) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "Invalid file-descriptor for perf event stream"); + return NULL; + } + + source = g_source_new ((GSourceFuncs *)&source_funcs, sizeof (PerfSource)); + g_source_set_static_name (source, "[perf-event-stream]"); + g_source_set_priority (source, G_PRIORITY_HIGH); + + if (!perf_source_init ((PerfSource *)source, perf_fd, unwinder, writer, cpu, stack_size, error)) + g_clear_pointer (&source, g_source_unref); + + return source; +} + +static void +clear_perf_fd (gpointer data) +{ + PerfFDArg *arg = data; + + if (arg->fd > STDERR_FILENO) + { + close (arg->fd); + arg->fd = -1; + } +} + +static gboolean +perf_fd_callback (const char *option_name, + const char *option_value, + gpointer data, + GError **error) +{ + PerfFDArg arg = {-1, -1}; + + if (sscanf (option_value, "%d:%d", &arg.fd, &arg.cpu) < 1) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "--perf-fd must be in the format FD or FD:CPU_NUMBER"); + return FALSE; + } + + if (arg.fd <= STDERR_FILENO) + { + g_set_error (error, + G_FILE_ERROR, + G_FILE_ERROR_BADF, + "--perf-fd must be >= %d", + STDERR_FILENO); + return FALSE; + } + + g_array_append_val (all_perf_fds, arg); + + return TRUE; +} + +static void +bump_to_max_fd_limit (void) +{ + struct rlimit limit; + + if (getrlimit (RLIMIT_NOFILE, &limit) == 0) + { + limit.rlim_cur = limit.rlim_max; + + if (setrlimit (RLIMIT_NOFILE, &limit) != 0) + g_warning ("Failed to set FD limit to %"G_GSSIZE_FORMAT"", + (gssize)limit.rlim_max); + else + g_debug ("Set RLIMIT_NOFILE to %"G_GSSIZE_FORMAT"", + (gssize)limit.rlim_max); + } +} + +static gboolean +exit_callback (gpointer user_data) +{ + g_main_loop_quit (user_data); + return G_SOURCE_REMOVE; +} + +int +main (int argc, + char *argv[]) +{ + g_autoptr(SysprofCaptureWriter) writer = NULL; + g_autoptr(SysprofLiveUnwinder) unwinder = NULL; + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GMainLoop) main_loop = NULL; + g_autoptr(GError) error = NULL; + g_autofd int capture_fd = -1; + g_autofd int kallsyms_fd = -1; + g_autofd int event_fd = -1; + int stack_size = 0; + const GOptionEntry entries[] = { + { "perf-fd", 0, 0, G_OPTION_ARG_CALLBACK, perf_fd_callback, "A file-descriptor to the perf event stream", "FD[:CPU]" }, + { "capture-fd", 0, 0, G_OPTION_ARG_INT, &capture_fd, "A file-descriptor to the sysprof capture", "FD" }, + { "event-fd", 0, 0, G_OPTION_ARG_INT, &event_fd, "A file-descriptor to an event-fd used to notify unwinder should exit", "FD" }, + { "kallsyms", 'k', 0, G_OPTION_ARG_INT, &kallsyms_fd, "Bundle kallsyms provided from passed FD", "FD" }, + { "stack-size", 's', 0, G_OPTION_ARG_INT, &stack_size, "Size of stacks being recorded", "STACK_SIZE" }, + { 0 } + }; + + main_loop = g_main_loop_new (NULL, FALSE); + + all_perf_fds = g_array_new (FALSE, FALSE, sizeof (PerfFDArg)); + g_array_set_clear_func (all_perf_fds, clear_perf_fd); + + context = g_option_context_new ("- translate perf event stream to sysprof"); + g_option_context_add_main_entries (context, entries, NULL); + g_option_context_set_summary (context, "\ + This tool is used by sysprofd to process incoming perf events that\n\ + include a copy of the stack and register state to the Sysprof capture\n\ + format.\n\ +\n\ + It should be provided two file-descriptors. One is for the perf-event\n\ + stream and one is for the Sysprof capture writer.\n\ +\n\ + Events that are not related to stack traces will also be passed along to\n\ + to the capture in the standard Sysprof capture format. That includes mmap\n\ + events, process information, and more.\n\ +\n\ +Examples:\n\ +\n\ + # FD 3 contains perf_event stream for CPU 1\n\ + sysprof-translate --perf-fd=3:1 --capture-fd=4"); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + + if (capture_fd <= STDERR_FILENO) + { + g_printerr ("--capture-fd must be > %d\n", STDERR_FILENO); + return EXIT_FAILURE; + } + + writer = sysprof_capture_writer_new_from_fd (g_steal_fd (&capture_fd), CAPTURE_BUFFER_SIZE); + + if (all_perf_fds->len == 0) + { + g_printerr ("You must secify at least one --perf-fd\n"); + return EXIT_FAILURE; + } + + bump_to_max_fd_limit (); + + unwinder = sysprof_live_unwinder_new (writer, g_steal_fd (&kallsyms_fd)); + + for (guint i = 0; i < all_perf_fds->len; i++) + { + const PerfFDArg *arg = &g_array_index (all_perf_fds, PerfFDArg, i); + g_autoptr(GSource) perf_source = NULL; + + if (!(perf_source = perf_source_new (arg->fd, unwinder, writer, arg->cpu, stack_size, &error))) + { + g_printerr ("Failed to initialize perf event stream: %s\n", + error->message); + return EXIT_FAILURE; + } + + g_source_attach (perf_source, NULL); + } + + if (event_fd != -1) + { + g_autoptr(GSource) exit_source = g_unix_fd_source_new (event_fd, G_IO_IN); + + g_source_set_callback (exit_source, + exit_callback, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + g_source_attach (exit_source, NULL); + } + + g_main_loop_run (main_loop); + + g_clear_pointer (&all_perf_fds, g_array_unref); + + return EXIT_SUCCESS; +} diff --git a/src/sysprof-live-unwinder/meson.build b/src/sysprof-live-unwinder/meson.build new file mode 100644 index 00000000..8cef7106 --- /dev/null +++ b/src/sysprof-live-unwinder/meson.build @@ -0,0 +1,19 @@ +sysprof_live_unwinder_deps = [ + libsysprof_static_dep, + dependency('libdw'), +] + +sysprof_live_unwinder_sources = [ + 'sysprof-live-process.c', + 'sysprof-live-unwinder.c', + 'main.c', +] + +sysprof_live_unwinder = executable('sysprof-live-unwinder', sysprof_live_unwinder_sources, + dependencies: sysprof_live_unwinder_deps, + c_args: release_flags, + install: true, + install_dir: pkglibexecdir, +) + +subdir('tests') diff --git a/src/sysprof-live-unwinder/sysprof-live-process.c b/src/sysprof-live-unwinder/sysprof-live-process.c new file mode 100644 index 00000000..7932048b --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-process.c @@ -0,0 +1,505 @@ +/* + * sysprof-live-pid.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include +#include + +/* Workaround for linux/fcntl.h also including the + * flock definitions in addition to libc. + */ +#ifndef _LINUX_FCNTL_H +# define _LINUX_FCNTL_H +# include +# undef _LINUX_FCNTL_H +#else +# include +#endif + +#include + +#include +#include +#include + +#include + +#include + +#include "sysprof-live-process.h" + +typedef struct _SysprofLiveProcess +{ + Dwfl_Callbacks callbacks; + Dwfl *dwfl; + Elf *elf; + char *root; + GPid pid; + int fd; +} SysprofLiveProcess; + +typedef struct _SysprofUnwinder +{ + SysprofLiveProcess *process; + const guint8 *stack; + gsize stack_len; + const guint64 *registers; + guint n_registers; + guint64 *addresses; + guint addresses_capacity; + guint addresses_len; + guint64 abi; + guint64 sp; + guint64 pc; + guint64 base_address; + GPid pid; + GPid tid; +} SysprofUnwinder; + +static SysprofUnwinder *current_unwinder; + +static inline GPid +sysprof_unwinder_next_thread (Dwfl *dwfl, + void *user_data, + void **thread_argp) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (*thread_argp == NULL) + { + *thread_argp = unwinder; + return unwinder->tid; + } + + return 0; +} + +static inline bool +sysprof_unwinder_get_thread (Dwfl *dwfl, + pid_t tid, + void *user_data, + void **thread_argp) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (unwinder->tid == tid) + { + *thread_argp = unwinder; + return TRUE; + } + + return FALSE; +} + +static inline bool +copy_word (const guint8 *data, + Dwarf_Word *result, + guint64 abi) +{ + if (abi == PERF_SAMPLE_REGS_ABI_64) + memcpy (result, data, sizeof (guint64)); + else if (abi == PERF_SAMPLE_REGS_ABI_32) + memcpy (result, data, sizeof (guint32)); + else + g_assert_not_reached (); + + return TRUE; +} + +static inline bool +sysprof_unwinder_memory_read (Dwfl *dwfl, + Dwarf_Addr addr, + Dwarf_Word *result, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + + if (addr < unwinder->base_address || addr - unwinder->base_address >= unwinder->stack_len) + { + Dwfl_Module *module = NULL; + Elf_Data *data = NULL; + Elf_Scn *section = NULL; + Dwarf_Addr bias; + + if (!(module = dwfl_addrmodule (dwfl, addr)) || + !(section = dwfl_module_address_section (module, &addr, &bias)) || + !(data = elf_getdata (section, NULL))) + return FALSE; + + if (data->d_buf && data->d_size > addr) + return copy_word ((guint8 *)data->d_buf + addr, result, unwinder->abi); + + return FALSE; + } + + return copy_word (&unwinder->stack[addr - unwinder->base_address], result, unwinder->abi); +} + +static inline bool +sysprof_unwinder_set_initial_registers (Dwfl_Thread *thread, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + + dwfl_thread_state_register_pc (thread, unwinder->pc); + + if (unwinder->abi == PERF_SAMPLE_REGS_ABI_64) + { + static const int regs_x86_64[] = {0, 3, 2, 1, 4, 5, 6, 7/*sp*/, 9, 10, 11, 12, 13, 14, 15, 16, 8/*ip*/}; + + for (int i = 0; i < G_N_ELEMENTS (regs_x86_64); i++) + { + int j = regs_x86_64[i]; + + dwfl_thread_state_registers (thread, i, 1, &unwinder->registers[j]); + } + } + else if (unwinder->abi == PERF_SAMPLE_REGS_ABI_32) + { + static const int regs_i386[] = {0, 2, 3, 1, 7/*sp*/, 6, 4, 5, 8/*ip*/}; + + for (int i = 0; i < G_N_ELEMENTS (regs_i386); i++) + { + int j = regs_i386[i]; + + dwfl_thread_state_registers (thread, i, 1, &unwinder->registers[j]); + } + } + + return TRUE; +} + +static const Dwfl_Thread_Callbacks thread_callbacks = { + sysprof_unwinder_next_thread, + sysprof_unwinder_get_thread, + sysprof_unwinder_memory_read, + sysprof_unwinder_set_initial_registers, + NULL, /* detach */ + NULL, /* thread_detach */ +}; + +static inline int +sysprof_unwinder_frame_cb (Dwfl_Frame *frame, + void *user_data) +{ + SysprofUnwinder *unwinder = current_unwinder; + Dwarf_Addr pc; + Dwarf_Addr sp; + bool is_activation; + guint8 sp_register_id; + + if (unwinder->addresses_len == unwinder->addresses_capacity) + return DWARF_CB_ABORT; + + if (!dwfl_frame_pc (frame, &pc, &is_activation)) + return DWARF_CB_ABORT; + + if (unwinder->abi == PERF_SAMPLE_REGS_ABI_64) + sp_register_id = 7; + else if (unwinder->abi == PERF_SAMPLE_REGS_ABI_32) + sp_register_id = 4; + else + return DWARF_CB_ABORT; + + if (dwfl_frame_reg (frame, sp_register_id, &sp) < 0) + return DWARF_CB_ABORT; + + unwinder->addresses[unwinder->addresses_len++] = pc; + + return DWARF_CB_OK; +} + +static inline guint +sysprof_unwind (SysprofLiveProcess *self, + Dwfl *dwfl, + Elf *elf, + GPid pid, + GPid tid, + guint64 abi, + const guint64 *registers, + guint n_registers, + const guint8 *stack, + gsize stack_len, + guint64 *addresses, + guint n_addresses) +{ +#if defined(__x86_64__) || defined(__i386__) + SysprofUnwinder unwinder; + + g_assert (dwfl != NULL); + g_assert (elf != NULL); + + /* Ignore anything byt 32/64 defined abi */ + if (!(abi == PERF_SAMPLE_REGS_ABI_32 || abi == PERF_SAMPLE_REGS_ABI_64)) + return 0; + + /* Make sure we have registers/stack to work with */ + if (registers == NULL || stack == NULL || stack_len == 0) + return 0; + + /* 9 registers on 32-bit x86, 17 on 64-bit x86_64 */ + if (!((abi == PERF_SAMPLE_REGS_ABI_32 && n_registers == 9) || + (abi == PERF_SAMPLE_REGS_ABI_64 && n_registers == 17))) + return 0; + + unwinder.process = self; + unwinder.sp = registers[7]; + unwinder.pc = registers[8]; + unwinder.base_address = unwinder.sp; + unwinder.addresses = addresses; + unwinder.addresses_capacity = n_addresses; + unwinder.addresses_len = 0; + unwinder.pid = pid; + unwinder.tid = tid; + unwinder.stack = stack; + unwinder.stack_len = stack_len; + unwinder.abi = abi; + unwinder.registers = registers; + unwinder.n_registers = n_registers; + + current_unwinder = &unwinder; + dwfl_getthread_frames (dwfl, tid, sysprof_unwinder_frame_cb, NULL); + current_unwinder = NULL; + + return unwinder.addresses_len; +#else + return 0; +#endif +} + +G_GNUC_NO_INLINE static int +_pidfd_open (int pid, + unsigned flags) +{ + int pidfd = syscall (SYS_pidfd_open, pid, flags); + + if (pidfd != -1) + { + int old_flags = fcntl (pidfd, F_GETFD); + + if (old_flags != -1) + fcntl (pidfd, F_SETFD, old_flags | FD_CLOEXEC); + } + + return pidfd; +} + +static int +sysprof_live_process_find_elf (Dwfl_Module *module, + void **user_data, + const char *module_name, + Dwarf_Addr base_addr, + char **filename, + Elf **elf) +{ + g_assert (current_unwinder != NULL); + g_assert (current_unwinder->process != NULL); + + *filename = NULL; + *elf = NULL; + + if (module_name[0] == '/') + { + g_autofree char *path = g_strdup_printf ("/proc/%u/root/%s", current_unwinder->pid, module_name); + g_autofd int fd = open (path, O_RDONLY | O_CLOEXEC); + + if (fd != -1) + { + *elf = dwelf_elf_begin (fd); + return g_steal_fd (&fd); + } + } + + return dwfl_linux_proc_find_elf (module, user_data, module_name, base_addr, filename, elf); +} + +static int +sysprof_live_process_find_debuginfo (Dwfl_Module *module, + void **user_data, + const char *module_name, + Dwarf_Addr base_addr, + const char *file_name, + const char *debuglink_file, + GElf_Word debuglink_crc, + char **debuginfo_file_name) +{ + return -1; +} + +SysprofLiveProcess * +sysprof_live_process_new (GPid pid) +{ + SysprofLiveProcess *live_process; + + live_process = g_atomic_rc_box_new0 (SysprofLiveProcess); + live_process->pid = pid; + live_process->fd = _pidfd_open (pid, 0); + live_process->root = g_strdup_printf ("/proc/%u/root/", pid); + live_process->callbacks.find_elf = sysprof_live_process_find_elf; + live_process->callbacks.find_debuginfo = sysprof_live_process_find_debuginfo; + live_process->callbacks.debuginfo_path = g_new0 (char *, 2); + live_process->callbacks.debuginfo_path[0] = g_build_filename (live_process->root, "usr/lib/debug", NULL); + + return live_process; +} + +SysprofLiveProcess * +sysprof_live_process_ref (SysprofLiveProcess *live_process) +{ + g_return_val_if_fail (live_process != NULL, NULL); + + return g_atomic_rc_box_acquire (live_process); +} + +static void +sysprof_live_process_finalize (gpointer data) +{ + SysprofLiveProcess *live_process = data; + + if (live_process->fd != -1) + { + close (live_process->fd); + live_process->fd = -1; + } + + g_clear_pointer (&live_process->elf, elf_end); + g_clear_pointer (&live_process->dwfl, dwfl_end); + g_clear_pointer (&live_process->root, g_free); + g_clear_pointer (&live_process->callbacks.debuginfo_path, g_free); +} + +void +sysprof_live_process_unref (SysprofLiveProcess *live_process) +{ + g_return_if_fail (live_process != NULL); + + g_atomic_rc_box_release_full (live_process, sysprof_live_process_finalize); +} + +gboolean +sysprof_live_process_is_active (SysprofLiveProcess *self) +{ + g_return_val_if_fail (self != NULL, FALSE); + + return self->fd > -1; +} + +static Dwfl * +sysprof_live_process_get_dwfl (SysprofLiveProcess *self) +{ + g_assert (self != NULL); + + if G_UNLIKELY (self->dwfl == NULL) + { + self->dwfl = dwfl_begin (&self->callbacks); + + dwfl_linux_proc_report (self->dwfl, self->pid); + dwfl_report_end (self->dwfl, NULL, NULL); + + if (self->fd > -1) + { + char path[64]; + g_autofd int exe_fd = -1; + + g_snprintf (path, sizeof path, "/proc/%u/exe", self->pid); + exe_fd = open (path, O_RDONLY); + + if (exe_fd > -1) + { + self->elf = elf_begin (exe_fd, ELF_C_READ_MMAP, NULL); + + if (self->elf != NULL) + dwfl_attach_state (self->dwfl, self->elf, self->pid, &thread_callbacks, self); + } + } + else + g_warning ("Attmpting to load exited process\n"); + } + + return self->dwfl; +} + +guint +sysprof_live_process_unwind (SysprofLiveProcess *self, + GPid tid, + guint64 abi, + const guint8 *stack, + gsize stack_len, + const guint64 *registers, + guint n_registers, + guint64 *addresses, + guint n_addresses) +{ +#if defined(__x86_64__) || defined(__i386__) + Dwfl *dwfl; + + g_assert (self != NULL); + g_assert (stack != NULL); + g_assert (registers != NULL); + g_assert (addresses != NULL); + + if (!sysprof_live_process_is_active (self)) + return 0; + + if (!(dwfl = sysprof_live_process_get_dwfl (self))) + return 0; + + if (self->elf == NULL) + return 0; + + g_assert (self->dwfl != NULL); + g_assert (self->elf != NULL); + + return sysprof_unwind (self, + self->dwfl, + self->elf, + self->pid, + tid, + abi, + registers, + n_registers, + stack, + stack_len, + addresses, + n_addresses); +#else + return 0; +#endif +} + +void +sysprof_live_process_add_map (SysprofLiveProcess *self, + guint64 begin, + guint64 end, + guint64 offset, + guint64 inode, + const char *filename) +{ + g_assert (self != NULL); + + /* We'll reparse VMAs on next use */ + g_clear_pointer (&self->dwfl, dwfl_end); + g_clear_pointer (&self->elf, elf_end); +} diff --git a/src/sysprof-live-unwinder/sysprof-live-process.h b/src/sysprof-live-unwinder/sysprof-live-process.h new file mode 100644 index 00000000..e2601a5b --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-process.h @@ -0,0 +1,50 @@ +/* + * sysprof-live-pid.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef struct _SysprofLiveProcess SysprofLiveProcess; + +SysprofLiveProcess *sysprof_live_process_new (GPid pid); +SysprofLiveProcess *sysprof_live_process_ref (SysprofLiveProcess *self); +void sysprof_live_process_unref (SysprofLiveProcess *self); +gboolean sysprof_live_process_is_active (SysprofLiveProcess *self); +void sysprof_live_process_add_map (SysprofLiveProcess *self, + guint64 begin, + guint64 end, + guint64 offset, + guint64 inode, + const char *filename); +guint sysprof_live_process_unwind (SysprofLiveProcess *self, + GPid tid, + guint64 abi, + const guint8 *stack, + gsize stack_len, + const guint64 *registers, + guint n_registers, + guint64 *addresses, + guint n_addresses); + +G_END_DECLS diff --git a/src/sysprof-live-unwinder/sysprof-live-unwinder.c b/src/sysprof-live-unwinder/sysprof-live-unwinder.c new file mode 100644 index 00000000..c3b954b2 --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-unwinder.c @@ -0,0 +1,427 @@ +/* + * sysprof-live-unwinder.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include + +#include + +#include "sysprof-live-process.h" +#include "sysprof-live-unwinder.h" +#include "sysprof-maps-parser-private.h" + +struct _SysprofLiveUnwinder +{ + GObject parent_instance; + SysprofCaptureWriter *writer; + GHashTable *live_pids_by_pid; +}; + +G_DEFINE_FINAL_TYPE (SysprofLiveUnwinder, sysprof_live_unwinder, G_TYPE_OBJECT) + +enum { + CLOSED, + N_SIGNALS +}; + +static guint signals[N_SIGNALS]; + +static char * +sysprof_live_unwinder_read_file (SysprofLiveUnwinder *self, + const char *path, + gboolean insert_into_capture) +{ + gint64 when = SYSPROF_CAPTURE_CURRENT_TIME; + char *contents = NULL; + gsize len = 0; + gsize offset = 0; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + if (!g_file_get_contents (path, &contents, &len, NULL)) + return NULL; + + if (insert_into_capture) + { + while (len > 0) + { + gsize this_write = MIN (len, 4096*4); + + if (!sysprof_capture_writer_add_file (self->writer, + when, + -1, + -1, + path, + this_write == len, + (const guint8 *)&contents[offset], this_write)) + break; + + len -= this_write; + offset += this_write; + } + } + + return contents; +} + +static char * +sysprof_live_unwinder_read_pid_file (SysprofLiveUnwinder *self, + GPid pid, + const char *path_part, + gboolean insert_into_capture) +{ + g_autofree char *path = NULL; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + path = g_strdup_printf ("/proc/%d/%s", pid, path_part); + + return sysprof_live_unwinder_read_file (self, path, insert_into_capture); +} + +static SysprofLiveProcess * +sysprof_live_unwinder_find_pid (SysprofLiveUnwinder *self, + GPid pid, + gboolean send_comm) +{ + SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + if (pid < 0) + return NULL; + + if (!(live_pid = g_hash_table_lookup (self->live_pids_by_pid, GINT_TO_POINTER (pid)))) + { + gint64 now = SYSPROF_CAPTURE_CURRENT_TIME; + + live_pid = sysprof_live_process_new (pid); + + g_hash_table_replace (self->live_pids_by_pid, GINT_TO_POINTER (pid), live_pid); + + if (send_comm) + { + g_autofree char *path = g_strdup_printf ("/proc/%d/comm", pid); + g_autofree char *comm = NULL; + gsize len; + + if (g_file_get_contents (path, &comm, &len, NULL)) + { + g_autofree char *tmp = comm; + comm = g_strstrip (g_utf8_make_valid (tmp, len)); + sysprof_capture_writer_add_process (self->writer, now, -1, pid, comm); + } + } + + if (sysprof_live_process_is_active (live_pid)) + { + g_autofree char *mountinfo = sysprof_live_unwinder_read_pid_file (self, pid, "mountinfo", TRUE); + g_autofree char *maps = sysprof_live_unwinder_read_pid_file (self, pid, "maps", FALSE); + + if (maps != NULL) + { + SysprofMapsParser maps_parser; + guint64 begin, end, offset, inode; + char *filename; + + sysprof_maps_parser_init (&maps_parser, maps, -1); + + while (sysprof_maps_parser_next (&maps_parser, &begin, &end, &offset, &inode, &filename)) + { + sysprof_live_process_add_map (live_pid, begin, end, offset, inode, filename); + sysprof_capture_writer_add_map (self->writer, now, -1, pid, + begin, end, offset, + inode, filename); + g_free (filename); + } + } + } + } + + return live_pid; +} + +static void +sysprof_live_unwinder_mine_pids (SysprofLiveUnwinder *self) +{ + g_autoptr(GDir) dir = NULL; + const char *name; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (self->writer != NULL); + + if (!(dir = g_dir_open ("/proc", 0, NULL))) + return; + + while ((name = g_dir_read_name (dir))) + { + GPid pid; + + if (!g_ascii_isdigit (*name)) + continue; + + if (!(pid = atoi (name))) + continue; + + sysprof_live_unwinder_find_pid (self, pid, TRUE); + } +} + +static void +sysprof_live_unwinder_finalize (GObject *object) +{ + SysprofLiveUnwinder *self = (SysprofLiveUnwinder *)object; + + g_clear_pointer (&self->writer, sysprof_capture_writer_unref); + g_clear_pointer (&self->live_pids_by_pid, g_hash_table_unref); + + G_OBJECT_CLASS (sysprof_live_unwinder_parent_class)->finalize (object); +} + +static void +sysprof_live_unwinder_class_init (SysprofLiveUnwinderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = sysprof_live_unwinder_finalize; + + signals[CLOSED] = + g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + NULL, + G_TYPE_NONE, 0); +} + +static void +sysprof_live_unwinder_init (SysprofLiveUnwinder *self) +{ + self->live_pids_by_pid = g_hash_table_new_full (NULL, + NULL, + NULL, + (GDestroyNotify)sysprof_live_process_unref); +} + +SysprofLiveUnwinder * +sysprof_live_unwinder_new (SysprofCaptureWriter *writer, + int kallsyms_fd) +{ + SysprofLiveUnwinder *self; + g_autofree char *mounts = NULL; + + g_return_val_if_fail (writer != NULL, NULL); + + self = g_object_new (SYSPROF_TYPE_LIVE_UNWINDER, NULL); + self->writer = sysprof_capture_writer_ref (writer); + + if (kallsyms_fd != -1) + { + sysprof_capture_writer_add_file_fd (writer, + SYSPROF_CAPTURE_CURRENT_TIME, + -1, + -1, + "/proc/kallsyms", + kallsyms_fd); + close (kallsyms_fd); + } + + mounts = sysprof_live_unwinder_read_file (self, "/proc/mounts", TRUE); + + sysprof_live_unwinder_mine_pids (self); + + return self; +} + +void +sysprof_live_unwinder_seen_process (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + const char *comm) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, FALSE); + + sysprof_capture_writer_add_process (self->writer, time, cpu, pid, comm); +} + +void +sysprof_live_unwinder_process_exited (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid) +{ + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + sysprof_capture_writer_add_exit (self->writer, time, cpu, pid); +} + +void +sysprof_live_unwinder_process_forked (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid parent_tid, + GPid tid) +{ + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + sysprof_capture_writer_add_fork (self->writer, time, cpu, parent_tid, tid); +} + +void +sysprof_live_unwinder_track_mmap (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + SysprofCaptureAddress begin, + SysprofCaptureAddress end, + SysprofCaptureAddress offset, + guint64 inode, + const char *filename, + const char *build_id) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + if (build_id != NULL) + sysprof_capture_writer_add_map_with_build_id (self->writer, time, cpu, pid, + begin, end, offset, + inode, filename, + build_id); + else + sysprof_capture_writer_add_map (self->writer, time, cpu, pid, + begin, end, offset, + inode, filename); + + sysprof_live_process_add_map (live_pid, begin, end, offset, inode, filename); +} + +void +sysprof_live_unwinder_process_sampled (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses) +{ + G_GNUC_UNUSED SysprofLiveProcess *live_pid; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + sysprof_capture_writer_add_sample (self->writer, time, cpu, pid, tid, + addresses, n_addresses); +} + +void +sysprof_live_unwinder_process_sampled_with_stack (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses, + const guint8 *stack, + guint64 stack_size, + guint64 stack_dyn_size, + guint64 abi, + const guint64 *registers, + guint n_registers) +{ + SysprofLiveProcess *live_pid; + SysprofCaptureAddress unwound[256]; + gboolean found_user = FALSE; + guint pos; + + g_assert (SYSPROF_IS_LIVE_UNWINDER (self)); + g_assert (stack != NULL); + g_assert (stack_dyn_size <= stack_size); + + if (stack_dyn_size == 0 || n_addresses >= G_N_ELEMENTS (unwound)) + { + sysprof_live_unwinder_process_sampled (self, time, cpu, pid, tid, addresses, n_addresses); + return; + } + + live_pid = sysprof_live_unwinder_find_pid (self, pid, TRUE); + + /* Copy addresses over (which might be kernel, context-switch, etc until + * we get to the PERF_CONTEXT_USER. We'll decode the stack right into the + * location after that. + */ + for (pos = 0; pos < n_addresses; pos++) + { + unwound[pos] = addresses[pos]; + + if (addresses[pos] == PERF_CONTEXT_USER) + { + found_user = TRUE; + break; + } + } + + /* If we didn't find a user context (but we have a stack size) synthesize + * the PERF_CONTEXT_USER now. + */ + if (!found_user && pos < G_N_ELEMENTS (unwound)) + unwound[pos++] = PERF_CONTEXT_USER; + + /* Now request the live process unwind the user-space stack */ + if (pos < G_N_ELEMENTS (unwound)) + { + guint n_unwound; + + n_unwound = sysprof_live_process_unwind (live_pid, + tid, + abi, + stack, + stack_dyn_size, + registers, + n_registers, + &unwound[pos], + G_N_ELEMENTS (unwound) - pos); + + /* Only take DWARF unwind if it was better */ + if (pos + n_unwound > n_addresses) + { + addresses = unwound; + n_addresses = pos + n_unwound; + } + } + + sysprof_capture_writer_add_sample (self->writer, time, cpu, pid, tid, addresses, n_addresses); +} diff --git a/src/sysprof-live-unwinder/sysprof-live-unwinder.h b/src/sysprof-live-unwinder/sysprof-live-unwinder.h new file mode 100644 index 00000000..e27ed9a4 --- /dev/null +++ b/src/sysprof-live-unwinder/sysprof-live-unwinder.h @@ -0,0 +1,79 @@ +/* + * sysprof-live-unwinder.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define SYSPROF_TYPE_LIVE_UNWINDER (sysprof_live_unwinder_get_type()) + +G_DECLARE_FINAL_TYPE (SysprofLiveUnwinder, sysprof_live_unwinder, SYSPROF, LIVE_UNWINDER, GObject) + +SysprofLiveUnwinder *sysprof_live_unwinder_new (SysprofCaptureWriter *writer, + int kallsyms_fd); +void sysprof_live_unwinder_seen_process (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + const char *comm); +void sysprof_live_unwinder_process_exited (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid); +void sysprof_live_unwinder_process_forked (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid parent_tid, + GPid tid); +void sysprof_live_unwinder_track_mmap (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + SysprofCaptureAddress begin, + SysprofCaptureAddress end, + SysprofCaptureAddress offset, + guint64 inode, + const char *filename, + const char *build_id); +void sysprof_live_unwinder_process_sampled (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses); +void sysprof_live_unwinder_process_sampled_with_stack (SysprofLiveUnwinder *self, + gint64 time, + int cpu, + GPid pid, + GPid tid, + const SysprofCaptureAddress *addresses, + guint n_addresses, + const guint8 *stack, + guint64 stack_size, + guint64 stack_dyn_size, + guint64 abi, + const guint64 *registers, + guint n_registers); + +G_END_DECLS diff --git a/src/sysprof-live-unwinder/tests/meson.build b/src/sysprof-live-unwinder/tests/meson.build new file mode 100644 index 00000000..9e1a945a --- /dev/null +++ b/src/sysprof-live-unwinder/tests/meson.build @@ -0,0 +1,35 @@ +sysprof_live_unwinder_test_env = [ + 'G_DEBUG=gc-friendly', + 'GSETTINGS_BACKEND=memory', + 'MALLOC_CHECK_=2', +] + +sysprof_live_unwinder_testsuite_c_args = [ + '-DG_ENABLE_DEBUG', + '-UG_DISABLE_ASSERT', + '-UG_DISABLE_CAST_CHECKS', + '-DBUILDDIR="@0@"'.format(meson.current_build_dir()), +] + +sysprof_live_unwinder_testsuite = { + 'test-live-unwinder' : {'skip': true}, +} + +sysprof_live_unwinder_testsuite_deps = [ + libsysprof_static_dep, +] + +if polkit_agent_dep.found() + sysprof_live_unwinder_testsuite_deps += polkit_agent_dep +endif + +foreach test, params: sysprof_live_unwinder_testsuite + test_exe = executable(test, '@0@.c'.format(test), + c_args: sysprof_live_unwinder_testsuite_c_args, + dependencies: sysprof_live_unwinder_testsuite_deps, + ) + + if not params.get('skip', false) + test(test, test_exe, env: sysprof_live_unwinder_test_env) + endif +endforeach diff --git a/src/sysprof-live-unwinder/tests/test-live-unwinder.c b/src/sysprof-live-unwinder/tests/test-live-unwinder.c new file mode 100644 index 00000000..114cc568 --- /dev/null +++ b/src/sysprof-live-unwinder/tests/test-live-unwinder.c @@ -0,0 +1,391 @@ +/* + * test-live-unwinder.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include + +#include +#include + +#include + +#include + +#include "sysprof-perf-event-stream-private.h" + +#if HAVE_POLKIT_AGENT +# define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +# include +# include +#endif + +#include + +#define N_WAKEUP_EVENTS 149 + +/* The following was provided to Sysprof by Serhei Makarov as part + * of the eu-stacktrace prototype work. + */ +#ifdef _ASM_X86_PERF_REGS_H +/* #define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK -- error on x86_64 due to including segment regs*/ +#define REG(R) (1ULL << PERF_REG_X86_ ## R) +#define DWARF_NEEDED_REGS (/* no FLAGS */ REG(IP) | REG(SP) | REG(AX) | REG(CX) | REG(DX) | REG(BX) | REG(SI) | REG(DI) | REG(SP) | REG(BP) | /* no segment regs */ REG(R8) | REG(R9) | REG(R10) | REG(R11) | REG(R12) | REG(R13) | REG(R14) | REG(R15)) +/* XXX register ordering is defined in linux arch/x86/include/uapi/asm/perf_regs.h; + see code in tools/perf/util/intel-pt.c intel_pt_add_gp_regs() + and note how registers are added in the same order as the perf_regs.h enum */ +#define SYSPROF_ARCH_PREFERRED_REGS DWARF_NEEDED_REGS +/* TODO: add other architectures, imitating the linux tools/perf tree */ +#else +# define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK +#endif /* _ASM_{arch}_PERF_REGS_H */ + +static gboolean sample_stack; +static char *kallsyms = NULL; +static int sample_stack_size = 8192; + +static void +open_perf_stream_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = (GDBusConnection *)object; + g_autoptr(DexPromise) promise = user_data; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_DBUS_CONNECTION (connection)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (DEX_IS_PROMISE (promise)); + + if ((ret = g_dbus_connection_call_with_unix_fd_list_finish (connection, &fd_list, result, &error))) + { + int handle; + int fd; + + g_variant_get (ret, "(h)", &handle); + + if (-1 != (fd = g_unix_fd_list_get (fd_list, handle, &error))) + { + dex_promise_resolve_fd (promise, g_steal_fd (&fd)); + return; + } + } + + g_assert (error != NULL); + + dex_promise_reject (promise, g_steal_pointer (&error)); +} + +static int +open_perf_stream (GDBusConnection *bus, + int cpu, + GError **error) +{ + struct perf_event_attr attr = {0}; + gboolean with_mmap2 = TRUE; + gboolean use_software = FALSE; + + g_assert (G_IS_DBUS_CONNECTION (bus)); + g_assert (cpu >= 0); + g_assert (error != NULL); + +try_again: + attr.sample_type = PERF_SAMPLE_IP + | PERF_SAMPLE_TID + | PERF_SAMPLE_IDENTIFIER + | PERF_SAMPLE_CALLCHAIN + | PERF_SAMPLE_TIME; + + if (sample_stack) + { + attr.sample_type |= (PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER); + attr.sample_stack_user = sample_stack_size; + attr.sample_regs_user = SYSPROF_ARCH_PREFERRED_REGS; + } + + attr.wakeup_events = N_WAKEUP_EVENTS; + attr.disabled = TRUE; + attr.mmap = TRUE; + attr.mmap2 = with_mmap2; + attr.comm = 1; + attr.task = 1; + attr.exclude_idle = 1; + attr.sample_id_all = 1; + +#ifdef HAVE_PERF_CLOCKID + attr.clockid = sysprof_clock; + attr.use_clockid = 1; +#endif + + attr.size = sizeof attr; + + if (use_software) + { + attr.type = PERF_TYPE_SOFTWARE; + attr.config = PERF_COUNT_SW_CPU_CLOCK; + attr.sample_period = 1000000; + } + else + { + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_CPU_CYCLES; + attr.sample_period = 1200000; + } + + { + g_autoptr(GVariant) options = _sysprof_perf_event_attr_to_variant (&attr); + g_autoptr(DexPromise) promise = dex_promise_new (); + g_autofd int fd = -1; + + g_dbus_connection_call_with_unix_fd_list (bus, + "org.gnome.Sysprof3", + "/org/gnome/Sysprof3", + "org.gnome.Sysprof3.Service", + "PerfEventOpen", + g_variant_new ("(@a{sv}iiht)", + options, + -1, + cpu, + -1, + 0), + G_VARIANT_TYPE ("(h)"), + G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION, + G_MAXUINT, + NULL, + dex_promise_get_cancellable (promise), + open_perf_stream_cb, + dex_ref (promise)); + + fd = dex_await_fd (dex_ref (promise), error); + + if (*error == NULL) + { + g_printerr ("CPU[%d]: opened perf_event stream as FD %d\n", cpu, fd); + return g_steal_fd (&fd); + } + + fd = -1; + } + + if (with_mmap2) + { + g_clear_error (error); + with_mmap2 = FALSE; + goto try_again; + } + + if (use_software == FALSE) + { + g_clear_error (error); + with_mmap2 = TRUE; + use_software = TRUE; + goto try_again; + } + + g_assert (*error != NULL); + + return -1; +} + +static DexFuture * +main_fiber (gpointer user_data) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GDBusConnection) bus = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GError) error = NULL; + g_autofd int writer_fd = -1; + int n_cpu = g_get_num_processors (); + int next_target_fd = 3; + + /* Get our bus we will use for authorization */ + if (!(bus = dex_await_object (dex_bus_get (G_BUS_TYPE_SYSTEM), &error))) + return dex_future_new_for_error (g_steal_pointer (&error)); + + /* Setup our launcher we'll use to map FDs into the translator */ + launcher = g_subprocess_launcher_new (0); + + /* Setup our argv which will notify the child about where to + * find the FDs containing perf event streams. + */ + argv = g_ptr_array_new_with_free_func (g_free); + g_ptr_array_add (argv, g_build_filename (BUILDDIR, "..", "sysprof-live-unwinder", NULL)); + + /* Provide kallsyms from the file provided */ + if (kallsyms != NULL) + { + int fd = open (kallsyms, O_RDONLY | O_CLOEXEC); + + if (fd != -1) + { + g_subprocess_launcher_take_fd (launcher, fd, next_target_fd); + g_ptr_array_add (argv, g_strdup_printf ("--kallsyms=%d", next_target_fd++)); + } + } + + if (sample_stack) + g_ptr_array_add (argv, g_strdup_printf ("--stack-size=%u", sample_stack_size)); + + g_printerr ("sysprof-live-unwinder at %s\n", (const char *)argv->pdata[0]); + + /* First try to open a perf_event stream for as many CPUs as we + * can before we get complaints from the kernel. + */ + for (int cpu = 0; cpu < n_cpu; cpu++) + { + g_autoptr(GError) cpu_error = NULL; + g_autofd int perf_fd = open_perf_stream (bus, cpu, &cpu_error); + + if (perf_fd == -1) + { + g_printerr ("CPU[%d]: %s\n", cpu, cpu_error->message); + continue; + } + + if (0 != ioctl (perf_fd, PERF_EVENT_IOC_ENABLE)) + { + int errsv = errno; + g_warning ("Failed to enable perf_fd: %s", g_strerror (errsv)); + } + + g_ptr_array_add (argv, g_strdup_printf ("--perf-fd=%d:%d", next_target_fd, cpu)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&perf_fd), next_target_fd); + + next_target_fd++; + } + + /* Now create a FD for our destination capture. */ + if (-1 == (writer_fd = open ("translated.syscap", O_CREAT|O_RDWR|O_CLOEXEC, 0664)) || + ftruncate (writer_fd, 0) != 0) + return dex_future_new_for_errno (errno); + g_ptr_array_add (argv, g_strdup_printf ("--capture-fd=%d", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&writer_fd), next_target_fd); + next_target_fd++; + + /* Null-terminate our argv */ + g_ptr_array_add (argv, NULL); + + /* Spawn our worker process with the perf FDs and writer provided */ + if (!(subprocess = g_subprocess_launcher_spawnv (launcher, + (const char * const *)argv->pdata, + &error))) + return dex_future_new_for_error (g_steal_pointer (&error)); + + /* Now wait for the translation process to complete */ + if (!dex_await_boolean (dex_subprocess_wait_check (subprocess), &error)) + return dex_future_new_for_error (g_steal_pointer (&error)); + + return dex_future_new_true (); +} + +static DexFuture * +finally_cb (DexFuture *future, + gpointer user_data) +{ + g_autoptr(GError) error = NULL; + GMainLoop *main_loop = user_data; + + if (!dex_await (dex_ref (future), &error)) + { + g_printerr ("Error: %s\n", error->message); + exit (EXIT_FAILURE); + } + + g_main_loop_quit (main_loop); + + return NULL; +} + +int +main (int argc, + char *argv[]) +{ +#if HAVE_POLKIT_AGENT + PolkitAgentListener *polkit = NULL; + PolkitSubject *subject = NULL; +#endif + g_autoptr(GOptionContext) context = NULL; + g_autoptr(GMainLoop) main_loop = NULL; + g_autoptr(GError) error = NULL; + DexFuture *future; + GOptionEntry entries[] = { + { "sample-stack", 's', 0, G_OPTION_ARG_NONE, &sample_stack, "If the stack should be sampled for user-space unwinding" }, + { "sample-stack-size", 'S', 0, G_OPTION_ARG_INT, &sample_stack_size, "If size of the stack to sample in bytes" }, + { "kallsyms", 'k', 0, G_OPTION_ARG_FILENAME, &kallsyms, "Specify kallsyms for use" }, + { NULL } + }; + + sysprof_clock_init (); + dex_init (); + + main_loop = g_main_loop_new (NULL, FALSE); + context = g_option_context_new ("- test sysprof-live-unwinder"); + g_option_context_add_main_entries (context, entries, NULL); + + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_printerr ("%s\n", error->message); + return EXIT_FAILURE; + } + +#if HAVE_POLKIT_AGENT + /* Start polkit agent so that we can elevate privileges from a TTY */ + if (g_getenv ("DESKTOP_SESSION") == NULL && + (subject = polkit_unix_process_new_for_owner (getpid (), 0, -1))) + { + g_autoptr(GError) pkerror = NULL; + + polkit = polkit_agent_text_listener_new (NULL, NULL); + polkit_agent_listener_register (polkit, + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + NULL, + NULL, + &pkerror); + + if (pkerror != NULL) + { + g_dbus_error_strip_remote_error (pkerror); + g_printerr ("Failed to register polkit agent: %s\n", + pkerror->message); + } + } +#endif + + future = dex_scheduler_spawn (NULL, 0, main_fiber, NULL, NULL); + future = dex_future_finally (future, + finally_cb, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + dex_future_disown (future); + + g_main_loop_run (main_loop); + + g_clear_pointer (&kallsyms, g_free); + + return EXIT_SUCCESS; +} diff --git a/src/sysprofd/ipc-unwinder-impl.c b/src/sysprofd/ipc-unwinder-impl.c new file mode 100644 index 00000000..7f218de6 --- /dev/null +++ b/src/sysprofd/ipc-unwinder-impl.c @@ -0,0 +1,247 @@ +/* + * ipc-unwinder-impl.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "ipc-unwinder-impl" + +#include "config.h" + +#include + +#include +#include +#include + +#include + +#include + +#include "ipc-unwinder-impl.h" + +struct _IpcUnwinderImpl +{ + IpcUnwinderSkeleton parent_instance; +}; + +static void +child_setup (gpointer data) +{ + prctl (PR_SET_PDEATHSIG, SIGKILL); +} + +static gboolean +ipc_unwinder_impl_handle_unwind (IpcUnwinder *unwinder, + GDBusMethodInvocation *invocation, + GUnixFDList *fd_list, + guint stack_size, + GVariant *arg_perf_fds, + GVariant *arg_event_fd) +{ + g_autoptr(GSubprocessLauncher) launcher = NULL; + g_autoptr(GSubprocess) subprocess = NULL; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GPtrArray) argv = NULL; + g_autoptr(GError) error = NULL; + g_autofd int our_fd = -1; + g_autofd int their_fd = -1; + g_autofd int event_fd = -1; + GVariantIter iter; + int capture_fd_handle; + int pair[2]; + int next_target_fd = 3; + int perf_fd_handle; + int cpu; + + g_assert (IPC_IS_UNWINDER_IMPL (unwinder)); + g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); + g_assert (!fd_list || G_IS_UNIX_FD_LIST (fd_list)); + + if (stack_size == 0 || stack_size % sysconf (_SC_PAGESIZE) != 0) + { + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Stack size must be a multiple of the page size"); + return TRUE; + } + + if (fd_list == NULL) + { + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_FILE_NOT_FOUND, + "Missing perf FDs"); + return TRUE; + } + + launcher = g_subprocess_launcher_new (0); + argv = g_ptr_array_new_with_free_func (g_free); + + g_ptr_array_add (argv, g_strdup (PACKAGE_LIBEXECDIR "/sysprof-live-unwinder")); + g_ptr_array_add (argv, g_strdup_printf ("--stack-size=%u", stack_size)); + + if (-1 == (event_fd = g_unix_fd_list_get (fd_list, g_variant_get_handle (arg_event_fd), &error))) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--event-fd=%u", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&event_fd), next_target_fd++); + + g_variant_iter_init (&iter, arg_perf_fds); + + while (g_variant_iter_loop (&iter, "(hi)", &perf_fd_handle, &cpu)) + { + g_autofd int perf_fd = g_unix_fd_list_get (fd_list, perf_fd_handle, &error); + + if (perf_fd < 0) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--perf-fd=%d:%d", next_target_fd, cpu)); + g_subprocess_launcher_take_fd (launcher, + g_steal_fd (&perf_fd), + next_target_fd++); + } + + g_subprocess_launcher_set_child_setup (launcher, child_setup, NULL, NULL); + + if (socketpair (AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, pair) < 0) + { + int errsv = errno; + g_dbus_method_invocation_return_error_literal (g_steal_pointer (&invocation), + G_IO_ERROR, + g_io_error_from_errno (errsv), + g_strerror (errsv)); + return TRUE; + } + + our_fd = g_steal_fd (&pair[0]); + their_fd = g_steal_fd (&pair[1]); + + out_fd_list = g_unix_fd_list_new (); + capture_fd_handle = g_unix_fd_list_append (out_fd_list, their_fd, &error); + + if (capture_fd_handle < 0) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + g_ptr_array_add (argv, g_strdup_printf ("--capture-fd=%d", next_target_fd)); + g_subprocess_launcher_take_fd (launcher, g_steal_fd (&our_fd), next_target_fd++); + + g_ptr_array_add (argv, NULL); + + if (!(subprocess = g_subprocess_launcher_spawnv (launcher, (const char * const *)argv->pdata, &error))) + { + g_dbus_method_invocation_return_gerror (g_steal_pointer (&invocation), error); + return TRUE; + } + + ipc_unwinder_complete_unwind (unwinder, + g_steal_pointer (&invocation), + out_fd_list, + g_variant_new_handle (capture_fd_handle)); + + g_subprocess_wait_async (subprocess, NULL, NULL, NULL); + + return TRUE; +} + +static void +unwinder_iface_init (IpcUnwinderIface *iface) +{ + iface->handle_unwind = ipc_unwinder_impl_handle_unwind; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (IpcUnwinderImpl, ipc_unwinder_impl, IPC_TYPE_UNWINDER_SKELETON, + G_IMPLEMENT_INTERFACE (IPC_TYPE_UNWINDER, unwinder_iface_init)) + +static gboolean +ipc_unwinder_impl_g_authorize_method (GDBusInterfaceSkeleton *skeleton, + GDBusMethodInvocation *invocation) +{ + PolkitAuthorizationResult *res = NULL; + PolkitAuthority *authority = NULL; + PolkitSubject *subject = NULL; + const gchar *peer_name; + gboolean ret = TRUE; + + g_assert (IPC_IS_UNWINDER_IMPL (skeleton)); + g_assert (G_IS_DBUS_METHOD_INVOCATION (invocation)); + + peer_name = g_dbus_method_invocation_get_sender (invocation); + + if (!(authority = polkit_authority_get_sync (NULL, NULL)) || + !(subject = polkit_system_bus_name_new (peer_name)) || + !(res = polkit_authority_check_authorization_sync (authority, + POLKIT_SUBJECT (subject), + "org.gnome.sysprof3.profile", + NULL, + POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, + NULL, + NULL)) || + !polkit_authorization_result_get_is_authorized (res)) + { + g_dbus_method_invocation_return_error (g_steal_pointer (&invocation), + G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Not authorized to make request"); + ret = FALSE; + } + + g_clear_object (&authority); + g_clear_object (&subject); + g_clear_object (&res); + + return ret; +} + +static void +ipc_unwinder_impl_finalize (GObject *object) +{ + G_OBJECT_CLASS (ipc_unwinder_impl_parent_class)->finalize (object); +} + +static void +ipc_unwinder_impl_class_init (IpcUnwinderImplClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GDBusInterfaceSkeletonClass *skeleton_class = G_DBUS_INTERFACE_SKELETON_CLASS (klass); + + object_class->finalize = ipc_unwinder_impl_finalize; + + skeleton_class->g_authorize_method = ipc_unwinder_impl_g_authorize_method; +} + +static void +ipc_unwinder_impl_init (IpcUnwinderImpl *self) +{ +} + +IpcUnwinder * +ipc_unwinder_impl_new (void) +{ + return g_object_new (IPC_TYPE_UNWINDER_IMPL, NULL); +} diff --git a/src/sysprofd/ipc-unwinder-impl.h b/src/sysprofd/ipc-unwinder-impl.h new file mode 100644 index 00000000..ebe7a2f0 --- /dev/null +++ b/src/sysprofd/ipc-unwinder-impl.h @@ -0,0 +1,34 @@ +/* + * ipc-unwinder-impl.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "ipc-unwinder.h" + +G_BEGIN_DECLS + +#define IPC_TYPE_UNWINDER_IMPL (ipc_unwinder_impl_get_type()) + +G_DECLARE_FINAL_TYPE (IpcUnwinderImpl, ipc_unwinder_impl, IPC, UNWINDER_IMPL, IpcUnwinderSkeleton) + +IpcUnwinder *ipc_unwinder_impl_new (void); + +G_END_DECLS diff --git a/src/sysprofd/meson.build b/src/sysprofd/meson.build index 34901f44..b7e87ebe 100644 --- a/src/sysprofd/meson.build +++ b/src/sysprofd/meson.build @@ -10,18 +10,24 @@ ipc_service_src = gnome.gdbus_codegen('ipc-service', namespace: 'Ipc', ) +ipc_unwinder_src = gnome.gdbus_codegen('ipc-unwinder', + sources: 'org.gnome.Sysprof3.Unwinder.xml', + interface_prefix: 'org.gnome.Sysprof3.', + namespace: 'Ipc', +) + sysprofd_sources = [ 'sysprofd.c', 'ipc-rapl-profiler.c', 'ipc-service-impl.c', + 'ipc-unwinder-impl.c', 'sysprof-turbostat.c', 'helpers.c', ipc_profiler_src, ipc_service_src, + ipc_unwinder_src, ] -pkglibexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) - sysprofd_deps = [ glib_dep, gio_dep, diff --git a/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml b/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml new file mode 100644 index 00000000..fb2c7848 --- /dev/null +++ b/src/sysprofd/org.gnome.Sysprof3.Unwinder.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/src/sysprofd/sysprofd.c b/src/sysprofd/sysprofd.c index e39d59c4..7a7101e8 100644 --- a/src/sysprofd/sysprofd.c +++ b/src/sysprofd/sysprofd.c @@ -30,9 +30,11 @@ #include "ipc-rapl-profiler.h" #include "ipc-service-impl.h" +#include "ipc-unwinder-impl.h" #define V3_PATH "/org/gnome/Sysprof3" #define RAPL_PATH "/org/gnome/Sysprof3/RAPL" +#define UNWINDER_PATH "/org/gnome/Sysprof3/Unwinder" #define NAME_ACQUIRE_DELAY_SECS 3 #define INACTIVITY_TIMEOUT_SECS 120 @@ -126,6 +128,7 @@ main (gint argc, { g_autoptr(IpcProfiler) rapl = ipc_rapl_profiler_new (); g_autoptr(IpcService) v3_service = ipc_service_impl_new (); + g_autoptr(IpcUnwinder) unwinder = ipc_unwinder_impl_new (); g_signal_connect (v3_service, "activity", G_CALLBACK (activity_cb), NULL); g_signal_connect (rapl, "activity", G_CALLBACK (activity_cb), NULL); @@ -133,7 +136,8 @@ main (gint argc, activity_cb (NULL, NULL); if (g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (v3_service), bus, V3_PATH, &error) && - g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (rapl), bus, RAPL_PATH, &error)) + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (rapl), bus, RAPL_PATH, &error) && + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (unwinder), bus, UNWINDER_PATH, &error)) { for (guint i = 0; i < G_N_ELEMENTS (bus_names); i++) { From 6e3e1637be9b507b5daa59d491f41ecfed0c2729 Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:53:29 -0800 Subject: [PATCH 4/6] libsysprof: add SysprofUserSampler for live unwinding This instrument triggers the live unwinder in sysprofd to capture a pre-configured amount of stack contents and CPU registers. You can use this instead of SysprofSampler in cases where you do not have frame- pointers but want a useful trace. It does have a moderate amount of CPU overhead compared to just relying on frame-pointers so keep that in mind. Generally useful on platforms that do not have frame pointers such as CentOS. --- src/libsysprof/meson.build | 7 + src/libsysprof/sysprof-user-sampler.c | 570 ++++++++++++++++++++++++++ src/libsysprof/sysprof-user-sampler.h | 43 ++ src/libsysprof/sysprof.h | 1 + 4 files changed, 621 insertions(+) create mode 100644 src/libsysprof/sysprof-user-sampler.c create mode 100644 src/libsysprof/sysprof-user-sampler.h diff --git a/src/libsysprof/meson.build b/src/libsysprof/meson.build index f3fa0850..e49c3a37 100644 --- a/src/libsysprof/meson.build +++ b/src/libsysprof/meson.build @@ -63,6 +63,7 @@ libsysprof_public_sources = [ 'sysprof-time-span.c', 'sysprof-tracefd-consumer.c', 'sysprof-tracer.c', + 'sysprof-user-sampler.c', ] libsysprof_public_headers = [ @@ -130,6 +131,7 @@ libsysprof_public_headers = [ 'sysprof-time-span.h', 'sysprof-tracefd-consumer.h', 'sysprof-tracer.h', + 'sysprof-user-sampler.h', ] libsysprof_private_sources = [ @@ -154,6 +156,11 @@ libsysprof_private_sources = [ 'sysprof-strings.c', 'sysprof-symbol-cache.c', 'timsort/gtktimsort.c', + + gnome.gdbus_codegen('ipc-unwinder', + sources: '../sysprofd/org.gnome.Sysprof3.Unwinder.xml', + interface_prefix: 'org.gnome.Sysprof3.', + namespace: 'Ipc'), ] if debuginfod_dep.found() diff --git a/src/libsysprof/sysprof-user-sampler.c b/src/libsysprof/sysprof-user-sampler.c new file mode 100644 index 00000000..0e3afeae --- /dev/null +++ b/src/libsysprof/sysprof-user-sampler.c @@ -0,0 +1,570 @@ +/* sysprof-user-sampler.c + * + * Copyright 2023 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include + +#include + +#include "sysprof-instrument-private.h" +#include "sysprof-perf-event-stream-private.h" +#include "sysprof-recording-private.h" +#include "sysprof-user-sampler.h" +#include "sysprof-muxer-source.h" + +#include "ipc-unwinder.h" + +/* The following was provided to Sysprof by Serhei Makarov as part + * of the eu-stacktrace prototype work. + */ +#ifdef _ASM_X86_PERF_REGS_H +/* #define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK -- error on x86_64 due to including segment regs*/ +#define REG(R) (1ULL << PERF_REG_X86_ ## R) +#define DWARF_NEEDED_REGS (/* no FLAGS */ REG(IP) | REG(SP) | REG(AX) | REG(CX) | REG(DX) | REG(BX) | REG(SI) | REG(DI) | REG(SP) | REG(BP) | /* no segment regs */ REG(R8) | REG(R9) | REG(R10) | REG(R11) | REG(R12) | REG(R13) | REG(R14) | REG(R15)) +/* XXX register ordering is defined in linux arch/x86/include/uapi/asm/perf_regs.h; + see code in tools/perf/util/intel-pt.c intel_pt_add_gp_regs() + and note how registers are added in the same order as the perf_regs.h enum */ +#define SYSPROF_ARCH_PREFERRED_REGS DWARF_NEEDED_REGS +/* TODO: add other architectures, imitating the linux tools/perf tree */ +#else +# define SYSPROF_ARCH_PREFERRED_REGS PERF_REG_EXTENDED_MASK +#endif /* _ASM_{arch}_PERF_REGS_H */ + +#define N_WAKEUP_EVENTS 149 + +struct _SysprofUserSampler +{ + SysprofInstrument parent_instance; + GArray *perf_fds; + int capture_fd; + int event_fd; + guint stack_size; +}; + +struct _SysprofUserSamplerClass +{ + SysprofInstrumentClass parent_class; +}; + +G_DEFINE_FINAL_TYPE (SysprofUserSampler, sysprof_user_sampler, SYSPROF_TYPE_INSTRUMENT) + +static void +close_fd (gpointer data) +{ + int *fdp = data; + + if (*fdp != -1) + { + close (*fdp); + *fdp = -1; + } +} + +static void +sysprof_user_sampler_ioctl (SysprofUserSampler *self, + gboolean enable) +{ + for (guint i = 0; i < self->perf_fds->len; i++) + { + int perf_fd = g_array_index (self->perf_fds, int, i); + + if (0 != ioctl (perf_fd, enable ? PERF_EVENT_IOC_ENABLE : PERF_EVENT_IOC_DISABLE)) + { + int errsv = errno; + g_warning ("Failed to toggle perf_fd: %s", g_strerror (errsv)); + } + } +} + +static char ** +sysprof_user_sampler_list_required_policy (SysprofInstrument *instrument) +{ + static const char *policy[] = {"org.gnome.sysprof3.profile", NULL}; + + return g_strdupv ((char **)policy); +} + +typedef struct _Prepare +{ + SysprofRecording *recording; + SysprofUserSampler *sampler; + guint stack_size; +} Prepare; + +static void +prepare_free (Prepare *prepare) +{ + g_clear_object (&prepare->recording); + g_clear_object (&prepare->sampler); + g_free (prepare); +} + +static void +_perf_event_open_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + GDBusConnection *connection = (GDBusConnection *)object; + g_autoptr(DexPromise) promise = user_data; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GVariant) ret = NULL; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_DBUS_CONNECTION (connection)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (DEX_IS_PROMISE (promise)); + + if ((ret = g_dbus_connection_call_with_unix_fd_list_finish (connection, &fd_list, result, &error))) + { + g_autofd int fd = -1; + int handle; + + g_variant_get (ret, "(h)", &handle); + + if (-1 == (fd = g_unix_fd_list_get (fd_list, handle, &error))) + goto failure; + + dex_promise_resolve_fd (promise, g_steal_fd (&fd)); + return; + } + +failure: + dex_promise_reject (promise, g_steal_pointer (&error)); +} + +static int +_perf_event_open (GDBusConnection *connection, + int cpu, + guint stack_size, + GError **error) +{ + g_autoptr(DexPromise) promise = NULL; + g_autoptr(GVariant) options = NULL; + g_autofd int perf_fd = -1; + struct perf_event_attr attr = {0}; + gboolean with_mmap2 = TRUE; + gboolean use_software = FALSE; + + g_assert (G_IS_DBUS_CONNECTION (connection)); + +try_again: + attr.sample_type = PERF_SAMPLE_IP + | PERF_SAMPLE_TID + | PERF_SAMPLE_IDENTIFIER + | PERF_SAMPLE_CALLCHAIN + | PERF_SAMPLE_STACK_USER + | PERF_SAMPLE_REGS_USER + | PERF_SAMPLE_TIME; + attr.wakeup_events = N_WAKEUP_EVENTS; + attr.disabled = TRUE; + attr.mmap = TRUE; + attr.mmap2 = with_mmap2; + attr.comm = 1; + attr.task = 1; + attr.exclude_idle = 1; + attr.sample_id_all = 1; + +#ifdef HAVE_PERF_CLOCKID + attr.clockid = sysprof_clock; + attr.use_clockid = 1; +#endif + + attr.sample_stack_user = stack_size; + attr.sample_regs_user = SYSPROF_ARCH_PREFERRED_REGS; + + attr.size = sizeof attr; + + if (use_software) + { + attr.type = PERF_TYPE_SOFTWARE; + attr.config = PERF_COUNT_SW_CPU_CLOCK; + attr.sample_period = 1000000; + } + else + { + attr.type = PERF_TYPE_HARDWARE; + attr.config = PERF_COUNT_HW_CPU_CYCLES; + attr.sample_period = 1200000; + } + + options = _sysprof_perf_event_attr_to_variant (&attr); + promise = dex_promise_new (); + + g_dbus_connection_call_with_unix_fd_list (connection, + "org.gnome.Sysprof3", + "/org/gnome/Sysprof3", + "org.gnome.Sysprof3.Service", + "PerfEventOpen", + g_variant_new ("(@a{sv}iiht)", + options, + -1, + cpu, + -1, + 0), + G_VARIANT_TYPE ("(h)"), + G_DBUS_CALL_FLAGS_NONE, + G_MAXUINT, + NULL, + NULL, + _perf_event_open_cb, + dex_ref (promise)); + + if (-1 == (perf_fd = dex_await_fd (dex_ref (promise), error))) + { + g_clear_pointer (&options, g_variant_unref); + + if (with_mmap2) + { + with_mmap2 = FALSE; + goto try_again; + } + + if (use_software == FALSE) + { + with_mmap2 = TRUE; + use_software = TRUE; + goto try_again; + } + + return -1; + } + + return g_steal_fd (&perf_fd); +} + +static void +call_unwind_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(DexPromise) promise = user_data; + g_autoptr(GUnixFDList) out_fd_list = NULL; + g_autoptr(GVariant) out_capture_fd = NULL; + g_autofd int capture_fd = -1; + GError *error = NULL; + + g_assert (IPC_IS_UNWINDER (object)); + g_assert (G_IS_ASYNC_RESULT (result)); + g_assert (DEX_IS_PROMISE (promise)); + + if (ipc_unwinder_call_unwind_finish (IPC_UNWINDER (object), &out_capture_fd, &out_fd_list, result, &error) && + -1 != (capture_fd = g_unix_fd_list_get (out_fd_list, g_variant_get_handle (out_capture_fd), &error))) + dex_promise_resolve_fd (promise, g_steal_fd (&capture_fd)); + else + dex_promise_reject (promise, error); +} + +static void +create_unwinder_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + g_autoptr(DexPromise) promise = user_data; + IpcUnwinder *unwinder; + GError *error = NULL; + + if ((unwinder = ipc_unwinder_proxy_new_finish (result, &error))) + dex_promise_resolve_object (promise, unwinder); + else + dex_promise_reject (promise, error); +} + +static IpcUnwinder * +create_unwinder (GDBusConnection *connection, + GError **error) +{ + g_autoptr(DexPromise) promise = dex_promise_new (); + ipc_unwinder_proxy_new (connection, G_DBUS_PROXY_FLAGS_NONE, + "org.gnome.Sysprof3", + "/org/gnome/Sysprof3/Unwinder", + NULL, + create_unwinder_cb, + dex_ref (promise)); + return dex_await_object (dex_ref (promise), error); +} + +static DexFuture * +sysprof_user_sampler_prepare_fiber (gpointer user_data) +{ + Prepare *prepare = user_data; + g_autoptr(GDBusConnection) connection = NULL; + g_autoptr(GUnixFDList) fd_list = NULL; + g_autoptr(GError) error = NULL; + GVariantBuilder builder; + gboolean all_failed = TRUE; + guint n_cpu; + + g_assert (prepare != NULL); + g_assert (SYSPROF_IS_RECORDING (prepare->recording)); + g_assert (SYSPROF_IS_USER_SAMPLER (prepare->sampler)); + + if (!(connection = dex_await_object (dex_bus_get (G_BUS_TYPE_SYSTEM), &error))) + return dex_future_new_for_error (g_steal_pointer (&error)); + + if (!dex_await (_sysprof_recording_add_file (prepare->recording, + "/proc/kallsyms", + TRUE), + &error)) + { + _sysprof_recording_diagnostic (prepare->recording, + "Sampler", + "Failed to record copy of “kallsyms” to capture: %s", + error->message); + g_clear_error (&error); + } + + n_cpu = g_get_num_processors (); + fd_list = g_unix_fd_list_new (); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(hi)")); + + for (guint i = 0; i < n_cpu; i++) + { + g_autofd int fd = _perf_event_open (connection, i, prepare->stack_size, &error); + + if (fd == -1) + { + _sysprof_recording_diagnostic (prepare->recording, + "Sampler", + "Failed to load Perf event stream for CPU %d with stack size %u: %s", + i, prepare->stack_size, error->message); + g_clear_error (&error); + } + else + { + int handle = g_unix_fd_list_append (fd_list, fd, &error); + + if (handle == -1) + { + _sysprof_recording_diagnostic (prepare->recording, + "Sampler", + "Out of FDs to add to FDList: %s", + error->message); + g_clear_error (&error); + } + else + { + g_array_append_val (prepare->sampler->perf_fds, fd); + fd = -1; + + g_variant_builder_add (&builder, "(hi)", handle, i); + + all_failed = FALSE; + } + } + } + + if (!all_failed) + { + g_autoptr(IpcUnwinder) unwinder = create_unwinder (connection, &error); + + if (unwinder == NULL) + { + _sysprof_recording_diagnostic (prepare->recording, + "Sampler", + "Failed to locate unwinder service: %s", + error->message); + g_clear_error (&error); + } + else + { + g_autoptr(DexPromise) promise = dex_promise_new (); + int event_fd_handle = g_unix_fd_list_append (fd_list, prepare->sampler->event_fd, NULL); + g_autofd int fd = -1; + + ipc_unwinder_call_unwind (unwinder, + prepare->stack_size, + g_variant_builder_end (&builder), + g_variant_new_handle (event_fd_handle), + fd_list, + NULL, + call_unwind_cb, + dex_ref (promise)); + + fd = dex_await_fd (dex_ref (promise), &error); + + if (fd == -1) + { + _sysprof_recording_diagnostic (prepare->recording, + "Sampler", + "Failed to setup user-space unwinder: %s", + error->message); + g_clear_error (&error); + } + else + { + prepare->sampler->capture_fd = g_steal_fd (&fd); + } + } + } + + g_variant_builder_clear (&builder); + + return dex_future_new_for_boolean (TRUE); +} + +static DexFuture * +sysprof_user_sampler_prepare (SysprofInstrument *instrument, + SysprofRecording *recording) +{ + SysprofUserSampler *self = (SysprofUserSampler *)instrument; + Prepare *prepare; + + g_assert (SYSPROF_IS_INSTRUMENT (instrument)); + g_assert (SYSPROF_IS_RECORDING (recording)); + + prepare = g_new0 (Prepare, 1); + prepare->recording = g_object_ref (recording); + prepare->sampler = g_object_ref (self); + prepare->stack_size = self->stack_size; + + return dex_scheduler_spawn (NULL, 0, + sysprof_user_sampler_prepare_fiber, + prepare, + (GDestroyNotify)prepare_free); +} + +typedef struct _Record +{ + SysprofRecording *recording; + SysprofUserSampler *sampler; + DexFuture *cancellable; +} Record; + +static void +record_free (Record *record) +{ + g_clear_object (&record->recording); + g_clear_object (&record->sampler); + dex_clear (&record->cancellable); + g_free (record); +} + +static DexFuture * +sysprof_user_sampler_record_fiber (gpointer user_data) +{ + SysprofCaptureWriter *writer; + Record *record = user_data; + g_autoptr(GError) error = NULL; + g_autoptr(GSource) muxer_source = NULL; + guint64 exiting = 1234; + + g_assert (record != NULL); + g_assert (SYSPROF_IS_USER_SAMPLER (record->sampler)); + g_assert (SYSPROF_IS_RECORDING (record->recording)); + g_assert (DEX_IS_FUTURE (record->cancellable)); + + writer = _sysprof_recording_writer (record->recording); + + sysprof_user_sampler_ioctl (record->sampler, TRUE); + + g_debug ("Staring muxer for capture_fd"); + muxer_source = sysprof_muxer_source_new (g_steal_fd (&record->sampler->capture_fd), writer); + g_source_set_static_name (muxer_source, "[stack-muxer]"); + g_source_attach (muxer_source, NULL); + + if (!dex_await (dex_ref (record->cancellable), &error)) + g_debug ("UserSampler shutting down for reason: %s", error->message); + + write (record->sampler->event_fd, &exiting, sizeof exiting); + + g_source_destroy (muxer_source); + + sysprof_user_sampler_ioctl (record->sampler, FALSE); + + return dex_future_new_for_boolean (TRUE); +} + +static DexFuture * +sysprof_user_sampler_record (SysprofInstrument *instrument, + SysprofRecording *recording, + GCancellable *cancellable) +{ + SysprofUserSampler *self = (SysprofUserSampler *)instrument; + Record *record; + + g_assert (SYSPROF_IS_INSTRUMENT (instrument)); + g_assert (SYSPROF_IS_RECORDING (recording)); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + record = g_new0 (Record, 1); + record->recording = g_object_ref (recording); + record->sampler = g_object_ref (self); + record->cancellable = dex_cancellable_new_from_cancellable (cancellable); + + return dex_scheduler_spawn (NULL, 0, + sysprof_user_sampler_record_fiber, + record, + (GDestroyNotify)record_free); +} + +static void +sysprof_user_sampler_finalize (GObject *object) +{ + SysprofUserSampler *self = (SysprofUserSampler *)object; + + g_clear_pointer (&self->perf_fds, g_array_unref); + + g_clear_fd (&self->capture_fd, NULL); + g_clear_fd (&self->event_fd, NULL); + + G_OBJECT_CLASS (sysprof_user_sampler_parent_class)->finalize (object); +} + +static void +sysprof_user_sampler_class_init (SysprofUserSamplerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + SysprofInstrumentClass *instrument_class = SYSPROF_INSTRUMENT_CLASS (klass); + + object_class->finalize = sysprof_user_sampler_finalize; + + instrument_class->list_required_policy = sysprof_user_sampler_list_required_policy; + instrument_class->prepare = sysprof_user_sampler_prepare; + instrument_class->record = sysprof_user_sampler_record; +} + +static void +sysprof_user_sampler_init (SysprofUserSampler *self) +{ + self->capture_fd = -1; + self->event_fd = eventfd (0, EFD_CLOEXEC); + + self->perf_fds = g_array_new (FALSE, FALSE, sizeof (int)); + g_array_set_clear_func (self->perf_fds, close_fd); +} + +SysprofInstrument * +sysprof_user_sampler_new (guint stack_size) +{ + SysprofUserSampler *self; + + g_return_val_if_fail (stack_size > 0, NULL); + g_return_val_if_fail (stack_size % sysprof_getpagesize () == 0, NULL); + + self = g_object_new (SYSPROF_TYPE_USER_SAMPLER, NULL); + self->stack_size = stack_size; + + return SYSPROF_INSTRUMENT (self); +} diff --git a/src/libsysprof/sysprof-user-sampler.h b/src/libsysprof/sysprof-user-sampler.h new file mode 100644 index 00000000..2d9e56b5 --- /dev/null +++ b/src/libsysprof/sysprof-user-sampler.h @@ -0,0 +1,43 @@ +/* + * sysprof-user-sampler.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "sysprof-instrument.h" + +G_BEGIN_DECLS + +#define SYSPROF_TYPE_USER_SAMPLER (sysprof_user_sampler_get_type()) +#define SYSPROF_IS_USER_SAMPLER(obj) G_TYPE_CHECK_INSTANCE_TYPE(obj, SYSPROF_TYPE_USER_SAMPLER) +#define SYSPROF_USER_SAMPLER(obj) G_TYPE_CHECK_INSTANCE_CAST(obj, SYSPROF_TYPE_USER_SAMPLER, SysprofUserSampler) +#define SYSPROF_USER_SAMPLER_CLASS(klass) G_TYPE_CHECK_CLASS_CAST(klass, SYSPROF_TYPE_USER_SAMPLER, SysprofUserSamplerClass) + +typedef struct _SysprofUserSampler SysprofUserSampler; +typedef struct _SysprofUserSamplerClass SysprofUserSamplerClass; + +SYSPROF_AVAILABLE_IN_48 +GType sysprof_user_sampler_get_type (void) G_GNUC_CONST; +SYSPROF_AVAILABLE_IN_48 +SysprofInstrument *sysprof_user_sampler_new (guint stack_size); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofUserSampler, g_object_unref) + +G_END_DECLS diff --git a/src/libsysprof/sysprof.h b/src/libsysprof/sysprof.h index d30f9fd4..514e332b 100644 --- a/src/libsysprof/sysprof.h +++ b/src/libsysprof/sysprof.h @@ -89,6 +89,7 @@ G_BEGIN_DECLS # include "sysprof-time-span.h" # include "sysprof-tracefd-consumer.h" # include "sysprof-tracer.h" +# include "sysprof-user-sampler.h" #undef SYSPROF_INSIDE G_END_DECLS From c3cb2f71bc73e3f4e9dc176261ee4dc187a7635c Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:54:57 -0800 Subject: [PATCH 5/6] sysprof-cli: add support for live unwinding This allows you to specify --stack-size=(multiple_of_page_size) to unwind from captured stack contents. It will use the new SysprofUserSampler to unwind stack traces via sysprof-live-unwinder. --- src/sysprof-cli/sysprof-cli.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/sysprof-cli/sysprof-cli.c b/src/sysprof-cli/sysprof-cli.c index e00bd6b1..1c9ce928 100644 --- a/src/sysprof-cli/sysprof-cli.c +++ b/src/sysprof-cli/sysprof-cli.c @@ -303,6 +303,7 @@ main (int argc, gboolean scheduler_details = FALSE; gboolean system_bus = FALSE; gboolean session_bus = FALSE; + int stack_size = 0; int pid = -1; int fd; int flags; @@ -335,6 +336,7 @@ main (int argc, { "version", 0, 0, G_OPTION_ARG_NONE, &version, N_("Print the sysprof-cli version and exit") }, { "buffer-size", 0, 0, G_OPTION_ARG_INT, &n_buffer_pages, N_("The size of the buffer in pages (1 = 1 page)") }, { "monitor-bus", 0, 0, G_OPTION_ARG_STRING_ARRAY, &monitor_bus, N_("Additional D-Bus address to monitor") }, + { "stack-size", 0, 0, G_OPTION_ARG_INT, &stack_size, N_("Stack size to copy for unwinding in user-space") }, { NULL } }; @@ -379,6 +381,10 @@ Examples:\n\ \n\ # Merge multiple syscap files into one\n\ sysprof-cli --merge a.syscap b.syscap > c.syscap\n\ +\n\ + # Unwind by capturing stack/register contents instead of frame-pointers\n\ + # where the stack-size is a multiple of page-size\n\ + sysprof-cli --stack-size=8192\n\ ")); if (!g_option_context_parse (context, &argc, &argv, &error)) @@ -533,7 +539,12 @@ Examples:\n\ sysprof_profiler_add_instrument (profiler, sysprof_system_logs_new ()); if (!no_perf) - sysprof_profiler_add_instrument (profiler, sysprof_sampler_new ()); + { + if (stack_size == 0) + sysprof_profiler_add_instrument (profiler, sysprof_sampler_new ()); + else + sysprof_profiler_add_instrument (profiler, sysprof_user_sampler_new (stack_size)); + } if (!no_disk) sysprof_profiler_add_instrument (profiler, sysprof_disk_usage_new ()); From f2b2dcf29de96bbe14b7c2449b02ba56278c774c Mon Sep 17 00:00:00 2001 From: Christian Hergert Date: Sun, 3 Nov 2024 10:57:58 -0800 Subject: [PATCH 6/6] sysprof: add UI for live unwinding This adds UI to specify the amount of stack contents to copy along with the CPU registers so that you may unwind in user-space. --- src/sysprof/meson.build | 1 + src/sysprof/sysprof-greeter.c | 32 +++++ src/sysprof/sysprof-greeter.ui | 67 +++++++++++ src/sysprof/sysprof-recording-template.c | 41 ++++++- src/sysprof/sysprof-stack-size.c | 141 +++++++++++++++++++++++ src/sysprof/sysprof-stack-size.h | 35 ++++++ 6 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 src/sysprof/sysprof-stack-size.c create mode 100644 src/sysprof/sysprof-stack-size.h diff --git a/src/sysprof/meson.build b/src/sysprof/meson.build index ca42ead3..ab6a701a 100644 --- a/src/sysprof/meson.build +++ b/src/sysprof/meson.build @@ -57,6 +57,7 @@ sysprof_sources = [ 'sysprof-sidebar.c', 'sysprof-single-model.c', 'sysprof-split-layer.c', + 'sysprof-stack-size.c', 'sysprof-storage-section.c', 'sysprof-symbol-label.c', 'sysprof-task-row.c', diff --git a/src/sysprof/sysprof-greeter.c b/src/sysprof/sysprof-greeter.c index 72f4dd5d..52eff370 100644 --- a/src/sysprof/sysprof-greeter.c +++ b/src/sysprof/sysprof-greeter.c @@ -31,6 +31,7 @@ #include "sysprof-power-profiles.h" #include "sysprof-recording-pad.h" #include "sysprof-recording-template.h" +#include "sysprof-stack-size.h" #include "sysprof-window.h" G_DEFINE_AUTOPTR_CLEANUP_FUNC (SysprofCaptureWriter, sysprof_capture_writer_unref) @@ -56,6 +57,7 @@ struct _SysprofGreeter GtkSwitch *bundle_symbols; GtkButton *record_to_memory; AdwComboRow *power_combo; + AdwComboRow *sample_user_stack_size; SysprofRecordingTemplate *recording_template; }; @@ -455,6 +457,26 @@ translate_power_profile (GtkStringObject *strobj) return g_strdup (str); } +static void +on_stack_size_changed_cb (SysprofGreeter *self, + GParamSpec *pspec, + AdwComboRow *row) +{ + GObject *item; + + g_assert (SYSPROF_IS_GREETER (self)); + g_assert (ADW_IS_COMBO_ROW (row)); + + if ((item = adw_combo_row_get_selected_item (row))) + { + guint stack_size = sysprof_stack_size_get_size (SYSPROF_STACK_SIZE (item)); + + g_object_set (self->recording_template, + "stack-size", stack_size, + NULL); + } +} + static void sysprof_greeter_dispose (GObject *object) { @@ -492,6 +514,7 @@ sysprof_greeter_class_init (SysprofGreeterClass *klass) gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, recording_template); gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, sample_javascript_stacks); gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, sample_native_stacks); + gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, sample_user_stack_size); gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, sidebar_list_box); gtk_widget_class_bind_template_child (widget_class, SysprofGreeter, view_stack); @@ -507,6 +530,7 @@ sysprof_greeter_class_init (SysprofGreeterClass *klass) g_type_ensure (SYSPROF_TYPE_ENTRY_POPOVER); g_type_ensure (SYSPROF_TYPE_RECORDING_TEMPLATE); + g_type_ensure (SYSPROF_TYPE_STACK_SIZE); } static void @@ -540,6 +564,14 @@ sysprof_greeter_init (SysprofGreeter *self) gtk_list_box_select_row (self->sidebar_list_box, row); sidebar_row_activated_cb (self, row, self->sidebar_list_box); + g_signal_connect_object (self->sample_user_stack_size, + "notify::selected-item", + G_CALLBACK (on_stack_size_changed_cb), + self, + G_CONNECT_SWAPPED); + /* Set to 16KB */ + adw_combo_row_set_selected (self->sample_user_stack_size, 1); + gtk_widget_grab_focus (GTK_WIDGET (self->record_to_memory)); } diff --git a/src/sysprof/sysprof-greeter.ui b/src/sysprof/sysprof-greeter.ui index 35e790f5..f7ebbc29 100644 --- a/src/sysprof/sysprof-greeter.ui +++ b/src/sysprof/sysprof-greeter.ui @@ -106,6 +106,41 @@ + + + Unwind Stacks in User Space + Copy stack contents and registers for unwinding in user-space + + + + end + center + + + + + + Stack Size + The number of bytes to copy from the stack + stack_sizes + + + + + + + + + + Unwinding in user-space has considerable overhead but may help in situations where frame-pointers are unavailable. + 0 + 8 + + + @@ -664,4 +699,36 @@ + + + + 8 KB + 8192 + + + + + 16 KB + 16384 + + + + + 24 KB + 24576 + + + + + 32 KB + 32768 + + + + + 64 KB + 65536 + + + diff --git a/src/sysprof/sysprof-recording-template.c b/src/sysprof/sysprof-recording-template.c index 50f6c957..ee4d4f01 100644 --- a/src/sysprof/sysprof-recording-template.c +++ b/src/sysprof/sysprof-recording-template.c @@ -22,6 +22,8 @@ #include "sysprof-recording-template.h" +#define DEFAULT_STACK_SIZE (4096*4) + struct _SysprofRecordingTemplate { GObject parent_instance; @@ -31,6 +33,8 @@ struct _SysprofRecordingTemplate char *power_profile; char **environ; + guint stack_size; + guint battery_charge : 1; guint bundle_symbols : 1; guint clear_environ : 1; @@ -49,6 +53,7 @@ struct _SysprofRecordingTemplate guint session_bus : 1; guint system_bus : 1; guint system_log : 1; + guint user_stacks : 1; }; enum { @@ -73,8 +78,10 @@ enum { PROP_POWER_PROFILE, PROP_SCHEDULER_DETAILS, PROP_SESSION_BUS, + PROP_STACK_SIZE, PROP_SYSTEM_BUS, PROP_SYSTEM_LOG, + PROP_USER_STACKS, N_PROPS }; @@ -185,6 +192,10 @@ sysprof_recording_template_get_property (GObject *object, g_value_set_boolean (value, self->session_bus); break; + case PROP_STACK_SIZE: + g_value_set_uint (value, self->stack_size); + break; + case PROP_SYSTEM_BUS: g_value_set_boolean (value, self->system_bus); break; @@ -193,6 +204,10 @@ sysprof_recording_template_get_property (GObject *object, g_value_set_boolean (value, self->system_log); break; + case PROP_USER_STACKS: + g_value_set_boolean (value, self->user_stacks); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -289,6 +304,10 @@ sysprof_recording_template_set_property (GObject *object, self->session_bus = g_value_get_boolean (value); break; + case PROP_STACK_SIZE: + self->stack_size = g_value_get_uint (value); + break; + case PROP_SYSTEM_BUS: self->system_bus = g_value_get_boolean (value); break; @@ -297,6 +316,10 @@ sysprof_recording_template_set_property (GObject *object, self->system_log = g_value_get_boolean (value); break; + case PROP_USER_STACKS: + self->user_stacks = g_value_get_boolean (value); + break; + default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } @@ -421,6 +444,16 @@ sysprof_recording_template_class_init (SysprofRecordingTemplateClass *klass) TRUE, (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + properties[PROP_USER_STACKS] = + g_param_spec_boolean ("user-stacks", NULL, NULL, + FALSE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_STACK_SIZE] = + g_param_spec_uint ("stack-size", NULL, NULL, + 0, G_MAXUINT, DEFAULT_STACK_SIZE, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_properties (object_class, N_PROPS, properties); } @@ -439,6 +472,7 @@ sysprof_recording_template_init (SysprofRecordingTemplate *self) self->system_log = TRUE; self->command_line = g_strdup (""); self->cwd = g_strdup(""); + self->stack_size = DEFAULT_STACK_SIZE; } SysprofRecordingTemplate * @@ -619,7 +653,12 @@ sysprof_recording_template_apply (SysprofRecordingTemplate *self, sysprof_profiler_add_instrument (profiler, sysprof_memory_usage_new ()); if (self->native_stacks) - sysprof_profiler_add_instrument (profiler, sysprof_sampler_new ()); + { + if (self->user_stacks) + sysprof_profiler_add_instrument (profiler, sysprof_user_sampler_new (self->stack_size)); + else + sysprof_profiler_add_instrument (profiler, sysprof_sampler_new ()); + } if (self->network_usage) sysprof_profiler_add_instrument (profiler, sysprof_network_usage_new ()); diff --git a/src/sysprof/sysprof-stack-size.c b/src/sysprof/sysprof-stack-size.c new file mode 100644 index 00000000..d6652766 --- /dev/null +++ b/src/sysprof/sysprof-stack-size.c @@ -0,0 +1,141 @@ +/* + * sysprof-stack-size.c + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include "sysprof-stack-size.h" + +struct _SysprofStackSize +{ + GObject parent_instance; + char *label; + guint size; +}; + +enum { + PROP_0, + PROP_SIZE, + PROP_LABEL, + N_PROPS +}; + +G_DEFINE_FINAL_TYPE (SysprofStackSize, sysprof_stack_size, G_TYPE_OBJECT) + +static GParamSpec *properties[N_PROPS]; + +static void +sysprof_stack_size_finalize (GObject *object) +{ + SysprofStackSize *self = (SysprofStackSize *)object; + + g_clear_pointer (&self->label, g_free); + + G_OBJECT_CLASS (sysprof_stack_size_parent_class)->finalize (object); +} + +static void +sysprof_stack_size_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + SysprofStackSize *self = SYSPROF_STACK_SIZE (object); + + switch (prop_id) + { + case PROP_SIZE: + g_value_set_uint (value, self->size); + break; + + case PROP_LABEL: + g_value_set_string (value, self->label); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +sysprof_stack_size_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + SysprofStackSize *self = SYSPROF_STACK_SIZE (object); + + switch (prop_id) + { + case PROP_SIZE: + self->size = g_value_get_uint (value); + break; + + case PROP_LABEL: + g_set_str (&self->label, g_value_get_string (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +sysprof_stack_size_class_init (SysprofStackSizeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = sysprof_stack_size_finalize; + object_class->get_property = sysprof_stack_size_get_property; + object_class->set_property = sysprof_stack_size_set_property; + + properties[PROP_SIZE] = + g_param_spec_uint ("size", NULL, NULL, + 0, (4096*32), 0, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + properties[PROP_LABEL] = + g_param_spec_string ("label", NULL, NULL, + NULL, + (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +sysprof_stack_size_init (SysprofStackSize *self) +{ +} + +guint +sysprof_stack_size_get_size (SysprofStackSize *self) +{ + g_return_val_if_fail (SYSPROF_IS_STACK_SIZE (self), 0); + + return self->size; +} + +const char * +sysprof_stack_size_get_label (SysprofStackSize *self) +{ + g_return_val_if_fail (SYSPROF_IS_STACK_SIZE (self), NULL); + + return self->label; +} diff --git a/src/sysprof/sysprof-stack-size.h b/src/sysprof/sysprof-stack-size.h new file mode 100644 index 00000000..5dd7f9c2 --- /dev/null +++ b/src/sysprof/sysprof-stack-size.h @@ -0,0 +1,35 @@ +/* + * sysprof-stack-size.h + * + * Copyright 2024 Christian Hergert + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define SYSPROF_TYPE_STACK_SIZE (sysprof_stack_size_get_type()) + +G_DECLARE_FINAL_TYPE (SysprofStackSize, sysprof_stack_size, SYSPROF, STACK_SIZE, GObject) + +guint sysprof_stack_size_get_size (SysprofStackSize *self); +const char *sysprof_stack_size_get_label (SysprofStackSize *self); + +G_END_DECLS