[PATCH 3/4] perf c2c: Add stdio support for the function view

From: Jiebin Sun

Date: Fri Aug 21 2026 - 01:50:55 EST


The function view is currently TUI-only, so it cannot be used by builds
without SLANG support, when output is piped, or from a script. Add a
--function option that prints the fully expanded three-level hierarchy to
stdout.

Keep the stdio renderer in builtin-c2c.c and reuse the common function-view
model introduced by the merged series. Export only the coalescing-field
capability check from the model, preserving the util/UI boundary and
leaving the TUI object in libperf-ui.a.

Stop padding the final identity column in symbol_view_entry(). The generic
formatter pads non-final columns but deliberately leaves the final column
unpadded, avoiding trailing whitespace in function-view table rows. The TUI
remains unchanged because its browser clears the rest of each rendered row.

--function implies --stdio and is rejected together with --stats. Validate
the iaddr requirement before processing events. Return function-view build
failures from the report command, and preserve TUI browser errors when
converting the display helpers to return a status.

Signed-off-by: Jiebin Sun <jiebin.sun@xxxxxxxxx>
Cc: Dapeng Mi <dapeng1.mi@xxxxxxxxxxxxxxx>
Cc: Ian Rogers <irogers@xxxxxxxxxx>
Cc: James Clark <james.clark@xxxxxxxxxx>
Cc: Namhyung Kim <namhyung@xxxxxxxxxx>
Cc: Thomas Falcon <thomas.falcon@xxxxxxxxx>
Reviewed-by: Tianyou Li <tianyou.li@xxxxxxxxx>
Reviewed-by: Wangyang Guo <wangyang.guo@xxxxxxxxx>
---
tools/perf/Documentation/perf-c2c.txt | 12 +++-
tools/perf/builtin-c2c.c | 85 ++++++++++++++++++++++++---
tools/perf/util/c2c-function.c | 9 +--
tools/perf/util/c2c.h | 1 +
4 files changed, 90 insertions(+), 17 deletions(-)

diff --git a/tools/perf/Documentation/perf-c2c.txt b/tools/perf/Documentation/perf-c2c.txt
index 9e58a51c55de..7a0cf31be7ed 100644
--- a/tools/perf/Documentation/perf-c2c.txt
+++ b/tools/perf/Documentation/perf-c2c.txt
@@ -107,6 +107,10 @@ REPORT OPTIONS
--stats::
Display only statistic tables and force stdio mode.

+--function::
+ Display the function view and force stdio mode. This requires `iaddr`
+ in the cacheline coalescing fields and cannot be used with `--stats`.
+
--full-symbols::
Display full length of symbols.

@@ -360,6 +364,10 @@ Following tables are displayed:
Shared Cache Line Distribution Pareto
- list of all accessed offsets for each cacheline

+With `--function`, the cacheline and Pareto tables are replaced by a fully
+expanded Shared Data Functions Table. Its three levels are the read-side
+function, contending writer, and shared cacheline, as detailed below.
+
TUI OUTPUT
----------
The TUI output provides interactive interface to navigate
@@ -374,8 +382,8 @@ Verbose mode also includes code addresses in function rows, and code addresses
remain available in the per-cacheline detail view ('d').

The function view requires `iaddr` in the cacheline coalescing fields. If
-`--coalesce` omits it, TAB reports that the view is unavailable rather than
-attributing already-coalesced samples to an arbitrary function.
+`--coalesce` omits it, TAB or `--function` reports that the view is unavailable
+rather than attributing already-coalesced samples to an arbitrary function.

Level 1: the read-side function, sorted by Cycles % (estimated load
cycles: HITM, peer-snoop and other-load cycles)
diff --git a/tools/perf/builtin-c2c.c b/tools/perf/builtin-c2c.c
index 715b75d42f2a..cce76a1e2ff5 100644
--- a/tools/perf/builtin-c2c.c
+++ b/tools/perf/builtin-c2c.c
@@ -73,6 +73,7 @@ struct perf_c2c {
bool show_all;
bool use_stdio;
bool stats_only;
+ bool function_view;
bool symbol_full;
bool stitch_lbr;

@@ -2530,7 +2531,48 @@ static void print_c2c_info(FILE *out, struct perf_session *session)
fprintf(out, " Cacheline data grouping : %s\n", c2c.cl_sort);
}

