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 v12 04/10] perf ftrace: Add basic task call stack management
Date: Sun, 25 Oct 2026 22:57:29 -0400 [thread overview]
Message-ID: <20261026025735.546101-5-atomlin@atomlin.com> (raw)
In-Reply-To: <20261026025735.546101-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().
Tested-by: Ian Rogers <irogers@google.com>
Reviewed-by: Ian Rogers <irogers@google.com>
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-26 2:58 UTC|newest]
Thread overview: 21+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-26 2:57 [PATCH perf-tools-next v12 00/10] perf ftrace: Support inlined functions and display enhancements in function graph tracer Aaron Tomlin
2026-10-26 2:57 ` [PATCH perf-tools-next v12 01/10] perf ftrace: Handle signals, non-blocking reads and pager exits in __cmd_ftrace() Aaron Tomlin
2026-10-26 9:23 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 02/10] perf ftrace: Optimise __cmd_ftrace() stack frame with strbuf streaming Aaron Tomlin
2026-10-26 9:27 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 03/10] perf ftrace: Support filtering return address comments Aaron Tomlin
2026-10-26 9:33 ` sashiko-bot
2026-10-26 2:57 ` Aaron Tomlin [this message]
2026-10-26 9:37 ` [PATCH perf-tools-next v12 04/10] perf ftrace: Add basic task call stack management sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 05/10] perf ftrace: Support context switch tracking in function graph tracer Aaron Tomlin
2026-10-26 9:45 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 06/10] perf ftrace: Support display of inlined functions " Aaron Tomlin
2026-10-26 9:55 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 07/10] perf symbol: Introduce vmlinux_path__find() helper Aaron Tomlin
2026-10-26 10:04 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 08/10] perf test: Add test for ftrace inline and task stack tracking Aaron Tomlin
2026-10-26 10:09 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 09/10] perf ftrace: Support omitting execution duration in function graph tracer Aaron Tomlin
2026-10-26 10:15 ` sashiko-bot
2026-10-26 2:57 ` [PATCH perf-tools-next v12 10/10] perf ftrace: Filter extraneous functions " Aaron Tomlin
2026-10-26 10:30 ` sashiko-bot
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=20261026025735.546101-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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.