mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: Ian Rogers <irogers@google.com>
To: Peter Zijlstra <peterz@infradead.org>,
	Ingo Molnar <mingo@redhat.com>,
	 Arnaldo Carvalho de Melo <acme@kernel.org>,
	Namhyung Kim <namhyung@kernel.org>
Cc: Jiri Olsa <jolsa@kernel.org>,
	Adrian Hunter <adrian.hunter@intel.com>,
	 James Clark <james.clark@linaro.org>,
	linux-perf-users@vger.kernel.org,  linux-kernel@vger.kernel.org,
	Ian Rogers <irogers@google.com>
Subject: [PATCH 08/11] perf list, jevents: Add --export-dir and --output-dir sysfs-style exporters
Date: Fri, 13 Nov 2026 13:10:59 -0800	[thread overview]
Message-ID: <20261113211105.3118281-9-irogers@google.com> (raw)
In-Reply-To: <20261113211105.3118281-1-irogers@google.com>

Add two complementary sysfs-style directory exporters so inbuilt or JSON
PMU events, metrics, and metricgroup descriptions can be materialized on
disk, inspected or edited, and layered back via pmu-events.dir:

1. perf list --export-dir <dir> (host-direct layout):
   Implement a struct print_callbacks backend in builtin-list.c backed
   by pmu_layer__export_{event,metric,metricgroup}() in pmu-layer.c
   (keeping struct perf_pmu_alias and struct perf_pmu_metric private) to
   materialize the running machine's active PMUs directly under <dir>/:

     <dir>/
     |-- cpu/
     |   |-- events/inst_retired.any{,.desc,.topic,...}
     |   `-- metrics/cpi{,.desc,.group,.scale-unit,...}
     |-- tool/events/duration_time{,.desc,.unit,...}
     `-- metricgroups/TopdownL1.desc

   This layout targets quick host-local inspection and in-place overrides
   and automatically inherits perf list filters (--unit, --cputype,
   --deprecated, and event/metric globs).

