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 10/11] perf list: Add --export-cpuid to export compiled-in CPU models
Date: Fri, 13 Nov 2026 13:11:01 -0800	[thread overview]
Message-ID: <20261113211105.3118281-11-irogers@google.com> (raw)
In-Reply-To: <20261113211105.3118281-1-irogers@google.com>

Currently 'perf list --export-dir <dir>' only exports the active PMUs on
the running system into host-direct '<dir>/<pmu>/' directories, while
exporting other CPU models or architectures requires running
'jevents.py --output-dir' against an unbuilt kernel JSON source tree.
With JEVENTS_ARCH=all and block-indexed string compression, the 'perf'
binary already carries all compiled-in architectures and CPU models.

Add '--export-cpuid <cpuid|arch[/model]|all>' to 'perf list' (for use
with '--export-dir <dir>') so compiled-in event, metric, and metricgroup
tables for any CPUID, model, or architecture can be materialized
directly from the 'perf' binary without needing 'jevents.py' or kernel
sources:

- Implement pmu_layer__export_cpuid() in pmu-layer.c, supporting:
  - Exact or stepping CPUIDs (e.g. 'GenuineIntel-6-8F-8' ->
    'GenuineIntel-6-8F', '0x00000000411fd493' -> '0x00000000410fd490',
    '0x00800200' -> '0x00800000', 'IBM,8561,703' -> 'IBM,8561', and
    '0x489-0x8000000000000007-0x1' -> '0x489-0x8000000000000007') as
    well as literal system PMU Compat identifiers.
  - Architecture names ('x86', 'arm64', 'riscv', 'powerpc', 's390',
    'common') and '<arch>/<model>' or '<model>' globs ('x86/skylake',
    'arm64/arm/neoverse-n2-v2', 'amdzen*').
  - 'all' to materialize all compiled-in architectures and models under
    '<dir>/models/<arch>/<model>/' with expanded relative POSIX symlinks
    under '<dir>/cpus/<cpuid>' and '<dir>/sys/<id>'.
- Preserve all 'perf list' filters (--unit, --cputype, --deprecated, and
  hw/sw/cache/pmu/metric/metricgroup/event_glob positional arguments).

For example, to inspect or migrate metrics from ARM64 Neoverse-N2 onto
another machine:

  $ perf list --export-dir /tmp/n2 \
        --export-cpuid arm64/arm/neoverse-n2-v2 metric metricgroup

produces:

  /tmp/n2/
  |-- cpus/
  |   |-- 0x00000000410fd490 -> ../models/arm64/arm/neoverse-n2-v2
  |   `-- 0x00000000410fd4f0 -> ../models/arm64/arm/neoverse-n2-v2
  |-- models/arm64/arm/neoverse-n2-v2/
  |   |-- default_core/metrics/...
  |   `-- metricgroups/...
  `-- metricgroups/...

Signed-off-by: Ian Rogers <irogers@google.com>
---
 tools/perf/Documentation/perf-config.txt |    6 +-
 tools/perf/Documentation/perf-list.txt   |   23 +-
 tools/perf/builtin-list.c                |  152 +++
 tools/perf/util/pmu-layer.c              | 1199 ++++++++++++++++++++--
 tools/perf/util/pmu-layer.h              |   12 +
 5 files changed, 1300 insertions(+), 92 deletions(-)

diff --git a/tools/perf/Documentation/perf-config.txt b/tools/perf/Documentation/perf-config.txt
index 95181ebfca23..d9afecde6019 100644
--- a/tools/perf/Documentation/perf-config.txt
+++ b/tools/perf/Documentation/perf-config.txt
@@ -302,10 +302,12 @@ For example, to layer local overrides ahead of a shared directory in
 		$ 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`, from compiled-in cross-architecture tables using
+`perf list --export-dir ... --export-cpuid` (see linkperf:perf-list[1]), or
+from JSON event trees using `jevents.py --output-dir`:
 
 		$ perf list --export-dir ~/.perf-events/local metric metricgroup
+		$ perf list --export-dir /opt/pmu-events --export-cpuid all
 		$ 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
 
