From: Aaron Tomlin <atomlin@atomlin.com>
To: peterz@infradead.org, mingo@redhat.com, acme@kernel.org,
namhyung@kernel.org
Cc: mark.rutland@arm.com, alexander.shishkin@linux.intel.com,
jolsa@kernel.org, irogers@google.com, adrian.hunter@intel.com,
atomlin@atomlin.com, chjohnst@mail.com, neelx@suse.com,
sean@ashe.io, linux-perf-users@vger.kernel.org,
linux-kernel@vger.kernel.org
Subject: [PATCH perf-tools-next v10 04/10] perf ftrace: Add basic task call stack management
Date: Sat, 24 Oct 2026 17:25:43 -0400 [thread overview]
Message-ID: <20261024212549.450634-5-atomlin@atomlin.com> (raw)
In-Reply-To: <20261024212549.450634-1-atomlin@atomlin.com>
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
next prev parent reply other threads:[~2026-10-24 21:26 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
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 ` Aaron Tomlin [this message]
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
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261024212549.450634-5-atomlin@atomlin.com \
--to=atomlin@atomlin.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=alexander.shishkin@linux.intel.com \
--cc=chjohnst@mail.com \
--cc=irogers@google.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mark.rutland@arm.com \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=neelx@suse.com \
--cc=peterz@infradead.org \
--cc=sean@ashe.io \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®