2. jevents.py --output-dir <dir> (portable CPUID/sys symlink layout):
   Materialize tools/perf/pmu-events/arch/ JSON trees into canonical
   model directories under <dir>/models/<arch>/<model>/ and expand
   arch/*/mapfile.csv CPUID patterns and literal uncore Compat IDs into
   relative POSIX symlinks under <dir>/cpus/<cpuid> and <dir>/sys/<id>,
   plus cross-CPU arch/common definitions at the top level:

     <dir>/
     |-- cpus/
     |   |-- GenuineIntel-6-4E -> ../models/x86/skylake
     |   |-- GenuineIntel-6-5E -> ../models/x86/skylake
     |   |-- 0x00000000410fd490 -> ../models/arm64/arm/neoverse-n2
     |   `-- 0x489-0x8000000000000007 -> ../models/riscv/sifive/u74
     |-- sys/
     |   `-- 43401 -> ../models/arm64/arm/cmn
     |-- models/
     |   |-- x86/skylake/{cpu,uncore_imc,...}/{events,metrics}/...
     |   `-- arm64/arm/neoverse-n2/armv8_pmuv3/{events,metrics}/...
     |-- default_core/events/...
     `-- metricgroups/<group>.desc

   This produces a self-contained multi-architecture directory tree with
   no runtime mapfile.csv or regex matching required.

Signed-off-by: Ian Rogers <irogers@google.com>
---
 tools/perf/Documentation/perf-config.txt |   8 +
 tools/perf/Documentation/perf-list.txt   |  31 +-
 tools/perf/builtin-list.c                | 212 +++++++++-
 tools/perf/pmu-events/README             |  19 +
 tools/perf/pmu-events/jevents.py         | 375 ++++++++++++++++--
 tools/perf/util/pmu-layer.c              | 472 +++++++++++++++++++++++
 tools/perf/util/pmu-layer.h              |  11 +
 tools/perf/util/print-events.c           |  29 +-
 8 files changed, 1109 insertions(+), 48 deletions(-)

diff --git a/tools/perf/Documentation/perf-config.txt b/tools/perf/Documentation/perf-config.txt
index e81c33717688..95181ebfca23 100644
--- a/tools/perf/Documentation/perf-config.txt
+++ b/tools/perf/Documentation/perf-config.txt
@@ -300,6 +300,14 @@ For example, to layer local overrides ahead of a shared directory in
 		$ printf 'instructions / cpu-cycles\n' > ~/.perf-events/local/default_core/metrics/my_ipc
 		$ printf 'Custom instructions per cycle\n' > ~/.perf-events/local/default_core/metrics/my_ipc.desc
 		$ perf stat -M my_ipc true
++
+Overlay directories can also be populated from the current system using
+`perf list --export-dir` (see linkperf:perf-list[1]) or from JSON event trees
+using `jevents.py --output-dir`:
+
+		$ perf list --export-dir ~/.perf-events/local metric metricgroup
+		$ python3 tools/perf/pmu-events/jevents.py --output-dir /opt/pmu-events x86 all
+		$ perf config pmu-events.dir=~/.perf-events/local:/opt/pmu-events
 
 annotate.*::
 	These are in control of addresses, jump function, source code
diff --git a/tools/perf/Documentation/perf-list.txt b/tools/perf/Documentation/perf-list.txt
index 17e582998474..dbad98da3899 100644
--- a/tools/perf/Documentation/perf-list.txt
+++ b/tools/perf/Documentation/perf-list.txt
@@ -52,6 +52,14 @@ Output in JSON format.
 --output=::
 	Output file name. By default output is written to stdout.
 
+--export-dir=::
+	Materialize matching PMU events (`<dir>/<pmu>/events/`), metrics
+	(`<dir>/<pmu>/metrics/`), and metricgroup descriptions
+	(`<dir>/metricgroups/`) into a sysfs-style directory tree suitable for
+	layering via `pmu-events.dir` in linkperf:perf-config[1], for example
+	`perf list --export-dir ~/.perf-events metric metricgroup`. See
+	<<EXPORTING_EVENTS, EXPORTING AND OVERLAYING EVENTS>>.
+
 --tui::
 Rather than printing the events, interactively browse the PMUs, events and
 metrics in the terminal using the 'ilist' python script (see
@@ -364,6 +372,27 @@ other events in the group.
 However, in the case AUX area events (e.g. Intel PT or CoreSight), the AUX
 area event must be the leader, so then the second event samples, not the first.
 
+[[EXPORTING_EVENTS]]
+EXPORTING AND OVERLAYING EVENTS
+-------------------------------
+
+Matching PMU events, metrics, and metricgroups on the current system can be
+materialized as a sysfs-style directory tree using '--export-dir', edited or
+extended in place, and layered back over the inbuilt tables and sysfs via
+'pmu-events.dir' (see linkperf:perf-config[1]):
+
+  perf list --export-dir ~/.perf-events sw metric metricgroup
+  printf 'Custom wall-clock duration\n' > ~/.perf-events/tool/events/duration_time.desc
+  perf config pmu-events.dir=~/.perf-events
+  perf list sw
+
+Architecture JSON definitions from the kernel source tree can also be exported
+directly into a self-contained directory tree (with literal 'cpus/<cpuid>' and
+'sys/<id>' symlinks and no 'mapfile.csv') using 'jevents.py --output-dir':
+
+  python3 tools/perf/pmu-events/jevents.py --output-dir /opt/pmu-events x86 all
+  perf config pmu-events.dir=~/.perf-events:/opt/pmu-events
+
 OPTIONS
 -------
 
@@ -408,6 +437,6 @@ include::intel-acr.txt[]
 SEE ALSO
 --------
 linkperf:perf-stat[1], linkperf:perf-top[1],
-linkperf:perf-record[1],
+linkperf:perf-record[1], linkperf:perf-config[1],
 http://www.intel.com/sdm/[Intel® 64 and IA-32 Architectures Software Developer's Manual Volume 3B: System Programming Guide],
 https://bugzilla.kernel.org/show_bug.cgi?id=206537[AMD Processor Programming Reference (PPR)]
diff --git a/tools/perf/builtin-list.c b/tools/perf/builtin-list.c
index 53e1834233c8..85de0bd73e32 100644
--- a/tools/perf/builtin-list.c
+++ b/tools/perf/builtin-list.c
@@ -13,6 +13,7 @@
 #include "util/print-events.h"
 #include "util/pmus.h"
 #include "util/pmu.h"
+#include "util/pmu-layer.h"
 #include "util/debug.h"
 #include "util/metricgroup.h"
 #include "util/pfm.h"
@@ -25,9 +26,11 @@
 #include <linux/zalloc.h>
 #include <ctype.h>
 #include <errno.h>
+#include <limits.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 
 /**
  * struct print_state - State and configuration passed to the default_print
@@ -540,6 +543,141 @@ static bool default_skip_duplicate_pmus(void *ps)
 	return !print_state->long_desc;
 }
 
+struct export_dir_print_state {
+	struct print_state common;
+	const char *root_dir;
+	struct strlist *visited_groups;
+	bool failed;
+};
+
+static void export_dir_print_start(void *ps)
+{
+	struct export_dir_print_state *eps = ps;
+	int ret = pmu_layer__mkdir_p(eps->root_dir);
+
+	if (ret) {
+		pr_err("Failed to create export directory '%s': %s\n",
+		       eps->root_dir, strerror(-ret));
+		eps->failed = true;
+	}
+}
+
+static void export_dir_print_end(void *ps __maybe_unused) {}
+
+static void export_dir_print_event(void *ps, const char *topic,
+				   const char *pmu_name, u32 pmu_type,
+				   const char *event_name, const char *event_alias,
+				   const char *scale_unit __maybe_unused,
+				   bool deprecated,
+				   const char *event_type_desc __maybe_unused,
+				   const char *desc __maybe_unused,
+				   const char *long_desc __maybe_unused,
+				   const char *encoding_desc)
+{
+	struct export_dir_print_state *eps = ps;
+
+	if (eps->failed)
+		return;
+
+	if (deprecated && !eps->common.deprecated)
+		return;
+
+	if (eps->common.pmu_glob &&
+		(!pmu_name || !strglobmatch(pmu_name, eps->common.pmu_glob)))
+		return;
+
+	if (eps->common.exclude_abi && pmu_type < PERF_TYPE_MAX &&
+	    pmu_type != PERF_TYPE_RAW)
+		return;
+
+	if (eps->common.event_glob &&
+	    (!event_name || !strglobmatch(event_name, eps->common.event_glob)) &&
+	    (!event_alias || !strglobmatch(event_alias, eps->common.event_glob)) &&
+	    (!topic || !strglobmatch_nocase(topic, eps->common.event_glob)))
+		return;
+
+	if (!pmu_name || !event_name || !encoding_desc ||
+	    pmu_type == PERF_TYPE_TRACEPOINT)
+		return;
+
+	if (pmu_layer__export_event(eps->root_dir, pmu_name, event_name) < 0)
+		eps->failed = true;
+}
+
+static bool export_pmu_glob_matches_metric(const char *pmu_glob,
+					   const char *eff_pmu)
+{
+	struct perf_pmu *pmu = NULL;
+
+	if (!pmu_glob)
+		return true;
+	if (strglobmatch(eff_pmu, pmu_glob))
+		return true;
+	if (!strcmp(eff_pmu, "default_core") && is_pmu_core(pmu_glob))
+		return true;
+
+	while ((pmu = perf_pmus__scan_for_metrics(pmu)) != NULL) {
+		if (pmu->type == PERF_TYPE_TRACEPOINT)
+			continue;
+		if (!strglobmatch(pmu->name, pmu_glob) &&
+		    (!pmu->alias_name || !strglobmatch(pmu->alias_name, pmu_glob)))
+			continue;
+		if (!strcmp(eff_pmu, "default_core") && pmu->is_core)
+			return true;
+		if (perf_pmu__name_wildcard_match(pmu, eff_pmu))
+			return true;
+	}
+	return false;
+}
+
+static void export_dir_print_metric(void *ps, const char *group,
+				    const char *name, const char *desc,
+				    const char *long_desc, const char *expr,
+				    const char *threshold, const char *unit,
+				    const char *pmu_name)
+{
+	struct export_dir_print_state *eps = ps;
+	const char *eff_pmu = (pmu_name && pmu_name[0]) ? pmu_name : "default_core";
+	char key[NAME_MAX + MAX_PMU_NAME_LEN];
+
+	if (eps->failed)
+		return;
+
+	if (!export_pmu_glob_matches_metric(eps->common.pmu_glob, eff_pmu))
+		return;
+
+	if (eps->common.event_glob &&
+	    (!eps->common.metrics || !name ||
+	     !strglobmatch(name, eps->common.event_glob)) &&
+	    (!eps->common.metricgroups || !group ||
+	     !strglobmatch(group, eps->common.event_glob)))
+		return;
+
+	if (eps->common.metricgroups && group && group[0] &&
+	    !strlist__has_entry(eps->visited_groups, group)) {
+		const char *gdesc;
+
+		strlist__add(eps->visited_groups, group);
+		gdesc = perf_pmus__describe_metricgroup(group);
+		if (pmu_layer__export_metricgroup(eps->root_dir, group, gdesc) < 0) {
+			eps->failed = true;
+			return;
+		}
+	}
+
+	if (!eps->common.metrics || !name || !name[0])
+		return;
+
+	snprintf(key, sizeof(key), "%s/%s", eff_pmu, name);
+	if (strlist__has_entry(eps->common.visited_metrics, key))
+		return;
+	strlist__add(eps->common.visited_metrics, key);
+
+	if (pmu_layer__export_metric(eps->root_dir, eff_pmu, group, name, desc,
+				     long_desc, expr, threshold, unit) < 0)
+		eps->failed = true;
+}
+
 /*
  * Launch the interactive textual based ilist python script via 'perf script'
  * that finds the script and sets up the python environment. Arguments are
@@ -577,6 +715,11 @@ int cmd_list(int argc, const char **argv)
 			.fp = stdout,
 		},
 	};
+	struct export_dir_print_state export_ps = {
+		.common = {
+			.fp = stdout,
+		},
+	};
 	struct print_state *ps = &default_ps;
 	struct print_callbacks print_cb = {
 		.print_start = default_print_start,
@@ -588,6 +731,7 @@ int cmd_list(int argc, const char **argv)
 	const char *cputype = NULL;
 	const char *unit_name = NULL;
 	const char *output_path = NULL;
+	const char *export_dir = NULL;
 	bool json = false;
 	bool use_tui = false;
 	struct option list_options[] = {
@@ -600,6 +744,8 @@ int cmd_list(int argc, const char **argv)
 		OPT_BOOLEAN(0, "details", &default_ps.detailed,
 			    "Print information on the perf event names and expressions used internally by events."),
 		OPT_STRING('o', "output", &output_path, "file", "output file name"),
+		OPT_STRING(0, "export-dir", &export_dir, "dir",
+			   "Export events, metrics, and metricgroups to a sysfs-style directory"),
 		OPT_BOOLEAN(0, "deprecated", &default_ps.deprecated,
 			    "Print deprecated events."),
 		OPT_STRING(0, "cputype", &cputype, "cpu type",
@@ -629,22 +775,59 @@ int cmd_list(int argc, const char **argv)
 	argc = parse_options(argc, argv, list_options, list_usage,
 			     PARSE_OPT_STOP_AT_NON_OPTION);
 
+	if (export_dir && (use_tui || json || output_path)) {
+		pr_err("ERROR: --export-dir cannot be combined with --tui, --json, or --output\n");
+		return -1;
+	}
+
 	if (use_tui)
 		return list__tui(argc, argv);
 
-	if (json)
+	if (export_dir) {
+		export_ps.root_dir = export_dir;
+		export_ps.common.long_desc = default_ps.long_desc;
+		export_ps.common.deprecated = default_ps.deprecated;
+		ps = &export_ps.common;
+	} else if (json) {
 		ps = &json_ps.common;
+	}
 
 	if (output_path) {
 		ps->fp = fopen(output_path, "w");
 	}
 
-	setup_pager();
-
-	if (!default_ps.name_only)
+	if (!export_dir) {
 		setup_pager();
 
-	if (json) {
+		if (!default_ps.name_only)
+			setup_pager();
+	}
+
+	if (export_dir) {
+		print_cb = (struct print_callbacks){
+			.print_start = export_dir_print_start,
+			.print_end = export_dir_print_end,
+			.print_event = export_dir_print_event,
+			.print_metric = export_dir_print_metric,
+			.skip_duplicate_pmus = json_skip_duplicate_pmus,
+		};
+		export_ps.visited_groups = strlist__new(NULL, NULL);
+		assert(export_ps.visited_groups);
+		ps->visited_metrics = strlist__new(NULL, NULL);
+		assert(ps->visited_metrics);
+		if (unit_name)
+			ps->pmu_glob = strdup(unit_name);
+		else if (cputype) {
+			const struct perf_pmu *pmu = perf_pmus__pmu_for_pmu_filter(cputype);
+
+			if (!pmu) {
+				pr_err("ERROR: cputype is not supported!\n");
+				ret = -1;
+				goto out;
+			}
+			ps->pmu_glob = strdup(pmu->name);
+		}
+	} else if (json) {
 		print_cb = (struct print_callbacks){
 			.print_start = json_print_start,
 			.print_end = json_print_end,
@@ -673,7 +856,7 @@ int cmd_list(int argc, const char **argv)
 	print_cb.print_start(ps);
 
 	if (argc == 0) {
-		if (!unit_name) {
+		if (!unit_name || export_dir) {
 			ps->metrics = true;
 			ps->metricgroups = true;
 		}
@@ -685,16 +868,17 @@ int cmd_list(int argc, const char **argv)
 		char *s;
 
 		if (strcmp(argv[i], "tracepoint") == 0) {
-			char *old_pmu_glob = default_ps.pmu_glob;
+			char *old_pmu_glob = ps->pmu_glob;
 
-			default_ps.pmu_glob = strdup("tracepoint");
-			if (!default_ps.pmu_glob) {
+			ps->pmu_glob = strdup("tracepoint");
+			if (!ps->pmu_glob) {
+				ps->pmu_glob = old_pmu_glob;
 				ret = -1;
 				goto out;
 			}
 			perf_pmus__print_pmu_events(&print_cb, ps);
-			zfree(&default_ps.pmu_glob);
-			default_ps.pmu_glob = old_pmu_glob;
+			zfree(&ps->pmu_glob);
+			ps->pmu_glob = old_pmu_glob;
 		} else if (strcmp(argv[i], "hw") == 0 ||
 			   strcmp(argv[i], "hardware") == 0) {
 			char *old_event_glob = ps->event_glob;
@@ -780,6 +964,8 @@ int cmd_list(int argc, const char **argv)
 			zfree(&ps->event_glob);
 			ps->event_glob = old_event_glob;
 		} else {
+			char *old_event_glob = ps->event_glob;
+
 			if (asprintf(&s, "*%s*", argv[i]) < 0) {
 				printf("Critical: Not enough memory! Trying to continue...\n");
 				continue;
@@ -791,15 +977,19 @@ int cmd_list(int argc, const char **argv)
 			ps->metricgroups = true;
 			metricgroup__print(&print_cb, ps);
 			free(s);
+			ps->event_glob = old_event_glob;
 		}
 	}
 
 out:
 	print_cb.print_end(ps);
+	if (export_dir && export_ps.failed)
+		ret = -1;
 	free(ps->pmu_glob);
 	free(ps->last_topic);
 	free(ps->last_metricgroups);
 	strlist__delete(ps->visited_metrics);
+	strlist__delete(export_ps.visited_groups);
 	if (output_path)
 		fclose(ps->fp);
 
diff --git a/tools/perf/pmu-events/README b/tools/perf/pmu-events/README
index de7efa2cebd1..b95d47e7a492 100644
--- a/tools/perf/pmu-events/README
+++ b/tools/perf/pmu-events/README
@@ -150,3 +150,22 @@ where:
 
 	i.e the three CPU models use the JSON files (i.e PMU events) listed
 	in the directory 'tools/perf/pmu-events/arch/x86/silvermont'.
+
+Sysfs-style directory export (--output-dir)
+===========================================
+
+Instead of generating C tables, 'jevents.py --output-dir <dir>' can export
+JSON events, metrics, and metricgroup descriptions directly into a sysfs-style
+directory tree suitable for layering via 'pmu-events.dir' in '~/.perfconfig'
+(see perf-config(1) and perf-list(1)):
+
+	$ python3 tools/perf/pmu-events/jevents.py --output-dir /tmp/pmu-events x86 skylake
+	$ python3 tools/perf/pmu-events/jevents.py --output-dir /opt/pmu-events x86 all
+
+Canonical model directories are written under '<dir>/models/<arch>/<model>/'
+and 'mapfile.csv' CPUID patterns and uncore 'Compat' identifiers are expanded
+into relative POSIX symlinks under '<dir>/cpus/<cpuid>' and '<dir>/sys/<id>',
+so no 'mapfile.csv' is needed at runtime:
+
+	$ perf config pmu-events.dir=/opt/pmu-events
+	$ perf list
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index b32344e7eb8e..8c2f6531b0c5 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -8,6 +8,7 @@ import importlib
 import json
 import metric
 import os
+import re
 import sys
 from typing import (Callable, Dict, NamedTuple, Optional, Sequence, Set, TextIO,
                     Tuple)
@@ -2087,6 +2088,298 @@ def _init_worker(std_events: dict) -> None:
   global _arch_std_events
   _arch_std_events = std_events
 
+
+_SCALE_UNIT_RE = re.compile(
+    r'^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)(.*)$'
+)
+_ENUM_AGGR_MODE = {
+    '1': 'PerChip',
+    '2': 'PerCore',
+}
+_ENUM_EVENT_GROUPING = {
+    '1': 'NO_GROUP_EVENTS',
+    '2': 'NO_GROUP_EVENTS_NMI',
+    '3': 'NO_GROUP_EVENTS_SMT',
+    '4': 'NO_THRESHOLD_AND_NMI',
+}
+
+
+def _encode_filename(name: str) -> str:
+  """Percent-encode '/' as '%2F' for POSIX filenames."""
+  return name.replace('/', '%2F')
+
+
+def _write_text_file(path: str, content: str) -> None:
+  """Write a single sysfs-style attribute file terminated by a newline."""
+  os.makedirs(os.path.dirname(path), exist_ok=True)
+  with open(path, 'w', encoding='utf-8') as f:
+    f.write(content)
+    if not content.endswith('\n'):
+      f.write('\n')
+
+
+def _export_event_files(events_dir: str, event: JsonEvent) -> None:
+  """Materialize a JsonEvent into <pmu>/events/<event>* files."""
+  if not event.name or not event.event:
+    return
+  enc = _encode_filename(event.name)
+  base = os.path.join(events_dir, enc)
+  if event.event.startswith(('legacy-hardware-config=', 'legacy-cache-config=')):
+    _write_text_file(base + '.legacy-terms', event.event)
+  else:
+    _write_text_file(base, event.event)
+
+  if event.unit:
+    m = _SCALE_UNIT_RE.match(event.unit)
+    if m:
+      scale_str, unit_str = m.group(1), m.group(2)
+      _write_text_file(base + '.scale', scale_str)
+      if unit_str:
+        _write_text_file(base + '.unit', unit_str)
+    else:
+      _write_text_file(base + '.scale-unit', event.unit)
+
+  if event.perpkg and event.perpkg != '0':
+    _write_text_file(base + '.per-pkg', '1')
+  if event.deprecated and event.deprecated != '0':
+    _write_text_file(base + '.deprecated', '1')
+  if event.desc:
+    _write_text_file(base + '.desc', event.desc)
+  if event.long_desc:
+    _write_text_file(base + '.long-desc', event.long_desc)
+  if event.topic:
+    _write_text_file(base + '.topic', event.topic)
+  if event.compat:
+    _write_text_file(base + '.compat', event.compat)
+  if event.retirement_latency_mean:
+    _write_text_file(base + '.retire-lat-mean', str(event.retirement_latency_mean))
+  if event.retirement_latency_min:
+    _write_text_file(base + '.retire-lat-min', str(event.retirement_latency_min))
+  if event.retirement_latency_max:
+    _write_text_file(base + '.retire-lat-max', str(event.retirement_latency_max))
+
+
+def _export_metric_files(metrics_dir: str, m: JsonEvent) -> None:
+  """Materialize a JsonEvent metric into <pmu>/metrics/<metric>* files."""
+  if not m.metric_name or m.metric_expr is None:
+    return
+  enc = _encode_filename(m.metric_name)
+  base = os.path.join(metrics_dir, enc)
+  _write_text_file(base, m.metric_expr.ToPerfJson())
+
+  if m.unit:
+    _write_text_file(base + '.scale-unit', m.unit)
+  if m.desc:
+    _write_text_file(base + '.desc', m.desc)
+  if m.long_desc:
+    _write_text_file(base + '.long-desc', m.long_desc)
+  if m.metric_group:
+    _write_text_file(base + '.group', m.metric_group)
+  if m.metricgroup_no_group:
+    _write_text_file(base + '.no-group', m.metricgroup_no_group)
+  if m.default_metricgroup_name:
+    _write_text_file(base + '.default-group', m.default_metricgroup_name)
+  if m.metric_threshold:
+    _write_text_file(base + '.threshold', m.metric_threshold)
+  if m.compat:
+    _write_text_file(base + '.compat', m.compat)
+  if m.aggr_mode and m.aggr_mode in _ENUM_AGGR_MODE:
+    _write_text_file(base + '.aggr-mode', _ENUM_AGGR_MODE[m.aggr_mode])
+  if m.event_grouping and m.event_grouping in _ENUM_EVENT_GROUPING:
+    _write_text_file(base + '.constraint', _ENUM_EVENT_GROUPING[m.event_grouping])
+  if m.default_show_events and m.default_show_events != '0':
+    _write_text_file(base + '.show-events', '1')
+
+
+def _expand_char_class(spec: str) -> list[str]:
+  """Expand a regex bracket expression body like '0-9A-F' or '56'."""
+  chars: list[str] = []
+  i = 0
+  while i < len(spec):
+    if i + 2 < len(spec) and spec[i + 1] == '-':
+      for code in range(ord(spec[i]), ord(spec[i + 2]) + 1):
+        chars.append(chr(code))
+      i += 3
+    else:
+      chars.append(spec[i])
+      i += 1
+  return chars
+
+
+def _expand_regex_fragment(pat: str) -> list[str]:
+  """Expand finite '(a|b)' alternations and '[...]' character classes."""
+  for idx, ch in enumerate(pat):
+    if ch == '(':
+      depth = 0
+      alts: list[str] = []
+      start = idx + 1
+      end_idx = -1
+      for j in range(idx, len(pat)):
+        if pat[j] == '(':
+          depth += 1
+        elif pat[j] == ')':
+          depth -= 1
+          if depth == 0:
+            alts.append(pat[start:j])
+            end_idx = j
+            break
+        elif pat[j] == '|' and depth == 1:
+          alts.append(pat[start:j])
+          start = j + 1
+      if end_idx != -1:
+        prefix = pat[:idx]
+        suffix_exp = _expand_regex_fragment(pat[end_idx + 1:])
+        out: list[str] = []
+        for alt in alts:
+          for mid in _expand_regex_fragment(alt):
+            for suf in suffix_exp:
+              out.append(prefix + mid + suf)
+        return out
+    elif ch == '[':
+      close = pat.find(']', idx + 1)
+      if close != -1:
+        prefix = pat[:idx]
+        choices = _expand_char_class(pat[idx + 1:close])
+        suffix_exp = _expand_regex_fragment(pat[close + 1:])
+        return [prefix + c + suf for c in choices for suf in suffix_exp]
+  return [pat]
+
+
+def _expand_cpuid_pattern(arch: str, pattern: str) -> Sequence[str]:
+  """Expand a mapfile.csv CPUID pattern into literal directory names."""
+  if arch == 's390':
+    m = re.match(r'^\^IBM\.([^.*]+)\.\*.*$', pattern)
+    if m:
+      return ['IBM,' + x for x in _expand_regex_fragment(m.group(1))]
+    return []
+
+  if arch == 'powerpc':
+    return _expand_regex_fragment(pattern.replace('[[:xdigit:]]{4}', '0000'))
+
+  xdigit_cls = '[0-9A-F]' if arch == 'x86' else '[0-9a-f]'
+  pat = pattern
+  if pat.endswith('-[[:xdigit:]]+'):
+    pat = pat[:-len('-[[:xdigit:]]+')]
+  elif pat.endswith('-0x[[:xdigit:]]+'):
+    pat = pat[:-len('-0x[[:xdigit:]]+')]
+  elif pat.endswith('[[:xdigit:]]+'):
+    pat = pat[:-len('[[:xdigit:]]+')] + f'({xdigit_cls}|)'
+  pat = pat.replace('[[:xdigit:]]', xdigit_cls)
+  return [c for c in _expand_regex_fragment(pat) if '/' not in c]
+
+
+def _is_literal_compat(compat: str) -> bool:
+  """Return True if compat is a plain PMU identifier without regex operators."""
+  return bool(compat) and '/' not in compat and not any(
+      ch in compat for ch in '*+?[]()|^$\\'
+  )
+
+
+def _ensure_rel_symlink(link_path: str, target_dir: str) -> None:
+  """Create a relative POSIX symlink link_path -> target_dir if absent."""
+  if os.path.lexists(link_path) or not os.path.isdir(target_dir):
+    return
+  os.makedirs(os.path.dirname(link_path), exist_ok=True)
+  rel_target = os.path.relpath(target_dir, os.path.dirname(link_path))
+  os.symlink(rel_target, link_path)
+
+
+def _export_arch_tree(
+    output_dir: str,
+    arch: str,
+    sys_compat_targets: Dict[str, Set[str]],
+    ftw_fn: Callable[[str, Sequence[str], Callable[[Sequence[str], os.DirEntry], None]], None],
+) -> None:
+  """Export a single architecture subtree and its CPUID symlinks."""
+  assert _args is not None
+  arch_path = f'{_args.starting_dir}/{arch}'
+  seen_metrics: Set[Tuple[str, str, str]] = set()
+
+  def export_item(parents: Sequence[str], item: os.DirEntry) -> None:
+    level = len(parents)
+    if level == 0 or level > 4 or not item.is_file() or not item.name.endswith('.json'):
+      return
+
+    target_root = (
+        output_dir
+        if arch == 'common'
+        else os.path.join(output_dir, 'models', arch, *parents)
+    )
+
+    if item.name.endswith('metricgroups.json'):
+      with open(item.path, encoding='utf-8') as f:
+        mgroups = json.load(f)
+      for mgroup, gdesc in mgroups.items():
+        enc_group = _encode_filename(mgroup)
+        _write_text_file(
+            os.path.join(target_root, 'metricgroups', enc_group + '.desc'),
+            gdesc,
+        )
+        if target_root != output_dir:
+          _write_text_file(
+              os.path.join(output_dir, 'metricgroups', enc_group + '.desc'),
+              gdesc,
+          )
+      return
+
+    is_sys = 'sys' in parents
+    topic = get_topic(item.name)
+    for ev in read_json_events(item.path, topic):
+      if ev.name:
+        _export_event_files(os.path.join(target_root, ev.pmu, 'events'), ev)
+        if is_sys and ev.compat and _is_literal_compat(ev.compat):
+          sys_compat_targets[ev.compat].add(target_root)
+      metric_key = (target_root, ev.pmu, ev.metric_name or '')
+      if ev.metric_name and metric_key not in seen_metrics:
+        seen_metrics.add(metric_key)
+        _export_metric_files(os.path.join(target_root, ev.pmu, 'metrics'), ev)
+        if is_sys and ev.compat and _is_literal_compat(ev.compat):
+          sys_compat_targets[ev.compat].add(target_root)
+
+  ftw_fn(arch_path, [], export_item)
+
+  if arch == 'test':
+    _ensure_rel_symlink(
+        os.path.join(output_dir, 'cpus', 'testcpu'),
+        os.path.join(output_dir, 'models', 'test', 'test_soc', 'cpu'),
+    )
+  elif arch != 'common':
+    mapfile = os.path.join(arch_path, 'mapfile.csv')
+    if os.path.isfile(mapfile):
+      with open(mapfile, encoding='utf-8') as csvfile:
+        for row in csv.reader(csvfile):
+          if not row or row[0].startswith('#') or row[0] == 'Family-model':
+            continue
+          model_rel = row[2].strip('/')
+          target_dir = os.path.join(
+              output_dir, 'models', arch, *model_rel.split('/')
+          )
+          if not os.path.isdir(target_dir):
+            continue
+          for cpuid in _expand_cpuid_pattern(arch, row[0]):
+            _ensure_rel_symlink(
+                os.path.join(output_dir, 'cpus', cpuid), target_dir
+            )
+
+
+def _export_directory_tree(
+    output_dir: str,
+    archs: Sequence[str],
+    ftw_fn: Callable[[str, Sequence[str], Callable[[Sequence[str], os.DirEntry], None]], None],
+) -> None:
+  """Materialize JSON events/metrics into a regex-free sysfs-style tree."""
+  os.makedirs(output_dir, exist_ok=True)
+  sys_compat_targets: Dict[str, Set[str]] = collections.defaultdict(set)
+
+  for arch in archs:
+    _export_arch_tree(output_dir, arch, sys_compat_targets, ftw_fn)
+
+  for compat, targets in sorted(sys_compat_targets.items()):
+    if len(targets) == 1:
+      _ensure_rel_symlink(
+          os.path.join(output_dir, 'sys', compat), next(iter(targets))
+      )
+
 def main() -> None:
   global _args
 
@@ -2115,6 +2408,9 @@ def main() -> None:
       if item.is_dir():
         ftw(item.path, list(parents) + [item.name], action)
 
+  default_arch_dir = os.path.join(
+      os.path.dirname(os.path.abspath(__file__)), 'arch'
+  )
   ap = argparse.ArgumentParser()
   ap.add_argument(
       '--compress',
@@ -2126,14 +2422,21 @@ def main() -> None:
           'blocks indexed by (block_id << 16) | sub_offset (e.g. zlib,none)'
       ),
   )
-  ap.add_argument('arch', help='Architecture name like x86')
-  ap.add_argument('model', help='''Select a model such as skylake to
+  ap.add_argument(
+      '--output-dir',
+      default=None,
+      help='Export events and metrics as a sysfs-style directory tree instead of C code',
+  )
+  ap.add_argument('arch', nargs='?', default='all', help='Architecture name like x86')
+  ap.add_argument('model', nargs='?', help='''Select a model such as skylake to
 reduce the code size.  Normally set to "all". For architectures like
 ARM64 with an implementor/model, the model must include the implementor
 such as "arm/cortex-a34".''',
                   default='all')
   ap.add_argument(
       'starting_dir',
+      nargs='?',
+      default=default_arch_dir,
       type=dir_path,
       help='Root of tree containing architecture directories containing json files'
   )
@@ -2143,6 +2446,45 @@ such as "arm/cortex-a34".''',
       'output_string_file', type=argparse.FileType('w', encoding='utf-8'), nargs='?', default=None)
   _args = ap.parse_args()
 
+  archs = []
+  for item in os.scandir(_args.starting_dir):
+    if not item.is_dir():
+      continue
+    if (
+        item.name == _args.arch
+        or _args.arch == 'all'
+        or (not _args.output_dir and item.name in ('test', 'common'))
+    ):
+      archs.append(item.name)
+
+  min_archs = 1 if _args.output_dir else 2
+  if len(archs) < min_archs and _args.arch != 'none':
+    raise IOError(f'Missing architecture directory \'{_args.arch}\'')
+
+  archs.sort()
+  import concurrent.futures
+  tasks = []
+  def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
+    if len(parents) == 0:
+      return
+    if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
+      tasks.append((item.path, get_topic(item.name)))
+
+  for arch in archs:
+    arch_path = f'{_args.starting_dir}/{arch}'
+    preprocess_arch_std_files(arch_path)
+    ftw(arch_path, [], collect_json)
+
+  with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
+                                              initializer=_init_worker,
+                                              initargs=(_arch_std_events,)) as executor:
+    for path, topic, events in executor.map(_parallel_read_json_events, tasks):
+      _json_cache[(path, topic)] = events
+
+  if _args.output_dir:
+    _export_directory_tree(_args.output_dir, archs, ftw)
+    return
+
   _args.output_file.write(f"""/* SPDX-License-Identifier: GPL-2.0 */
 /* THIS FILE WAS AUTOGENERATED BY `jevents.py arch={_args.arch} model={_args.model}` ! */
 """)
@@ -2173,35 +2515,6 @@ struct pmu_table_entry {
 };
 
 """)
