[RFC PATCH 1/2] perf inject: Support piped data with --convert-callchain

From: Serhei Makarov

Date: Fri Sep 25 2026 - 17:03:19 EST


When profiling applications without framepointers or non-framepointer
distros, it's helpful to pipe stack sample data directly to perf
inject --convert-callchain, e.g.:

perf record -F 999 --call-graph dwarf -o - -- myapp \
| perf inject --convert-callchain -i - -o perf.data

This piped setup doesn't store stack samples on disk, removing a major
performance bottleneck for DWARF stack unwinding as noted in [1].

To support piped output, write a modified copy of the event attributes
in perf_event__repipe_attr. This gets around the inability to modify
event attribute records in-place after they are written to the pipe.

To support piped input, perform a more-exact sample size calculation
to perf_event__convert_sample_callchain.

[1]: https://rwmj.wordpress.com/2023/02/14/frame-pointers-vs-dwarf-my-verdict/
> The first most obvious thing is that even with the smallest stack
> data collection, DWARF’s perf.data is over 10 times larger, and it
> balloons even larger once you start to collect more reasonable stack
> sizes. For a single minute of data collection, collecting 10s of
> gigabytes of data is not very practical even on high end machines, and
> continuous performance analysis would be impossible at these data
> rates.

Signed-off-by: Serhei Makarov <serhei@xxxxxxxxx>
---
tools/perf/builtin-inject.c | 88 ++++++++++++++++++++++++++-----------
1 file changed, 63 insertions(+), 25 deletions(-)