-static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session)
+static void c2c_function__unfold_all(struct rb_root_cached *root)
+{
+ struct rb_node *nd;
+
+ for (nd = rb_first_cached(root); nd; nd = rb_next(nd)) {
+ struct hist_entry *he = rb_entry(nd, struct hist_entry, rb_node);
+
+ if (!he->has_children)
+ continue;
+ he->unfolded = true;
+ c2c_function__unfold_all(&he->hroot_out);
+ }
+}
+
+static int perf_c2c__function_fprintf(FILE *out)
+{
+ bool saved_use_callchain = symbol_conf.use_callchain;
+ struct hists *hists;
+ int ret;
+
+ /* Function-view entries aggregate samples and never display callchains. */
+ symbol_conf.use_callchain = false;
+ ret = c2c_function__build(&c2c.hists, c2c.cl_sort, c2c.symbol_full,
+ &hists);
+ if (ret) {
+ if (ret == -EOPNOTSUPP)
+ pr_err("The function view requires iaddr in --coalesce.\n");
+ else
+ pr_err("Failed to build function view hierarchy (ret=%d)\n", ret);
+ goto out;
+ }
+
+ /* Match fold signs to hists__fprintf()'s forced child traversal. */
+ c2c_function__unfold_all(&hists->entries);
+ hists__fprintf(hists, true, 0, 0, 0, out, true);
+ c2c_function__reset();
+out:
+ symbol_conf.use_callchain = saved_use_callchain;
+ return ret;
+}
+
+static int perf_c2c__hists_fprintf(FILE *out, struct perf_session *session)
{
setup_pager();

@@ -2541,7 +2583,17 @@ static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session)
print_c2c_info(out, session);

if (c2c.stats_only)
- return;
+ return 0;
+
+ if (c2c.function_view) {
+ fprintf(out, "\n");
+ fprintf(out, "=================================================\n");
+ fprintf(out, " Shared Data Functions Table\n");
+ fprintf(out, "=================================================\n");
+ fprintf(out, "#\n");
+
+ return perf_c2c__function_fprintf(out);
+ }

fprintf(out, "\n");
fprintf(out, "=================================================\n");
@@ -2558,6 +2610,7 @@ static void perf_c2c__hists_fprintf(FILE *out, struct perf_session *session)
fprintf(out, "#\n");

print_pareto(out, perf_session__env(session));
+ return 0;
}

#ifdef HAVE_SLANG_SUPPORT
@@ -2794,18 +2847,18 @@ static int perf_c2c__hists_browse(struct hists *hists)
return 0;
}

-static void perf_c2c_display(struct perf_session *session)
+static int perf_c2c_display(struct perf_session *session)
{
if (use_browser == 0)
- perf_c2c__hists_fprintf(stdout, session);
- else
- perf_c2c__hists_browse(&c2c.hists.hists);
+ return perf_c2c__hists_fprintf(stdout, session);
+
+ return perf_c2c__hists_browse(&c2c.hists.hists);
}
#else
-static void perf_c2c_display(struct perf_session *session)
+static int perf_c2c_display(struct perf_session *session)
{
use_browser = 0;
- perf_c2c__hists_fprintf(stdout, session);
+ return perf_c2c__hists_fprintf(stdout, session);
}
#endif /* HAVE_SLANG_SUPPORT */