-  archs = []
-  for item in os.scandir(_args.starting_dir):
-    if not item.is_dir():
-      continue
-    if item.name == _args.arch or _args.arch == 'all' or item.name == 'test' or item.name == 'common':
-      archs.append(item.name)
-
-  if len(archs) < 2 and _args.arch != 'none':
-    raise IOError(f'Missing architecture directory \'{_args.arch}\'')
-
-  archs.sort()
-  import concurrent.futures
-  tasks = []
-  def collect_json(parents: Sequence[str], item: os.DirEntry) -> None:
-    if len(parents) == 0:
-      return
-    if item.is_file() and item.name.endswith('.json') and not item.name.endswith('metricgroups.json'):
-      tasks.append((item.path, get_topic(item.name)))
-
-  for arch in archs:
-    arch_path = f'{_args.starting_dir}/{arch}'
-    preprocess_arch_std_files(arch_path)
-    ftw(arch_path, [], collect_json)
-
-  with concurrent.futures.ProcessPoolExecutor(max_workers=_parallel_worker_count(len(tasks)),
-                                              initializer=_init_worker,
-                                              initargs=(_arch_std_events,)) as executor:
-    for path, topic, events in executor.map(_parallel_read_json_events, tasks):
-      _json_cache[(path, topic)] = events
 
   for arch in archs:
     arch_path = f'{_args.starting_dir}/{arch}'