diff --git a/tools/perf/Documentation/perf-list.txt b/tools/perf/Documentation/perf-list.txt
index dbad98da3899..e2f7d8407a4c 100644
--- a/tools/perf/Documentation/perf-list.txt
+++ b/tools/perf/Documentation/perf-list.txt
@@ -57,8 +57,18 @@ Output in JSON format.
 	(`<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>>.
+	`perf list --export-dir ~/.perf-events metric metricgroup`. By default
+	only the current system's active PMUs are exported; combine with
+	`--export-cpuid` to export compiled-in tables for other CPUs or
+	architectures. See <<EXPORTING_EVENTS, EXPORTING AND OVERLAYING EVENTS>>.
+
+--export-cpuid=::
+	When combined with `--export-dir`, export compiled-in PMU event and
+	metric tables matching a CPUID (for example `GenuineIntel-6-8F-8` or
+	`0x00000000410fd490`), architecture (`x86`, `arm64`, `riscv`), model
+	path or glob (`x86/skylake`, `arm64/arm/neoverse-n2-v2`, `amdzen*`), or
+	`all` into `<dir>/models/<arch>/<model>/` with relative POSIX symlinks
+	under `<dir>/cpus/<cpuid>` and `<dir>/sys/<id>`.
 
 --tui::
 Rather than printing the events, interactively browse the PMUs, events and
@@ -386,10 +396,13 @@ extended in place, and layered back over the inbuilt tables and sysfs via
   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':
+Compiled-in tables for other CPUIDs, models, or architectures can also be
+exported directly from the 'perf' binary using '--export-cpuid' (for example,
+to inspect or migrate a metric from one architecture or model to another), or
+exported from unbuilt kernel source JSON trees using 'jevents.py --output-dir':
 
+  perf list --export-dir /tmp/skl --export-cpuid x86/skylake metric
+  perf list --export-dir /opt/pmu-events --export-cpuid all
   python3 tools/perf/pmu-events/jevents.py --output-dir /opt/pmu-events x86 all
   perf config pmu-events.dir=~/.perf-events:/opt/pmu-events
 
diff --git a/tools/perf/builtin-list.c b/tools/perf/builtin-list.c
index 85de0bd73e32..f87a8b2147d4 100644
--- a/tools/perf/builtin-list.c
+++ b/tools/perf/builtin-list.c
@@ -703,6 +703,140 @@ static int list__tui(int argc, const char **argv)
 	return ret;
 }
 
+static int run_export_cpuid_pass(const char *export_dir,
+				 const char *export_cpuid,
+				 const struct pmu_layer_export_opts *opts)
+{
+	int ret = pmu_layer__export_cpuid(export_dir, export_cpuid, opts);
+
+	if (ret == -ENOENT) {
+		pr_err("ERROR: No compiled-in PMU tables matched --export-cpuid '%s'\n",
+		       export_cpuid);
+		return -1;
+	}
+	if (ret < 0) {
+		pr_err("Failed to export PMU tables to '%s': %s\n",
+		       export_dir, strerror(-ret));
+		return -1;
+	}
+	return 0;
+}
+
+static int cmd_list__export_cpuid(const char *export_dir,
+				  const char *export_cpuid,
+				  const char *unit_name,
+				  const char *cputype,
+				  bool deprecated,
+				  int argc, const char **argv)
+{
+	const char *base_pmu_glob = NULL;
+	bool ran_pass = false;
+
+	if (unit_name) {
+		base_pmu_glob = unit_name;
+	} else if (cputype) {
+		const struct perf_pmu *pmu = perf_pmus__pmu_for_pmu_filter(cputype);
+
+		if (pmu)
+			base_pmu_glob = pmu->name;
+		else if (!strcasecmp(cputype, "core"))
+			base_pmu_glob = "cpu_core";
+		else if (!strcasecmp(cputype, "atom"))
+			base_pmu_glob = "cpu_atom";
+		else
+			base_pmu_glob = cputype;
+	}
+
+	if (argc == 0) {
+		struct pmu_layer_export_opts opts = {
+			.pmu_glob = base_pmu_glob,
+			.events = true,
+			.metrics = true,
+			.metricgroups = true,
+			.deprecated = deprecated,
+		};
+
+		return run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+	}
+
+	for (int i = 0; i < argc; i++) {
+		struct pmu_layer_export_opts opts = {
+			.pmu_glob = base_pmu_glob,
+			.deprecated = deprecated,
+		};
+		int ret;
+
+		if (!strcmp(argv[i], "tracepoint") || !strcmp(argv[i], "sdt") ||
+		    !strcmp(argv[i], "pfm")) {
+			continue;
+		} else if (!strcmp(argv[i], "hw") || !strcmp(argv[i], "hardware")) {
+			opts.events = true;
+			opts.event_glob = "legacy hardware";
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else if (!strcmp(argv[i], "sw") || !strcmp(argv[i], "software")) {
+			static const char * const sw_globs[] = { "software", "tool" };
+
+			ret = 0;
+			for (size_t j = 0; j < ARRAY_SIZE(sw_globs) && !ret; j++) {
+				if (base_pmu_glob &&
+				    !strglobmatch(sw_globs[j], base_pmu_glob))
+					continue;
+				opts.pmu_glob = sw_globs[j];
+				opts.events = true;
+				ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+				ran_pass = true;
+			}
+			if (ret)
+				return ret;
+			continue;
+		} else if (!strcmp(argv[i], "cache") || !strcmp(argv[i], "hwcache")) {
+			opts.events = true;
+			opts.event_glob = "legacy cache";
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else if (!strcmp(argv[i], "pmu")) {
+			opts.events = true;
+			opts.exclude_abi = true;
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else if (!strcmp(argv[i], "metric") || !strcmp(argv[i], "metrics")) {
+			opts.metrics = true;
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else if (!strcmp(argv[i], "metricgroup") ||
+			   !strcmp(argv[i], "metricgroups")) {
+			opts.metricgroups = true;
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else if (strchr(argv[i], ':') != NULL) {
+			opts.event_glob = argv[i];
+			opts.metrics = true;
+			opts.metricgroups = true;
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+		} else {
+			char *s = NULL;
+
+			if (asprintf(&s, "*%s*", argv[i]) < 0)
+				return -1;
+			opts.event_glob = s;
+			opts.events = true;
+			opts.metrics = true;
+			opts.metricgroups = true;
+			ret = run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+			free(s);
+		}
+		if (ret)
+			return ret;
+		ran_pass = true;
+	}
+
+	if (!ran_pass) {
+		struct pmu_layer_export_opts opts = {
+			.pmu_glob = base_pmu_glob,
+			.deprecated = deprecated,
+		};
+
+		return run_export_cpuid_pass(export_dir, export_cpuid, &opts);
+	}
+	return 0;
+}
+
 int cmd_list(int argc, const char **argv)
 {
 	int i, ret = 0;
@@ -732,6 +866,7 @@ int cmd_list(int argc, const char **argv)
 	const char *unit_name = NULL;
 	const char *output_path = NULL;
 	const char *export_dir = NULL;
+	const char *export_cpuid = NULL;
 	bool json = false;
 	bool use_tui = false;
 	struct option list_options[] = {
@@ -746,6 +881,8 @@ int cmd_list(int argc, const char **argv)
 		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_STRING(0, "export-cpuid", &export_cpuid, "cpuid|arch[/model]|all",
+			   "Export compiled-in PMU tables for CPUID, arch[/model], or 'all'"),
 		OPT_BOOLEAN(0, "deprecated", &default_ps.deprecated,
 			    "Print deprecated events."),
 		OPT_STRING(0, "cputype", &cputype, "cpu type",
@@ -779,10 +916,25 @@ int cmd_list(int argc, const char **argv)
 		pr_err("ERROR: --export-dir cannot be combined with --tui, --json, or --output\n");
 		return -1;
 	}
+	if (export_cpuid && !export_dir) {
+		pr_err("ERROR: --export-cpuid requires --export-dir\n");
+		return -1;
+	}
+	if (export_cpuid && !export_cpuid[0]) {
+		pr_err("ERROR: --export-cpuid requires a CPUID, arch, arch/model, or 'all'\n");
+		return -1;
+	}
 
 	if (use_tui)
 		return list__tui(argc, argv);
 
+	if (export_cpuid) {
+		return cmd_list__export_cpuid(export_dir, export_cpuid,
+					      unit_name, cputype,
+					      default_ps.deprecated,
+					      argc, argv);
+	}
+
 	if (export_dir) {
 		export_ps.root_dir = export_dir;
 		export_ps.common.long_desc = default_ps.long_desc;
diff --git a/tools/perf/util/pmu-layer.c b/tools/perf/util/pmu-layer.c
index 35a816db3f4f..74c07ffc8495 100644
--- a/tools/perf/util/pmu-layer.c
+++ b/tools/perf/util/pmu-layer.c
@@ -29,6 +29,7 @@
 #include "pmu.h"
 #include "pmus.h"
 #include "pmu-layer.h"
+#include "string2.h"
 #include "pmu-events/pmu-events.h"
 
 static struct pmu_layer *pmu_layers;
@@ -2594,36 +2595,11 @@ static struct perf_pmu_alias *export_find_pmu_and_alias(const char *pmu_name,
 	return NULL;
 }
 
-int pmu_layer__export_event(const char *root_dir, const char *pmu_name,
-			    const char *event_name)
+static int export_write_alias_files(int events_fd, const char *enc_name,
+				    const struct perf_pmu_alias *alias)
 {
-	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;
+	int ret;
 
 	if (!strncmp(alias->terms, "legacy-hardware-config=", 23) ||
 	    !strncmp(alias->terms, "legacy-cache-config=", 20)) {
@@ -2636,7 +2612,7 @@ int pmu_layer__export_event(const char *root_dir, const char *pmu_name,
 						   ".legacy-terms",
 						   alias->legacy_terms);
 	}
-	if (!ret && (alias->has_unit || alias->unit[0] != '\0'))
+	if (!ret && 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);
@@ -2678,7 +2654,40 @@ int pmu_layer__export_event(const char *root_dir, const char *pmu_name,
 		ret = export_write_sidecar(events_fd, enc_name, ".retire-lat-max",
 					   num_buf);
 	}
+	return ret;
+}
+
+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];
+	int events_fd, n, ret;
+
+	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;
 
+	ret = export_write_alias_files(events_fd, enc_name, alias);
 	close(events_fd);
 	return ret;
 }
@@ -2772,6 +2781,50 @@ int pmu_layer__export_metricgroup(const char *root_dir, const char *group,
 	return ret;
 }
 
+static int export_write_pm_files(int fd, const char *enc_name,
+				 const struct pmu_metric *pm,
+				 bool has_aggr, bool has_grouping,
+				 bool has_show, bool show_val)
+{
+	int ret = export_write_sidecar(fd, enc_name, "", pm->metric_expr);
+
+	if (!ret && pm->unit && pm->unit[0])
+		ret = export_write_sidecar(fd, enc_name, ".scale-unit", pm->unit);
+	if (!ret && pm->desc && pm->desc[0])
+		ret = export_write_sidecar(fd, enc_name, ".desc", pm->desc);
+	if (!ret && pm->long_desc && pm->long_desc[0])
+		ret = export_write_sidecar(fd, enc_name, ".long-desc", pm->long_desc);
+	if (!ret && pm->metric_group && pm->metric_group[0])
+		ret = export_write_sidecar(fd, enc_name, ".group", pm->metric_group);
+	if (!ret && pm->metricgroup_no_group && pm->metricgroup_no_group[0])
+		ret = export_write_sidecar(fd, enc_name, ".no-group",
+					   pm->metricgroup_no_group);
+	if (!ret && pm->default_metricgroup_name && pm->default_metricgroup_name[0])
+		ret = export_write_sidecar(fd, enc_name, ".default-group",
+					   pm->default_metricgroup_name);
+	if (!ret && pm->metric_threshold && pm->metric_threshold[0])
+		ret = export_write_sidecar(fd, enc_name, ".threshold",
+					   pm->metric_threshold);
+	if (!ret && pm->compat && pm->compat[0])
+		ret = export_write_sidecar(fd, enc_name, ".compat", pm->compat);
+	if (!ret && has_aggr) {
+		const char *aggr = export_aggr_mode_str(pm->aggr_mode);
+
+		if (aggr)
+			ret = export_write_sidecar(fd, enc_name, ".aggr-mode", aggr);
+	}
+	if (!ret && has_grouping) {
+		const char *cstr = export_constraint_str(pm->event_grouping);
+
+		if (cstr)
+			ret = export_write_sidecar(fd, enc_name, ".constraint", cstr);
+	}
+	if (!ret && has_show)
+		ret = export_write_sidecar(fd, enc_name, ".show-events",
+					   show_val ? "1" : "0");
+	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,
@@ -2779,6 +2832,7 @@ int pmu_layer__export_metric(const char *root_dir, const char *eff_pmu,
 			     const char *unit)
 {
 	const struct perf_pmu_metric *m;
+	struct pmu_metric pm = {};
 	char dir_path[PATH_MAX];
 	char enc_name[NAME_MAX];
 	int fd, n, ret;
@@ -2801,65 +2855,1040 @@ int pmu_layer__export_metric(const char *root_dir, const char *eff_pmu,
 		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");
+		perf_pmu_metric__to_pmu_metric(m, &pm);
+		if (!pm.metric_expr)
+			pm.metric_expr = expr;
+		ret = export_write_pm_files(fd, enc_name, &pm,
+					    m->has_aggr_mode,
+					    m->has_event_grouping,
+					    m->has_default_show_events,
+					    m->default_show_events);
 	} 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);
+		pm.metric_expr = expr;
+		pm.unit = unit;
+		pm.desc = desc;
+		pm.long_desc = long_desc;
+		pm.metric_group = group;
+		pm.metric_threshold = threshold;
+		ret = export_write_pm_files(fd, enc_name, &pm,
+					    false, false, false, false);
+	}
+
+	close(fd);
+	return ret;
+}
+
+struct export_str_vec {
+	char **items;
+	size_t nr;
+	size_t cap;
+};
+
+static void export_str_vec__clear(struct export_str_vec *v)
+{
+	for (size_t i = 0; i < v->nr; i++)
+		free(v->items[i]);
+	zfree(&v->items);
+	v->nr = 0;
+	v->cap = 0;
+}
+
+static bool export_str_vec__contains(const struct export_str_vec *v, const char *s)
+{
+	for (size_t i = 0; i < v->nr; i++) {
+		if (!strcmp(v->items[i], s))
+			return true;
+	}
+	return false;
+}
+
+static int export_str_vec__add(struct export_str_vec *v, const char *s)
+{
+	char *dup_s;
+
+	if (v->nr == v->cap) {
+		size_t new_cap = v->cap ? v->cap * 2 : 16;
+		char **new_items = realloc(v->items, new_cap * sizeof(*new_items));
+
+		if (!new_items)
+			return -ENOMEM;
+		v->items = new_items;
+		v->cap = new_cap;
+	}
+	dup_s = strdup(s);
+	if (!dup_s)
+		return -ENOMEM;
+	v->items[v->nr++] = dup_s;
+	return 0;
+}
+
+static int export_str_vec__add_concat3(struct export_str_vec *v,
+				       const char *p1, size_t l1,
+				       const char *p2, size_t l2,
+				       const char *p3, size_t l3)
+{
+	char buf[256];
+
+	if (l1 + l2 + l3 >= sizeof(buf))
+		return 0;
+	memcpy(buf, p1, l1);
+	memcpy(buf + l1, p2, l2);
+	memcpy(buf + l1 + l2, p3, l3);
+	buf[l1 + l2 + l3] = '\0';
+	return export_str_vec__add(v, buf);
+}
+
+static int expand_regex_fragment(const char *pat, struct export_str_vec *out);
+
+static int expand_alt_slice(const char *prefix, size_t plen,
+			    const char *slice, size_t slen,
+			    const struct export_str_vec *suf_vec,
+			    struct export_str_vec *out)
+{
+	struct export_str_vec alt_vec = {};
+	char alt_buf[256];
+	int ret;
+
+	if (slen >= sizeof(alt_buf))
+		return 0;
+	memcpy(alt_buf, slice, slen);
+	alt_buf[slen] = '\0';
+
+	ret = expand_regex_fragment(alt_buf, &alt_vec);
+	for (size_t a = 0; !ret && a < alt_vec.nr; a++) {
+		const char *as = alt_vec.items[a];
+		size_t al = strlen(as);
+
+		for (size_t s = 0; !ret && s < suf_vec->nr; s++) {
+			const char *ss = suf_vec->items[s];
+
+			ret = export_str_vec__add_concat3(out, prefix, plen,
+							  as, al,
+							  ss, strlen(ss));
+		}
 	}
+	export_str_vec__clear(&alt_vec);
+	return ret;
+}
+
+static int expand_paren_group(const char *pat, size_t idx, size_t close_idx,
+			      struct export_str_vec *out)
+{
+	struct export_str_vec suf_vec = {};
+	size_t start = idx + 1, depth = 0;
+	int ret;
+
+	ret = expand_regex_fragment(pat + close_idx + 1, &suf_vec);
+	if (ret)
+		goto out;
+
+	for (size_t j = idx + 1; j <= close_idx; j++) {
+		if (j < close_idx && pat[j] == '(') {
+			depth++;
+		} else if (j < close_idx && pat[j] == ')') {
+			depth--;
+		} else if (j == close_idx || (pat[j] == '|' && depth == 0)) {
+			ret = expand_alt_slice(pat, idx, pat + start, j - start,
+					       &suf_vec, out);
+			if (ret)
+				goto out;
+			start = j + 1;
+		}
+	}
+
+out:
+	export_str_vec__clear(&suf_vec);
+	return ret;
+}
+
+static int expand_bracket_group(const char *pat, size_t idx,
+				const char *close_p, struct export_str_vec *out)
+{
+	struct export_str_vec suf_vec = {};
+	const char *p = pat + idx + 1;
+	int ret;
+
+	ret = expand_regex_fragment(close_p + 1, &suf_vec);
+	if (ret)
+		goto out;
+
+	while (p < close_p) {
+		unsigned int c_lo, c_hi;
+
+		if (p + 2 < close_p && p[1] == '-' &&
+		    (unsigned char)p[0] <= (unsigned char)p[2]) {
+			c_lo = (unsigned char)p[0];
+			c_hi = (unsigned char)p[2];
+			p += 3;
+		} else {
+			c_lo = (unsigned char)p[0];
+			c_hi = c_lo;
+			p += 1;
+		}
+		for (unsigned int ch = c_lo; ch <= c_hi; ch++) {
+			char one = (char)ch;
+
+			for (size_t s = 0; s < suf_vec.nr; s++) {
+				const char *ss = suf_vec.items[s];
+
+				ret = export_str_vec__add_concat3(out, pat, idx,
+								  &one, 1,
+								  ss, strlen(ss));
+				if (ret)
+					goto out;
+			}
+		}
+	}
+
+out:
+	export_str_vec__clear(&suf_vec);
+	return ret;
+}
+
+static int expand_regex_fragment(const char *pat, struct export_str_vec *out)
+{
+	for (size_t idx = 0; pat[idx]; idx++) {
+		if (pat[idx] == '(') {
+			size_t depth = 0;
+
+			for (size_t j = idx; pat[j]; j++) {
+				if (pat[j] == '(') {
+					depth++;
+				} else if (pat[j] == ')') {
+					depth--;
+					if (depth == 0)
+						return expand_paren_group(pat, idx,
+									  j, out);
+				}
+			}
+		} else if (pat[idx] == '[') {
+			const char *close_p = strchr(pat + idx + 1, ']');
+
+			if (close_p)
+				return expand_bracket_group(pat, idx, close_p, out);
+		}
+	}
+	return export_str_vec__add(out, pat);
+}
+
+static bool replace_substr_all(const char *src, const char *needle,
+			       const char *repl, char *dst, size_t dst_size)
+{
+	size_t nlen = strlen(needle), rlen = strlen(repl), out_pos = 0;
+	const char *p = src;
+
+	while (*p) {
+		const char *hit = strstr(p, needle);
+
+		if (!hit) {
+			size_t tail = strlen(p);
+
+			if (out_pos + tail >= dst_size)
+				return false;
+			memcpy(dst + out_pos, p, tail + 1);
+			return true;
+		}
+		if (out_pos + (size_t)(hit - p) + rlen >= dst_size)
+			return false;
+		memcpy(dst + out_pos, p, (size_t)(hit - p));
+		out_pos += (size_t)(hit - p);
+		memcpy(dst + out_pos, repl, rlen);
+		out_pos += rlen;
+		p = hit + nlen;
+	}
+	if (out_pos >= dst_size)
+		return false;
+	dst[out_pos] = '\0';
+	return true;
+}
+
+static int expand_cpuid_pattern(const char *arch, const char *pattern,
+				struct export_str_vec *out)
+{
+	struct export_str_vec tmp = {};
+	char work[256], pat_buf[256];
+	int ret = 0;
+
+	if (!arch || !pattern || !pattern[0])
+		return 0;
+
+	if (!strcmp(arch, "s390")) {
+		size_t flen = 0;
+
+		if (strncmp(pattern, "^IBM.", 5) != 0)
+			return 0;
+		while (pattern[5 + flen] && pattern[5 + flen] != '.' &&
+		       pattern[5 + flen] != '*')
+			flen++;
+		if (flen == 0 || flen >= sizeof(work))
+			return 0;
+		memcpy(work, pattern + 5, flen);
+		work[flen] = '\0';
+		ret = expand_regex_fragment(work, &tmp);
+		for (size_t i = 0; !ret && i < tmp.nr; i++) {
+			if (!strchr(tmp.items[i], '/'))
+				ret = export_str_vec__add_concat3(
+					out, "IBM,", 4,
+					tmp.items[i], strlen(tmp.items[i]),
+					"", 0);
+		}
+		export_str_vec__clear(&tmp);
+		return ret;
+	}
+
+	if (!strcmp(arch, "powerpc")) {
+		if (!replace_substr_all(pattern, "[[:xdigit:]]{4}", "0000",
+					pat_buf, sizeof(pat_buf)))
+			return 0;
+		ret = expand_regex_fragment(pat_buf, &tmp);
+	} else {
+		const char *xdigit_cls = !strcmp(arch, "x86") ? "[0-9A-F]" : "[0-9a-f]";
+		const char *opt_xdigit = !strcmp(arch, "x86") ? "([0-9A-F]|)" : "([0-9a-f]|)";
+		size_t plen = strlen(pattern);
+
+		if (plen >= sizeof(work))
+			return 0;
+		memcpy(work, pattern, plen + 1);
+
+		if (plen >= 14 && !strcmp(work + plen - 14, "-[[:xdigit:]]+")) {
+			work[plen - 14] = '\0';
+		} else if (plen >= 16 && !strcmp(work + plen - 16, "-0x[[:xdigit:]]+")) {
+			work[plen - 16] = '\0';
+		} else if (plen >= 13 && !strcmp(work + plen - 13, "[[:xdigit:]]+")) {
+			if (plen - 13 + strlen(opt_xdigit) >= sizeof(work))
+				return 0;
+			strcpy(work + plen - 13, opt_xdigit);
+		}
+
+		if (!replace_substr_all(work, "[[:xdigit:]]", xdigit_cls,
+					pat_buf, sizeof(pat_buf)))
+			return 0;
+		ret = expand_regex_fragment(pat_buf, &tmp);
+	}
+
+	for (size_t i = 0; !ret && i < tmp.nr; i++) {
+		if (!strchr(tmp.items[i], '/'))
+			ret = export_str_vec__add(out, tmp.items[i]);
+	}
+	export_str_vec__clear(&tmp);
+	return ret;
+}
+
+static bool is_literal_compat(const char *compat)
+{
+	if (!compat || !compat[0] || strchr(compat, '/'))
+		return false;
+	return strpbrk(compat, "*+?[]()|^$\\") == NULL;
+}
+
+static bool is_known_core_pmu_glob(const char *glob)
+{
+	return !strcmp(glob, "cpu") || !strcmp(glob, "cpum_cf") ||
+	       !strcmp(glob, "default_core") || !strncmp(glob, "armv8_pmuv3", 11);
+}
+
+static bool export_cpuid_pmu_matches(const char *eff_pmu, const char *pmu_glob)
+{
+	if (!pmu_glob)
+		return true;
+	if (strglobmatch(eff_pmu, pmu_glob))
+		return true;
+	if (!strcmp(eff_pmu, "default_core") && is_known_core_pmu_glob(pmu_glob))
+		return true;
+	return false;
+}
+
+static int ensure_rel_symlink(const char *root_dir, const char *subdir,
+			      const char *link_name, const char *arch,
+			      const char *model)
+{
+	char target_dir[PATH_MAX], sub_path[PATH_MAX], link_path[PATH_MAX];
+	char rel_target[PATH_MAX];
+	struct stat st;
+	int n, ret;
+
+	if (!link_name || !link_name[0] || strchr(link_name, '/'))
+		return 0;
+
+	n = snprintf(target_dir, sizeof(target_dir), "%s/models/%s/%s",
+		     root_dir, arch, model);
+	if (n < 0 || (size_t)n >= sizeof(target_dir))
+		return -ENAMETOOLONG;
+	if (stat(target_dir, &st) < 0 || !S_ISDIR(st.st_mode))
+		return 0;
+
+	n = snprintf(sub_path, sizeof(sub_path), "%s/%s", root_dir, subdir);
+	if (n < 0 || (size_t)n >= sizeof(sub_path))
+		return -ENAMETOOLONG;
+
+	n = snprintf(link_path, sizeof(link_path), "%s/%s", sub_path, link_name);
+	if (n < 0 || (size_t)n >= sizeof(link_path))
+		return -ENAMETOOLONG;
+	if (lstat(link_path, &st) == 0)
+		return 0;
+
+	ret = pmu_layer__mkdir_p(sub_path);
+	if (ret < 0)
+		return ret;
+
+	n = snprintf(rel_target, sizeof(rel_target), "../models/%s/%s", arch, model);
+	if (n < 0 || (size_t)n >= sizeof(rel_target))
+		return -ENAMETOOLONG;
+
+	if (symlink(rel_target, link_path) < 0 && errno != EEXIST)
+		return -errno;
+	return 0;
+}
+
+static int export_builtin_pe_to_dir(const char *target_root,
+				    const struct pmu_event *pe)
+{
+	const char *eff_pmu = (pe->pmu && pe->pmu[0]) ? pe->pmu : "default_core";
+	struct perf_pmu_alias *alias;
+	char dir_path[PATH_MAX];
+	char enc_name[NAME_MAX];
+	int fd, n, ret;
+
+	if (!pe->name || !pe->event)
+		return 0;
+	if (pmu_layer__encode_filename(pe->name, enc_name, sizeof(enc_name)) != 0)
+		return 0;
+
+	alias = pmu_layer_alloc_alias(pe->name);
+	if (!alias)
+		return -ENOMEM;
+
+	ret = pmu_layer_merge_pe(alias, pe, /*is_common=*/false);
+	if (ret) {
+		perf_pmu_free_alias(alias);
+		return ret;
+	}
+
+	n = snprintf(dir_path, sizeof(dir_path), "%s/%s/events", target_root, eff_pmu);
+	if (n < 0 || (size_t)n >= sizeof(dir_path)) {
+		perf_pmu_free_alias(alias);
+		return -ENAMETOOLONG;
+	}
+
+	ret = pmu_layer__mkdir_p(dir_path);
+	if (ret == 0) {
+		fd = open(dir_path, O_RDONLY | O_DIRECTORY);
+		if (fd < 0) {
+			ret = -errno;
+		} else {
+			ret = export_write_alias_files(fd, enc_name, alias);
+			close(fd);
+		}
+	}
+	perf_pmu_free_alias(alias);
+	return ret;
+}
+
+static int export_builtin_pm_to_dir(const char *target_root,
+				    const struct pmu_metric *pm)
+{
+	const char *eff_pmu = (pm->pmu && pm->pmu[0]) ? pm->pmu : "default_core";
+	char dir_path[PATH_MAX];
+	char enc_name[NAME_MAX];
+	int fd, n, ret;
+
+	if (!pm->metric_name || !pm->metric_expr)
+		return 0;
+	if (pmu_layer__encode_filename(pm->metric_name, enc_name, sizeof(enc_name)) != 0)
+		return 0;
+
+	n = snprintf(dir_path, sizeof(dir_path), "%s/%s/metrics", target_root, 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;
 
+	ret = export_write_pm_files(fd, enc_name, pm, true, true,
+				    pm->default_show_events, true);
 	close(fd);
 	return ret;
 }
+
+struct export_compat_entry {
+	char *compat;
+	const char *arch;
+	const char *model;
+	bool ambiguous;
+};
+
+struct export_table_walk_ctx {
+	const char *root_dir;
+	const char *target_root;
+	const char *arch;
+	const char *model;
+	const struct pmu_layer_export_opts *opts;
+	bool is_sys;
+	struct export_compat_entry **compats;
+	size_t *nr_compats;
+	size_t *cap_compats;
+};
+
+static int record_sys_compat(struct export_table_walk_ctx *ctx, const char *compat)
+{
+	struct export_compat_entry *arr;
+
+	if (!ctx->is_sys || !is_literal_compat(compat))
+		return 0;
+
+	arr = *ctx->compats;
+	for (size_t i = 0; i < *ctx->nr_compats; i++) {
+		if (!strcmp(arr[i].compat, compat)) {
+			if (strcmp(arr[i].arch, ctx->arch) != 0 ||
+			    strcmp(arr[i].model, ctx->model) != 0)
+				arr[i].ambiguous = true;
+			return 0;
+		}
+	}
+	if (*ctx->nr_compats == *ctx->cap_compats) {
+		size_t new_cap = *ctx->cap_compats ? *ctx->cap_compats * 2 : 16;
+		struct export_compat_entry *new_arr =
+			realloc(arr, new_cap * sizeof(*new_arr));
+
+		if (!new_arr)
+			return -ENOMEM;
+		*ctx->compats = new_arr;
+		*ctx->cap_compats = new_cap;
+		arr = new_arr;
+	}
+	arr[*ctx->nr_compats].compat = strdup(compat);
+	if (!arr[*ctx->nr_compats].compat)
+		return -ENOMEM;
+	arr[*ctx->nr_compats].arch = ctx->arch;
+	arr[*ctx->nr_compats].model = ctx->model;
+	arr[*ctx->nr_compats].ambiguous = false;
+	(*ctx->nr_compats)++;
+	return 0;
+}
+
+static int export_cpuid_event_cb(const struct pmu_event *pe,
+				 const struct pmu_events_table *table __maybe_unused,
+				 void *vdata)
+{
+	struct export_table_walk_ctx *ctx = vdata;
+	const char *eff_pmu = (pe->pmu && pe->pmu[0]) ? pe->pmu : "default_core";
+	int ret;
+
+	if (!pe->name || !pe->event)
+		return 0;
+	if (pe->deprecated && !ctx->opts->deprecated)
+		return 0;
+	if (ctx->opts->exclude_abi && !strcmp(eff_pmu, "software"))
+		return 0;
+	if (!export_cpuid_pmu_matches(eff_pmu, ctx->opts->pmu_glob))
+		return 0;
+	if (ctx->opts->event_glob &&
+	    !strglobmatch(pe->name, ctx->opts->event_glob) &&
+	    (!pe->topic || !strglobmatch_nocase(pe->topic, ctx->opts->event_glob)))
+		return 0;
+
+	ret = export_builtin_pe_to_dir(ctx->target_root, pe);
+	if (ret)
+		return ret;
+	return record_sys_compat(ctx, pe->compat);
+}
+
+static bool metric_groups_match_glob(const char *groups, const char *glob)
+{
+	const char *p = groups;
+
+	if (!groups || !glob)
+		return false;
+
+	while (*p) {
+		const char *semi = strchr(p, ';');
+		size_t len = semi ? (size_t)(semi - p) : strlen(p);
+		char gbuf[NAME_MAX];
+
+		while (len > 0 && isspace((unsigned char)*p)) {
+			p++;
+			len--;
+		}
+		while (len > 0 && isspace((unsigned char)p[len - 1]))
+			len--;
+
+		if (len > 0 && len < sizeof(gbuf)) {
+			memcpy(gbuf, p, len);
+			gbuf[len] = '\0';
+			if (strglobmatch(gbuf, glob))
+				return true;
+		}
+		if (!semi)
+			break;
+		p = semi + 1;
+	}
+	return false;
+}
+
+static int export_metric_groups_for_pm(struct export_table_walk_ctx *ctx,
+				       const struct pmu_metric *pm)
+{
+	const char *p = pm->metric_group;
+
+	if (!p || !p[0])
+		return 0;
+
+	while (*p) {
+		const char *semi = strchr(p, ';');
+		size_t len = semi ? (size_t)(semi - p) : strlen(p);
+		char gbuf[NAME_MAX];
+
+		while (len > 0 && isspace((unsigned char)*p)) {
+			p++;
+			len--;
+		}
+		while (len > 0 && isspace((unsigned char)p[len - 1]))
+			len--;
+
+		if (len > 0 && len < sizeof(gbuf)) {
+			const char *gdesc;
+			int ret;
+
+			memcpy(gbuf, p, len);
+			gbuf[len] = '\0';
+			if (!ctx->opts->event_glob ||
+			    (ctx->opts->metrics && pm->metric_name &&
+			     strglobmatch(pm->metric_name, ctx->opts->event_glob)) ||
+			    strglobmatch(gbuf, ctx->opts->event_glob)) {
+				gdesc = describe_metricgroup(gbuf);
+				ret = pmu_layer__export_metricgroup(ctx->target_root,
+								    gbuf, gdesc);
+				if (ret)
+					return ret;
+				if (strcmp(ctx->target_root, ctx->root_dir) != 0) {
+					ret = pmu_layer__export_metricgroup(
+						ctx->root_dir, gbuf, gdesc);
+					if (ret)
+						return ret;
+				}
+			}
+		}
+		if (!semi)
+			break;
+		p = semi + 1;
+	}
+	return 0;
+}
+
+static int export_cpuid_metric_cb(const struct pmu_metric *pm,
+				  const struct pmu_metrics_table *table __maybe_unused,
+				  void *vdata)
+{
+	struct export_table_walk_ctx *ctx = vdata;
+	const char *eff_pmu = (pm->pmu && pm->pmu[0]) ? pm->pmu : "default_core";
+	int ret;
+
+	if (!pm->metric_name || !pm->metric_expr)
+		return 0;
+	if (!export_cpuid_pmu_matches(eff_pmu, ctx->opts->pmu_glob))
+		return 0;
+	if (ctx->opts->event_glob &&
+	    (!ctx->opts->metrics || !strglobmatch(pm->metric_name, ctx->opts->event_glob)) &&
+	    (!ctx->opts->metricgroups || !metric_groups_match_glob(pm->metric_group,
+								   ctx->opts->event_glob)))
+		return 0;
+
+	if (ctx->opts->metricgroups) {
+		ret = export_metric_groups_for_pm(ctx, pm);
+		if (ret)
+			return ret;
+	}
+	if (!ctx->opts->metrics)
+		return 0;
+
+	ret = export_builtin_pm_to_dir(ctx->target_root, pm);
+	if (ret)
+		return ret;
+	return record_sys_compat(ctx, pm->compat);
+}
+
+struct export_map_row {
+	const char *arch;
+	const char *cpuid;
+	const char *model;
+	const struct pmu_events_table *events_table;
+	const struct pmu_metrics_table *metrics_table;
+	struct export_str_vec cpuids;
+	bool selected;
+};
+
+struct export_sys_row {
+	const char *arch;
+	const char *model;
+	const struct pmu_events_table *events_table;
+	const struct pmu_metrics_table *metrics_table;
+	bool selected;
+};
+
+struct collect_rows_ctx {
+	struct export_map_row *map_rows;
+	size_t nr_map_rows;
+	size_t cap_map_rows;
+	struct export_sys_row *sys_rows;
+	size_t nr_sys_rows;
+	size_t cap_sys_rows;
+};
+
+static int collect_map_row_cb(const char *arch, const char *cpuid,
+			      const char *model,
+			      const struct pmu_events_table *events_table,
+			      const struct pmu_metrics_table *metrics_table,
+			      void *vdata)
+{
+	struct collect_rows_ctx *ctx = vdata;
+
+	if (ctx->nr_map_rows == ctx->cap_map_rows) {
+		size_t new_cap = ctx->cap_map_rows ? ctx->cap_map_rows * 2 : 32;
+		struct export_map_row *new_arr =
+			realloc(ctx->map_rows, new_cap * sizeof(*new_arr));
+
+		if (!new_arr)
+			return -ENOMEM;
+		ctx->map_rows = new_arr;
+		ctx->cap_map_rows = new_cap;
+	}
+	ctx->map_rows[ctx->nr_map_rows++] = (struct export_map_row){
+		.arch = arch,
+		.cpuid = cpuid,
+		.model = model,
+		.events_table = events_table,
+		.metrics_table = metrics_table,
+	};
+	return 0;
+}
+
+static int collect_sys_row_cb(const char *arch, const char *model,
+			      const struct pmu_events_table *events_table,
+			      const struct pmu_metrics_table *metrics_table,
+			      void *vdata)
+{
+	struct collect_rows_ctx *ctx = vdata;
+
+	if (ctx->nr_sys_rows == ctx->cap_sys_rows) {
+		size_t new_cap = ctx->cap_sys_rows ? ctx->cap_sys_rows * 2 : 16;
+		struct export_sys_row *new_arr =
+			realloc(ctx->sys_rows, new_cap * sizeof(*new_arr));
+
+		if (!new_arr)
+			return -ENOMEM;
+		ctx->sys_rows = new_arr;
+		ctx->cap_sys_rows = new_cap;
+	}
+	ctx->sys_rows[ctx->nr_sys_rows++] = (struct export_sys_row){
+		.arch = arch,
+		.model = model,
+		.events_table = events_table,
+		.metrics_table = metrics_table,
+	};
+	return 0;
+}
+
+struct sys_compat_match_ctx {
+	const char *selector;
+	bool matched;
+};
+
+static int check_sys_event_compat_cb(const struct pmu_event *pe,
+				     const struct pmu_events_table *table __maybe_unused,
+				     void *vdata)
+{
+	struct sys_compat_match_ctx *ctx = vdata;
+
+	if (pe->compat && is_literal_compat(pe->compat) &&
+	    strglobmatch(pe->compat, ctx->selector)) {
+		ctx->matched = true;
+		return 1;
+	}
+	return 0;
+}
+
+static int check_sys_metric_compat_cb(const struct pmu_metric *pm,
+				      const struct pmu_metrics_table *table __maybe_unused,
+				      void *vdata)
+{
+	struct sys_compat_match_ctx *ctx = vdata;
+
+	if (pm->compat && is_literal_compat(pm->compat) &&
+	    strglobmatch(pm->compat, ctx->selector)) {
+		ctx->matched = true;
+		return 1;
+	}
+	return 0;
+}
+
+static bool arch_or_model_matches(const char *arch, const char *model,
+				  const char *selector)
+{
+	char full_model[256];
+
+	if (!arch || !model)
+		return false;
+	if (!strcmp(arch, selector) ||
+	    (!strcmp(selector, "test") && !strcmp(arch, "testarch")))
+		return true;
+	if (!strcmp(arch, "common"))
+		return !strcmp(selector, "common/common");
+
+	snprintf(full_model, sizeof(full_model), "%s/%s", arch, model);
+	return strglobmatch(full_model, selector) || strglobmatch(model, selector);
+}
+
+static bool row_cpuids_match_cand(const struct export_str_vec *cpuids,
+				  const char *cand)
+{
+	for (size_t j = 0; j < cpuids->nr; j++) {
+		if (strglobmatch(cpuids->items[j], cand))
+			return true;
+	}
+	return false;
+}
+
+int pmu_layer__export_cpuid(const char *root_dir, const char *selector,
+			    const struct pmu_layer_export_opts *opts)
+{
+	struct collect_rows_ctx rows = {};
+	struct export_str_vec exported_models = {};
+	struct export_compat_entry *compats = NULL;
+	size_t nr_compats = 0, cap_compats = 0;
+	bool any_selected = false;
+	int ret;
+
+	if (!root_dir || !root_dir[0] || !selector || !selector[0] || !opts)
+		return -EINVAL;
+
+	ret = pmu_events_map__for_each(collect_map_row_cb, &rows);
+	if (ret)
+		goto out;
+	ret = pmu_sys_events__for_each(collect_sys_row_cb, &rows);
+	if (ret)
+		goto out;
+
+	for (size_t i = 0; i < rows.nr_map_rows; i++) {
+		struct export_map_row *r = &rows.map_rows[i];
+
+		if (r->arch && strcmp(r->arch, "common") != 0) {
+			ret = expand_cpuid_pattern(r->arch, r->cpuid, &r->cpuids);
+			if (ret)
+				goto out;
+		}
+	}
+
+	if (!strcmp(selector, "all")) {
+		for (size_t i = 0; i < rows.nr_map_rows; i++)
+			rows.map_rows[i].selected = true;
+		for (size_t i = 0; i < rows.nr_sys_rows; i++)
+			rows.sys_rows[i].selected = true;
+		any_selected = rows.nr_map_rows > 0 || rows.nr_sys_rows > 0;
+	} else {
+		for (size_t i = 0; i < rows.nr_map_rows; i++) {
+			struct export_map_row *r = &rows.map_rows[i];
+
+			if (arch_or_model_matches(r->arch, r->model, selector)) {
+				r->selected = true;
+				any_selected = true;
+			}
+		}
+		for (size_t i = 0; i < rows.nr_sys_rows; i++) {
+			struct export_sys_row *r = &rows.sys_rows[i];
+
+			if (arch_or_model_matches(r->arch, r->model, selector)) {
+				r->selected = true;
+				any_selected = true;
+			}
+		}
+
+		if (!any_selected) {
+			char cands[6][128];
+			size_t nr_cands = build_cpuid_candidates(selector, cands,
+								 ARRAY_SIZE(cands));
+
+			for (size_t c = 0; c < nr_cands && !any_selected; c++) {
+				for (size_t i = 0; i < rows.nr_map_rows; i++) {
+					struct export_map_row *r = &rows.map_rows[i];
+
+					if (row_cpuids_match_cand(&r->cpuids, cands[c])) {
+						r->selected = true;
+						any_selected = true;
+					}
+				}
+			}
+
+			for (size_t i = 0; i < rows.nr_sys_rows; i++) {
+				struct export_sys_row *r = &rows.sys_rows[i];
+				struct sys_compat_match_ctx mctx = {
+					.selector = selector,
+				};
+
+				if (r->events_table)
+					pmu_events_table__for_each_event(
+						r->events_table, NULL,
+						check_sys_event_compat_cb, &mctx);
+				if (!mctx.matched && r->metrics_table)
+					pmu_metrics_table__for_each_metric(
+						r->metrics_table,
+						check_sys_metric_compat_cb, &mctx);
+				if (mctx.matched) {
+					r->selected = true;
+					any_selected = true;
+				}
+			}
+		}
+	}
+
+	if (!any_selected) {
+		ret = -ENOENT;
+		goto out;
+	}
+
+	ret = pmu_layer__mkdir_p(root_dir);
+	if (ret)
+		goto out;
+
+	for (size_t i = 0; i < rows.nr_map_rows; i++) {
+		struct export_map_row *r = &rows.map_rows[i];
+		char target_root[PATH_MAX];
+		char model_key[256];
+
+		if (!r->selected)
+			continue;
+
+		if (!strcmp(r->arch, "common")) {
+			snprintf(target_root, sizeof(target_root), "%s", root_dir);
+			snprintf(model_key, sizeof(model_key), "common");
+		} else {
+			int n = snprintf(target_root, sizeof(target_root),
+					 "%s/models/%s/%s",
+					 root_dir, r->arch, r->model);
+
+			if (n < 0 || (size_t)n >= sizeof(target_root)) {
+				ret = -ENAMETOOLONG;
+				goto out;
+			}
+			snprintf(model_key, sizeof(model_key), "%s/%s",
+				 r->arch, r->model);
+		}
+
+		if (!export_str_vec__contains(&exported_models, model_key)) {
+			struct export_table_walk_ctx wctx = {
+				.root_dir = root_dir,
+				.target_root = target_root,
+				.arch = r->arch,
+				.model = r->model,
+				.opts = opts,
+				.is_sys = false,
+				.compats = &compats,
+				.nr_compats = &nr_compats,
+				.cap_compats = &cap_compats,
+			};
+
+			ret = export_str_vec__add(&exported_models, model_key);
+			if (ret)
+				goto out;
+			if (opts->events && r->events_table) {
+				ret = pmu_events_table__for_each_event(
+					r->events_table, NULL,
+					export_cpuid_event_cb, &wctx);
+				if (ret)
+					goto out;
+			}
+			if ((opts->metrics || opts->metricgroups) && r->metrics_table) {
+				ret = pmu_metrics_table__for_each_metric(
+					r->metrics_table,
+					export_cpuid_metric_cb, &wctx);
+				if (ret)
+					goto out;
+			}
+		}
+
+		if (strcmp(r->arch, "common") != 0) {
+			for (size_t j = 0; j < r->cpuids.nr; j++) {
+				ret = ensure_rel_symlink(root_dir, "cpus",
+							 r->cpuids.items[j],
+							 r->arch, r->model);
+				if (ret)
+					goto out;
+			}
+		}
+	}
+
+	for (size_t i = 0; i < rows.nr_sys_rows; i++) {
+		struct export_sys_row *r = &rows.sys_rows[i];
+		char target_root[PATH_MAX];
+		char model_key[256];
+		struct export_table_walk_ctx wctx;
+		int n;
+
+		if (!r->selected)
+			continue;
+
+		n = snprintf(target_root, sizeof(target_root), "%s/models/%s/%s",
+			     root_dir, r->arch, r->model);
+		if (n < 0 || (size_t)n >= sizeof(target_root)) {
+			ret = -ENAMETOOLONG;
+			goto out;
+		}
+		snprintf(model_key, sizeof(model_key), "sys:%s/%s", r->arch, r->model);
+		if (export_str_vec__contains(&exported_models, model_key))
+			continue;
+
+		ret = export_str_vec__add(&exported_models, model_key);
+		if (ret)
+			goto out;
+
+		wctx = (struct export_table_walk_ctx){
+			.root_dir = root_dir,
+			.target_root = target_root,
+			.arch = r->arch,
+			.model = r->model,
+			.opts = opts,
+			.is_sys = true,
+			.compats = &compats,
+			.nr_compats = &nr_compats,
+			.cap_compats = &cap_compats,
+		};
+		if (opts->events && r->events_table) {
+			ret = pmu_events_table__for_each_event(
+				r->events_table, NULL,
+				export_cpuid_event_cb, &wctx);
+			if (ret)
+				goto out;
+		}
+		if ((opts->metrics || opts->metricgroups) && r->metrics_table) {
+			ret = pmu_metrics_table__for_each_metric(
+				r->metrics_table,
+				export_cpuid_metric_cb, &wctx);
+			if (ret)
+				goto out;
+		}
+	}
+
+	for (size_t i = 0; i < nr_compats; i++) {
+		if (compats[i].ambiguous)
+			continue;
+		ret = ensure_rel_symlink(root_dir, "sys", compats[i].compat,
+					 compats[i].arch, compats[i].model);
+		if (ret)
+			goto out;
+	}
+
+out:
+	for (size_t i = 0; i < rows.nr_map_rows; i++)
+		export_str_vec__clear(&rows.map_rows[i].cpuids);
+	free(rows.map_rows);
+	free(rows.sys_rows);
+	export_str_vec__clear(&exported_models);
+	for (size_t i = 0; i < nr_compats; i++)
+		free(compats[i].compat);
+	free(compats);
+	return ret;
+}
diff --git a/tools/perf/util/pmu-layer.h b/tools/perf/util/pmu-layer.h
index 183bed9cc10e..df32acdb1b63 100644
--- a/tools/perf/util/pmu-layer.h
+++ b/tools/perf/util/pmu-layer.h
@@ -173,6 +173,16 @@ int pmu_layer__load_metrics(struct perf_pmu *pmu);
 const char *pmu_layer__describe_metricgroup(struct perf_pmu *pmu,
 					    const char *group);
 
+struct pmu_layer_export_opts {
+	const char *pmu_glob;
+	const char *event_glob;
+	bool events;
+	bool metrics;
+	bool metricgroups;
+	bool deprecated;
+	bool exclude_abi;
+};
+
 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);
@@ -183,5 +193,7 @@ int pmu_layer__export_metric(const char *root_dir, const char *eff_pmu,
 			     const char *desc, const char *long_desc,
 			     const char *expr, const char *threshold,
 			     const char *unit);
+int pmu_layer__export_cpuid(const char *root_dir, const char *selector,
+			    const struct pmu_layer_export_opts *opts);
 
 #endif /* __PMU_LAYER_H */
-- 
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 ` [PATCH 08/11] perf list, jevents: Add --export-dir and --output-dir sysfs-style exporters Ian Rogers
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 ` Ian Rogers [this message]
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-11-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®