@@ -3081,6 +3134,8 @@ static int perf_c2c__report(int argc, const char **argv)
OPT_BOOLEAN(0, "stdio", &c2c.use_stdio, "Use the stdio interface"),
OPT_BOOLEAN(0, "stats", &c2c.stats_only,
"Display only statistic tables (implies --stdio)"),
+ OPT_BOOLEAN(0, "function", &c2c.function_view,
+ "Display the function view (implies --stdio)"),
OPT_BOOLEAN(0, "full-symbols", &c2c.symbol_full,
"Display full length of symbols"),
OPT_BOOLEAN(0, "no-source", &no_source,
@@ -3119,6 +3174,11 @@ static int perf_c2c__report(int argc, const char **argv)
PARSE_OPT_STOP_AT_NON_OPTION);
if (argc)
usage_with_options(report_c2c_usage, options);
+ if (c2c.stats_only && c2c.function_view) {
+ pr_err("--stats and --function cannot be used together.\n");
+ err = -EINVAL;
+ goto out;
+ }

#ifndef HAVE_SLANG_SUPPORT
c2c.use_stdio = true;
@@ -3126,6 +3186,8 @@ static int perf_c2c__report(int argc, const char **argv)

if (c2c.stats_only)
c2c.use_stdio = true;
+ if (c2c.function_view)
+ c2c.use_stdio = true;

/**
* Annotation related options disassembler_style, objdump_path are set
@@ -3199,6 +3261,11 @@ static int perf_c2c__report(int argc, const char **argv)
pr_debug("Failed to initialize hists\n");
goto out_session;
}
+ if (c2c.function_view && !c2c_function__has_iaddr(c2c.cl_sort)) {
+ pr_err("The function view requires iaddr in --coalesce.\n");
+ err = -EINVAL;
+ goto out_session;
+ }

err = c2c_hists__init(&c2c.hists, "dcacheline", 2, perf_session__env(session));
if (err) {
@@ -3332,7 +3399,7 @@ static int perf_c2c__report(int argc, const char **argv)
goto out_mem2node;
}

- perf_c2c_display(session);
+ err = perf_c2c_display(session);

out_mem2node:
mem2node__exit(&c2c.mem2node);
diff --git a/tools/perf/util/c2c-function.c b/tools/perf/util/c2c-function.c
index 5b6a06a5a067..d410196d0429 100644
--- a/tools/perf/util/c2c-function.c
+++ b/tools/perf/util/c2c-function.c
@@ -305,8 +305,8 @@ symbol_view_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
char symbuf[32];

scnprintf(symbuf, sizeof(symbuf), "0x%" PRIx64, addr);
- ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%-*.*s",
- text_width, text_width, symbuf);
+ ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%.*s",
+ text_width, symbuf);
} else {
/* Level 1 and level 2 are both functions. */
size_t cell_size;
@@ -331,9 +331,6 @@ symbol_view_entry(struct perf_hpp_fmt *fmt, struct perf_hpp *hpp,
len = min_t(size_t, len, cell_size - 1);

ret += len;
- if (len < text_width)
- ret += scnprintf(hpp->buf + ret, hpp->size - ret, "%*s",
- text_width - len, "");
}

return ret;
@@ -1519,7 +1516,7 @@ void c2c_function__reset(void)
symbol_conf.use_callchain = saved_use_callchain;
}

-static bool c2c_function__has_iaddr(const char *cl_sort)
+bool c2c_function__has_iaddr(const char *cl_sort)
{
const char *field = cl_sort;

diff --git a/tools/perf/util/c2c.h b/tools/perf/util/c2c.h
index 53f024e25d99..198032ec7c87 100644
--- a/tools/perf/util/c2c.h
+++ b/tools/perf/util/c2c.h
@@ -107,6 +107,7 @@ bool c2c_fmt_equal(struct perf_hpp_fmt *a, struct perf_hpp_fmt *b);
int c2c_function__build(struct c2c_hists *cl_hists, const char *cl_sort,
bool symbol_full, struct hists **hists);
void c2c_function__reset(void);
+bool c2c_function__has_iaddr(const char *cl_sort);
/* Valid only between a successful build and c2c_function__reset(). */
struct hist_entry *c2c_function__find_cacheline(struct hist_entry *he);

--
2.52.0