From: Namhyung Kim <namhyung@kernel.org>
To: Aaron Tomlin <atomlin@atomlin.com>
Cc: peterz@infradead.org, mingo@redhat.com, acme@kernel.org,
mark.rutland@arm.com, alexander.shishkin@linux.intel.com,
jolsa@kernel.org, irogers@google.com, adrian.hunter@intel.com,
linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org
Subject: Re: [PATCH perf-tools-next v9 4/6] perf ftrace: Support display of inlined functions in function graph tracer
Date: Thu, 22 Oct 2026 14:53:54 -0700 [thread overview]
Message-ID: <atqF8rgKGnaH7EhQ@google.com> (raw)
In-Reply-To: <20261021023337.809936-5-atomlin@atomlin.com>
On Tue, Oct 20, 2026 at 10:33:35PM -0400, Aaron Tomlin wrote:
> The Linux kernel's function graph tracer operates at the machine
> instruction level via compiler instrumentation
> (-fpatchable-function-entry), recording entry and return events for
> physical function calls. Consequently, functions inlined by the compiler
> are invisible in the resulting call graph, as no discrete call or return
> instructions are emitted for them. While technically expected, this
> omission frequently obscures the logical execution flow when analysing
> kernel subsystems heavily reliant upon inlining.
>
> Address this by introducing the --inline option to perf ftrace for the
> function_graph tracer.
>
> When the kernel is configured with CONFIG_FUNCTION_GRAPH_RETADDR=y, the
> funcgraph-retaddr trace option records the caller's return address on
> each function entry, manifested in the trace stream as a comment
> (e.g. /* <-irq_work_queue_on+0x81/0xa0 */). By interrogating DWARF debug
> information from the kernel image (vmlinux) utilising libdw, perf ftrace
> resolves this return address to its inlined callchain and synthesises the
> intermediate inlined frames directly into the streamed call graph.
>
> Key aspects of this implementation:
>
> 1. Inlined frame presentation
>
> Synthesised inlined functions are rendered with an explicit
> /* (inline) */ annotation at both entry and exit:
>
> # tracer: function_graph
> #
> # CPU FUNCTION CALLS
> # | | | | |
> 6) activate_task() {
> 6) enqueue_task() {
> 6) enqueue_task_fair() {
> 6) update_curr_eevdf() { /* (inline) */
> 6) update_curr() {
> 6) update_se() {
> 6) cgroup_account_cputime() { /* (inline) */
> 6) cpuacct_charge();
> 6) } /* cgroup_account_cputime (inline) */
> 6) }
> 6) dl_server_update() {
> 6) update_curr_dl_se();
> 6) }
> 6) } /* update_curr_eevdf (inline) */
> 6) enqueue_hierarchy() { /* (inline) */
> 6) update_curr() {
> 6) update_se();
> 6) }
> 6) enqueue_entity() {
> 6) account_entity_enqueue() { /* (inline) */
> 6) account_llc_enqueue();
> 6) } /* account_entity_enqueue (inline) */
> 6) }
> 6) } /* enqueue_hierarchy (inline) */
> 6) place_entity() {
> 6) avg_vruntime();
> 6) }
> 6) __enqueue_entity();
> 6) dl_server_start();
> 6) hrtick_update() { /* (inline) */
> 6) hrtick_active() { /* (inline) */
> 6) hrtimer_active();
> 6) } /* hrtick_active (inline) */
> 6) } /* hrtick_update (inline) */
> 6) hrtick_start_fair();
> 6) psi_enqueue() { /* (inline) */
> 6) psi_task_change() {
> 6) psi_flags_change();
> 6) psi_group_change() {
> 6) record_times();
> 6) }
> 6) }
> 6) } /* psi_enqueue (inline) */
> 6) }
> 6) }
>
> This affords immediate visual clarity to the reader upon
> entering an inlined scope, without necessitating a search for
> the closing delimiter.
>
> 2. Call graph reconstruction and per-task stack tracking
>
> Maintains a per-task synthetic call stack tracking real and
> inlined frames across context switches, synthesising entry and
> exit delimiters to accurately reconstruct the execution
> hierarchy. Tasks are tracked in a hash table keyed by PID, with
> per-CPU active bindings updated upon context-switch banners or
> verbose line records. Inlined frames are structured strictly
> root-to-leaf and validated against the active stack to prevent
> un-traced callers from attaching orphan inlines. Bare closing
> braces without trailing function names (the default when
> funcgraph-tail is disabled) are correctly matched against
> preceding frames on the stack. Context column alignment,
> including task comm-pid delimiters in verbose mode, is preserved
> when generating prefixes for synthesised inline frames.
> Furthermore, stack depth saturation guards ensure that inlined
> frame depth tracking remains clamped, indentation cannot drift
> even under deep call trees exceeding FTRACE_MAX_STACK, and
> overflow depth is automatically recovered upon caller detection.
>
> 3. Option handling and diagnostics
>
> Adds -k or --vmlinux to designate an explicit debug image, and
> provides clear diagnostic guidance should --inline be invoked on
> a kernel lacking CONFIG_FUNCTION_GRAPH_RETADDR. Automatically
> enables return address comment filtering unless explicitly
> requested.
>
> 4. Automated verification
>
> Expands the unit test suite in tools/perf/tests/ftrace.c to
> validate DWARF inline symbol resolution, comment filtering
> toggles, bare closing braces handling, task context-switch
> isolation and migration, and stack saturation/overflow recovery.
>
> Assisted-by: Antigravity:gemini-3.1-pro
> Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
> ---
> tools/perf/Documentation/perf-ftrace.txt | 17 +-
> tools/perf/builtin-ftrace.c | 76 +-
> tools/perf/tests/ftrace.c | 914 ++++++++++++++++++-
> tools/perf/util/ftrace.c | 1059 +++++++++++++++++++++-
> tools/perf/util/ftrace.h | 14 +
> 5 files changed, 2059 insertions(+), 21 deletions(-)
I think it's too big and you can split into pieces like:
1. add the basic task stack management code
2. support context switch
3. support inline functions
4. add test code
Thanks,
Namhyung
>
> diff --git a/tools/perf/Documentation/perf-ftrace.txt b/tools/perf/Documentation/perf-ftrace.txt
> index e8105083e67c..445c8ccc22d2 100644
> --- a/tools/perf/Documentation/perf-ftrace.txt
> +++ b/tools/perf/Documentation/perf-ftrace.txt
> @@ -134,9 +134,24 @@ OPTIONS for 'perf ftrace trace'
> - depth=<n> - Set max depth for function graph tracer to follow.
> - tail - Print function name at the end.
>
> +--inline::
> + Show inlined functions in function_graph tracer. This requires
> + kernel support for the funcgraph-retaddr option and a vmlinux with
> + debug symbols. Inlined functions are displayed in the call graph
> + hierarchy, and their inlined status is indicated with `/* (inline) */`
> + on entry and exit, e.g. `func() { /* (inline) */` and `} /* func (inline) */`.
> + By default, raw return address comments (`/* <-caller+offset */`)
> + are hidden in the output; specify `--graph-opts retaddr` to show them.
> +
> --filter-retaddr::
> Filter out function return address comments (`/* <-caller+offset */`)
> - from function_graph output.
> + from function_graph output. This is enabled by default when `--inline`
> + is used; use `--no-filter-retaddr` to disable it.
> +
> +-k::
> +--vmlinux=::
> + Path to the vmlinux file containing debug symbols for resolving
> + inlined functions.
>
>
> OPTIONS for 'perf ftrace latency'
> diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
> index c715cbebaabe..ed483bb20ed5 100644
> --- a/tools/perf/builtin-ftrace.c
> +++ b/tools/perf/builtin-ftrace.c
> @@ -575,9 +575,14 @@ static int set_tracing_funcgraph_retval(struct perf_ftrace *ftrace)
>
> static int set_tracing_funcgraph_retaddr(struct perf_ftrace *ftrace)
> {
> - if (ftrace->graph_retaddr) {
> - if (write_tracing_option_file("funcgraph-retaddr", "1") < 0)
> + if (ftrace->graph_retaddr || ftrace->use_inline) {
> + if (write_tracing_option_file("funcgraph-retaddr", "1") < 0) {
> + if (ftrace->use_inline) {
> + pr_err("failed to set funcgraph-retaddr option in tracing\n");
> + pr_err("Hint: ensure kernel is compiled with CONFIG_FUNCTION_GRAPH_RETADDR=y\n");
> + }
> return -1;
> + }
> }
>
> return 0;
> @@ -744,6 +749,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
> int trace_fd;
> char *buf = NULL;
> struct strbuf linebuf = STRBUF_INIT;
> + int ret = 0;
> struct pollfd pollfd = {
> .events = POLLIN,
> };
> @@ -770,6 +776,9 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
> goto out_reset;
> }
>
> + if (perf_ftrace__setup_inlines(ftrace) < 0)
> + goto out_reset;
> +
> setup_pager();
>
> buf = malloc(TRACE_BUF_SIZE);
> @@ -837,14 +846,20 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
> break;
> for (int i = 0; i < n; i++) {
> if (buf[i] == '\n') {
> - if (ftrace->filter_retaddr)
> - ftrace_filter_retaddr(linebuf.buf);
> - if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
> + if (ftrace_process_fgraph_line(ftrace,
> + linebuf.buf,
> + stdout) < 0) {
> + ret = -1;
> + goto out_close_fd;
> + }
> + if (ferror(stdout))
> break;
> strbuf_setlen(&linebuf, 0);
> } else {
> - if (strbuf_addch(&linebuf, buf[i]) < 0)
> + if (strbuf_addch(&linebuf, buf[i]) < 0) {
> + ret = -1;
> goto out_close_fd;
> + }
> }
> }
> /* flush output since stdout is in full buffering mode due to pager */
> @@ -876,22 +891,30 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
> break;
> for (int i = 0; i < n; i++) {
> if (buf[i] == '\n') {
> - if (ftrace->filter_retaddr)
> - ftrace_filter_retaddr(linebuf.buf);
> - if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
> + if (ftrace_process_fgraph_line(ftrace,
> + linebuf.buf,
> + stdout) < 0) {
> + ret = -1;
> + goto out_close_fd;
> + }
> + if (ferror(stdout))
> break;
> strbuf_setlen(&linebuf, 0);
> } else {
> - if (strbuf_addch(&linebuf, buf[i]) < 0)
> + if (strbuf_addch(&linebuf, buf[i]) < 0) {
> + ret = -1;
> goto out_close_fd;
> + }
> }
> }
> }
>
> if (!ferror(stdout) && linebuf.len > 0) {
> - if (ftrace->filter_retaddr)
> - ftrace_filter_retaddr(linebuf.buf);
> - fprintf(stdout, "%s\n", linebuf.buf);
> + if (ftrace_process_fgraph_line(ftrace, linebuf.buf,
> + stdout) < 0) {
> + ret = -1;
> + goto out_close_fd;
> + }
> strbuf_setlen(&linebuf, 0);
> }
>
> @@ -901,9 +924,10 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
> free(buf);
> strbuf_release(&linebuf);
> out_reset:
> + perf_ftrace__cleanup_inlines(ftrace);
> exit_tracing_instance();
> out:
> - return (done && !workload_exec_errno) ? 0 : -1;
> + return (ret == 0 && done && !workload_exec_errno) ? 0 : -1;
> }
>
> static void make_histogram(struct perf_ftrace *ftrace, int buckets[],
> @@ -1856,8 +1880,13 @@ int cmd_ftrace(int argc, const char **argv)
> "Size of per cpu buffer, needs to use a B, K, M or G suffix.", parse_buffer_size),
> OPT_BOOLEAN(0, "inherit", &ftrace.inherit,
> "Trace children processes"),
> - OPT_BOOLEAN_SET(0, "filter-retaddr", &ftrace.filter_retaddr, &ftrace.filter_retaddr_set,
> - "Filter out funcgraph return address comments"),
> + OPT_BOOLEAN(0, "inline", &ftrace.use_inline,
> + "Show inlined functions in function_graph tracer"),
> + OPT_BOOLEAN_SET(0, "filter-retaddr", &ftrace.filter_retaddr,
> + &ftrace.filter_retaddr_set,
> + "Filter out funcgraph return address comments (default with --inline)"),
> + OPT_STRING('k', "vmlinux", &symbol_conf.vmlinux_name, "file",
> + "vmlinux pathname"),
> OPT_INTEGER('D', "delay", &ftrace.target.initial_delay,
> "Number of milliseconds to wait before starting tracing after program start"),
> OPT_PARENT(common_options),
> @@ -1973,6 +2002,21 @@ int cmd_ftrace(int argc, const char **argv)
>
> switch (subcmd) {
> case PERF_FTRACE_TRACE:
> + select_tracer(&ftrace);
> +
> + if (ftrace.use_inline) {
> + if (strcmp(ftrace.tracer, "function_graph") != 0) {
> + pr_err("Error: --inline option is only supported with function_graph tracer\n");
> + ret = -EINVAL;
> + goto out_delete_filters;
> + }
> + }
> + if (!ftrace.filter_retaddr_set) {
> + if (ftrace.use_inline && !ftrace.graph_retaddr)
> + ftrace.filter_retaddr = true;
> + else
> + ftrace.filter_retaddr = false;
> + }
> cmd_func = __cmd_ftrace;
> break;
> case PERF_FTRACE_LATENCY:
> diff --git a/tools/perf/tests/ftrace.c b/tools/perf/tests/ftrace.c
> index 42604811a2fb..44ed5d7dcf98 100644
> --- a/tools/perf/tests/ftrace.c
> +++ b/tools/perf/tests/ftrace.c
> @@ -7,7 +7,13 @@
> #include <unistd.h>
> #include "tests.h"
> #include "debug.h"
> +#include "dso.h"
> +#include "machine.h"
> +#include "map.h"
> +#include "symbol.h"
> #include "util/ftrace.h"
> +#include "evlist.h"
> +#include "thread_map.h"
>
> static int test_parse_retaddr(void)
> {
> @@ -63,6 +69,896 @@ static int test_filter_retaddr(void)
> return TEST_OK;
> }
>
> +#define TEST_CHECK(text, cond) \
> +do { \
> + if (!(cond)) { \
> + pr_debug("FAILED %s:%d %s\n", __FILE__, __LINE__, text); \
> + ret = TEST_FAIL; \
> + goto out_cleanup; \
> + } \
> +} while (0)
> +
> +static int test_inline_processing(void)
> +{
> + static const char line_entry[] = " 0) | wake_up_new_task() {";
> + static const char line_enqueue[] =
> + " 0) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */";
> + static const char line_exit_enqueue[] = " 0) + 10.000 us | } /* enqueue_task */";
> + static const char line_exit_task[] = " 0) + 20.000 us | } /* wake_up_new_task */";
> + static const char line_bare_enqueue[] = " 0) + 10.000 us | }";
> + static const char line_bare_task[] = " 0) + 20.000 us | }";
> + static const char line_unmatched[] = " 0) + 5.000 us | } /* unrelated_func */";
> + static const char line_verbose_entry[] =
> + " 1234.567890 | 0) bash-1234 | .... | | wake_up_new_task() {";
> + static const char line_verbose_enqueue[] =
> + " 1234.567890 | 0) bash-1234 | .... | | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */";
> + static const char line_verbose_exit_enqueue[] =
> + " 1234.567890 | 0) bash-1234 | .... | 1.234 us | } /* enqueue_task */";
> + static const char line_verbose_exit_task[] =
> + " 1234.567890 | 0) bash-1234 | .... | 2.567 us | } /* wake_up_new_task */";
> + static const char line_verbose_inline_entry[] =
> + " 1234.567890 | 0) bash-1234 | .... | | activate_task() { /* (inline) */";
> + static const char line_verbose_inline_exit[] =
> + " 1234.567890 | 0) bash-1234 | .... | | } /* activate_task (inline) */";
> + struct perf_ftrace ftrace;
> + char *out_buf = NULL;
> + size_t out_len = 0;
> + FILE *out;
> + const char *unmatched_pos;
> + const char *inline_exit_pos;
> + const char *vmlinux;
> + const char *saved_vmlinux = symbol_conf.vmlinux_name;
> + bool saved_ignore_buildid = symbol_conf.ignore_vmlinux_buildid;
> + int ret = TEST_OK;
> +
> + memset(&ftrace, 0, sizeof(ftrace));
> + ftrace.use_inline = true;
> + ftrace.filter_retaddr = 1; /* Default when --graph-opts retaddr is not specified */
> + ftrace.tracer = "function_graph";
> +
> + vmlinux = vmlinux_path__find();
> +
> + /* If vmlinux is present, test resolving symbols */
> + if (vmlinux) {
> + const char *inlined_names[FTRACE_MAX_INLINES];
> + int num_inlines;
> + int j;
> +
> + symbol_conf.vmlinux_name = vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = true;
> + ret = perf_ftrace__setup_inlines(&ftrace);
> + if (ret < 0 || !ftrace.machine) {
> + ret = TEST_SKIP;
> + goto out_cleanup;
> + }
> +
> + num_inlines = ftrace_resolve_inlines(ftrace.machine,
> + "wake_up_new_task", 0x1d1,
> + inlined_names,
> + FTRACE_MAX_INLINES);
> + if (num_inlines <= 0 || strcmp(inlined_names[0], "activate_task") != 0) {
> + pr_debug("vmlinux does not inline activate_task at wake_up_new_task+0x1d1; skipping inline test\n");
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + ret = TEST_SKIP;
> + goto out_cleanup;
> + }
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> +
> + if (ftrace_resolve_inlines(ftrace.machine, "wake_up_new_task", 0,
> + inlined_names, FTRACE_MAX_INLINES) != 0) {
> + pr_debug("ftrace_resolve_inlines with offset 0 should return 0\n");
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + /* Feed simulated function graph trace lines */
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + pr_debug("Processed output:\n%s\n", out_buf);
> +
> + /* Verify activate_task entry with inline hint */
> + TEST_CHECK("contains activate_task entry with inline hint",
> + strstr(out_buf, "activate_task() { /* (inline) */") != NULL);
> +
> + /* Verify activate_task exit with inline hint */
> + TEST_CHECK("contains activate_task exit with inline hint",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> +
> + /* Verify return address comment was filtered out by default */
> + TEST_CHECK("retaddr comment filtered out",
> + strstr(out_buf, "/* <-wake_up_new_task") == NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Now test with filter_retaddr = 0 (user specified --graph-opts retaddr) */
> + ftrace.filter_retaddr = 0;
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + /* Verify return address comment IS preserved when filter_retaddr = 0 */
> + TEST_CHECK("retaddr comment preserved when requested",
> + strstr(out_buf, "/* <-wake_up_new_task") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test with bare closing braces (funcgraph-tail disabled by default) */
> + ftrace.filter_retaddr = 1;
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_bare_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_bare_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("bare braces inline entry",
> + strstr(out_buf, "activate_task() { /* (inline) */") != NULL);
> + TEST_CHECK("bare braces inline exit",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test bare closing brace matches outer frame when inner callee exit was dropped */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + /* line_bare_enqueue is dropped! */
> + ftrace_process_fgraph_line(&ftrace, line_bare_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("dropped callee exit: inlined frame closed",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> + TEST_CHECK("dropped callee exit: outer bare brace emitted",
> + strstr(out_buf, " }") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test that unmatched exit events do not pop active inlined frames */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_enqueue, out);
> + /* Unmatched exit event while activate_task is still active */
> + ftrace_process_fgraph_line(&ftrace, line_unmatched, out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> +
> + unmatched_pos = strstr(out_buf, "} /* unrelated_func */");
> + inline_exit_pos = strstr(out_buf, "} /* activate_task (inline) */");
> +
> + TEST_CHECK("unmatched exit present", unmatched_pos != NULL);
> + TEST_CHECK("inline exit present", inline_exit_pos != NULL);
> + TEST_CHECK("inline exit not prematurely popped before unmatched exit",
> + inline_exit_pos > unmatched_pos);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test recovery when an intermediate real function exit event is dropped */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + /* line_exit_enqueue is dropped; outer wake_up_new_task exits directly */
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("inline exit synthesized despite dropped callee exit",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test recovery when intermediate callee exit is dropped before sibling call */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + /* line_exit_enqueue dropped; caller makes sibling call without inlines */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | check_preempt_curr(); /* <-wake_up_new_task+0x0/0x3e0 */",
> + out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("inline exit synthesized when callee exit dropped before sibling call",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test that callers not on active stack do not synthesise inlines */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + /* housekeeping_any_cpu called by untraced ring_buffer_unlock_commit */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | housekeeping_any_cpu(); /* <-ring_buffer_unlock_commit+0x1a0/0x1f0 */",
> + out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("untraced caller inlines not synthesised",
> + strstr(out_buf, "rb_wakeups") == NULL &&
> + strstr(out_buf, "rb_irq_work_queue") == NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test trailing inlined frame closed on untraced entry (caller_idx < 0) */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + /* line_enqueue has inlined activate_task() open; next entry has caller_idx < 0 */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | irq_handler() {",
> + out);
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | } /* irq_handler */",
> + out);
> + ftrace_process_fgraph_line(&ftrace, line_exit_task, out);
> +
> + fclose(out);
> +
> + TEST_CHECK("trailing inline frame closed before untraced call",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> + TEST_CHECK("untraced call not indented inside trailing inline frame",
> + strstr(out_buf, " irq_handler() {") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test dropped exits unwound when new function starts at shallower indent */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_enqueue, out);
> + /* Exits dropped! New root entry arrives at indent 2 */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | schedule() {",
> + out);
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | } /* schedule */",
> + out);
> +
> + fclose(out);
> +
> + TEST_CHECK("stale inlines closed when new root arrives",
> + strstr(out_buf, "} /* activate_task (inline) */") != NULL);
> + TEST_CHECK("new root entry printed without stale indentation",
> + strstr(out_buf, " schedule() {") != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test verbose mode prefix formatting for synthesised inlines */
> + ftrace.graph_verbose = true;
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + ret = TEST_FAIL;
> + goto out_cleanup;
> + }
> +
> + ftrace_process_fgraph_line(&ftrace, line_verbose_entry, out);
> + ftrace_process_fgraph_line(&ftrace, line_verbose_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_verbose_exit_enqueue, out);
> + ftrace_process_fgraph_line(&ftrace, line_verbose_exit_task, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + TEST_CHECK("verbose inline entry preserves comm-pid",
> + strstr(out_buf, line_verbose_inline_entry) != NULL);
> + TEST_CHECK("verbose inline exit preserves comm-pid",
> + strstr(out_buf, line_verbose_inline_exit) != NULL);
> +
> + free(out_buf);
> + out_buf = NULL;
> + ftrace.graph_verbose = false;
> + }
> +
> +out_cleanup:
> + free(out_buf);
> + perf_ftrace__cleanup_inlines(&ftrace);
> + symbol_conf.vmlinux_name = saved_vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = saved_ignore_buildid;
> + return ret;
> +}
> +#undef TEST_CHECK
> +
> +static void feed_fgraph_entry(struct perf_ftrace *ftrace, int cpu, int depth,
> + const char *name, const char *retaddr, FILE *out)
> +{
> + char line[1024];
> + int indent = 2 + 2 * depth;
> +
> + if (retaddr) {
> + snprintf(line, sizeof(line), " %d) |%*s%s() { /* <-%s */",
> + cpu, indent, "", name, retaddr);
> + } else {
> + snprintf(line, sizeof(line), " %d) |%*s%s() {",
> + cpu, indent, "", name);
> + }
> + ftrace_process_fgraph_line(ftrace, line, out);
> +}
> +
> +static void feed_fgraph_exit(struct perf_ftrace *ftrace, int cpu, int depth,
> + const char *name, FILE *out)
> +{
> + char line[1024];
> + int indent = 2 + 2 * depth;
> +
> + if (name) {
> + snprintf(line, sizeof(line), " %d) 1.000 us |%*s} /* %s */",
> + cpu, indent, "", name);
> + } else {
> + snprintf(line, sizeof(line), " %d) 1.000 us |%*s}",
> + cpu, indent, "");
> + }
> + ftrace_process_fgraph_line(ftrace, line, out);
> +}
> +
> +static int test_stack_saturation(void)
> +{
> + struct perf_ftrace ftrace;
> + char *out_buf = NULL;
> + size_t out_len = 0;
> + FILE *out;
> + const char *vmlinux;
> + int i;
> +
> + memset(&ftrace, 0, sizeof(ftrace));
> + ftrace.use_inline = true;
> + ftrace_cpu_states__clear();
> +
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + /* Push root frame at depth 0 */
> + feed_fgraph_entry(&ftrace, 0, 0, "root_func", NULL, out);
> +
> + /* Push 255 more frames to fill FTRACE_MAX_STACK (256) */
> + for (i = 1; i < 256; i++)
> + feed_fgraph_entry(&ftrace, 0, i, "sub_func", NULL, out);
> +
> + /* Push 44 overflow frames beyond FTRACE_MAX_STACK */
> + for (i = 0; i < 44; i++)
> + feed_fgraph_entry(&ftrace, 0, 256 + i, "overflow_func", NULL, out);
> +
> + /* Pop the 44 overflow frames with bare closing braces */
> + for (i = 43; i >= 0; i--)
> + feed_fgraph_exit(&ftrace, 0, 256 + i, NULL, out);
> +
> + /* Pop 255 tracked sub_func frames */
> + for (i = 255; i >= 1; i--)
> + feed_fgraph_exit(&ftrace, 0, i, NULL, out);
> +
> + /* Pop root frame - must cleanly match root_func on stack, not popped prematurely */
> + feed_fgraph_exit(&ftrace, 0, 0, "root_func", out);
> +
> + /* Feed one more function entry - it must have base indentation (no indentation drift) */
> + feed_fgraph_entry(&ftrace, 0, 0, "base_func", NULL, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + /* Verify base_func starts at standard 2 spaces indentation without drift */
> + if (!strstr(out_buf, "| base_func() {")) {
> + pr_debug("FAILED %s:%d no indentation drift after stack saturation\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test overflow recovery when events are dropped */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + feed_fgraph_entry(&ftrace, 0, 0, "root_func", NULL, out);
> + for (i = 1; i < 256; i++)
> + feed_fgraph_entry(&ftrace, 0, i, "sub_func", NULL, out);
> + for (i = 0; i < 44; i++)
> + feed_fgraph_entry(&ftrace, 0, 256 + i, "overflow_func", NULL, out);
> +
> + /* Dropped events: root_func directly calls recovery_func */
> + feed_fgraph_entry(&ftrace, 0, 1, "recovery_func", "root_func+0x20", out);
> + feed_fgraph_exit(&ftrace, 0, 1, NULL, out);
> + feed_fgraph_exit(&ftrace, 0, 0, "root_func", out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + if (!strstr(out_buf, "recovery_func() {")) {
> + pr_debug("FAILED %s:%d recovery_func missing after overflow recovery\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> + if (!strstr(out_buf, "} /* root_func */")) {
> + pr_debug("FAILED %s:%d root_func exit missing after overflow recovery\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test that bare braces during overflow do not prematurely pop tracked frames */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + feed_fgraph_entry(&ftrace, 0, 0, "root_func", NULL, out);
> + for (i = 1; i < 256; i++)
> + feed_fgraph_entry(&ftrace, 0, i, "sub_func", NULL, out);
> + for (i = 0; i < 10; i++)
> + feed_fgraph_entry(&ftrace, 0, 256 + i, "overflow_func", NULL, out);
> +
> + /* Pop 10 overflow frames with bare braces */
> + for (i = 9; i >= 0; i--)
> + feed_fgraph_exit(&ftrace, 0, 256 + i, NULL, out);
> +
> + /* Pop 255 sub_func frames with bare braces */
> + for (i = 255; i >= 1; i--)
> + feed_fgraph_exit(&ftrace, 0, i, NULL, out);
> +
> + /* Root frame must still be on the stack to match caller */
> + feed_fgraph_entry(&ftrace, 0, 1, "child_func", "root_func+0x10", out);
> + feed_fgraph_exit(&ftrace, 0, 1, NULL, out);
> + feed_fgraph_exit(&ftrace, 0, 0, "root_func", out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + if (!strstr(out_buf, "child_func() {")) {
> + pr_debug("FAILED %s:%d child_func missing after bare braces in overflow\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> + if (!strstr(out_buf, "} /* root_func */")) {
> + pr_debug("FAILED %s:%d root_func exit missing after bare braces in overflow\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> +
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test recursive caller lookup skips stale inner invocation */
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + /* Outer recursive invocation at indent 2 */
> + ftrace_process_fgraph_line(&ftrace, " 0) | rec_func() {", out);
> + /* Inner recursive invocation at indent 4 */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) | rec_func() { /* <-rec_func+0x10 */",
> + out);
> + /* Inner invocation exit is dropped by ring buffer */
> + /* Outer invocation calls sibling_func at indent 4 */
> + ftrace_process_fgraph_line(&ftrace,
> + " 0) 1.000 us | sibling_func(); /* <-rec_func+0x20 */",
> + out);
> + ftrace_process_fgraph_line(&ftrace, " 0) 2.000 us | } /* rec_func */", out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + if (!strstr(out_buf, "sibling_func();")) {
> + pr_debug("FAILED %s:%d sibling_func missing under recursive caller\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> + if (!strstr(out_buf, "} /* rec_func */")) {
> + pr_debug("FAILED %s:%d rec_func exit missing after dropped inner exit\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> + free(out_buf);
> + out_buf = NULL;
> +
> + /* Test top inlined frame when cs->overflow_depth > 0 does not misclassify exit */
> + vmlinux = vmlinux_path__find();
> + if (vmlinux) {
> + const char *saved_vmlinux = symbol_conf.vmlinux_name;
> + bool saved_ignore_buildid = symbol_conf.ignore_vmlinux_buildid;
> + const char *inlined_names[FTRACE_MAX_INLINES];
> + int num_inlines;
> + int j;
> +
> + symbol_conf.vmlinux_name = vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = true;
> + if (perf_ftrace__setup_inlines(&ftrace) == 0 && ftrace.machine) {
> + num_inlines = ftrace_resolve_inlines(ftrace.machine,
> + "wake_up_new_task", 0x1d1,
> + inlined_names,
> + FTRACE_MAX_INLINES);
> + if (num_inlines > 0 && !strcmp(inlined_names[0], "activate_task")) {
> + out = open_memstream(&out_buf, &out_len);
> + if (!out) {
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + perf_ftrace__cleanup_inlines(&ftrace);
> + symbol_conf.vmlinux_name = saved_vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = saved_ignore_buildid;
> + return TEST_FAIL;
> + }
> +
> + /* Fill stack up to 254 with tracked frames */
> + feed_fgraph_entry(&ftrace, 0, 0, "root_func", NULL, out);
> + for (i = 1; i < 254; i++)
> + feed_fgraph_entry(&ftrace, 0, i, "sub_func", NULL, out);
> +
> + /* Frame 254 is wake_up_new_task */
> + feed_fgraph_entry(&ftrace, 0, 254, "wake_up_new_task", NULL, out);
> +
> + /*
> + * Frame 255: enqueue_task resolves inline activate_task.
> + * Inlined frame activate_task is pushed at stack[255]
> + * (cs->depth = 256). Real frame enqueue_task overflows
> + * into cs->overflow_depth = 1.
> + */
> + feed_fgraph_entry(&ftrace, 0, 255, "enqueue_task",
> + "wake_up_new_task+0x1d1/0x3e0", out);
> +
> + /*
> + * Nested child call inside overflow: does NOT pop the
> + * inlined frame!
> + */
> + feed_fgraph_entry(&ftrace, 0, 256, "child_overflow",
> + "enqueue_task+0x10", out);
> +
> + /* Exit child_overflow (overflow_depth decrements to 1) */
> + feed_fgraph_exit(&ftrace, 0, 256, NULL, out);
> +
> + /* Exit enqueue_task (overflow_depth decrements to 0) */
> + feed_fgraph_exit(&ftrace, 0, 255, NULL, out);
> +
> + /* Exit wake_up_new_task: synthesises inline exit */
> + feed_fgraph_exit(&ftrace, 0, 254, NULL, out);
> +
> + /* Pop remaining sub_func frames and root_func */
> + for (i = 253; i >= 1; i--)
> + feed_fgraph_exit(&ftrace, 0, i, NULL, out);
> + feed_fgraph_exit(&ftrace, 0, 0, "root_func", out);
> +
> + /* Feed base frame to check for indentation drift */
> + feed_fgraph_entry(&ftrace, 0, 0, "drift_check", NULL, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + if (!strstr(out_buf, "activate_task() { /* (inline) */") ||
> + !strstr(out_buf, "} /* activate_task (inline) */") ||
> + !strstr(out_buf, "| drift_check() {")) {
> + pr_debug("FAILED %s:%d top inlined frame saturation failed\n",
> + __FILE__, __LINE__);
> + free(out_buf);
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + perf_ftrace__cleanup_inlines(&ftrace);
> + symbol_conf.vmlinux_name = saved_vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = saved_ignore_buildid;
> + return TEST_FAIL;
> + }
> +
> + free(out_buf);
> + out_buf = NULL;
> + }
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + perf_ftrace__cleanup_inlines(&ftrace);
> + }
> + symbol_conf.vmlinux_name = saved_vmlinux;
> + symbol_conf.ignore_vmlinux_buildid = saved_ignore_buildid;
> + }
> +
> + free(out_buf);
> + return TEST_OK;
> +}
> +
> +static int test_column_layouts(void)
> +{
> + static const struct {
> + const char *desc;
> + bool verbose;
> + const char *in;
> + const char *expected;
> + } cases[] = {
> + {
> + "default layout", false,
> + " 7) | wake_up_new_task() { /* <-kernel_clone+0x160/0x510 */",
> + " 7) | wake_up_new_task() {\n",
> + },
> + {
> + "verbose layout", true,
> + " 1234.567890 | 7) bash-1234 | .... | | wake_up_new_task() { /* <-kernel_clone+0x160/0x510 */",
> + " 1234.567890 | 7) bash-1234 | .... | | wake_up_new_task() {\n",
> + },
> + };
> + struct perf_ftrace ftrace;
> + char *out_buf = NULL;
> + size_t out_len = 0;
> + FILE *out;
> + size_t i;
> +
> + for (i = 0; i < ARRAY_SIZE(cases); i++) {
> + memset(&ftrace, 0, sizeof(ftrace));
> + ftrace.use_inline = true;
> + ftrace.filter_retaddr = true;
> + ftrace.graph_verbose = cases[i].verbose;
> +
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + ftrace_process_fgraph_line(&ftrace, cases[i].in, out);
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + if (strcmp(out_buf, cases[i].expected)) {
> + pr_debug("%s: expected '%s', got '%s'\n",
> + cases[i].desc, cases[i].expected, out_buf);
> + free(out_buf);
> + return TEST_FAIL;
> + }
> + free(out_buf);
> + out_buf = NULL;
> + }
> +
> + return TEST_OK;
> +}
> +
> +static int test_context_switch_handling(void)
> +{
> + struct perf_ftrace ftrace;
> + struct perf_thread_map *threads;
> + char *out_buf = NULL;
> + size_t out_len = 0;
> + FILE *out;
> + int i;
> +
> + memset(&ftrace, 0, sizeof(ftrace));
> + ftrace.use_inline = true;
> + ftrace_cpu_states__clear();
> +
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + /*
> + * Initial Task 1 (PID 100) starts running on CPU 0 before any banner
> + * has been emitted on that CPU (default kernel ftrace behaviour).
> + */
> + ftrace_process_fgraph_line(&ftrace, " 0) | func1() {", out);
> +
> + /* First context switch on CPU 0: Task 1 switches to <idle>-0 */
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 0) task1-100 => <idle>-0 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> +
> + /* Idle task executes on CPU 0 - should NOT inherit func1's stack */
> + ftrace_process_fgraph_line(&ftrace, " 0) | default_idle() {", out);
> + ftrace_process_fgraph_line(&ftrace, " 0) | } /* default_idle */", out);
> +
> + /* Context switch to Task 2 (PID 200) on CPU 0 */
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 0) <idle>-0 => task2-200 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> +
> + /* Task 2 executes func2 - should NOT inherit func1's indentation */
> + ftrace_process_fgraph_line(&ftrace, " 0) | func2() {", out);
> + ftrace_process_fgraph_line(&ftrace, " 0) | } /* func2 */", out);
> +
> + /* Task 1 migrates and resumes on CPU 1 */
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 1) <idle>-0 => task1-100 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> +
> + /* Task 1 exits func1 on CPU 1 - should cleanly match func1 from its preserved stack */
> + ftrace_process_fgraph_line(&ftrace, " 1) | } /* func1 */", out);
> +
> + /*
> + * Test delayed context switch banner migration where a task state
> + * allocated on CPU 3 is rekeyed from CPU 2.
> + */
> + ftrace_process_fgraph_line(&ftrace, " 2) | func3() {", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) <idle>-0 => task3-300 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 2) task3-300 => <idle>-0 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | func4() {", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | } /* func4 */", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | } /* func3 */", out);
> +
> + /*
> + * Test delayed context switch banner migration where CPU 3 enters func6()
> + * BEFORE CPU 2 emits its delayed context switch banner. ts from CPU 2
> + * must be merged underneath existing on CPU 3.
> + */
> + ftrace_process_fgraph_line(&ftrace, " 2) | func5() {", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) <idle>-0 => task4-400 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | func6() {", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 2) task4-400 => <idle>-0 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | } /* func6 */", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) | } /* func5 */", out);
> +
> + /*
> + * Test single-task profiling across CPU migration where context switch
> + * banners are suppressed by the kernel (verif_pid prev_pid == -1).
> + */
> + ftrace.evlist = evlist__new();
> + if (ftrace.evlist) {
> + threads = thread_map__new_by_tid(500);
> + perf_evlist__set_maps(evlist__core(ftrace.evlist), NULL, threads);
> + perf_thread_map__put(threads);
> + }
> +
> + ftrace_cpu_states__clear();
> + ftrace_process_fgraph_line(&ftrace, " 0) | func_a() {", out);
> + ftrace_process_fgraph_line(&ftrace, " 1) | func_b() {", out);
> + ftrace_process_fgraph_line(&ftrace, " 1) | } /* func_b */", out);
> + ftrace_process_fgraph_line(&ftrace, " 0) | } /* func_a */", out);
> +
> + evlist__put(ftrace.evlist);
> + ftrace.evlist = NULL;
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + /* Verify default_idle in Idle task started at base indentation */
> + TEST_ASSERT_VAL("idle task does not inherit initial task call stack",
> + strstr(out_buf, "| default_idle() {") != NULL);
> +
> + /* Verify func2 in Task 2 started at base indentation "| func2() {" */
> + TEST_ASSERT_VAL("task2 does not inherit task1 call stack",
> + strstr(out_buf, "| func2() {") != NULL);
> +
> + /* Verify task1 exit on CPU 1 matches cleanly */
> + TEST_ASSERT_VAL("task1 preserved stack across migration",
> + strstr(out_buf, "1) | } /* func1 */") != NULL);
> +
> + /* Verify task3 preserved stack across delayed context switch banner */
> + TEST_ASSERT_VAL("task3 preserved stack across delayed banner",
> + strstr(out_buf, "3) | } /* func3 */") != NULL);
> +
> + /* Verify task4 merged outer stack when entered before delayed banner */
> + TEST_ASSERT_VAL("task4 merged outer stack when entered before delayed banner",
> + strstr(out_buf, "3) | } /* func5 */") != NULL);
> +
> + /* Verify single-task stack tracked across CPUs without banners */
> + TEST_ASSERT_VAL("single-task stack tracked across CPUs without banners",
> + strstr(out_buf, "0) | } /* func_a */") != NULL);
> +
> + /*
> + * Test migration where merged stack depth exceeds FTRACE_MAX_STACK
> + * does not cause indentation drift or corruption.
> + * CPU 2 starts without a context switch banner, accumulating under
> + * FTRACE_UNKNOWN_PID(2).
> + */
> + free(out_buf);
> + out_buf = NULL;
> + out = open_memstream(&out_buf, &out_len);
> + if (!out)
> + return TEST_FAIL;
> +
> + for (i = 0; i < 200; i++)
> + feed_fgraph_entry(&ftrace, 2, i, "deep_func", NULL, out);
> +
> + /* Migrate to CPU 3, enter more frames */
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 3) <idle>-0 => task5-500 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + for (i = 0; i < 100; i++)
> + feed_fgraph_entry(&ftrace, 3, 200 + i, "more_func", NULL, out);
> +
> + /* Delayed context switch banner from CPU 2 rekeys and merges stacks (> 256) */
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> + ftrace_process_fgraph_line(&ftrace, " 2) task5-500 => <idle>-0 ", out);
> + ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
> +
> + /* Unwind all frames on CPU 3 (44 in overflow_depth + 256 on stack = 300) */
> + for (i = 299; i >= 0; i--)
> + feed_fgraph_exit(&ftrace, 3, i, NULL, out);
> + feed_fgraph_entry(&ftrace, 3, 0, "after_merge", NULL, out);
> +
> + fclose(out);
> + ftrace_cpu_states__clear();
> +
> + TEST_ASSERT_VAL("no indentation drift after saturated stack rekey",
> + strstr(out_buf, "| after_merge() {") != NULL);
> +
> + free(out_buf);
> + return TEST_OK;
> +}
> +
> static int test__ftrace(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
> {
> int ret;
> @@ -75,7 +971,23 @@ static int test__ftrace(struct test_suite *test __maybe_unused, int subtest __ma
> if (ret != TEST_OK)
> return ret;
>
> + ret = test_stack_saturation();
> + if (ret != TEST_OK)
> + return ret;
> +
> + ret = test_context_switch_handling();
> + if (ret != TEST_OK)
> + return ret;
> +
> + ret = test_column_layouts();
> + if (ret != TEST_OK)
> + return ret;
> +
> + ret = test_inline_processing();
> + if (ret != TEST_OK)
> + return ret;
> +
> return TEST_OK;
> }
>
> -DEFINE_SUITE("Ftrace return address processing", ftrace);
> +DEFINE_SUITE("Ftrace inline processing", ftrace);
> diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
> index 452231c01568..c3da2d1f44f4 100644
> --- a/tools/perf/util/ftrace.c
> +++ b/tools/perf/util/ftrace.c
> @@ -1,12 +1,346 @@
> // SPDX-License-Identifier: GPL-2.0
> -#include <linux/compiler.h>
> -#include <linux/kernel.h>
> -#include <linux/string.h>
> +/*
> + * ftrace.c - Ftrace utilities and inlined function processing
> + *
> + * Copyright (C) 2026 Aaron Tomlin <atomlin@atomlin.com>
> + */
> #include <stdio.h>
> #include <stdlib.h>
> +#include <errno.h>
> #include <string.h>
> #include <ctype.h>
> +#include <stdbool.h>
> +#include <linux/kernel.h>
> +#include <linux/zalloc.h>
> +#include <linux/hash.h>
> +#include <linux/list.h>
> +#include <linux/string.h>
> +#include "debug.h"
> +#include "dso.h"
> +#include "evlist.h"
> +#include "machine.h"
> +#include "map.h"
> +#include "srcline.h"
> +#include "symbol.h"
> #include "util/ftrace.h"
> +#include <perf/threadmap.h>
> +
> +#define FTRACE_MAX_CPUS 4096
> +#define FTRACE_MAX_STACK 256
> +#define FTRACE_PID_HASH_BITS 10
> +#define FTRACE_PID_HASH_SIZE (1 << FTRACE_PID_HASH_BITS)
> +
> +#define FTRACE_IDLE_PID(cpu) ((pid_t)(-1 - (cpu)))
> +#define FTRACE_UNKNOWN_PID(cpu) ((pid_t)(-1 - FTRACE_MAX_CPUS - (cpu)))
> +
> +struct ftrace_stack_frame {
> + char *name;
> + bool inlined;
> + int indent;
> + int raw_indent;
> +};
> +
> +struct ftrace_task_state {
> + struct hlist_node node;
> + pid_t pid;
> + struct ftrace_stack_frame stack[FTRACE_MAX_STACK];
> + int depth;
> + int inlined_depth;
> + int overflow_depth;
> +};
> +
> +#define ftrace_cpu_state ftrace_task_state
> +
> +static struct hlist_head ftrace_task_hash[FTRACE_PID_HASH_SIZE];
> +static struct ftrace_task_state *ftrace_cpu_tasks[FTRACE_MAX_CPUS];
> +
> +/**
> + * ftrace_cpu_states__clear - Free allocated CPU and task stack states
> + *
> + * Free all memory associated with the per-task stack tracking states
> + * used for inlined function resolution, including the strings
> + * allocated for stack frames.
> + */
> +void ftrace_cpu_states__clear(void)
> +{
> + struct ftrace_task_state *ts;
> + struct hlist_node *tmp;
> + int i, j;
> +
> + for (i = 0; i < FTRACE_PID_HASH_SIZE; i++) {
> + hlist_for_each_entry_safe(ts, tmp, &ftrace_task_hash[i], node) {
> + for (j = 0; j < ts->depth; j++)
> + free(ts->stack[j].name);
> + hlist_del_init(&ts->node);
> + free(ts);
> + }
> + }
> +
> + memset(ftrace_cpu_tasks, 0, sizeof(ftrace_cpu_tasks));
> +}
> +
> +/**
> + * find_task_state - Look up call stack state for a task by PID
> + * @pid: Process ID of the task
> + *
> + * Return: A pointer to the task's stack state, or NULL if not found.
> + */
> +static struct ftrace_task_state *find_task_state(pid_t pid)
> +{
> + struct ftrace_task_state *ts;
> + struct hlist_head *head;
> + u32 key = hash_32((u32)pid, FTRACE_PID_HASH_BITS);
> +
> + head = &ftrace_task_hash[key];
> + hlist_for_each_entry(ts, head, node) {
> + if (ts->pid == pid)
> + return ts;
> + }
> +
> + return NULL;
> +}
> +
> +/**
> + * get_task_state - Get or allocate stack state for a given task PID
> + * @pid: Process ID of the task
> + *
> + * Return: A pointer to the task's stack state, or NULL on allocation failure.
> + */
> +static struct ftrace_task_state *get_task_state(pid_t pid)
> +{
> + struct ftrace_task_state *ts = find_task_state(pid);
> + struct hlist_head *head;
> + u32 key;
> +
> + if (ts)
> + return ts;
> +
> + ts = zalloc(sizeof(*ts));
> + if (!ts)
> + return NULL;
> +
> + ts->pid = pid;
> + key = hash_32((u32)pid, FTRACE_PID_HASH_BITS);
> + head = &ftrace_task_hash[key];
> + hlist_add_head(&ts->node, head);
> + return ts;
> +}
> +
> +/**
> + * ftrace_task_state__rekey - Reassign a task state to a new PID
> + * @ts: The task state to re-key
> + * @new_pid: The new process ID to associate with the state
> + *
> + * Rehash @ts under @new_pid. If a task state for @new_pid already exists,
> + * merge or replace it and update active CPU references accordingly.
> + *
> + * Return: A pointer to the active task state associated with @new_pid.
> + */
> +static struct ftrace_task_state *ftrace_task_state__rekey(struct ftrace_task_state *ts,
> + pid_t new_pid)
> +{
> + struct ftrace_task_state *existing;
> + u32 key;
> + int keep_existing;
> + int j, c;
> +
> + if (ts->pid == new_pid)
> + return ts;
> +
> + hlist_del_init(&ts->node);
> +
> + existing = find_task_state(new_pid);
> + if (existing) {
> + if (existing->depth == 0) {
> + for (c = 0; c < FTRACE_MAX_CPUS; c++) {
> + if (ftrace_cpu_tasks[c] == existing)
> + ftrace_cpu_tasks[c] = ts;
> + }
> + hlist_del_init(&existing->node);
> + free(existing);
> + } else {
> + for (c = 0; c < FTRACE_MAX_CPUS; c++) {
> + if (ftrace_cpu_tasks[c] == ts)
> + ftrace_cpu_tasks[c] = existing;
> + }
> +
> + if (ts->depth >= FTRACE_MAX_STACK) {
> + for (j = 0; j < existing->depth; j++) {
> + if (!existing->stack[j].inlined)
> + existing->overflow_depth++;
> + free(existing->stack[j].name);
> + }
> + existing->inlined_depth = 0;
> + for (j = 0; j < FTRACE_MAX_STACK; j++) {
> + if (ts->stack[j].inlined)
> + existing->inlined_depth++;
> + }
> + memcpy(&existing->stack[0], &ts->stack[0],
> + FTRACE_MAX_STACK * sizeof(existing->stack[0]));
> + existing->depth = FTRACE_MAX_STACK;
> + existing->overflow_depth += ts->overflow_depth;
> + } else if (existing->depth + ts->depth > FTRACE_MAX_STACK) {
> + keep_existing = FTRACE_MAX_STACK - ts->depth;
> +
> + for (j = keep_existing; j < existing->depth; j++) {
> + if (!existing->stack[j].inlined)
> + existing->overflow_depth++;
> + else
> + existing->inlined_depth--;
> + free(existing->stack[j].name);
> + }
> + memmove(&existing->stack[ts->depth], &existing->stack[0],
> + keep_existing * sizeof(existing->stack[0]));
> + memcpy(&existing->stack[0], &ts->stack[0],
> + ts->depth * sizeof(existing->stack[0]));
> + existing->depth = FTRACE_MAX_STACK;
> + existing->inlined_depth += ts->inlined_depth;
> + existing->overflow_depth += ts->overflow_depth;
> + } else {
> + memmove(&existing->stack[ts->depth], &existing->stack[0],
> + existing->depth * sizeof(existing->stack[0]));
> + memcpy(&existing->stack[0], &ts->stack[0],
> + ts->depth * sizeof(existing->stack[0]));
> + existing->depth += ts->depth;
> + existing->inlined_depth += ts->inlined_depth;
> + existing->overflow_depth += ts->overflow_depth;
> + }
> +
> + free(ts);
> + return existing;
> + }
> + }
> +
> + ts->pid = new_pid;
> + key = hash_32((u32)new_pid, FTRACE_PID_HASH_BITS);
> + hlist_add_head(&ts->node, &ftrace_task_hash[key]);
> + return ts;
> +}
> +
> +static pid_t ftrace__get_single_pid(struct perf_ftrace *ftrace)
> +{
> + struct perf_thread_map *threads;
> + pid_t pid;
> +
> + if (!ftrace || !ftrace->evlist || ftrace->inherit)
> + return -1;
> +
> + threads = evlist__core(ftrace->evlist)->threads;
> + if (!threads || perf_thread_map__nr(threads) != 1)
> + return -1;
> +
> + pid = perf_thread_map__pid(threads, 0);
> + return (pid > 0) ? pid : -1;
> +}
> +
> +/**
> + * get_cpu_state - Get or allocate active task stack state for a given CPU
> + * @ftrace: Perf ftrace context
> + * @cpu: The CPU number
> + *
> + * Return: A pointer to the CPU's active task stack state, or NULL on error or
> + * out-of-bounds CPU index.
> + */
> +static struct ftrace_task_state *get_cpu_state(struct perf_ftrace *ftrace, int cpu)
> +{
> + pid_t single_pid;
> +
> + if (cpu < 0 || cpu >= FTRACE_MAX_CPUS)
> + return NULL;
> +
> + if (!ftrace_cpu_tasks[cpu]) {
> + single_pid = ftrace__get_single_pid(ftrace);
> + if (single_pid > 0)
> + ftrace_cpu_tasks[cpu] = get_task_state(single_pid);
> + else
> + ftrace_cpu_tasks[cpu] = get_task_state(FTRACE_UNKNOWN_PID(cpu));
> + }
> +
> + return ftrace_cpu_tasks[cpu];
> +}
> +
> +/**
> + * parse_context_switch - Parse context switch banner and update CPU's task
> + * @line: The trace line containing the banner
> + *
> + * Check if @line is a context switch banner (e.g. " 0) <idle>-0 => migrati-19").
> + * If so, extract the CPU number and both the outgoing and incoming task PIDs,
> + * re-key any initial unknown task state on this CPU to the outgoing PID, and
> + * update the active task mapping for that CPU to the incoming PID.
> + *
> + * Return: true if @line was a context switch banner, false otherwise.
> + */
> +static bool parse_context_switch(const char *line)
> +{
> + const char *arrow = strstr(line, " => ");
> + const char *p;
> + const char *next;
> + const char *hyphen;
> + const char *h;
> + char *endptr;
> + char *end_pid;
> + long cpu_val;
> + long prev_pid;
> + long next_pid;
> + pid_t real_prev;
> + pid_t real_next;
> +
> + if (!arrow)
> + return false;
> +
> + /* Find CPU number before ')' */
> + p = skip_spaces(line);
> + cpu_val = strtol(p, &endptr, 10);
> + if (endptr == p || *endptr != ')')
> + return false;
> +
> + if (cpu_val < 0 || cpu_val >= FTRACE_MAX_CPUS)
> + return false;
> +
> + /*
> + * Parse outgoing task PID before " => " (" CPU) comm-pid => ...").
> + * Scan backwards from arrow to find the hyphen before the numeric PID.
> + */
> + h = arrow - 1;
> + while (h > endptr && *h == ' ')
> + h--;
> + while (h > endptr && *h != '-')
> + h--;
> + if (h <= endptr || *h != '-')
> + return false;
> +
> + prev_pid = strtol(h + 1, &end_pid, 10);
> + if (end_pid == h + 1)
> + return false;
> +
> + /* Parse target task PID after " => " (" => comm-pid") */
> + next = skip_spaces(arrow + 4);
> + hyphen = strrchr(next, '-');
> + if (!hyphen)
> + return false;
> +
> + next_pid = strtol(hyphen + 1, &end_pid, 10);
> + if (end_pid == hyphen + 1)
> + return false;
> +
> + real_prev = (prev_pid == 0) ? FTRACE_IDLE_PID(cpu_val) : (pid_t)prev_pid;
> + real_next = (next_pid == 0) ? FTRACE_IDLE_PID(cpu_val) : (pid_t)next_pid;
> +
> + /*
> + * If no context switch was seen previously on this CPU, its initial
> + * pre-banner trace records were accumulated under an unknown synthetic
> + * PID. Re-key that state to real_prev so the initial task's stack and
> + * inlined frames are preserved when it schedules back in.
> + */
> + if (ftrace_cpu_tasks[cpu_val] &&
> + ftrace_cpu_tasks[cpu_val]->pid == FTRACE_UNKNOWN_PID(cpu_val)) {
> + ftrace_cpu_tasks[cpu_val] =
> + ftrace_task_state__rekey(ftrace_cpu_tasks[cpu_val], real_prev);
> + }
> +
> + ftrace_cpu_tasks[cpu_val] = get_task_state(real_next);
> + return true;
> +}
>
> /**
> * ftrace_parse_retaddr - Parse return address comment from trace line
> @@ -91,3 +425,722 @@ void ftrace_filter_retaddr(char *str)
>
> memmove(start, end, strlen(end) + 1);
> }
> +
> +/**
> + * ftrace_resolve_inlines - Resolve inlined functions for a return address
> + * @machine: Perf machine context
> + * @sym_name: Name of the caller function
> + * @offset: Instruction offset within the caller function
> + * @inlined_names: Array to store the resolved inline function names
> + * @max_inlines: Maximum number of inline frames to resolve
> + *
> + * Use DWARF debug information from the kernel image to resolve a caller's
> + * return address to its inlined callchain.
> + *
> + * Return: The number of inline functions successfully resolved.
> + */
> +int ftrace_resolve_inlines(struct machine *machine,
> + const char *sym_name, u64 offset,
> + const char **inlined_names, int max_inlines)
> +{
> + struct map *map = NULL;
> + struct symbol *sym;
> + struct dso *dso;
> + struct inline_node *node;
> + struct inline_list *ilist;
> + u64 ip, addr;
> + int count = 0;
> + int j;
> +
> + if (!machine || !offset)
> + return 0;
> +
> + sym = machine__find_kernel_symbol_by_name(machine, sym_name, &map);
> + if (!sym || !map)
> + return 0;
> +
> + dso = map__dso(map);
> + if (!dso) {
> + map__put(map);
> + return 0;
> + }
> +
> + ip = sym->start + offset - 1;
> + addr = map__rip_2objdump(map, ip);
> +
> + node = inlines__tree_find(dso__inlined_nodes(dso), addr);
> + if (!node) {
> + node = dso__parse_addr_inlines(dso, addr, sym);
> + if (node)
> + inlines__tree_insert(dso__inlined_nodes(dso), node);
> + }
> +
> + if (!node) {
> + map__put(map);
> + return 0;
> + }
> +
> + list_for_each_entry_reverse(ilist, &node->val, list) {
> + if (ilist->symbol && symbol__inlined(ilist->symbol)) {
> + if (count < max_inlines) {
> + char *name = strdup(ilist->symbol->name);
> +
> + if (!name) {
> + for (j = 0; j < count; j++)
> + free((void *)inlined_names[j]);
> + map__put(map);
> + return -ENOMEM;
> + }
> + inlined_names[count++] = name;
> + }
> + }
> + }
> +
> + map__put(map);
> + return count;
> +}
> +
> +/**
> + * extract_entry_func_name - Extract the function name from an entry record
> + * @str: The trace line's function column string
> + * @buf: Buffer to store the extracted name
> + * @size: Size of the @buf
> + *
> + * Parse the function name out of an entry trace record by stripping away
> + * parentheses or braces.
> + */
> +static void extract_entry_func_name(const char *str, char *buf, size_t size)
> +{
> + size_t i = 0;
> +
> + while (*str && *str != '(' && *str != '{' && !isspace(*str) && i < size - 1)
> + buf[i++] = *str++;
> + buf[i] = '\0';
> +}
> +
> +/**
> + * extract_exit_func_name - Extract the function name from an exit record
> + * @str: The trace line's function column string
> + * @buf: Buffer to store the extracted name
> + * @size: Size of the @buf
> + *
> + * Parse the original function name out of the trailing comment in an
> + * exit trace record.
> + */
> +static void extract_exit_func_name(const char *str, char *buf, size_t size)
> +{
> + const char *p = strchr(str, '}');
> + size_t i = 0;
> +
> + buf[0] = '\0';
> + if (!p)
> + return;
> +
> + p = strstr(p, "/*");
> + if (!p)
> + return;
> +
> + p = skip_spaces(p + 2);
> + while (*p && !isspace(*p) && *p != '*' && *p != '/' && i < size - 1)
> + buf[i++] = *p++;
> + buf[i] = '\0';
> +}
> +
> +/**
> + * make_blank_prefix - Create a blank prefix mirroring a trace record's context
> + * @ftrace: Perf ftrace context
> + * @line: The original trace record
> + * @pipe_char: Pointer to the final delimiter character of the context columns
> + * @buf: Buffer to store the blank prefix
> + * @size: Size of the @buf
> + *
> + * Generate a prefix matching the context columns of @line (e.g. CPU, duration)
> + * but with the duration field replaced by spaces.
> + */
> +static void make_blank_prefix(const struct perf_ftrace *ftrace, const char *line,
> + const char *pipe_char, char *buf, size_t size)
> +{
> + size_t len = pipe_char - line + 1;
> + const char *start_delim = NULL;
> + char *c;
> +
> + if (!size)
> + return;
> +
> + if (len >= size)
> + len = size - 1;
> +
> + memcpy(buf, line, len);
> + buf[len] = '\0';
> +
> + if (*pipe_char == '|') {
> + if (ftrace->graph_verbose) {
> + /* In verbose mode, duration starts after the comm-pid '|' */
> + for (const char *p = pipe_char - 1; p >= line; p--) {
> + if (*p == '|') {
> + start_delim = p;
> + break;
> + }
> + }
> + } else {
> + /* In default mode, duration starts after CPU ')' */
> + start_delim = strchr(line, ')');
> + }
> +
> + if (start_delim && start_delim < pipe_char && start_delim < line + len) {
> + char *end = buf + (pipe_char - line);
> +
> + if (end > buf + len)
> + end = buf + len;
> + for (c = buf + (start_delim - line) + 1; c < end; c++)
> + *c = ' ';
> + }
> + }
> +}
> +
> +/**
> + * find_func_column - Locate the function call column in a trace record
> + * @ftrace: Perf ftrace context
> + * @line: The trace record to parse
> + * @cpu: Pointer to store the CPU number
> + *
> + * Locate the context columns that precede the function call column of a
> + * function_graph trace record. Depending on the funcgraph-* options set
> + * up by perf, a record looks like:
> + *
> + * [ abstime |] CPU) [ comm-pid |] [ duration |] func
> + *
> + * The abstime and comm-pid columns are only present with --graph-opts
> + * verbose.
> + *
> + * Return: A pointer to the last character of the context columns (the final
> + * '|' delimiter). Returns NULL if @line is not a trace record.
> + */
> +static const char *find_func_column(const struct perf_ftrace *ftrace,
> + const char *line, int *cpu)
> +{
> + const char *p = line;
> + char *endptr;
> + long val;
> +
> + if (ftrace->graph_verbose) {
> + p = strchr(p, '|');
> + if (!p)
> + return NULL;
> + p++;
> + }
> +
> + p = skip_spaces(p);
> + val = strtol(p, &endptr, 10);
> + if (endptr == p || *endptr != ')')
> + return NULL;
> + p = endptr;
> +
> + if (ftrace->graph_verbose) {
> + const char *proc_start = p;
> +
> + p = strchr(p, '|');
> + if (!p)
> + return NULL;
> +
> + if (val >= 0 && val < FTRACE_MAX_CPUS) {
> + const char *h = p - 1;
> +
> + while (h >= proc_start && *h != '-')
> + h--;
> + if (h >= proc_start && *h == '-') {
> + long pid_val = strtol(h + 1, NULL, 10);
> + pid_t task_pid = (pid_val == 0) ?
> + FTRACE_IDLE_PID(val) :
> + (pid_t)pid_val;
> +
> + ftrace_cpu_tasks[(int)val] = get_task_state(task_pid);
> + }
> + }
> +
> + /*
> + * In verbose mode, latency-format is also enabled by
> + * set_tracing_funcgraph_verbose(), emitting a "<lat-fmt> |"
> + * column after the comm-pid column.
> + */
> + p = strchr(p + 1, '|');
> + if (!p)
> + return NULL;
> + }
> +
> + p = strchr(p + 1, '|');
> + if (!p)
> + return NULL;
> +
> + *cpu = (int)val;
> + return p;
> +}
> +
> +/**
> + * print_line - Output a trace line verbatim, filtering return addresses if needed
> + * @ftrace: Perf ftrace context
> + * @line: The trace line to output
> + * @out: File stream to output to
> + *
> + * Output @line to @out. If ftrace->filter_retaddr is set, return address
> + * comments are filtered out first.
> + *
> + * Return: 0 on success, or a negative error code (e.g. -ENOMEM) on failure.
> + */
> +static int print_line(struct perf_ftrace *ftrace, const char *line, FILE *out)
> +{
> + char *filtered;
> +
> + if (!ftrace->filter_retaddr) {
> + fprintf(out, "%s\n", line);
> + return 0;
> + }
> +
> + filtered = strdup(line);
> + if (!filtered) {
> + pr_err("Not enough memory\n");
> + return -ENOMEM;
> + }
> +
> + ftrace_filter_retaddr(filtered);
> + fprintf(out, "%s\n", filtered);
> + free(filtered);
> + return 0;
> +}
> +
> +/**
> + * ftrace_process_fgraph_line - Process and output a function graph trace line
> + * @ftrace: Perf ftrace context
> + * @line: The trace line to process
> + * @out: File stream to output to
> + *
> + * Process a trace line from the function_graph tracer. If --inline is set,
> + * this function parses the return address comment, resolves it to its
> + * inlined callchain, and synthesises intermediate frames into the streamed
> + * call graph.
> + *
> + * Return: 0 on success, or a negative error code on failure.
> + */
> +int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FILE *out)
> +{
> + char caller_sym[128];
> + char blank_prefix[128];
> + const char *inlined_names[FTRACE_MAX_INLINES];
> + const char *pipe_char;
> + const char *after_pipe;
> + const char *func_start;
> + struct ftrace_cpu_state *cs;
> + u64 caller_offset = 0;
> + int num_inlines = 0;
> + int cpu = -1;
> + int leading_spaces;
> + int extra_indent;
> + int caller_idx;
> + int active_inlines;
> + int common;
> + int match_idx;
> + int top_raw_indent;
> + int i, j;
> + bool is_exit;
> + bool has_open_brace;
> + bool has_semicolon;
> + bool has_retaddr = false;
> +
> + if (!ftrace->use_inline)
> + return print_line(ftrace, line, out);
> +
> + pipe_char = find_func_column(ftrace, line, &cpu);
> + if (!pipe_char) {
> + parse_context_switch(line);
> + return print_line(ftrace, line, out);
> + }
> +
> + cs = get_cpu_state(ftrace, cpu);
> + if (!cs)
> + return print_line(ftrace, line, out);
> +
> + make_blank_prefix(ftrace, line, pipe_char, blank_prefix, sizeof(blank_prefix));
> +
> + after_pipe = pipe_char + 1;
> + func_start = skip_spaces(after_pipe);
> + leading_spaces = func_start - after_pipe;
> +
> + if (*func_start == '\0')
> + return print_line(ftrace, line, out);
> +
> + is_exit = (*func_start == '}');
> +
> + if (is_exit) {
> + char exit_name[128];
> +
> + extract_exit_func_name(func_start, exit_name, sizeof(exit_name));
> +
> + top_raw_indent = -1;
> + for (i = cs->depth - 1; i >= 0; i--) {
> + if (!cs->stack[i].inlined) {
> + top_raw_indent = cs->stack[i].raw_indent;
> + break;
> + }
> + }
> +
> + /*
> + * If currently overflowed beyond FTRACE_MAX_STACK, any exit whose
> + * indentation is deeper than the top tracked frame belongs to the
> + * overflow region.
> + */
> + if (cs->overflow_depth > 0 &&
> + (top_raw_indent < 0 || leading_spaces > top_raw_indent)) {
> + cs->overflow_depth--;
> + extra_indent = cs->inlined_depth * 2;
> + fprintf(out, "%.*s%*s%s\n",
> + (int)(pipe_char - line + 1), line,
> + leading_spaces + extra_indent, "", func_start);
> + return 0;
> + }
> +
> + /* Find matching frame on stack */
> + match_idx = -1;
> + if (exit_name[0] != '\0') {
> + for (i = cs->depth - 1; i >= 0; i--) {
> + if (!cs->stack[i].inlined && cs->stack[i].name &&
> + strcmp(cs->stack[i].name, exit_name) == 0) {
> + match_idx = i;
> + break;
> + }
> + }
> + }
> +
> + /*
> + * If bare brace ('}'), match the topmost non-inlined frame
> + * with matching indentation.
> + */
> + if (match_idx < 0 && exit_name[0] == '\0') {
> + for (i = cs->depth - 1; i >= 0; i--) {
> + if (!cs->stack[i].inlined &&
> + cs->stack[i].raw_indent == leading_spaces) {
> + match_idx = i;
> + break;
> + }
> + }
> + }
> +
> + if (match_idx >= 0) {
> + cs->overflow_depth = 0;
> + } else if (cs->overflow_depth > 0) {
> + cs->overflow_depth--;
> + extra_indent = cs->inlined_depth * 2;
> + fprintf(out, "%.*s%*s%s\n",
> + (int)(pipe_char - line + 1), line,
> + leading_spaces + extra_indent, "", func_start);
> + return 0;
> + }
> +
> + if (match_idx >= 0) {
> + /* Close all inlined and orphaned frames above match_idx */
> + while (cs->depth > 0 && cs->depth - 1 > match_idx) {
> + struct ftrace_stack_frame *top = &cs->stack[cs->depth - 1];
> +
> + if (top->inlined) {
> + int exit_indent = top->indent;
> +
> + if (cs->inlined_depth > 0)
> + cs->inlined_depth--;
> + fprintf(out, "%s%*s} /* %s (inline) */\n",
> + blank_prefix, exit_indent, "", top->name);
> + }
> + free(top->name);
> + cs->depth--;
> + }
> +
> + /* Pop the matched real frame */
> + if (cs->depth > 0 && match_idx == cs->depth - 1 &&
> + !cs->stack[cs->depth - 1].inlined) {
> + free(cs->stack[cs->depth - 1].name);
> + cs->depth--;
> + }
> +
> + extra_indent = cs->inlined_depth * 2;
> + } else {
> + extra_indent = 0;
> + }
> +
> + fprintf(out, "%.*s%*s%s\n",
> + (int)(pipe_char - line + 1), line,
> + leading_spaces + extra_indent, "", func_start);
> + return 0;
> + }
> +
> + /* It's an entry or a leaf call */
> + has_open_brace = (strchr(func_start, '{') != NULL);
> + has_semicolon = (strchr(func_start, ';') != NULL);
> +
> + top_raw_indent = -1;
> + for (i = cs->depth - 1; i >= 0; i--) {
> + if (!cs->stack[i].inlined) {
> + top_raw_indent = cs->stack[i].raw_indent;
> + break;
> + }
> + }
> +
> + if (cs->overflow_depth > 0 && top_raw_indent >= 0 &&
> + leading_spaces <= top_raw_indent)
> + cs->overflow_depth = 0;
> +
> + if (ftrace_parse_retaddr(func_start, caller_sym, sizeof(caller_sym), &caller_offset)) {
> + has_retaddr = true;
> + num_inlines = ftrace_resolve_inlines(ftrace->machine, caller_sym,
> + caller_offset, inlined_names,
> + FTRACE_MAX_INLINES);
> + if (num_inlines < 0)
> + return num_inlines;
> + }
> +
> + /* Determine which inlined frames are currently open above the caller */
> + caller_idx = -1;
> + if (has_retaddr && cs->overflow_depth == 0) {
> + for (i = cs->depth - 1; i >= 0; i--) {
> + if (!cs->stack[i].inlined && cs->stack[i].name &&
> + cs->stack[i].raw_indent < leading_spaces &&
> + strcmp(cs->stack[i].name, caller_sym) == 0) {
> + caller_idx = i;
> + break;
> + }
> + }
> + }
> +
> + /* If caller is not on the tracked stack, do not synthesise inlines */
> + if (has_retaddr && caller_idx < 0) {
> + for (i = 0; i < num_inlines; i++)
> + free((void *)inlined_names[i]);
> + num_inlines = 0;
> + }
> +
> + active_inlines = 0;
> + if (caller_idx >= 0) {
> + for (i = caller_idx + 1; i < cs->depth; i++) {
> + if (cs->stack[i].inlined)
> + active_inlines++;
> + else
> + break;
> + }
> + }
> +
> + /* Find common prefix of inlined functions */
> + common = 0;
> + if (caller_idx >= 0) {
> + while (common < active_inlines && common < num_inlines &&
> + cs->stack[caller_idx + 1 + common].name &&
> + !strcmp(cs->stack[caller_idx + 1 + common].name,
> + inlined_names[common])) {
> + common++;
> + }
> + }
> +
> + /* Close stale callee frames and inactive inlined frames above caller */
> + if (caller_idx >= 0)
> + cs->overflow_depth = 0;
> +
> + while (caller_idx >= 0 && cs->depth > caller_idx + 1 + common) {
> + struct ftrace_stack_frame *top = &cs->stack[cs->depth - 1];
> +
> + if (top->inlined) {
> + int exit_indent = top->indent;
> +
> + if (cs->inlined_depth > 0)
> + cs->inlined_depth--;
> + fprintf(out, "%s%*s} /* %s (inline) */\n",
> + blank_prefix, exit_indent, "", top->name);
> + }
> + free(top->name);
> + cs->depth--;
> + }
> +
> + if (caller_idx < 0 && cs->overflow_depth == 0) {
> + /*
> + * If caller is not on the tracked stack, unwind any frames whose
> + * raw indentation is greater than or equal to the new entry's
> + * indentation (their exit events were dropped), and close any
> + * trailing inlined frames that were left open from prior calls.
> + */
> + while (cs->depth > 0) {
> + struct ftrace_stack_frame *top = &cs->stack[cs->depth - 1];
> +
> + if (top->inlined) {
> + int exit_indent = top->indent;
> +
> + if (cs->inlined_depth > 0)
> + cs->inlined_depth--;
> + fprintf(out, "%s%*s} /* %s (inline) */\n",
> + blank_prefix, exit_indent, "", top->name);
> + free(top->name);
> + cs->depth--;
> + } else if (top->raw_indent >= leading_spaces) {
> + free(top->name);
> + cs->depth--;
> + } else {
> + break;
> + }
> + }
> + }
> +
> + /* Open new inlined frames */
> + for (i = common; i < num_inlines; i++) {
> + int entry_indent = leading_spaces + cs->inlined_depth * 2;
> +
> + if (!inlined_names[i]) {
> + pr_err("Not enough memory\n");
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + return -ENOMEM;
> + }
> + if (cs->depth >= FTRACE_MAX_STACK)
> + break;
> +
> + fprintf(out, "%s%*s%s() { /* (inline) */\n",
> + blank_prefix, entry_indent, "", inlined_names[i]);
> + cs->stack[cs->depth].name = strdup(inlined_names[i]);
> + if (!cs->stack[cs->depth].name) {
> + pr_err("Not enough memory\n");
> + for (j = 0; j < num_inlines; j++)
> + free((void *)inlined_names[j]);
> + return -ENOMEM;
> + }
> + cs->stack[cs->depth].inlined = true;
> + cs->stack[cs->depth].indent = entry_indent;
> + cs->stack[cs->depth].raw_indent = -1;
> + cs->depth++;
> + cs->inlined_depth++;
> + }
> +
> + /* Print current line with updated indentation */
> + extra_indent = cs->inlined_depth * 2;
> +
> + if (ftrace->filter_retaddr) {
> + char *filtered = strdup(func_start);
> +
> + if (filtered) {
> + ftrace_filter_retaddr(filtered);
> + fprintf(out, "%.*s%*s%s\n",
> + (int)(pipe_char - line + 1), line,
> + leading_spaces + extra_indent, "", filtered);
> + free(filtered);
> + } else {
> + pr_err("Not enough memory\n");
> + for (i = 0; i < num_inlines; i++)
> + free((void *)inlined_names[i]);
> + return -ENOMEM;
> + }
> + } else {
> + fprintf(out, "%.*s%*s%s\n",
> + (int)(pipe_char - line + 1), line,
> + leading_spaces + extra_indent, "", func_start);
> + }
> +
> + /* If non-leaf entry, push onto stack */
> + if (has_open_brace && !has_semicolon) {
> + char entry_name[128];
> +
> + extract_entry_func_name(func_start, entry_name, sizeof(entry_name));
> + if (cs->depth < FTRACE_MAX_STACK) {
> + cs->stack[cs->depth].name = strdup(entry_name);
> + if (!cs->stack[cs->depth].name) {
> + pr_err("Not enough memory\n");
> + for (i = 0; i < num_inlines; i++)
> + free((void *)inlined_names[i]);
> + return -ENOMEM;
> + }
> + cs->stack[cs->depth].inlined = false;
> + cs->stack[cs->depth].indent = leading_spaces + extra_indent;
> + cs->stack[cs->depth].raw_indent = leading_spaces;
> + cs->depth++;
> + } else {
> + cs->overflow_depth++;
> + }
> + }
> +
> + for (i = 0; i < num_inlines; i++)
> + free((void *)inlined_names[i]);
> + return 0;
> +}
> +
> +/**
> + * perf_ftrace__setup_inlines - Initialise symbol resolution for inlines
> + * @ftrace: Perf ftrace context
> + *
> + * Initialise symbol tracking and load the kernel image (vmlinux) for
> + * DWARF debug information parsing.
> + *
> + * Return: 0 on success, or a negative error code on failure.
> + */
> +int perf_ftrace__setup_inlines(struct perf_ftrace *ftrace)
> +{
> + struct map *kmap;
> + struct ftrace_task_state *ts;
> + pid_t single_pid;
> + int c;
> +
> + if (!ftrace->use_inline)
> + return 0;
> +
> + single_pid = ftrace__get_single_pid(ftrace);
> + if (single_pid > 0) {
> + ts = get_task_state(single_pid);
> + if (ts) {
> + for (c = 0; c < FTRACE_MAX_CPUS; c++)
> + ftrace_cpu_tasks[c] = ts;
> + }
> + }
> +
> + symbol_conf.try_vmlinux_path = (symbol_conf.vmlinux_name == NULL);
> + symbol_conf.inline_name = true;
> + if (symbol_conf.vmlinux_name)
> + symbol_conf.ignore_vmlinux_buildid = true;
> +
> + if (symbol__init(NULL) < 0) {
> + pr_warning("Failed to initialize symbols for inlined functions\n");
> + return -1;
> + }
> +
> + ftrace->machine = machine__new_host(NULL);
> + if (!ftrace->machine) {
> + pr_warning("Failed to create host machine for symbols\n");
> + return -1;
> + }
> +
> + if (symbol_conf.vmlinux_name) {
> + kmap = machine__kernel_map(ftrace->machine);
> + if (!kmap ||
> + dso__load_vmlinux(map__dso(kmap), kmap,
> + symbol_conf.vmlinux_name, false) <= 0) {
> + pr_warning("Failed to load kernel image '%s'\n", symbol_conf.vmlinux_name);
> + goto out_delete_machine;
> + }
> + } else {
> + if (machine__load_vmlinux_path(ftrace->machine) <= 0) {
> + pr_warning("Failed to load vmlinux from search path\n");
> + goto out_delete_machine;
> + }
> + }
> +
> + return 0;
> +
> +out_delete_machine:
> + machine__delete(ftrace->machine);
> + ftrace->machine = NULL;
> + return -1;
> +}
> +
> +/**
> + * perf_ftrace__cleanup_inlines - Clean up resources used for inline resolution
> + * @ftrace: Perf ftrace context
> + *
> + * Free the machine context and the CPU stack tracking states.
> + */
> +void perf_ftrace__cleanup_inlines(struct perf_ftrace *ftrace)
> +{
> + if (ftrace->machine) {
> + machine__delete(ftrace->machine);
> + ftrace->machine = NULL;
> + }
> + ftrace_cpu_states__clear();
> +}
> diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
> index 50216d3fef64..4d8a56ce878c 100644
> --- a/tools/perf/util/ftrace.h
> +++ b/tools/perf/util/ftrace.h
> @@ -1,6 +1,7 @@
> #ifndef __PERF_FTRACE_H__
> #define __PERF_FTRACE_H__
>
> +#include <stdio.h>
> #include <linux/list.h>
>
> #include "target.h"
> @@ -41,6 +42,8 @@ struct perf_ftrace {
> int graph_tail;
> bool filter_retaddr;
> bool filter_retaddr_set;
> + bool use_inline;
> + struct machine *machine;
> };
>
> struct filter_entry {
> @@ -95,7 +98,18 @@ perf_ftrace__latency_cleanup_bpf(struct perf_ftrace *ftrace __maybe_unused)
>
> #endif /* HAVE_BPF_SKEL */
>
> +struct machine;
> +
> +#define FTRACE_MAX_INLINES 32
> +
> bool ftrace_parse_retaddr(const char *str, char *sym_name, size_t sym_len, u64 *offset);
> void ftrace_filter_retaddr(char *str);
> +int ftrace_resolve_inlines(struct machine *machine,
> + const char *sym_name, u64 offset,
> + const char **inlined_names, int max_inlines);
> +int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FILE *out);
> +void ftrace_cpu_states__clear(void);
> +int perf_ftrace__setup_inlines(struct perf_ftrace *ftrace);
> +void perf_ftrace__cleanup_inlines(struct perf_ftrace *ftrace);
>
> #endif /* __PERF_FTRACE_H__ */
> --
> 2.55.0
>
next prev parent reply other threads:[~2026-10-22 21:53 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-21 2:33 [PATCH perf-tools-next v9 0/6] perf ftrace: Support inlined functions and display enhancements " Aaron Tomlin
2026-10-21 2:33 ` [PATCH perf-tools-next v9 1/6] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming Aaron Tomlin
2026-10-21 23:56 ` Namhyung Kim
2026-10-22 14:31 ` Aaron Tomlin
2026-10-21 2:33 ` [PATCH perf-tools-next v9 2/6] perf ftrace: Support filtering return address comments Aaron Tomlin
2026-10-22 19:51 ` Namhyung Kim
2026-10-21 2:33 ` [PATCH perf-tools-next v9 3/6] perf symbol: Introduce vmlinux_path__find() helper Aaron Tomlin
2026-10-21 2:33 ` [PATCH perf-tools-next v9 4/6] perf ftrace: Support display of inlined functions in function graph tracer Aaron Tomlin
2026-10-22 21:53 ` Namhyung Kim [this message]
2026-10-21 2:33 ` [PATCH perf-tools-next v9 5/6] perf ftrace: Support omitting execution duration " Aaron Tomlin
2026-10-21 2:33 ` [PATCH perf-tools-next v9 6/6] perf ftrace: Filter extraneous functions " Aaron Tomlin
2026-10-21 16:36 ` [PATCH perf-tools-next v9 0/6] perf ftrace: Support inlined functions and display enhancements " Ian Rogers
2026-10-21 18:23 ` Aaron Tomlin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=atqF8rgKGnaH7EhQ@google.com \
--to=namhyung@kernel.org \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=atomlin@atomlin.com \
--cc=irogers@google.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=peterz@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®