diff --git a/tools/perf/util/pmu-layer.c b/tools/perf/util/pmu-layer.c
index 2c60fc1e3285..35a816db3f4f 100644
--- a/tools/perf/util/pmu-layer.c
+++ b/tools/perf/util/pmu-layer.c
@@ -2391,3 +2391,475 @@ const char *pmu_layer__describe_metricgroup(struct perf_pmu *pmu,
 	perf_pmu_free_metricgroup(old_mg);
 	return mg->desc;
 }
+
+int pmu_layer__mkdir_p(const char *path)
+{
+	char tmp[PATH_MAX];
+	size_t len;
+
+	if (!path || !path[0])
+		return -EINVAL;
+
+	len = strlen(path);
+	if (len >= sizeof(tmp))
+		return -ENAMETOOLONG;
+
+	memcpy(tmp, path, len + 1);
+	while (len > 1 && tmp[len - 1] == '/')
+		tmp[--len] = '\0';
+
+	for (char *p = tmp + 1; *p; p++) {
+		if (*p != '/')
+			continue;
+		*p = '\0';
+		if (mkdir(tmp, 0755) < 0 && errno != EEXIST)
+			return -errno;
+		*p = '/';
+	}
+	if (mkdir(tmp, 0755) < 0) {
+		struct stat st;
+
+		if (errno != EEXIST || stat(tmp, &st) < 0 || !S_ISDIR(st.st_mode))
+			return -errno;
+	}
+	return 0;
+}
+
+static int export_write_str_at(int dirfd, const char *filename, const char *content)
+{
+	size_t len = strlen(content);
+	size_t off = 0;
+	int fd;
+
+	fd = openat(dirfd, filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+	if (fd < 0)
+		return -errno;
+
+	while (off < len) {
+		ssize_t n = write(fd, content + off, len - off);
+
+		if (n < 0) {
+			if (errno == EINTR)
+				continue;
+			close(fd);
+			return -errno;
+		}
+		off += (size_t)n;
+	}
+	for (;;) {
+		ssize_t n = write(fd, "\n", 1);
+
+		if (n < 0 && errno == EINTR)
+			continue;
+		if (n != 1) {
+			int err = errno ? -errno : -EIO;
+
+			close(fd);
+			return err;
+		}
+		break;
+	}
+	close(fd);
+	return 0;
+}
+
+static int export_write_sidecar(int dirfd, const char *enc_base,
+				const char *suffix, const char *content)
+{
+	char filename[NAME_MAX + 32];
+	int n;
+
+	if (!content)
+		return 0;
+
+	n = snprintf(filename, sizeof(filename), "%s%s", enc_base, suffix);
+	if (n < 0 || (size_t)n >= sizeof(filename))
+		return -ENAMETOOLONG;
+
+	return export_write_str_at(dirfd, filename, content);
+}
+
+static struct perf_pmu_alias *export_lookup_cached_alias(struct perf_pmu *pmu,
+							 const char *name)
+{
+	struct perf_pmu_alias *alias = NULL;
+
+	if (!pmu || !name || !pmu->aliases)
+		return NULL;
+	if (hashmap__find(pmu->aliases, name, &alias))
+		return alias->terms ? alias : NULL;
+	return NULL;
+}
+
+static struct perf_pmu_alias *export_find_alias(struct perf_pmu *pmu,
+						const char *event_name)
+{
+	struct perf_pmu_alias *alias;
+	const char *slash, *end, *comma;
+	char buf[NAME_MAX];
+	size_t len;
+
+	alias = export_lookup_cached_alias(pmu, event_name);
+	if (alias)
+		return alias;
+
+	slash = strchr(event_name, '/');
+	if (!slash)
+		return NULL;
+	slash++;
+	end = strrchr(slash, '/');
+	if (!end || end <= slash)
+		return NULL;
+
+	len = (size_t)(end - slash);
+	if (len < sizeof(buf)) {
+		memcpy(buf, slash, len);
+		buf[len] = '\0';
+		alias = export_lookup_cached_alias(pmu, buf);
+		if (alias)
+			return alias;
+	}
+
+	comma = memchr(slash, ',', len);
+	if (comma && comma > slash) {
+		len = (size_t)(comma - slash);
+		if (len < sizeof(buf)) {
+			memcpy(buf, slash, len);
+			buf[len] = '\0';
+			alias = export_lookup_cached_alias(pmu, buf);
+			if (alias)
+				return alias;
+		}
+	}
+	return NULL;
+}
+
+static struct perf_pmu_alias *export_find_pmu_and_alias(const char *pmu_name,
+							const char *event_name,
+							struct perf_pmu **out_pmu)
+{
+	struct perf_pmu *pmu = perf_pmus__find(pmu_name);
+	struct perf_pmu *fallback_pmu = NULL;
+	struct perf_pmu_alias *fallback_alias = NULL;
+	struct perf_pmu_alias *alias;
+
+	if (pmu) {
+		enum pmu_kind kind = perf_pmu__kind(pmu);
+
+		if (kind != PERF_PMU_KIND_DRM && kind != PERF_PMU_KIND_HWMON) {
+			alias = export_find_alias(pmu, event_name);
+			if (alias && alias->name && alias->terms) {
+				const char *alias_pmu =
+					(alias->pmu_name && alias->pmu_name[0])
+					? alias->pmu_name : pmu->name;
+
+				if (!strcmp(alias_pmu, pmu_name)) {
+					*out_pmu = pmu;
+					return alias;
+				}
+				fallback_pmu = pmu;
+				fallback_alias = alias;
+			}
+		}
+		pmu = NULL;
+	}
+
+	while ((pmu = perf_pmus__scan(pmu)) != NULL) {
+		enum pmu_kind kind = perf_pmu__kind(pmu);
+		const char *alias_pmu;
+
+		if (kind == PERF_PMU_KIND_DRM || kind == PERF_PMU_KIND_HWMON)
+			continue;
+		if (!perf_pmu__name_wildcard_match(pmu, pmu_name) &&
+		    !perf_pmu__wildcard_match(pmu, pmu_name))
+			continue;
+		alias = export_find_alias(pmu, event_name);
+		if (!alias || !alias->name || !alias->terms)
+			continue;
+		alias_pmu = (alias->pmu_name && alias->pmu_name[0])
+			? alias->pmu_name : pmu->name;
+		if (!strcmp(alias_pmu, pmu_name)) {
+			*out_pmu = pmu;
+			return alias;
+		}
+		if (!fallback_alias) {
+			fallback_pmu = pmu;
+			fallback_alias = alias;
+		}
+	}
+	if (fallback_alias) {
+		*out_pmu = fallback_pmu;
+		return fallback_alias;
+	}
+	return NULL;
+}
+
+int pmu_layer__export_event(const char *root_dir, const char *pmu_name,
+			    const char *event_name)
+{
+	struct perf_pmu *pmu = NULL;
+	struct perf_pmu_alias *alias;
+	const char *target_pmu;
+	char dir_path[PATH_MAX];
+	char enc_name[NAME_MAX];
+	char num_buf[64];
+	int events_fd, n, ret = 0;
+
+	alias = export_find_pmu_and_alias(pmu_name, event_name, &pmu);
+	if (!alias)
+		return 0;
+
+	if (pmu_layer__encode_filename(alias->name, enc_name, sizeof(enc_name)) != 0)
+		return 0;
+
+	target_pmu = (alias->pmu_name && alias->pmu_name[0]) ? alias->pmu_name : pmu->name;
+	n = snprintf(dir_path, sizeof(dir_path), "%s/%s/events", root_dir, target_pmu);
+	if (n < 0 || (size_t)n >= sizeof(dir_path))
+		return -ENAMETOOLONG;
+
+	ret = pmu_layer__mkdir_p(dir_path);
+	if (ret < 0)
+		return ret;
+
+	events_fd = open(dir_path, O_RDONLY | O_DIRECTORY);
+	if (events_fd < 0)
+		return -errno;
+
+	if (!strncmp(alias->terms, "legacy-hardware-config=", 23) ||
+	    !strncmp(alias->terms, "legacy-cache-config=", 20)) {
+		ret = export_write_sidecar(events_fd, enc_name, ".legacy-terms",
+					   alias->terms);
+	} else {
+		ret = export_write_sidecar(events_fd, enc_name, "", alias->terms);
+		if (!ret && alias->legacy_terms && alias->legacy_terms[0])
+			ret = export_write_sidecar(events_fd, enc_name,
+						   ".legacy-terms",
+						   alias->legacy_terms);
+	}
+	if (!ret && (alias->has_unit || alias->unit[0] != '\0'))
+		ret = export_write_sidecar(events_fd, enc_name, ".unit", alias->unit);
+	if (!ret && (alias->has_scale || alias->scale != 1.0)) {
+		snprintf(num_buf, sizeof(num_buf), "%.17g", alias->scale);
+		ret = export_write_sidecar(events_fd, enc_name, ".scale", num_buf);
+	}
+	if (!ret && (alias->has_per_pkg || alias->per_pkg))
+		ret = export_write_sidecar(events_fd, enc_name, ".per-pkg",
+					   alias->per_pkg ? "1" : "0");
+	if (!ret && (alias->has_snapshot || alias->snapshot))
+		ret = export_write_sidecar(events_fd, enc_name, ".snapshot",
+					   alias->snapshot ? "1" : "0");
+	if (!ret && (alias->has_deprecated || alias->deprecated))
+		ret = export_write_sidecar(events_fd, enc_name, ".deprecated",
+					   alias->deprecated ? "1" : "0");
+	if (!ret && alias->desc && alias->desc[0])
+		ret = export_write_sidecar(events_fd, enc_name, ".desc", alias->desc);
+	if (!ret && alias->long_desc && alias->long_desc[0])
+		ret = export_write_sidecar(events_fd, enc_name, ".long-desc",
+					   alias->long_desc);
+	if (!ret && alias->topic && alias->topic[0])
+		ret = export_write_sidecar(events_fd, enc_name, ".topic", alias->topic);
+	if (!ret && alias->compat && alias->compat[0])
+		ret = export_write_sidecar(events_fd, enc_name, ".compat", alias->compat);
+	if (!ret && alias->has_retire_lat_mean) {
+		snprintf(num_buf, sizeof(num_buf), "%.17g",
+			 alias->retirement_latency_mean);
+		ret = export_write_sidecar(events_fd, enc_name, ".retire-lat-mean",
+					   num_buf);
+	}
+	if (!ret && alias->has_retire_lat_min) {
+		snprintf(num_buf, sizeof(num_buf), "%.17g",
+			 alias->retirement_latency_min);
+		ret = export_write_sidecar(events_fd, enc_name, ".retire-lat-min",
+					   num_buf);
+	}
+	if (!ret && alias->has_retire_lat_max) {
+		snprintf(num_buf, sizeof(num_buf), "%.17g",
+			 alias->retirement_latency_max);
+		ret = export_write_sidecar(events_fd, enc_name, ".retire-lat-max",
+					   num_buf);
+	}
+
+	close(events_fd);
+	return ret;
+}
+
+static const char *export_aggr_mode_str(enum aggr_mode_class mode)
+{
+	switch (mode) {
+	case PerChip:
+		return "PerChip";
+	case PerCore:
+		return "PerCore";
+	default:
+		return NULL;
+	}
+}
+
+static const char *export_constraint_str(enum metric_event_groups grouping)
+{
+	switch (grouping) {
+	case MetricNoGroupEvents:
+		return "NO_GROUP_EVENTS";
+	case MetricNoGroupEventsNmi:
+		return "NO_GROUP_EVENTS_NMI";
+	case MetricNoGroupEventsSmt:
+		return "NO_GROUP_EVENTS_SMT";
+	case MetricNoGroupEventsThresholdAndNmi:
+		return "NO_THRESHOLD_AND_NMI";
+	case MetricGroupEvents:
+	default:
+		return NULL;
+	}
+}
+
+static const struct perf_pmu_metric *
+export_find_pmu_metric(const char *eff_pmu, const char *name,
+		       const char *expr, const char *threshold)
+{
+	struct perf_pmu *pmu = NULL;
+	const struct perf_pmu_metric *fallback = NULL;
+
+	while ((pmu = perf_pmus__scan_for_metrics(pmu)) != NULL) {
+		const struct perf_pmu_metric *m;
+		const char *m_pmu;
+
+		if (pmu->type == PERF_TYPE_TRACEPOINT)
+			continue;
+
+		m = pmu_layer__find_metric(pmu, name);
+		if (!m)
+			continue;
+
+		m_pmu = (m->pmu && m->pmu[0]) ? m->pmu : "default_core";
+		if (strcmp(m_pmu, eff_pmu) != 0)
+			continue;
+
+		if (!strcmp(m->metric_expr ?: "", expr ?: "") &&
+		    !strcmp(m->metric_threshold ?: "", threshold ?: ""))
+			return m;
+
+		if (!fallback)
+			fallback = m;
+	}
+	return fallback;
+}
+
+int pmu_layer__export_metricgroup(const char *root_dir, const char *group,
+				  const char *desc)
+{
+	char dir_path[PATH_MAX];
+	char enc_name[NAME_MAX];
+	int fd, n, ret;
+
+	if (!desc || !desc[0] ||
+	    pmu_layer__encode_filename(group, enc_name, sizeof(enc_name)) != 0)
+		return 0;
+
+	n = snprintf(dir_path, sizeof(dir_path), "%s/metricgroups", root_dir);
+	if (n < 0 || (size_t)n >= sizeof(dir_path))
+		return -ENAMETOOLONG;
+
+	ret = pmu_layer__mkdir_p(dir_path);
+	if (ret < 0)
+		return ret;
+
+	fd = open(dir_path, O_RDONLY | O_DIRECTORY);
+	if (fd < 0)
+		return -errno;
+
+	ret = export_write_sidecar(fd, enc_name, ".desc", desc);
+	close(fd);
+	return ret;
+}
+
+int pmu_layer__export_metric(const char *root_dir, const char *eff_pmu,
+			     const char *group, const char *name,
+			     const char *desc, const char *long_desc,
+			     const char *expr, const char *threshold,
+			     const char *unit)
+{
+	const struct perf_pmu_metric *m;
+	char dir_path[PATH_MAX];
+	char enc_name[NAME_MAX];
+	int fd, n, ret;
+
+	if (pmu_layer__encode_filename(name, enc_name, sizeof(enc_name)) != 0)
+		return 0;
+
+	m = export_find_pmu_metric(eff_pmu, name, expr, threshold);
+
+	n = snprintf(dir_path, sizeof(dir_path), "%s/%s/metrics", root_dir, eff_pmu);
+	if (n < 0 || (size_t)n >= sizeof(dir_path))
+		return -ENAMETOOLONG;
+
+	ret = pmu_layer__mkdir_p(dir_path);
+	if (ret < 0)
+		return ret;
+
+	fd = open(dir_path, O_RDONLY | O_DIRECTORY);
+	if (fd < 0)
+		return -errno;
+
+	if (m) {
+		ret = export_write_sidecar(fd, enc_name, "",
+					   m->metric_expr ?: expr);
+		if (!ret && m->unit && m->unit[0])
+			ret = export_write_sidecar(fd, enc_name, ".scale-unit",
+						   m->unit);
+		if (!ret && m->desc && m->desc[0])
+			ret = export_write_sidecar(fd, enc_name, ".desc", m->desc);
+		if (!ret && m->long_desc && m->long_desc[0])
+			ret = export_write_sidecar(fd, enc_name, ".long-desc",
+						   m->long_desc);
+		if (!ret && m->metric_group && m->metric_group[0])
+			ret = export_write_sidecar(fd, enc_name, ".group",
+						   m->metric_group);
+		if (!ret && m->metricgroup_no_group && m->metricgroup_no_group[0])
+			ret = export_write_sidecar(fd, enc_name, ".no-group",
+						   m->metricgroup_no_group);
+		if (!ret && m->default_metricgroup_name &&
+		    m->default_metricgroup_name[0])
+			ret = export_write_sidecar(fd, enc_name, ".default-group",
+						   m->default_metricgroup_name);
+		if (!ret && m->metric_threshold && m->metric_threshold[0])
+			ret = export_write_sidecar(fd, enc_name, ".threshold",
+						   m->metric_threshold);
+		if (!ret && m->compat && m->compat[0])
+			ret = export_write_sidecar(fd, enc_name, ".compat",
+						   m->compat);
+		if (!ret && m->has_aggr_mode) {
+			const char *aggr = export_aggr_mode_str(m->aggr_mode);
+
+			if (aggr)
+				ret = export_write_sidecar(fd, enc_name,
+							   ".aggr-mode", aggr);
+		}
+		if (!ret && m->has_event_grouping) {
+			const char *cstr = export_constraint_str(m->event_grouping);
+
+			if (cstr)
+				ret = export_write_sidecar(fd, enc_name,
+							   ".constraint", cstr);
+		}
+		if (!ret && m->has_default_show_events)
+			ret = export_write_sidecar(fd, enc_name, ".show-events",
+						   m->default_show_events ? "1" : "0");
+	} else {
+		ret = export_write_sidecar(fd, enc_name, "", expr);
+		if (!ret && unit && unit[0])
+			ret = export_write_sidecar(fd, enc_name, ".scale-unit", unit);
+		if (!ret && desc && desc[0])
+			ret = export_write_sidecar(fd, enc_name, ".desc", desc);
+		if (!ret && long_desc && long_desc[0])
+			ret = export_write_sidecar(fd, enc_name, ".long-desc",
+						   long_desc);
+		if (!ret && group && group[0])
+			ret = export_write_sidecar(fd, enc_name, ".group", group);
+		if (!ret && threshold && threshold[0])
+			ret = export_write_sidecar(fd, enc_name, ".threshold",
+						   threshold);
+	}
+
+	close(fd);
+	return ret;
+}
diff --git a/tools/perf/util/pmu-layer.h b/tools/perf/util/pmu-layer.h
index 5d8a33481410..183bed9cc10e 100644
--- a/tools/perf/util/pmu-layer.h
+++ b/tools/perf/util/pmu-layer.h
@@ -173,4 +173,15 @@ int pmu_layer__load_metrics(struct perf_pmu *pmu);
 const char *pmu_layer__describe_metricgroup(struct perf_pmu *pmu,
 					    const char *group);
 
+int pmu_layer__mkdir_p(const char *path);
+int pmu_layer__export_event(const char *root_dir, const char *pmu_name,
+			    const char *event_name);
+int pmu_layer__export_metricgroup(const char *root_dir, const char *group,
+				  const char *desc);
+int pmu_layer__export_metric(const char *root_dir, const char *eff_pmu,
+			     const char *group, const char *name,
+			     const char *desc, const char *long_desc,
+			     const char *expr, const char *threshold,
+			     const char *unit);
+
 #endif /* __PMU_LAYER_H */
diff --git a/tools/perf/util/print-events.c b/tools/perf/util/print-events.c
index eb44026972b9..e6f6d8231ce0 100644
--- a/tools/perf/util/print-events.c
+++ b/tools/perf/util/print-events.c
@@ -210,6 +210,18 @@ static int mep_cmp(struct rb_node *rb_node, const void *entry)
 	return strcmp(a->metric_name, b->metric_name);
 }
 
