* [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer
@ 2026-10-24 21:25 Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace() Aaron Tomlin
` (9 more replies)
0 siblings, 10 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
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 (such as the scheduler,
memory management, locking primitives, and RCU).
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. /* <-wake_up_new_task+0x1d1/0x3e0 */). 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.
This patch series addresses these challenges across ten commits covering
six key areas:
1. Signal handling and stack frame optimisation in __cmd_ftrace()
Separates robust signal handling (EINTR), non-blocking read
retries, and pager exit handling into a dedicated patch, and
replaces the 4096-byte stack array in __cmd_ftrace() with
dynamic heap allocation alongside struct strbuf streaming,
reducing the stack frame footprint by over 96% (down to
~160 bytes) while ensuring complete line-delimited records.
2. Return address comment filtering
Introduces ftrace_parse_retaddr() and ftrace_filter_retaddr()
to automatically strip caller comments from trace output unless
explicitly requested via --graph-opts retaddr, accompanied by
a unit test suite in tools/perf/tests/ftrace.c.
3. Preparatory vmlinux lookup helper
Introduces vmlinux_path__find() in tools/perf/util/symbol.c and
symbol.h to search standard vmlinux paths and return the first
accessible kernel image, providing a generic helper across
tools/perf and avoiding hardcoded access() checks in tests.
4. Inlined function call graph reconstruction
Introduces --inline and -k/--vmlinux, dynamically synthesising
inlined frames marked with /* (inline) */. Correctly matches
bare closing braces when funcgraph-tail is disabled, clamps stack
depth to prevent indentation drift, and expands the automated test
suite to verify inline resolution, comment filtering, and stack
saturation recovery utilising vmlinux_path__find().
5. Execution duration suppression
Introduces --graph-opts noduration (accepting noduration=[0|1])
to toggle the kernel's funcgraph-duration tracing option,
omitting the duration column when the user is focused purely on
call graph topology and inlined hierarchy. Rejects noduration
when invoked under 'perf ftrace profile'.
6. Extraneous function filtering in function graph tracer
Introduces userspace call graph filtering when targeted
tracing (-G or --graph-funcs) is active. Suppresses transient
unrelated entries arriving before filter activation as well
as detached callee trees (such as ring-buffer wakeups)
originating outside the target hierarchy. Dynamically resizes
the synthetic call stack to eliminate saturation edge cases and
accurately suppresses detached subtrees without orphan exit name
collisions.
Synthesised inlined functions are rendered with an explicit /* (inline) */
annotation at both entry and exit:
# tracer: function_graph
#
# CPU FUNCTION CALLS
# | | | | |
0) enqueue_entity() {
0) update_cfs_group() { /* (inline) */
0) calc_concur_shares() {
0) tg_cpus() {
0) cpuset_num_cpus() {
0) class_rcu_constructor() { /* (inline) */
0) rcu_read_lock() { /* (inline) */
0) __rcu_read_lock();
0) } /* rcu_read_lock (inline) */
0) } /* class_rcu_constructor (inline) */
0) cgroup_e_css();
0) class_rcu_destructor() { /* (inline) */
0) rcu_read_unlock() { /* (inline) */
0) __rcu_read_unlock();
0) } /* rcu_read_unlock (inline) */
0) } /* class_rcu_destructor (inline) */
0) }
0) }
0) __calc_smp_shares();
0) reweight_entity();
0) } /* update_cfs_group (inline) */
0) }
Changes since v9:
- Reorganised the patch series from 6 to 10 commits following review
feedback:
- Split error handling (signals on EINTR, non-blocking read retries,
and pager/pipe exits) out of the strbuf streaming commit into a
dedicated preparatory commit ("perf ftrace: Handle signals,
non-blocking reads and pager exits in __cmd_ftrace()")
(Namhyung Kim)
- Decomposed the monolithic inlined functions commit into four
focused, bisectable commits covering basic task stack management,
context-switch tracking, inlined function display, and automated
unit tests (Namhyung Kim)
- Removed the standalone --filter-retaddr command-line option and
--graph-opts filter-retaddr flag; return address comments are now
automatically stripped from function graph output unless explicitly
requested via --graph-opts retaddr (Namhyung Kim)
- Link to v9: https://lore.kernel.org/lkml/20261021023337.809936-1-atomlin@atomlin.com/
Changes since v8:
- Preserved funcgraph-retval return value comments when filtering
funcgraph-retaddr comments (/* <-caller ret=0x0 */ ->
/* ret=0x0 */) in ftrace_filter_retaddr()
- Enhanced vmlinux_path__find() helper in tools/perf/util/symbol.c:
- Avoided redundant memory allocation in vmlinux_path__init() when
vmlinux_path is already initialised
- Ensured symbol configuration is initialised via symbol__init() prior
to path resolution
- Guaranteed vmlinux_path memory cleanup by invoking
vmlinux_path__exit() unconditionally in symbol__exit()
- Tracked raw (pre-inline-expansion) indentation on synthetic stack
frames, ensuring bare closing braces ("}") are accurately matched
against corresponding non-inlined frames without indentation
distortion
- Unwound stale frames on untracked caller entries (caller_idx < 0) whose
raw indentation is greater than or equal to the new entry, and closed
trailing open inlined frames resulting from dropped exit events
- Pre-bound single-PID tracing across all CPUs in
perf_ftrace__setup_inlines(), ensuring consistent synthetic call stack
tracking across CPU migration when kernel context-switch banners are
suppressed (verif_pid prev_pid == -1)
- Strengthened error handling during DWARF/vmlinux initialisation in
perf_ftrace__setup_inlines(), checking load return codes, emitting
diagnostic warnings, and safely freeing ftrace->machine on failure
- Recognised interrupt/IRQ entry and exit markers ("=====>" and "<=====")
alongside context-switch markers under --graph-opts noduration,
preventing non-function lines from corrupting function column detection
or popping active stack frames
- Handled leaf function lines with trailing comments
(e.g. funcgraph-retaddr or funcgraph-retval comments) in
parse_func_duration() for 'perf ftrace profile', ensuring parentheses
are stripped cleanly and leaf records match non-leaf entries in duration
tracking
- Preserved tracked ancestor frames on cs->stack across dropped target
root exits by verifying caller provenance
(caller_idx < cs->target_root_depth) before checking indentation bounds
in ftrace_check_target_caller()
- Purged stale non-target frames below target root upon target root entry
when caller_idx < 0, preventing unrelated ancestor frames from
prematurely terminating subsequent target calls
- Prevented cs->orphan_depth lockouts when both an orphan exit and
enclosing target root exit are dropped by resetting cs->orphan_depth on
any line at or outside target root indentation
(leading_spaces <= cs->target_root_indent), and allowing ancestor exits
to fall through to match frames on cs->stack
- Preserved interrupt/IRQ markers inside active target callgraphs while
suppressing those outside target filters under -G or --graph-funcs
- Fixed test_stack_saturation() to avoid premature frame unwinding,
ensuring depth clamping and recovery are tested as intended
- Expanded automated test coverage in tools/perf/tests/ftrace.c:
- Added test_context_switch_handling() for CPU migration and task
tracking
- Added test_column_layouts() covering standard, verbose, and
noduration formats including IRQ and context-switch markers
- Added regression tests in test_graph_target_filter() covering double
dropped exits, ancestor frame preservation, leaf duration parsing,
and deep orphan recovery
- Link to v8: https://lore.kernel.org/lkml/20261018023753.584873-1-atomlin@atomlin.com/
Changes since v7:
- Rebased onto latest perf-tools-next/perf-tools-next (commit 1c90f0a3c8a1
("perf kvm: Cast tv_usec to long to fix build on MIPS 32-bits"))
- Link to v7: https://lore.kernel.org/all/20261018021346.582845-1-atomlin@atomlin.com/
Changes since v6:
- Expanded the series from 5 to 6 patches by introducing a standalone
preparatory commit ("perf symbol: Introduce vmlinux_path__find()
helper") in tools/perf/util/symbol.c and symbol.h to locate accessible
kernel images via configured vmlinux paths, replacing ad-hoc
access("vmlinux", R_OK) checks in unit tests (Ian Rogers)
- Explicitly included stdio.h in tools/perf/util/ftrace.h to declare the
FILE type for ftrace_process_fgraph_line(), ensuring clean compilation
under musl libc and non-glibc environments
- Prevented name collisions between orphan callee exits and active target
frames in ftrace_process_fgraph_line()
- Allowed dropped-orphan exit recovery on closing braces only when the
exit explicitly names the target root function itself, ensuring that
lost orphan exits do not swallow the target root's exit while protecting
intermediate callee frames from collisions
- Replaced the fixed-size array in struct ftrace_task_state with a
dynamically resized stack grown via realloc(), completely eliminating
cs->overflow_depth and its associated failure modes:
- Fixed skipped filter teardown when a target function begins at the
stack limit, ensuring standard teardown runs upon root exit and
preventing subsequent sibling functions from leaking into filtered
output
- Fixed orphan filter bypass during deep call stacks by preserving all
caller frames on the stack, ensuring unknown callers (caller_idx < 0)
are accurately identified as orphans and suppressed
- Link to v6: https://lore.kernel.org/all/20261016040310.482876-1-atomlin@atomlin.com/
Changes since v5:
- Prevented call stack desynchronisation on dropped orphan exit events by
clearing orphan_depth when a named exit matches a tracked frame or an
entry descends from the target callgraph
- Fixed premature target filter termination on child exits by capturing
cs->target_root_depth after unwinding stale frames above the caller
- Ensured synthesised caller inlines above cs->target_root_depth are
cleanly closed upon target function exit before resetting filter state
- Prevented legitimate deep calls exceeding FTRACE_MAX_STACK from being
falsely dropped as orphans by checking cs->overflow_depth
- Expanded unit tests to cover dropped orphan exits, target root depth
synchronisation after stale siblings, and deep call stack overflows
- Provided support for glob pattern matching with -G or --graph-funcs
- Link to v5: https://lore.kernel.org/all/20261015011155.414667-1-atomlin@atomlin.com/
Changes since v4:
- Handled non-fatal EAGAIN, EWOULDBLOCK, and EINTR in __cmd_ftrace()
polling and draining loops to prevent premature session termination on
non-blocking reads or signal interruptions
- Eliminated an unused local variable endptr in ftrace_parse_retaddr() by
passing NULL to strtoull()
- Prevented stack buffer overflows in make_blank_prefix() by clamping the
blanking loop bounds and end pointer to the destination buffer length
when processing trace records with exceptionally long prefixes
- Tracked call stack saturation with overflow_depth to prevent untracked
function exits with bare closing braces from prematurely popping tracked
frames from the parser stack on deep call chains exceeding
FTRACE_MAX_STACK
- Ensured stale non-inlined callee frames resulting from dropped function
exits do not block inactive inlined frames from being closed upon
subsequent entries by unwinding the stack down to caller_idx + 1 +
common
- Fixed filter state not resetting for targeted leaf calls when --inline
is active: synthesised inline frames inflated cs->depth beyond
cs->target_root_depth, causing subsequent sibling functions to be
falsely classified as detached orphans and bypassing inline unwinding;
resolved by tracking target root entries (is_target_root), explicitly
unwinding all synthesised inlines, and resetting filter state upon leaf
completion
- Expanded unit test coverage to verify recovery across dropped exit
events and bare closing braces, and to verify that targeted leaf calls
with synthesised inlined frames properly close inlines and reset filter
state
- Link to v4: https://lore.kernel.org/lkml/20261014161457.373253-1-atomlin@atomlin.com/
Changes since v3:
- Explicitly rejected the noduration option when invoked under the profile
subcommand, since profiling inherently requires function duration to
compute timing statistics
- Fixed make_blank_prefix() clobbering the comm-pid column in verbose mode
by searching backwards for the preceding "|" delimiter, and returning
early when graph_noduration is active
- Fixed filter state not resetting for targeted leaf calls at non-zero
call stack depths by checking cs->depth <= cs->target_root_depth and
clearing cs->target_root_depth, preventing subsequent non-targeted
sibling functions within the parent frame from leaking into trace output
- Expanded unit test coverage to verify comm-pid preservation under
verbose and verbose-noduration inline modes, and to verify that targeted
leaf calls at depth > 0 properly reset the target filter state.
- Link to v3: https://lore.kernel.org/lkml/20261012003016.263063-1-atomlin@atomlin.com/
Changes since v2:
- Expanded the series from 4 to 5 patches by adding a dedicated commit
("perf ftrace: Filter extraneous functions in function graph tracer") to
enforce strict target callgraph boundaries when -G or --graph-funcs is
specified
- Suppressed unrelated top-level functions that leak through the transient
tracefs filter initialisation window before RCU synchronisation
completes
- Filtered detached/orphan calls from appearing as nested children within
the target graph by validating caller return addresses against the
active target stack
- Tracked nested orphan scope depth (cs->orphan_depth) to cleanly suppress
child calls and closing delimiters of detached subtrees without
desynchronising the target stack
- Added automated unit test coverage in tools/perf/tests/ftrace.c
(test_graph_target_filter()) validating top-level suppression (both with
and without --inline) and detached orphan wakeup suppression
- Link to v2: https://lore.kernel.org/lkml/20261006232756.65620-1-atomlin@atomlin.com/
Changes since v1:
- Modularised the implementation into a 4-patch bisectable series rather
than a monolithic commit
- Resolved potential infinite loop on poll() upon trace_pipe EOF
- Fixed stuck call stack frames when funcgraph-tail is disabled by
supporting bare closing delimiters matching LIFO stack frames
- Prevented unbounded indentation growth on deep call trees by clamping
inline expansion and synchronising cs->inlined_depth to frames
successfully pushed onto cs->stack
- Added dedicated unit tests covering bare closing braces and stack
saturation recovery
- Refined option documentation in perf-ftrace.txt
- Link to v1: https://lore.kernel.org/lkml/20261005211518.26786-1-atomlin@atomlin.com/
Aaron Tomlin (10):
perf ftrace: Handle signals, non-blocking reads and pager exits in
__cmd_ftrace()
perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming
perf ftrace: Support filtering return address comments
perf ftrace: Add basic task call stack management
perf ftrace: Support context switch tracking in function graph tracer
perf ftrace: Support display of inlined functions in function graph
tracer
perf symbol: Introduce vmlinux_path__find() helper
perf test: Add test for ftrace inline and task stack tracking
perf ftrace: Support omitting execution duration in function graph
tracer
perf ftrace: Filter extraneous functions in function graph tracer
tools/perf/Documentation/perf-ftrace.txt | 14 +
tools/perf/builtin-ftrace.c | 202 +-
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/ftrace.c | 3744 ++++++++++++++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/Build | 1 +
tools/perf/util/ftrace.c | 1736 ++++++++++
tools/perf/util/ftrace.h | 28 +
tools/perf/util/symbol.c | 38 +-
tools/perf/util/symbol.h | 2 +
11 files changed, 5739 insertions(+), 29 deletions(-)
create mode 100644 tools/perf/tests/ftrace.c
create mode 100644 tools/perf/util/ftrace.c
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace()
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 02/10] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming Aaron Tomlin
` (8 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
In __cmd_ftrace(), trace_fd is configured for non-blocking I/O via
fcntl(trace_fd, F_SETFL, O_NONBLOCK) and polled using poll(). However,
the polling loop previously treated any negative return code from poll()
or read() as an unrecoverable failure.
If poll() or read() is interrupted by a non-fatal signal (i.e. SIGWINCH
upon terminal resizing), errno is set to EINTR. Furthermore, read() on
trace_pipe can return EAGAIN/EWOULDBLOCK even after poll() indicates
POLLIN (for instance, under concurrent readers, spurious wakeups, or
block-mode trace iterator options). In all such cases, breaking from the
loop causes perf ftrace to terminate tracing prematurely.
Additionally, output errors on stdout (i.e. when a pager or consumer
process exits, closing the downstream pipe) were ignored by fflush(),
leaving __cmd_ftrace() polling indefinitely until the workload
completes.
Resolve these concerns by retrying poll() upon receiving EINTR, retrying
read() on EAGAIN, EWOULDBLOCK, and EINTR, and breaking on EOF (n == 0).
Furthermore, detect stdout errors and broken pipes via fflush() and
ferror() to exit immediately when the downstream output sink closes, and
handle EINTR and ferror(stdout) cleanly in the subsequent buffer
draining loop.
Fixes: d01f4e8db22c ("perf ftrace: Introduce new 'ftrace' tool")
Cc: stable@vger.kernel.org
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/builtin-ftrace.c | 27 ++++++++++++++++++++++-----
1 file changed, 22 insertions(+), 5 deletions(-)
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index 6017493ff179..7cdc9ae2fbbb 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -14,6 +14,7 @@
#include <poll.h>
#include <semaphore.h>
#include <signal.h>
+#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <linux/capability.h>
@@ -806,17 +807,27 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
while (!done) {
- if (poll(&pollfd, 1, -1) < 0)
+ if (poll(&pollfd, 1, -1) < 0) {
+ if (errno == EINTR)
+ continue;
break;
+ }
if (pollfd.revents & POLLIN) {
int n = read(trace_fd, buf, sizeof(buf));
- if (n < 0)
+
+ if (n < 0) {
+ if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
+ continue;
+ break;
+ }
+ if (n == 0)
break;
if (fwrite(buf, n, 1, stdout) != 1)
break;
/* flush output since stdout is in full buffering mode due to pager */
- fflush(stdout);
+ if (fflush(stdout) < 0 || ferror(stdout))
+ break;
}
}
@@ -831,9 +842,15 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
/* read remaining buffer contents */
- while (true) {
+ while (!ferror(stdout)) {
int n = read(trace_fd, buf, sizeof(buf));
- if (n <= 0)
+
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ break;
+ }
+ if (n == 0)
break;
if (fwrite(buf, n, 1, stdout) != 1)
break;
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 02/10] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace() Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 03/10] perf ftrace: Support filtering return address comments Aaron Tomlin
` (7 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
In __cmd_ftrace(), incoming trace data from trace_pipe was historically
buffered in a fixed 4096-byte array allocated directly on the stack
frame. Furthermore, reads were written directly to stdout via fwrite()
in arbitrary chunk sizes, rather than as discrete, well-delimited
trace lines.
Refactor __cmd_ftrace() to employ dynamic heap allocation for the read
buffer alongside a dynamic struct strbuf to accumulate streamed
characters into whole lines. Pre-allocating the strbuf ensures that
line processing incurs zero heap reallocations during steady-state
polling, whilst the stack frame of __cmd_ftrace() is reduced by over 96%
(down to ~160 bytes). Emitting complete lines provides discrete records
necessary for subsequent call graph parsing and inlined function
reconstruction.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/builtin-ftrace.c | 56 ++++++++++++++++++++++++++++++-------
1 file changed, 46 insertions(+), 10 deletions(-)
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index 7cdc9ae2fbbb..65983259b07e 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -42,7 +42,10 @@
#include "util/units.h"
#include "util/parse-sublevel-options.h"
+#include "util/strbuf.h"
+
#define DEFAULT_TRACER "function_graph"
+#define TRACE_BUF_SIZE 4096
static volatile sig_atomic_t workload_exec_errno;
static volatile sig_atomic_t done;
@@ -739,7 +742,8 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
{
char *trace_file;
int trace_fd;
- char buf[4096];
+ char *buf = NULL;
+ struct strbuf linebuf = STRBUF_INIT;
struct pollfd pollfd = {
.events = POLLIN,
};
@@ -768,10 +772,18 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
setup_pager();
+ buf = malloc(TRACE_BUF_SIZE);
+ if (!buf) {
+ pr_err("failed to allocate trace buffer\n");
+ goto out_reset;
+ }
+ strbuf_init(&linebuf, 512);
+ strbuf_setlen(&linebuf, 0);
+
trace_file = get_tracing_instance_file("trace_pipe");
if (!trace_file) {
pr_err("failed to open trace_pipe\n");
- goto out_reset;
+ goto out_free;
}
trace_fd = open(trace_file, O_RDONLY);
@@ -780,7 +792,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
if (trace_fd < 0) {
pr_err("failed to open trace_pipe\n");
- goto out_reset;
+ goto out_free;
}
fcntl(trace_fd, F_SETFL, O_NONBLOCK);
@@ -814,7 +826,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
if (pollfd.revents & POLLIN) {
- int n = read(trace_fd, buf, sizeof(buf));
+ int n = read(trace_fd, buf, TRACE_BUF_SIZE);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
@@ -823,8 +835,16 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
if (n == 0)
break;
- if (fwrite(buf, n, 1, stdout) != 1)
- break;
+ for (int i = 0; i < n; i++) {
+ if (buf[i] == '\n') {
+ if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
+ break;
+ strbuf_setlen(&linebuf, 0);
+ } else {
+ if (strbuf_addch(&linebuf, buf[i]) < 0)
+ goto out_close_fd;
+ }
+ }
/* flush output since stdout is in full buffering mode due to pager */
if (fflush(stdout) < 0 || ferror(stdout))
break;
@@ -834,7 +854,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
write_tracing_file("tracing_on", "0");
if (workload_exec_errno) {
- const char *emsg = str_error_r(workload_exec_errno, buf, sizeof(buf));
+ const char *emsg = str_error_r(workload_exec_errno, buf, TRACE_BUF_SIZE);
/* flush stdout first so below error msg appears at the end. */
fflush(stdout);
pr_err("workload failed: %s\n", emsg);
@@ -843,7 +863,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
/* read remaining buffer contents */
while (!ferror(stdout)) {
- int n = read(trace_fd, buf, sizeof(buf));
+ int n = read(trace_fd, buf, TRACE_BUF_SIZE);
if (n < 0) {
if (errno == EINTR)
@@ -852,12 +872,28 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
if (n == 0)
break;
- if (fwrite(buf, n, 1, stdout) != 1)
- break;
+ for (int i = 0; i < n; i++) {
+ if (buf[i] == '\n') {
+ if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
+ break;
+ strbuf_setlen(&linebuf, 0);
+ } else {
+ if (strbuf_addch(&linebuf, buf[i]) < 0)
+ goto out_close_fd;
+ }
+ }
+ }
+
+ if (!ferror(stdout) && linebuf.len > 0) {
+ fprintf(stdout, "%s\n", linebuf.buf);
+ strbuf_setlen(&linebuf, 0);
}
out_close_fd:
close(trace_fd);
+out_free:
+ free(buf);
+ strbuf_release(&linebuf);
out_reset:
exit_tracing_instance();
out:
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 03/10] perf ftrace: Support filtering return address comments
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace() Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 02/10] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 04/10] perf ftrace: Add basic task call stack management Aaron Tomlin
` (6 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
When tracing with the Linux kernel's function graph tracer, enabling
funcgraph-retaddr records the return address of each function entry as
a comment in the trace stream (e.g. /* <-load_elf_phdrs+0x6c/0xb0 */).
Whilst valuable for tracing execution origins, these trailing comments
can add considerable visual clutter to the call graph, particularly
when examining deeply nested call trees or when synthesising higher-level
abstractions.
Introduce support for filtering out return address comments from
function graph trace output whenever return address display is not
explicitly requested via --graph-opts retaddr.
Key aspects of this implementation:
1. Return address parsing and filtering
Implement ftrace_filter_retaddr() in tools/perf/util/ftrace.c
to locate return address comments and strip them in-place with
memmove(3).
Additionally, introduce ftrace_parse_retaddr() to parse caller
symbol names and offsets across diverse output styles (standard
format, return values, and bracketed addresses), establishing
the parsing foundation for subsequent inlined callchain
resolution.
2. Automatic comment filtering
In __cmd_ftrace(), filter return address comments from whole
trace lines whenever !ftrace->graph_retaddr, ensuring clean
output unless the user explicitly requests return address
comments via --graph-opts retaddr.
3. Automated verification
Add a dedicated unit test suite ("Ftrace return address
processing") in tools/perf/tests/ftrace.c to validate comment
parsing and string filtering across all supported formats.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/builtin-ftrace.c | 6 +++
tools/perf/tests/Build | 1 +
tools/perf/tests/builtin-test.c | 1 +
tools/perf/tests/ftrace.c | 81 ++++++++++++++++++++++++++++
tools/perf/tests/tests.h | 1 +
tools/perf/util/Build | 1 +
tools/perf/util/ftrace.c | 93 +++++++++++++++++++++++++++++++++
tools/perf/util/ftrace.h | 3 ++
8 files changed, 187 insertions(+)
create mode 100644 tools/perf/tests/ftrace.c
create mode 100644 tools/perf/util/ftrace.c
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index 65983259b07e..e00621703dd7 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -837,6 +837,8 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
break;
for (int i = 0; i < n; i++) {
if (buf[i] == '\n') {
+ if (!ftrace->graph_retaddr)
+ ftrace_filter_retaddr(linebuf.buf);
if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
break;
strbuf_setlen(&linebuf, 0);
@@ -874,6 +876,8 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
break;
for (int i = 0; i < n; i++) {
if (buf[i] == '\n') {
+ if (!ftrace->graph_retaddr)
+ ftrace_filter_retaddr(linebuf.buf);
if (fprintf(stdout, "%s\n", linebuf.buf) < 0)
break;
strbuf_setlen(&linebuf, 0);
@@ -885,6 +889,8 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
}
if (!ferror(stdout) && linebuf.len > 0) {
+ if (!ftrace->graph_retaddr)
+ ftrace_filter_retaddr(linebuf.buf);
fprintf(stdout, "%s\n", linebuf.buf);
strbuf_setlen(&linebuf, 0);
}
diff --git a/tools/perf/tests/Build b/tools/perf/tests/Build
index 395f2866094a..136138932fcb 100644
--- a/tools/perf/tests/Build
+++ b/tools/perf/tests/Build
@@ -72,6 +72,7 @@ perf-test-y += hwmon_pmu.o
perf-test-y += tool_pmu.o
perf-test-y += subcmd-help.o
perf-test-y += kallsyms-split.o
+perf-test-y += ftrace.o
ifeq ($(SRCARCH),$(filter $(SRCARCH),x86 arm arm64 powerpc riscv))
perf-test-$(CONFIG_DWARF_UNWIND) += dwarf-unwind.o
diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
index 880eece0bc12..640d6d327215 100644
--- a/tools/perf/tests/builtin-test.c
+++ b/tools/perf/tests/builtin-test.c
@@ -156,6 +156,7 @@ static struct test_suite *generic_tests[] = {
&suite__perf_hooks,
&suite__unit_number__scnprint,
&suite__mem2node,
+ &suite__ftrace,
&suite__time_utils,
&suite__jit_write_elf,
&suite__pfm,
diff --git a/tools/perf/tests/ftrace.c b/tools/perf/tests/ftrace.c
new file mode 100644
index 000000000000..42604811a2fb
--- /dev/null
+++ b/tools/perf/tests/ftrace.c
@@ -0,0 +1,81 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/compiler.h>
+#include <linux/kernel.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include "tests.h"
+#include "debug.h"
+#include "util/ftrace.h"
+
+static int test_parse_retaddr(void)
+{
+ char sym[128];
+ u64 offset = 0;
+
+ /* Standard format */
+ TEST_ASSERT_VAL("parse standard retaddr",
+ ftrace_parse_retaddr("kernel_read() { /* <-load_elf_phdrs+0x6c/0xb0 */",
+ sym, sizeof(sym), &offset));
+ TEST_ASSERT_EQUAL("sym name", strcmp(sym, "load_elf_phdrs"), 0);
+ TEST_ASSERT_VAL("offset", offset == 0x6c);
+
+ /* With return value */
+ TEST_ASSERT_VAL("parse retaddr with ret",
+ ftrace_parse_retaddr(
+ "__cond_resched(); /* <-load_elf_phdrs+0x6c/0xb0 ret=0x0 */",
+ sym, sizeof(sym), &offset));
+ TEST_ASSERT_EQUAL("sym name", strcmp(sym, "load_elf_phdrs"), 0);
+ TEST_ASSERT_VAL("offset", offset == 0x6c);
+
+ /* Bracketed address */
+ TEST_ASSERT_VAL("parse bracketed retaddr",
+ ftrace_parse_retaddr("foo() { /* <-[0xffffffff818534c8] bar+0x20/0x40 */",
+ sym, sizeof(sym), &offset));
+ TEST_ASSERT_EQUAL("sym name", strcmp(sym, "bar"), 0);
+ TEST_ASSERT_VAL("offset", offset == 0x20);
+
+ /* No return address */
+ TEST_ASSERT_VAL("no retaddr",
+ !ftrace_parse_retaddr("do_filp_open();", sym, sizeof(sym), &offset));
+
+ return TEST_OK;
+}
+
+static int test_filter_retaddr(void)
+{
+ char line1[] = "kernel_read() { /* <-load_elf_phdrs+0x6c/0xb0 */";
+ char line2[] = "__cond_resched(); /* <-load_elf_phdrs+0x6c/0xb0 ret=0x0 */";
+ char line3[] = "do_filp_open();";
+
+ ftrace_filter_retaddr(line1);
+ TEST_ASSERT_EQUAL("filter standard retaddr", strcmp(line1, "kernel_read() {"), 0);
+
+ ftrace_filter_retaddr(line2);
+ TEST_ASSERT_EQUAL("filter retaddr with ret",
+ strcmp(line2, "__cond_resched(); /* ret=0x0 */"), 0);
+
+ ftrace_filter_retaddr(line3);
+ TEST_ASSERT_EQUAL("filter line without retaddr unchanged",
+ strcmp(line3, "do_filp_open();"), 0);
+
+ return TEST_OK;
+}
+
+static int test__ftrace(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ int ret;
+
+ ret = test_parse_retaddr();
+ if (ret != TEST_OK)
+ return ret;
+
+ ret = test_filter_retaddr();
+ if (ret != TEST_OK)
+ return ret;
+
+ return TEST_OK;
+}
+
+DEFINE_SUITE("Ftrace return address processing", ftrace);
diff --git a/tools/perf/tests/tests.h b/tools/perf/tests/tests.h
index 5982c4e62cd3..cd12ae85ccfa 100644
--- a/tools/perf/tests/tests.h
+++ b/tools/perf/tests/tests.h
@@ -180,6 +180,7 @@ DECLARE_SUITE(perf_hooks);
DECLARE_SUITE(unit_number__scnprint);
DECLARE_SUITE(mem2node);
DECLARE_SUITE(maps);
+DECLARE_SUITE(ftrace);
DECLARE_SUITE(time_utils);
DECLARE_SUITE(jit_write_elf);
DECLARE_SUITE(api_io);
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 2c1f880c4c47..5b082d09006d 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -169,6 +169,7 @@ perf-util-y += list_sort.o
perf-util-y += mutex.o
perf-util-y += sharded_mutex.o
perf-util-y += intel-tpebs.o
+perf-util-y += ftrace.o
perf-util-$(CONFIG_PERF_BPF_SKEL) += bpf_counter.o
perf-util-$(CONFIG_PERF_BPF_SKEL) += bpf_counter_cgroup.o
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
new file mode 100644
index 000000000000..452231c01568
--- /dev/null
+++ b/tools/perf/util/ftrace.c
@@ -0,0 +1,93 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/compiler.h>
+#include <linux/kernel.h>
+#include <linux/string.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include "util/ftrace.h"
+
+/**
+ * ftrace_parse_retaddr - Parse return address comment from trace line
+ * @str: The trace line containing the comment
+ * @sym_name: Buffer to store the parsed symbol name
+ * @sym_len: Length of the @sym_name buffer
+ * @offset: Pointer to store the parsed offset
+ *
+ * Extract the function name and offset from a funcgraph-retaddr comment
+ * (e.g. "<-load_elf_phdrs+0x6c/0xb0").
+ *
+ * Return: true if successfully parsed, false otherwise.
+ */
+bool ftrace_parse_retaddr(const char *str, char *sym_name, size_t sym_len, u64 *offset)
+{
+ const char *p = strstr(str, "<-");
+ const char *plus;
+ const char *bracket;
+ size_t name_len;
+
+ if (!p)
+ return false;
+
+ p += 2;
+ while (*p == ' ')
+ p++;
+
+ if (*p == '[') {
+ bracket = strchr(p, ']');
+ if (bracket)
+ p = bracket + 1;
+ while (*p == ' ')
+ p++;
+ }
+
+ plus = strchr(p, '+');
+ if (!plus)
+ return false;
+
+ name_len = plus - p;
+ if (name_len == 0 || name_len >= sym_len)
+ return false;
+
+ memcpy(sym_name, p, name_len);
+ sym_name[name_len] = '\0';
+
+ *offset = strtoull(plus + 1, NULL, 16);
+ return true;
+}
+
+/**
+ * ftrace_filter_retaddr - Remove return address comment from trace line
+ * @str: The trace line to modify
+ *
+ * Find and strip the funcgraph-retaddr comment from the trace line
+ * in-place, removing trailing spaces where applicable.
+ */
+void ftrace_filter_retaddr(char *str)
+{
+ char *start = strstr(str, "/* <-");
+ char *end;
+ char *ret_ptr;
+
+ if (!start)
+ return;
+
+ end = strstr(start, "*/");
+ if (!end)
+ return;
+
+ ret_ptr = strstr(start, " ret=");
+ if (ret_ptr && ret_ptr < end) {
+ memmove(start + 3, ret_ptr + 1, strlen(ret_ptr + 1) + 1);
+ return;
+ }
+
+ end += 2; /* skip end-of-comment delimiter */
+
+ /* Also backtrack any spaces before comment */
+ while (start > str && *(start - 1) == ' ')
+ start--;
+
+ memmove(start, end, strlen(end) + 1);
+}
diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
index 950f2efafad2..a3a679f74e9d 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -93,4 +93,7 @@ perf_ftrace__latency_cleanup_bpf(struct perf_ftrace *ftrace __maybe_unused)
#endif /* HAVE_BPF_SKEL */
+bool ftrace_parse_retaddr(const char *str, char *sym_name, size_t sym_len, u64 *offset);
+void ftrace_filter_retaddr(char *str);
+
#endif /* __PERF_FTRACE_H__ */
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 04/10] perf ftrace: Add basic task call stack management
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (2 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 03/10] perf ftrace: Support filtering return address comments Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 05/10] perf ftrace: Support context switch tracking in function graph tracer Aaron Tomlin
` (5 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
The function_graph tracer records kernel function entries and exits with
nested indentation. To perform call graph analysis and subsequent inlined
function reconstruction, userspace must track the active call stack on a
per-task and per-CPU basis.
Introduce basic call stack management for the function graph tracer:
1. Data structures
Define struct ftrace_stack_frame to track function names,
indentation, and inlined status.
Define struct ftrace_task_state with a dynamically resisable call
stack array, tracked in a hash table keyed by PID alongside per-CPU
active task bindings (ftrace_cpu_tasks).
2. Trace stream processing
Implement find_func_column() to isolate context columns (CPU,
timestamps, duration) from function graph trace lines across
standard and verbose layouts.
Implement ftrace_process_fgraph_line() to parse function entries
("{") and exits ("}"), pushing and popping frames on the tracked
stack and matching bare closing braces against preceding frames.
3. Integration in __cmd_ftrace()
Stream complete trace lines through ftrace_process_fgraph_line()
in __cmd_ftrace() and clean up allocated CPU states on exit via
ftrace_cpu_states__clear().
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/builtin-ftrace.c | 40 ++--
tools/perf/util/ftrace.c | 394 ++++++++++++++++++++++++++++++++++++
tools/perf/util/ftrace.h | 30 +++
3 files changed, 452 insertions(+), 12 deletions(-)
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index e00621703dd7..e3909cff287b 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -744,6 +744,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,
};
@@ -837,14 +838,20 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
break;
for (int i = 0; i < n; i++) {
if (buf[i] == '\n') {
- if (!ftrace->graph_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 +883,30 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
break;
for (int i = 0; i < n; i++) {
if (buf[i] == '\n') {
- if (!ftrace->graph_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->graph_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 +916,10 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
free(buf);
strbuf_release(&linebuf);
out_reset:
+ ftrace_cpu_states__clear();
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[],
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
index 452231c01568..95db57e70d11 100644
--- a/tools/perf/util/ftrace.c
+++ b/tools/perf/util/ftrace.c
@@ -2,11 +2,146 @@
#include <linux/compiler.h>
#include <linux/kernel.h>
#include <linux/string.h>
+#include <linux/zalloc.h>
+#include <linux/hash.h>
+#include <linux/list.h>
+#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
+#include <errno.h>
+#include <sys/types.h>
+#include "debug.h"
#include "util/ftrace.h"
+#include "util/string2.h"
+
+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,
+ * 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);
+ free(ts->stack);
+ 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->stack = calloc(FTRACE_MAX_STACK, sizeof(*ts->stack));
+ if (!ts->stack) {
+ free(ts);
+ return NULL;
+ }
+ ts->max_stack = FTRACE_MAX_STACK;
+
+ 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__grow_stack - Double the capacity of a task's call stack
+ * @cs: Task state whose stack should be resized
+ *
+ * Reallocate @cs->stack with twice the current capacity, zero-initialising
+ * the newly allocated frames.
+ *
+ * Return: 0 on success, or -ENOMEM if memory allocation fails.
+ */
+static int ftrace_task_state__grow_stack(struct ftrace_task_state *cs)
+{
+ int new_max = cs->max_stack ? cs->max_stack * 2 : FTRACE_MAX_STACK;
+ struct ftrace_stack_frame *new_stack;
+
+ new_stack = realloc(cs->stack, new_max * sizeof(*new_stack));
+ if (!new_stack) {
+ pr_err("Not enough memory to resize ftrace stack\n");
+ return -ENOMEM;
+ }
+
+ memset(new_stack + cs->max_stack, 0, (new_max - cs->max_stack) * sizeof(*new_stack));
+ cs->stack = new_stack;
+ cs->max_stack = new_max;
+ return 0;
+}
+
+/**
+ * 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 __maybe_unused, int cpu)
+{
+ if (cpu < 0 || cpu >= FTRACE_MAX_CPUS)
+ return NULL;
+
+ if (!ftrace_cpu_tasks[cpu])
+ ftrace_cpu_tasks[cpu] = get_task_state(FTRACE_UNKNOWN_PID(cpu));
+
+ return ftrace_cpu_tasks[cpu];
+}
/**
* ftrace_parse_retaddr - Parse return address comment from trace line
@@ -91,3 +226,262 @@ void ftrace_filter_retaddr(char *str)
memmove(start, end, strlen(end) + 1);
}
+
+/**
+ * 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';
+}
+
+/**
+ * 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.
+ *
+ * Return: A pointer to the last character of the context columns (the final
+ * '|' delimiter, or the CPU ')' when no delimiter follows it). 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) {
+ p = strchr(p, '|');
+ if (!p)
+ return NULL;
+
+ 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. Unless ftrace->graph_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->graph_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
+ *
+ * Parse function entries and exits, maintain the task's call stack, and
+ * output the trace line.
+ *
+ * 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)
+{
+ const char *pipe_char;
+ const char *after_pipe;
+ const char *func_start;
+ struct ftrace_task_state *cs;
+ int cpu = -1;
+ int leading_spaces;
+ int top_raw_indent;
+ int match_idx;
+ int i;
+ bool is_exit;
+ bool has_open_brace;
+ bool has_semicolon;
+
+ pipe_char = find_func_column(ftrace, line, &cpu);
+ if (!pipe_char)
+ return print_line(ftrace, line, out);
+
+ cs = get_cpu_state(ftrace, cpu);
+ if (!cs)
+ return print_line(ftrace, line, out);
+
+ 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--) {
+ 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--;
+ return print_line(ftrace, line, out);
+ }
+
+ match_idx = -1;
+ if (exit_name[0] != '\0') {
+ for (i = cs->depth - 1; i >= 0; i--) {
+ if (cs->stack[i].name &&
+ cs->stack[i].raw_indent == leading_spaces &&
+ strcmp(cs->stack[i].name, exit_name) == 0) {
+ match_idx = i;
+ break;
+ }
+ }
+ }
+
+ if (match_idx < 0 && exit_name[0] == '\0') {
+ for (i = cs->depth - 1; i >= 0; i--) {
+ if (cs->stack[i].raw_indent == leading_spaces) {
+ match_idx = i;
+ break;
+ }
+ }
+ }
+
+ if (match_idx >= 0) {
+ cs->overflow_depth = 0;
+ while (cs->depth > match_idx) {
+ free(cs->stack[cs->depth - 1].name);
+ cs->depth--;
+ }
+ } else if (cs->overflow_depth > 0) {
+ cs->overflow_depth--;
+ }
+
+ return print_line(ftrace, line, out);
+ }
+
+ 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--) {
+ 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 (has_open_brace && !has_semicolon) {
+ char entry_name[128];
+
+ extract_entry_func_name(func_start, entry_name, sizeof(entry_name));
+ if (cs->depth >= cs->max_stack &&
+ ftrace_task_state__grow_stack(cs) < 0) {
+ cs->overflow_depth++;
+ } else {
+ cs->stack[cs->depth].name = strdup(entry_name);
+ if (!cs->stack[cs->depth].name) {
+ pr_err("Not enough memory\n");
+ return -ENOMEM;
+ }
+ cs->stack[cs->depth].inlined = false;
+ cs->stack[cs->depth].indent = leading_spaces;
+ cs->stack[cs->depth].raw_indent = leading_spaces;
+ cs->depth++;
+ }
+ }
+
+ return print_line(ftrace, line, out);
+}
diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
index a3a679f74e9d..fa2974f46582 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -1,6 +1,9 @@
#ifndef __PERF_FTRACE_H__
#define __PERF_FTRACE_H__
+#include <stdbool.h>
+#include <stdio.h>
+#include <sys/types.h>
#include <linux/list.h>
#include "target.h"
@@ -93,7 +96,34 @@ perf_ftrace__latency_cleanup_bpf(struct perf_ftrace *ftrace __maybe_unused)
#endif /* HAVE_BPF_SKEL */
+#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;
+ int max_stack;
+ int depth;
+ int inlined_depth;
+ int overflow_depth;
+};
+
bool ftrace_parse_retaddr(const char *str, char *sym_name, size_t sym_len, u64 *offset);
void ftrace_filter_retaddr(char *str);
+void ftrace_cpu_states__clear(void);
+int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FILE *out);
#endif /* __PERF_FTRACE_H__ */
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 05/10] perf ftrace: Support context switch tracking in function graph tracer
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (3 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 04/10] perf ftrace: Add basic task call stack management Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 06/10] perf ftrace: Support display of inlined functions " Aaron Tomlin
` (4 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
When tracing multiple tasks across multiple CPUs, kernel context switches
cause different threads of execution to interleave on the same CPU. Without
context switch awareness, interleaved function calls corrupt the tracked
call stack.
Add context switch detection and per-task state tracking:
1. Context switch banner parsing
Implement parse_context_switch() to parse context switch banners
(e.g. " 0) <idle>-0 => migrati-19") in the trace stream, extracting
the CPU number and both outgoing and incoming task PIDs.
2. Task state rekeying and migration
Implement ftrace_task_state__rekey() to reassign initial unknown
task states to real PIDs upon discovering outgoing task identity,
pruning exited frames from older states and merging active call
stacks when a task migrates across CPUs.
3. Verbose mode and single-PID binding
In find_func_column(), parse the comm-pid column in verbose mode
to bind active task state on each line record.
In get_cpu_state(), pre-bind single-PID workloads across all CPUs
when context-switch banners are absent.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/util/ftrace.c | 284 ++++++++++++++++++++++++++++++++++++++-
tools/perf/util/ftrace.h | 2 +
2 files changed, 282 insertions(+), 4 deletions(-)
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
index 95db57e70d11..fea61a9e17b1 100644
--- a/tools/perf/util/ftrace.c
+++ b/tools/perf/util/ftrace.c
@@ -13,11 +13,14 @@
#include <errno.h>
#include <sys/types.h>
#include "debug.h"
+#include "evlist.h"
#include "util/ftrace.h"
#include "util/string2.h"
+#include <perf/threadmap.h>
static struct hlist_head ftrace_task_hash[FTRACE_PID_HASH_SIZE];
static struct ftrace_task_state *ftrace_cpu_tasks[FTRACE_MAX_CPUS];
+static u64 ftrace_event_seq;
/**
* ftrace_cpu_states__clear - Free allocated CPU and task stack states
@@ -42,6 +45,7 @@ void ftrace_cpu_states__clear(void)
}
memset(ftrace_cpu_tasks, 0, sizeof(ftrace_cpu_tasks));
+ ftrace_event_seq = 0;
}
/**
@@ -92,6 +96,7 @@ static struct ftrace_task_state *get_task_state(pid_t pid)
ts->max_stack = FTRACE_MAX_STACK;
ts->pid = pid;
+ ts->min_raw_indent = -1;
key = hash_32((u32)pid, FTRACE_PID_HASH_BITS);
head = &ftrace_task_hash[key];
hlist_add_head(&ts->node, head);
@@ -124,6 +129,157 @@ static int ftrace_task_state__grow_stack(struct ftrace_task_state *cs)
return 0;
}
+/**
+ * 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 j, c;
+ int to_copy;
+
+ if (ts->pid == new_pid)
+ return ts;
+
+ hlist_del_init(&ts->node);
+
+ existing = find_task_state(new_pid);
+ if (existing) {
+ struct ftrace_task_state *older, *newer;
+ int base_raw_indent;
+
+ for (c = 0; c < FTRACE_MAX_CPUS; c++) {
+ if (ftrace_cpu_tasks[c] == ts)
+ ftrace_cpu_tasks[c] = existing;
+ }
+
+ if (ts->seq < existing->seq) {
+ older = ts;
+ newer = existing;
+ } else {
+ older = existing;
+ newer = ts;
+ }
+
+ base_raw_indent = -1;
+ if (newer->depth > 0)
+ base_raw_indent = newer->stack[0].raw_indent;
+ else if (newer->min_raw_indent >= 0)
+ base_raw_indent = newer->min_raw_indent;
+
+ /*
+ * Prune frames on the older stack that have already exited
+ * (i.e. whose raw_indent >= base_raw_indent, along with any
+ * synthesized inlined frames).
+ */
+ if (base_raw_indent >= 0) {
+ while (older->depth > 0) {
+ struct ftrace_stack_frame *top =
+ &older->stack[older->depth - 1];
+
+ if (top->inlined) {
+ free(top->name);
+ if (older->inlined_depth > 0)
+ older->inlined_depth--;
+ older->depth--;
+ } else if (top->raw_indent >= base_raw_indent) {
+ free(top->name);
+ older->depth--;
+ } else {
+ break;
+ }
+ }
+ }
+
+ to_copy = ts->depth;
+ if (to_copy > 0) {
+ while (existing->depth + to_copy > existing->max_stack) {
+ if (ftrace_task_state__grow_stack(existing) < 0)
+ break;
+ }
+ if (existing->depth + to_copy > existing->max_stack)
+ to_copy = existing->max_stack - existing->depth;
+
+ if (older == ts) {
+ memmove(&existing->stack[to_copy], &existing->stack[0],
+ existing->depth * sizeof(existing->stack[0]));
+ memcpy(&existing->stack[0], &ts->stack[0],
+ to_copy * sizeof(existing->stack[0]));
+ } else {
+ memcpy(&existing->stack[existing->depth], &ts->stack[0],
+ to_copy * sizeof(existing->stack[0]));
+ }
+
+ existing->depth += to_copy;
+ for (j = 0; j < to_copy; j++) {
+ if (ts->stack[j].inlined)
+ existing->inlined_depth++;
+ }
+ for (j = to_copy; j < ts->depth; j++)
+ free(ts->stack[j].name);
+ }
+
+ if (ts->seq > existing->seq)
+ existing->seq = ts->seq;
+ if (existing->depth > 0) {
+ existing->min_raw_indent = -1;
+ } else {
+ int m = newer->min_raw_indent;
+
+ if (older->min_raw_indent >= 0 &&
+ (m < 0 || older->min_raw_indent < m))
+ m = older->min_raw_indent;
+ existing->min_raw_indent = m;
+ }
+
+ free(ts->stack);
+ 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;
+}
+
+/**
+ * ftrace__get_single_pid - Retrieve the single target process ID being traced
+ * @ftrace: Perf ftrace context
+ *
+ * Check whether @ftrace is configured to trace a single process without
+ * child inheritance. If the thread map contains exactly one valid process
+ * ID and child task inheritance is disabled, return that PID so that
+ * initial per-CPU task states can be bound directly to the target process.
+ *
+ * Return: The target process ID if tracing a single process without
+ * inheritance, or -1 otherwise.
+ */
+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
@@ -132,17 +288,107 @@ static int ftrace_task_state__grow_stack(struct ftrace_task_state *cs)
* 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 __maybe_unused, int cpu)
+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])
- ftrace_cpu_tasks[cpu] = get_task_state(FTRACE_UNKNOWN_PID(cpu));
+ 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
+ * 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
* @str: The trace line containing the comment
@@ -307,10 +553,32 @@ static const char *find_func_column(const struct perf_ftrace *ftrace,
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;
@@ -383,13 +651,17 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
bool has_semicolon;
pipe_char = find_func_column(ftrace, line, &cpu);
- if (!pipe_char)
+ 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);
+ cs->seq = ++ftrace_event_seq;
+
after_pipe = pipe_char + 1;
func_start = skip_spaces(after_pipe);
leading_spaces = func_start - after_pipe;
@@ -402,6 +674,9 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
if (is_exit) {
char exit_name[128];
+ if (cs->min_raw_indent < 0 || leading_spaces < cs->min_raw_indent)
+ cs->min_raw_indent = leading_spaces;
+
extract_exit_func_name(func_start, exit_name, sizeof(exit_name));
top_raw_indent = -1;
@@ -480,6 +755,7 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
cs->stack[cs->depth].indent = leading_spaces;
cs->stack[cs->depth].raw_indent = leading_spaces;
cs->depth++;
+ cs->min_raw_indent = -1;
}
}
diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
index fa2974f46582..00e7eb636083 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -119,6 +119,8 @@ struct ftrace_task_state {
int depth;
int inlined_depth;
int overflow_depth;
+ u64 seq;
+ int min_raw_indent;
};
bool ftrace_parse_retaddr(const char *str, char *sym_name, size_t sym_len, u64 *offset);
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 06/10] perf ftrace: Support display of inlined functions in function graph tracer
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (4 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 05/10] perf ftrace: Support context switch tracking in function graph tracer Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 07/10] perf symbol: Introduce vmlinux_path__find() helper Aaron Tomlin
` (3 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
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. DWARF inline resolution
Implement ftrace_resolve_inlines() using libdw to locate inlined
subroutines at the instruction address corresponding to the
caller's return address.
Implement perf_ftrace__setup_inlines() and
perf_ftrace__cleanup_inlines() to initialise machine symbol
tracking, load vmlinux symbols, and verify kernel
CONFIG_FUNCTION_GRAPH_RETADDR support.
2. Inline frame synthesis
In ftrace_process_fgraph_line(), when --inline is active:
- Interrogate return addresses using ftrace_parse_retaddr()
and resolve inlines via ftrace_resolve_inlines().
- Identify common inlined ancestors on the tracked stack and
unwind inactive frames.
- Open new inlined frames with /* (inline) */ and indent by
inlined_depth * 2.
- On function exit, close matching inlined frames with
} /* func (inline) */.
3. Command-line options and documentation
Add --inline and -k/--vmlinux options in
tools/perf/builtin-ftrace.c and document their usage in
tools/perf/Documentation/perf-ftrace.txt.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/Documentation/perf-ftrace.txt | 13 +
tools/perf/builtin-ftrace.c | 27 +-
tools/perf/util/ftrace.c | 623 ++++++++++++++++++++---
tools/perf/util/ftrace.h | 36 +-
4 files changed, 597 insertions(+), 102 deletions(-)
diff --git a/tools/perf/Documentation/perf-ftrace.txt b/tools/perf/Documentation/perf-ftrace.txt
index 3f3808e513fe..0af6b0c1f134 100644
--- a/tools/perf/Documentation/perf-ftrace.txt
+++ b/tools/perf/Documentation/perf-ftrace.txt
@@ -134,6 +134,19 @@ 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.
+
+-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 e3909cff287b..1f652d1e2754 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;
@@ -771,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);
@@ -916,7 +924,7 @@ static int __cmd_ftrace(struct perf_ftrace *ftrace)
free(buf);
strbuf_release(&linebuf);
out_reset:
- ftrace_cpu_states__clear();
+ perf_ftrace__cleanup_inlines(ftrace);
exit_tracing_instance();
out:
return (ret == 0 && done && !workload_exec_errno) ? 0 : -1;
@@ -1872,6 +1880,10 @@ 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(0, "inline", &ftrace.use_inline,
+ "Show inlined functions in function_graph tracer"),
+ 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),
@@ -1987,6 +1999,15 @@ 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;
+ }
+ }
cmd_func = __cmd_ftrace;
break;
case PERF_FTRACE_LATENCY:
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
index fea61a9e17b1..746e966b4049 100644
--- a/tools/perf/util/ftrace.c
+++ b/tools/perf/util/ftrace.c
@@ -1,23 +1,60 @@
// SPDX-License-Identifier: GPL-2.0
-#include <linux/compiler.h>
-#include <linux/kernel.h>
-#include <linux/string.h>
-#include <linux/zalloc.h>
-#include <linux/hash.h>
-#include <linux/list.h>
-#include <stdbool.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 <errno.h>
+#include <stdbool.h>
#include <sys/types.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 "util/string2.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;
+ int max_stack;
+ int depth;
+ int inlined_depth;
+ int overflow_depth;
+ u64 seq;
+ int min_raw_indent;
+};
+
+#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];
static u64 ftrace_event_seq;
@@ -25,8 +62,9 @@ static u64 ftrace_event_seq;
/**
* ftrace_cpu_states__clear - Free allocated CPU and task stack states
*
- * Free all memory associated with the per-task stack tracking states,
- * including the strings allocated for stack frames.
+ * 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)
{
@@ -69,6 +107,32 @@ static struct ftrace_task_state *find_task_state(pid_t pid)
return NULL;
}
+/**
+ * ftrace_task_state__grow_stack - Double the capacity of a task's call stack
+ * @cs: Task state whose stack should be resized
+ *
+ * Reallocate @cs->stack with twice the current capacity, zero-initialising
+ * the newly allocated frames.
+ *
+ * Return: 0 on success, or -ENOMEM if memory allocation fails.
+ */
+static int ftrace_task_state__grow_stack(struct ftrace_task_state *cs)
+{
+ int new_max = cs->max_stack ? cs->max_stack * 2 : FTRACE_MAX_STACK;
+ struct ftrace_stack_frame *new_stack;
+
+ new_stack = realloc(cs->stack, new_max * sizeof(*new_stack));
+ if (!new_stack) {
+ pr_err("Not enough memory to resize ftrace stack\n");
+ return -ENOMEM;
+ }
+
+ memset(new_stack + cs->max_stack, 0, (new_max - cs->max_stack) * sizeof(*new_stack));
+ cs->stack = new_stack;
+ cs->max_stack = new_max;
+ return 0;
+}
+
/**
* get_task_state - Get or allocate stack state for a given task PID
* @pid: Process ID of the task
@@ -88,12 +152,10 @@ static struct ftrace_task_state *get_task_state(pid_t pid)
if (!ts)
return NULL;
- ts->stack = calloc(FTRACE_MAX_STACK, sizeof(*ts->stack));
- if (!ts->stack) {
+ if (ftrace_task_state__grow_stack(ts) < 0) {
free(ts);
return NULL;
}
- ts->max_stack = FTRACE_MAX_STACK;
ts->pid = pid;
ts->min_raw_indent = -1;
@@ -103,32 +165,6 @@ static struct ftrace_task_state *get_task_state(pid_t pid)
return ts;
}
-/**
- * ftrace_task_state__grow_stack - Double the capacity of a task's call stack
- * @cs: Task state whose stack should be resized
- *
- * Reallocate @cs->stack with twice the current capacity, zero-initialising
- * the newly allocated frames.
- *
- * Return: 0 on success, or -ENOMEM if memory allocation fails.
- */
-static int ftrace_task_state__grow_stack(struct ftrace_task_state *cs)
-{
- int new_max = cs->max_stack ? cs->max_stack * 2 : FTRACE_MAX_STACK;
- struct ftrace_stack_frame *new_stack;
-
- new_stack = realloc(cs->stack, new_max * sizeof(*new_stack));
- if (!new_stack) {
- pr_err("Not enough memory to resize ftrace stack\n");
- return -ENOMEM;
- }
-
- memset(new_stack + cs->max_stack, 0, (new_max - cs->max_stack) * sizeof(*new_stack));
- cs->stack = new_stack;
- cs->max_stack = new_max;
- return 0;
-}
-
/**
* ftrace_task_state__rekey - Reassign a task state to a new PID
* @ts: The task state to re-key
@@ -377,7 +413,7 @@ static bool parse_context_switch(const char *line)
* 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
- * frames are preserved when it schedules back in.
+ * 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)) {
@@ -473,6 +509,80 @@ 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
@@ -519,6 +629,58 @@ static void extract_exit_func_name(const char *str, char *buf, size_t size)
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
@@ -526,11 +688,16 @@ static void extract_exit_func_name(const char *str, char *buf, size_t size)
* @cpu: Pointer to store the CPU number
*
* Locate the context columns that precede the function call column of a
- * function_graph trace record.
+ * 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, or the CPU ')' when no delimiter follows it). Returns NULL
- * if @line is not a trace record.
+ * '|' 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)
@@ -630,25 +797,40 @@ static int print_line(struct perf_ftrace *ftrace, const char *line, FILE *out)
* @line: The trace line to process
* @out: File stream to output to
*
- * Parse function entries and exits, maintain the task's call stack, and
- * output the trace line.
+ * 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_task_state *cs;
+ struct ftrace_cpu_state *cs;
+ u64 caller_offset = 0;
+ int num_inlines = 0;
int cpu = -1;
int leading_spaces;
- int top_raw_indent;
+ int extra_indent;
+ int caller_idx;
+ int active_inlines;
+ int common;
int match_idx;
- int i;
+ 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) {
@@ -661,6 +843,7 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
return print_line(ftrace, line, out);
cs->seq = ++ftrace_event_seq;
+ make_blank_prefix(ftrace, line, pipe_char, blank_prefix, sizeof(blank_prefix));
after_pipe = pipe_char + 1;
func_start = skip_spaces(after_pipe);
@@ -681,20 +864,32 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
top_raw_indent = -1;
for (i = cs->depth - 1; i >= 0; i--) {
- top_raw_indent = cs->stack[i].raw_indent;
- break;
+ 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--;
- return print_line(ftrace, line, out);
+ 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].name &&
+ if (!cs->stack[i].inlined && cs->stack[i].name &&
cs->stack[i].raw_indent == leading_spaces &&
strcmp(cs->stack[i].name, exit_name) == 0) {
match_idx = i;
@@ -703,9 +898,14 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
}
}
+ /*
+ * 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].raw_indent == leading_spaces) {
+ if (!cs->stack[i].inlined &&
+ cs->stack[i].raw_indent == leading_spaces) {
match_idx = i;
break;
}
@@ -714,50 +914,327 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
if (match_idx >= 0) {
cs->overflow_depth = 0;
- while (cs->depth > match_idx) {
+ } 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--;
}
- } else if (cs->overflow_depth > 0) {
- cs->overflow_depth--;
+
+ extra_indent = cs->inlined_depth * 2;
+ } else {
+ extra_indent = 0;
}
- return print_line(ftrace, line, out);
+ 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--) {
- top_raw_indent = cs->stack[i].raw_indent;
- break;
+ 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 >= cs->max_stack &&
+ ftrace_task_state__grow_stack(cs) < 0)
+ 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++;
+ cs->min_raw_indent = -1;
+ }
+
+ /* Print current line with updated indentation */
+ extra_indent = cs->inlined_depth * 2;
+
+ if (!ftrace->graph_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 >= cs->max_stack &&
ftrace_task_state__grow_stack(cs) < 0) {
- cs->overflow_depth++;
- } else {
- cs->stack[cs->depth].name = strdup(entry_name);
- if (!cs->stack[cs->depth].name) {
- pr_err("Not enough memory\n");
- return -ENOMEM;
- }
- cs->stack[cs->depth].inlined = false;
- cs->stack[cs->depth].indent = leading_spaces;
- cs->stack[cs->depth].raw_indent = leading_spaces;
- cs->depth++;
- cs->min_raw_indent = -1;
+ for (i = 0; i < num_inlines; i++)
+ free((void *)inlined_names[i]);
+ return -ENOMEM;
}
+
+ 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++;
+ cs->min_raw_indent = -1;
}
- return print_line(ftrace, line, out);
+ 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 00e7eb636083..7b9e0919ae8d 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -42,6 +42,8 @@ struct perf_ftrace {
int graph_verbose;
int graph_thresh;
int graph_tail;
+ bool use_inline;
+ struct machine *machine;
};
struct filter_entry {
@@ -96,36 +98,18 @@ perf_ftrace__latency_cleanup_bpf(struct perf_ftrace *ftrace __maybe_unused)
#endif /* HAVE_BPF_SKEL */
-#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)
+struct machine;
-#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;
- int max_stack;
- int depth;
- int inlined_depth;
- int overflow_depth;
- u64 seq;
- int min_raw_indent;
-};
+#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);
-void ftrace_cpu_states__clear(void);
+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
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 07/10] perf symbol: Introduce vmlinux_path__find() helper
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (5 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 06/10] perf ftrace: Support display of inlined functions " Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 08/10] perf test: Add test for ftrace inline and task stack tracking Aaron Tomlin
` (2 subsequent siblings)
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
The vmlinux_path array maintains standard paths used by perf to locate
the kernel image (e.g. "vmlinux", "/boot/vmlinux",
"/lib/modules/%s/build/vmlinux", "/usr/lib/debug/..."). While
vmlinux_path__init() populates these candidates and dso__load_vmlinux_path()
loads symbols into a DSO/map, callers (such as test suites or probing tools)
often need to verify the existence of an accessible vmlinux on disk before
attempting machine setup or test execution.
Introduce vmlinux_path__find() to search through the configured vmlinux_path
entries (or symbol_conf.vmlinux_name if explicitly set) and return the
path to the first readable vmlinux file, or NULL if none is accessible.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/util/symbol.c | 38 +++++++++++++++++++++++++++++++++++++-
tools/perf/util/symbol.h | 2 ++
2 files changed, 39 insertions(+), 1 deletion(-)
diff --git a/tools/perf/util/symbol.c b/tools/perf/util/symbol.c
index b61a574ce9a3..fd4a4441718e 100644
--- a/tools/perf/util/symbol.c
+++ b/tools/perf/util/symbol.c
@@ -2337,6 +2337,9 @@ static int vmlinux_path__init(struct perf_env *env)
const char *kernel_version;
unsigned int i;
+ if (vmlinux_path)
+ return 0;
+
vmlinux_path = malloc(sizeof(char *) * (ARRAY_SIZE(vmlinux_paths) +
ARRAY_SIZE(vmlinux_paths_upd)));
if (vmlinux_path == NULL)
@@ -2372,6 +2375,38 @@ static int vmlinux_path__init(struct perf_env *env)
return -1;
}
+/**
+ * vmlinux_path__find - Locate the first accessible vmlinux file
+ *
+ * Check symbol_conf.vmlinux_name first if set, then search through the
+ * initialized paths in vmlinux_path.
+ *
+ * Return: A pointer to the path of the first readable vmlinux file, or NULL if
+ * none is accessible.
+ */
+const char *vmlinux_path__find(void)
+{
+ int i;
+
+ if (symbol_conf.vmlinux_name && access(symbol_conf.vmlinux_name, R_OK) == 0)
+ return symbol_conf.vmlinux_name;
+
+ if (symbol__init(NULL) < 0)
+ return NULL;
+
+ if (!vmlinux_path) {
+ if (vmlinux_path__init(NULL) < 0)
+ return NULL;
+ }
+
+ for (i = 0; i < vmlinux_path__nr_entries; ++i) {
+ if (access(vmlinux_path[i], R_OK) == 0)
+ return vmlinux_path[i];
+ }
+
+ return NULL;
+}
+
int setup_list(struct strlist **list, const char *list_str,
const char *list_name)
{
@@ -2598,6 +2633,8 @@ int symbol__init(struct perf_env *env)
void symbol__exit(void)
{
+ vmlinux_path__exit();
+
if (!symbol_conf.initialized)
return;
@@ -2608,7 +2645,6 @@ void symbol__exit(void)
intlist__delete(symbol_conf.tid_list);
intlist__delete(symbol_conf.pid_list);
intlist__delete(symbol_conf.addr_list);
- vmlinux_path__exit();
symbol_conf.sym_list = symbol_conf.dso_list = symbol_conf.comm_list = NULL;
symbol_conf.bt_stop_list = NULL;
symbol_conf.initialized = false;
diff --git a/tools/perf/util/symbol.h b/tools/perf/util/symbol.h
index d0bac824c79c..6d1eb08076ab 100644
--- a/tools/perf/util/symbol.h
+++ b/tools/perf/util/symbol.h
@@ -184,6 +184,8 @@ static inline int __symbol__join_symfs(char *bf, size_t size, const char *path)
extern int vmlinux_path__nr_entries;
extern char **vmlinux_path;
+const char *vmlinux_path__find(void);
+
static inline void *symbol__priv(struct symbol *sym)
{
return ((void *)sym) - symbol_conf.priv_size;
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 08/10] perf test: Add test for ftrace inline and task stack tracking
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (6 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 07/10] perf symbol: Introduce vmlinux_path__find() helper Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 09/10] perf ftrace: Support omitting execution duration in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 10/10] perf ftrace: Filter extraneous functions " Aaron Tomlin
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
Add an automated unit test suite ("Ftrace inline processing") in
tools/perf/tests/ftrace.c to validate the function graph call stack
tracking and inlined function reconstruction implementation.
The test suite verifies:
1. DWARF inline symbol resolution
Uses vmlinux_path__find() to locate the kernel image, setting up
symbol resolution and validating activate_task inlining within
wake_up_new_task at offset 0x1d1.
2. Inline presentation and return address filtering
Verifies that synthesised inlined entries and exits are
correctly annotated with /* (inline) */, that return address
comments are filtered out by default, and that comments are
preserved when graph_retaddr is requested.
3. Bare closing brace matching
Verifies that bare closing braces without trailing function
names (the default when funcgraph-tail is disabled) correctly
pop and match their corresponding LIFO stack frames.
4. Task context-switch tracking and CPU migration
Verifies call stack isolation across context switch banners and
task migration across different CPUs.
5. Deep stack growth and recovery
Verifies that dynamic call stack growth and depth recovery
operate cleanly under deep call stacks exceeding the initial
FTRACE_MAX_STACK capacity.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/tests/ftrace.c | 1324 ++++++++++++++++++++++++++++++++++++-
1 file changed, 1315 insertions(+), 9 deletions(-)
diff --git a/tools/perf/tests/ftrace.c b/tools/perf/tests/ftrace.c
index 42604811a2fb..457ab6eebbb2 100644
--- a/tools/perf/tests/ftrace.c
+++ b/tools/perf/tests/ftrace.c
@@ -7,7 +7,14 @@
#include <unistd.h>
#include "tests.h"
#include "debug.h"
+#include "dso.h"
+#include "machine.h"
+#include "map.h"
+#include "symbol.h"
+#include "srcline.h"
#include "util/ftrace.h"
+#include "evlist.h"
+#include "thread_map.h"
static int test_parse_retaddr(void)
{
@@ -63,19 +70,1318 @@ static int test_filter_retaddr(void)
return TEST_OK;
}
-static int test__ftrace(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+static void ensure_synthetic_inline(struct machine *machine,
+ const char *caller, u64 offset,
+ const char *inline_name)
{
- int ret;
+ struct map *kmap = NULL;
+ struct symbol *sym;
- ret = test_parse_retaddr();
- if (ret != TEST_OK)
- return ret;
+ if (!machine || !offset)
+ return;
- ret = test_filter_retaddr();
- if (ret != TEST_OK)
- return ret;
+ sym = machine__find_kernel_symbol_by_name(machine, caller, &kmap);
+ if (sym && kmap) {
+ struct dso *dso = map__dso(kmap);
+ u64 ip = sym->start + offset - 1;
+ u64 addr = map__rip_2objdump(kmap, ip);
+ struct inline_node *node;
+
+ node = inlines__tree_find(dso__inlined_nodes(dso), addr);
+ if (!node) {
+ struct symbol *isym = new_inline_sym(dso, sym, inline_name);
+
+ node = zalloc(sizeof(*node));
+ if (node && isym) {
+ INIT_LIST_HEAD(&node->val);
+ node->addr = addr;
+ if (inline_list__append(isym, NULL, node) != 0) {
+ symbol__delete(isym);
+ free(node);
+ } else {
+ inlines__tree_insert(dso__inlined_nodes(dso), node);
+ }
+ } else {
+ if (isym)
+ symbol__delete(isym);
+ free(node);
+ }
+ } else {
+ struct inline_list *pos, *outer = NULL;
+
+ list_for_each_entry_reverse(pos, &node->val, list) {
+ if (pos->symbol && symbol__inlined(pos->symbol)) {
+ outer = pos;
+ break;
+ }
+ }
+ if (!outer || strcmp(outer->symbol->name, inline_name)) {
+ struct symbol *isym = new_inline_sym(dso, sym, inline_name);
+
+ if (isym) {
+ inline_node__clear_frames(node);
+ if (inline_list__append(isym, NULL, node) != 0)
+ symbol__delete(isym);
+ }
+ }
+ }
+ map__put(kmap);
+ }
+}
+
+#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;
+ /* By default, graph_retaddr is 0 (return address comments are filtered) */
+ ftrace.tracer = "function_graph";
+
+ vmlinux = vmlinux_path__find();
+ if (!vmlinux)
+ return TEST_SKIP;
+
+ /* If vmlinux is present, test resolving symbols */
+ {
+ 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; falling back to synthetic inline\n");
+ for (j = 0; j < num_inlines; j++)
+ free((void *)inlined_names[j]);
+ ensure_synthetic_inline(ftrace.machine, "wake_up_new_task", 0x1d1,
+ "activate_task");
+ 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) {
+ 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 graph_retaddr = 1 (user specified --graph-opts retaddr) */
+ ftrace.graph_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_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 graph_retaddr = 1 */
+ 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.graph_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_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, "| }\n") != 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 initial FTRACE_MAX_STACK (256) */
+ for (i = 1; i < 256; i++)
+ feed_fgraph_entry(&ftrace, 0, i, "sub_func", NULL, out);
+
+ /* Push 44 additional frames triggering dynamic stack expansion beyond 256 */
+ for (i = 0; i < 44; i++)
+ feed_fgraph_entry(&ftrace, 0, 256 + i, "overflow_func", NULL, out);
+
+ /* Pop the 44 expanded 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 call depth recovery when events are dropped on deep stack */
+ 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 on deep stack 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 dropped inner recursive entry does not prematurely unwind outer caller */
+ 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 entry at indent 4 is DROPPED by ring buffer */
+ /* Inner recursive invocation exits at indent 4 */
+ ftrace_process_fgraph_line(&ftrace, " 0) 1.000 us | } /* rec_func */", out);
+ /* Outer invocation is still active; calls child_func at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | child_func(); /* <-rec_func+0x20 */",
+ out);
+ /* Outer invocation exits at indent 2 */
+ ftrace_process_fgraph_line(&ftrace, " 0) 2.000 us | } /* rec_func */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "child_func();")) {
+ pr_debug("FAILED %s:%d child_func missing after dropped inner recursive entry\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) {
+ ensure_synthetic_inline(ftrace.machine, "wake_up_new_task", 0x1d1,
+ "activate_task");
+ 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), and real frame enqueue_task dynamically
+ * grows the stack capacity beyond 256.
+ */
+ feed_fgraph_entry(&ftrace, 0, 255, "enqueue_task",
+ "wake_up_new_task+0x1d1/0x3e0", out);
+
+ /*
+ * Nested child call on deep stack: does NOT pop the
+ * inlined frame!
+ */
+ feed_fgraph_entry(&ftrace, 0, 256, "child_overflow",
+ "enqueue_task+0x10", out);
+
+ /* Exit child_overflow */
+ feed_fgraph_exit(&ftrace, 0, 256, NULL, out);
+
+ /* Exit enqueue_task */
+ 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.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;
}
-DEFINE_SUITE("Ftrace return address processing", ftrace);
+#define TEST_CS_ASSERT(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_context_switch_handling(void)
+{
+ struct perf_ftrace ftrace;
+ struct perf_thread_map *threads;
+ const char *saved_vmlinux = symbol_conf.vmlinux_name;
+ bool saved_ignore_buildid = symbol_conf.ignore_vmlinux_buildid;
+ char *out_buf = NULL;
+ size_t out_len = 0;
+ const char *vmlinux;
+ struct map *kmap = NULL;
+ struct symbol *sym;
+ FILE *out;
+ int i;
+ int ret = TEST_OK;
+
+ vmlinux = vmlinux_path__find();
+ if (!vmlinux)
+ return TEST_SKIP;
+
+ memset(&ftrace, 0, sizeof(ftrace));
+ ftrace.use_inline = true;
+ symbol_conf.vmlinux_name = vmlinux;
+ symbol_conf.ignore_vmlinux_buildid = true;
+ if (perf_ftrace__setup_inlines(&ftrace) < 0 || !ftrace.machine) {
+ ret = TEST_SKIP;
+ goto out_cleanup_conf;
+ }
+
+ sym = machine__find_kernel_symbol_by_name(ftrace.machine, "wake_up_new_task", &kmap);
+ if (!sym || !kmap) {
+ ret = TEST_SKIP;
+ goto out_cleanup;
+ }
+ map__put(kmap);
+
+ ensure_synthetic_inline(ftrace.machine, "wake_up_new_task", 0x1d1,
+ "activate_task");
+
+ ftrace_cpu_states__clear();
+
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_cleanup;
+ }
+
+ /*
+ * Initial Task 1 (PID 100) starts running on CPU 0 before any banner
+ * has been emitted on that CPU (default kernel ftrace behaviour).
+ * Frame wake_up_new_task at offset 0x1d1 resolves inline activate_task.
+ */
+ ftrace_process_fgraph_line(&ftrace, " 0) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ 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 - must NOT inherit Task 1's inlined call 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 - must NOT inherit Task 1's inlined call stack */
+ 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 enqueue_task, then wake_up_new_task on CPU 1 */
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 2.000 us | } /* wake_up_new_task */", out);
+
+ /*
+ * Test delayed context switch banner migration where a task state
+ * with an inlined frame on CPU 2 is rekeyed from CPU 2 to CPU 3.
+ */
+ ftrace_process_fgraph_line(&ftrace, " 2) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ 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) | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) | } /* wake_up_new_task */", 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;
+
+ /*
+ * Test migration where task with active outer frame on CPU 0 migrates
+ * to CPU 1 without an initial context switch banner, accumulating inner
+ * frames in ts under FTRACE_UNKNOWN_PID(1). Rekeying must append inner
+ * frames to existing stack rather than inverting the stack.
+ */
+ ftrace_cpu_states__clear();
+ ftrace_process_fgraph_line(&ftrace, " 0) | caller_outer() {", out);
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task7-700 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ /* Migrates to CPU 1 without banner; inner frame called */
+ ftrace_process_fgraph_line(&ftrace, " 1) | callee_inner() {", out);
+ /* Banner on CPU 1 rekeys CPU 1 to task7 */
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) task7-700 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ /* Resumes on CPU 0: callee_inner exits, followed by caller_outer */
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <idle>-0 => task7-700 ", out);
+ ftrace_process_fgraph_line(&ftrace, " ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | } /* callee_inner */", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | } /* caller_outer */", out);
+
+ /*
+ * Test task migration where older stack has scheduler frames that already
+ * exited before the task entered a callee on the new CPU.
+ * CPU 0 has caller_outer (indent 2) -> schedule (indent 4) -> __schedule (indent 6).
+ * Resumes on CPU 1 where schedule frames returned and callee_worker (indent 4) runs.
+ * Rekeying must prune schedule and __schedule from older stack rather than
+ * leaving stranded frames between caller_outer and callee_worker.
+ */
+ ftrace_cpu_states__clear();
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <idle>-0 => task8-800 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | caller_outer() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | __schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) | callee_worker() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task8-800 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) task8-800 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ /* Resumes on CPU 0: bare braces pop callee_worker then caller_outer */
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <idle>-0 => task8-800 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) 1.000 us | }", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) 2.000 us | }", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | base_verify() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) 1.000 us | }", out);
+
+ /*
+ * Test migration where task unwinds to depth 0 on CPU 1 before CPU 0
+ * emits its delayed context switch banner. Rekeying must prune the
+ * already-exited frames from the older stack rather than restoring them.
+ */
+ ftrace_cpu_states__clear();
+ ftrace_process_fgraph_line(&ftrace, " 0) | caller_deep() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) | callee_deep() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) <idle>-0 => task9-900 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* callee_deep */", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 2.000 us | } /* caller_deep */", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task9-900 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) | fresh_root() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* fresh_root */", out);
+
+ /*
+ * Test migration where older stack has an inlined frame at the top
+ * when rekeying. Pruning must not stall on raw_indent == -1.
+ */
+ ftrace_cpu_states__clear();
+ ftrace_process_fgraph_line(&ftrace, " 2) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace, " 3) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) <idle>-0 => task10-1000 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) 1.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) | other_work() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) task10-1000 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) 1.000 us | } /* other_work */", out);
+ ftrace_process_fgraph_line(&ftrace, " 3) 2.000 us | } /* wake_up_new_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ /* Verify synthetic inlined entry was emitted for Task 1 */
+ TEST_CS_ASSERT("task1 emitted inlined activate_task entry",
+ strstr(out_buf, "activate_task() { /* (inline) */") != NULL);
+
+ /* Verify default_idle in Idle task started at base indentation without extra indent */
+ TEST_CS_ASSERT("idle task starts at base indentation",
+ strstr(out_buf, "| default_idle() {") != NULL);
+ TEST_CS_ASSERT("idle task does not inherit extra inlined indent",
+ strstr(out_buf, "| default_idle() {") == NULL);
+
+ /* Verify func2 in Task 2 started at base indentation "| func2() {" without extra indent */
+ TEST_CS_ASSERT("task2 starts at base indentation",
+ strstr(out_buf, "| func2() {") != NULL);
+ TEST_CS_ASSERT("task2 does not inherit extra inlined indent",
+ strstr(out_buf, "| func2() {") == NULL);
+
+ /* Verify task1 synthesized inlined exit on CPU 1 */
+ TEST_CS_ASSERT("task1 emitted synthesized inlined exit on CPU 1",
+ strstr(out_buf,
+ "1) | } /* activate_task (inline) */") != NULL);
+
+ /* Verify task3 preserved inlined stack across delayed context switch banner */
+ TEST_CS_ASSERT("task3 preserved inlined frame across delayed banner",
+ strstr(out_buf,
+ "3) | } /* activate_task (inline) */") != NULL);
+
+ /* Verify task4 merged outer stack when entered before delayed banner */
+ TEST_CS_ASSERT("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_CS_ASSERT("single-task stack tracked across CPUs without banners",
+ strstr(out_buf, "0) | } /* func_a */") != NULL);
+
+ /* Verify migrated task with unbannered entry appended rather than inverted stack */
+ TEST_CS_ASSERT("migrated task unwound inner callee cleanly",
+ strstr(out_buf, "0) | } /* callee_inner */") != NULL);
+ TEST_CS_ASSERT("migrated task unwound outer caller cleanly",
+ strstr(out_buf, "0) | } /* caller_outer */") != NULL);
+
+ /* Verify pruned scheduler frames allowed clean bare brace unwinding */
+ TEST_CS_ASSERT("pruned scheduler frames allowed clean bare brace unwinding",
+ strstr(out_buf, "| base_verify() {") != NULL);
+
+ /* Verify unwound migrated task starts at base indent */
+ TEST_CS_ASSERT("unwound migrated task starts at base indent",
+ strstr(out_buf, " 1) | fresh_root() {") != NULL);
+
+ /* Verify inlined frame pruned without stalling on migration */
+ TEST_CS_ASSERT("inlined frame pruned without stalling on migration",
+ strstr(out_buf, " 3) 1.000 us | } /* other_work */") != NULL &&
+ strstr(out_buf, " 3) 2.000 us | } /* wake_up_new_task */") != NULL);
+
+ /*
+ * Test migration where merged stack depth exceeds initial FTRACE_MAX_STACK (256)
+ * triggers dynamic stack growth and unwinds without 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) {
+ ret = TEST_FAIL;
+ goto out_cleanup;
+ }
+
+ 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 300 frames on CPU 3 on dynamically resized stack */
+ 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_CS_ASSERT("no indentation drift after saturated stack rekey",
+ strstr(out_buf, "| after_merge() {") != NULL);
+
+ /*
+ * Test migration where task exits to depth 0 on CPU 1 (setting min_raw_indent = 2),
+ * migrates to CPU 0 entering outer frames wake_up_new_task (indent 2) -> enqueue_task (indent 4)
+ * with activate_task inlined, migrates back to CPU 1 entering inner callee_c (indent 6).
+ * Delayed rekeying of CPU 0 must not over-prune caller frames using
+ * CPU 1's previous min_raw_indent.
+ */
+ free(out_buf);
+ out_buf = NULL;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_cleanup;
+ }
+
+ ftrace_cpu_states__clear();
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) <idle>-0 => task11-1100 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) | root_prior() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* root_prior */", out);
+ /* Task 11 migrates to CPU 0 without banner, entering active caller frames with inline */
+ ftrace_process_fgraph_line(&ftrace, " 0) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ out);
+ /* Task 11 migrates back to CPU 1 and enters inner frame */
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) <idle>-0 => task11-1100 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) | callee_c() {", out);
+ /* Delayed banner on CPU 0 triggers rekey */
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task11-1100 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ /* Resumes on CPU 1: callee_c, enqueue_task (synthesizing activate_task exit), wake_up_new_task */
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* callee_c */", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 2.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 3.000 us | } /* wake_up_new_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ /* Verify active caller frames preserved despite prior exit min_raw_indent */
+ TEST_CS_ASSERT("task11 emitted synthesized inlined exit on CPU 1",
+ strstr(out_buf,
+ " 1) | } /* activate_task (inline) */") != NULL);
+
+ /*
+ * Test multi-stage rekeying where CPU 1 unwinds frames to depth 0 first
+ * (recording min_raw_indent = 2) and merges into an existing state,
+ * followed by CPU 0 rekeying its older UNKNOWN_PID state.
+ * The merge into existing must preserve min_raw_indent so CPU 0's older
+ * exited frames are properly pruned rather than resurrected.
+ */
+ free(out_buf);
+ out_buf = NULL;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_cleanup;
+ }
+
+ ftrace_cpu_states__clear();
+ /* CPU 0: Task 12 enters frames under UNKNOWN_PID(0) with inline */
+ ftrace_process_fgraph_line(&ftrace, " 0) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ out);
+ /* CPU 1: Task 12 unwinds both frames to depth 0 under UNKNOWN_PID(1) */
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 2.000 us | } /* wake_up_new_task */", out);
+ /* CPU 2: pre-bind an empty state for task 12 */
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) <idle>-0 => task12-1200 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) task12-1200 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ /* CPU 1 emits banner first: merges ts1 (depth 0, min_raw_indent 2) into existing */
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) task12-1200 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ /* CPU 0 emits banner second: merges ts0 into existing, must prune exited frames */
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task12-1200 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ /* Task 12 resumes on CPU 1 and runs fresh function: bare braces must not pop stale frames */
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) <idle>-0 => task12-1200 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) | fresh_task12() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) 1.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ /* Verify multi-stage rekey preserved min_raw_indent to prune stale exited frames */
+ TEST_CS_ASSERT("fresh_task12 starts at base indent without resurrected frames",
+ strstr(out_buf, " 1) | fresh_task12() {") != NULL);
+ TEST_CS_ASSERT("stale inlined frame not resurrected before fresh_task12",
+ strstr(out_buf, "} /* activate_task (inline) */\n 1) | fresh_task12() {") == NULL);
+
+out_cleanup:
+ if (out_buf)
+ free(out_buf);
+ perf_ftrace__cleanup_inlines(&ftrace);
+out_cleanup_conf:
+ symbol_conf.vmlinux_name = saved_vmlinux;
+ symbol_conf.ignore_vmlinux_buildid = saved_ignore_buildid;
+ return ret;
+}
+#undef TEST_CS_ASSERT
+
+static int test__parse_retaddr(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_parse_retaddr();
+}
+
+static int test__filter_retaddr(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_filter_retaddr();
+}
+
+static int test__column_layouts(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_column_layouts();
+}
+
+static int test__stack_saturation(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_stack_saturation();
+}
+
+static int test__context_switch_handling(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_context_switch_handling();
+}
+
+static int test__inline_processing(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_inline_processing();
+}
+
+static struct test_case ftrace_test_cases[] = {
+ TEST_CASE("Parse return address comments", parse_retaddr),
+ TEST_CASE("Filter return address comments", filter_retaddr),
+ TEST_CASE("Function graph column layouts", column_layouts),
+ TEST_CASE("Stack saturation and expansion", stack_saturation),
+ TEST_CASE("Context switch handling", context_switch_handling),
+ TEST_CASE("Inline symbol and trace processing", inline_processing),
+ { .name = NULL, }
+};
+
+struct test_suite suite__ftrace = {
+ .desc = "Ftrace inline processing",
+ .test_cases = ftrace_test_cases,
+};
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 09/10] perf ftrace: Support omitting execution duration in function graph tracer
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (7 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 08/10] perf test: Add test for ftrace inline and task stack tracking Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 10/10] perf ftrace: Filter extraneous functions " Aaron Tomlin
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
The Linux kernel's function graph tracer displays the execution duration
of completed functions by default via the funcgraph-duration trace
option. Whilst informative, in scenarios where the user is solely
interested in the call graph topology, call flow sequence, or inlined
hierarchy, the duration column can add visual clutter and widen each
trace line.
Introduce support for omitting function duration by exposing the
noduration sub-option under --graph-opts. Consistent with the existing
nosleep-time and noirqs sub-options, it accepts an optional value, so
noduration=0 explicitly retains the duration column; when specified
more than once, the last occurrence takes effect. When enabled, perf
ftrace configures the kernel's tracing instance option
funcgraph-duration to 0, and restores it to 1 upon resetting tracing
options.
Because the profile subcommand fundamentally requires function execution
duration to compute timing statistics and call counts, combining
noduration with perf ftrace profile is rejected with an explicit error.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/Documentation/perf-ftrace.txt | 1 +
tools/perf/builtin-ftrace.c | 34 +++++++-
tools/perf/tests/ftrace.c | 104 ++++++++++++++++++++++-
tools/perf/util/ftrace.c | 25 ++++--
tools/perf/util/ftrace.h | 1 +
5 files changed, 154 insertions(+), 11 deletions(-)
diff --git a/tools/perf/Documentation/perf-ftrace.txt b/tools/perf/Documentation/perf-ftrace.txt
index 0af6b0c1f134..ed2e45b8507f 100644
--- a/tools/perf/Documentation/perf-ftrace.txt
+++ b/tools/perf/Documentation/perf-ftrace.txt
@@ -129,6 +129,7 @@ OPTIONS for 'perf ftrace trace'
- retaddr - Show function return address.
- nosleep-time - Measure on-CPU time only for function_graph tracer.
- noirqs - Ignore functions that happen inside interrupt.
+ - noduration - Do not print function duration.
- verbose - Show process names, PIDs, timestamps, etc.
- thresh=<n> - Setup trace duration threshold in microseconds.
- depth=<n> - Set max depth for function graph tracer to follow.
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index 1f652d1e2754..db9616ad68af 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -300,6 +300,7 @@ static void reset_tracing_options(struct perf_ftrace *ftrace __maybe_unused)
write_tracing_option_file("func_stack_trace", "0");
write_tracing_option_file("sleep-time", "1");
write_tracing_option_file("funcgraph-irqs", "1");
+ write_tracing_option_file("funcgraph-duration", "1");
write_tracing_option_file("funcgraph-proc", "0");
write_tracing_option_file("funcgraph-abstime", "0");
write_tracing_option_file("funcgraph-tail", "0");
@@ -599,6 +600,26 @@ static int set_tracing_funcgraph_irqs(struct perf_ftrace *ftrace)
return 0;
}
+/**
+ * set_tracing_funcgraph_duration - Set funcgraph-duration tracing option
+ * @ftrace: Perf ftrace context
+ *
+ * Configure the kernel's funcgraph-duration tracing option based on the
+ * ftrace->graph_noduration flag.
+ *
+ * Return: 0 on success, or a negative error code on failure.
+ */
+static int set_tracing_funcgraph_duration(struct perf_ftrace *ftrace)
+{
+ if (!ftrace->graph_noduration)
+ return 0;
+
+ if (write_tracing_option_file("funcgraph-duration", "0") < 0)
+ return -1;
+
+ return 0;
+}
+
static int set_tracing_funcgraph_verbose(struct perf_ftrace *ftrace)
{
if (!ftrace->graph_verbose)
@@ -708,6 +729,11 @@ static int set_tracing_options(struct perf_ftrace *ftrace)
return -1;
}
+ if (set_tracing_funcgraph_duration(ftrace) < 0) {
+ pr_err("failed to set tracing option funcgraph-duration\n");
+ return -1;
+ }
+
if (set_tracing_funcgraph_verbose(ftrace) < 0) {
pr_err("failed to set tracing option funcgraph-proc/funcgraph-abstime\n");
return -1;
@@ -1785,6 +1811,7 @@ static int parse_graph_tracer_opts(const struct option *opt,
{ .name = "retaddr", .value_ptr = &ftrace->graph_retaddr },
{ .name = "nosleep-time", .value_ptr = &ftrace->graph_nosleep_time },
{ .name = "noirqs", .value_ptr = &ftrace->graph_noirqs },
+ { .name = "noduration", .value_ptr = &ftrace->graph_noduration },
{ .name = "verbose", .value_ptr = &ftrace->graph_verbose },
{ .name = "thresh", .value_ptr = &ftrace->graph_thresh },
{ .name = "depth", .value_ptr = &ftrace->graph_depth },
@@ -1874,7 +1901,7 @@ int cmd_ftrace(int argc, const char **argv)
OPT_CALLBACK('g', "nograph-funcs", &ftrace.nograph_funcs, "func",
"Set nograph filter on given functions", parse_filter_func),
OPT_CALLBACK(0, "graph-opts", &ftrace, "options",
- "Graph tracer options, available options: args,retval,retval-hex,retaddr,nosleep-time,noirqs,verbose,thresh=<n>,depth=<n>",
+ "Graph tracer options, available options: args,retval,retval-hex,retaddr,nosleep-time,noirqs,noduration,verbose,thresh=<n>,depth=<n>,tail",
parse_graph_tracer_opts),
OPT_CALLBACK('m', "buffer-size", &ftrace.percpu_buffer_size, "size",
"Size of per cpu buffer, needs to use a B, K, M or G suffix.", parse_buffer_size),
@@ -2073,6 +2100,11 @@ int cmd_ftrace(int argc, const char **argv)
cmd_func = __cmd_latency;
break;
case PERF_FTRACE_PROFILE:
+ if (ftrace.graph_noduration) {
+ pr_err("Error: 'noduration' option cannot be used with profile\n");
+ ret = -EINVAL;
+ goto out_delete_filters;
+ }
cmd_func = __cmd_profile;
break;
case PERF_FTRACE_NONE:
diff --git a/tools/perf/tests/ftrace.c b/tools/perf/tests/ftrace.c
index 457ab6eebbb2..b738f04da13c 100644
--- a/tools/perf/tests/ftrace.c
+++ b/tools/perf/tests/ftrace.c
@@ -160,6 +160,18 @@ static int test_inline_processing(void)
" 1234.567890 | 0) bash-1234 | .... | | activate_task() { /* (inline) */";
static const char line_verbose_inline_exit[] =
" 1234.567890 | 0) bash-1234 | .... | | } /* activate_task (inline) */";
+ static const char line_verbose_nodur_entry[] =
+ " 1234.567890 | 0) bash-1234 | .... | wake_up_new_task() {";
+ static const char line_verbose_nodur_enqueue[] =
+ " 1234.567890 | 0) bash-1234 | .... | enqueue_task() { /* <-wake_up_new_task+0x1d1/0x3e0 */";
+ static const char line_verbose_nodur_exit_enqueue[] =
+ " 1234.567890 | 0) bash-1234 | .... | } /* enqueue_task */";
+ static const char line_verbose_nodur_exit_task[] =
+ " 1234.567890 | 0) bash-1234 | .... | } /* wake_up_new_task */";
+ static const char line_verbose_nodur_inline_entry[] =
+ " 1234.567890 | 0) bash-1234 | .... | activate_task() { /* (inline) */";
+ static const char line_verbose_nodur_inline_exit[] =
+ " 1234.567890 | 0) bash-1234 | .... | } /* activate_task (inline) */";
struct perf_ftrace ftrace;
char *out_buf = NULL;
size_t out_len = 0;
@@ -497,6 +509,33 @@ static int test_inline_processing(void)
free(out_buf);
out_buf = NULL;
ftrace.graph_verbose = false;
+
+ /* Test verbose noduration prefix formatting for synthesised inlines */
+ ftrace.graph_verbose = true;
+ ftrace.graph_noduration = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_cleanup;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_verbose_nodur_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_verbose_nodur_enqueue, out);
+ ftrace_process_fgraph_line(&ftrace, line_verbose_nodur_exit_enqueue, out);
+ ftrace_process_fgraph_line(&ftrace, line_verbose_nodur_exit_task, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ TEST_CHECK("verbose noduration inline entry preserves comm-pid",
+ strstr(out_buf, line_verbose_nodur_inline_entry) != NULL);
+ TEST_CHECK("verbose noduration inline exit preserves comm-pid",
+ strstr(out_buf, line_verbose_nodur_inline_exit) != NULL);
+
+ free(out_buf);
+ out_buf = NULL;
+ ftrace.graph_verbose = false;
+ ftrace.graph_noduration = false;
}
out_cleanup:
@@ -845,20 +884,56 @@ static int test_column_layouts(void)
{
static const struct {
const char *desc;
- bool verbose;
+ bool noduration;
+ bool verbose;
const char *in;
const char *expected;
} cases[] = {
{
- "default layout", false,
+ "default layout", false, false,
" 7) | wake_up_new_task() { /* <-kernel_clone+0x160/0x510 */",
" 7) | wake_up_new_task() {\n",
},
{
- "verbose layout", true,
+ "noduration layout", true, false,
+ " 7) housekeeping_any_cpu(); /* <-ring_buffer_unlock_commit+0x1a0/0x1f0 */",
+ " 7) housekeeping_any_cpu();\n",
+ },
+ {
+ "verbose layout", false, true,
" 1234.567890 | 7) bash-1234 | .... | | wake_up_new_task() { /* <-kernel_clone+0x160/0x510 */",
" 1234.567890 | 7) bash-1234 | .... | | wake_up_new_task() {\n",
},
+ {
+ "verbose noduration layout", true, true,
+ " 1234.567890 | 7) bash-1234 | .... | wake_up_new_task() { /* <-kernel_clone+0x160/0x510 */",
+ " 1234.567890 | 7) bash-1234 | .... | wake_up_new_task() {\n",
+ },
+ {
+ "noduration context switch marker", true, false,
+ " 7) bash-1234 => sh-5678",
+ " 7) bash-1234 => sh-5678\n",
+ },
+ {
+ "noduration irq entry marker", true, false,
+ " 7) ==========>",
+ " 7) ==========>\n",
+ },
+ {
+ "noduration irq exit marker", true, false,
+ " 7) <==========",
+ " 7) <==========\n",
+ },
+ {
+ "verbose noduration irq entry marker", true, true,
+ " 1234.567890 | 7) bash-1234 | .... | ==========>",
+ " 1234.567890 | 7) bash-1234 | .... | ==========>\n",
+ },
+ {
+ "verbose noduration irq exit marker", true, true,
+ " 1234.567890 | 7) bash-1234 | .... | <==========",
+ " 1234.567890 | 7) bash-1234 | .... | <==========\n",
+ },
};
struct perf_ftrace ftrace;
char *out_buf = NULL;
@@ -869,6 +944,7 @@ static int test_column_layouts(void)
for (i = 0; i < ARRAY_SIZE(cases); i++) {
memset(&ftrace, 0, sizeof(ftrace));
ftrace.use_inline = true;
+ ftrace.graph_noduration = cases[i].noduration;
ftrace.graph_verbose = cases[i].verbose;
out = open_memstream(&out_buf, &out_len);
@@ -889,6 +965,28 @@ static int test_column_layouts(void)
out_buf = NULL;
}
+ /* Test noduration IRQ markers do not pop active frames from tracked stack */
+ memset(&ftrace, 0, sizeof(ftrace));
+ ftrace.use_inline = true;
+ ftrace.graph_noduration = true;
+ ftrace_cpu_states__clear();
+
+ out = open_memstream(&out_buf, &out_len);
+ if (!out)
+ return TEST_FAIL;
+
+ ftrace_process_fgraph_line(&ftrace, " 0) root_func() {", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ==========>", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <==========", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) } /* root_func */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ TEST_ASSERT_VAL("noduration irq markers preserved stack",
+ strstr(out_buf, " 0) } /* root_func */\n") != NULL);
+ free(out_buf);
+
return TEST_OK;
}
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
index 746e966b4049..72c3fddd93bc 100644
--- a/tools/perf/util/ftrace.c
+++ b/tools/perf/util/ftrace.c
@@ -656,6 +656,9 @@ static void make_blank_prefix(const struct perf_ftrace *ftrace, const char *line
memcpy(buf, line, len);
buf[len] = '\0';
+ if (ftrace->graph_noduration)
+ return;
+
if (*pipe_char == '|') {
if (ftrace->graph_verbose) {
/* In verbose mode, duration starts after the comm-pid '|' */
@@ -694,10 +697,12 @@ static void make_blank_prefix(const struct perf_ftrace *ftrace, const char *line
* [ abstime |] CPU) [ comm-pid |] [ duration |] func
*
* The abstime and comm-pid columns are only present with --graph-opts
- * verbose.
+ * verbose, whereas the duration column, and thus its '|' delimiter, is
+ * omitted with --graph-opts noduration.
*
* Return: A pointer to the last character of the context columns (the final
- * '|' delimiter). Returns NULL if @line is not a trace record.
+ * '|' delimiter, or the CPU ')' when no delimiter follows it). Returns NULL
+ * if @line is not a trace record (e.g. a header or a context switch marker).
*/
static const char *find_func_column(const struct perf_ftrace *ftrace,
const char *line, int *cpu)
@@ -751,9 +756,14 @@ static const char *find_func_column(const struct perf_ftrace *ftrace,
return NULL;
}
- p = strchr(p + 1, '|');
- if (!p)
+ if (!ftrace->graph_noduration) {
+ p = strchr(p + 1, '|');
+ if (!p)
+ return NULL;
+ } else if (strstr(p, " => ") || strstr(p, "=====>") || strstr(p, "<=====")) {
+ /* Context switch or interrupt marker */
return NULL;
+ }
*cpu = (int)val;
return p;
@@ -849,11 +859,12 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
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 (*func_start == '\0' ||
+ (!isalpha(*func_start) && *func_start != '_' && !is_exit))
+ return print_line(ftrace, line, out);
+
if (is_exit) {
char exit_name[128];
diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
index 7b9e0919ae8d..0afaa9695691 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -39,6 +39,7 @@ struct perf_ftrace {
int graph_retaddr;
int graph_nosleep_time;
int graph_noirqs;
+ int graph_noduration;
int graph_verbose;
int graph_thresh;
int graph_tail;
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH perf-tools-next v10 10/10] perf ftrace: Filter extraneous functions in function graph tracer
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
` (8 preceding siblings ...)
2026-10-24 21:25 ` [PATCH perf-tools-next v10 09/10] perf ftrace: Support omitting execution duration in function graph tracer Aaron Tomlin
@ 2026-10-24 21:25 ` Aaron Tomlin
9 siblings, 0 replies; 11+ messages in thread
From: Aaron Tomlin @ 2026-10-24 21:25 UTC (permalink / raw)
To: peterz, mingo, acme, namhyung
Cc: mark.rutland, alexander.shishkin, jolsa, irogers, adrian.hunter,
atomlin, chjohnst, neelx, sean, linux-perf-users, linux-kernel
When tracing with the function graph tracer using a targeted filter
(-G or --graph-funcs), users occasionally observe unrelated functions in
the trace output:
1. Top-level functions (e.g. most notably
finish_task_switch.isra.0()) appearing before or alongside the
requested target function. This occurs because clearing and
rewriting set_graph_function creates a transient window where
remote CPUs still observe an empty filter (tracing all
functions) before RCU synchronisation completes, coupled with
perf ftrace sleeping in poll() on trace_pipe right as tracing
starts.
2. Detached/orphan calls inside the target callgraph
(e.g. housekeeping_any_cpu() and arch_irq_work_raise())
appearing as child frames under the target function. These
routines are invoked by rb_wakeups() to notify perf's poll() on
trace_pipe whenever an event is committed, and execute while the
target function is on the CPU.
Resolve these issues deterministically in userspace:
1. Target callgraph state tracking
Track active target callgraph state per-CPU in
tools/perf/util/ftrace.c. Any entry not matching the requested
-G targets is suppressed along with its nested children until
its scope exits. When a target function is matched (at root or
nested depths), record its root depth; for leaf target calls,
immediately reset the filter state upon completion to ensure
subsequent sibling functions remain suppressed. Support glob
wildcard matching (-G 'prefix_*' and compiler clone suffixes).
2. Caller provenance and orphan suppression
Within an active target callgraph, validate that child entries
descend from an active frame on the target stack using their
return addresses (CONFIG_FUNCTION_GRAPH_RETADDR). Any detached
calls originating outside the target hierarchy (such as
ring-buffer wakeups or asynchronous interrupts) are suppressed
along with their subtrees (cs->orphan_depth).
Exits within an orphan subtree are suppressed without querying
the target stack, preventing common helpers (such as
_raw_spin_unlock) from colliding with or prematurely unspooling
active target frames. Dropped-orphan recovery safely resets
orphan depth whenever an entry or exit occurs at or outside
the target root indentation.
3. Dynamic call stack allocation
Maintain the synthetic call stack as a dynamically reallocated
array, growing on demand via realloc(). This supports arbitrarily
deep call trees without fixed-size truncation, ensures unknown
callers are unambiguously identified as orphans, and guarantees
that standard filter teardown executes cleanly regardless of
stack depth upon target function entry.
4. Automated verification
Add comprehensive regression tests in tools/perf/tests/ftrace.c
(test_graph_target_filter()) validating top-level suppression,
detached orphan suppression, glob matching, deep stack
traversal, dropped-orphan recovery, and orphan exit collision
prevention.
Signed-off-by: Aaron Tomlin <atomlin@atomlin.com>
---
tools/perf/builtin-ftrace.c | 42 +-
tools/perf/tests/ftrace.c | 2259 +++++++++++++++++++++++++++++++++++
tools/perf/util/ftrace.c | 671 +++++++++--
tools/perf/util/ftrace.h | 8 +
4 files changed, 2875 insertions(+), 105 deletions(-)
diff --git a/tools/perf/builtin-ftrace.c b/tools/perf/builtin-ftrace.c
index db9616ad68af..1302c9cbfcd6 100644
--- a/tools/perf/builtin-ftrace.c
+++ b/tools/perf/builtin-ftrace.c
@@ -576,13 +576,19 @@ static int set_tracing_funcgraph_retval(struct perf_ftrace *ftrace)
static int set_tracing_funcgraph_retaddr(struct perf_ftrace *ftrace)
{
- if (ftrace->graph_retaddr || ftrace->use_inline) {
+ bool need_retaddr = ftrace->graph_retaddr || ftrace->use_inline ||
+ (ftrace->subcmd == PERF_FTRACE_TRACE &&
+ !list_empty(&ftrace->graph_funcs));
+
+ if (need_retaddr) {
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 -1;
+ if (ftrace->graph_retaddr)
+ return -1;
}
}
@@ -1381,6 +1387,8 @@ static int parse_func_duration(struct perf_ftrace *ftrace, char *line, size_t le
{
char *p;
char *func;
+ char *semi;
+ char *ret_str;
double duration;
/* skip CPU */
@@ -1423,12 +1431,21 @@ static int parse_func_duration(struct perf_ftrace *ftrace, char *line, size_t le
func = skip_spaces(p);
/* skip the closing bracket and the start of comment */
- if (*func == '}')
+ if (*func == '}') {
func += 5;
+ ret_str = strstr(func, " ret=");
+ if (ret_str)
+ *ret_str = '\0';
+ p = func + strlen(func) - 1;
+ } else {
+ semi = strchr(func, ';');
+ if (semi)
+ *semi = '\0';
+ p = func + strlen(func) - 1;
+ }
- /* remove semi-colon or end of comment at the end */
- p = line + len - 1;
- while (!isalnum(*p) && *p != ']') {
+ /* remove parens, semi-colon or end of comment at the end */
+ while (p > func && !isalnum(*p) && *p != ']') {
*p = '\0';
--p;
}
@@ -1853,12 +1870,6 @@ static int parse_sort_key(const struct option *opt, const char *str, int unset)
return 0;
}
-enum perf_ftrace_subcommand {
- PERF_FTRACE_NONE,
- PERF_FTRACE_TRACE,
- PERF_FTRACE_LATENCY,
- PERF_FTRACE_PROFILE,
-};
int cmd_ftrace(int argc, const char **argv)
{
@@ -2013,6 +2024,8 @@ int cmd_ftrace(int argc, const char **argv)
if (subcmd == PERF_FTRACE_NONE)
subcmd = PERF_FTRACE_TRACE;
+ ftrace.subcmd = subcmd;
+
argc = parse_options(argc, argv, options, ftrace_usage,
PARSE_OPT_STOP_AT_NON_OPTION);
if (argc < 0) {
@@ -2034,6 +2047,11 @@ int cmd_ftrace(int argc, const char **argv)
ret = -EINVAL;
goto out_delete_filters;
}
+ if (ftrace.graph_thresh) {
+ pr_err("Error: --inline cannot be combined with --graph-opts thresh\n");
+ ret = -EINVAL;
+ goto out_delete_filters;
+ }
}
cmd_func = __cmd_ftrace;
break;
diff --git a/tools/perf/tests/ftrace.c b/tools/perf/tests/ftrace.c
index b738f04da13c..85454a8d8928 100644
--- a/tools/perf/tests/ftrace.c
+++ b/tools/perf/tests/ftrace.c
@@ -1439,6 +1439,2259 @@ static int test_context_switch_handling(void)
}
#undef TEST_CS_ASSERT
+static int test_graph_target_filter(void)
+{
+ static const char line_idle[] = " 7) 3.000 us | sched_idle_set_state();";
+ static const char line_sw_entry[] = " 6) | finish_task_switch.isra.0() {";
+ static const char line_sw_child[] = " 6) 1.000 us | _raw_spin_unlock();";
+ static const char line_sw_exit[] = " 6) 2.000 us | }";
+ static const char line_tgt_entry[] = " 2) | activate_task() {";
+ static const char line_tgt_child[] = " 2) 1.500 us | enqueue_task();";
+ static const char line_tgt_exit[] = " 2) 2.500 us | }";
+ struct perf_ftrace ftrace;
+ struct filter_entry *entry;
+ struct filter_entry *pos, *tmp;
+ char *out_buf = NULL;
+ size_t out_len = 0;
+ FILE *out;
+ const char *vmlinux;
+ const char *inline_entry, *inline_dup, *inline_exit, *child_exit, *sibling_call;
+ const char *first_act, *second_act, *first_enq, *second_enq;
+ int ret = TEST_OK;
+ int i;
+
+ memset(&ftrace, 0, sizeof(ftrace));
+ INIT_LIST_HEAD(&ftrace.graph_funcs);
+
+ entry = malloc(sizeof(*entry) + strlen("activate_task") + 1);
+ if (!entry)
+ return TEST_FAIL;
+ strcpy(entry->name, "activate_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ /* 1. Test without --inline */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_idle, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+ if (strstr(out_buf, "sched_idle_set_state")) {
+ pr_debug("FAILED: sched_idle_set_state not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "finish_task_switch")) {
+ pr_debug("FAILED: finish_task_switch not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "_raw_spin_unlock")) {
+ pr_debug("FAILED: _raw_spin_unlock not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: enqueue_task child missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 2. Test with --inline */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_idle, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ pr_debug("Target filter output with --inline:\n%s\n", out_buf);
+
+ if (strstr(out_buf, "finish_task_switch")) {
+ pr_debug("FAILED: finish_task_switch not suppressed with --inline\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing with --inline\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 3. Test suppressing detached orphan calls within target graph */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_tgt_entry, out);
+ /* Leaf orphan call from ring_buffer_unlock_commit */
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | housekeeping_any_cpu(); /* <-ring_buffer_unlock_commit+0x1a0/0x1f0 */",
+ out);
+ /* Non-leaf orphan call from irq_work_queue_on */
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | arch_irq_work_raise() { /* <-irq_work_queue_on+0x81/0xa0 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | apic_wait_icr_idle() { /* (inline) */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | x2apic_send_IPI_self();",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | } /* apic_wait_icr_idle (inline) */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) 1.000 us | }",
+ out);
+ /* Genuine child call from activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) | enqueue_task() { /* <-activate_task+0x1d1/0x3e0 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 2) 1.500 us | }",
+ out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ pr_debug("Orphan call filter output:\n%s\n", out_buf);
+
+ if (strstr(out_buf, "housekeeping_any_cpu")) {
+ pr_debug("FAILED: housekeeping_any_cpu orphan not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "arch_irq_work_raise")) {
+ pr_debug("FAILED: arch_irq_work_raise orphan not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "x2apic_send_IPI_self")) {
+ pr_debug("FAILED: x2apic_send_IPI_self not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task() {")) {
+ pr_debug("FAILED: genuine enqueue_task missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 4. Test targeted leaf call at depth > 0 resetting filter */
+ ftrace.use_inline = false;
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("enqueue_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "enqueue_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "finish_task_switch")) {
+ pr_debug("FAILED: finish_task_switch not suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: targeted leaf enqueue_task missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "_raw_spin_unlock")) {
+ pr_debug("FAILED: sibling _raw_spin_unlock not suppressed after leaf target\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 5. Test targeted leaf call at depth > 0 with inlines resetting filter */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "finish_task_switch")) {
+ pr_debug("FAILED: finish_task_switch not suppressed with inlines\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: targeted leaf enqueue_task missing with inlines\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "_raw_spin_unlock")) {
+ pr_debug("FAILED: sibling _raw_spin_unlock leaked with inlines\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ vmlinux = vmlinux_path__find();
+
+ /* 6. Test targeted leaf call with synthesized DWARF inlines closing and resetting filter */
+ if (vmlinux) {
+ const char *inlined_names[FTRACE_MAX_INLINES];
+ int num_inlines;
+ int j;
+ const char *saved_vmlinux = symbol_conf.vmlinux_name;
+ bool saved_ignore_buildid = symbol_conf.ignore_vmlinux_buildid;
+
+ 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;
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() {", out);
+ /* Targeted leaf with caller having inline activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | enqueue_task(); /* <-wake_up_new_task+0x1d1/0x3e0 */",
+ out);
+ /* Sibling call after targeted leaf */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | check_preempt_curr(); /* <-wake_up_new_task+0x0/0x3e0 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* wake_up_new_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: targeted leaf enqueue_task missing\n");
+ ret = TEST_FAIL;
+ } else if (!strstr(out_buf, "} /* activate_task (inline) */")) {
+ pr_debug("FAILED: inline activate_task not closed after leaf target\n");
+ ret = TEST_FAIL;
+ } else if (strstr(out_buf, "check_preempt_curr")) {
+ pr_debug("FAILED: sibling check_preempt_curr leaked after leaf target\n");
+ ret = TEST_FAIL;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 6b. Test child function exit inside inlined function does not
+ * prematurely close inlined frame (Issue 1).
+ */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("wake_up_new_task") + 1);
+ if (!entry) {
+ 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;
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "wake_up_new_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ 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;
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-wake_up_new_task+0x1d1 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | check_preempt_curr(); /* <-wake_up_new_task+0x1d1 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* wake_up_new_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ inline_entry = strstr(out_buf, "activate_task() { /* (inline) */");
+ if (inline_entry)
+ inline_dup = strstr(inline_entry + 1,
+ "activate_task() { /* (inline) */");
+ else
+ inline_dup = NULL;
+ inline_exit = strstr(out_buf, "} /* activate_task (inline) */");
+ child_exit = strstr(out_buf, "} /* enqueue_task */");
+ sibling_call = strstr(out_buf, "check_preempt_curr();");
+
+ if (!inline_entry) {
+ pr_debug("FAILED: inline activate_task entry missing\n");
+ ret = TEST_FAIL;
+ } else if (inline_dup) {
+ pr_debug("FAILED: duplicate inline activate_task synthesized\n");
+ ret = TEST_FAIL;
+ } else if (!inline_exit || !child_exit || !sibling_call) {
+ pr_debug("FAILED: expected trace frames missing\n");
+ ret = TEST_FAIL;
+ } else if (inline_exit < sibling_call) {
+ pr_debug("FAILED: inline activate_task closed before sibling call\n");
+ ret = TEST_FAIL;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 6c. Test recursive target root function with --inline (Issue 2).
+ * Inner target root exit must not prematurely close inlined frames
+ * or unseat outer target root.
+ */
+ 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;
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Outer target root */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() {", out);
+ /* Recursive target root call with inline activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() { /* <-wake_up_new_task+0x1d1 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-wake_up_new_task+0x10 */",
+ out);
+ /* Inner target root exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* wake_up_new_task */", out);
+ /* Sibling call under inlined activate_task in outer target */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | check_preempt_curr(); /* <-wake_up_new_task+0x1d1 */",
+ out);
+ /* Outer target root exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 4.000 us | } /* wake_up_new_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ inline_entry = strstr(out_buf, "activate_task() { /* (inline) */");
+ inline_exit = strstr(out_buf, "} /* activate_task (inline) */");
+ sibling_call = strstr(out_buf, "check_preempt_curr();");
+
+ if (!inline_entry) {
+ pr_debug("FAILED: recursive target: inline activate_task missing\n");
+ ret = TEST_FAIL;
+ } else if (!inline_exit || !sibling_call) {
+ pr_debug("FAILED: recursive target: expected frames missing\n");
+ ret = TEST_FAIL;
+ } else if (inline_exit < sibling_call) {
+ pr_debug("FAILED: recursive target: inline closed before outer sibling\n");
+ ret = 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;
+ if (ret != TEST_OK)
+ goto out_free_filter;
+ }
+
+ /* 7. Test recovery when an orphan exit event is dropped by the ring buffer */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("activate_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "activate_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ /* 7a. Method 1: Target function exits directly after a dropped orphan exit */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Orphan entry: housekeeping_any_cpu called from outside activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | housekeeping_any_cpu() { /* <-rb_wakeups+0x10 */",
+ out);
+ /* Exit of housekeeping_any_cpu is DROPPED by ring buffer (never arrives) */
+ /* Target function exits with named closing brace */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: target activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "housekeeping_any_cpu")) {
+ pr_debug("FAILED: orphan housekeeping_any_cpu leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target activate_task exit swallowed after orphan exit dropped\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 7b. Method 2: Target function calls legitimate child after a dropped orphan exit */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Orphan entry: housekeeping_any_cpu called from outside activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | housekeeping_any_cpu() { /* <-rb_wakeups+0x10 */",
+ out);
+ /* Exit of housekeeping_any_cpu is DROPPED by ring buffer */
+ /* Legitimate child call from activate_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.200 us | enqueue_task(); /* <-activate_task+0x1d1/0x3e0 */",
+ out);
+ /* Target function exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "housekeeping_any_cpu")) {
+ pr_debug("FAILED: orphan housekeeping_any_cpu leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: legitimate child enqueue_task swallowed after orphan exit dropped\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target activate_task exit missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 8. Test target_root_depth synchronisation when target root enters
+ * after dropped sibling exit.
+ */
+ /* 8a. Without --inline */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Caller at depth 0 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() {", out);
+ /* Extraneous sibling at depth 1 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | stale_sibling() {", out);
+ /* Exit of stale_sibling is DROPPED by ring buffer */
+ /* Target function enters with caller wake_up_new_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-wake_up_new_task+0x10 */", out);
+ /* First child of target enters and exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ /* Second child of target enters and exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | check_preempt_curr(); /* <-activate_task+0x30 */", out);
+ /* Target function exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "stale_sibling")) {
+ pr_debug("FAILED: stale_sibling leaked into output (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task() {")) {
+ pr_debug("FAILED: first child enqueue_task missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "check_preempt_curr();")) {
+ pr_debug("FAILED: second child check_preempt_curr missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target activate_task exit missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 8b. With --inline */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Caller at depth 0 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | wake_up_new_task() {", out);
+ /* Extraneous sibling at depth 1 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | stale_sibling() {", out);
+ /* Exit of stale_sibling is DROPPED by ring buffer */
+ /* Target function enters with caller wake_up_new_task */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-wake_up_new_task+0x10 */", out);
+ /* First child of target enters and exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ /* Second child of target enters and exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | check_preempt_curr(); /* <-activate_task+0x30 */", out);
+ /* Target function exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+ /* Sibling call after target function */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | after_target(); /* <-wake_up_new_task+0x40 */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "stale_sibling")) {
+ pr_debug("FAILED: stale_sibling leaked into output (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task() {")) {
+ pr_debug("FAILED: first child enqueue_task missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "check_preempt_curr();")) {
+ pr_debug("FAILED: second child check_preempt_curr missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target activate_task exit missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "after_target")) {
+ pr_debug("FAILED: after_target leaked into output (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 9. Test deep legitimate subtrees when stack depth exceeds
+ * FTRACE_MAX_STACK (overflow_depth > 0).
+ */
+ /* 9a. Without --inline */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | sub_func() { /* <-activate_task+0x10 */", out);
+ }
+ /* Push frames beyond FTRACE_MAX_STACK (256) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | overflow_parent() { /* <-sub_func+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | overflow_child() { /* <-overflow_parent+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | overflow_leaf(); /* <-overflow_child+0x30 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.200 us | }", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.400 us | }", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ }
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "overflow_parent() {")) {
+ pr_debug("FAILED: overflow_parent entry missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "overflow_child() {")) {
+ pr_debug("FAILED: overflow_child entry missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "overflow_leaf();")) {
+ pr_debug("FAILED: overflow_leaf missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 9b. With --inline */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | sub_func() { /* <-activate_task+0x10 */", out);
+ }
+ /* Push frames beyond FTRACE_MAX_STACK (256) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | overflow_parent() { /* <-sub_func+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | overflow_child() { /* <-overflow_parent+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | overflow_leaf(); /* <-overflow_child+0x30 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.200 us | }", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.400 us | }", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ }
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "overflow_parent() {")) {
+ pr_debug("FAILED: overflow_parent entry missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "overflow_child() {")) {
+ pr_debug("FAILED: overflow_child entry missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "overflow_leaf();")) {
+ pr_debug("FAILED: overflow_leaf missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 9. Test glob wildcard filter (-G 'activate_*') */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("activate_*") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "activate_*");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_idle, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "finish_task_switch")) {
+ pr_debug("FAILED: finish_task_switch leaked with wildcard\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: wildcard target activate_* missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: wildcard child enqueue_task missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 10. Test wildcard filter matching compiler clone with suffix (-G '*task_switch') */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("*task_switch") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "*task_switch");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace, line_idle, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_child, out);
+ ftrace_process_fgraph_line(&ftrace, line_sw_exit, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_entry, out);
+ ftrace_process_fgraph_line(&ftrace, line_tgt_exit, out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "finish_task_switch.isra.0() {")) {
+ pr_debug("FAILED: wildcard target *task_switch did not match clone\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "_raw_spin_unlock();")) {
+ pr_debug("FAILED: child _raw_spin_unlock missing under clone target\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "activate_task")) {
+ pr_debug("FAILED: unrelated activate_task leaked under *task_switch\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 11. Test that orphan exits sharing name with target active frame
+ * do not collide (Issue 1).
+ */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("activate_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "activate_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | _raw_spin_unlock() { /* <-activate_task+0x10 */", out);
+ /* Detached interrupt enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */", out);
+ /* Interrupt invokes _raw_spin_unlock and exits with named brace */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | _raw_spin_unlock() { /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | } /* _raw_spin_unlock */", out);
+ /* Interrupt exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Target's _raw_spin_unlock exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* _raw_spin_unlock */", out);
+ /* Target continues and exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 4.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt")) {
+ pr_debug("FAILED: orphan smp_apic_timer_interrupt leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: activate_task exit missing or prematurely unspooled\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 12. Test target function entering at deep stack (depth >= 256)
+ * and exiting without leaking sibling (Issue 2).
+ */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Push 256 non-target outer frames */
+ for (i = 0; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | outer_func() {", out);
+ }
+ /* Target function enters at depth 256 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ /* Unrelated sibling enters at depth 256 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | unrelated_sibling() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | unrelated_child();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Unspool outer frames */
+ for (i = 0; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | }", out);
+ }
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: deep activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: deep enqueue_task child missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "unrelated_sibling")) {
+ pr_debug("FAILED: unrelated_sibling leaked after deep target exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 13. Test orphan suppression during deep stacks (Issue 3) */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | sub_func() { /* <-activate_task+0x10 */", out);
+ }
+ /* Target call at depth 256 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | deep_child() { /* <-sub_func+0x10 */", out);
+ /* Detached interrupt fires at depth 257 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | irq_sub(); /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Target continues */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | deep_leaf(); /* <-deep_child+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ for (i = 1; i < 256; i++) {
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ }
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt") || strstr(out_buf, "irq_sub")) {
+ pr_debug("FAILED: interrupt leaked during deep stack\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "deep_leaf();")) {
+ pr_debug("FAILED: legitimate deep_leaf missing during deep stack\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 14. Test orphan calling common helper present on stack does not leak child (Issue 2) */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | _raw_spin_unlock() { /* <-activate_task+0x10 */", out);
+ /* Detached interrupt fires */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */",
+ out);
+ /* Orphan interrupt calls _raw_spin_unlock */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | _raw_spin_unlock() { /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ /* Orphan _raw_spin_unlock calls child helper with retaddr <-_raw_spin_unlock+0x10 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | native_queued_spin_unlock(); /* <-_raw_spin_unlock+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | }", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Legitimate target child call */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | queued_spin_unlock(); /* <-_raw_spin_unlock+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | }", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 4.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "native_queued_spin_unlock")) {
+ pr_debug("FAILED: orphan child native_queued_spin_unlock leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "queued_spin_unlock();")) {
+ pr_debug("FAILED: target child queued_spin_unlock missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 15. Test dropped target root exit followed by outer ancestor call
+ * (Issues 4A & 4B).
+ */
+ /* 15a. Without --inline */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Ancestor frame at depth 0 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ /* Target function enters at depth 1 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ /* Target root exit is DROPPED by ring buffer! */
+ /* Outer ancestor schedule() calls another function */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | context_switch() { /* <-schedule+0x50 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | prepare_task_switch(); /* <-context_switch+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | }", out);
+ /* Ancestor schedule() exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+ /* Subsequent target function invocation on same CPU */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: outer ancestor schedule leaked into output (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "context_switch")) {
+ pr_debug("FAILED: ancestor child context_switch leaked (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: legitimate enqueue_task missing (!inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 15b. With --inline */
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ /* Dropped target root exit */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | context_switch() { /* <-schedule+0x50 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | prepare_task_switch(); /* <-context_switch+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | }", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+ /* Subsequent target function invocation on same CPU */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: outer ancestor schedule leaked into output (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "context_switch")) {
+ pr_debug("FAILED: ancestor child context_switch leaked (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: legitimate enqueue_task missing (--inline)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 16. Test dropped-orphan exit recovery when target root exits (Issue 5) */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ /* Detached orphan interrupt enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | irq_sub(); /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ /* Orphan exit is DROPPED by ring buffer! orphan_depth remains 1 */
+ /* Target function exits with named closing brace */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ /* Ancestor schedule exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt") || strstr(out_buf, "irq_sub")) {
+ pr_debug("FAILED: dropped-orphan interrupt leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: activate_task exit missing after dropped orphan\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: schedule leaked after target exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 17. Test dropped-orphan exit recovery with bare '}' exit (Issue 3.1) */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ /* Detached orphan interrupt enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | irq_sub(); /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ /* Orphan exit is DROPPED by ring buffer! orphan_depth remains 1 */
+ /* Target function exits with bare closing brace '}' */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Ancestor schedule exits with bare '}' */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt") || strstr(out_buf, "irq_sub")) {
+ pr_debug("FAILED: dropped-orphan interrupt leaked (bare exit)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing (bare exit)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "}")) {
+ pr_debug("FAILED: activate_task exit missing after dropped orphan (bare exit)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: schedule leaked after target bare exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 18. Test dropped top-level target root exit followed by next invocation (Issue 3.2) */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* First top-level invocation of target root */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ /* Target root exit is DROPPED by ring buffer! */
+ /* Second top-level invocation of target root with untraced caller */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-untraced_caller+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ first_act = strstr(out_buf, "activate_task() {");
+ second_act = first_act ? strstr(first_act + 1, "activate_task() {") : NULL;
+ first_enq = strstr(out_buf, "enqueue_task();");
+ second_enq = first_enq ? strstr(first_enq + 1, "enqueue_task();") : NULL;
+
+ if (!first_act || !second_act) {
+ pr_debug("FAILED: subsequent target root dropped after dropped top-level exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!first_enq || !second_enq) {
+ pr_debug("FAILED: child calls missing after dropped top-level exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 19. Test dropped target root exit (depth > 0) followed by target recall (Issue 3.3) */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Ancestor schedule() at depth 0 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ /* Target function at depth 1 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ /* Target exit is DROPPED by ring buffer! */
+ /* Ancestor calls target function again */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x30 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: ancestor schedule leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ first_act = strstr(out_buf, "activate_task() {");
+ second_act = first_act ? strstr(first_act + 1, "activate_task() {") : NULL;
+
+ if (!first_act || !second_act) {
+ pr_debug("FAILED: target root not recovered on recall by ancestor\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 20. Test nested target-matching call with caller_idx < 0 inside
+ * an orphan subtree does not unwind outer target call stack.
+ */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("activate_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "activate_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ entry = malloc(sizeof(*entry) + strlen("enqueue_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "enqueue_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters at root indentation */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Detached orphan interrupt enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */", out);
+ /* Nested target-matching function called inside orphan with caller_idx < 0 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | enqueue_task() { /* <-smp_apic_timer_interrupt+0x20 */",
+ out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | } /* enqueue_task */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | }", out);
+ /* Target continues with legitimate child */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.200 us | enqueue_task(); /* <-activate_task+0x1d1 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt")) {
+ pr_debug("FAILED: nested orphan interrupt leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: legitimate child enqueue_task missing after nested target-match in orphan\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: activate_task exit missing after nested target-match in orphan\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 21. Test orphan exit named differently from target root is not
+ * misidentified as target root exit when extra_indent > 0.
+ */
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ entry = malloc(sizeof(*entry) + strlen("activate_task") + 1);
+ if (!entry) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ strcpy(entry->name, "activate_task");
+ list_add_tail(&entry->list, &ftrace.graph_funcs);
+
+ ftrace.use_inline = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Synthesised inline child adds extra_indent */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | inline_child() { /* (inline) */", out);
+ /* Detached orphan interrupt enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */", out);
+ /* Orphan exits with named closing brace */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | } /* _raw_spin_unlock */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+ /* Genuine target child */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | } /* inline_child (inline) */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 4.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt")) {
+ pr_debug("FAILED: orphan smp_apic_timer_interrupt leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: genuine child missing after orphan exit with different name\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target root exit prematurely unspooled by orphan exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 22. Test target root entry with caller_idx < 0 purges stale
+ * non-target frames so calls matching old frames do not terminate target.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Non-target function enters; its exit is dropped */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | stale_ancestor() {", out);
+ /* Target root enters with untraced caller (caller_idx < 0) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-untraced+0x10 */", out);
+ /* Target child calls helper with retaddr to stale_ancestor name */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | helper() { /* <-stale_ancestor+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ /* Target legitimate child */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.500 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "stale_ancestor")) {
+ pr_debug("FAILED: stale_ancestor leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: target prematurely terminated by match on stale frame below target root\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: target root exit missing after stale frame purge\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 23. Test !use_inline dropped child exit does not prevent bare '}'
+ * from exiting target root.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Child 1 enters; its exit is DROPPED by ring buffer */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | child_one() { /* <-activate_task+0x10 */", out);
+ /* Caller activate_task calls Child 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | child_two(); /* <-activate_task+0x20 */", out);
+ /* Target root exits with bare closing brace '}' */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.500 us | }", out);
+ /* Subsequent non-target function enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | unrelated_after() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | unrelated_child();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task entry missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "child_two();")) {
+ pr_debug("FAILED: child_two missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "unrelated_after")) {
+ pr_debug("FAILED: unrelated_after leaked; bare '}' matched dropped child instead of target root\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 24. Test graph_thresh with -G outputs return lines when !use_inline.
+ */
+ ftrace.use_inline = false;
+ ftrace.graph_thresh = 1000;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 5.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+ ftrace.graph_thresh = 0;
+
+ if (!strstr(out_buf, "} /* activate_task */")) {
+ pr_debug("FAILED: graph_thresh return event suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 25. Test orphan subtree invoking target root function T at deeper
+ * indentation does not reset orphan_depth or target root state.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Detached orphan interrupt enters at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | irq_handler() { /* <-unrelated_irq+0x10 */", out);
+ /* Orphan invokes activate_task at indent 6 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-irq_handler+0x20 */", out);
+ /* Nested activate_task calls child inside orphan */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | orphan_child(); /* <-activate_task+0x10 */", out);
+ /* Nested activate_task exits inside orphan */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ /* Orphan interrupt exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* irq_handler */", out);
+ /* Real child of outer target root executes */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | real_child(); /* <-activate_task+0x30 */", out);
+ /* Outer target root exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 5.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "orphan_child")) {
+ pr_debug("FAILED: orphan_child leaked from nested invocation\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "real_child")) {
+ pr_debug("FAILED: real_child missing; outer target root was prematurely closed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 26. Test IRQ markers outside target function are suppressed with -G,
+ * but preserved when inside target callgraph.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* IRQ marker outside target */
+ ftrace_process_fgraph_line(&ftrace, " 0) ==========> |", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <========== |", out);
+
+ /* Target enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* IRQ marker inside target */
+ ftrace_process_fgraph_line(&ftrace, " 0) ==========> |", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <========== |", out);
+ /* Target exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ first_act = strstr(out_buf, "==========>");
+ if (!first_act) {
+ pr_debug("FAILED: IRQ marker inside target suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ second_act = strstr(first_act + 1, "==========>");
+ if (second_act) {
+ pr_debug("FAILED: IRQ marker outside target leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 27. Test !has_retaddr dropped target root exit does not leak non-target.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters at indent 2; exit is dropped */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Non-target enters at indent 2 without retaddr */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | nontarget_leak() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | nontarget_child();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "nontarget_leak")) {
+ pr_debug("FAILED: nontarget_leak leaked after dropped target exit (!has_retaddr)\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 28. Test !in_target unwinds frames with raw_indent >= leading_spaces
+ * before pushing to avoid stale ancestor accumulation.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Non-target stale ancestor enters at indent 2; exit is dropped */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | stale_ancestor() {", out);
+ /* Real non-target parent enters at indent 2 (same indent) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | real_parent() {", out);
+ /* Real parent calls target root */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-real_parent+0x10 */", out);
+ /* Target calls child that has return address pointing to stale_ancestor */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | shared_child(); /* <-stale_ancestor+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* real_parent */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "stale_ancestor() {")) {
+ pr_debug("FAILED: stale_ancestor leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "activate_task() {")) {
+ pr_debug("FAILED: activate_task missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 29. Test has_retaddr dropped target root exit followed by non-target
+ * entry at root indent resets target state and allows subsequent
+ * child target invocation.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root 1 enters at indent 2; exit is DROPPED by ring buffer */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+
+ /* Non-target parent enters at indent 2 with retaddr to untraced caller */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | unrelated_caller() { /* <-untraced_kernel+0x10 */", out);
+
+ /* Unrelated caller invokes target root as child at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-unrelated_caller+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x30 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ /* Unrelated caller exits with bare closing brace '}' at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "unrelated_caller")) {
+ pr_debug("FAILED: unrelated_caller leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: child target enqueue_task missing\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 30. Test noduration IRQ markers outside target function are suppressed
+ * with -G, but preserved when inside target callgraph.
+ */
+ ftrace.use_inline = false;
+ ftrace.graph_noduration = true;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Noduration IRQ marker outside target */
+ ftrace_process_fgraph_line(&ftrace, " 0) ==========>", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <==========", out);
+
+ /* Target enters */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) activate_task() {", out);
+ /* Noduration IRQ marker inside target */
+ ftrace_process_fgraph_line(&ftrace, " 0) ==========>", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <==========", out);
+ /* Target exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+ ftrace.graph_noduration = false;
+
+ first_act = strstr(out_buf, "==========>");
+ if (!first_act) {
+ pr_debug("FAILED: noduration IRQ marker inside target suppressed\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ second_act = strstr(first_act + 1, "==========>");
+ if (second_act) {
+ pr_debug("FAILED: noduration IRQ marker outside target leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 31. Test ancestor frame preservation when ancestor calls sibling
+ * at leading_spaces <= target_root_indent after dropped target root exit.
+ */
+ ftrace.use_inline = false;
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Ancestor schedule() enters at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ /* Target function enters at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ /* Target root exit is DROPPED by ring buffer */
+ /*
+ * Ancestor schedule() calls sibling non-target function at indent 4
+ * with return address pointing into schedule (caller_idx == 0 < target_root_depth).
+ */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | context_switch() { /* <-schedule+0x50 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | }", out);
+ /* Ancestor schedule() calls target function again at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x60 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ /* Ancestor schedule() exits */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: ancestor schedule leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "context_switch")) {
+ pr_debug("FAILED: sibling context_switch leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ first_act = strstr(out_buf, "activate_task() {");
+ second_act = first_act ? strstr(first_act + 1, "activate_task() {") : NULL;
+ if (!first_act || !second_act) {
+ pr_debug("FAILED: target function not tracked across ancestor sibling call\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /*
+ * 32. Test recovery when both an orphan exit and enclosing target root exit
+ * are dropped by ring buffer.
+ */
+ /* 32a. Followed by non-target entry at root indent and child target invocation */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target root enters at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ /* Detached orphan interrupt enters inside target at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | irq_sub(); /* <-smp_apic_timer_interrupt+0x20 */", out);
+ /* Orphan exit and target root exit are BOTH DROPPED by ring buffer */
+ /* Non-target caller enters at root indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | unrelated_caller() { /* <-untraced_kernel+0x10 */", out);
+ /* Unrelated caller invokes target root as child at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-unrelated_caller+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task(); /* <-activate_task+0x30 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | }", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "smp_apic_timer_interrupt") || strstr(out_buf, "irq_sub")) {
+ pr_debug("FAILED: orphan leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "unrelated_caller")) {
+ pr_debug("FAILED: unrelated_caller leaked into output\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: target child enqueue_task missing after double drop recovery\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 32b. Followed by ancestor exit at root indent */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Ancestor schedule() enters at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | schedule() {", out);
+ /* Target function enters at indent 4 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ /* Orphan interrupt enters at indent 6 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | smp_apic_timer_interrupt() { /* <-irq_work+0x10 */", out);
+ /* Orphan exit and target root exit are BOTH DROPPED by ring buffer */
+ /* Ancestor schedule() exits at indent 2 */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+ /* Subsequent target function invocation */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: ancestor schedule leaked after double drop exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "smp_apic_timer_interrupt")) {
+ pr_debug("FAILED: orphan smp_apic_timer_interrupt leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: subsequent target invocation suppressed after double drop exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 32c. Dropped target root exit followed by untraced ancestor exit (match_idx < 0) */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* Target function enters at indent 4 with caller_idx < 0 (untraced caller) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-untraced_parent+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ /* Target root exit is DROPPED by ring buffer */
+ /* Untraced ancestor schedule() exits at indent 2 (match_idx < 0) */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 3.000 us | } /* schedule */", out);
+ /* Out-of-target IRQ marker must NOT be printed because target has exited */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) ==========>", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) <==========", out);
+ /* Subsequent target function invocation */
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 2.000 us | } /* activate_task */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "schedule")) {
+ pr_debug("FAILED: untraced ancestor schedule leaked\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (strstr(out_buf, "==========")) {
+ pr_debug("FAILED: out-of-target IRQ marker leaked after dropped root exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ if (!strstr(out_buf, "enqueue_task();")) {
+ pr_debug("FAILED: second activate_task invocation suppressed after untraced drop exit\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+ /* 33. Rekey with to_copy == 0 when target root exited on new CPU */
+ out = open_memstream(&out_buf, &out_len);
+ if (!out) {
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+
+ /* CPU 0: Task 15 enters non-target ancestor and target function */
+ ftrace_process_fgraph_line(&ftrace, " 0) | schedule() {", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | activate_task() { /* <-schedule+0x10 */", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) 1.000 us | enqueue_task();", out);
+ /* CPU 1: Task 15 unwinds target root down to schedule (indent 2) */
+ ftrace_process_fgraph_line(&ftrace, " 1) 2.000 us | } /* activate_task */", out);
+ /* Pre-bind empty task15 state on CPU 2 */
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) <idle>-0 => task15-1500 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) task15-1500 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 2) ------------------------------------------", out);
+ /* CPU 1 emits banner first: merges ts1 (depth 0, min_raw_indent 2) into existing */
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) task15-1500 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 1) ------------------------------------------", out);
+ /* CPU 0 emits banner second: to_copy is 0, base_raw_indent 2 prunes activate_task */
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) task15-1500 => <idle>-0 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ /* Task 15 resumes: ancestor schedule() calls non-target sibling child */
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) <idle>-0 => task15-1500 ", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) ------------------------------------------", out);
+ ftrace_process_fgraph_line(&ftrace,
+ " 0) | nontarget_child(); /* <-schedule+0x20 */", out);
+ ftrace_process_fgraph_line(&ftrace, " 0) 3.000 us | } /* schedule */", out);
+
+ fclose(out);
+ ftrace_cpu_states__clear();
+
+ if (strstr(out_buf, "nontarget_child")) {
+ pr_debug("FAILED: nontarget_child leaked after to_copy == 0 rekey\n");
+ ret = TEST_FAIL;
+ goto out_free_filter;
+ }
+ free(out_buf);
+ out_buf = NULL;
+
+out_free_filter:
+ if (out_buf)
+ free(out_buf);
+ list_for_each_entry_safe(pos, tmp, &ftrace.graph_funcs, list) {
+ list_del(&pos->list);
+ free(pos);
+ }
+ return ret;
+}
+
static int test__parse_retaddr(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
{
return test_parse_retaddr();
@@ -1454,6 +3707,11 @@ static int test__column_layouts(struct test_suite *test __maybe_unused, int subt
return test_column_layouts();
}
+static int test__graph_target_filter(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
+{
+ return test_graph_target_filter();
+}
+
static int test__stack_saturation(struct test_suite *test __maybe_unused, int subtest __maybe_unused)
{
return test_stack_saturation();
@@ -1473,6 +3731,7 @@ static struct test_case ftrace_test_cases[] = {
TEST_CASE("Parse return address comments", parse_retaddr),
TEST_CASE("Filter return address comments", filter_retaddr),
TEST_CASE("Function graph column layouts", column_layouts),
+ TEST_CASE("Graph target filtering", graph_target_filter),
TEST_CASE("Stack saturation and expansion", stack_saturation),
TEST_CASE("Context switch handling", context_switch_handling),
TEST_CASE("Inline symbol and trace processing", inline_processing),
diff --git a/tools/perf/util/ftrace.c b/tools/perf/util/ftrace.c
index 72c3fddd93bc..ab8300602fa8 100644
--- a/tools/perf/util/ftrace.c
+++ b/tools/perf/util/ftrace.c
@@ -24,6 +24,7 @@
#include "srcline.h"
#include "symbol.h"
#include "util/ftrace.h"
+#include "util/string2.h"
#include <perf/threadmap.h>
#define FTRACE_MAX_CPUS 4096
@@ -48,9 +49,16 @@ struct ftrace_task_state {
int max_stack;
int depth;
int inlined_depth;
- int overflow_depth;
u64 seq;
int min_raw_indent;
+ u64 seq;
+ int min_raw_indent;
+ bool in_target;
+ int target_root_depth;
+ int target_root_frame_idx;
+ int target_root_indent;
+ char target_root_name[128];
+ int orphan_depth;
};
#define ftrace_cpu_state ftrace_task_state
@@ -165,6 +173,23 @@ static struct ftrace_task_state *get_task_state(pid_t pid)
return ts;
}
+/**
+ * reset_target_root_state - Clear graph target filter state for a task
+ * @cs: Task state to reset
+ *
+ * Reset all state tracking associated with an active graph target filter,
+ * restoring the task to an out-of-target state.
+ */
+static void reset_target_root_state(struct ftrace_task_state *cs)
+{
+ cs->in_target = false;
+ cs->target_root_depth = 0;
+ cs->target_root_frame_idx = 0;
+ cs->target_root_indent = 0;
+ cs->target_root_name[0] = '\0';
+ cs->orphan_depth = 0;
+}
+
/**
* ftrace_task_state__rekey - Reassign a task state to a new PID
* @ts: The task state to re-key
@@ -236,6 +261,9 @@ static struct ftrace_task_state *ftrace_task_state__rekey(struct ftrace_task_sta
}
}
+ if (older->in_target && older->target_root_frame_idx >= older->depth)
+ reset_target_root_state(older);
+
to_copy = ts->depth;
if (to_copy > 0) {
while (existing->depth + to_copy > existing->max_stack) {
@@ -250,9 +278,42 @@ static struct ftrace_task_state *ftrace_task_state__rekey(struct ftrace_task_sta
existing->depth * sizeof(existing->stack[0]));
memcpy(&existing->stack[0], &ts->stack[0],
to_copy * sizeof(existing->stack[0]));
+
+ if (ts->in_target &&
+ ts->target_root_frame_idx < ts->depth) {
+ existing->in_target = true;
+ existing->target_root_depth =
+ ts->target_root_depth;
+ existing->target_root_frame_idx =
+ ts->target_root_frame_idx;
+ existing->target_root_indent =
+ ts->target_root_indent;
+ strlcpy(existing->target_root_name,
+ ts->target_root_name,
+ sizeof(existing->target_root_name));
+ existing->orphan_depth = ts->orphan_depth;
+ } else if (existing->in_target &&
+ existing->target_root_frame_idx >= 0) {
+ existing->target_root_depth += to_copy;
+ existing->target_root_frame_idx += to_copy;
+ }
} else {
memcpy(&existing->stack[existing->depth], &ts->stack[0],
to_copy * sizeof(existing->stack[0]));
+
+ if (!existing->in_target && ts->in_target) {
+ existing->in_target = true;
+ existing->target_root_depth =
+ existing->depth + ts->target_root_depth;
+ existing->target_root_frame_idx =
+ existing->depth + ts->target_root_frame_idx;
+ existing->target_root_indent =
+ ts->target_root_indent;
+ strlcpy(existing->target_root_name,
+ ts->target_root_name,
+ sizeof(existing->target_root_name));
+ existing->orphan_depth = ts->orphan_depth;
+ }
}
existing->depth += to_copy;
@@ -629,6 +690,36 @@ static void extract_exit_func_name(const char *str, char *buf, size_t size)
buf[i] = '\0';
}
+/**
+ * push_nontarget_frame - Push non-target entry frame to call stack
+ * @cs: CPU stack state
+ * @func_start: Pointer to start of function name in trace record
+ * @leading_spaces: Indentation of trace record
+ *
+ * Return: 0 on success, or negative error code on failure.
+ */
+static int push_nontarget_frame(struct ftrace_task_state *cs, const char *func_start,
+ int leading_spaces)
+{
+ char entry_name[128];
+
+ extract_entry_func_name(func_start, entry_name, sizeof(entry_name));
+ if (cs->depth >= cs->max_stack && ftrace_task_state__grow_stack(cs) < 0)
+ return -ENOMEM;
+
+ cs->stack[cs->depth].name = strdup(entry_name);
+ if (!cs->stack[cs->depth].name) {
+ pr_err("Not enough memory\n");
+ return -ENOMEM;
+ }
+ cs->stack[cs->depth].inlined = false;
+ cs->stack[cs->depth].indent = leading_spaces;
+ cs->stack[cs->depth].raw_indent = leading_spaces;
+ cs->depth++;
+ cs->min_raw_indent = -1;
+ return 0;
+}
+
/**
* make_blank_prefix - Create a blank prefix mirroring a trace record's context
* @ftrace: Perf ftrace context
@@ -684,6 +775,40 @@ static void make_blank_prefix(const struct perf_ftrace *ftrace, const char *line
}
}
+/**
+ * parse_graph_cpu - Extract CPU number from a function_graph trace record
+ * @ftrace: Perf ftrace context
+ * @line: The trace record to parse
+ * @cpu: Pointer to store the CPU number
+ *
+ * Return: true if CPU was successfully parsed, false otherwise.
+ */
+static bool parse_graph_cpu(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 false;
+ p++;
+ }
+
+ p = skip_spaces(p);
+ val = strtol(p, &endptr, 10);
+ if (endptr == p || *endptr != ')')
+ return false;
+
+ if (val < 0 || val >= FTRACE_MAX_CPUS)
+ return false;
+
+ *cpu = (int)val;
+ return true;
+}
+
/**
* find_func_column - Locate the function call column in a trace record
* @ftrace: Perf ftrace context
@@ -801,6 +926,201 @@ static int print_line(struct perf_ftrace *ftrace, const char *line, FILE *out)
return 0;
}
+/**
+ * ftrace_has_graph_funcs - Test whether graph target filters are active
+ * @ftrace: Perf ftrace context
+ *
+ * Return: true if any target filter functions are configured, false otherwise.
+ */
+static inline bool ftrace_has_graph_funcs(const struct perf_ftrace *ftrace)
+{
+ return ftrace->graph_funcs.next != NULL && !list_empty(&ftrace->graph_funcs);
+}
+
+/**
+ * ftrace_is_graph_func - Check if a function matches the graph_funcs target filter
+ * @ftrace: Perf ftrace context
+ * @func_name: The function name to test
+ *
+ * Return: true if ftrace->graph_funcs is empty or @func_name matches a
+ * target filter (including glob and compiler clone matches), false otherwise.
+ */
+static bool ftrace_is_graph_func(const struct perf_ftrace *ftrace, const char *func_name)
+{
+ struct filter_entry *pos;
+ char base_name[128];
+ const char *dot;
+
+ if (!ftrace_has_graph_funcs(ftrace))
+ return true;
+
+ /* Extract base symbol name by stripping compiler clone suffixes (.isra.*, .part.*, etc.) */
+ dot = strchr(func_name, '.');
+ if (dot) {
+ size_t base_len = dot - func_name;
+
+ if (base_len < sizeof(base_name)) {
+ memcpy(base_name, func_name, base_len);
+ base_name[base_len] = '\0';
+ } else {
+ dot = NULL;
+ }
+ }
+
+ list_for_each_entry(pos, &ftrace->graph_funcs, list) {
+ /* Fast path: non-glob patterns */
+ if (!strisglob(pos->name)) {
+ size_t len = strlen(pos->name);
+
+ if (strcmp(pos->name, func_name) == 0)
+ return true;
+ if (!strncmp(pos->name, func_name, len) &&
+ (func_name[len] == '\0' || func_name[len] == '.'))
+ return true;
+ continue;
+ }
+
+ /* Glob pattern matching */
+ if (strglobmatch(func_name, pos->name))
+ return true;
+ if (dot && strglobmatch(base_name, pos->name))
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * ftrace_reset_to_target_root - Initialise a new target root frame state
+ * @cs: Task state to update
+ * @entry_name: Function symbol name matching the graph filter
+ * @depth: Call stack depth at which the target root function entered
+ * @leading_spaces: Indentation of the target root function
+ *
+ * Mark the task as actively inside a target filter hierarchy rooted at @depth
+ * for function @entry_name, and clear any stale orphan tracking depth.
+ */
+static void ftrace_reset_to_target_root(struct ftrace_task_state *cs,
+ const char *entry_name, int depth,
+ int leading_spaces)
+{
+ cs->in_target = true;
+ cs->target_root_depth = depth;
+ cs->target_root_frame_idx = depth;
+ cs->target_root_indent = leading_spaces;
+ strlcpy(cs->target_root_name, entry_name, sizeof(cs->target_root_name));
+ cs->orphan_depth = 0;
+}
+
+/**
+ * ftrace_unwind_to_depth - Unwind call stack frames down to a specified depth
+ * @cs: Task state whose call stack is unwound
+ * @target_depth: Depth to unwind down to
+ * @blank_prefix: Column alignment prefix for synthesised closing lines
+ * @out: File stream to output to, or NULL if suppressing output
+ *
+ * Pop and free call stack frames until the stack depth reaches @target_depth.
+ * For any inlined frames encountered, decrement the inlined depth counter
+ * and, if @out is non-NULL, emit the corresponding synthesised inline closing
+ * delimiter line.
+ */
+static void ftrace_unwind_to_depth(struct ftrace_task_state *cs, int target_depth,
+ const char *blank_prefix, FILE *out)
+{
+ while (cs->depth > target_depth) {
+ struct ftrace_stack_frame *top = &cs->stack[cs->depth - 1];
+
+ if (top->inlined) {
+ if (cs->inlined_depth > 0)
+ cs->inlined_depth--;
+ if (out)
+ fprintf(out, "%s%*s} /* %s (inline) */\n",
+ blank_prefix, top->indent, "", top->name);
+ }
+ free(top->name);
+ cs->depth--;
+ }
+}
+
+/**
+ * ftrace_check_target_caller - Validate caller of function inside target filter
+ * @ftrace: Perf ftrace context
+ * @cs: CPU stack state
+ * @func_start: Pointer to start of function name in trace record
+ * @entry_name: Function entry symbol name
+ * @is_graph_target: Whether function matches target filter
+ * @leading_spaces: Indentation of trace record
+ * @has_open_brace: Whether record opens a frame
+ * @has_semicolon: Whether record is a leaf call
+ * @has_retaddr: Whether return address comment was parsed
+ * @caller_idx: Index of caller on tracked stack, or -1 if untraced
+ * @blank_prefix: Space prefix for synthesised closing lines
+ * @out: File stream to output to
+ * @is_target_root: Pointer to target root flag to update
+ *
+ * Return: 1 if line should be dropped (orphan or non-target ancestor),
+ * 0 if entry should proceed as valid call or new target root,
+ * negative error code on memory failure.
+ */
+static int ftrace_check_target_caller(struct perf_ftrace *ftrace,
+ struct ftrace_task_state *cs,
+ const char *func_start,
+ const char *entry_name,
+ bool is_graph_target,
+ int leading_spaces,
+ bool has_open_brace,
+ bool has_semicolon,
+ bool has_retaddr,
+ int caller_idx,
+ const char *blank_prefix,
+ FILE *out,
+ bool *is_target_root)
+{
+ if (!ftrace_has_graph_funcs(ftrace) || !cs->in_target ||
+ cs->depth <= cs->target_root_depth)
+ return 0;
+
+ if (has_retaddr && caller_idx >= 0 && caller_idx < cs->target_root_depth) {
+ ftrace_unwind_to_depth(cs, caller_idx + 1, blank_prefix, out);
+ if (is_graph_target) {
+ ftrace_reset_to_target_root(cs, entry_name, cs->depth,
+ leading_spaces);
+ *is_target_root = true;
+ return 0;
+ }
+ reset_target_root_state(cs);
+ if (has_open_brace && !has_semicolon &&
+ push_nontarget_frame(cs, func_start, leading_spaces) < 0)
+ return -ENOMEM;
+ return 1;
+ }
+
+ if (leading_spaces <= cs->target_root_indent) {
+ ftrace_unwind_to_depth(cs, 0, blank_prefix, out);
+ if (is_graph_target) {
+ ftrace_reset_to_target_root(cs, entry_name, 0,
+ leading_spaces);
+ *is_target_root = true;
+ return 0;
+ }
+ reset_target_root_state(cs);
+ if (has_open_brace && !has_semicolon &&
+ push_nontarget_frame(cs, func_start, leading_spaces) < 0)
+ return -ENOMEM;
+ return 1;
+ }
+
+ if (has_retaddr) {
+ if (caller_idx < 0) {
+ if (has_open_brace && !has_semicolon)
+ cs->orphan_depth = 1;
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
/**
* ftrace_process_fgraph_line - Process and output a function graph trace line
* @ftrace: Perf ftrace context
@@ -832,16 +1152,33 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
int active_inlines;
int common;
int match_idx;
- int top_raw_indent;
+ int ret;
int i, j;
+ char entry_name[128];
bool is_exit;
bool has_open_brace;
bool has_semicolon;
bool has_retaddr = false;
+ bool has_filter = ftrace_has_graph_funcs(ftrace);
+ bool is_graph_target = false;
+ bool is_target_root = false;
+ bool was_target_root = false;
- if (!ftrace->use_inline)
+ if ((!has_filter || ftrace->graph_thresh) && !ftrace->use_inline)
return print_line(ftrace, line, out);
+ if (strstr(line, "==========>") != NULL ||
+ strstr(line, "<==========") != NULL) {
+ if (has_filter && parse_graph_cpu(ftrace, line, &cpu)) {
+ cs = get_cpu_state(ftrace, cpu);
+ if (cs && cs->in_target && cs->orphan_depth == 0)
+ print_line(ftrace, line, out);
+ } else if (!has_filter) {
+ print_line(ftrace, line, out);
+ }
+ return 0;
+ }
+
pipe_char = find_func_column(ftrace, line, &cpu);
if (!pipe_char) {
parse_context_switch(line);
@@ -873,30 +1210,17 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
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 (cs->orphan_depth > 0) {
+ if (has_filter && cs->in_target &&
+ leading_spaces <= cs->target_root_indent) {
+ cs->orphan_depth = 0;
+ } else {
+ cs->orphan_depth--;
+ return 0;
}
}
- /*
- * 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 */
+ /* Find matching frame on stack if exit is named */
match_idx = -1;
if (exit_name[0] != '\0') {
for (i = cs->depth - 1; i >= 0; i--) {
@@ -924,17 +1248,9 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
}
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;
- }
+ was_target_root = (has_filter && cs->in_target &&
+ match_idx == cs->target_root_frame_idx);
- 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];
@@ -944,8 +1260,10 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
if (cs->inlined_depth > 0)
cs->inlined_depth--;
- fprintf(out, "%s%*s} /* %s (inline) */\n",
- blank_prefix, exit_indent, "", top->name);
+ if (!has_filter || (cs->in_target &&
+ cs->depth - 1 >= cs->target_root_depth))
+ fprintf(out, "%s%*s} /* %s (inline) */\n",
+ blank_prefix, exit_indent, "", top->name);
}
free(top->name);
cs->depth--;
@@ -963,9 +1281,40 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
extra_indent = 0;
}
- fprintf(out, "%.*s%*s%s\n",
- (int)(pipe_char - line + 1), line,
- leading_spaces + extra_indent, "", func_start);
+ if (!has_filter || (cs->in_target && match_idx >= cs->target_root_depth)) {
+ if (!ftrace->use_inline) {
+ print_line(ftrace, line, out);
+ } else {
+ fprintf(out, "%.*s%*s%s\n",
+ (int)(pipe_char - line + 1), line,
+ leading_spaces + extra_indent, "", func_start);
+ }
+ }
+
+ if (has_filter && cs->in_target) {
+ if (was_target_root) {
+ while (cs->depth > cs->target_root_depth &&
+ cs->stack[cs->depth - 1].inlined) {
+ struct ftrace_stack_frame *top =
+ &cs->stack[cs->depth - 1];
+ 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--;
+ }
+ reset_target_root_state(cs);
+ } else if (cs->depth <= cs->target_root_depth) {
+ reset_target_root_state(cs);
+ } else if (leading_spaces <= cs->target_root_indent) {
+ ftrace_unwind_to_depth(cs, cs->target_root_depth,
+ blank_prefix, out);
+ reset_target_root_state(cs);
+ }
+ }
return 0;
}
@@ -973,30 +1322,13 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
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;
+ extract_entry_func_name(func_start, entry_name, sizeof(entry_name));
+ if (has_filter)
+ is_graph_target = ftrace_is_graph_func(ftrace, entry_name);
+ caller_idx = -1;
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 &&
@@ -1007,11 +1339,135 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
}
}
- /* If caller is not on the tracked stack, do not synthesise inlines */
- if (has_retaddr && caller_idx < 0) {
+ if (cs->orphan_depth > 0) {
+ /*
+ * If an orphan exit was dropped by the kernel ring buffer,
+ * check whether this entry is a new target root function
+ * or descends directly from the target root function.
+ */
+ if (has_filter && cs->in_target) {
+ if (leading_spaces <= cs->target_root_indent) {
+ cs->orphan_depth = 0;
+ } else if (cs->target_root_name[0] != '\0' && has_retaddr &&
+ leading_spaces == cs->target_root_indent + 2) {
+ if (strcmp(cs->target_root_name, caller_sym) == 0)
+ cs->orphan_depth = 0;
+ }
+ }
+
+ if (cs->orphan_depth > 0) {
+ if (has_open_brace && !has_semicolon)
+ cs->orphan_depth++;
+ return 0;
+ }
+ }
+
+ if (has_filter) {
+ if (cs->depth == 0 || !cs->in_target) {
+ if (is_graph_target) {
+ if (has_retaddr && caller_idx >= 0)
+ ftrace_unwind_to_depth(cs, caller_idx + 1,
+ blank_prefix,
+ ftrace->use_inline ? out : NULL);
+ else
+ ftrace_unwind_to_depth(cs, 0,
+ blank_prefix,
+ ftrace->use_inline ? out : NULL);
+ ftrace_reset_to_target_root(cs, entry_name, cs->depth,
+ leading_spaces);
+ is_target_root = true;
+ } else if (cs->depth == 0) {
+ reset_target_root_state(cs);
+ }
+ }
+
+ if (!cs->in_target) {
+ /* Non-matching call tree; track depth if entering scope, suppress output */
+ while (cs->depth > 0 &&
+ cs->stack[cs->depth - 1].raw_indent >= leading_spaces) {
+ free(cs->stack[cs->depth - 1].name);
+ cs->depth--;
+ }
+ if (has_open_brace && !has_semicolon) {
+ if (push_nontarget_frame(cs, func_start, leading_spaces) < 0)
+ return -ENOMEM;
+ }
+ return 0;
+ }
+ }
+
+ if (!ftrace->use_inline) {
+ if (is_target_root) {
+ if (has_retaddr && caller_idx >= 0)
+ ftrace_unwind_to_depth(cs, caller_idx + 1, NULL, NULL);
+ else
+ ftrace_unwind_to_depth(cs, 0, NULL, NULL);
+ cs->target_root_depth = cs->depth;
+ cs->target_root_frame_idx = cs->depth;
+ cs->target_root_indent = leading_spaces;
+ }
+
+ ret = ftrace_check_target_caller(ftrace, cs, func_start, entry_name,
+ is_graph_target, leading_spaces,
+ has_open_brace, has_semicolon,
+ has_retaddr, caller_idx, NULL, NULL,
+ &is_target_root);
+ if (ret != 0)
+ return (ret < 0) ? ret : 0;
+
+ if (!is_target_root) {
+ if (has_retaddr && caller_idx >= 0) {
+ if (cs->depth > caller_idx + 1)
+ ftrace_unwind_to_depth(cs, caller_idx + 1, NULL, NULL);
+ } else if (caller_idx < 0) {
+ while (cs->depth > cs->target_root_depth &&
+ cs->stack[cs->depth - 1].raw_indent >= leading_spaces) {
+ free(cs->stack[cs->depth - 1].name);
+ cs->depth--;
+ }
+ }
+ }
+
+ if (has_open_brace && !has_semicolon) {
+ if (is_target_root) {
+ cs->target_root_frame_idx = cs->depth;
+ cs->target_root_indent = leading_spaces;
+ }
+ if (push_nontarget_frame(cs, func_start, leading_spaces) < 0)
+ return -ENOMEM;
+ }
+ print_line(ftrace, line, out);
+ if (has_filter && cs->in_target && is_target_root &&
+ has_semicolon && !has_open_brace) {
+ reset_target_root_state(cs);
+ }
+ return 0;
+ }
+
+ if (has_retaddr) {
+ num_inlines = ftrace_resolve_inlines(ftrace->machine, caller_sym,
+ caller_offset, inlined_names,
+ FTRACE_MAX_INLINES);
+ if (num_inlines < 0)
+ return num_inlines;
+
+ /* If caller is not on the tracked stack, do not synthesise inlines */
+ if (caller_idx < 0) {
+ for (i = 0; i < num_inlines; i++)
+ free((void *)inlined_names[i]);
+ num_inlines = 0;
+ }
+ }
+
+ ret = ftrace_check_target_caller(ftrace, cs, func_start, entry_name,
+ is_graph_target, leading_spaces,
+ has_open_brace, has_semicolon,
+ has_retaddr, caller_idx, blank_prefix,
+ out, &is_target_root);
+ if (ret != 0) {
for (i = 0; i < num_inlines; i++)
free((void *)inlined_names[i]);
- num_inlines = 0;
+ return (ret < 0) ? ret : 0;
}
active_inlines = 0;
@@ -1036,9 +1492,6 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
}
/* 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];
@@ -1047,41 +1500,52 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
if (cs->inlined_depth > 0)
cs->inlined_depth--;
- fprintf(out, "%s%*s} /* %s (inline) */\n",
- blank_prefix, exit_indent, "", top->name);
+ if (!has_filter || (cs->in_target && !is_target_root))
+ 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 (caller_idx < 0) {
+ if (is_target_root) {
+ ftrace_unwind_to_depth(cs, 0, blank_prefix, out);
+ } else {
+ /*
+ * 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 (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;
+ if (cs->inlined_depth > 0)
+ cs->inlined_depth--;
+ if (!has_filter || (cs->in_target && !is_target_root))
+ 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;
+ }
}
}
}
+ if (is_target_root) {
+ cs->target_root_depth = cs->depth;
+ cs->target_root_indent = leading_spaces;
+ }
+
/* Open new inlined frames */
for (i = common; i < num_inlines; i++) {
int entry_indent = leading_spaces + cs->inlined_depth * 2;
@@ -1139,9 +1603,6 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
/* 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 >= cs->max_stack &&
ftrace_task_state__grow_stack(cs) < 0) {
for (i = 0; i < num_inlines; i++)
@@ -1149,6 +1610,11 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
return -ENOMEM;
}
+ if (is_target_root) {
+ cs->target_root_frame_idx = cs->depth;
+ cs->target_root_indent = leading_spaces;
+ }
+
cs->stack[cs->depth].name = strdup(entry_name);
if (!cs->stack[cs->depth].name) {
pr_err("Not enough memory\n");
@@ -1163,6 +1629,25 @@ int ftrace_process_fgraph_line(struct perf_ftrace *ftrace, const char *line, FIL
cs->min_raw_indent = -1;
}
+ if (has_filter && cs->in_target && is_target_root &&
+ has_semicolon && !has_open_brace) {
+ while (cs->depth > cs->target_root_depth) {
+ 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--;
+ }
+ reset_target_root_state(cs);
+ }
+
for (i = 0; i < num_inlines; i++)
free((void *)inlined_names[i]);
return 0;
diff --git a/tools/perf/util/ftrace.h b/tools/perf/util/ftrace.h
index 0afaa9695691..1dc808b8a7df 100644
--- a/tools/perf/util/ftrace.h
+++ b/tools/perf/util/ftrace.h
@@ -12,10 +12,18 @@ struct evlist;
struct hashamp;
struct stats;
+enum perf_ftrace_subcommand {
+ PERF_FTRACE_NONE,
+ PERF_FTRACE_TRACE,
+ PERF_FTRACE_LATENCY,
+ PERF_FTRACE_PROFILE,
+};
+
struct perf_ftrace {
struct evlist *evlist;
struct target target;
const char *tracer;
+ enum perf_ftrace_subcommand subcmd;
struct list_head filters;
struct list_head notrace;
struct list_head graph_funcs;
--
2.55.0
^ permalink raw reply [flat|nested] 11+ messages in thread
end of thread, other threads:[~2026-10-24 21:26 UTC | newest]
Thread overview: 11+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-24 21:25 [PATCH perf-tools-next v10 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace() Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 02/10] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 03/10] perf ftrace: Support filtering return address comments Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 04/10] perf ftrace: Add basic task call stack management Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 05/10] perf ftrace: Support context switch tracking in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 06/10] perf ftrace: Support display of inlined functions " Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 07/10] perf symbol: Introduce vmlinux_path__find() helper Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 08/10] perf test: Add test for ftrace inline and task stack tracking Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 09/10] perf ftrace: Support omitting execution duration in function graph tracer Aaron Tomlin
2026-10-24 21:25 ` [PATCH perf-tools-next v10 10/10] perf ftrace: Filter extraneous functions " Aaron Tomlin
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®