tools/perf/builtin-stat.c | 96 +- .../tests/shell/lib/perf_metric_validation.py | 12 +- tools/perf/tests/shell/stat+csv_output.sh | 19 + tools/perf/tests/shell/stat+json_output.sh | 74 +- tools/perf/tests/shell/stat+std_output.sh | 18 + tools/perf/tests/shell/stat_metrics_values.sh | 22 +- tools/perf/util/Build | 4 + tools/perf/util/stat-display.c | 6 +- tools/perf/util/stat-print-csv.c | 534 +++++++++++ tools/perf/util/stat-print-json.c | 330 +++++++ tools/perf/util/stat-print-std.c | 828 ++++++++++++++++++ tools/perf/util/stat-print.c | 489 +++++++++++ tools/perf/util/stat-print.h | 133 +++ tools/perf/util/stat.h | 2 + 14 files changed, 2488 insertions(+), 79 deletions(-) create mode 100644 tools/perf/util/stat-print-csv.c create mode 100644 tools/perf/util/stat-print-json.c create mode 100644 tools/perf/util/stat-print-std.c create mode 100644 tools/perf/util/stat-print.c create mode 100644 tools/perf/util/stat-print.h
This is V3 of the patch series implementing the decoupled perf stat printing
architecture. This series fully separates the format-specific display engines
(STD, CSV, JSON) into distinct isolated files, and guarantees 100% streaming,
zero-allocation output for interval metric-only JSON data to avoid debug
artifact overwrites and large allocations on extended system-wide runs.
Changes since v2:
- Fixed trailing newline count and padding logic for wide metric names
in standard console (STD) metric-only interval modes, resolving reports
from Sashiko AI.
- Resolved aggr header label misalignments for Socket/Die/Node/Cache modes.
- Addressed debug artifact overwrite bugs in the `stat_metrics_values.sh`
rules validation engine by correctly scoping execution passes and preserving
artifacts upon failure.
- Reverted all reflow diff noise in `builtin-stat.c` and legacy metric print
functions in `util/stat-display.c` per feedback from Arnaldo.
- Converted all format callback file creations to use standard `//` SPDX header
formatting directly in the patch series to prevent downstream diff noise.
- Resolved all internal brace styles, variable declaration spacing, and unwrapped
commit description warnings reported by checkpatch.pl.
- Added all requested Acked-by tags from reviewers.
Ian Rogers (14):
perf stat: Introduce core generic print traversal engine and header
stubs
perf stat: Implement standard console (STD) formatting callbacks
perf stat: Extend STD output linter to test basic New API checks
perf stat: Extend STD output linter to test core aggregation checks
perf stat: Extend STD output linter to test advanced PMU checks
perf stat: Extend STD output linter to test metric-only checks
perf stat: Implement CSV formatting callbacks
perf stat: Extend CSV output linter to test core aggregation checks
perf stat: Extend CSV output linter to test advanced PMU and
metric-only checks
perf stat: Implement streaming JSON formatting callbacks
perf stat: Extend JSON output linter to test core aggregation checks
perf stat: Extend JSON output linter to test advanced PMU and
metric-only checks
perf stat: Add --new support to PMU metrics Python validator
perf stat: Extend PMU metrics value linter to validate --new outputs
tools/perf/builtin-stat.c | 96 +-
.../tests/shell/lib/perf_metric_validation.py | 12 +-
tools/perf/tests/shell/stat+csv_output.sh | 19 +
tools/perf/tests/shell/stat+json_output.sh | 74 +-
tools/perf/tests/shell/stat+std_output.sh | 18 +
tools/perf/tests/shell/stat_metrics_values.sh | 22 +-
tools/perf/util/Build | 4 +
tools/perf/util/stat-display.c | 6 +-
tools/perf/util/stat-print-csv.c | 534 +++++++++++
tools/perf/util/stat-print-json.c | 330 +++++++
tools/perf/util/stat-print-std.c | 828 ++++++++++++++++++
tools/perf/util/stat-print.c | 489 +++++++++++
tools/perf/util/stat-print.h | 133 +++
tools/perf/util/stat.h | 2 +
14 files changed, 2488 insertions(+), 79 deletions(-)
create mode 100644 tools/perf/util/stat-print-csv.c
create mode 100644 tools/perf/util/stat-print-json.c
create mode 100644 tools/perf/util/stat-print-std.c
create mode 100644 tools/perf/util/stat-print.c
create mode 100644 tools/perf/util/stat-print.h
--
2.55.0.141.g00534a21ce-goog
This patch set decouples and modularizes `perf stat` metric and event
counter display processing into a unified traversal engine supporting
pluggable, format-specific callbacks for standard (STD), CSV, and JSON
outputs via the new `--new` flag.
Changes in v4:
- Squash all multi-metric column padding, zero-skip logic, and formatting
fixes directly into their introducing commits for a perfectly bisectable,
pristine Git history.
- Avoid newlines before signed-off-by, assisted-by, and acked-by tags,
organizing all contributor tags into a single, contiguous block for strict
checkpatch and LKML compliance.
- Introduce strict, grid-based column padding to perfectly align multi-metric
events (e.g. TopdownL1) across both standard and metric-only CSV rows, guaranteeing
perfect 6-comma and 7-comma parsing compliance by external DataFrame engines.
- Refactor the PMU and hybrid metric event printer to propagate identical zero-counter
skipping semantics in alignment with the legacy abs_printout path.
- Re-run all STD, JSON, and CSV linter test suites to achieve 100% passing status.
Ian Rogers (14):
perf stat: Introduce core generic print traversal engine and header
stubs
perf stat: Implement standard console (STD) formatting callbacks
perf stat: Extend STD output linter to test basic New API checks
perf stat: Extend STD output linter to test core aggregation checks
perf stat: Extend STD output linter to test advanced PMU checks
perf stat: Extend STD output linter to test metric-only checks
perf stat: Implement CSV formatting callbacks
perf stat: Extend CSV output linter to test core aggregation checks
perf stat: Extend CSV output linter to test advanced PMU and
metric-only checks
perf stat: Implement streaming JSON formatting callbacks
perf stat: Extend JSON output linter to test core aggregation checks
perf stat: Extend JSON output linter to test advanced PMU and
metric-only checks
perf stat: Add --new support to PMU metrics Python validator
perf stat: Extend PMU metrics value linter to validate --new outputs
tools/perf/builtin-stat.c | 96 +-
.../tests/shell/lib/perf_metric_validation.py | 12 +-
tools/perf/tests/shell/stat+csv_output.sh | 19 +
tools/perf/tests/shell/stat+json_output.sh | 74 +-
tools/perf/tests/shell/stat+std_output.sh | 18 +
tools/perf/tests/shell/stat_metrics_values.sh | 22 +-
tools/perf/util/Build | 4 +
tools/perf/util/stat-display.c | 12 +-
tools/perf/util/stat-print-csv.c | 677 ++++++++++++++
tools/perf/util/stat-print-json.c | 339 +++++++
tools/perf/util/stat-print-std.c | 854 ++++++++++++++++++
tools/perf/util/stat-print.c | 500 ++++++++++
tools/perf/util/stat-print.h | 137 +++
tools/perf/util/stat.h | 6 +
14 files changed, 2688 insertions(+), 82 deletions(-)
create mode 100644 tools/perf/util/stat-print-csv.c
create mode 100644 tools/perf/util/stat-print-json.c
create mode 100644 tools/perf/util/stat-print-std.c
create mode 100644 tools/perf/util/stat-print.c
create mode 100644 tools/perf/util/stat-print.h
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:49AM -0700, Ian Rogers wrote: > This patch set decouples and modularizes `perf stat` metric and event > counter display processing into a unified traversal engine supporting > pluggable, format-specific callbacks for standard (STD), CSV, and JSON > outputs via the new `--new` flag. Thanks for working on this! I'm not sure the goal is to produce the exactly same output or not. If so, we need to verify that by comparing outputs before and after. Probably you can use perf stat record and report. If not, please show the output here. Actually I cannot say I like the name of the flag, but it's ok if we want to keep it for a short period time and eventually get rid of it. Thanks, Namhyung > > Changes in v4: > - Squash all multi-metric column padding, zero-skip logic, and formatting > fixes directly into their introducing commits for a perfectly bisectable, > pristine Git history. > - Avoid newlines before signed-off-by, assisted-by, and acked-by tags, > organizing all contributor tags into a single, contiguous block for strict > checkpatch and LKML compliance. > - Introduce strict, grid-based column padding to perfectly align multi-metric > events (e.g. TopdownL1) across both standard and metric-only CSV rows, guaranteeing > perfect 6-comma and 7-comma parsing compliance by external DataFrame engines. > - Refactor the PMU and hybrid metric event printer to propagate identical zero-counter > skipping semantics in alignment with the legacy abs_printout path. > - Re-run all STD, JSON, and CSV linter test suites to achieve 100% passing status. > > Ian Rogers (14): > perf stat: Introduce core generic print traversal engine and header > stubs > perf stat: Implement standard console (STD) formatting callbacks > perf stat: Extend STD output linter to test basic New API checks > perf stat: Extend STD output linter to test core aggregation checks > perf stat: Extend STD output linter to test advanced PMU checks > perf stat: Extend STD output linter to test metric-only checks > perf stat: Implement CSV formatting callbacks > perf stat: Extend CSV output linter to test core aggregation checks > perf stat: Extend CSV output linter to test advanced PMU and > metric-only checks > perf stat: Implement streaming JSON formatting callbacks > perf stat: Extend JSON output linter to test core aggregation checks > perf stat: Extend JSON output linter to test advanced PMU and > metric-only checks > perf stat: Add --new support to PMU metrics Python validator > perf stat: Extend PMU metrics value linter to validate --new outputs > > tools/perf/builtin-stat.c | 96 +- > .../tests/shell/lib/perf_metric_validation.py | 12 +- > tools/perf/tests/shell/stat+csv_output.sh | 19 + > tools/perf/tests/shell/stat+json_output.sh | 74 +- > tools/perf/tests/shell/stat+std_output.sh | 18 + > tools/perf/tests/shell/stat_metrics_values.sh | 22 +- > tools/perf/util/Build | 4 + > tools/perf/util/stat-display.c | 12 +- > tools/perf/util/stat-print-csv.c | 677 ++++++++++++++ > tools/perf/util/stat-print-json.c | 339 +++++++ > tools/perf/util/stat-print-std.c | 854 ++++++++++++++++++ > tools/perf/util/stat-print.c | 500 ++++++++++ > tools/perf/util/stat-print.h | 137 +++ > tools/perf/util/stat.h | 6 + > 14 files changed, 2688 insertions(+), 82 deletions(-) > create mode 100644 tools/perf/util/stat-print-csv.c > create mode 100644 tools/perf/util/stat-print-json.c > create mode 100644 tools/perf/util/stat-print-std.c > create mode 100644 tools/perf/util/stat-print.c > create mode 100644 tools/perf/util/stat-print.h > > -- > 2.55.0.141.g00534a21ce-goog >
On Thu, Jul 16, 2026 at 3:49 PM Namhyung Kim <namhyung@kernel.org> wrote: > > On Thu, Jul 16, 2026 at 12:02:49AM -0700, Ian Rogers wrote: > > This patch set decouples and modularizes `perf stat` metric and event > > counter display processing into a unified traversal engine supporting > > pluggable, format-specific callbacks for standard (STD), CSV, and JSON > > outputs via the new `--new` flag. > > Thanks for working on this! > > I'm not sure the goal is to produce the exactly same output or not. > If so, we need to verify that by comparing outputs before and after. > Probably you can use perf stat record and report. > > If not, please show the output here. > > Actually I cannot say I like the name of the flag, but it's ok if we > want to keep it for a short period time and eventually get rid of it. I don't like the flag name either :-) I wanted the output generated with `--new` to pass all the existing perf stat tests, which requires the output to be as close as possible. Only the json output changes things as the order of results matters less with json. The csv and std printing buffer events and metrics to make sure the output matches without relying on a particular printing an event, then printing a metric, etc. order from the caller. My hope in doing things this way was that we could land `--new`, another patch can make it the default, another patch series can delete the old code and another patch can remove the flag. Once we are happy with the --new version of the code we can change what CSV and std output looks like without worrying about the existing stat output code, etc. Thanks, Ian > Thanks, > Namhyung > > > > > Changes in v4: > > - Squash all multi-metric column padding, zero-skip logic, and formatting > > fixes directly into their introducing commits for a perfectly bisectable, > > pristine Git history. > > - Avoid newlines before signed-off-by, assisted-by, and acked-by tags, > > organizing all contributor tags into a single, contiguous block for strict > > checkpatch and LKML compliance. > > - Introduce strict, grid-based column padding to perfectly align multi-metric > > events (e.g. TopdownL1) across both standard and metric-only CSV rows, guaranteeing > > perfect 6-comma and 7-comma parsing compliance by external DataFrame engines. > > - Refactor the PMU and hybrid metric event printer to propagate identical zero-counter > > skipping semantics in alignment with the legacy abs_printout path. > > - Re-run all STD, JSON, and CSV linter test suites to achieve 100% passing status. > > > > Ian Rogers (14): > > perf stat: Introduce core generic print traversal engine and header > > stubs > > perf stat: Implement standard console (STD) formatting callbacks > > perf stat: Extend STD output linter to test basic New API checks > > perf stat: Extend STD output linter to test core aggregation checks > > perf stat: Extend STD output linter to test advanced PMU checks > > perf stat: Extend STD output linter to test metric-only checks > > perf stat: Implement CSV formatting callbacks > > perf stat: Extend CSV output linter to test core aggregation checks > > perf stat: Extend CSV output linter to test advanced PMU and > > metric-only checks > > perf stat: Implement streaming JSON formatting callbacks > > perf stat: Extend JSON output linter to test core aggregation checks > > perf stat: Extend JSON output linter to test advanced PMU and > > metric-only checks > > perf stat: Add --new support to PMU metrics Python validator > > perf stat: Extend PMU metrics value linter to validate --new outputs > > > > tools/perf/builtin-stat.c | 96 +- > > .../tests/shell/lib/perf_metric_validation.py | 12 +- > > tools/perf/tests/shell/stat+csv_output.sh | 19 + > > tools/perf/tests/shell/stat+json_output.sh | 74 +- > > tools/perf/tests/shell/stat+std_output.sh | 18 + > > tools/perf/tests/shell/stat_metrics_values.sh | 22 +- > > tools/perf/util/Build | 4 + > > tools/perf/util/stat-display.c | 12 +- > > tools/perf/util/stat-print-csv.c | 677 ++++++++++++++ > > tools/perf/util/stat-print-json.c | 339 +++++++ > > tools/perf/util/stat-print-std.c | 854 ++++++++++++++++++ > > tools/perf/util/stat-print.c | 500 ++++++++++ > > tools/perf/util/stat-print.h | 137 +++ > > tools/perf/util/stat.h | 6 + > > 14 files changed, 2688 insertions(+), 82 deletions(-) > > create mode 100644 tools/perf/util/stat-print-csv.c > > create mode 100644 tools/perf/util/stat-print-json.c > > create mode 100644 tools/perf/util/stat-print-std.c > > create mode 100644 tools/perf/util/stat-print.c > > create mode 100644 tools/perf/util/stat-print.h > > > > -- > > 2.55.0.141.g00534a21ce-goog > >
This patch introduces the initial infrastructure for decoupling the perf
stat printing API. It declares the struct perf_stat_print_callbacks
interface and the core traversal driver perf_stat__print_cb() inside the
newly created util/stat-print.h and util/stat-print.c files.
The generic traversal driver perf_stat__print_cb() drive traversing the
event lists across all supported cpu aggregation modes (global, die,
socket, cache, cluster, core, thread, none). It implements the clean
display filtering checks (perf_stat__skip_metric_event(), hybrid
wildcard merges) and the basic metrics allowlist filter
(is_basic_shadow_metric()) to keep formatting callbacks decoupled.
This also introduces two format-agnostic shared helpers to centralize
aggregation prefix formatting:
- perf_stat__get_aggr_key(): resolves JSON key names.
- perf_stat__get_aggr_id_char(): formats unified aggregation identifiers.
Adds empty format-specific stubs (perf_stat__print_std, _csv, _json) to
ensure that the new print files link and compile cleanly under
util/Build, without affecting the legacy print path.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/builtin-stat.c | 96 +++---
tools/perf/util/Build | 4 +
tools/perf/util/stat-display.c | 12 +-
tools/perf/util/stat-print-csv.c | 13 +
tools/perf/util/stat-print-json.c | 13 +
tools/perf/util/stat-print-std.c | 13 +
tools/perf/util/stat-print.c | 500 ++++++++++++++++++++++++++++++
tools/perf/util/stat-print.h | 137 ++++++++
tools/perf/util/stat.h | 6 +
9 files changed, 744 insertions(+), 50 deletions(-)
create mode 100644 tools/perf/util/stat-print-csv.c
create mode 100644 tools/perf/util/stat-print-json.c
create mode 100644 tools/perf/util/stat-print-std.c
create mode 100644 tools/perf/util/stat-print.c
create mode 100644 tools/perf/util/stat-print.h
diff --git a/tools/perf/builtin-stat.c b/tools/perf/builtin-stat.c
index 3f685beba384..532856f8be90 100644
--- a/tools/perf/builtin-stat.c
+++ b/tools/perf/builtin-stat.c
@@ -40,62 +40,64 @@
* Jaswinder Singh Rajput <jaswinder@kernel.org>
*/
+#include <errno.h>
+#include <inttypes.h>
+#include <locale.h>
+#include <math.h>
+#include <signal.h>
+#include <stdlib.h>
+
+#include <linux/ctype.h>
+#include <linux/err.h>
+#include <linux/list_sort.h>
+#include <linux/time64.h>
+#include <linux/zalloc.h>
+#include <sys/prctl.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <api/fs/fs.h>
+#include <internal/threadmap.h>
+#include <perf/evlist.h>
+#include <subcmd/parse-options.h>
+
+#include "asm/bug.h"
#include "builtin.h"
+#include "util/affinity.h"
+#include "util/bpf_counter.h"
#include "util/cgroup.h"
-#include <subcmd/parse-options.h>
-#include "util/parse-events.h"
-#include "util/pmus.h"
-#include "util/pmu.h"
-#include "util/tool_pmu.h"
+#include "util/color.h"
+#include "util/counts.h"
+#include "util/cpumap.h"
+#include "util/debug.h"
#include "util/event.h"
#include "util/evlist.h"
#include "util/evsel.h"
-#include "util/debug.h"
-#include "util/color.h"
-#include "util/stat.h"
#include "util/header.h"
-#include "util/cpumap.h"
-#include "util/thread_map.h"
-#include "util/counts.h"
-#include "util/topdown.h"
+#include "util/intel-tpebs.h"
+#include "util/iostat.h"
+#include "util/metricgroup.h"
+#include "util/parse-events.h"
+#include "util/pfm.h"
+#include "util/pmu.h"
+#include "util/pmus.h"
#include "util/session.h"
-#include "util/tool.h"
+#include "util/stat-print.h"
+#include "util/stat.h"
#include "util/string2.h"
-#include "util/metricgroup.h"
#include "util/synthetic-events.h"
#include "util/target.h"
+#include "util/thread_map.h"
#include "util/time-utils.h"
+#include "util/tool.h"
+#include "util/tool_pmu.h"
#include "util/top.h"
-#include "util/affinity.h"
-#include "util/pfm.h"
-#include "util/bpf_counter.h"
-#include "util/iostat.h"
+#include "util/topdown.h"
#include "util/util.h"
-#include "util/intel-tpebs.h"
-#include "asm/bug.h"
-
-#include <linux/list_sort.h>
-#include <linux/time64.h>
-#include <linux/zalloc.h>
-#include <api/fs/fs.h>
-#include <errno.h>
-#include <signal.h>
-#include <stdlib.h>
-#include <sys/prctl.h>
-#include <inttypes.h>
-#include <locale.h>
-#include <math.h>
-#include <sys/types.h>
-#include <sys/stat.h>
-#include <sys/wait.h>
-#include <unistd.h>
-#include <sys/time.h>
-#include <sys/resource.h>
-#include <linux/err.h>
-
-#include <linux/ctype.h>
-#include <perf/evlist.h>
-#include <internal/threadmap.h>
#ifdef HAVE_BPF_SKEL
#include "util/bpf_skel/bperf_cgroup.h"
@@ -123,6 +125,7 @@ static struct target target;
static volatile sig_atomic_t child_pid = -1;
static int detailed_run = 0;
static bool transaction_run;
+static bool use_perf_stat_print;
static bool topdown_run = false;
static bool smi_cost = false;
static bool smi_reset = false;
@@ -1094,7 +1097,10 @@ static void print_counters(struct timespec *ts, int argc, const char **argv)
if (quiet)
return;
- evlist__print_counters(evsel_list, &stat_config, &target, ts, argc, argv);
+ if (use_perf_stat_print)
+ perf_stat__print(evsel_list, &stat_config, &target, ts, argc, argv);
+ else
+ evlist__print_counters(evsel_list, &stat_config, &target, ts, argc, argv);
}
static volatile sig_atomic_t signr = -1;
@@ -2593,6 +2599,8 @@ int cmd_stat(int argc, const char **argv)
"Use with 'percore' event qualifier to show the event "
"counts of one hardware thread by sum up total hardware "
"threads of same physical core"),
+ OPT_BOOLEAN(0, "new", &use_perf_stat_print,
+ "use new clean API code for display output"),
OPT_BOOLEAN(0, "summary", &stat_config.summary,
"print summary for interval mode"),
OPT_BOOLEAN(0, "no-csv-summary", &stat_config.no_csv_summary,
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 330311cac550..0cf5bf625664 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -113,6 +113,10 @@ perf-util-y += counts.o
perf-util-y += stat.o
perf-util-y += stat-shadow.o
perf-util-y += stat-display.o
+perf-util-y += stat-print.o
+perf-util-y += stat-print-std.o
+perf-util-y += stat-print-csv.o
+perf-util-y += stat-print-json.o
perf-util-y += perf_api_probe.o
perf-util-y += record.o
perf-util-y += srcline.o
diff --git a/tools/perf/util/stat-display.c b/tools/perf/util/stat-display.c
index f94f1324d24a..ab67c80a895e 100644
--- a/tools/perf/util/stat-display.c
+++ b/tools/perf/util/stat-display.c
@@ -37,7 +37,7 @@
#define PID_LEN 7
#define CPUS_LEN 4
-static int aggr_header_lens[] = {
+const int aggr_header_lens[] = {
[AGGR_CORE] = 18,
[AGGR_CACHE] = 22,
[AGGR_CLUSTER] = 20,
@@ -49,7 +49,7 @@ static int aggr_header_lens[] = {
[AGGR_GLOBAL] = 0,
};
-static const char *aggr_header_csv[] = {
+const char *aggr_header_csv[] = {
[AGGR_CORE] = "core,ctrs,",
[AGGR_CACHE] = "cache,ctrs,",
[AGGR_CLUSTER] = "cluster,ctrs,",
@@ -61,7 +61,7 @@ static const char *aggr_header_csv[] = {
[AGGR_GLOBAL] = ""
};
-static const char *aggr_header_std[] = {
+const char *aggr_header_std[] = {
[AGGR_CORE] = "core",
[AGGR_CACHE] = "cache",
[AGGR_CLUSTER] = "cluster",
@@ -890,9 +890,9 @@ static void printout(struct perf_stat_config *config, struct outstate *os,
* Return: %true if the value should NOT be printed, %false if the value
* needs to be printed like "<not counted>" or "<not supported>".
*/
-static bool should_skip_zero_counter(struct perf_stat_config *config,
- struct evsel *counter,
- const struct aggr_cpu_id *id)
+bool should_skip_zero_counter(struct perf_stat_config *config,
+ struct evsel *counter,
+ const struct aggr_cpu_id *id)
{
struct perf_cpu cpu;
unsigned int idx;
diff --git a/tools/perf/util/stat-print-csv.c b/tools/perf/util/stat-print-csv.c
new file mode 100644
index 000000000000..ee86d07636ea
--- /dev/null
+++ b/tools/perf/util/stat-print-csv.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "stat-print.h"
+#include <linux/compiler.h>
+
+int perf_stat__print_csv(struct evlist *evlist __maybe_unused,
+ const struct perf_stat_config *config __maybe_unused,
+ const struct target *target __maybe_unused,
+ const struct timespec *ts __maybe_unused,
+ int argc __maybe_unused,
+ const char **argv __maybe_unused)
+{
+ return 0;
+}
diff --git a/tools/perf/util/stat-print-json.c b/tools/perf/util/stat-print-json.c
new file mode 100644
index 000000000000..4690c4286fd2
--- /dev/null
+++ b/tools/perf/util/stat-print-json.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "stat-print.h"
+#include <linux/compiler.h>
+
+int perf_stat__print_json(struct evlist *evlist __maybe_unused,
+ const struct perf_stat_config *config __maybe_unused,
+ const struct target *target __maybe_unused,
+ const struct timespec *ts __maybe_unused,
+ int argc __maybe_unused,
+ const char **argv __maybe_unused)
+{
+ return 0;
+}
diff --git a/tools/perf/util/stat-print-std.c b/tools/perf/util/stat-print-std.c
new file mode 100644
index 000000000000..a1311f950559
--- /dev/null
+++ b/tools/perf/util/stat-print-std.c
@@ -0,0 +1,13 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "stat-print.h"
+#include <linux/compiler.h>
+
+int perf_stat__print_std(struct evlist *evlist __maybe_unused,
+ const struct perf_stat_config *config __maybe_unused,
+ const struct target *target __maybe_unused,
+ const struct timespec *ts __maybe_unused,
+ int argc __maybe_unused,
+ const char **argv __maybe_unused)
+{
+ return 0;
+}
diff --git a/tools/perf/util/stat-print.c b/tools/perf/util/stat-print.c
new file mode 100644
index 000000000000..419fecfd9198
--- /dev/null
+++ b/tools/perf/util/stat-print.c
@@ -0,0 +1,500 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "stat-print.h"
+#include "cgroup.h"
+
+#include <errno.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <linux/compiler.h>
+
+#include "cpumap.h"
+#include "debug.h"
+#include "evlist.h"
+#include "evsel.h"
+#include "expr.h"
+#include "metricgroup.h"
+#include "stat.h"
+#include "thread_map.h"
+#include "tool_pmu.h"
+
+/*
+ * Unified Aggregation Helpers (Shared by STD, CSV, JSON Formats)
+ */
+
+const char *perf_stat__get_aggr_key(const struct perf_stat_config *config,
+ const struct evsel *evsel)
+{
+ switch (config->aggr_mode) {
+ case AGGR_CORE:
+ return "core";
+ case AGGR_CACHE:
+ return "cache";
+ case AGGR_CLUSTER:
+ return "cluster";
+ case AGGR_DIE:
+ return "die";
+ case AGGR_SOCKET:
+ return "socket";
+ case AGGR_NODE:
+ return "node";
+ case AGGR_NONE:
+ if (evsel->percore && !config->percore_show_thread)
+ return "core";
+ return "cpu";
+ case AGGR_THREAD:
+ return "thread";
+ case AGGR_GLOBAL:
+ case AGGR_UNSET:
+ case AGGR_MAX:
+ default:
+ return "";
+ }
+}
+
+int perf_stat__get_aggr_id_char(const struct perf_stat_config *config, struct evsel *evsel,
+ struct aggr_cpu_id id, char *buf, size_t buf_size)
+{
+ switch (config->aggr_mode) {
+ case AGGR_CORE:
+ return scnprintf(buf, buf_size, "S%d-D%d-C%d", id.socket, id.die, id.core);
+ case AGGR_CACHE:
+ return scnprintf(buf, buf_size, "S%d-D%d-L%d-ID%d", id.socket, id.die, id.cache_lvl,
+ id.cache);
+ case AGGR_CLUSTER:
+ return scnprintf(buf, buf_size, "S%d-D%d-CLS%d", id.socket, id.die, id.cluster);
+ case AGGR_DIE:
+ return scnprintf(buf, buf_size, "S%d-D%d", id.socket, id.die);
+ case AGGR_SOCKET:
+ return scnprintf(buf, buf_size, "S%d", id.socket);
+ case AGGR_NODE:
+ return scnprintf(buf, buf_size, "N%d", id.node);
+ case AGGR_NONE:
+ if (evsel->percore && !config->percore_show_thread)
+ return scnprintf(buf, buf_size, "S%d-D%d-C%d", id.socket, id.die, id.core);
+ else if (id.cpu.cpu > -1)
+ return scnprintf(buf, buf_size, "%d", id.cpu.cpu);
+ break;
+ case AGGR_THREAD:
+ return scnprintf(buf, buf_size, "%s-%d",
+ perf_thread_map__comm(evsel->core.threads, id.thread_idx),
+ perf_thread_map__pid(evsel->core.threads, id.thread_idx));
+ case AGGR_GLOBAL:
+ case AGGR_UNSET:
+ case AGGR_MAX:
+ default:
+ break;
+ }
+ buf[0] = '\0';
+ return -1;
+}
+
+/*
+ * Traversal Driver and Calculation Code
+ */
+
+/**
+ * tool_pmu__is_time_event - Check if event is a tool PMU time event.
+ *
+ * Copied from stat-shadow.c to make stat-print.c self-contained.
+ */
+static bool tool_pmu__is_time_event(const struct perf_stat_config *config,
+ const struct evsel *evsel, int *tool_aggr_idx)
+{
+ enum tool_pmu_event event = evsel__tool_event(evsel);
+ int aggr_idx;
+
+ if (event != TOOL_PMU__EVENT_DURATION_TIME && event != TOOL_PMU__EVENT_USER_TIME &&
+ event != TOOL_PMU__EVENT_SYSTEM_TIME)
+ return false;
+
+ if (config && config->aggr_map) {
+ for (aggr_idx = 0; aggr_idx < config->aggr_map->nr; aggr_idx++) {
+ if (config->aggr_map->map[aggr_idx].cpu.cpu == 0) {
+ *tool_aggr_idx = aggr_idx;
+ return true;
+ }
+ }
+ pr_debug("Unexpected CPU0 missing in aggregation for tool event.\n");
+ }
+ *tool_aggr_idx = 0; /* Assume the first aggregation index works. */
+ return true;
+}
+
+/**
+ * prepare_metric - Collect event values required for a metric.
+ * @config: Perf stat configuration.
+ * @mexp: The metric expression.
+ * @evsel: The associated event selector.
+ * @pctx: Expr parse context to add ID/values to.
+ * @aggr_idx: Aggregation index to read values from.
+ *
+ * Iterates over the events required for the metric expression, reads their
+ * counts for the given aggregation index, and adds them to the expression
+ * parser context.
+ *
+ * Copied and refactored from stat-shadow.c.
+ */
+static int prepare_metric(const struct perf_stat_config *config, const struct metric_expr *mexp,
+ struct evsel *evsel, struct expr_parse_ctx *pctx, int aggr_idx)
+{
+ struct evsel *const *metric_events = mexp->metric_events;
+ struct metric_ref *metric_refs = mexp->metric_refs;
+ int i;
+
+ for (i = 0; metric_events[i]; i++) {
+ int source_count = 0, tool_aggr_idx;
+ bool is_tool_time =
+ tool_pmu__is_time_event(config, metric_events[i], &tool_aggr_idx);
+ struct perf_stat_evsel *ps = metric_events[i]->stats;
+ char *n;
+ double val;
+
+ /*
+ * If there are multiple uncore PMUs and we're not reading the
+ * leader's stats, determine the stats for the appropriate
+ * uncore PMU.
+ */
+ if (evsel && evsel->metric_leader && evsel->pmu != evsel->metric_leader->pmu &&
+ mexp->metric_events[i]->pmu == evsel->metric_leader->pmu) {
+ struct evsel *pos;
+
+ evlist__for_each_entry(evsel->evlist, pos) {
+ if (pos->pmu != evsel->pmu)
+ continue;
+ if (pos->metric_leader != mexp->metric_events[i])
+ continue;
+ ps = pos->stats;
+ source_count = 1;
+ break;
+ }
+ }
+ /* Time events are always on CPU0, the first aggregation index. */
+ if (!ps || !metric_events[i]->supported || !ps->aggr ||
+ aggr_idx < 0 || (is_tool_time && tool_aggr_idx < 0)) {
+ val = NAN;
+ source_count = 0;
+ } else {
+ struct perf_stat_aggr *aggr =
+ &ps->aggr[is_tool_time ? tool_aggr_idx :
+ aggr_idx];
+
+ if (aggr->counts.run == 0) {
+ val = NAN;
+ source_count = 0;
+ } else {
+ val = aggr->counts.val;
+ if (is_tool_time) {
+ /* Convert time event nanoseconds to seconds. */
+ val *= 1e-9;
+ }
+ if (!source_count)
+ source_count = evsel__source_count(metric_events[i]);
+ }
+ }
+ n = strdup(evsel__metric_id(metric_events[i]));
+ if (!n)
+ return -ENOMEM;
+
+ expr__add_id_val_source_count(pctx, n, val, source_count);
+ }
+
+ for (int j = 0; metric_refs && metric_refs[j].metric_name; j++) {
+ int ret = expr__add_ref(pctx, &metric_refs[j]);
+
+ if (ret)
+ return ret;
+ }
+
+ return i;
+}
+
+/**
+ * calculate_and_print_metric - Compute and print a single metric.
+ *
+ * Parses the metric expression, computes the ratio, and calls the print_metric
+ * callback directly with clean parameters.
+ * Returns the return value of the print_metric callback (0 on success, or error).
+ */
+static int calculate_and_print_metric(struct perf_stat_config *config,
+ const struct perf_stat_print_callbacks *cb, void *outer_ctx,
+ struct metric_expr *mexp, struct evsel *evsel, int aggr_idx)
+{
+ const char *metric_name = mexp->metric_name;
+ const char *metric_expr = mexp->metric_expr;
+ const char *metric_threshold = mexp->metric_threshold;
+ const char *metric_unit = mexp->metric_unit;
+ struct evsel *const *metric_events = mexp->metric_events;
+ int runtime = mexp->runtime;
+ struct expr_parse_ctx *pctx;
+ double ratio, scale, threshold;
+ int i;
+ enum metric_threshold_classify thresh = METRIC_THRESHOLD_UNKNOWN;
+ int ret = 0;
+
+ if (!cb->print_metric)
+ return 0;
+
+ pctx = expr__ctx_new();
+ if (!pctx)
+ return -ENOMEM;
+
+ if (config->user_requested_cpu_list)
+ pctx->sctx.user_requested_cpu_list = strdup(config->user_requested_cpu_list);
+ pctx->sctx.runtime = runtime;
+ pctx->sctx.system_wide = config->system_wide;
+ i = prepare_metric(config, mexp, evsel, pctx, aggr_idx);
+ if (i < 0) {
+ expr__ctx_free(pctx);
+ return i;
+ }
+ if (!metric_events[i]) {
+ if (expr__parse(&ratio, pctx, metric_expr) == 0) {
+ char *unit = NULL;
+
+ if (metric_threshold &&
+ expr__parse(&threshold, pctx, metric_threshold) == 0 &&
+ !isnan(threshold)) {
+ thresh = fpclassify(threshold) == FP_ZERO ? METRIC_THRESHOLD_GOOD :
+ METRIC_THRESHOLD_BAD;
+ }
+
+ if (metric_unit && metric_name) {
+ if (perf_pmu__convert_scale(metric_unit, &unit,
+ &scale) < 0)
+ unit = NULL;
+ else
+ ratio *= scale;
+
+ ret = cb->print_metric(outer_ctx, config, evsel,
+ aggr_idx, metric_name,
+ unit, ratio, thresh);
+ } else {
+ ret = cb->print_metric(outer_ctx, config, evsel,
+ aggr_idx,
+ metric_name ?: (evsel->name ?: ""),
+ NULL, ratio, thresh);
+ }
+ }
+ }
+
+ expr__ctx_free(pctx);
+ return ret;
+}
+
+/**
+ * perf_stat_print_metricgroup - Traverse metrics for an event.
+ *
+ * Returns 0 on success, or a negative error code on failure.
+ */
+static bool __maybe_unused is_basic_shadow_metric(const char *name)
+{
+ static const char *const basic_metrics[] = {
+ "insn_per_cycle", "branch_miss_rate", "branch_frequency",
+ "cycles_frequency", "page_faults_per_second", "migrations_per_second",
+ "cs_per_second", "CPUs_utilized",
+ };
+ size_t i;
+
+ if (!name)
+ return false;
+
+ for (i = 0; i < ARRAY_SIZE(basic_metrics); i++) {
+ if (!strcmp(basic_metrics[i], name))
+ return true;
+ }
+ return false;
+}
+
+static int perf_stat_print_metricgroup(struct perf_stat_config *config,
+ const struct perf_stat_print_callbacks *cb, void *outer_ctx,
+ struct evsel *evsel, int aggr_idx)
+{
+ struct metric_event *me;
+ struct metric_expr *mexp;
+ struct rblist *metric_events = &evsel->evlist->metric_events;
+ int ret;
+
+ me = metricgroup__lookup(metric_events, evsel, false);
+ if (me == NULL)
+ return 0;
+
+ list_for_each_entry(mexp, &me->head, nd) {
+ ret = calculate_and_print_metric(config, cb, outer_ctx, mexp,
+ evsel, aggr_idx);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+/**
+ * perf_stat_print_metrics - Entry point for metric calculation & printing.
+ *
+ * Returns 0 on success, or a negative error code on failure.
+ */
+static int perf_stat_print_metrics(struct perf_stat_config *config,
+ const struct perf_stat_print_callbacks *cb, void *outer_ctx,
+ struct evsel *evsel, int aggr_idx)
+{
+ if (config->iostat_run) {
+ /* IOSTAT metrics not supported yet in new API */
+ return 0;
+ }
+
+ return perf_stat_print_metricgroup(config, cb, outer_ctx, evsel, aggr_idx);
+}
+
+int perf_stat__print_cb(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target __maybe_unused,
+ const struct timespec *ts __maybe_unused, int argc __maybe_unused,
+ const char **argv __maybe_unused,
+ const struct perf_stat_print_callbacks *cb, void *ctx)
+{
+ struct evsel *counter;
+ int aggr_idx;
+ int ret = 0;
+
+ evlist__uniquify_evsel_names(evlist, config);
+
+ if (cb->print_start) {
+ ret = cb->print_start(ctx, config);
+ if (ret)
+ return ret;
+ }
+
+ switch (config->aggr_mode) {
+ case AGGR_GLOBAL:
+ case AGGR_NONE:
+ case AGGR_SOCKET:
+ case AGGR_DIE:
+ case AGGR_CLUSTER:
+ case AGGR_CACHE:
+ case AGGR_CORE:
+ case AGGR_THREAD:
+ case AGGR_NODE:
+ if (config->aggr_map) {
+ cpu_aggr_map__for_each_idx(aggr_idx, config->aggr_map) {
+ evlist__for_each_entry(evlist, counter) {
+ struct perf_stat_evsel *ps = counter->stats;
+ u64 val = 0, ena = 0, run = 0;
+
+ if (ps && ps->aggr) {
+ val = ps->aggr[aggr_idx].counts.val;
+ ena = ps->aggr[aggr_idx].counts.ena;
+ run = ps->aggr[aggr_idx].counts.run;
+ }
+
+ /* Skip already merged uncore/hybrid events */
+ if (config->aggr_mode != AGGR_NONE) {
+ bool skip_hw = evsel__is_hybrid(counter) ?
+ (config->hybrid_merge &&
+ counter->first_wildcard_match != NULL) :
+ (counter->first_wildcard_match != NULL);
+
+ if (skip_hw)
+ continue;
+ }
+
+ if (perf_stat__skip_metric_event(counter))
+ continue;
+
+ if (cb->print_event) {
+ double stdev_pct = 0.0;
+
+ if (ps && ps->res_stats.n > 1) {
+ stdev_pct = rel_stddev_stats(
+ stddev_stats(&ps->res_stats), val);
+ }
+ ret = cb->print_event(
+ ctx, config, counter, aggr_idx, val,
+ ena, run, stdev_pct,
+ counter->cgrp ? counter->cgrp->name : NULL);
+ if (ret)
+ goto out;
+ }
+
+ ret = perf_stat_print_metrics(config, cb, ctx, counter,
+ aggr_idx);
+ if (ret)
+ goto out;
+ }
+ }
+ } else {
+ evlist__for_each_entry(evlist, counter) {
+ struct perf_stat_evsel *ps = counter->stats;
+ u64 val = 0, ena = 0, run = 0;
+
+ if (ps && ps->aggr) {
+ val = ps->aggr[0].counts.val;
+ ena = ps->aggr[0].counts.ena;
+ run = ps->aggr[0].counts.run;
+ }
+
+ /* Skip already merged uncore/hybrid events */
+ if (config->aggr_mode != AGGR_NONE) {
+ bool skip_hw = evsel__is_hybrid(counter) ?
+ (config->hybrid_merge &&
+ counter->first_wildcard_match != NULL) :
+ (counter->first_wildcard_match != NULL);
+
+ if (skip_hw)
+ continue;
+ }
+
+ if (perf_stat__skip_metric_event(counter))
+ continue;
+
+ if (cb->print_event) {
+ double stdev_pct = 0.0;
+
+ if (ps && ps->res_stats.n > 1) {
+ stdev_pct = rel_stddev_stats(
+ stddev_stats(&ps->res_stats), val);
+ }
+ ret = cb->print_event(
+ ctx, config, counter, 0, val, ena, run,
+ stdev_pct,
+ counter->cgrp ? counter->cgrp->name : NULL);
+ if (ret)
+ goto out;
+ }
+
+ ret = perf_stat_print_metrics(config, cb, ctx, counter, 0);
+ if (ret)
+ goto out;
+ }
+ }
+ break;
+ case AGGR_UNSET:
+ case AGGR_MAX:
+ default:
+ fprintf(config->output, "Aggregation mode %d not supported in new API yet\n",
+ config->aggr_mode);
+ break;
+ }
+
+out:
+ if (cb->print_end) {
+ int err = cb->print_end(ctx, config);
+
+ if (!ret)
+ ret = err;
+ }
+
+ return ret;
+}
+
+int perf_stat__print(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv)
+{
+ if (config->csv_output)
+ return perf_stat__print_csv(evlist, config, target, ts, argc, argv);
+ else if (config->json_output)
+ return perf_stat__print_json(evlist, config, target, ts, argc, argv);
+ else
+ return perf_stat__print_std(evlist, config, target, ts, argc, argv);
+}
diff --git a/tools/perf/util/stat-print.h b/tools/perf/util/stat-print.h
new file mode 100644
index 000000000000..7c0af4541ab8
--- /dev/null
+++ b/tools/perf/util/stat-print.h
@@ -0,0 +1,137 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PERF_STAT_PRINT_H
+#define __PERF_STAT_PRINT_H
+
+#include <linux/types.h>
+
+#include "stat.h"
+
+#define CNTR_NOT_SUPPORTED "<not supported>"
+#define CNTR_NOT_COUNTED "<not counted>"
+
+#include <time.h>
+#include <stdbool.h>
+
+struct evlist;
+struct perf_stat_config;
+struct target;
+struct evsel;
+struct aggr_cpu_id;
+
+extern const int aggr_header_lens[];
+extern const char *aggr_header_csv[];
+extern const char *aggr_header_std[];
+
+/**
+ * struct perf_stat_print_callbacks - Callbacks for rendering perf stat output.
+ *
+ * This structure defines the interface for different output formats (e.g.,
+ * Standard, CSV, JSON) to render the collected performance counter statistics.
+ * The core display logic traverses the events and metrics and calls these
+ * callbacks in a streaming fashion, which build an in-memory DOM tree. The
+ * final rendering and output formatting is executed entirely in print_end.
+ */
+struct perf_stat_print_callbacks {
+ /**
+ * print_start - Called before any event or metric is printed.
+ * @ctx: Opaque context pointer passed to the print function.
+ * @config: Perf stat configuration.
+ */
+ int (*print_start)(void *ctx, struct perf_stat_config *config);
+
+ /**
+ * print_end - Called after all events and metrics have been traversed.
+ * Executes the actual formatting and printing of the buffered tree.
+ * @ctx: Opaque context pointer.
+ * @config: Perf stat configuration.
+ */
+ int (*print_end)(void *ctx, struct perf_stat_config *config);
+
+ /**
+ * print_event - Called to buffer an event (counter) value.
+ * @ctx: Opaque context pointer.
+ * @config: Perf stat configuration.
+ * @evsel: The event selector being printed (mutable for lazy initialization).
+ * @aggr_idx: Aggregation index in evsel->stats.
+ * @val: Raw counter value.
+ * @ena: Enabled time for the counter (for multiplexing).
+ * @run: Running time for the counter (for multiplexing).
+ * @stdev_pct: Standard deviation percentage across multiple repeated runs.
+ *
+ * Returns 0 on success, or a negative error code (e.g., -ENOMEM) on failure.
+ */
+ int (*print_event)(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx, u64 val, u64 ena,
+ u64 run, double stdev_pct, const char *cgrp);
+
+ /**
+ * print_metric - Called to buffer a metric value associated with an event.
+ * @ctx: Opaque context pointer.
+ * @config: Perf stat configuration.
+ * @evsel: The event selector associated with the metric (mutable).
+ * @aggr_idx: Aggregation index.
+ * @name: The display name of the metric.
+ * @unit: The unit of the metric (e.g., "%", "GHz", or NULL).
+ * @val: The calculated metric value.
+ * @thresh: Threshold classification (e.g., good, bad) for color coding.
+ *
+ * Returns 0 on success, or a negative error code (e.g., -ENOMEM) on failure.
+ */
+ int (*print_metric)(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx, const char *name,
+ const char *unit, double val,
+ enum metric_threshold_classify thresh);
+};
+
+/**
+ * perf_stat__get_aggr_key - Get the JSON key name for an aggregation mode.
+ */
+const char *perf_stat__get_aggr_key(const struct perf_stat_config *config,
+ const struct evsel *evsel);
+
+/**
+ * perf_stat__get_aggr_id_char - Get the unified aggregation ID string.
+ *
+ * Returns the formatted string size, or a negative error code on failure.
+ */
+int perf_stat__get_aggr_id_char(const struct perf_stat_config *config, struct evsel *evsel,
+ struct aggr_cpu_id id, char *buf, size_t buf_size);
+
+/**
+ * perf_stat__print_cb - Drive the traversal and call callbacks.
+ *
+ * Defined in stat-print.c. Called by format-specific entry points.
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int perf_stat__print_cb(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv, const struct perf_stat_print_callbacks *cb, void *ctx);
+
+/**
+ * perf_stat__print - Entry point for the decoupled print API.
+ *
+ * Defined in stat-print.c. Dispatches to format-specific entry points.
+ * Returns 0 on success, or a negative error code on failure.
+ */
+int perf_stat__print(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv);
+
+/*
+ * Format-specific entry points, implemented in their respective files.
+ * All return 0 on success, or a negative error code on failure.
+ */
+
+int perf_stat__print_std(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv);
+
+int perf_stat__print_csv(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv);
+
+int perf_stat__print_json(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts, int argc,
+ const char **argv);
+
+#endif /* __PERF_STAT_PRINT_H */
diff --git a/tools/perf/util/stat.h b/tools/perf/util/stat.h
index 4bced233d2fc..ddc895bc93ca 100644
--- a/tools/perf/util/stat.h
+++ b/tools/perf/util/stat.h
@@ -106,6 +106,8 @@ struct perf_stat_config {
bool ctl_fd_close;
const char *cgroup_list;
unsigned int topdown_level;
+ bool headers_printed;
+ bool metric_only_headers_printed;
};
extern struct perf_stat_config stat_config;
@@ -188,6 +190,10 @@ int perf_stat_process_counter(struct perf_stat_config *config,
void perf_stat_merge_counters(struct perf_stat_config *config, struct evlist *evlist);
void perf_stat_process_percore(struct perf_stat_config *config, struct evlist *evlist);
+bool should_skip_zero_counter(struct perf_stat_config *config,
+ struct evsel *counter,
+ const struct aggr_cpu_id *id);
+
struct perf_tool;
union perf_event;
struct perf_session;
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:50AM -0700, Ian Rogers wrote: > This patch introduces the initial infrastructure for decoupling the perf > stat printing API. It declares the struct perf_stat_print_callbacks > interface and the core traversal driver perf_stat__print_cb() inside the > newly created util/stat-print.h and util/stat-print.c files. > > The generic traversal driver perf_stat__print_cb() drive traversing the > event lists across all supported cpu aggregation modes (global, die, > socket, cache, cluster, core, thread, none). It implements the clean > display filtering checks (perf_stat__skip_metric_event(), hybrid > wildcard merges) and the basic metrics allowlist filter > (is_basic_shadow_metric()) to keep formatting callbacks decoupled. > > This also introduces two format-agnostic shared helpers to centralize > aggregation prefix formatting: > - perf_stat__get_aggr_key(): resolves JSON key names. > - perf_stat__get_aggr_id_char(): formats unified aggregation identifiers. > > Adds empty format-specific stubs (perf_stat__print_std, _csv, _json) to > ensure that the new print files link and compile cleanly under > util/Build, without affecting the legacy print path. > > Signed-off-by: Ian Rogers <irogers@google.com> > Assisted-by: Antigravity:gemini-3.5-flash > Acked-by: Chun-Tse Shao <ctshao@google.com> > --- > tools/perf/builtin-stat.c | 96 +++--- > tools/perf/util/Build | 4 + > tools/perf/util/stat-display.c | 12 +- > tools/perf/util/stat-print-csv.c | 13 + > tools/perf/util/stat-print-json.c | 13 + > tools/perf/util/stat-print-std.c | 13 + > tools/perf/util/stat-print.c | 500 ++++++++++++++++++++++++++++++ > tools/perf/util/stat-print.h | 137 ++++++++ > tools/perf/util/stat.h | 6 + > 9 files changed, 744 insertions(+), 50 deletions(-) > create mode 100644 tools/perf/util/stat-print-csv.c > create mode 100644 tools/perf/util/stat-print-json.c > create mode 100644 tools/perf/util/stat-print-std.c > create mode 100644 tools/perf/util/stat-print.c > create mode 100644 tools/perf/util/stat-print.h > > diff --git a/tools/perf/builtin-stat.c b/tools/perf/builtin-stat.c > index 3f685beba384..532856f8be90 100644 > --- a/tools/perf/builtin-stat.c > +++ b/tools/perf/builtin-stat.c > @@ -40,62 +40,64 @@ > * Jaswinder Singh Rajput <jaswinder@kernel.org> > */ > > +#include <errno.h> > +#include <inttypes.h> > +#include <locale.h> > +#include <math.h> > +#include <signal.h> > +#include <stdlib.h> > + > +#include <linux/ctype.h> > +#include <linux/err.h> > +#include <linux/list_sort.h> > +#include <linux/time64.h> > +#include <linux/zalloc.h> > +#include <sys/prctl.h> > +#include <sys/resource.h> > +#include <sys/stat.h> > +#include <sys/time.h> > +#include <sys/types.h> > +#include <sys/wait.h> > +#include <unistd.h> > + > +#include <api/fs/fs.h> > +#include <internal/threadmap.h> > +#include <perf/evlist.h> > +#include <subcmd/parse-options.h> > + > +#include "asm/bug.h" > #include "builtin.h" > +#include "util/affinity.h" > +#include "util/bpf_counter.h" > #include "util/cgroup.h" > -#include <subcmd/parse-options.h> > -#include "util/parse-events.h" > -#include "util/pmus.h" > -#include "util/pmu.h" > -#include "util/tool_pmu.h" > +#include "util/color.h" > +#include "util/counts.h" > +#include "util/cpumap.h" > +#include "util/debug.h" > #include "util/event.h" > #include "util/evlist.h" > #include "util/evsel.h" > -#include "util/debug.h" > -#include "util/color.h" > -#include "util/stat.h" > #include "util/header.h" > -#include "util/cpumap.h" > -#include "util/thread_map.h" > -#include "util/counts.h" > -#include "util/topdown.h" > +#include "util/intel-tpebs.h" > +#include "util/iostat.h" > +#include "util/metricgroup.h" > +#include "util/parse-events.h" > +#include "util/pfm.h" > +#include "util/pmu.h" > +#include "util/pmus.h" > #include "util/session.h" > -#include "util/tool.h" > +#include "util/stat-print.h" > +#include "util/stat.h" > #include "util/string2.h" > -#include "util/metricgroup.h" > #include "util/synthetic-events.h" > #include "util/target.h" > +#include "util/thread_map.h" > #include "util/time-utils.h" > +#include "util/tool.h" > +#include "util/tool_pmu.h" > #include "util/top.h" > -#include "util/affinity.h" > -#include "util/pfm.h" > -#include "util/bpf_counter.h" > -#include "util/iostat.h" > +#include "util/topdown.h" > #include "util/util.h" > -#include "util/intel-tpebs.h" > -#include "asm/bug.h" > - > -#include <linux/list_sort.h> > -#include <linux/time64.h> > -#include <linux/zalloc.h> > -#include <api/fs/fs.h> > -#include <errno.h> > -#include <signal.h> > -#include <stdlib.h> > -#include <sys/prctl.h> > -#include <inttypes.h> > -#include <locale.h> > -#include <math.h> > -#include <sys/types.h> > -#include <sys/stat.h> > -#include <sys/wait.h> > -#include <unistd.h> > -#include <sys/time.h> > -#include <sys/resource.h> > -#include <linux/err.h> > - > -#include <linux/ctype.h> > -#include <perf/evlist.h> > -#include <internal/threadmap.h> > > #ifdef HAVE_BPF_SKEL > #include "util/bpf_skel/bperf_cgroup.h" > @@ -123,6 +125,7 @@ static struct target target; > static volatile sig_atomic_t child_pid = -1; > static int detailed_run = 0; > static bool transaction_run; > +static bool use_perf_stat_print; Please align. Thanks, Namhyung
This patch implements standard console formatting callbacks inside
util/stat-print-std.c, replacing the empty stubs introduced in Commit 1.
Introduces the format-private `struct queued_event` and `struct
queued_metric` DOM nodes to buffer traversal streams, and fully
encapsulates DOM state initialization and queue cleanups inside
std_print_start() and std_print_end().
Utilizes the newly centralized unified aggregation helpers to resolve
CPU and thread prefixes cleanly, and incorporates full interval-mode
timestamp printing support across all rows.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/util/stat-print-std.c | 855 ++++++++++++++++++++++++++++++-
1 file changed, 848 insertions(+), 7 deletions(-)
diff --git a/tools/perf/util/stat-print-std.c b/tools/perf/util/stat-print-std.c
index a1311f950559..d83410ae3385 100644
--- a/tools/perf/util/stat-print-std.c
+++ b/tools/perf/util/stat-print-std.c
@@ -1,13 +1,854 @@
// SPDX-License-Identifier: GPL-2.0
-#include "stat-print.h"
+#include <errno.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <time.h>
+#include <sys/time.h>
+#include <sys/resource.h>
+#include <stdbool.h>
+
#include <linux/compiler.h>
+#include <linux/kernel.h>
+#include <linux/list.h>
+
+#include "color.h"
+#include "cpumap.h"
+#include "debug.h"
+#include "evlist.h"
+#include "evsel.h"
+#include "metricgroup.h"
+#include "pmus.h"
+#include "stat-print.h"
+#include "stat.h"
+#include "target.h"
+#include "thread_map.h"
+#include "tool_pmu.h"
+
+#define COUNTS_LEN 18
+#define EVNAME_LEN 32
+#define COMM_LEN 16
+#define PID_LEN 7
+#define MGROUP_LEN 50
+#define CGROUP_LEN 16
+#define METRIC_LEN 38
+
+
+
+/**
+ * struct queued_metric - In-memory record of a buffered metric.
+ * @list: Linked list node for queueing.
+ * @name: The display name of the metric.
+ * @unit: The metric's unit (e.g., "%", "GHz", or NULL).
+ * @val: The calculated ratio/metric value.
+ * @thresh: Threshold classification for color coding.
+ * @aggr_idx: Aggregation index in evsel stats.
+ */
+struct queued_metric {
+ struct list_head list;
+ struct evsel *evsel;
+ char *name;
+ char *unit;
+ double val;
+ enum metric_threshold_classify thresh;
+ int aggr_idx;
+};
+
+/**
+ * struct queued_event - In-memory record of a buffered counter event.
+ * @list: Linked list node for queueing.
+ * @evsel: The associated performance event selector.
+ * @name: The uniquely formatted/resolved event name.
+ * @val: Raw aggregated counter value.
+ * @ena: Enabled time for multiplexing percentage.
+ * @run: Running time for multiplexing percentage.
+ * @stdev_pct: Standard deviation percentage across repeated runs.
+ * @cgrp: Cgroup name string (optional).
+ * @aggr_idx: Aggregation index.
+ * @is_metricgroup: Whether this represents a unified metricgroup header.
+ * @metrics_list: Linked list head containing nested queued_metric structures.
+ */
+struct queued_event {
+ struct list_head list;
+ struct evsel *evsel;
+ char *name;
+ char *cgrp;
+ u64 val, ena, run;
+ double stdev_pct;
+ int aggr_idx;
+ bool is_metricgroup;
+ struct list_head metrics_list;
+};
+
+/**
+ * struct std_print_state - Print state context for Standard console output.
+ * @fp: File descriptor to output to.
+ * @timestamp: Formatted interval timestamp (optional).
+ * @events_list: Linked list head containing queued_event nodes.
+ * @current_event: Pointer to the currently active event being printed.
+ * Serves as a temporary bridge to associate streaming metrics back to
+ * their parent event node during list buffering. This relies on a
+ * strict temporal coupling in the traversal driver: the driver always
+ * invokes print_metric() callbacks for a counter synchronously and
+ * immediately after its print_event() callback, prior to advancing
+ * to the next event or aggregation node. This pointer is completely
+ * private to standard printing, keeping the traversal driver decoupled
+ * and preserving strict encapsulation.
+ * @target: target query parameters for header printout.
+ * @argc: Command argument count.
+ * @argv: Command argument values.
+ */
+struct std_print_state {
+ FILE *fp;
+ char timestamp[64];
+ struct list_head events_list;
+ struct queued_event *current_event;
+ const struct target *target;
+ int argc;
+ const char **argv;
+};
+
+/**
+ * struct std_metric_only_print_state - Metric-only print state context for Standard console output.
+ * @fp: File descriptor to output to.
+ * @queued_metrics: Linked list head containing queued_metric nodes.
+ * @timestamp: Formatted interval timestamp (optional).
+ * @target: target query parameters.
+ * @argc: Command argument count.
+ * @argv: Command argument values.
+ */
+struct std_metric_only_print_state {
+ FILE *fp;
+ struct list_head queued_metrics;
+ char timestamp[64];
+ const struct target *target;
+ int argc;
+ const char **argv;
+ struct evlist *evlist;
+};
+
+/**
+ * print_aggr_id_std - Print the aggregation prefix for STD format.
+ *
+ * Uses the unified perf_stat__get_aggr_id_char helper to format the base
+ * aggregation string, and pads it dynamically using aggr_header_lens.
+ */
+static void print_aggr_id_std(const struct perf_stat_config *config, FILE *output,
+ struct evsel *evsel, struct aggr_cpu_id id, int aggr_nr)
+{
+ char buf[128];
+
+ if (perf_stat__get_aggr_id_char(config, evsel, id, buf, sizeof(buf)) < 0)
+ return;
+
+ if (config->aggr_mode == AGGR_NONE) {
+ if (evsel->percore && !config->percore_show_thread) {
+ fprintf(output, "%-*s ", aggr_header_lens[AGGR_CORE], buf);
+ } else if (id.cpu.cpu > -1) {
+ /* For CPU none mode, prepend "CPU" during console print */
+ char cpu_buf[160];
+
+ snprintf(cpu_buf, sizeof(cpu_buf), "CPU%s", buf);
+ fprintf(output, "%-*s ", aggr_header_lens[AGGR_NONE], cpu_buf);
+ }
+ return;
+ }
+
+ if (config->aggr_mode == AGGR_THREAD) {
+ fprintf(output, "%-*s ", aggr_header_lens[AGGR_THREAD], buf);
+ return;
+ }
+
+ /* Socket/Die/Node/Cache/Cluster modes print base ID and aggr count */
+ fprintf(output, "%-*s %*d ", aggr_header_lens[config->aggr_mode], buf, 4, aggr_nr);
+}
+
+/**
+ * should_skip_zero_counter - Check if a zero-valued counter should be skipped.
+ *
+ * Implemented locally for standard console formatting.
+ */
+static bool std_should_skip_zero_counter(struct perf_stat_config *config,
+ struct evsel *counter, int aggr_idx)
+{
+ struct perf_cpu cpu;
+ unsigned int idx;
+ struct aggr_cpu_id id;
+
+ if (verbose == 0 && counter->skippable && !counter->supported)
+ return true;
+
+ if (config->metric_only)
+ return false;
+
+ if (config->aggr_mode == AGGR_THREAD && config->system_wide)
+ return true;
+
+ if (aggr_idx < 0 || !config->aggr_map || !config->aggr_get_id)
+ return false;
+
+ id = config->aggr_map->map[aggr_idx];
+
+ if (evsel__is_tool(counter)) {
+ struct aggr_cpu_id own_id = config->aggr_get_id(config,
+ (struct perf_cpu){ .cpu = 0 });
+
+ return !aggr_cpu_id__equal(&id, &own_id);
+ }
+
+ perf_cpu_map__for_each_cpu(cpu, idx, counter->core.cpus) {
+ struct aggr_cpu_id own_id = config->aggr_get_id(config, cpu);
+
+ if (aggr_cpu_id__equal(&id, &own_id))
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Standard (STD) Output Callbacks - Normal Mode
+ */
+
+static int std_print_start(void *ctx, struct perf_stat_config *config __maybe_unused)
+{
+ struct std_print_state *ps = ctx;
+
+ INIT_LIST_HEAD(&ps->events_list);
+ ps->current_event = NULL;
+ return 0;
+}
+
+static int std_print_event(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx, u64 val, u64 ena,
+ u64 run, double stdev_pct, const char *cgrp)
+{
+ struct std_print_state *ps = ctx;
+ struct queued_event *ev;
+ /* Skip zero counters locally in STD callbacks if they qualify */
+ if (val == 0 && std_should_skip_zero_counter(config, evsel, aggr_idx)) {
+ ps->current_event = NULL;
+ return 0;
+ }
+
+ ev = malloc(sizeof(*ev));
+ if (!ev)
+ return -ENOMEM;
+
+ ev->name = strdup(evsel__name(evsel));
+ if (!ev->name) {
+ free(ev);
+ return -ENOMEM;
+ }
+
+ if (cgrp && cgrp[0]) {
+ ev->cgrp = strdup(cgrp);
+ if (!ev->cgrp) {
+ free(ev->name);
+ free(ev);
+ return -ENOMEM;
+ }
+ } else {
+ ev->cgrp = NULL;
+ }
+
+ ev->evsel = evsel;
+ ev->val = val;
+ ev->ena = ena;
+ ev->run = run;
+ ev->stdev_pct = stdev_pct;
+ ev->aggr_idx = aggr_idx;
+ INIT_LIST_HEAD(&ev->metrics_list);
+
+ list_add_tail(&ev->list, &ps->events_list);
+ ps->current_event = ev;
+
+ return 0;
+}
+
+static int std_print_metric(void *ctx, struct perf_stat_config *config __maybe_unused,
+ struct evsel *evsel __maybe_unused, int aggr_idx __maybe_unused,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh)
+{
+ struct std_print_state *ps = ctx;
+ struct queued_metric *b;
+
+ if (!ps->current_event)
+ return 0;
+
+ if (evsel != ps->current_event->evsel) {
+ pr_err("decoupled print engine: temporal coupling violation: evsel mismatch!\n");
+ return -EINVAL;
+ }
+
+ b = malloc(sizeof(*b));
+ if (!b)
+ return -ENOMEM;
+
+ b->name = strdup(name);
+ if (!b->name) {
+ free(b);
+ return -ENOMEM;
+ }
+
+ if (unit && unit[0]) {
+ b->unit = strdup(unit);
+ if (!b->unit) {
+ free(b->name);
+ free(b);
+ return -ENOMEM;
+ }
+ } else {
+ b->unit = NULL;
+ }
+
+ b->val = val;
+ b->thresh = thresh;
+ list_add_tail(&b->list, &ps->current_event->metrics_list);
+
+ return 0;
+}
+
+#define USEC_PER_SEC 1000000ULL
+#define NSEC_PER_SEC 1000000000ULL
+
+static double timeval2double(struct timeval *t)
+{
+ return t->tv_sec + (double)t->tv_usec / USEC_PER_SEC;
+}
+
+static void print_footer_std(const struct perf_stat_config *config)
+{
+ double avg = avg_stats(config->walltime_nsecs_stats) / NSEC_PER_SEC;
+ FILE *output = config->output;
+
+ if (config->interval)
+ return;
+
+ if (!config->null_run)
+ fprintf(output, "\n");
+
+ if (config->run_count == 1) {
+ fprintf(output, " %17.9f seconds time elapsed", avg);
+
+ if (config->ru_display) {
+ double ru_utime =
+ timeval2double((struct timeval *)&config->ru_data.ru_utime);
+ double ru_stime =
+ timeval2double((struct timeval *)&config->ru_data.ru_stime);
+
+ fprintf(output, "\n\n");
+ fprintf(output, " %17.9f seconds user\n", ru_utime);
+ fprintf(output, " %17.9f seconds sys\n", ru_stime);
+ }
+ } else {
+ double sd = stddev_stats(config->walltime_nsecs_stats) / NSEC_PER_SEC;
+
+ fprintf(output, " %17.9f +- %-17.9f seconds time elapsed", avg, sd);
+ }
+ fprintf(output, "\n");
+}
+
+/**
+ * print_header_std - Print the header prefix matching old API.
+ *
+ * Copied and adapted from stat-display.c.
+ */
+static void print_header_std(const struct perf_stat_config *config, const struct target *target,
+ int argc, const char **argv)
+{
+ FILE *output = config->output;
+ int i;
+
+ fprintf(output, "\n");
+ fprintf(output, " Performance counter stats for ");
+ if (target->bpf_str)
+ fprintf(output, "\'BPF program(s) %s", target->bpf_str);
+ else if (target->system_wide)
+ fprintf(output, "\'system wide");
+ else if (target->cpu_list)
+ fprintf(output, "\'CPU(s) %s", target->cpu_list);
+ else if (!target__has_task(target)) {
+ fprintf(output, "\'%s", argv ? argv[0] : "pipe");
+ for (i = 1; argv && (i < argc); i++)
+ fprintf(output, " %s", argv[i]);
+ } else if (target->pid)
+ fprintf(output, "process id \'%s", target->pid);
+ else
+ fprintf(output, "thread id \'%s", target->tid);
-int perf_stat__print_std(struct evlist *evlist __maybe_unused,
- const struct perf_stat_config *config __maybe_unused,
- const struct target *target __maybe_unused,
- const struct timespec *ts __maybe_unused,
- int argc __maybe_unused,
- const char **argv __maybe_unused)
+ fprintf(output, "\'");
+ if (config->run_count > 1)
+ fprintf(output, " (%d runs)", config->run_count);
+ fprintf(output, ":\n\n");
+}
+
+static int std_print_end(void *ctx, struct perf_stat_config *config)
{
+ struct std_print_state *ps = ctx;
+ struct queued_event *ev, *tmp_ev;
+ struct queued_metric *met, *tmp_met;
+ FILE *out = ps->fp;
+ bool first;
+ const char *last_mg_name = NULL;
+ const struct perf_pmu *last_pmu = NULL;
+ int last_aggr_idx = -1;
+
+ /* Print the formatted header prefix (only in non-interval mode) */
+ if (!config->interval)
+ print_header_std(config, ps->target, ps->argc, ps->argv);
+
+ list_for_each_entry_safe(ev, tmp_ev, &ps->events_list, list) {
+ struct evsel *evsel = ev->evsel;
+ double sc = evsel->scale;
+ const char *fmt;
+ const char *bad_count = evsel->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
+ struct metric_event *me =
+ metricgroup__lookup(&evsel->evlist->metric_events, evsel, false);
+ bool is_metricgroup = false;
+ bool skip_header = false;
+ char full_name[128] = "";
+
+ if (me && me->is_default && !evsel->default_show_events) {
+ struct metric_expr *mexp =
+ list_first_entry(&me->head, struct metric_expr, nd);
+ const char *mg_name = mexp->default_metricgroup_name;
+ bool need_full_name = perf_pmus__num_core_pmus() > 1;
+
+ if (need_full_name && evsel->pmu)
+ scnprintf(full_name, sizeof(full_name), "%s (%s)", mg_name,
+ evsel->pmu->name);
+ else
+ scnprintf(full_name, sizeof(full_name), "%s", mg_name);
+ is_metricgroup = true;
+
+ if (last_mg_name && !strcmp(last_mg_name, mg_name) &&
+ last_pmu == evsel->pmu && last_aggr_idx == ev->aggr_idx) {
+ skip_header = true;
+ }
+ last_mg_name = mg_name;
+ last_pmu = evsel->pmu;
+ last_aggr_idx = ev->aggr_idx;
+ }
+
+ /* Print interval timestamp if configured */
+ if (config->interval && ps->timestamp[0] && !skip_header)
+ fprintf(out, "%s", ps->timestamp);
+
+ /* 1. Print aggregation prefix first (if we don't skip header) */
+ if (!skip_header && config->aggr_map && ev->aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
+
+ print_aggr_id_std(config, out, evsel, id, aggr_nr);
+ }
+
+ /* 2. Print event value (scaled) or spaces if metricgroup */
+ if (is_metricgroup) {
+ if (!skip_header) {
+ int n = fprintf(out, " %*s", EVNAME_LEN, full_name);
+ int pad = MGROUP_LEN + config->unit_width + 2 - n;
+
+ fprintf(out, "%*s", pad > 0 ? pad : 0, "");
+ }
+ } else {
+ if (config->big_num)
+ fmt = floor(sc) != sc ? "%'*.2f " : "%'*.0f ";
+ else
+ fmt = floor(sc) != sc ? "%*.2f " : "%*.0f ";
+
+ if (ev->run == 0 || ev->ena == 0) {
+ fprintf(out, "%*s ", COUNTS_LEN, bad_count);
+ } else {
+ double scaled = (double)ev->val;
+ double avg;
+
+ if (ev->ena < ev->run)
+ scaled = (double)ev->val * ev->run / ev->ena;
+
+ avg = scaled * sc;
+ fprintf(out, fmt, COUNTS_LEN, avg);
+ }
+
+ /* 3. Print unit */
+ if (evsel->unit) {
+ fprintf(out, "%-*s ", config->unit_width, evsel->unit);
+ } else {
+ if (config->unit_width > 0)
+ fprintf(out, "%-*s ", config->unit_width, "");
+ }
+
+ /* 4. Print event name and cgroup */
+ fprintf(out, "%-*s", EVNAME_LEN, evsel__name(evsel));
+ if (ev->cgrp && ev->cgrp[0])
+ fprintf(out, " %-*s", CGROUP_LEN, ev->cgrp);
+
+ /* If there are no metrics, print noise and multiplexing percentage */
+ if (list_empty(&ev->metrics_list)) {
+ if (ev->stdev_pct)
+ fprintf(out, " ( +-%6.2f%% )", ev->stdev_pct);
+ if (ev->run != ev->ena)
+ fprintf(out, " (%.2f%%)", 100.0 * ev->run / ev->ena);
+ }
+ }
+
+ first = true;
+ list_for_each_entry_safe(met, tmp_met, &ev->metrics_list, list) {
+ const char *color = metric_threshold_classify__color(met->thresh);
+ char unit_name[128];
+ const char *m_fmt = (met->unit && met->unit[0]) ? "%8.1f" : "%8.2f";
+
+ if (met->unit && met->unit[0]) {
+ snprintf(unit_name, sizeof(unit_name), "%s %s", met->unit,
+ met->name);
+ } else {
+ snprintf(unit_name, sizeof(unit_name), "%s", met->name);
+ }
+
+ if (first) {
+ if (skip_header) {
+ if (config->interval && ps->timestamp[0])
+ fprintf(out, "%s", ps->timestamp);
+ if (config->aggr_map && ev->aggr_idx >= 0) {
+ struct aggr_cpu_id id =
+ config->aggr_map->map[ev->aggr_idx];
+ struct perf_stat_evsel *se = evsel->stats;
+ int nr = (se && se->aggr) ?
+ se->aggr[ev->aggr_idx].nr : 0;
+
+ print_aggr_id_std(config, out, evsel, id, nr);
+ }
+ fprintf(out, "%*s# ",
+ COUNTS_LEN + EVNAME_LEN + config->unit_width + 3,
+ "");
+ } else {
+ fprintf(out, " # ");
+ }
+ first = false;
+ } else {
+ /* Align subsequent metric lines */
+ fprintf(out, "\n");
+ if (config->interval && ps->timestamp[0])
+ fprintf(out, "%s", ps->timestamp);
+ if (config->aggr_map && ev->aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
+
+ print_aggr_id_std(config, out, evsel, id, aggr_nr);
+ }
+ fprintf(out, "%*s# ",
+ COUNTS_LEN + EVNAME_LEN + config->unit_width + 3, "");
+ }
+
+ if (color && color[0])
+ color_fprintf(out, color, m_fmt, met->val);
+ else
+ fprintf(out, m_fmt, met->val);
+
+ /*
+ * Print the metric unit and name left-aligned
+ * padded to METRIC_LEN - n - 1 = 26
+ */
+ fprintf(out, " %-26s", unit_name);
+
+ /*
+ * If this is the last metric in the list, print
+ * noise and multiplexing percentage
+ */
+ if (list_is_last(&met->list, &ev->metrics_list)) {
+ if (ev->stdev_pct)
+ fprintf(out, " ( +-%6.2f%% )", ev->stdev_pct);
+ if (ev->run != ev->ena)
+ fprintf(out, " (%.2f%%)", 100.0 * ev->run / ev->ena);
+ }
+
+ list_del(&met->list);
+ free(met->name);
+ free(met->unit);
+ free(met);
+ }
+ fprintf(out, "\n");
+
+ list_del(&ev->list);
+ free(ev->name);
+ free(ev->cgrp);
+ free(ev);
+ }
+ print_footer_std(config);
return 0;
}
+
+static const struct perf_stat_print_callbacks std_print_callbacks = {
+ .print_start = std_print_start,
+ .print_end = std_print_end,
+ .print_event = std_print_event,
+ .print_metric = std_print_metric,
+};
+
+/*
+ * Standard (STD) Output Callbacks - Metric-Only Mode
+ */
+
+static int std_metric_only_print_start(void *ctx,
+ struct perf_stat_config *config __maybe_unused)
+{
+ struct std_metric_only_print_state *ps = ctx;
+
+ INIT_LIST_HEAD(&ps->queued_metrics);
+ return 0;
+}
+
+static int std_metric_only_print_metric(void *ctx,
+ struct perf_stat_config *config __maybe_unused,
+ struct evsel *evsel, int aggr_idx,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh)
+{
+ struct std_metric_only_print_state *ps = ctx;
+ struct queued_metric *b = malloc(sizeof(*b));
+
+ if (!b)
+ return -ENOMEM;
+
+ b->evsel = evsel;
+ b->name = strdup(name);
+ if (!b->name) {
+ free(b);
+ return -ENOMEM;
+ }
+
+ if (unit && unit[0]) {
+ b->unit = strdup(unit);
+ if (!b->unit) {
+ free(b->name);
+ free(b);
+ return -ENOMEM;
+ }
+ } else {
+ b->unit = NULL;
+ }
+
+ b->val = val;
+ b->thresh = thresh;
+ b->aggr_idx = aggr_idx;
+ list_add_tail(&b->list, &ps->queued_metrics);
+
+ return 0;
+}
+
+static int std_metric_only_print_end(void *ctx, struct perf_stat_config *config)
+{
+ struct std_metric_only_print_state *ps = ctx;
+ struct queued_metric *b, *tmp;
+ FILE *out = ps->fp;
+ int first_aggr = -1;
+ /* Initialize to -2 to distinguish from -1 (a valid index in AGGR_GLOBAL mode) */
+ int current_aggr = -2;
+ const char *color;
+ char *str;
+ size_t mlen;
+ size_t header_len;
+ int ret = 0;
+ int err;
+
+ if (list_empty(&ps->queued_metrics))
+ return 0;
+
+ if (!config->metric_only_headers_printed) {
+ first_aggr = list_first_entry(&ps->queued_metrics,
+ struct queued_metric, list)->aggr_idx;
+
+ /* Print the formatted header prefix */
+ if (config->interval) {
+ /* Timestamp header: 17 chars wide ("# time ") */
+ fprintf(out, "#%15s ", "time");
+ } else {
+ print_header_std(config, ps->target, ps->argc, ps->argv);
+ }
+
+ if (config->aggr_map && first_aggr >= 0 &&
+ config->aggr_mode != AGGR_GLOBAL) {
+ int len = aggr_header_lens[config->aggr_mode];
+
+ if (config->aggr_mode == AGGR_CORE ||
+ config->aggr_mode == AGGR_NONE ||
+ config->aggr_mode == AGGR_THREAD) {
+ fprintf(out, "%-*s ", len,
+ aggr_header_std[config->aggr_mode]);
+ } else {
+ /*
+ * Socket/Die/Node/Cache/Cluster modes print
+ * label and "ctrs"
+ */
+ fprintf(out, "%-*s %4s ", len,
+ aggr_header_std[config->aggr_mode],
+ "ctrs");
+ }
+ }
+
+ /* Print headers */
+ list_for_each_entry(b, &ps->queued_metrics, list) {
+ if (b->aggr_idx == first_aggr) {
+ char *header_name;
+
+
+ if (b->unit && b->unit[0]) {
+ err = asprintf(&header_name, "%s %s", b->unit, b->name);
+ header_len = strlen(b->unit) + 2 + strlen(b->name);
+ } else {
+ header_name = strdup(b->name);
+ header_len = strlen(b->name);
+ err = header_name ? 0 : -1;
+ }
+ if (err < 0) {
+ ret = -ENOMEM;
+ goto cleanup;
+ }
+ mlen = config->metric_only_len;
+ if (mlen < header_len)
+ mlen = header_len;
+ fprintf(out, "%*s ", (int)mlen, header_name);
+ free(header_name);
+ }
+ }
+ fprintf(out, "\n");
+ config->metric_only_headers_printed = true;
+ }
+
+ /* Print values */
+ list_for_each_entry_safe(b, tmp, &ps->queued_metrics, list) {
+ if (b->aggr_idx != current_aggr) {
+ if (current_aggr != -2)
+ fprintf(out, "\n");
+ current_aggr = b->aggr_idx;
+ if (config->interval && ps->timestamp[0])
+ fprintf(out, "%s", ps->timestamp);
+ if (config->aggr_map && current_aggr >= 0 &&
+ current_aggr < config->aggr_map->nr) {
+ struct aggr_cpu_id id = config->aggr_map->map[current_aggr];
+ struct evsel *metric_evsel = b->evsel;
+ int aggr_nr = 0;
+
+ if (metric_evsel && metric_evsel->stats &&
+ metric_evsel->stats->aggr &&
+ current_aggr < metric_evsel->stats->nr_aggr)
+ aggr_nr = metric_evsel->stats->aggr[current_aggr].nr;
+
+ print_aggr_id_std(config, out, metric_evsel, id,
+ aggr_nr);
+ }
+ }
+ color = metric_threshold_classify__color(b->thresh);
+ if (b->unit && b->unit[0])
+ header_len = strlen(b->unit) + 2 + strlen(b->name);
+ else
+ header_len = strlen(b->name);
+
+ mlen = config->metric_only_len;
+ if (mlen < header_len)
+ mlen = header_len;
+
+ if (color && color[0]) {
+ err = asprintf(&str, "%s%.1f%s", color, b->val, PERF_COLOR_RESET);
+ mlen += strlen(color) + sizeof(PERF_COLOR_RESET) - 1;
+ } else {
+ err = asprintf(&str, "%.1f", b->val);
+ }
+ if (err < 0) {
+ ret = -ENOMEM;
+ goto cleanup;
+ }
+ fprintf(out, "%*s ", (int)mlen, str);
+ free(str);
+
+ list_del(&b->list);
+ free(b->name);
+ free(b->unit);
+ free(b);
+ }
+ if (config->interval && current_aggr != -2)
+ fprintf(out, "\n");
+ print_footer_std(config);
+ return 0;
+
+cleanup:
+ list_for_each_entry_safe(b, tmp, &ps->queued_metrics, list) {
+ list_del(&b->list);
+ free(b->name);
+ free(b->unit);
+ free(b);
+ }
+ return ret;
+}
+
+static const struct perf_stat_print_callbacks std_metric_only_print_callbacks = {
+ .print_start = std_metric_only_print_start,
+ .print_end = std_metric_only_print_end,
+ .print_event = NULL,
+ .print_metric = std_metric_only_print_metric,
+};
+
+int perf_stat__print_std(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts,
+ int argc, const char **argv)
+{
+ struct std_print_state ps = {
+ .fp = config->output,
+ .target = target,
+ .argc = argc,
+ .argv = argv,
+ };
+
+ if (config->metric_only) {
+ struct std_metric_only_print_state mops = {
+ .fp = config->output,
+ .target = target,
+ .argc = argc,
+ .argv = argv,
+ .evlist = evlist,
+ };
+ if (config->interval && ts) {
+ scnprintf(mops.timestamp, sizeof(mops.timestamp), "%6lu.%09lu ",
+ (unsigned long)ts->tv_sec, ts->tv_nsec);
+ } else {
+ mops.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &std_metric_only_print_callbacks, &mops);
+ } else {
+ if (config->interval && !config->headers_printed) {
+ FILE *output = config->output;
+
+ if (config->aggr_mode == AGGR_GLOBAL) {
+ fprintf(output, "#%*s %*s %*s events\n",
+ 15 - 2, "time", 18, "counts",
+ config->unit_width, "unit");
+ } else {
+ fprintf(output,
+ "#%*s %-*s ctrs %*s %*s events\n",
+ 15 - 2, "time",
+ aggr_header_lens[config->aggr_mode],
+ aggr_header_std[config->aggr_mode],
+ 18, "counts", config->unit_width,
+ "unit");
+ }
+ config->headers_printed = true;
+ }
+ if (config->interval && ts) {
+ scnprintf(ps.timestamp, sizeof(ps.timestamp), "%6lu.%09lu ",
+ (unsigned long)ts->tv_sec, ts->tv_nsec);
+ } else {
+ ps.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &std_print_callbacks, &ps);
+ }
+}
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:51AM -0700, Ian Rogers wrote:
> This patch implements standard console formatting callbacks inside
> util/stat-print-std.c, replacing the empty stubs introduced in Commit 1.
>
> Introduces the format-private `struct queued_event` and `struct
> queued_metric` DOM nodes to buffer traversal streams, and fully
> encapsulates DOM state initialization and queue cleanups inside
> std_print_start() and std_print_end().
>
> Utilizes the newly centralized unified aggregation helpers to resolve
> CPU and thread prefixes cleanly, and incorporates full interval-mode
> timestamp printing support across all rows.
>
> Signed-off-by: Ian Rogers <irogers@google.com>
> Assisted-by: Antigravity:gemini-3.5-flash
> Acked-by: Chun-Tse Shao <ctshao@google.com>
> ---
[SNIP]
> +static int std_print_event(void *ctx, struct perf_stat_config *config,
> + struct evsel *evsel, int aggr_idx, u64 val, u64 ena,
> + u64 run, double stdev_pct, const char *cgrp)
> +{
> + struct std_print_state *ps = ctx;
> + struct queued_event *ev;
> + /* Skip zero counters locally in STD callbacks if they qualify */
> + if (val == 0 && std_should_skip_zero_counter(config, evsel, aggr_idx)) {
> + ps->current_event = NULL;
> + return 0;
> + }
> +
> + ev = malloc(sizeof(*ev));
> + if (!ev)
> + return -ENOMEM;
I expected this function prints the event info right away. Why we need
to save it and print later?
> +
> + ev->name = strdup(evsel__name(evsel));
> + if (!ev->name) {
> + free(ev);
> + return -ENOMEM;
> + }
> +
> + if (cgrp && cgrp[0]) {
> + ev->cgrp = strdup(cgrp);
> + if (!ev->cgrp) {
> + free(ev->name);
> + free(ev);
> + return -ENOMEM;
> + }
> + } else {
> + ev->cgrp = NULL;
> + }
Do you really need this? Why not using evsel__name() and
evsel->cgrp->name directly?
> +
> + ev->evsel = evsel;
> + ev->val = val;
> + ev->ena = ena;
> + ev->run = run;
> + ev->stdev_pct = stdev_pct;
> + ev->aggr_idx = aggr_idx;
> + INIT_LIST_HEAD(&ev->metrics_list);
> +
> + list_add_tail(&ev->list, &ps->events_list);
> + ps->current_event = ev;
> +
> + return 0;
> +}
[SNIP]
> +static int std_print_end(void *ctx, struct perf_stat_config *config)
> {
> + struct std_print_state *ps = ctx;
> + struct queued_event *ev, *tmp_ev;
> + struct queued_metric *met, *tmp_met;
> + FILE *out = ps->fp;
> + bool first;
> + const char *last_mg_name = NULL;
> + const struct perf_pmu *last_pmu = NULL;
> + int last_aggr_idx = -1;
> +
> + /* Print the formatted header prefix (only in non-interval mode) */
> + if (!config->interval)
> + print_header_std(config, ps->target, ps->argc, ps->argv);
> +
> + list_for_each_entry_safe(ev, tmp_ev, &ps->events_list, list) {
> + struct evsel *evsel = ev->evsel;
> + double sc = evsel->scale;
> + const char *fmt;
> + const char *bad_count = evsel->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
> + struct metric_event *me =
> + metricgroup__lookup(&evsel->evlist->metric_events, evsel, false);
> + bool is_metricgroup = false;
> + bool skip_header = false;
> + char full_name[128] = "";
> +
> + if (me && me->is_default && !evsel->default_show_events) {
> + struct metric_expr *mexp =
> + list_first_entry(&me->head, struct metric_expr, nd);
> + const char *mg_name = mexp->default_metricgroup_name;
> + bool need_full_name = perf_pmus__num_core_pmus() > 1;
> +
> + if (need_full_name && evsel->pmu)
> + scnprintf(full_name, sizeof(full_name), "%s (%s)", mg_name,
> + evsel->pmu->name);
> + else
> + scnprintf(full_name, sizeof(full_name), "%s", mg_name);
> + is_metricgroup = true;
> +
> + if (last_mg_name && !strcmp(last_mg_name, mg_name) &&
> + last_pmu == evsel->pmu && last_aggr_idx == ev->aggr_idx) {
> + skip_header = true;
> + }
> + last_mg_name = mg_name;
> + last_pmu = evsel->pmu;
> + last_aggr_idx = ev->aggr_idx;
> + }
> +
> + /* Print interval timestamp if configured */
> + if (config->interval && ps->timestamp[0] && !skip_header)
> + fprintf(out, "%s", ps->timestamp);
> +
> + /* 1. Print aggregation prefix first (if we don't skip header) */
> + if (!skip_header && config->aggr_map && ev->aggr_idx >= 0) {
> + struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
> + int aggr_nr = 0;
> +
> + if (evsel->stats && evsel->stats->aggr)
> + aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
> +
> + print_aggr_id_std(config, out, evsel, id, aggr_nr);
> + }
> +
> + /* 2. Print event value (scaled) or spaces if metricgroup */
> + if (is_metricgroup) {
> + if (!skip_header) {
> + int n = fprintf(out, " %*s", EVNAME_LEN, full_name);
> + int pad = MGROUP_LEN + config->unit_width + 2 - n;
> +
> + fprintf(out, "%*s", pad > 0 ? pad : 0, "");
> + }
> + } else {
> + if (config->big_num)
> + fmt = floor(sc) != sc ? "%'*.2f " : "%'*.0f ";
> + else
> + fmt = floor(sc) != sc ? "%*.2f " : "%*.0f ";
> +
> + if (ev->run == 0 || ev->ena == 0) {
> + fprintf(out, "%*s ", COUNTS_LEN, bad_count);
> + } else {
> + double scaled = (double)ev->val;
> + double avg;
> +
> + if (ev->ena < ev->run)
> + scaled = (double)ev->val * ev->run / ev->ena;
I think the value is already scaled in process_counter_values().
Thanks,
Namhyung
> +
> + avg = scaled * sc;
> + fprintf(out, fmt, COUNTS_LEN, avg);
> + }
This patch extends the standard console (STD) output linter script
tools/perf/tests/shell/stat+std_output.sh to run the basic no-argument
check a second time using the --new print flag:
perf_cmd="--new -o ${stat_output}"
check_no_args "STD (New API)" "$perf_cmd"
This ensures that standard console outputs produced by the decoupled
printing callbacks are formally validated by the test suite.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Arnaldo Carvalho de Melo <acme@redhat.com>
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+std_output.sh | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh
index 9c4b92ecf448..233e0a50eb33 100755
--- a/tools/perf/tests/shell/stat+std_output.sh
+++ b/tools/perf/tests/shell/stat+std_output.sh
@@ -118,5 +118,9 @@ then
else
echo "[Skip] Skipping tests for system_wide_no_aggr, per_core, per_die and per_socket since socket id exposed via topology is invalid"
fi
+# New API basic checks
+perf_cmd="--new -o ${stat_output}"
+check_no_args "STD (New API)" "$perf_cmd"
+
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch extends tools/perf/tests/shell/stat+std_output.sh to run all
standard CPU and thread aggregation checks under the --new print flag:
- check_system_wide
- check_system_wide_no_aggr
- check_interval
- check_per_thread
- check_per_node
- check_per_core
- check_per_socket
- check_per_die
This guarantees that standard console outputs produced by the decoupled
printing engine are verified across all core CPU-aggregation modes.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+std_output.sh | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh
index 233e0a50eb33..8dee005a7281 100755
--- a/tools/perf/tests/shell/stat+std_output.sh
+++ b/tools/perf/tests/shell/stat+std_output.sh
@@ -121,6 +121,17 @@ fi
# New API basic checks
perf_cmd="--new -o ${stat_output}"
check_no_args "STD (New API)" "$perf_cmd"
+check_system_wide "STD (New API)" "$perf_cmd"
+check_interval "STD (New API)" "$perf_cmd"
+check_per_thread "STD (New API)" "$perf_cmd"
+check_per_node "STD (New API)" "$perf_cmd"
+if [ $skip_test -ne 1 ]
+then
+ check_system_wide_no_aggr "STD (New API)" "$perf_cmd"
+ check_per_core "STD (New API)" "$perf_cmd"
+ check_per_die "STD (New API)" "$perf_cmd"
+ check_per_socket "STD (New API)" "$perf_cmd"
+fi
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch extends tools/perf/tests/shell/stat+std_output.sh to run the
advanced hardware PMU and topology checks under the --new print flag:
- check_per_cache_instance
- check_per_cluster
This ensures that standard console outputs are verified under advanced
topology-aware aggregation environments.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+std_output.sh | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh
index 8dee005a7281..0d38a1283967 100755
--- a/tools/perf/tests/shell/stat+std_output.sh
+++ b/tools/perf/tests/shell/stat+std_output.sh
@@ -131,6 +131,8 @@ then
check_per_core "STD (New API)" "$perf_cmd"
check_per_die "STD (New API)" "$perf_cmd"
check_per_socket "STD (New API)" "$perf_cmd"
+ check_per_cache_instance "STD (New API)" "$perf_cmd"
+ check_per_cluster "STD (New API)" "$perf_cmd"
fi
cleanup
--
2.55.0.141.g00534a21ce-goog
This patch extends tools/perf/tests/shell/stat+std_output.sh to run the
metric-only check under the --new print flag:
- check_metric_only
This guarantees that standard console metric-only outputs produced by
the decoupled printing callbacks are formally validated by the test
linter.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+std_output.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh
index 0d38a1283967..69720b19b908 100755
--- a/tools/perf/tests/shell/stat+std_output.sh
+++ b/tools/perf/tests/shell/stat+std_output.sh
@@ -134,6 +134,7 @@ then
check_per_cache_instance "STD (New API)" "$perf_cmd"
check_per_cluster "STD (New API)" "$perf_cmd"
fi
+check_metric_only "STD (New API)" "$perf_cmd"
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:55AM -0700, Ian Rogers wrote: > This patch extends tools/perf/tests/shell/stat+std_output.sh to run the > metric-only check under the --new print flag: > > - check_metric_only > > This guarantees that standard console metric-only outputs produced by > the decoupled printing callbacks are formally validated by the test > linter. > > Signed-off-by: Ian Rogers <irogers@google.com> > Assisted-by: Antigravity:gemini-3.5-flash > Acked-by: Chun-Tse Shao <ctshao@google.com> Can you please combine test changes into a commit? I guess it's ok to have separate one for std, csv and json. But I'm seeing this as a temporary changes and we will remove it once we switch to the new way. Thanks, Namhyung > --- > tools/perf/tests/shell/stat+std_output.sh | 1 + > 1 file changed, 1 insertion(+) > > diff --git a/tools/perf/tests/shell/stat+std_output.sh b/tools/perf/tests/shell/stat+std_output.sh > index 0d38a1283967..69720b19b908 100755 > --- a/tools/perf/tests/shell/stat+std_output.sh > +++ b/tools/perf/tests/shell/stat+std_output.sh > @@ -134,6 +134,7 @@ then > check_per_cache_instance "STD (New API)" "$perf_cmd" > check_per_cluster "STD (New API)" "$perf_cmd" > fi > +check_metric_only "STD (New API)" "$perf_cmd" > > cleanup > exit 0 > -- > 2.55.0.141.g00534a21ce-goog >
This patch implements CSV output formatting callbacks inside
util/stat-print-csv.c, replacing the empty stubs introduced in Commit 1.
Defines the format-private `struct queued_event` and `struct
queued_metric` DOM nodes to buffer traversal streams, and fully
encapsulates CSV queued lists lifecycle and deallocations inside
csv_print_start() and csv_print_end().
Utilizes the newly centralized unified aggregation helpers to format CPU
and thread column prefixes cleanly, fixes metrics separators padding,
and incorporates full interval-mode timestamp printing support.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/util/stat-print-csv.c | 678 ++++++++++++++++++++++++++++++-
1 file changed, 671 insertions(+), 7 deletions(-)
diff --git a/tools/perf/util/stat-print-csv.c b/tools/perf/util/stat-print-csv.c
index ee86d07636ea..f93ca1885d46 100644
--- a/tools/perf/util/stat-print-csv.c
+++ b/tools/perf/util/stat-print-csv.c
@@ -1,13 +1,677 @@
// SPDX-License-Identifier: GPL-2.0
-#include "stat-print.h"
+#include <errno.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <time.h>
+#include <stdbool.h>
#include <linux/compiler.h>
+#include <linux/list.h>
+
+#include "cpumap.h"
+#include "evlist.h"
+#include "evsel.h"
+#include "stat-print.h"
+#include "stat.h"
+#include "thread_map.h"
+#include "debug.h"
+
+#define COMM_LEN 16
+#define PID_LEN 7
+
+struct queued_metric {
+ struct list_head list;
+ struct evsel *evsel;
+ char *name;
+ char *unit;
+ double val;
+ int aggr_idx;
+};
+
+/**
+ * struct queued_event - In-memory record of a buffered CSV counter event.
+ * @list: Linked list node for queueing.
+ * @evsel: The associated performance event selector.
+ * @name: The uniquely formatted/resolved event name.
+ * @unit: The event's unit (e.g. "msec", "cycles").
+ * @cgrp: Cgroup name (optional).
+ * @val: Raw aggregated counter value.
+ * @ena: Enabled time for multiplexing percentage.
+ * @run: Running time for multiplexing percentage.
+ * @scale: Event scale factor.
+ * @stdev_pct: Standard deviation percentage.
+ * @supported: Event hardware support indicator.
+ * @aggr_idx: Aggregation index.
+ * @metrics_list: Linked list head containing nested queued_metric structures.
+ */
+struct queued_event {
+ struct list_head list;
+ struct evsel *evsel;
+ char *name;
+ char *unit;
+ char *cgrp;
+ u64 val, ena, run;
+ double scale;
+ double stdev_pct;
+ bool supported;
+ int aggr_idx;
+ struct list_head metrics_list;
+};
+
+/**
+ * struct csv_print_state - Print state context for CSV output.
+ * @fp: File descriptor to output to.
+ * @sep: CSV column separator character/string.
+ * @timestamp: Formatted interval timestamp (optional).
+ * @events_list: Linked list head containing queued_event nodes.
+ * @current_event: Pointer to the currently active event being printed.
+ * Serves as a temporary bridge to associate streaming metrics back to
+ * their parent event node during list buffering. This relies on a
+ * strict temporal coupling in the traversal driver: the driver always
+ * invokes print_metric() callbacks for a counter synchronously and
+ * immediately after its print_event() callback, prior to advancing
+ * to the next event or aggregation node. This pointer is completely
+ * private to CSV printing, keeping the traversal driver decoupled
+ * and preserving strict encapsulation.
+ */
+struct csv_print_state {
+ FILE *fp;
+ const char *sep;
+ char timestamp[64];
+ struct list_head events_list;
+ struct queued_event *current_event;
+};
+
+/**
+ * struct csv_metric_only_print_state - Metric-only print state context for CSV output.
+ * @fp: File descriptor to output to.
+ * @sep: CSV column separator.
+ * @timestamp: Formatted interval timestamp (optional).
+ * @evlist: Evlist to query entries from.
+ * @queued_metrics: Linked list head containing queued_metric nodes.
+ */
+struct csv_metric_only_print_state {
+ FILE *fp;
+ const char *sep;
+ char timestamp[64];
+ struct evlist *evlist;
+ struct list_head queued_metrics;
+};
+
+/**
+ * print_aggr_id_csv - Print the aggregation prefix for CSV format.
+ *
+ * Copied and adapted from stat-display.c.
+ */
+static void print_aggr_id_csv(const struct perf_stat_config *config, FILE *output,
+ struct evsel *evsel, struct aggr_cpu_id id, int aggr_nr)
+{
+ const char *sep = config->csv_sep;
+
+ switch (config->aggr_mode) {
+ case AGGR_CORE:
+ fprintf(output, "S%d-D%d-C%d%s%d%s", id.socket, id.die, id.core, sep, aggr_nr, sep);
+ break;
+ case AGGR_CACHE:
+ fprintf(output, "S%d-D%d-L%d-ID%d%s%d%s", id.socket, id.die, id.cache_lvl, id.cache,
+ sep, aggr_nr, sep);
+ break;
+ case AGGR_CLUSTER:
+ fprintf(output, "S%d-D%d-CLS%d%s%d%s", id.socket, id.die, id.cluster, sep, aggr_nr,
+ sep);
+ break;
+ case AGGR_DIE:
+ fprintf(output, "S%d-D%d%s%d%s", id.socket, id.die, sep, aggr_nr, sep);
+ break;
+ case AGGR_SOCKET:
+ fprintf(output, "S%d%s%d%s", id.socket, sep, aggr_nr, sep);
+ break;
+ case AGGR_NODE:
+ fprintf(output, "N%d%s%d%s", id.node, sep, aggr_nr, sep);
+ break;
+ case AGGR_NONE:
+ if (evsel->percore && !config->percore_show_thread)
+ fprintf(output, "S%d-D%d-C%d%s", id.socket, id.die, id.core, sep);
+ else if (id.cpu.cpu > -1)
+ fprintf(output, "CPU%d%s", id.cpu.cpu, sep);
+ break;
+ case AGGR_THREAD: {
+ const char *comm = "unknown";
+ int pid = -1;
+
+ if (evsel && evsel->core.threads && id.thread_idx >= 0 &&
+ id.thread_idx < perf_thread_map__nr(evsel->core.threads)) {
+ comm = perf_thread_map__comm(evsel->core.threads,
+ id.thread_idx);
+ pid = perf_thread_map__pid(evsel->core.threads,
+ id.thread_idx);
+ }
+ fprintf(output, "%s-%d%s", comm, pid, sep);
+ break;
+ }
+ case AGGR_GLOBAL:
+ case AGGR_UNSET:
+ case AGGR_MAX:
+ default:
+ break;
+ }
+}
+
+/*
+ * CSV Output Callbacks - Normal Mode
+ */
+
+static int csv_print_start(void *ctx, struct perf_stat_config *config __maybe_unused)
+{
+ struct csv_print_state *ps = ctx;
+
+ INIT_LIST_HEAD(&ps->events_list);
+ ps->current_event = NULL;
+ return 0;
+}
+
+static int csv_print_event(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx, u64 val, u64 ena,
+ u64 run, double stdev_pct, const char *cgrp)
+{
+ struct csv_print_state *ps = ctx;
+ struct queued_event *ev;
+ struct aggr_cpu_id id = aggr_cpu_id__empty();
+
+ if (config && config->aggr_map && aggr_idx >= 0 &&
+ aggr_idx < config->aggr_map->nr)
+ id = config->aggr_map->map[aggr_idx];
+
+ /* Skip zero counters in CSV callbacks if they qualify */
+ if (val == 0 && should_skip_zero_counter(config, evsel, &id)) {
+ ps->current_event = NULL;
+ return 0;
+ }
+
+ ev = malloc(sizeof(*ev));
+ if (!ev)
+ return -ENOMEM;
+
+ ev->name = strdup(evsel__name(evsel));
+ if (!ev->name) {
+ free(ev);
+ return -ENOMEM;
+ }
+
+ if (evsel->unit) {
+ ev->unit = strdup(evsel->unit);
+ if (!ev->unit) {
+ free(ev->name);
+ free(ev);
+ return -ENOMEM;
+ }
+ } else {
+ ev->unit = NULL;
+ }
+
+ if (cgrp && cgrp[0]) {
+ ev->cgrp = strdup(cgrp);
+ if (!ev->cgrp) {
+ free(ev->unit);
+ free(ev->name);
+ free(ev);
+ return -ENOMEM;
+ }
+ } else {
+ ev->cgrp = NULL;
+ }
+
+ ev->evsel = evsel;
+ ev->val = val;
+ ev->ena = ena;
+ ev->run = run;
+ ev->scale = evsel->scale;
+ ev->stdev_pct = stdev_pct;
+ ev->supported = evsel->supported;
+ ev->aggr_idx = aggr_idx;
+ INIT_LIST_HEAD(&ev->metrics_list);
+
+ list_add_tail(&ev->list, &ps->events_list);
+ ps->current_event = ev;
+
+ return 0;
+}
+
+static int csv_print_metric(void *ctx, struct perf_stat_config *config __maybe_unused,
+ struct evsel *evsel, int aggr_idx,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh __maybe_unused)
+{
+ struct csv_print_state *ps = ctx;
+ struct queued_metric *b;
+
+ if (!ps->current_event)
+ return 0;
+
+ if (evsel != ps->current_event->evsel) {
+ pr_err("decoupled print engine: temporal coupling violation: evsel mismatch!\n");
+ return -EINVAL;
+ }
+
+ b = malloc(sizeof(*b));
+ if (!b)
+ return -ENOMEM;
+
+ b->evsel = evsel;
+ b->name = strdup(name);
+ if (!b->name) {
+ free(b);
+ return -ENOMEM;
+ }
+
+ if (unit && unit[0]) {
+ b->unit = strdup(unit);
+ if (!b->unit) {
+ free(b->name);
+ free(b);
+ return -ENOMEM;
+ }
+ } else {
+ b->unit = NULL;
+ }
+
+ b->val = val;
+ b->aggr_idx = aggr_idx;
+ list_add_tail(&b->list, &ps->current_event->metrics_list);
+
+ return 0;
+}
+
+static int csv_print_end(void *ctx, struct perf_stat_config *config)
+{
+ struct csv_print_state *ps = ctx;
+ struct queued_event *ev, *tmp_ev;
+ struct queued_metric *met, *tmp_met;
+ FILE *output = ps->fp;
+ const char *sep = ps->sep;
+ bool has_metrics;
+
+ list_for_each_entry_safe(ev, tmp_ev, &ps->events_list, list) {
+ struct evsel *evsel = ev->evsel;
+ bool ok = (ev->run != 0 && ev->ena != 0);
+ const char *bad_count = ev->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
+ double enabled_percent = 100;
+
+ /* Print interval timestamp first if configured */
+ if (config->interval && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+
+ /* Print aggregation prefix first in CSV normal mode */
+ if (config->aggr_map && ev->aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
+
+ print_aggr_id_csv(config, output, evsel, id, aggr_nr);
+ }
+
+ /* 1. Print Value, Unit, Name (Columns 1, 2, 3) */
+ if (ok) {
+ double sc = ev->scale;
+ double avg = ev->val * sc;
+ const char *fmt = floor(sc) != sc ? "%.2f%s" : "%.0f%s";
+
+ fprintf(output, fmt, avg, sep);
+ } else {
+ fprintf(output, "%s%s", bad_count, sep);
+ }
-int perf_stat__print_csv(struct evlist *evlist __maybe_unused,
- const struct perf_stat_config *config __maybe_unused,
- const struct target *target __maybe_unused,
- const struct timespec *ts __maybe_unused,
- int argc __maybe_unused,
- const char **argv __maybe_unused)
+ if (ev->unit)
+ fprintf(output, "%s%s", ev->unit, sep);
+ else
+ fprintf(output, "%s", sep);
+
+ fprintf(output, "%s", ev->name);
+
+ /* 2. Print Runtime and Enabled Percentage (Columns 4, 5) */
+ if (ev->run != ev->ena)
+ enabled_percent = 100.0 * ev->run / ev->ena;
+ fprintf(output, "%s%" PRIu64 "%s%.2f", sep, ev->run, sep,
+ enabled_percent);
+
+ /* 3. Print Metrics (Columns 6, 7) */
+ has_metrics = false;
+ list_for_each_entry_safe(met, tmp_met, &ev->metrics_list, list) {
+ if (!has_metrics) {
+ has_metrics = true;
+ } else {
+ fprintf(output, "\n");
+ if (config->interval && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+ if (config->aggr_map && ev->aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
+
+ print_aggr_id_csv(config, output, evsel, id, aggr_nr);
+ }
+ /*
+ * Pad exactly 4 commas (Value, Unit, Name, Run, Enabled)
+ * to line up with the Metric Value column.
+ */
+ fprintf(output, "%s%s%s%s", sep, sep, sep, sep);
+ }
+ fprintf(output, "%s%.2f%s", sep, met->val, sep);
+ if (met->name && met->name[0])
+ fprintf(output, "%s", met->name);
+
+ list_del(&met->list);
+ free(met->name);
+ free(met->unit);
+ free(met);
+ }
+ if (!has_metrics)
+ fprintf(output, "%s%s", sep, sep);
+ fprintf(output, "\n");
+
+ list_del(&ev->list);
+ free(ev->name);
+ free(ev->unit);
+ free(ev->cgrp);
+ free(ev);
+ }
+ return 0;
+}
+
+static const struct perf_stat_print_callbacks csv_print_callbacks = {
+ .print_start = csv_print_start,
+ .print_end = csv_print_end,
+ .print_event = csv_print_event,
+ .print_metric = csv_print_metric,
+};
+
+/*
+ * CSV Output Callbacks - Metric-Only Mode
+ */
+
+static int csv_metric_only_print_start(void *ctx,
+ struct perf_stat_config *config __maybe_unused)
{
+ struct csv_metric_only_print_state *ps = ctx;
+
+ INIT_LIST_HEAD(&ps->queued_metrics);
return 0;
}
+
+static int csv_metric_only_print_metric(void *ctx,
+ struct perf_stat_config *config __maybe_unused,
+ struct evsel *evsel, int aggr_idx,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh __maybe_unused)
+{
+ struct csv_metric_only_print_state *ps = ctx;
+ struct queued_metric *b = malloc(sizeof(*b));
+
+ if (!b)
+ return -ENOMEM;
+
+ b->evsel = evsel;
+ b->name = strdup(name);
+ if (!b->name) {
+ free(b);
+ return -ENOMEM;
+ }
+
+ if (unit && unit[0]) {
+ b->unit = strdup(unit);
+ if (!b->unit) {
+ free(b->name);
+ free(b);
+ return -ENOMEM;
+ }
+ } else {
+ b->unit = NULL;
+ }
+
+ b->val = val;
+ b->aggr_idx = aggr_idx;
+ list_add_tail(&b->list, &ps->queued_metrics);
+
+ return 0;
+}
+
+static int csv_metric_only_print_end(void *ctx, struct perf_stat_config *config)
+{
+ struct csv_metric_only_print_state *ps = ctx;
+ FILE *output = ps->fp;
+ const char *sep = ps->sep;
+ struct queued_metric *b, *tmp;
+ char **unique_names = NULL;
+ size_t num_unique = 0;
+ size_t unique_alloc = 0;
+ int *unique_aggrs = NULL;
+ size_t num_aggrs = 0;
+ size_t aggr_alloc = 0;
+ size_t i, j;
+ int ret = 0;
+ int err;
+
+ if (list_empty(&ps->queued_metrics))
+ return 0;
+
+ /* 1. Build unique metric names and unique aggr_idx lists */
+ list_for_each_entry(b, &ps->queued_metrics, list) {
+ bool found_metric = false;
+ bool found_aggr = false;
+
+ for (i = 0; i < num_unique; i++) {
+ if (!strcmp(unique_names[i], b->name)) {
+ found_metric = true;
+ break;
+ }
+ }
+ if (!found_metric) {
+ if (num_unique >= unique_alloc) {
+ unique_alloc = unique_alloc ? unique_alloc * 2 : 16;
+ unique_names = realloc(unique_names,
+ unique_alloc * sizeof(char *));
+ if (!unique_names) {
+ ret = -ENOMEM;
+ goto cleanup_grid;
+ }
+ }
+ unique_names[num_unique++] = strdup(b->name);
+ if (!unique_names[num_unique - 1]) {
+ num_unique--;
+ ret = -ENOMEM;
+ goto cleanup_grid;
+ }
+ }
+
+ for (i = 0; i < num_aggrs; i++) {
+ if (unique_aggrs[i] == b->aggr_idx) {
+ found_aggr = true;
+ break;
+ }
+ }
+ if (!found_aggr) {
+ if (num_aggrs >= aggr_alloc) {
+ aggr_alloc = aggr_alloc ? aggr_alloc * 2 : 16;
+ unique_aggrs = realloc(unique_aggrs,
+ aggr_alloc * sizeof(int));
+ if (!unique_aggrs) {
+ ret = -ENOMEM;
+ goto cleanup_grid;
+ }
+ }
+ unique_aggrs[num_aggrs++] = b->aggr_idx;
+ }
+ }
+
+ /* 2. Print Headers (if not already printed) */
+ if (!config->metric_only_headers_printed) {
+ if (config->interval) {
+ fprintf(output, "#%s%s", "time", sep);
+ } else {
+ if (config->aggr_map && num_aggrs > 0 &&
+ config->aggr_mode != AGGR_GLOBAL) {
+ const char *header = aggr_header_csv[config->aggr_mode];
+ const char *p = header;
+
+ while (*p) {
+ if (*p == ',')
+ fputs(sep, output);
+ else
+ fputc(*p, output);
+ p++;
+ }
+ }
+ }
+
+ /* Print uniquely collected headers in exact columnar order */
+ for (i = 0; i < num_unique; i++) {
+ char *header_name = NULL;
+ /* Find first matching metric to grab its unit for the header */
+ char *unit = NULL;
+
+ list_for_each_entry(b, &ps->queued_metrics, list) {
+ if (!strcmp(b->name, unique_names[i])) {
+ unit = b->unit;
+ break;
+ }
+ }
+
+ if (unit && unit[0])
+ err = asprintf(&header_name, "%s %s", unit,
+ unique_names[i]);
+ else
+ err = asprintf(&header_name, "%s",
+ unique_names[i]);
+
+ if (err < 0 || !header_name) {
+ ret = -ENOMEM;
+ goto cleanup_grid;
+ }
+ fprintf(output, "%s%s", header_name, sep);
+ free(header_name);
+ }
+ fprintf(output, "\n");
+ config->metric_only_headers_printed = true;
+ }
+
+ /* 3. Print Values for Each Aggregation Node */
+ for (i = 0; i < num_aggrs; i++) {
+ int current_aggr = unique_aggrs[i];
+ struct evsel *last_evsel = NULL;
+
+ /* Print aggregation prefix */
+ if (config->interval && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+
+ if (config->aggr_map && current_aggr >= 0 &&
+ current_aggr < config->aggr_map->nr) {
+ struct aggr_cpu_id id = config->aggr_map->map[current_aggr];
+ int aggr_nr = 0;
+
+ /* Find evsel for this aggr_idx */
+ list_for_each_entry(b, &ps->queued_metrics, list) {
+ if (b->aggr_idx == current_aggr) {
+ last_evsel = b->evsel;
+ break;
+ }
+ }
+
+ if (last_evsel && last_evsel->stats &&
+ last_evsel->stats->aggr &&
+ current_aggr < last_evsel->stats->nr_aggr)
+ aggr_nr = last_evsel->stats->aggr[current_aggr].nr;
+
+ print_aggr_id_csv(config, output, last_evsel, id, aggr_nr);
+ }
+
+ /* Print each metric, or pad with empty comma if missing for this node */
+ for (j = 0; j < num_unique; j++) {
+ struct queued_metric *found_metric = NULL;
+
+ list_for_each_entry(b, &ps->queued_metrics, list) {
+ if (b->aggr_idx == current_aggr &&
+ !strcmp(b->name, unique_names[j])) {
+ found_metric = b;
+ break;
+ }
+ }
+
+ if (found_metric) {
+ if (isnan(found_metric->val))
+ fprintf(output, "%s", sep);
+ else
+ fprintf(output, "%.1f%s",
+ found_metric->val, sep);
+ } else {
+ /* Padding for missing metric to preserve column alignment */
+ fprintf(output, "%s", sep);
+ }
+ }
+ fprintf(output, "\n");
+ }
+
+cleanup_grid:
+ for (i = 0; i < num_unique; i++)
+ free(unique_names[i]);
+ free(unique_names);
+ free(unique_aggrs);
+
+ list_for_each_entry_safe(b, tmp, &ps->queued_metrics, list) {
+ list_del(&b->list);
+ free(b->name);
+ free(b->unit);
+ free(b);
+ }
+ return ret;
+}
+
+static const struct perf_stat_print_callbacks csv_metric_only_print_callbacks = {
+ .print_start = csv_metric_only_print_start,
+ .print_end = csv_metric_only_print_end,
+ .print_event = NULL,
+ .print_metric = csv_metric_only_print_metric,
+};
+
+int perf_stat__print_csv(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts,
+ int argc, const char **argv)
+{
+ if (config->metric_only) {
+ struct csv_metric_only_print_state ps = {
+ .fp = config->output,
+ .sep = config->csv_sep,
+ .evlist = evlist,
+ };
+ if (config->interval && ts) {
+ scnprintf(ps.timestamp, sizeof(ps.timestamp), "%lu.%09lu%s",
+ (unsigned long)ts->tv_sec, ts->tv_nsec, config->csv_sep);
+ } else {
+ ps.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &csv_metric_only_print_callbacks, &ps);
+ } else {
+ struct csv_print_state ps = {
+ .fp = config->output,
+ .sep = config->csv_sep,
+ };
+
+
+
+ if (config->interval && ts) {
+ scnprintf(ps.timestamp, sizeof(ps.timestamp), "%lu.%09lu%s",
+ (unsigned long)ts->tv_sec, ts->tv_nsec, config->csv_sep);
+ } else {
+ ps.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &csv_print_callbacks, &ps);
+ }
+}
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:56AM -0700, Ian Rogers wrote:
> This patch implements CSV output formatting callbacks inside
> util/stat-print-csv.c, replacing the empty stubs introduced in Commit 1.
>
> Defines the format-private `struct queued_event` and `struct
> queued_metric` DOM nodes to buffer traversal streams, and fully
> encapsulates CSV queued lists lifecycle and deallocations inside
> csv_print_start() and csv_print_end().
>
> Utilizes the newly centralized unified aggregation helpers to format CPU
> and thread column prefixes cleanly, fixes metrics separators padding,
> and incorporates full interval-mode timestamp printing support.
>
> Signed-off-by: Ian Rogers <irogers@google.com>
> Assisted-by: Antigravity:gemini-3.5-flash
> Acked-by: Chun-Tse Shao <ctshao@google.com>
> ---
[SNIP]
> +/**
> + * print_aggr_id_csv - Print the aggregation prefix for CSV format.
> + *
> + * Copied and adapted from stat-display.c.
> + */
> +static void print_aggr_id_csv(const struct perf_stat_config *config, FILE *output,
> + struct evsel *evsel, struct aggr_cpu_id id, int aggr_nr)
> +{
> + const char *sep = config->csv_sep;
> +
> + switch (config->aggr_mode) {
> + case AGGR_CORE:
> + fprintf(output, "S%d-D%d-C%d%s%d%s", id.socket, id.die, id.core, sep, aggr_nr, sep);
> + break;
> + case AGGR_CACHE:
> + fprintf(output, "S%d-D%d-L%d-ID%d%s%d%s", id.socket, id.die, id.cache_lvl, id.cache,
> + sep, aggr_nr, sep);
> + break;
> + case AGGR_CLUSTER:
> + fprintf(output, "S%d-D%d-CLS%d%s%d%s", id.socket, id.die, id.cluster, sep, aggr_nr,
> + sep);
> + break;
> + case AGGR_DIE:
> + fprintf(output, "S%d-D%d%s%d%s", id.socket, id.die, sep, aggr_nr, sep);
> + break;
> + case AGGR_SOCKET:
> + fprintf(output, "S%d%s%d%s", id.socket, sep, aggr_nr, sep);
> + break;
> + case AGGR_NODE:
> + fprintf(output, "N%d%s%d%s", id.node, sep, aggr_nr, sep);
> + break;
> + case AGGR_NONE:
> + if (evsel->percore && !config->percore_show_thread)
> + fprintf(output, "S%d-D%d-C%d%s", id.socket, id.die, id.core, sep);
> + else if (id.cpu.cpu > -1)
> + fprintf(output, "CPU%d%s", id.cpu.cpu, sep);
> + break;
> + case AGGR_THREAD: {
> + const char *comm = "unknown";
> + int pid = -1;
> +
> + if (evsel && evsel->core.threads && id.thread_idx >= 0 &&
> + id.thread_idx < perf_thread_map__nr(evsel->core.threads)) {
> + comm = perf_thread_map__comm(evsel->core.threads,
> + id.thread_idx);
> + pid = perf_thread_map__pid(evsel->core.threads,
> + id.thread_idx);
> + }
> + fprintf(output, "%s-%d%s", comm, pid, sep);
> + break;
> + }
Can you use the type-agnostic helper here?
> + case AGGR_GLOBAL:
> + case AGGR_UNSET:
> + case AGGR_MAX:
> + default:
> + break;
> + }
> +}
[SNIP]
> +static int csv_print_end(void *ctx, struct perf_stat_config *config)
> +{
> + struct csv_print_state *ps = ctx;
> + struct queued_event *ev, *tmp_ev;
> + struct queued_metric *met, *tmp_met;
> + FILE *output = ps->fp;
> + const char *sep = ps->sep;
> + bool has_metrics;
> +
> + list_for_each_entry_safe(ev, tmp_ev, &ps->events_list, list) {
> + struct evsel *evsel = ev->evsel;
> + bool ok = (ev->run != 0 && ev->ena != 0);
> + const char *bad_count = ev->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
> + double enabled_percent = 100;
> +
> + /* Print interval timestamp first if configured */
> + if (config->interval && ps->timestamp[0])
> + fprintf(output, "%s", ps->timestamp);
Doesn't it need a separator?
> +
> + /* Print aggregation prefix first in CSV normal mode */
> + if (config->aggr_map && ev->aggr_idx >= 0) {
> + struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
> + int aggr_nr = 0;
> +
> + if (evsel->stats && evsel->stats->aggr)
> + aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
> +
> + print_aggr_id_csv(config, output, evsel, id, aggr_nr);
> + }
> +
> + /* 1. Print Value, Unit, Name (Columns 1, 2, 3) */
> + if (ok) {
> + double sc = ev->scale;
> + double avg = ev->val * sc;
> + const char *fmt = floor(sc) != sc ? "%.2f%s" : "%.0f%s";
> +
> + fprintf(output, fmt, avg, sep);
> + } else {
> + fprintf(output, "%s%s", bad_count, sep);
> + }
>
> -int perf_stat__print_csv(struct evlist *evlist __maybe_unused,
> - const struct perf_stat_config *config __maybe_unused,
> - const struct target *target __maybe_unused,
> - const struct timespec *ts __maybe_unused,
> - int argc __maybe_unused,
> - const char **argv __maybe_unused)
> + if (ev->unit)
> + fprintf(output, "%s%s", ev->unit, sep);
> + else
> + fprintf(output, "%s", sep);
> +
> + fprintf(output, "%s", ev->name);
> +
> + /* 2. Print Runtime and Enabled Percentage (Columns 4, 5) */
> + if (ev->run != ev->ena)
> + enabled_percent = 100.0 * ev->run / ev->ena;
> + fprintf(output, "%s%" PRIu64 "%s%.2f", sep, ev->run, sep,
> + enabled_percent);
Cgroups?
Thanks,
Namhyung
> +
> + /* 3. Print Metrics (Columns 6, 7) */
> + has_metrics = false;
> + list_for_each_entry_safe(met, tmp_met, &ev->metrics_list, list) {
> + if (!has_metrics) {
> + has_metrics = true;
> + } else {
> + fprintf(output, "\n");
> + if (config->interval && ps->timestamp[0])
> + fprintf(output, "%s", ps->timestamp);
> + if (config->aggr_map && ev->aggr_idx >= 0) {
> + struct aggr_cpu_id id = config->aggr_map->map[ev->aggr_idx];
> + int aggr_nr = 0;
> +
> + if (evsel->stats && evsel->stats->aggr)
> + aggr_nr = evsel->stats->aggr[ev->aggr_idx].nr;
> +
> + print_aggr_id_csv(config, output, evsel, id, aggr_nr);
> + }
> + /*
> + * Pad exactly 4 commas (Value, Unit, Name, Run, Enabled)
> + * to line up with the Metric Value column.
> + */
> + fprintf(output, "%s%s%s%s", sep, sep, sep, sep);
> + }
> + fprintf(output, "%s%.2f%s", sep, met->val, sep);
> + if (met->name && met->name[0])
> + fprintf(output, "%s", met->name);
> +
> + list_del(&met->list);
> + free(met->name);
> + free(met->unit);
> + free(met);
> + }
> + if (!has_metrics)
> + fprintf(output, "%s%s", sep, sep);
> + fprintf(output, "\n");
> +
> + list_del(&ev->list);
> + free(ev->name);
> + free(ev->unit);
> + free(ev->cgrp);
> + free(ev);
> + }
> + return 0;
> +}
This patch extends tools/perf/tests/shell/stat+csv_output.sh to run all
basic and core aggregation linter checks a second time under the --new
CSV print flag:
- check_no_args
- check_system_wide
- check_interval
- check_event
- check_per_thread
- check_per_node
- check_system_wide_no_aggr
- check_per_core
- check_per_socket
- check_per_die
This guarantees that CSV outputs produced by the decoupled printing
engine are formally verified and column-valid across standard and
interval modes.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+csv_output.sh | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/tools/perf/tests/shell/stat+csv_output.sh b/tools/perf/tests/shell/stat+csv_output.sh
index cd6fff597091..e4153a03d716 100755
--- a/tools/perf/tests/shell/stat+csv_output.sh
+++ b/tools/perf/tests/shell/stat+csv_output.sh
@@ -88,5 +88,21 @@ then
else
echo "[Skip] Skipping tests for system_wide_no_aggr, per_core, per_die and per_socket since socket id exposed via topology is invalid"
fi
+# New API CSV checks
+perf_cmd="--new -x$csv_sep -o ${stat_output}"
+check_no_args "CSV (New API)" "$perf_cmd"
+check_system_wide "CSV (New API)" "$perf_cmd"
+check_interval "CSV (New API)" "$perf_cmd"
+check_event "CSV (New API)" "$perf_cmd"
+check_per_thread "CSV (New API)" "$perf_cmd"
+check_per_node "CSV (New API)" "$perf_cmd"
+if [ $skip_test -ne 1 ]
+then
+ check_system_wide_no_aggr "CSV (New API)" "$perf_cmd"
+ check_per_core "CSV (New API)" "$perf_cmd"
+ check_per_die "CSV (New API)" "$perf_cmd"
+ check_per_socket "CSV (New API)" "$perf_cmd"
+fi
+
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch extends tools/perf/tests/shell/stat+csv_output.sh to run the
advanced hardware PMU, topology-aware aggregation, and metric-only
checks a second time under the --new CSV print flag:
- check_per_cache_instance
- check_per_cluster
- check_metric_only
This guarantees that CSV outputs produced by the decoupled printing
callbacks are verified and column-valid under advanced aggregation modes
and metric-only CSV row-column layouts.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+csv_output.sh | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/perf/tests/shell/stat+csv_output.sh b/tools/perf/tests/shell/stat+csv_output.sh
index e4153a03d716..c48d9e2b6429 100755
--- a/tools/perf/tests/shell/stat+csv_output.sh
+++ b/tools/perf/tests/shell/stat+csv_output.sh
@@ -102,7 +102,10 @@ then
check_per_core "CSV (New API)" "$perf_cmd"
check_per_die "CSV (New API)" "$perf_cmd"
check_per_socket "CSV (New API)" "$perf_cmd"
+ check_per_cache_instance "CSV (New API)" "$perf_cmd"
+ check_per_cluster "CSV (New API)" "$perf_cmd"
fi
+check_metric_only "CSV (New API)" "$perf_cmd"
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch implements streaming JSON formatting callbacks inside
util/stat-print-json.c, replacing the empty stubs introduced in Commit
1.
Delivers a highly optimized, zero-allocation, and 100% streaming print
engine for JSON normal and metric-only modes. It bypasses dynamic queue
events and metric lists entirely, formatting and streaming JSON objects
directly onto the output file descriptor.
Utilizes the newly centralized unified aggregation helpers to format CPU
and thread keys inside the JSON objects, and incorporates full
interval-mode timestamp printing support.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/util/stat-print-json.c | 340 +++++++++++++++++++++++++++++-
1 file changed, 333 insertions(+), 7 deletions(-)
diff --git a/tools/perf/util/stat-print-json.c b/tools/perf/util/stat-print-json.c
index 4690c4286fd2..7ed00aae89f4 100644
--- a/tools/perf/util/stat-print-json.c
+++ b/tools/perf/util/stat-print-json.c
@@ -1,13 +1,339 @@
// SPDX-License-Identifier: GPL-2.0
-#include "stat-print.h"
+#include <errno.h>
+#include <inttypes.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <time.h>
+#include <stdbool.h>
#include <linux/compiler.h>
+#include <linux/kernel.h>
+
+#include "cpumap.h"
+#include "evlist.h"
+#include "evsel.h"
+#include "stat-print.h"
+#include "stat.h"
+#include "thread_map.h"
+
+static const char *metric_threshold_classify__str(enum metric_threshold_classify thresh)
+{
+ const char *const strs[] = {
+ "unknown", "bad", "nearly bad", "less good", "good",
+ };
+ _Static_assert(ARRAY_SIZE(strs) - 1 == METRIC_THRESHOLD_GOOD, "missing enum value");
+ return strs[thresh];
+}
+
+/**
+ * struct json_print_state - Print state context for JSON output.
+ * @fp: File descriptor to output to.
+ * @timestamp: Formatted interval timestamp (optional).
+ */
+struct json_print_state {
+ FILE *fp;
+ char timestamp[64];
+};
+
+/**
+ * struct json_metric_only_print_state - Metric-only print state context for JSON output.
+ * @fp: File descriptor to output to.
+ * @timestamp: Formatted interval timestamp (optional).
+ * @evlist: Evlist to query entries from.
+ * @last_aggr_idx: The aggregation index of the last printed metric.
+ * @first_in_group: Whether the current metric is the first in its group.
+ */
+struct json_metric_only_print_state {
+ FILE *fp;
+ char timestamp[64];
+ struct evlist *evlist;
+ int last_aggr_idx;
+ bool first_in_group;
+};
+
+/**
+ * print_aggr_id_json - Print the aggregation prefix for JSON format.
+ *
+ * Copied and adapted from stat-display.c.
+ */
+static void print_aggr_id_json(const struct perf_stat_config *config, FILE *output,
+ struct evsel *evsel, struct aggr_cpu_id id, int aggr_nr)
+{
+ switch (config->aggr_mode) {
+ case AGGR_CORE:
+ fprintf(output, "\"core\" : \"S%d-D%d-C%d\", \"counters\" : %d, ", id.socket,
+ id.die, id.core, aggr_nr);
+ break;
+ case AGGR_CACHE:
+ fprintf(output, "\"cache\" : \"S%d-D%d-L%d-ID%d\", \"counters\" : %d, ", id.socket,
+ id.die, id.cache_lvl, id.cache, aggr_nr);
+ break;
+ case AGGR_CLUSTER:
+ fprintf(output, "\"cluster\" : \"S%d-D%d-CLS%d\", \"counters\" : %d, ", id.socket,
+ id.die, id.cluster, aggr_nr);
+ break;
+ case AGGR_DIE:
+ fprintf(output, "\"die\" : \"S%d-D%d\", \"counters\" : %d, ", id.socket, id.die,
+ aggr_nr);
+ break;
+ case AGGR_SOCKET:
+ fprintf(output, "\"socket\" : \"S%d\", \"counters\" : %d, ", id.socket, aggr_nr);
+ break;
+ case AGGR_NODE:
+ fprintf(output, "\"node\" : \"N%d\", \"counters\" : %d, ", id.node, aggr_nr);
+ break;
+ case AGGR_NONE:
+ if (evsel->percore && !config->percore_show_thread)
+ fprintf(output, "\"core\" : \"S%d-D%d-C%d\", ", id.socket, id.die, id.core);
+ else if (id.cpu.cpu > -1)
+ fprintf(output, "\"cpu\" : \"%d\", ", id.cpu.cpu);
+ break;
+ case AGGR_THREAD:
+ fprintf(output, "\"thread\" : \"%s-%d\", ",
+ perf_thread_map__comm(evsel->core.threads, id.thread_idx),
+ perf_thread_map__pid(evsel->core.threads, id.thread_idx));
+ break;
+ case AGGR_GLOBAL:
+ case AGGR_UNSET:
+ case AGGR_MAX:
+ default:
+ break;
+ }
+}
+
+/*
+ * JSON Output Callbacks - Normal Mode (100% Streaming & Zero-Allocation)
+ */
+
+static int json_print_start(void *ctx __maybe_unused,
+ struct perf_stat_config *config __maybe_unused)
+{
+ return 0;
+}
+
+static int json_print_event(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx, u64 val,
+ u64 ena, u64 run, double stdev_pct __maybe_unused,
+ const char *cgrp __maybe_unused)
+{
+ struct json_print_state *ps = ctx;
+ FILE *output = config->output;
+ bool ok = (run != 0 && ena != 0);
+ double enabled_percent = 100.0;
+
+ fprintf(output, "{");
+
+ /* Print interval timestamp first if configured */
+ if (config->interval && ps && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+
+ /* Print aggregation JSON fields if configured */
+ if (config->aggr_map && aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[aggr_idx].nr;
+
+ print_aggr_id_json(config, output, evsel, id, aggr_nr);
+ }
+
+ if (ok) {
+ double sc = evsel->scale;
+ double avg = val * sc;
+
+ fprintf(output, "\"counter-value\" : \"%f\"", avg);
+ } else {
+ const char *bad_count = evsel->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
+
+ fprintf(output, "\"counter-value\" : \"%s\"", bad_count);
+ }
+
+ fprintf(output, ", \"unit\" : \"%s\"", evsel->unit ?: "");
+ /* Cast away const for legacy evsel__name */
+ fprintf(output, ", \"event\" : \"%s\"", evsel__name((struct evsel *)evsel));
+
+ if (run != ena)
+ enabled_percent = 100.0 * run / ena;
+ fprintf(output, ", \"event-runtime\" : %" PRIu64 ", \"pcnt-running\" : %.2f", run,
+ enabled_percent);
+ fprintf(output, "}\n");
+
+ return 0;
+}
+
+static int json_print_metric(void *ctx, struct perf_stat_config *config,
+ struct evsel *evsel, int aggr_idx,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh)
+{
+ struct json_print_state *ps = ctx;
+ FILE *output = config->output;
+ u64 run = 0, ena = 0;
+ double enabled_percent = 100.0;
+ struct perf_stat_evsel *ps_evsel = evsel->stats;
+
+ if (ps_evsel && ps_evsel->aggr && aggr_idx >= 0 &&
+ aggr_idx < ps_evsel->nr_aggr) {
+ run = ps_evsel->aggr[aggr_idx].counts.run;
+ ena = ps_evsel->aggr[aggr_idx].counts.ena;
+ }
-int perf_stat__print_json(struct evlist *evlist __maybe_unused,
- const struct perf_stat_config *config __maybe_unused,
- const struct target *target __maybe_unused,
- const struct timespec *ts __maybe_unused,
- int argc __maybe_unused,
- const char **argv __maybe_unused)
+ fprintf(output, "{");
+
+ /* Print interval timestamp first if configured */
+ if (config->interval && ps && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+
+ /* Print aggregation JSON fields if configured */
+ if (config->aggr_map && aggr_idx >= 0) {
+ struct aggr_cpu_id id = config->aggr_map->map[aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel->stats && evsel->stats->aggr)
+ aggr_nr = evsel->stats->aggr[aggr_idx].nr;
+
+ print_aggr_id_json(config, output, evsel, id, aggr_nr);
+ }
+
+ if (run != ena)
+ enabled_percent = 100.0 * run / ena;
+ fprintf(output, "\"event-runtime\" : %" PRIu64 ", \"pcnt-running\" : %.2f", run,
+ enabled_percent);
+ fprintf(output, ", \"metric-value\" : \"%f\"", val);
+ if (unit && unit[0] && name && name[0])
+ fprintf(output, ", \"metric-unit\" : \"%s %s\"", unit, name);
+ else if (name && name[0])
+ fprintf(output, ", \"metric-unit\" : \"%s\"", name);
+ else if (unit && unit[0])
+ fprintf(output, ", \"metric-unit\" : \"%s\"", unit);
+ if (thresh != METRIC_THRESHOLD_UNKNOWN) {
+ fprintf(output, ", \"metric-threshold\" : \"%s\"",
+ metric_threshold_classify__str(thresh));
+ }
+ fprintf(output, "}\n");
+
+ return 0;
+}
+
+static int json_print_end(void *ctx __maybe_unused,
+ struct perf_stat_config *config __maybe_unused)
{
return 0;
}
+
+static const struct perf_stat_print_callbacks json_print_callbacks = {
+ .print_start = json_print_start,
+ .print_end = json_print_end,
+ .print_event = json_print_event,
+ .print_metric = json_print_metric,
+};
+
+/*
+ * JSON Output Callbacks - Metric-Only Mode (100% Streaming & Zero-Allocation)
+ */
+
+static int json_metric_only_print_start(void *ctx,
+ struct perf_stat_config *config __maybe_unused)
+{
+ struct json_metric_only_print_state *ps = ctx;
+
+ /* Initialize to -2 to distinguish from -1 (a valid index in AGGR_GLOBAL mode) */
+ ps->last_aggr_idx = -2;
+ ps->first_in_group = true;
+ return 0;
+}
+
+static int json_metric_only_print_metric(void *ctx,
+ struct perf_stat_config *config __maybe_unused,
+ struct evsel *evsel, int aggr_idx,
+ const char *name, const char *unit, double val,
+ enum metric_threshold_classify thresh __maybe_unused)
+{
+ struct json_metric_only_print_state *ps = ctx;
+ FILE *output = ps->fp;
+
+
+ if (aggr_idx != ps->last_aggr_idx) {
+ if (ps->last_aggr_idx != -2)
+ fprintf(output, "}\n");
+ fprintf(output, "{");
+ if (config->interval && ps->timestamp[0])
+ fprintf(output, "%s", ps->timestamp);
+ if (config->aggr_map && aggr_idx >= 0 &&
+ aggr_idx < config->aggr_map->nr) {
+ struct aggr_cpu_id id = config->aggr_map->map[aggr_idx];
+ int aggr_nr = 0;
+
+ if (evsel && evsel->stats && evsel->stats->aggr &&
+ aggr_idx < evsel->stats->nr_aggr)
+ aggr_nr = evsel->stats->aggr[aggr_idx].nr;
+
+ print_aggr_id_json(config, output, evsel, id, aggr_nr);
+ }
+ ps->last_aggr_idx = aggr_idx;
+ ps->first_in_group = true;
+ }
+
+ if (!ps->first_in_group)
+ fprintf(output, ", ");
+ ps->first_in_group = false;
+
+ if (unit && unit[0])
+ fprintf(output, "\"%s %s\" : \"%.1f\"", unit, name, val);
+ else
+ fprintf(output, "\"%s\" : \"%.1f\"", name, val);
+ return 0;
+}
+
+static int json_metric_only_print_end(void *ctx,
+ struct perf_stat_config *config __maybe_unused)
+{
+ struct json_metric_only_print_state *ps = ctx;
+ FILE *output = ps->fp;
+
+ if (ps->last_aggr_idx != -2)
+ fprintf(output, "}\n");
+ return 0;
+}
+
+static const struct perf_stat_print_callbacks json_metric_only_print_callbacks = {
+ .print_start = json_metric_only_print_start,
+ .print_end = json_metric_only_print_end,
+ .print_event = NULL,
+ .print_metric = json_metric_only_print_metric,
+};
+
+int perf_stat__print_json(struct evlist *evlist, struct perf_stat_config *config,
+ const struct target *target, const struct timespec *ts,
+ int argc, const char **argv)
+{
+ if (config->metric_only) {
+ struct json_metric_only_print_state ps = {
+ .fp = config->output,
+ .evlist = evlist,
+ };
+ if (config->interval && ts) {
+ scnprintf(ps.timestamp, sizeof(ps.timestamp), "\"interval\" : %lu.%09lu, ",
+ (unsigned long)ts->tv_sec, ts->tv_nsec);
+ } else {
+ ps.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &json_metric_only_print_callbacks, &ps);
+ } else {
+ struct json_print_state ps = {
+ .fp = config->output,
+ };
+ if (config->interval && ts) {
+ scnprintf(ps.timestamp, sizeof(ps.timestamp), "\"interval\" : %lu.%09lu, ",
+ (unsigned long)ts->tv_sec, ts->tv_nsec);
+ } else {
+ ps.timestamp[0] = '\0';
+ }
+ return perf_stat__print_cb(evlist, config, target, ts, argc, argv,
+ &json_print_callbacks, &ps);
+ }
+}
--
2.55.0.141.g00534a21ce-goog
On Thu, Jul 16, 2026 at 12:02:59AM -0700, Ian Rogers wrote:
> This patch implements streaming JSON formatting callbacks inside
> util/stat-print-json.c, replacing the empty stubs introduced in Commit
> 1.
>
> Delivers a highly optimized, zero-allocation, and 100% streaming print
> engine for JSON normal and metric-only modes. It bypasses dynamic queue
> events and metric lists entirely, formatting and streaming JSON objects
> directly onto the output file descriptor.
>
> Utilizes the newly centralized unified aggregation helpers to format CPU
> and thread keys inside the JSON objects, and incorporates full
> interval-mode timestamp printing support.
>
> Signed-off-by: Ian Rogers <irogers@google.com>
> Assisted-by: Antigravity:gemini-3.5-flash
> Acked-by: Chun-Tse Shao <ctshao@google.com>
> ---
[SNIP]
> +/**
> + * print_aggr_id_json - Print the aggregation prefix for JSON format.
> + *
> + * Copied and adapted from stat-display.c.
> + */
> +static void print_aggr_id_json(const struct perf_stat_config *config, FILE *output,
> + struct evsel *evsel, struct aggr_cpu_id id, int aggr_nr)
> +{
> + switch (config->aggr_mode) {
> + case AGGR_CORE:
> + fprintf(output, "\"core\" : \"S%d-D%d-C%d\", \"counters\" : %d, ", id.socket,
> + id.die, id.core, aggr_nr);
> + break;
> + case AGGR_CACHE:
> + fprintf(output, "\"cache\" : \"S%d-D%d-L%d-ID%d\", \"counters\" : %d, ", id.socket,
> + id.die, id.cache_lvl, id.cache, aggr_nr);
> + break;
> + case AGGR_CLUSTER:
> + fprintf(output, "\"cluster\" : \"S%d-D%d-CLS%d\", \"counters\" : %d, ", id.socket,
> + id.die, id.cluster, aggr_nr);
> + break;
> + case AGGR_DIE:
> + fprintf(output, "\"die\" : \"S%d-D%d\", \"counters\" : %d, ", id.socket, id.die,
> + aggr_nr);
> + break;
> + case AGGR_SOCKET:
> + fprintf(output, "\"socket\" : \"S%d\", \"counters\" : %d, ", id.socket, aggr_nr);
> + break;
> + case AGGR_NODE:
> + fprintf(output, "\"node\" : \"N%d\", \"counters\" : %d, ", id.node, aggr_nr);
> + break;
> + case AGGR_NONE:
> + if (evsel->percore && !config->percore_show_thread)
> + fprintf(output, "\"core\" : \"S%d-D%d-C%d\", ", id.socket, id.die, id.core);
> + else if (id.cpu.cpu > -1)
> + fprintf(output, "\"cpu\" : \"%d\", ", id.cpu.cpu);
> + break;
> + case AGGR_THREAD:
> + fprintf(output, "\"thread\" : \"%s-%d\", ",
> + perf_thread_map__comm(evsel->core.threads, id.thread_idx),
> + perf_thread_map__pid(evsel->core.threads, id.thread_idx));
> + break;
> + case AGGR_GLOBAL:
> + case AGGR_UNSET:
> + case AGGR_MAX:
> + default:
> + break;
> + }
It'd be great if you can use the format-agnostic helpers.
> +}
> +
> +/*
> + * JSON Output Callbacks - Normal Mode (100% Streaming & Zero-Allocation)
> + */
> +
> +static int json_print_start(void *ctx __maybe_unused,
> + struct perf_stat_config *config __maybe_unused)
> +{
> + return 0;
> +}
> +
> +static int json_print_event(void *ctx, struct perf_stat_config *config,
> + struct evsel *evsel, int aggr_idx, u64 val,
> + u64 ena, u64 run, double stdev_pct __maybe_unused,
> + const char *cgrp __maybe_unused)
> +{
> + struct json_print_state *ps = ctx;
> + FILE *output = config->output;
> + bool ok = (run != 0 && ena != 0);
> + double enabled_percent = 100.0;
> +
> + fprintf(output, "{");
> +
> + /* Print interval timestamp first if configured */
> + if (config->interval && ps && ps->timestamp[0])
> + fprintf(output, "%s", ps->timestamp);
"Interval" : ?
> +
> + /* Print aggregation JSON fields if configured */
> + if (config->aggr_map && aggr_idx >= 0) {
> + struct aggr_cpu_id id = config->aggr_map->map[aggr_idx];
> + int aggr_nr = 0;
> +
> + if (evsel->stats && evsel->stats->aggr)
> + aggr_nr = evsel->stats->aggr[aggr_idx].nr;
> +
> + print_aggr_id_json(config, output, evsel, id, aggr_nr);
> + }
> +
> + if (ok) {
> + double sc = evsel->scale;
> + double avg = val * sc;
> +
> + fprintf(output, "\"counter-value\" : \"%f\"", avg);
> + } else {
> + const char *bad_count = evsel->supported ? CNTR_NOT_COUNTED : CNTR_NOT_SUPPORTED;
> +
> + fprintf(output, "\"counter-value\" : \"%s\"", bad_count);
> + }
> +
> + fprintf(output, ", \"unit\" : \"%s\"", evsel->unit ?: "");
> + /* Cast away const for legacy evsel__name */
> + fprintf(output, ", \"event\" : \"%s\"", evsel__name((struct evsel *)evsel));
Cgroups?
> +
> + if (run != ena)
> + enabled_percent = 100.0 * run / ena;
> + fprintf(output, ", \"event-runtime\" : %" PRIu64 ", \"pcnt-running\" : %.2f", run,
> + enabled_percent);
> + fprintf(output, "}\n");
Now metrics are printed in a separate object/line?
Thanks,
Namhyung
> +
> + return 0;
> +}
This patch extends tools/perf/tests/shell/stat+json_output.sh to run all
basic and standard CPU/thread aggregation checks under the --new JSON
print flag:
- check_no_args
- check_system_wide
- check_interval
- check_event
- check_per_thread
- check_per_node
- check_system_wide_no_aggr
- check_per_core
- check_per_socket
- check_per_die
This guarantees that JSON outputs produced by the decoupled,
zero-allocation, and streaming print callbacks are formally validated
across standard and interval modes.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+json_output.sh | 71 ++++++++++++++--------
1 file changed, 45 insertions(+), 26 deletions(-)
diff --git a/tools/perf/tests/shell/stat+json_output.sh b/tools/perf/tests/shell/stat+json_output.sh
index 85d1ad7186c6..8168b861ade7 100755
--- a/tools/perf/tests/shell/stat+json_output.sh
+++ b/tools/perf/tests/shell/stat+json_output.sh
@@ -13,6 +13,7 @@ shelldir=$(dirname "$0")
. "${shelldir}"/lib/setup_python.sh
pythonchecker=$(dirname $0)/lib/perf_json_output_lint.py
+perf_new_opt=""
stat_output=$(mktemp /tmp/__perf_test.stat_output.json.XXXXX)
cleanup() {
@@ -35,42 +36,42 @@ function ParanoidAndNotRoot()
check_no_args()
{
- echo -n "Checking json output: no args "
- perf stat -j -o "${stat_output}" true
+ echo -n "Checking $api_label: no args "
+ perf stat -j $perf_new_opt -o "${stat_output}" true
$PYTHON $pythonchecker --no-args --file "${stat_output}"
echo "[Success]"
}
check_system_wide()
{
- echo -n "Checking json output: system wide "
+ echo -n "Checking $api_label: system wide "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j -a -o "${stat_output}" true
+ perf stat -j $perf_new_opt -a -o "${stat_output}" true
$PYTHON $pythonchecker --system-wide --file "${stat_output}"
echo "[Success]"
}
check_system_wide_no_aggr()
{
- echo -n "Checking json output: system wide no aggregation "
+ echo -n "Checking $api_label: system wide no aggregation "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j -A -a --no-merge -o "${stat_output}" true
+ perf stat -j $perf_new_opt -A -a --no-merge -o "${stat_output}" true
$PYTHON $pythonchecker --system-wide-no-aggr --file "${stat_output}"
echo "[Success]"
}
check_interval()
{
- echo -n "Checking json output: interval "
- perf stat -j -I 1000 -o "${stat_output}" true
+ echo -n "Checking $api_label: interval "
+ perf stat -j $perf_new_opt -I 1000 -o "${stat_output}" true
$PYTHON $pythonchecker --interval --file "${stat_output}"
echo "[Success]"
}
@@ -78,110 +79,110 @@ check_interval()
check_event()
{
- echo -n "Checking json output: event "
- perf stat -j -e cpu-clock -o "${stat_output}" true
+ echo -n "Checking $api_label: event "
+ perf stat -j $perf_new_opt -e cpu-clock -o "${stat_output}" true
$PYTHON $pythonchecker --event --file "${stat_output}"
echo "[Success]"
}
check_per_core()
{
- echo -n "Checking json output: per core "
+ echo -n "Checking $api_label: per core "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-core -a -o "${stat_output}" true
+ perf stat -j $perf_new_opt --per-core -a -o "${stat_output}" true
$PYTHON $pythonchecker --per-core --file "${stat_output}"
echo "[Success]"
}
check_per_thread()
{
- echo -n "Checking json output: per thread "
+ echo -n "Checking $api_label: per thread "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-thread -p $$ -o "${stat_output}" true
+ perf stat -j $perf_new_opt --per-thread -p $$ -o "${stat_output}" true
$PYTHON $pythonchecker --per-thread --file "${stat_output}"
echo "[Success]"
}
check_per_cache_instance()
{
- echo -n "Checking json output: per cache_instance "
+ echo -n "Checking $api_label: per cache_instance "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-cache -a true 2>&1 | $PYTHON $pythonchecker --per-cache
+ perf stat -j $perf_new_opt --per-cache -a true 2>&1 | $PYTHON $pythonchecker --per-cache
echo "[Success]"
}
check_per_cluster()
{
- echo -n "Checking json output: per cluster "
+ echo -n "Checking $api_label: per cluster "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-cluster -a true 2>&1 | $PYTHON $pythonchecker --per-cluster
+ perf stat -j $perf_new_opt --per-cluster -a true 2>&1 | $PYTHON $pythonchecker --per-cluster
echo "[Success]"
}
check_per_die()
{
- echo -n "Checking json output: per die "
+ echo -n "Checking $api_label: per die "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-die -a -o "${stat_output}" true
+ perf stat -j $perf_new_opt --per-die -a -o "${stat_output}" true
$PYTHON $pythonchecker --per-die --file "${stat_output}"
echo "[Success]"
}
check_per_node()
{
- echo -n "Checking json output: per node "
+ echo -n "Checking $api_label: per node "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-node -a -o "${stat_output}" true
+ perf stat -j $perf_new_opt --per-node -a -o "${stat_output}" true
$PYTHON $pythonchecker --per-node --file "${stat_output}"
echo "[Success]"
}
check_per_socket()
{
- echo -n "Checking json output: per socket "
+ echo -n "Checking $api_label: per socket "
if ParanoidAndNotRoot 0
then
echo "[Skip] paranoia and not root"
return
fi
- perf stat -j --per-socket -a -o "${stat_output}" true
+ perf stat -j $perf_new_opt --per-socket -a -o "${stat_output}" true
$PYTHON $pythonchecker --per-socket --file "${stat_output}"
echo "[Success]"
}
check_metric_only()
{
- echo -n "Checking json output: metric only "
+ echo -n "Checking $api_label: metric only "
if [ "$(uname -m)" = "s390x" ] && ! grep '^facilities' /proc/cpuinfo | grep -qw 67
then
echo "[Skip] CPU-measurement counter facility not installed"
return
fi
- perf stat -j --metric-only -M page_faults_per_second -o "${stat_output}" true
+ perf stat -j $perf_new_opt --metric-only -M page_faults_per_second -o "${stat_output}" true
$PYTHON $pythonchecker --metric-only --file "${stat_output}"
echo "[Success]"
}
@@ -214,6 +215,7 @@ check_for_topology()
}
check_for_topology
+api_label="json output"
check_no_args
check_system_wide
check_interval
@@ -232,5 +234,22 @@ then
else
echo "[Skip] Skipping tests for system_wide_no_aggr, per_core, per_die and per_socket since socket id exposed via topology is invalid"
fi
+# Run New API JSON basic and standard aggregation checks
+perf_new_opt="--new"
+api_label="json (New API)"
+check_no_args
+check_system_wide
+check_interval
+check_event
+check_per_thread
+check_per_node
+if [ $skip_test -ne 1 ]
+then
+ check_system_wide_no_aggr
+ check_per_core
+ check_per_die
+ check_per_socket
+fi
+
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch extends tools/perf/tests/shell/stat+json_output.sh to run the
advanced hardware PMU, topology-aware aggregation, and metric-only JSON
checks a second time under the --new print flag:
- check_per_cache_instance
- check_per_cluster
- check_metric_only
This guarantees that JSON outputs produced by the decoupled streaming
printing callbacks are verified and structurally valid under advanced
topology-aware modes and metric-only JSON row-column layouts.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat+json_output.sh | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tools/perf/tests/shell/stat+json_output.sh b/tools/perf/tests/shell/stat+json_output.sh
index 8168b861ade7..d18cac5dcff1 100755
--- a/tools/perf/tests/shell/stat+json_output.sh
+++ b/tools/perf/tests/shell/stat+json_output.sh
@@ -249,7 +249,10 @@ then
check_per_core
check_per_die
check_per_socket
+ check_per_cache_instance
+ check_per_cluster
fi
+check_metric_only
cleanup
exit 0
--
2.55.0.141.g00534a21ce-goog
This patch extends the performance metrics value Python validation
script tools/perf/tests/shell/lib/perf_metric_validation.py to support a
new command-line argument `-new`:
parser.add_argument("-new", help="Use new printing API (--new)", ...)
When set, the Validator class appends the `--new` option flag to its
internally spawned `perf stat` commands:
command = [tool, 'stat']
if self.new_print:
command.append('--new')
This enables validating Intel PMU metric mathematical values generated
specifically by the decoupled, streaming JSON printing callbacks.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/lib/perf_metric_validation.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/tools/perf/tests/shell/lib/perf_metric_validation.py b/tools/perf/tests/shell/lib/perf_metric_validation.py
index 3d52f94f22b9..b8397234eebc 100644
--- a/tools/perf/tests/shell/lib/perf_metric_validation.py
+++ b/tools/perf/tests/shell/lib/perf_metric_validation.py
@@ -36,7 +36,7 @@ class TestError:
class Validator:
def __init__(self, rulefname, reportfname='', t=5, debug=False, datafname='', fullrulefname='',
- workload='true', metrics='', cputype='cpu'):
+ workload='true', metrics='', cputype='cpu', new_print=False):
self.rulefname = rulefname
self.reportfname = reportfname
self.rules = None
@@ -68,6 +68,7 @@ class Validator:
self.datafname = datafname
self.debug = debug
self.fullrulefname = fullrulefname
+ self.new_print = new_print
def __set_metrics(self, metrics=''):
if metrics != '':
@@ -379,7 +380,10 @@ class Validator:
def _run_perf(self, metric, workload: str):
tool = 'perf'
- command = [tool, 'stat', '--cputype', self.cputype, '-j', '-M', f"{metric}", "-a"]
+ command = [tool, 'stat']
+ if self.new_print:
+ command.append('--new')
+ command.extend(['--cputype', self.cputype, '-j', '-M', f"{metric}", "-a"])
wl = workload.split()
command.extend(wl)
print(" ".join(command))
@@ -587,6 +591,8 @@ def main() -> None:
parser.add_argument("-m", help="Metric list to validate", default="")
parser.add_argument("-cputype", help="Only test metrics for the given CPU/PMU type",
default="cpu")
+ parser.add_argument("-new", help="Use new printing API (--new)",
+ action="store_true", default=False)
args = parser.parse_args()
outpath = Path(args.output_dir)
reportf = Path.joinpath(outpath, 'perf_report.json')
@@ -595,7 +601,7 @@ def main() -> None:
validator = Validator(args.rule, reportf, debug=args.debug,
datafname=datafile, fullrulefname=fullrule, workload=args.wl,
- metrics=args.m, cputype=args.cputype)
+ metrics=args.m, cputype=args.cputype, new_print=args.new)
ret = validator.test()
return ret
--
2.55.0.141.g00534a21ce-goog
This patch updates tools/perf/tests/shell/stat_metrics_values.sh to run
the metrics value validation linter twice: once for the legacy path, and
once under the --new print flag using the newly introduced python
validator `-new` argument:
$PYTHON $pythonvalidator -rule $rulefile -output_dir $tmpdir ... -new
This mathematically guarantees that calculated Intel PMU metric values
staged inside the decoupled streaming JSON callbacks are parsed and
validated cleanly against the formal rules engine.
Signed-off-by: Ian Rogers <irogers@google.com>
Assisted-by: Antigravity:gemini-3.5-flash
Acked-by: Chun-Tse Shao <ctshao@google.com>
---
tools/perf/tests/shell/stat_metrics_values.sh | 22 ++++++++++++++++---
1 file changed, 19 insertions(+), 3 deletions(-)
diff --git a/tools/perf/tests/shell/stat_metrics_values.sh b/tools/perf/tests/shell/stat_metrics_values.sh
index 86c5c7f70933..3f545e88b69f 100755
--- a/tools/perf/tests/shell/stat_metrics_values.sh
+++ b/tools/perf/tests/shell/stat_metrics_values.sh
@@ -26,13 +26,29 @@ echo "Output will be stored in: $tmpdir"
for cputype in /sys/bus/event_source/devices/cpu_*; do
cputype=$(basename "$cputype")
echo "Testing metrics for: $cputype"
- $PYTHON $pythonvalidator -rule $rulefile -output_dir $tmpdir -wl "${workload}" \
+ mkdir -p "$tmpdir/legacy"
+ $PYTHON $pythonvalidator -rule $rulefile -output_dir "$tmpdir/legacy" -wl "${workload}" \
-cputype "${cputype}"
ret=$?
- rm -rf $tmpdir
if [ $ret -ne 0 ]; then
- echo "Metric validation return with errors. Please check metrics reported with errors."
+ echo "Metric validation return with errors. " \
+ "Please check metrics reported with errors in: " \
+ "$tmpdir/legacy"
+ exit $ret
+ fi
+
+ echo "Testing metrics for: $cputype (New API)"
+ mkdir -p "$tmpdir/new"
+ $PYTHON $pythonvalidator -rule $rulefile -output_dir "$tmpdir/new" -wl "${workload}" \
+ -cputype "${cputype}" -new
+ ret=$?
+ if [ $ret -ne 0 ]; then
+ echo "Metric validation return with errors (New API). " \
+ "Please check metrics reported with errors in: " \
+ "$tmpdir/new"
+ exit $ret
fi
done
+rm -rf "$tmpdir"
exit $ret
--
2.55.0.141.g00534a21ce-goog
© 2016 - 2026 Red Hat, Inc.