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/libsysprof/meson.build b/src/libsysprof/meson.build index 549042de..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 = [ @@ -147,12 +149,18 @@ 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', '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-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 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/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 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-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 ()); 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/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 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) 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++) {