diff --git a/tools/perf/builtin-inject.c b/tools/perf/builtin-inject.c
index f174bc69cec4..29b22b52a631 100644
--- a/tools/perf/builtin-inject.c
+++ b/tools/perf/builtin-inject.c
@@ -219,6 +219,7 @@ static int perf_event__repipe_attr(const struct perf_tool *tool,
union perf_event *event,
struct evlist **pevlist)
{
+ union perf_event *event2;
struct perf_inject *inject = container_of(tool, struct perf_inject,
tool);
int ret;
@@ -231,7 +232,28 @@ static int perf_event__repipe_attr(const struct perf_tool *tool,
if (!inject->output.is_pipe)
return 0;

- return perf_event__repipe_synth(tool, event);
+ /* We can repipe the original event in most cases: */
+ event2 = event;
+
+ if (inject->convert_callchain) {
+ /* Copy event to repipe with corrected final attributes,
+ without confusing downstream users of pevlist: */
+ event2 = (void *)inject->event_copy;
+ if (event2 == NULL) {
+ inject->event_copy = malloc(PERF_SAMPLE_MAX_SIZE);
+ if (!inject->event_copy)
+ return -ENOMEM;
+ event2 = (void *)inject->event_copy;
+ }
+ memcpy(event2, event, event->header.size);
+
+ event2->attr.attr.sample_type &= ~(PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER);
+ event2->attr.attr.sample_regs_user = 0;
+ event2->attr.attr.sample_stack_user = 0;
+ event2->attr.attr.exclude_callchain_user = 0;
+ }
+
+ return perf_event__repipe_synth(tool, event2);
}

static int perf_event__repipe_event_update(const struct perf_tool *tool,
@@ -384,6 +406,18 @@ static int perf_event__repipe_sample(const struct perf_tool *tool,
return perf_event__repipe_synth(tool, event);
}

+static bool evsel__has_dwarf_callchain(struct evsel *evsel)
+{
+ struct perf_event_attr *attr = &evsel->core.attr;
+ const u64 dwarf_callchain_flags =
+ PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER | PERF_SAMPLE_CALLCHAIN;
+
+ if (!attr->exclude_callchain_user)
+ return false;
+
+ return (attr->sample_type & dwarf_callchain_flags) == dwarf_callchain_flags;
+}
+
static int perf_event__convert_sample_callchain(const struct perf_tool *tool,
union perf_event *event,
struct perf_sample *sample,
@@ -391,15 +425,21 @@ static int perf_event__convert_sample_callchain(const struct perf_tool *tool,
struct machine *machine)
{
struct perf_inject *inject = container_of(tool, struct perf_inject, tool);
- struct callchain_cursor *cursor = get_tls_callchain_cursor();
+ struct callchain_cursor *cursor;
union perf_event *event_copy = (void *)inject->event_copy;
struct callchain_cursor_node *node;
struct thread *thread;
u64 sample_type = evsel->core.attr.sample_type;
u32 sample_size = event->header.size;
+ u64 prev_callchain_nr;
u64 i, k;
int ret;

+ if (!evsel__has_dwarf_callchain(evsel))
+ return perf_event__repipe(tool, event, sample, machine);
+ cursor = get_tls_callchain_cursor();
+ prev_callchain_nr = sample->callchain->nr;
+
if (event_copy == NULL) {
inject->event_copy = malloc(PERF_SAMPLE_MAX_SIZE);
if (!inject->event_copy)
@@ -455,8 +495,18 @@ static int perf_event__convert_sample_callchain(const struct perf_tool *tool,
memcpy(event_copy, event, sizeof(event->header));

/* adjust sample size for stack and regs */
- sample_size -= sample->user_stack.size;
- sample_size -= (hweight64(evsel->core.attr.sample_regs_user) + 1) * sizeof(u64);
+ {
+ /* need to get stack size from the raw event */
+ const u64 *raw_stack = (const u64 *)((void *)event + sample->user_stack.offset);
+ u64 raw_alloc = raw_stack[0];
+ sample_size -= sizeof(u64);
+ if (raw_alloc)
+ sample_size -= raw_alloc + sizeof(u64);
+ }
+ sample_size -= sizeof(u64); /* abi */
+ if (sample->user_regs && sample->user_regs->abi)
+ sample_size -= hweight64(evsel->core.attr.sample_regs_user) * sizeof(u64);
+ sample_size -= (prev_callchain_nr + 1) * sizeof(u64);
sample_size += (sample->callchain->nr + 1) * sizeof(u64);
event_copy->header.size = sample_size;

@@ -2482,18 +2532,6 @@ static int __cmd_inject(struct perf_inject *inject)
return ret;
}

-static bool evsel__has_dwarf_callchain(struct evsel *evsel)
-{
- struct perf_event_attr *attr = &evsel->core.attr;
- const u64 dwarf_callchain_flags =
- PERF_SAMPLE_STACK_USER | PERF_SAMPLE_REGS_USER | PERF_SAMPLE_CALLCHAIN;
-
- if (!attr->exclude_callchain_user)
- return false;
-
- return (attr->sample_type & dwarf_callchain_flags) == dwarf_callchain_flags;
-}
-
int cmd_inject(int argc, const char **argv)
{
struct perf_inject inject = {
@@ -2746,15 +2784,15 @@ int cmd_inject(int argc, const char **argv)
if (inject.convert_callchain) {
struct evsel *evsel;

- if (inject.output.is_pipe || inject.session->data->is_pipe) {
- pr_err("--convert-callchain cannot work with pipe\n");
- goto out_delete;
- }
-
- evlist__for_each_entry(inject.session->evlist, evsel) {
- if (!evsel__has_dwarf_callchain(evsel) && !evsel__is_dummy_event(evsel)) {
- pr_err("--convert-callchain requires DWARF call graph.\n");
- goto out_delete;
+ /* For on-disk data, check evlist up-front.
+ For piped data, evlist is not available yet;
+ check in perf_event__convert_sample_callchain. */
+ if (!inject.session->data->is_pipe) {
+ evlist__for_each_entry(inject.session->evlist, evsel) {
+ if (!evsel__has_dwarf_callchain(evsel) && !evsel__is_dummy_event(evsel)) {
+ pr_err("--convert-callchain requires DWARF call graph.\n");
+ goto out_delete;
+ }
}
}

--
2.55.0