+static int mep_cmp_pmu(struct rb_node *rb_node, const void *entry)
+{
+	struct mep *a = container_of(rb_node, struct mep, nd);
+	struct mep *b = (struct mep *)entry;
+	int ret = mep_cmp(rb_node, entry);
+
+	if (ret)
+		return ret;
+
+	return strcmp(a->pmu_name ?: "", b->pmu_name ?: "");
+}
+
 static struct rb_node *mep_new(struct rblist *rl __maybe_unused, const void *entry)
 {
 	struct mep *me = malloc(sizeof(struct mep));
@@ -231,19 +243,26 @@ static void mep_delete(struct rblist *rl __maybe_unused,
 }
 
 static struct mep *mep_lookup(struct rblist *groups, const char *metric_group,
-			      const char *metric_name)
+			      const char *metric_name, const char *pmu_name)
 {
 	struct rb_node *nd;
 	struct mep me = {
 		.metric_group = strdup(metric_group),
 		.metric_name = metric_name,
+		.pmu_name = pmu_name,
 	};
+
+	if (!me.metric_group)
+		return NULL;
 	nd = rblist__find(groups, &me);
 	if (nd) {
 		free(me.metric_group);
 		return container_of(nd, struct mep, nd);
 	}
-	rblist__add_node(groups, &me);
+	if (rblist__add_node(groups, &me)) {
+		free(me.metric_group);
+		return NULL;
+	}
 	nd = rblist__find(groups, &me);
 	if (nd)
 		return container_of(nd, struct mep, nd);
@@ -267,9 +286,9 @@ static int metricgroup__add_to_mep_groups_callback(const struct pmu_metric *pm,
 
 		g = skip_spaces(g);
 		if (strlen(g))
-			me = mep_lookup(groups, g, pm->metric_name);
+			me = mep_lookup(groups, g, pm->metric_name, pm->pmu);
 		else
-			me = mep_lookup(groups, pm->metric_name, pm->metric_name);
+			me = mep_lookup(groups, pm->metric_name, pm->metric_name, pm->pmu);
 
 		if (me) {
 			me->metric_desc = pm->desc;
@@ -292,7 +311,7 @@ void metricgroup__print(const struct print_callbacks *print_cb, void *print_stat
 
 	rblist__init(&groups);
 	groups.node_new = mep_new;
-	groups.node_cmp = mep_cmp;
+	groups.node_cmp = print_cb->skip_duplicate_pmus(print_state) ? mep_cmp : mep_cmp_pmu;
 	groups.node_delete = mep_delete;
 
 	metricgroup__for_each_metric(/*table=*/NULL,
-- 
2.56.1.703.gf848ed807c-goog


  parent reply	other threads:[~2026-11-13 21:11 UTC|newest]

Thread overview: 12+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
2026-11-13 21:10 ` [PATCH 01/11] perf pmu: Generalize pmu_alias_info_file() and lazy single-event sysfs loading Ian Rogers
2026-11-13 21:10 ` [PATCH 02/11] perf pmu: Add metric and metricgroup storage on struct perf_pmu Ian Rogers
2026-11-13 21:10 ` [PATCH 03/11] perf pmu-layer: Introduce stateless struct pmu_layer abstraction and builtin adapter Ian Rogers
2026-11-13 21:10 ` [PATCH 04/11] perf pmu-layer: Add openat-based directory layer with literal CPUID/symlink lookup Ian Rogers
2026-11-13 21:10 ` [PATCH 05/11] perf config: Add pmu-events.dir configuration option Ian Rogers
2026-11-13 21:10 ` [PATCH 06/11] perf pmu: Switch alias resolution and enumeration to layered PMU stack Ian Rogers
2026-11-13 21:10 ` [PATCH 07/11] perf metricgroup: Route metric and metricgroup lookups through struct perf_pmu Ian Rogers
2026-11-13 21:10 ` Ian Rogers [this message]
2026-11-13 21:11 ` [PATCH 09/11] perf jevents: Add compiled-in model and sys table iterators Ian Rogers
2026-11-13 21:11 ` [PATCH 10/11] perf list: Add --export-cpuid to export compiled-in CPU models Ian Rogers
2026-11-13 21:11 ` [PATCH 11/11] perf tests: Add round-trip export, CPUID symlink, and pmu-events.dir overlay tests Ian Rogers

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261113211105.3118281-9-irogers@google.com \
    --to=irogers@google.com \
    --cc=acme@kernel.org \
    --cc=adrian.hunter@intel.com \
    --cc=james.clark@linaro.org \
    --cc=jolsa@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mingo@redhat.com \
    --cc=namhyung@kernel.org \
    --cc=peterz@infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®