* [PATCH 01/11] perf pmu: Generalize pmu_alias_info_file() and lazy single-event sysfs loading
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 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 02/11] perf pmu: Add metric and metricgroup storage on struct perf_pmu Ian Rogers
` (9 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Prepare struct perf_pmu_alias and sysfs event loading for layered
filesystem overlays while keeping struct perf_pmu_alias private to
pmu.c:
1. Add enum pmu_event_file_kind and pmu_event_file_kind() in pmu.c,
teaching pmu_alias_info_file() to recognize extended sidecar
extensions (.scale-unit, .desc, .long-desc, .topic, .compat,
.deprecated, .legacy-terms, .retire-lat-mean, .retire-lat-min, and
.retire-lat-max) alongside existing kernel sysfs sidecars (.unit,
.scale, .per-pkg, and .snapshot).
2. Switch sidecar attribute readers to openat() relative to an open
events directory fd, allowing boolean sidecars to express both 0
and 1 and marking alias->info_loaded once read so sidecars are not
re-opened repeatedly.
3. Make perf_pmu__find_alias() lazily open only the requested event
(trying exact, lowercase, and uppercase names) and its sidecars via
openat() instead of calling pmu_aliases_parse() to readdir() and
open every file in <sysfs>/<pmu>/events/ on a single-event lookup.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/pmu.c | 365 +++++++++++++++++++++++++++++-------------
1 file changed, 252 insertions(+), 113 deletions(-)
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index 01753d5c69d1..d10b8c0810fb 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -10,9 +10,11 @@
#include <unistd.h>
#include <errno.h>
#include <stdbool.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <strings.h>
#include <dirent.h>
#include <api/fs/fs.h>
#include <api/io.h>
@@ -54,9 +56,28 @@ enum event_source {
EVENT_SRC_SYS_JSON,
};
+enum pmu_event_file_kind {
+ PMU_EVENT_FILE_BASE,
+ PMU_EVENT_FILE_UNIT,
+ PMU_EVENT_FILE_SCALE,
+ PMU_EVENT_FILE_PER_PKG,
+ PMU_EVENT_FILE_SNAPSHOT,
+ PMU_EVENT_FILE_SCALE_UNIT,
+ PMU_EVENT_FILE_DESC,
+ PMU_EVENT_FILE_LONG_DESC,
+ PMU_EVENT_FILE_TOPIC,
+ PMU_EVENT_FILE_COMPAT,
+ PMU_EVENT_FILE_DEPRECATED,
+ PMU_EVENT_FILE_LEGACY_TERMS,
+ PMU_EVENT_FILE_RETIRE_LAT_MEAN,
+ PMU_EVENT_FILE_RETIRE_LAT_MIN,
+ PMU_EVENT_FILE_RETIRE_LAT_MAX,
+};
+
/**
- * struct perf_pmu_alias - An event either read from sysfs or builtin in
- * pmu-events.c, created by parsing the pmu-events json files.
+ * struct perf_pmu_alias - An event either read from sysfs, a directory
+ * overlay, or builtin in pmu-events.c, created by parsing the pmu-events json
+ * files.
*/
struct perf_pmu_alias {
/** @name: Name of the event like "mem-loads". */
@@ -74,7 +95,7 @@ struct perf_pmu_alias {
char *terms;
/**
* @legacy_terms: If the event aliases a legacy event, holds a copy
- * ofthe legacy event string.
+ * of the legacy event string.
*/
char *legacy_terms;
/**
@@ -82,8 +103,10 @@ struct perf_pmu_alias {
* differ from the PMU name as it won't have suffixes.
*/
char *pmu_name;
+ /** @compat: Optional uncore/system PMU identifier match pattern. */
+ char *compat;
/** @unit: Units for the event, such as bytes or cache lines. */
- char unit[UNIT_MAX_LEN+1];
+ char unit[UNIT_MAX_LEN + 1];
/** @scale: Value to scale read counter values by. */
double scale;
/** @retirement_latency_mean: Value to be given for unsampled retirement latency mean. */
@@ -111,7 +134,7 @@ struct perf_pmu_alias {
bool deprecated;
/**
* @legacy_deprecated_checked: Legacy events may not be supported by the
- * PMU need to be checked. If they aren't supported they are marked
+ * PMU and need to be checked. If they aren't supported they are marked
* deprecated.
*/
bool legacy_deprecated_checked;
@@ -119,6 +142,26 @@ struct perf_pmu_alias {
bool from_sysfs;
/** @info_loaded: Have the scale, unit and other values been read from disk? */
bool info_loaded;
+ /** @resolved: Fully resolved across all PMU layers. */
+ bool resolved;
+ /** @terms_from_legacy: @terms was promoted from @legacy_terms. */
+ bool terms_from_legacy;
+ /** @has_scale: Has scale been explicitly set by a layer? */
+ bool has_scale;
+ /** @has_unit: Has unit been explicitly set by a layer? */
+ bool has_unit;
+ /** @has_per_pkg: Has per_pkg been explicitly set by a layer? */
+ bool has_per_pkg;
+ /** @has_snapshot: Has snapshot been explicitly set by a layer? */
+ bool has_snapshot;
+ /** @has_deprecated: Has deprecated been explicitly set by a layer? */
+ bool has_deprecated;
+ /** @has_retire_lat_mean: Has retirement_latency_mean been explicitly set? */
+ bool has_retire_lat_mean;
+ /** @has_retire_lat_min: Has retirement_latency_min been explicitly set? */
+ bool has_retire_lat_min;
+ /** @has_retire_lat_max: Has retirement_latency_max been explicitly set? */
+ bool has_retire_lat_max;
};
static int pmu_aliases_parse(struct perf_pmu *pmu);
@@ -295,28 +338,64 @@ int perf_pmu__convert_scale(const char *scale, char **end, double *sval)
return parse_double(scale, end, sval);
}
-static int perf_pmu__parse_scale(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
+static const struct {
+ const char *suffix;
+ size_t len;
+ enum pmu_event_file_kind kind;
+} pmu_event_suffixes[] = {
+ { ".unit", 5, PMU_EVENT_FILE_UNIT },
+ { ".desc", 5, PMU_EVENT_FILE_DESC },
+ { ".scale", 6, PMU_EVENT_FILE_SCALE },
+ { ".topic", 6, PMU_EVENT_FILE_TOPIC },
+ { ".compat", 7, PMU_EVENT_FILE_COMPAT },
+ { ".per-pkg", 8, PMU_EVENT_FILE_PER_PKG },
+ { ".snapshot", 9, PMU_EVENT_FILE_SNAPSHOT },
+ { ".long-desc", 10, PMU_EVENT_FILE_LONG_DESC },
+ { ".scale-unit", 11, PMU_EVENT_FILE_SCALE_UNIT },
+ { ".deprecated", 11, PMU_EVENT_FILE_DEPRECATED },
+ { ".legacy-terms", 13, PMU_EVENT_FILE_LEGACY_TERMS },
+ { ".retire-lat-min", 15, PMU_EVENT_FILE_RETIRE_LAT_MIN },
+ { ".retire-lat-max", 15, PMU_EVENT_FILE_RETIRE_LAT_MAX },
+ { ".retire-lat-mean", 16, PMU_EVENT_FILE_RETIRE_LAT_MEAN },
+};
+
+static enum pmu_event_file_kind pmu_event_file_kind(const char *name, size_t *base_len)
+{
+ size_t len = strlen(name);
+
+ for (size_t i = 0; i < ARRAY_SIZE(pmu_event_suffixes); i++) {
+ size_t slen = pmu_event_suffixes[i].len;
+
+ if (len > slen && !strcasecmp(name + len - slen, pmu_event_suffixes[i].suffix)) {
+ if (base_len)
+ *base_len = len - slen;
+ return pmu_event_suffixes[i].kind;
+ }
+ }
+ if (base_len)
+ *base_len = len;
+ return PMU_EVENT_FILE_BASE;
+}
+
+static inline bool pmu_alias_info_file(const char *name)
+{
+ return pmu_event_file_kind(name, NULL) != PMU_EVENT_FILE_BASE;
+}
+
+static int perf_pmu__parse_scale_at(int events_dir_fd, const char *event_name,
+ struct perf_pmu_alias *alias)
{
- struct stat st;
ssize_t sret;
- size_t len;
char scale[128];
+ char filename[NAME_MAX + 16];
int fd, ret = -1;
- char path[PATH_MAX];
-
- len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!len)
- return 0;
- scnprintf(path + len, sizeof(path) - len, "%s/events/%s.scale", pmu->name, alias->name);
- fd = open(path, O_RDONLY);
+ scnprintf(filename, sizeof(filename), "%s.scale", event_name);
+ fd = openat(events_dir_fd, filename, O_RDONLY);
if (fd == -1)
return -1;
- if (fstat(fd, &st) < 0)
- goto error;
-
- sret = read(fd, scale, sizeof(scale)-1);
+ sret = read(fd, scale, sizeof(scale) - 1);
if (sret <= 0)
goto error;
@@ -326,25 +405,22 @@ static int perf_pmu__parse_scale(struct perf_pmu *pmu, struct perf_pmu_alias *al
scale[sret] = '\0';
ret = perf_pmu__convert_scale(scale, NULL, &alias->scale);
+ if (!ret)
+ alias->has_scale = true;
error:
close(fd);
return ret;
}
-static int perf_pmu__parse_unit(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
+static int perf_pmu__parse_unit_at(int events_dir_fd, const char *event_name,
+ struct perf_pmu_alias *alias)
{
- char path[PATH_MAX];
- size_t len;
+ char filename[NAME_MAX + 16];
ssize_t sret;
int fd;
-
- len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!len)
- return 0;
- scnprintf(path + len, sizeof(path) - len, "%s/events/%s.unit", pmu->name, alias->name);
-
- fd = open(path, O_RDONLY);
+ scnprintf(filename, sizeof(filename), "%s.unit", event_name);
+ fd = openat(events_dir_fd, filename, O_RDONLY);
if (fd == -1)
return -1;
@@ -359,6 +435,7 @@ static int perf_pmu__parse_unit(struct perf_pmu *pmu, struct perf_pmu_alias *ali
else
alias->unit[sret] = '\0';
+ alias->has_unit = true;
return 0;
error:
close(fd);
@@ -366,45 +443,57 @@ static int perf_pmu__parse_unit(struct perf_pmu *pmu, struct perf_pmu_alias *ali
return -1;
}
-static bool perf_pmu__parse_event_source_bool(const char *pmu_name, const char *event_name,
- const char *suffix)
+static int perf_pmu__parse_bool_at(int events_dir_fd, const char *event_name,
+ const char *suffix, bool *out_val)
{
- char path[PATH_MAX];
- size_t len;
+ char filename[NAME_MAX + 32];
+ char buf[16];
+ ssize_t sret;
int fd;
- len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!len)
- return false;
-
- scnprintf(path + len, sizeof(path) - len, "%s/events/%s.%s", pmu_name, event_name, suffix);
-
- fd = open(path, O_RDONLY);
+ scnprintf(filename, sizeof(filename), "%s.%s", event_name, suffix);
+ fd = openat(events_dir_fd, filename, O_RDONLY);
if (fd == -1)
- return false;
+ return -ENOENT;
-#ifndef NDEBUG
- {
- char buf[8];
+ sret = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (sret <= 0)
+ return -EINVAL;
- len = read(fd, buf, sizeof(buf));
- assert(len == 1 || len == 2);
- assert(buf[0] == '1');
- }
-#endif
+ while (sret > 0 && isspace(buf[sret - 1]))
+ sret--;
+ buf[sret] = '\0';
- close(fd);
- return true;
+ if (!strcmp(buf, "1") || !strcasecmp(buf, "true") || !strcasecmp(buf, "yes")) {
+ *out_val = true;
+ return 0;
+ }
+ if (!strcmp(buf, "0") || !strcasecmp(buf, "false") || !strcasecmp(buf, "no")) {
+ *out_val = false;
+ return 0;
+ }
+ return -EINVAL;
}
-static void perf_pmu__parse_per_pkg(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
+static void read_alias_info_at(int events_dir_fd, struct perf_pmu_alias *alias)
{
- alias->per_pkg = perf_pmu__parse_event_source_bool(pmu->name, alias->name, "per-pkg");
-}
+ bool val;
-static void perf_pmu__parse_snapshot(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
-{
- alias->snapshot = perf_pmu__parse_event_source_bool(pmu->name, alias->name, "snapshot");
+ if (!alias->from_sysfs || alias->info_loaded || events_dir_fd < 0)
+ return;
+
+ perf_pmu__parse_unit_at(events_dir_fd, alias->name, alias);
+ perf_pmu__parse_scale_at(events_dir_fd, alias->name, alias);
+ if (perf_pmu__parse_bool_at(events_dir_fd, alias->name, "per-pkg", &val) == 0) {
+ alias->per_pkg = val;
+ alias->has_per_pkg = true;
+ }
+ if (perf_pmu__parse_bool_at(events_dir_fd, alias->name, "snapshot", &val) == 0) {
+ alias->snapshot = val;
+ alias->has_snapshot = true;
+ }
+ alias->info_loaded = true;
}
/* Delete an alias entry. */
@@ -418,6 +507,7 @@ static void perf_pmu_free_alias(struct perf_pmu_alias *alias)
zfree(&alias->long_desc);
zfree(&alias->topic);
zfree(&alias->pmu_name);
+ zfree(&alias->compat);
zfree(&alias->terms);
zfree(&alias->legacy_terms);
free(alias);
@@ -438,13 +528,22 @@ static void perf_pmu__del_aliases(struct perf_pmu *pmu)
pmu->aliases = NULL;
}
+static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
+ const char *desc, const char *val, int val_fd,
+ const struct pmu_event *pe, enum event_source src,
+ int events_dir_fd);
+
static struct perf_pmu_alias *perf_pmu__find_alias(struct perf_pmu *pmu,
const char *name,
bool load)
{
- struct perf_pmu_alias *alias;
- bool has_sysfs_event;
- char event_file_name[NAME_MAX + 8];
+ struct perf_pmu_alias *alias = NULL;
+ char path[PATH_MAX];
+ char lower_name[NAME_MAX];
+ char upper_name[NAME_MAX];
+ const char *candidates[3];
+ size_t nr_candidates = 0, name_len, path_len;
+ int events_dir_fd;
if (hashmap__find(pmu->aliases, name, &alias))
return alias;
@@ -452,29 +551,61 @@ static struct perf_pmu_alias *perf_pmu__find_alias(struct perf_pmu *pmu,
if (!load || pmu->sysfs_aliases_loaded)
return NULL;
- /*
- * Test if alias/event 'name' exists in the PMU's sysfs/events
- * directory. If not skip parsing the sysfs aliases. Sysfs event
- * name must be all lower or all upper case.
- */
- scnprintf(event_file_name, sizeof(event_file_name), "events/%s", name);
- for (size_t i = 7, n = 7 + strlen(name); i < n; i++)
- event_file_name[i] = tolower(event_file_name[i]);
+ name_len = strlen(name);
+ if (name_len == 0 || name_len >= NAME_MAX || name[0] == '.' ||
+ strchr(name, '/') || pmu_alias_info_file(name))
+ return NULL;
- has_sysfs_event = perf_pmu__file_exists(pmu, event_file_name);
- if (!has_sysfs_event) {
- for (size_t i = 7, n = 7 + strlen(name); i < n; i++)
- event_file_name[i] = toupper(event_file_name[i]);
+ path_len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
+ if (!path_len)
+ return NULL;
+ scnprintf(path + path_len, sizeof(path) - path_len, "%s/events", pmu->name);
- has_sysfs_event = perf_pmu__file_exists(pmu, event_file_name);
+ events_dir_fd = open(path, O_DIRECTORY | O_RDONLY);
+ if (events_dir_fd == -1) {
+ if (errno == ENOENT)
+ pmu->sysfs_aliases_loaded = true;
+ return NULL;
}
- if (has_sysfs_event) {
- pmu_aliases_parse(pmu);
- if (hashmap__find(pmu->aliases, name, &alias))
- return alias;
+
+ candidates[nr_candidates++] = name;
+ for (size_t i = 0; i < name_len; i++) {
+ lower_name[i] = tolower((unsigned char)name[i]);
+ upper_name[i] = toupper((unsigned char)name[i]);
}
+ lower_name[name_len] = '\0';
+ upper_name[name_len] = '\0';
- return NULL;
+ if (strcmp(lower_name, name) != 0)
+ candidates[nr_candidates++] = lower_name;
+ if (strcmp(upper_name, name) != 0 && strcmp(upper_name, lower_name) != 0)
+ candidates[nr_candidates++] = upper_name;
+
+ /*
+ * Lazily open only the requested event and its sidecars via openat()
+ * rather than scanning every file in sysfs/events/.
+ */
+ for (size_t i = 0; i < nr_candidates; i++) {
+ struct stat st;
+ int fd = openat(events_dir_fd, candidates[i], O_RDONLY);
+
+ if (fd < 0)
+ continue;
+ if (fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode)) {
+ if (perf_pmu__new_alias(pmu, candidates[i], /*desc=*/NULL,
+ /*val=*/NULL, fd, /*pe=*/NULL,
+ EVENT_SRC_SYSFS, events_dir_fd) == 0 &&
+ hashmap__find(pmu->aliases, name, &alias)) {
+ read_alias_info_at(events_dir_fd, alias);
+ close(fd);
+ break;
+ }
+ }
+ close(fd);
+ }
+
+ close(events_dir_fd);
+ return alias;
}
static bool assign_str(const char *name, const char *field, char **old_str,
@@ -497,21 +628,30 @@ static bool assign_str(const char *name, const char *field, char **old_str,
static void read_alias_info(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
{
+ char path[PATH_MAX];
+ size_t len;
+ int events_dir_fd;
+
if (!alias->from_sysfs || alias->info_loaded)
return;
- /*
- * load unit name and scale if available
- */
- perf_pmu__parse_unit(pmu, alias);
- perf_pmu__parse_scale(pmu, alias);
- perf_pmu__parse_per_pkg(pmu, alias);
- perf_pmu__parse_snapshot(pmu, alias);
+ len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
+ if (!len)
+ return;
+ scnprintf(path + len, sizeof(path) - len, "%s/events", pmu->name);
+
+ events_dir_fd = open(path, O_DIRECTORY | O_RDONLY);
+ if (events_dir_fd == -1)
+ return;
+
+ read_alias_info_at(events_dir_fd, alias);
+ close(events_dir_fd);
}
struct update_alias_data {
struct perf_pmu *pmu;
struct perf_pmu_alias *alias;
+ int events_dir_fd;
bool legacy;
};
@@ -522,7 +662,10 @@ static int update_alias(const struct pmu_event *pe,
struct update_alias_data *data = vdata;
int ret = 0;
- read_alias_info(data->pmu, data->alias);
+ if (data->events_dir_fd >= 0)
+ read_alias_info_at(data->events_dir_fd, data->alias);
+ else
+ read_alias_info(data->pmu, data->alias);
assign_str(pe->name, "desc", &data->alias->desc, pe->desc);
assign_str(pe->name, "long_desc", &data->alias->long_desc, pe->long_desc);
assign_str(pe->name, "topic", &data->alias->topic, pe->topic);
@@ -559,8 +702,9 @@ static int update_alias(const struct pmu_event *pe,
}
static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
- const char *desc, const char *val, int val_fd,
- const struct pmu_event *pe, enum event_source src)
+ const char *desc, const char *val, int val_fd,
+ const struct pmu_event *pe, enum event_source src,
+ int events_dir_fd)
{
struct perf_pmu_alias *alias, *old_alias;
int ret = 0;
@@ -607,8 +751,10 @@ static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
ret = parse_double(pe->retirement_latency_max, NULL,
&alias->retirement_latency_max);
}
- if (ret)
+ if (ret) {
+ perf_pmu_free_alias(alias);
return ret;
+ }
if (val_fd < 0) {
alias->terms = strdup(val);
@@ -621,6 +767,7 @@ static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
ret = io__getline(&io, &alias->terms, &line_len) < 0 ? -errno : 0;
if (ret) {
pr_err("Failed to read alias %s\n", name);
+ perf_pmu_free_alias(alias);
return ret;
}
if (line_len >= 1 && alias->terms[line_len - 1] == '\n')
@@ -631,6 +778,7 @@ static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
alias->long_desc = long_desc ? strdup(long_desc) : NULL;
alias->topic = topic ? strdup(topic) : NULL;
alias->pmu_name = pmu_name ? strdup(pmu_name) : NULL;
+ alias->compat = (pe && pe->compat) ? strdup(pe->compat) : NULL;
if (unit) {
if (perf_pmu__convert_scale(unit, (char **)&unit, &alias->scale) < 0) {
perf_pmu_free_alias(alias);
@@ -647,6 +795,7 @@ static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
struct update_alias_data data = {
.pmu = pmu,
.alias = alias,
+ .events_dir_fd = events_dir_fd,
.legacy = false,
};
if ((pmu_events_table__find_event(pmu->events_table, pmu, name,
@@ -681,28 +830,11 @@ static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
return 0;
}
-static inline bool pmu_alias_info_file(const char *name)
-{
- size_t len;
-
- len = strlen(name);
- if (len > 5 && !strcmp(name + len - 5, ".unit"))
- return true;
- if (len > 6 && !strcmp(name + len - 6, ".scale"))
- return true;
- if (len > 8 && !strcmp(name + len - 8, ".per-pkg"))
- return true;
- if (len > 9 && !strcmp(name + len - 9, ".snapshot"))
- return true;
-
- return false;
-}
-
/*
* Reading the pmu event aliases definition, which should be located at:
* /sys/bus/event_source/devices/<dev>/events as sysfs group attributes.
*/
-static int __pmu_aliases_parse(struct perf_pmu *pmu, int events_dir_fd)
+static int __pmu_aliases_parse(struct perf_pmu *pmu, int events_dir_fd, bool eager_info)
{
struct io_dirent64 *evt_ent;
struct io_dir event_dir;
@@ -713,7 +845,7 @@ static int __pmu_aliases_parse(struct perf_pmu *pmu, int events_dir_fd)
char *name = evt_ent->d_name;
int fd;
- if (!strcmp(name, ".") || !strcmp(name, ".."))
+ if (!strcmp(name, ".") || !strcmp(name, "..") || name[0] == '.')
continue;
/*
@@ -730,8 +862,14 @@ static int __pmu_aliases_parse(struct perf_pmu *pmu, int events_dir_fd)
if (perf_pmu__new_alias(pmu, name, /*desc=*/ NULL,
/*val=*/ NULL, fd, /*pe=*/ NULL,
- EVENT_SRC_SYSFS) < 0)
+ EVENT_SRC_SYSFS, events_dir_fd) < 0)
pr_debug("Cannot set up %s\n", name);
+ else if (eager_info) {
+ struct perf_pmu_alias *alias;
+
+ if (hashmap__find(pmu->aliases, name, &alias))
+ read_alias_info_at(events_dir_fd, alias);
+ }
close(fd);
}
@@ -758,7 +896,7 @@ static int pmu_aliases_parse(struct perf_pmu *pmu)
pmu->sysfs_aliases_loaded = true;
return 0;
}
- ret = __pmu_aliases_parse(pmu, events_dir_fd);
+ ret = __pmu_aliases_parse(pmu, events_dir_fd, /*eager_info=*/false);
close(events_dir_fd);
return ret;
}
@@ -774,7 +912,7 @@ static int pmu_aliases_parse_eager(struct perf_pmu *pmu, int sysfs_fd)
pmu->sysfs_aliases_loaded = true;
return 0;
}
- ret = __pmu_aliases_parse(pmu, events_dir_fd);
+ ret = __pmu_aliases_parse(pmu, events_dir_fd, /*eager_info=*/true);
close(events_dir_fd);
return ret;
}
@@ -1066,7 +1204,7 @@ static int pmu_add_cpu_aliases_map_callback(const struct pmu_event *pe,
struct perf_pmu *pmu = vdata;
perf_pmu__new_alias(pmu, pe->name, pe->desc, pe->event, /*val_fd=*/ -1,
- pe, EVENT_SRC_CPU_JSON);
+ pe, EVENT_SRC_CPU_JSON, /*events_dir_fd=*/ -1);
return 0;
}
@@ -1119,7 +1257,8 @@ static int pmu_add_sys_aliases_iter_fn(const struct pmu_event *pe,
pe->event,
/*val_fd=*/ -1,
pe,
- EVENT_SRC_SYS_JSON);
+ EVENT_SRC_SYS_JSON,
+ /*events_dir_fd=*/ -1);
}
return 0;
}
base-commit: 4af65363b11fc4fe65fcb497f68bbe4f3e3a4d2c
prerequisite-patch-id: 3e93fd262f5e70cff78114a0736c8fc8abeb7977
prerequisite-patch-id: 5e284ee3a3af71e8bfe3e40719396cf60fbece96
prerequisite-patch-id: 4d4b5b43c0ca0ac929fb3b73db615805e022d86a
prerequisite-patch-id: 447412f5e17799157dc8981ffa28378bc5f7fe72
prerequisite-patch-id: ac7b807fcd0e795cc831307eed2d7148a609b941
prerequisite-patch-id: 8c306425c73d176dfeeede044b57f7ed383e19d1
prerequisite-patch-id: dfce7f2910fbd18bc8dc3965c52cf6b08a825180
prerequisite-patch-id: 4730aa572afe4e6d47f6aceaff92fccc15327881
prerequisite-patch-id: 17cb0dd6eaa58cec5d6309dda84f122c6e381d4d
prerequisite-patch-id: cda8414fd68671e1333246f22cbfedb980173821
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 02/11] perf pmu: Add metric and metricgroup storage on struct perf_pmu
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 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 03/11] perf pmu-layer: Introduce stateless struct pmu_layer abstraction and builtin adapter Ian Rogers
` (8 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Extend struct perf_pmu symmetrically with pmu->aliases to own resolved
metrics and metricgroup descriptions for the lifetime of the PMU while
keeping struct perf_pmu_metric and struct perf_pmu_metricgroup private
out of pmu.h:
- Add struct perf_pmu_metric, struct perf_pmu_metricgroup, and
pmu_metric_file_kind() in pmu.c for metric sidecar extensions
(.scale-unit, .desc, .long-desc, .group, .no-group, .default-group,
.threshold, .compat, .aggr-mode, .constraint, and .show-events).
- Initialize pmu->metrics, pmu->metricgroups, and pmu->metrics_table in
perf_pmu__init(), perf_pmu__lookup(), and
perf_pmu__create_placeholder_core_pmu().
- Free owned metric and metricgroup strings in perf_pmu__delete().
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/pmu.c | 230 ++++++++++++++++++++++++++++++++++++++++--
tools/perf/util/pmu.h | 18 ++++
2 files changed, 238 insertions(+), 10 deletions(-)
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index d10b8c0810fb..8baeba49d2e8 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <linux/list.h>
#include <linux/compiler.h>
+#include <linux/err.h>
#include <linux/string.h>
#include <linux/zalloc.h>
#include <linux/ctype.h>
@@ -164,6 +165,54 @@ struct perf_pmu_alias {
bool has_retire_lat_max;
};
+enum pmu_metric_file_kind {
+ PMU_METRIC_FILE_BASE,
+ PMU_METRIC_FILE_SCALE_UNIT,
+ PMU_METRIC_FILE_DESC,
+ PMU_METRIC_FILE_LONG_DESC,
+ PMU_METRIC_FILE_GROUP,
+ PMU_METRIC_FILE_NO_GROUP,
+ PMU_METRIC_FILE_DEFAULT_GROUP,
+ PMU_METRIC_FILE_THRESHOLD,
+ PMU_METRIC_FILE_COMPAT,
+ PMU_METRIC_FILE_AGGR_MODE,
+ PMU_METRIC_FILE_CONSTRAINT,
+ PMU_METRIC_FILE_SHOW_EVENTS,
+};
+
+/**
+ * struct perf_pmu_metric - Owned metric entry cached on struct perf_pmu.
+ */
+struct perf_pmu_metric {
+ char *pmu;
+ char *metric_name;
+ char *metric_group;
+ char *metric_expr;
+ char *metric_threshold;
+ char *unit;
+ char *compat;
+ char *desc;
+ char *long_desc;
+ char *metricgroup_no_group;
+ char *default_metricgroup_name;
+ enum aggr_mode_class aggr_mode;
+ enum metric_event_groups event_grouping;
+ bool default_show_events;
+ bool has_aggr_mode;
+ bool has_event_grouping;
+ bool has_default_show_events;
+};
+
+/**
+ * struct perf_pmu_metricgroup - Owned metricgroup description cached on
+ * struct perf_pmu.
+ */
+struct perf_pmu_metricgroup {
+ char *name;
+ char *desc;
+ bool from_builtin;
+};
+
static int pmu_aliases_parse(struct perf_pmu *pmu);
static struct perf_pmu_format *perf_pmu__new_format(struct list_head *list, char *name)
@@ -382,6 +431,43 @@ static inline bool pmu_alias_info_file(const char *name)
return pmu_event_file_kind(name, NULL) != PMU_EVENT_FILE_BASE;
}
+static const struct {
+ const char *suffix;
+ size_t len;
+ enum pmu_metric_file_kind kind;
+} pmu_metric_suffixes[] = {
+ { ".desc", 5, PMU_METRIC_FILE_DESC },
+ { ".group", 6, PMU_METRIC_FILE_GROUP },
+ { ".compat", 7, PMU_METRIC_FILE_COMPAT },
+ { ".no-group", 9, PMU_METRIC_FILE_NO_GROUP },
+ { ".aggr-mode", 10, PMU_METRIC_FILE_AGGR_MODE },
+ { ".long-desc", 10, PMU_METRIC_FILE_LONG_DESC },
+ { ".threshold", 10, PMU_METRIC_FILE_THRESHOLD },
+ { ".scale-unit", 11, PMU_METRIC_FILE_SCALE_UNIT },
+ { ".constraint", 11, PMU_METRIC_FILE_CONSTRAINT },
+ { ".show-events", 12, PMU_METRIC_FILE_SHOW_EVENTS },
+ { ".default-group", 14, PMU_METRIC_FILE_DEFAULT_GROUP },
+};
+
+static __maybe_unused enum pmu_metric_file_kind
+pmu_metric_file_kind(const char *name, size_t *base_len)
+{
+ size_t len = strlen(name);
+
+ for (size_t i = 0; i < ARRAY_SIZE(pmu_metric_suffixes); i++) {
+ size_t slen = pmu_metric_suffixes[i].len;
+
+ if (len > slen && !strcasecmp(name + len - slen, pmu_metric_suffixes[i].suffix)) {
+ if (base_len)
+ *base_len = len - slen;
+ return pmu_metric_suffixes[i].kind;
+ }
+ }
+ if (base_len)
+ *base_len = len;
+ return PMU_METRIC_FILE_BASE;
+}
+
static int perf_pmu__parse_scale_at(int events_dir_fd, const char *event_name,
struct perf_pmu_alias *alias)
{
@@ -518,7 +604,7 @@ static void perf_pmu__del_aliases(struct perf_pmu *pmu)
struct hashmap_entry *entry;
size_t bkt;
- if (!pmu->aliases)
+ if (IS_ERR_OR_NULL(pmu->aliases))
return;
hashmap__for_each_entry(pmu->aliases, entry, bkt)
@@ -528,6 +614,87 @@ static void perf_pmu__del_aliases(struct perf_pmu *pmu)
pmu->aliases = NULL;
}
+static void perf_pmu_free_metric(struct perf_pmu_metric *metric)
+{
+ if (!metric)
+ return;
+
+ zfree(&metric->pmu);
+ zfree(&metric->metric_name);
+ zfree(&metric->metric_group);
+ zfree(&metric->metric_expr);
+ zfree(&metric->metric_threshold);
+ zfree(&metric->unit);
+ zfree(&metric->compat);
+ zfree(&metric->desc);
+ zfree(&metric->long_desc);
+ zfree(&metric->metricgroup_no_group);
+ zfree(&metric->default_metricgroup_name);
+ free(metric);
+}
+
+static void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg)
+{
+ if (!mg)
+ return;
+
+ zfree(&mg->name);
+ zfree(&mg->desc);
+ free(mg);
+}
+
+static __maybe_unused void
+perf_pmu_metric__to_pmu_metric(const struct perf_pmu_metric *src,
+ struct pmu_metric *dst)
+{
+ *dst = (struct pmu_metric){
+ .pmu = src->pmu,
+ .metric_name = src->metric_name,
+ .metric_group = src->metric_group,
+ .metric_expr = src->metric_expr,
+ .metric_threshold = src->metric_threshold,
+ .unit = src->unit,
+ .compat = src->compat,
+ .desc = src->desc,
+ .long_desc = src->long_desc,
+ .metricgroup_no_group = src->metricgroup_no_group,
+ .default_metricgroup_name = src->default_metricgroup_name,
+ .aggr_mode = src->aggr_mode,
+ .event_grouping = src->event_grouping,
+ .default_show_events = src->default_show_events,
+ };
+}
+
+static void perf_pmu__del_metrics(struct perf_pmu *pmu)
+{
+ struct hashmap_entry *entry;
+ size_t bkt;
+
+ if (IS_ERR_OR_NULL(pmu->metrics))
+ return;
+
+ hashmap__for_each_entry(pmu->metrics, entry, bkt)
+ perf_pmu_free_metric(entry->pvalue);
+
+ hashmap__free(pmu->metrics);
+ pmu->metrics = NULL;
+}
+
+static void perf_pmu__del_metricgroups(struct perf_pmu *pmu)
+{
+ struct hashmap_entry *entry;
+ size_t bkt;
+
+ if (IS_ERR_OR_NULL(pmu->metricgroups))
+ return;
+
+ hashmap__for_each_entry(pmu->metricgroups, entry, bkt)
+ perf_pmu_free_metricgroup(entry->pvalue);
+
+ hashmap__free(pmu->metricgroups);
+ pmu->metricgroups = NULL;
+}
+
static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
const char *desc, const char *val, int val_fd,
const struct pmu_event *pe, enum event_source src,
@@ -1327,6 +1494,33 @@ static bool aliases__equal(long key1, long key2, void *ctx __maybe_unused)
return strcasecmp((const char *)key1, (const char *)key2) == 0;
}
+static size_t metrics__hash(long key, void *ctx __maybe_unused)
+{
+ return str_hash((const char *)key);
+}
+
+static bool metrics__equal(long key1, long key2, void *ctx __maybe_unused)
+{
+ return strcmp((const char *)key1, (const char *)key2) == 0;
+}
+
+static const struct pmu_metrics_table *perf_pmu__lookup_metrics_table(struct perf_pmu *pmu)
+{
+ const struct pmu_metrics_table *table = NULL;
+ const char *arch = perf_env__arch(NULL);
+ struct perf_cpu cpu = pmu ? perf_cpu_map__min(pmu->cpus) : (struct perf_cpu){ -1 };
+ char *cpuid = get_cpuid_allow_env_override(cpu);
+
+ if (cpuid) {
+ if (arch)
+ table = find_core_metrics_table(arch, cpuid);
+ free(cpuid);
+ }
+ if (!table)
+ table = pmu_metrics_table__find();
+ return table;
+}
+
int perf_pmu__init(struct perf_pmu *pmu, __u32 type, const char *name)
{
pmu->type = type;
@@ -1338,8 +1532,22 @@ int perf_pmu__init(struct perf_pmu *pmu, __u32 type, const char *name)
return -ENOMEM;
pmu->aliases = hashmap__new(aliases__hash, aliases__equal, /*ctx=*/ NULL);
- if (!pmu->aliases)
+ if (IS_ERR(pmu->aliases)) {
+ pmu->aliases = NULL;
return -ENOMEM;
+ }
+
+ pmu->metrics = hashmap__new(metrics__hash, metrics__equal, /*ctx=*/ NULL);
+ if (IS_ERR(pmu->metrics)) {
+ pmu->metrics = NULL;
+ return -ENOMEM;
+ }
+
+ pmu->metricgroups = hashmap__new(metrics__hash, metrics__equal, /*ctx=*/ NULL);
+ if (IS_ERR(pmu->metricgroups)) {
+ pmu->metricgroups = NULL;
+ return -ENOMEM;
+ }
return 0;
}
@@ -1416,6 +1624,7 @@ struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char
pmu->max_precise = pmu_max_precise(dirfd, pmu);
pmu->alias_name = pmu_find_alias_name(pmu, dirfd);
pmu->events_table = perf_pmu__find_events_table(pmu);
+ pmu->metrics_table = perf_pmu__lookup_metrics_table(pmu);
/*
* Load the sys json events/aliases when loading the PMU as each event
* may have a different compat regular expression. We therefore can't
@@ -1427,8 +1636,10 @@ struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char
perf_pmu__arch_init(pmu);
- if (eager_load)
+ if (eager_load) {
pmu_aliases_parse_eager(pmu, dirfd);
+ pmu->eager_sysfs_loaded = true;
+ }
return pmu;
}
@@ -1441,19 +1652,16 @@ struct perf_pmu *perf_pmu__create_placeholder_core_pmu(struct list_head *core_pm
if (!pmu)
return NULL;
- pmu->name = strdup("cpu");
- if (!pmu->name) {
- free(pmu);
+ if (perf_pmu__init(pmu, PERF_TYPE_RAW, "cpu") != 0) {
+ perf_pmu__delete(pmu);
return NULL;
}
pmu->is_core = true;
- pmu->type = PERF_TYPE_RAW;
pmu->cpus = cpu_map__online();
+ pmu->events_table = perf_pmu__find_events_table(pmu);
+ pmu->metrics_table = perf_pmu__lookup_metrics_table(pmu);
- INIT_LIST_HEAD(&pmu->format);
- pmu->aliases = hashmap__new(aliases__hash, aliases__equal, /*ctx=*/ NULL);
- INIT_LIST_HEAD(&pmu->caps);
list_add_tail(&pmu->list, core_pmus);
return pmu;
}
@@ -2930,6 +3138,8 @@ void perf_pmu__delete(struct perf_pmu *pmu)
perf_pmu__del_formats(&pmu->format);
perf_pmu__del_aliases(pmu);
+ perf_pmu__del_metrics(pmu);
+ perf_pmu__del_metricgroups(pmu);
perf_pmu__del_caps(pmu);
perf_cpu_map__put(pmu->cpus);
diff --git a/tools/perf/util/pmu.h b/tools/perf/util/pmu.h
index 2c8916ad073e..2587279fa0ce 100644
--- a/tools/perf/util/pmu.h
+++ b/tools/perf/util/pmu.h
@@ -148,6 +148,18 @@ struct perf_pmu {
* @events_table: The events table for json events in pmu-events.c.
*/
const struct pmu_events_table *events_table;
+ /**
+ * @metrics: Hashmap of struct perf_pmu_metric resolved across layers.
+ */
+ struct hashmap *metrics;
+ /**
+ * @metricgroups: Hashmap of struct perf_pmu_metricgroup descriptions.
+ */
+ struct hashmap *metricgroups;
+ /**
+ * @metrics_table: The metrics table for json metrics in pmu-events.c.
+ */
+ const struct pmu_metrics_table *metrics_table;
/** @sysfs_aliases: Number of sysfs aliases loaded. */
uint32_t sysfs_aliases;
/** @cpu_json_aliases: Number of json event aliases loaded specific to the CPUID. */
@@ -166,6 +178,12 @@ struct perf_pmu {
* been added?
*/
bool cpu_aliases_added;
+ /** @aliases_loaded: Have all layered event aliases been enumerated? */
+ bool aliases_loaded;
+ /** @metrics_loaded: Have all layered metrics been enumerated? */
+ bool metrics_loaded;
+ /** @eager_sysfs_loaded: Were sysfs events eagerly loaded from a custom dirfd? */
+ bool eager_sysfs_loaded;
/** @caps_initialized: Has the list caps been initialized? */
bool caps_initialized;
/** @nr_caps: The length of the list caps. */
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 03/11] perf pmu-layer: Introduce stateless struct pmu_layer abstraction and builtin adapter
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 ` 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
` (7 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Introduce tools/perf/util/pmu-layer.[ch] defining a stateless VTable
abstraction (struct pmu_layer_ops) for resolving and enumerating PMU
events, metrics, and metricgroup descriptions top-down into struct
perf_pmu's owned aliases, metrics, and metricgroups hashmaps, sharing
struct perf_pmu_alias, struct perf_pmu_metric, and struct
perf_pmu_metricgroup privately between pmu.c and pmu-layer.c via
pmu-layer.h rather than exposing them in pmu.h.
Implement the compiled-in block-compressed adapters
(PMU_LAYER_BUILTIN_CPU and PMU_LAYER_BUILTIN_COMMON) wrapping
pmu_events_table, pmu_metrics_table, uncore/system PMU compat tables,
and describe_metricgroup() with "first non-empty attribute wins"
merging semantics.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/Build | 1 +
tools/perf/util/pmu-layer.c | 1003 +++++++++++++++++++++++++++++++++++
tools/perf/util/pmu-layer.h | 170 ++++++
tools/perf/util/pmu.c | 175 +-----
4 files changed, 1182 insertions(+), 167 deletions(-)
create mode 100644 tools/perf/util/pmu-layer.c
create mode 100644 tools/perf/util/pmu-layer.h
diff --git a/tools/perf/util/Build b/tools/perf/util/Build
index 2c1f880c4c47..ae681b863ea7 100644
--- a/tools/perf/util/Build
+++ b/tools/perf/util/Build
@@ -86,6 +86,7 @@ perf-util-y += thread_map.o
perf-util-y += parse-events-flex.o
perf-util-y += parse-events-bison.o
perf-util-y += pmu.o
+perf-util-y += pmu-layer.o
perf-util-y += pmus.o
perf-util-y += pmu-flex.o
perf-util-y += pmu-bison.o
diff --git a/tools/perf/util/pmu-layer.c b/tools/perf/util/pmu-layer.c
new file mode 100644
index 000000000000..e4c50cc6d1ea
--- /dev/null
+++ b/tools/perf/util/pmu-layer.c
@@ -0,0 +1,1003 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <linux/compiler.h>
+#include <linux/err.h>
+#include <linux/zalloc.h>
+#include "debug.h"
+#include "hashmap.h"
+#include "pmu.h"
+#include "pmu-layer.h"
+#include "pmu-events/pmu-events.h"
+
+static struct pmu_layer *pmu_layers;
+static size_t nr_pmu_layers;
+static bool pmu_layers_initialized;
+
+static struct perf_pmu_alias *pmu_layer_alloc_alias(const char *name)
+{
+ struct perf_pmu_alias *alias = zalloc(sizeof(*alias));
+
+ if (!alias)
+ return NULL;
+
+ alias->name = strdup(name);
+ if (!alias->name) {
+ free(alias);
+ return NULL;
+ }
+ alias->scale = 1.0;
+ alias->unit[0] = '\0';
+ return alias;
+}
+
+static struct perf_pmu_alias *pmu_layer_get_or_create_alias(struct perf_pmu *pmu,
+ const char *name)
+{
+ struct perf_pmu_alias *alias = NULL, *old_alias = NULL;
+
+ if (hashmap__find(pmu->aliases, name, &alias))
+ return alias;
+
+ alias = pmu_layer_alloc_alias(name);
+ if (!alias)
+ return NULL;
+
+ if (hashmap__set(pmu->aliases, alias->name, alias,
+ /*old_key=*/NULL, &old_alias)) {
+ perf_pmu_free_alias(alias);
+ return NULL;
+ }
+ perf_pmu_free_alias(old_alias);
+ return alias;
+}
+
+static struct perf_pmu_metric *pmu_layer_alloc_metric(const char *name)
+{
+ struct perf_pmu_metric *metric = zalloc(sizeof(*metric));
+
+ if (!metric)
+ return NULL;
+
+ metric->metric_name = strdup(name);
+ if (!metric->metric_name) {
+ free(metric);
+ return NULL;
+ }
+ return metric;
+}
+
+static struct perf_pmu_metric *pmu_layer_get_or_create_metric(struct perf_pmu *pmu,
+ const char *name)
+{
+ struct perf_pmu_metric *metric = NULL, *old_metric = NULL;
+
+ if (hashmap__find(pmu->metrics, name, &metric))
+ return metric;
+
+ metric = pmu_layer_alloc_metric(name);
+ if (!metric)
+ return NULL;
+
+ if (hashmap__set(pmu->metrics, metric->metric_name, metric,
+ /*old_key=*/NULL, &old_metric)) {
+ perf_pmu_free_metric(metric);
+ return NULL;
+ }
+ perf_pmu_free_metric(old_metric);
+ return metric;
+}
+
+static int pmu_layer_merge_pe(struct perf_pmu_alias *alias,
+ const struct pmu_event *pe,
+ bool is_common)
+{
+ int ret = 0;
+
+ if (pe->event) {
+ if (is_common) {
+ if (!alias->legacy_terms) {
+ alias->legacy_terms = strdup(pe->event);
+ if (!alias->legacy_terms)
+ return -ENOMEM;
+ }
+ } else if (!alias->terms) {
+ alias->terms = strdup(pe->event);
+ if (!alias->terms)
+ return -ENOMEM;
+ }
+ }
+
+ if (!alias->desc && pe->desc) {
+ alias->desc = strdup(pe->desc);
+ if (!alias->desc)
+ return -ENOMEM;
+ }
+ if (!alias->long_desc && pe->long_desc) {
+ alias->long_desc = strdup(pe->long_desc);
+ if (!alias->long_desc)
+ return -ENOMEM;
+ }
+ if (!alias->topic && pe->topic) {
+ alias->topic = strdup(pe->topic);
+ if (!alias->topic)
+ return -ENOMEM;
+ }
+ if (!alias->pmu_name && pe->pmu && strcmp(pe->pmu, "default_core")) {
+ alias->pmu_name = strdup(pe->pmu);
+ if (!alias->pmu_name)
+ return -ENOMEM;
+ }
+ if (!alias->compat && pe->compat) {
+ alias->compat = strdup(pe->compat);
+ if (!alias->compat)
+ return -ENOMEM;
+ }
+
+ if (!alias->has_per_pkg && pe->perpkg) {
+ alias->per_pkg = true;
+ alias->has_per_pkg = true;
+ }
+ if (!alias->has_deprecated && pe->deprecated) {
+ alias->deprecated = true;
+ alias->has_deprecated = true;
+ }
+
+ if (pe->unit && (!alias->has_scale || !alias->has_unit)) {
+ char *unit_str = NULL;
+ double sval = 1.0;
+
+ ret = perf_pmu__convert_scale(pe->unit, &unit_str, &sval);
+ if (ret)
+ return ret;
+ if (!alias->has_scale) {
+ alias->scale = sval;
+ alias->has_scale = true;
+ }
+ if (!alias->has_unit && unit_str) {
+ snprintf(alias->unit, sizeof(alias->unit), "%s", unit_str);
+ alias->has_unit = true;
+ }
+ }
+
+ if (!alias->has_retire_lat_mean && pe->retirement_latency_mean) {
+ ret = perf_pmu__convert_scale(pe->retirement_latency_mean, NULL,
+ &alias->retirement_latency_mean);
+ if (ret)
+ return ret;
+ alias->has_retire_lat_mean = true;
+ }
+ if (!alias->has_retire_lat_min && pe->retirement_latency_min) {
+ ret = perf_pmu__convert_scale(pe->retirement_latency_min, NULL,
+ &alias->retirement_latency_min);
+ if (ret)
+ return ret;
+ alias->has_retire_lat_min = true;
+ }
+ if (!alias->has_retire_lat_max && pe->retirement_latency_max) {
+ ret = perf_pmu__convert_scale(pe->retirement_latency_max, NULL,
+ &alias->retirement_latency_max);
+ if (ret)
+ return ret;
+ alias->has_retire_lat_max = true;
+ }
+
+ return 0;
+}
+
+static int pmu_layer_merge_pm(struct perf_pmu_metric *metric,
+ const struct pmu_metric *pm)
+{
+ if (!metric->pmu && pm->pmu) {
+ metric->pmu = strdup(pm->pmu);
+ if (!metric->pmu)
+ return -ENOMEM;
+ }
+ if (!metric->metric_expr && pm->metric_expr) {
+ metric->metric_expr = strdup(pm->metric_expr);
+ if (!metric->metric_expr)
+ return -ENOMEM;
+ }
+ if (!metric->metric_group && pm->metric_group) {
+ metric->metric_group = strdup(pm->metric_group);
+ if (!metric->metric_group)
+ return -ENOMEM;
+ }
+ if (!metric->metric_threshold && pm->metric_threshold) {
+ metric->metric_threshold = strdup(pm->metric_threshold);
+ if (!metric->metric_threshold)
+ return -ENOMEM;
+ }
+ if (!metric->unit && pm->unit) {
+ metric->unit = strdup(pm->unit);
+ if (!metric->unit)
+ return -ENOMEM;
+ }
+ if (!metric->compat && pm->compat) {
+ metric->compat = strdup(pm->compat);
+ if (!metric->compat)
+ return -ENOMEM;
+ }
+ if (!metric->desc && pm->desc) {
+ metric->desc = strdup(pm->desc);
+ if (!metric->desc)
+ return -ENOMEM;
+ }
+ if (!metric->long_desc && pm->long_desc) {
+ metric->long_desc = strdup(pm->long_desc);
+ if (!metric->long_desc)
+ return -ENOMEM;
+ }
+ if (!metric->metricgroup_no_group && pm->metricgroup_no_group) {
+ metric->metricgroup_no_group = strdup(pm->metricgroup_no_group);
+ if (!metric->metricgroup_no_group)
+ return -ENOMEM;
+ }
+ if (!metric->default_metricgroup_name && pm->default_metricgroup_name) {
+ metric->default_metricgroup_name = strdup(pm->default_metricgroup_name);
+ if (!metric->default_metricgroup_name)
+ return -ENOMEM;
+ }
+ if (!metric->has_aggr_mode && pm->aggr_mode) {
+ metric->aggr_mode = pm->aggr_mode;
+ metric->has_aggr_mode = true;
+ }
+ if (!metric->has_event_grouping && pm->event_grouping != MetricGroupEvents) {
+ metric->event_grouping = pm->event_grouping;
+ metric->has_event_grouping = true;
+ }
+ if (!metric->has_default_show_events && pm->default_show_events) {
+ metric->default_show_events = true;
+ metric->has_default_show_events = true;
+ }
+ return 0;
+}
+
+struct builtin_find_event_ctx {
+ struct perf_pmu *pmu;
+ const char *name;
+ struct perf_pmu_alias *alias;
+ bool is_common;
+};
+
+static int builtin_find_event_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_find_event_ctx *ctx = vdata;
+
+ return pmu_layer_merge_pe(ctx->alias, pe, ctx->is_common);
+}
+
+static int builtin_find_sys_event_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_find_event_ctx *ctx = vdata;
+
+ if (!pe->name || !pe->compat || !pe->pmu)
+ return 0;
+ if (strcasecmp(pe->name, ctx->name) != 0)
+ return 0;
+ if (!perf_pmu__name_wildcard_match(ctx->pmu, pe->pmu))
+ return 0;
+ if (!pmu_uncore_identifier_match(pe->compat, ctx->pmu->id))
+ return 0;
+ return pmu_layer_merge_pe(ctx->alias, pe, /*is_common=*/false);
+}
+
+struct builtin_iter_event_ctx {
+ struct perf_pmu *pmu;
+ bool is_common;
+ bool override_existing;
+};
+
+static int pmu_layer_override_pe(struct perf_pmu_alias *alias,
+ const struct pmu_event *pe)
+{
+ if (pe->event) {
+ char *dup_terms = strdup(pe->event);
+
+ if (!dup_terms)
+ return -ENOMEM;
+ if (alias->terms_from_legacy && !alias->legacy_terms) {
+ alias->legacy_terms = alias->terms;
+ alias->terms = NULL;
+ alias->terms_from_legacy = false;
+ } else {
+ zfree(&alias->terms);
+ }
+ alias->terms = dup_terms;
+ }
+ if (pe->desc)
+ zfree(&alias->desc);
+ if (pe->long_desc)
+ zfree(&alias->long_desc);
+ if (pe->topic)
+ zfree(&alias->topic);
+ if (pe->pmu && strcmp(pe->pmu, "default_core"))
+ zfree(&alias->pmu_name);
+ if (pe->compat)
+ zfree(&alias->compat);
+ if (pe->perpkg)
+ alias->has_per_pkg = false;
+ if (pe->deprecated)
+ alias->has_deprecated = false;
+ if (pe->unit) {
+ alias->has_scale = false;
+ alias->has_unit = false;
+ }
+ if (pe->retirement_latency_mean)
+ alias->has_retire_lat_mean = false;
+ if (pe->retirement_latency_min)
+ alias->has_retire_lat_min = false;
+ if (pe->retirement_latency_max)
+ alias->has_retire_lat_max = false;
+
+ return pmu_layer_merge_pe(alias, pe, /*is_common=*/false);
+}
+
+static int builtin_for_each_event_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_iter_event_ctx *ctx = vdata;
+ struct perf_pmu_alias *alias;
+
+ if (!pe->name)
+ return 0;
+
+ alias = pmu_layer_get_or_create_alias(ctx->pmu, pe->name);
+ if (!alias)
+ return -ENOMEM;
+ if (ctx->override_existing)
+ return pmu_layer_override_pe(alias, pe);
+ if (alias->resolved)
+ return 0;
+
+ return pmu_layer_merge_pe(alias, pe, ctx->is_common);
+}
+
+static int builtin_for_each_sys_event_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_iter_event_ctx *ctx = vdata;
+ struct perf_pmu_alias *alias;
+
+ if (!pe->name || !pe->compat || !pe->pmu)
+ return 0;
+ if (!perf_pmu__name_wildcard_match(ctx->pmu, pe->pmu))
+ return 0;
+ if (!pmu_uncore_identifier_match(pe->compat, ctx->pmu->id))
+ return 0;
+
+ alias = pmu_layer_get_or_create_alias(ctx->pmu, pe->name);
+ if (!alias)
+ return -ENOMEM;
+ if (ctx->override_existing)
+ return pmu_layer_override_pe(alias, pe);
+ if (alias->resolved)
+ return 0;
+
+ return pmu_layer_merge_pe(alias, pe, /*is_common=*/false);
+}
+
+struct builtin_find_metric_ctx {
+ struct perf_pmu *pmu;
+ const char *name;
+ struct perf_pmu_metric *metric;
+};
+
+static int builtin_find_metric_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_find_metric_ctx *ctx = vdata;
+
+ return pmu_layer_merge_pm(ctx->metric, pm);
+}
+
+static int builtin_find_sys_metric_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *vdata)
+{
+ struct builtin_find_metric_ctx *ctx = vdata;
+
+ if (!pm->metric_name || !pm->compat || !pm->pmu)
+ return 0;
+ if (strcmp(pm->metric_name, ctx->name) != 0)
+ return 0;
+ if (!perf_pmu__name_wildcard_match(ctx->pmu, pm->pmu))
+ return 0;
+ if (!pmu_uncore_identifier_match(pm->compat, ctx->pmu->id))
+ return 0;
+ return pmu_layer_merge_pm(ctx->metric, pm);
+}
+
+static int builtin_for_each_metric_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *vdata)
+{
+ struct perf_pmu *pmu = vdata;
+ struct perf_pmu_metric *metric;
+
+ if (!pm->metric_name)
+ return 0;
+ if (pm->pmu && !perf_pmu__name_wildcard_match(pmu, pm->pmu))
+ return 0;
+
+ metric = pmu_layer_get_or_create_metric(pmu, pm->metric_name);
+ if (!metric)
+ return -ENOMEM;
+
+ return pmu_layer_merge_pm(metric, pm);
+}
+
+static int builtin_for_each_sys_metric_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *vdata)
+{
+ struct perf_pmu *pmu = vdata;
+ struct perf_pmu_metric *metric;
+
+ if (!pm->metric_name || !pm->compat || !pm->pmu)
+ return 0;
+ if (!perf_pmu__name_wildcard_match(pmu, pm->pmu))
+ return 0;
+ if (!pmu_uncore_identifier_match(pm->compat, pmu->id))
+ return 0;
+
+ metric = pmu_layer_get_or_create_metric(pmu, pm->metric_name);
+ if (!metric)
+ return -ENOMEM;
+
+ return pmu_layer_merge_pm(metric, pm);
+}
+
+static int builtin_cpu_find_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_alias *alias)
+{
+ struct builtin_find_event_ctx ctx = {
+ .pmu = pmu,
+ .name = name,
+ .alias = alias,
+ .is_common = false,
+ };
+ int ret;
+
+ if (pmu->events_table) {
+ ret = pmu_events_table__find_event(pmu->events_table, pmu, name,
+ builtin_find_event_cb, &ctx);
+ if (ret && ret != PMU_EVENTS__NOT_FOUND)
+ return ret;
+ }
+ if (pmu->id) {
+ ret = pmu_for_each_sys_event(builtin_find_sys_event_cb, &ctx);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int builtin_cpu_for_each_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu)
+{
+ struct builtin_iter_event_ctx ctx = {
+ .pmu = pmu,
+ .is_common = false,
+ };
+ int ret;
+
+ if (pmu->events_table) {
+ ret = pmu_events_table__for_each_event(pmu->events_table, pmu,
+ builtin_for_each_event_cb, &ctx);
+ if (ret)
+ return ret;
+ }
+ if (pmu->id) {
+ ret = pmu_for_each_sys_event(builtin_for_each_sys_event_cb, &ctx);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int builtin_cpu_find_metric(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_metric *metric)
+{
+ struct builtin_find_metric_ctx ctx = {
+ .pmu = pmu,
+ .name = name,
+ .metric = metric,
+ };
+ int ret;
+
+ if (pmu->metrics_table) {
+ ret = pmu_metrics_table__find_metric(pmu->metrics_table, pmu, name,
+ builtin_find_metric_cb, &ctx);
+ if (ret && ret != PMU_METRICS__NOT_FOUND)
+ return ret;
+ }
+ if (pmu->id) {
+ ret = pmu_for_each_sys_metric(builtin_find_sys_metric_cb, &ctx);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static int builtin_cpu_for_each_metric(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu)
+{
+ int ret;
+
+ if (pmu->metrics_table) {
+ ret = pmu_metrics_table__for_each_metric(pmu->metrics_table,
+ builtin_for_each_metric_cb, pmu);
+ if (ret)
+ return ret;
+ }
+ if (pmu->id) {
+ ret = pmu_for_each_sys_metric(builtin_for_each_sys_metric_cb, pmu);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+static const struct pmu_layer_ops builtin_cpu_layer_ops = {
+ .find_event = builtin_cpu_find_event,
+ .for_each_event = builtin_cpu_for_each_event,
+ .find_metric = builtin_cpu_find_metric,
+ .for_each_metric = builtin_cpu_for_each_metric,
+};
+
+static int builtin_common_find_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_alias *alias)
+{
+ const struct pmu_events_table *table;
+ struct builtin_find_event_ctx ctx = {
+ .pmu = pmu,
+ .name = name,
+ .alias = alias,
+ .is_common = true,
+ };
+ int ret;
+
+ if (!pmu->is_core)
+ return 0;
+
+ table = perf_pmu__default_core_events_table();
+ if (!table)
+ return 0;
+
+ ret = pmu_events_table__find_event(table, pmu, name,
+ builtin_find_event_cb, &ctx);
+ if (ret == PMU_EVENTS__NOT_FOUND)
+ return 0;
+ return ret;
+}
+
+static int builtin_common_for_each_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu)
+{
+ const struct pmu_events_table *table;
+ struct builtin_iter_event_ctx ctx = {
+ .pmu = pmu,
+ .is_common = true,
+ };
+
+ if (!pmu->is_core)
+ return 0;
+
+ table = perf_pmu__default_core_events_table();
+ if (!table)
+ return 0;
+
+ return pmu_events_table__for_each_event(table, pmu,
+ builtin_for_each_event_cb, &ctx);
+}
+
+static int builtin_common_find_metric(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_metric *metric)
+{
+ const struct pmu_metrics_table *table;
+ struct builtin_find_metric_ctx ctx = {
+ .pmu = pmu,
+ .name = name,
+ .metric = metric,
+ };
+ int ret;
+
+ if (!pmu->is_core)
+ return 0;
+
+ table = pmu_metrics_table__default();
+ if (!table)
+ return 0;
+
+ ret = pmu_metrics_table__find_metric(table, pmu, name,
+ builtin_find_metric_cb, &ctx);
+ if (ret == PMU_METRICS__NOT_FOUND)
+ return 0;
+ return ret;
+}
+
+static int builtin_common_for_each_metric(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu)
+{
+ const struct pmu_metrics_table *table;
+
+ if (!pmu->is_core)
+ return 0;
+
+ table = pmu_metrics_table__default();
+ if (!table)
+ return 0;
+
+ return pmu_metrics_table__for_each_metric(table,
+ builtin_for_each_metric_cb, pmu);
+}
+
+static int builtin_common_load_metricgroup(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu,
+ const char *group,
+ struct perf_pmu_metricgroup *mg)
+{
+ const char *desc;
+
+ if (!pmu->is_core || mg->desc)
+ return 0;
+
+ desc = describe_metricgroup(group);
+ if (!desc)
+ return 0;
+
+ mg->desc = strdup(desc);
+ if (!mg->desc)
+ return -ENOMEM;
+ mg->from_builtin = true;
+ return 0;
+}
+
+static const struct pmu_layer_ops builtin_common_layer_ops = {
+ .find_event = builtin_common_find_event,
+ .for_each_event = builtin_common_for_each_event,
+ .find_metric = builtin_common_find_metric,
+ .for_each_metric = builtin_common_for_each_metric,
+ .load_metricgroup = builtin_common_load_metricgroup,
+};
+
+static int pmu_layers_append(enum pmu_layer_kind kind,
+ const struct pmu_layer_ops *ops,
+ const char *root_path)
+{
+ struct pmu_layer *next;
+ char *dup_path = NULL;
+
+ if (root_path) {
+ dup_path = strdup(root_path);
+ if (!dup_path)
+ return -ENOMEM;
+ }
+
+ next = realloc(pmu_layers, (nr_pmu_layers + 1) * sizeof(*pmu_layers));
+ if (!next) {
+ free(dup_path);
+ return -ENOMEM;
+ }
+ pmu_layers = next;
+ pmu_layers[nr_pmu_layers++] = (struct pmu_layer){
+ .kind = kind,
+ .ops = ops,
+ .root_path = dup_path,
+ };
+ return 0;
+}
+
+void pmu_layers__exit(void)
+{
+ for (size_t i = 0; i < nr_pmu_layers; i++)
+ zfree(&pmu_layers[i].root_path);
+ zfree(&pmu_layers);
+ nr_pmu_layers = 0;
+ pmu_layers_initialized = false;
+}
+
+static int pmu_layers_ensure_init(void)
+{
+ int ret;
+
+ if (pmu_layers_initialized)
+ return 0;
+
+ ret = pmu_layers_append(PMU_LAYER_BUILTIN_CPU, &builtin_cpu_layer_ops, NULL);
+ if (ret)
+ goto err;
+
+ ret = pmu_layers_append(PMU_LAYER_BUILTIN_COMMON, &builtin_common_layer_ops, NULL);
+ if (ret)
+ goto err;
+
+ pmu_layers_initialized = true;
+ return 0;
+
+err:
+ pmu_layers__exit();
+ return ret;
+}
+
+static void pmu_layer_finalize_aliases(struct perf_pmu *pmu, bool promote_legacy)
+{
+ struct hashmap_entry *cur, *tmp;
+ size_t bkt;
+
+ hashmap__for_each_entry_safe(pmu->aliases, cur, tmp, bkt) {
+ struct perf_pmu_alias *alias = cur->pvalue;
+
+ if (promote_legacy && !alias->terms && alias->legacy_terms) {
+ alias->terms = alias->legacy_terms;
+ alias->legacy_terms = NULL;
+ alias->terms_from_legacy = true;
+ }
+ if (!alias->terms) {
+ hashmap__delete(pmu->aliases, cur->pkey, NULL, NULL);
+ perf_pmu_free_alias(alias);
+ } else if (promote_legacy) {
+ alias->resolved = true;
+ }
+ }
+}
+
+static void pmu_layer_prune_incomplete_metrics(struct perf_pmu *pmu)
+{
+ struct hashmap_entry *cur, *tmp;
+ size_t bkt;
+
+ hashmap__for_each_entry_safe(pmu->metrics, cur, tmp, bkt) {
+ struct perf_pmu_metric *metric = cur->pvalue;
+
+ if (!metric->metric_expr) {
+ hashmap__delete(pmu->metrics, cur->pkey, NULL, NULL);
+ perf_pmu_free_metric(metric);
+ }
+ }
+}
+
+struct perf_pmu_alias *pmu_layer__find_event(struct perf_pmu *pmu,
+ const char *name)
+{
+ struct perf_pmu_alias *alias = NULL, *old_alias = NULL;
+
+ if (!pmu || !name || !name[0] || name[0] == '.' ||
+ pmu_event_file_kind(name, NULL) != PMU_EVENT_FILE_BASE)
+ return NULL;
+
+ if (hashmap__find(pmu->aliases, name, &alias))
+ return alias->terms ? alias : NULL;
+
+ if (pmu->aliases_loaded)
+ return NULL;
+
+ if (pmu_layers_ensure_init())
+ return NULL;
+
+ alias = pmu_layer_alloc_alias(name);
+ if (!alias)
+ return NULL;
+
+ for (size_t i = 0; i < nr_pmu_layers; i++) {
+ const struct pmu_layer *layer = &pmu_layers[i];
+
+ if (!layer->ops->find_event)
+ continue;
+ if (layer->ops->find_event(layer, pmu, name, alias) < 0) {
+ perf_pmu_free_alias(alias);
+ return NULL;
+ }
+ }
+
+ if (!alias->terms && alias->legacy_terms) {
+ alias->terms = alias->legacy_terms;
+ alias->legacy_terms = NULL;
+ alias->terms_from_legacy = true;
+ }
+
+ if (!alias->terms) {
+ perf_pmu_free_alias(alias);
+ return NULL;
+ }
+
+ alias->resolved = true;
+ if (hashmap__set(pmu->aliases, alias->name, alias,
+ /*old_key=*/NULL, &old_alias)) {
+ perf_pmu_free_alias(alias);
+ return NULL;
+ }
+ perf_pmu_free_alias(old_alias);
+ return alias;
+}
+
+int pmu_layer__load_events(struct perf_pmu *pmu)
+{
+ int ret;
+
+ if (!pmu || pmu->aliases_loaded)
+ return 0;
+
+ ret = pmu_layers_ensure_init();
+ if (ret)
+ return ret;
+
+ /*
+ * Any single-event entries cached before full enumeration remain in
+ * pmu->aliases; top-down iteration fills any missing events cleanly.
+ */
+ for (size_t i = 0; i < nr_pmu_layers; i++) {
+ const struct pmu_layer *layer = &pmu_layers[i];
+
+ if (!layer->ops->for_each_event)
+ continue;
+ ret = layer->ops->for_each_event(layer, pmu);
+ if (ret) {
+ pmu_layer_finalize_aliases(pmu, /*promote_legacy=*/false);
+ return ret;
+ }
+ }
+
+ pmu_layer_finalize_aliases(pmu, /*promote_legacy=*/true);
+ pmu->aliases_loaded = true;
+ return 0;
+}
+
+int pmu_layer__merge_event_table(struct perf_pmu *pmu,
+ const struct pmu_events_table *table)
+{
+ struct builtin_iter_event_ctx ctx = {
+ .pmu = pmu,
+ .is_common = false,
+ .override_existing = true,
+ };
+
+ if (!pmu || !table)
+ return 0;
+
+ return pmu_events_table__for_each_event(table, pmu,
+ builtin_for_each_event_cb, &ctx);
+}
+
+struct perf_pmu_metric *pmu_layer__find_metric(struct perf_pmu *pmu,
+ const char *name)
+{
+ struct perf_pmu_metric *metric = NULL, *old_metric = NULL;
+
+ if (!pmu || !name || !name[0] || name[0] == '.' ||
+ pmu_metric_file_kind(name, NULL) != PMU_METRIC_FILE_BASE)
+ return NULL;
+
+ if (hashmap__find(pmu->metrics, name, &metric))
+ return metric->metric_expr ? metric : NULL;
+
+ if (pmu->metrics_loaded)
+ return NULL;
+
+ if (pmu_layers_ensure_init())
+ return NULL;
+
+ metric = pmu_layer_alloc_metric(name);
+ if (!metric)
+ return NULL;
+
+ for (size_t i = 0; i < nr_pmu_layers; i++) {
+ const struct pmu_layer *layer = &pmu_layers[i];
+
+ if (!layer->ops->find_metric)
+ continue;
+ if (layer->ops->find_metric(layer, pmu, name, metric) < 0) {
+ perf_pmu_free_metric(metric);
+ return NULL;
+ }
+ }
+
+ if (!metric->metric_expr) {
+ perf_pmu_free_metric(metric);
+ return NULL;
+ }
+
+ if (hashmap__set(pmu->metrics, metric->metric_name, metric,
+ /*old_key=*/NULL, &old_metric)) {
+ perf_pmu_free_metric(metric);
+ return NULL;
+ }
+ perf_pmu_free_metric(old_metric);
+ return metric;
+}
+
+int pmu_layer__load_metrics(struct perf_pmu *pmu)
+{
+ int ret;
+
+ if (!pmu || pmu->metrics_loaded)
+ return 0;
+
+ ret = pmu_layers_ensure_init();
+ if (ret)
+ return ret;
+
+ for (size_t i = 0; i < nr_pmu_layers; i++) {
+ const struct pmu_layer *layer = &pmu_layers[i];
+
+ if (!layer->ops->for_each_metric)
+ continue;
+ ret = layer->ops->for_each_metric(layer, pmu);
+ if (ret) {
+ pmu_layer_prune_incomplete_metrics(pmu);
+ return ret;
+ }
+ }
+
+ pmu_layer_prune_incomplete_metrics(pmu);
+ pmu->metrics_loaded = true;
+ return 0;
+}
+
+const char *pmu_layer__describe_metricgroup(struct perf_pmu *pmu,
+ const char *group)
+{
+ struct perf_pmu_metricgroup *mg = NULL, *old_mg = NULL;
+
+ if (!pmu || !group || !group[0] || group[0] == '.')
+ return NULL;
+
+ if (hashmap__find(pmu->metricgroups, group, &mg))
+ return mg->desc;
+
+ if (pmu_layers_ensure_init())
+ return NULL;
+
+ mg = zalloc(sizeof(*mg));
+ if (!mg)
+ return NULL;
+
+ mg->name = strdup(group);
+ if (!mg->name) {
+ free(mg);
+ return NULL;
+ }
+
+ for (size_t i = 0; i < nr_pmu_layers; i++) {
+ const struct pmu_layer *layer = &pmu_layers[i];
+
+ if (!layer->ops->load_metricgroup)
+ continue;
+ if (layer->ops->load_metricgroup(layer, pmu, group, mg) < 0) {
+ perf_pmu_free_metricgroup(mg);
+ return NULL;
+ }
+ if (mg->desc)
+ break;
+ }
+
+ if (!mg->desc) {
+ perf_pmu_free_metricgroup(mg);
+ return NULL;
+ }
+
+ if (hashmap__set(pmu->metricgroups, mg->name, mg,
+ /*old_key=*/NULL, &old_mg)) {
+ perf_pmu_free_metricgroup(mg);
+ return NULL;
+ }
+ perf_pmu_free_metricgroup(old_mg);
+ return mg->desc;
+}
diff --git a/tools/perf/util/pmu-layer.h b/tools/perf/util/pmu-layer.h
new file mode 100644
index 000000000000..330af9415448
--- /dev/null
+++ b/tools/perf/util/pmu-layer.h
@@ -0,0 +1,170 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+#ifndef __PMU_LAYER_H
+#define __PMU_LAYER_H
+
+#include <stdbool.h>
+#include <stddef.h>
+#include "pmu-events/pmu-events.h"
+
+#define UNIT_MAX_LEN 31 /* max length for event unit name */
+
+struct perf_pmu;
+struct pmu_layer;
+
+enum pmu_event_file_kind {
+ PMU_EVENT_FILE_BASE,
+ PMU_EVENT_FILE_UNIT,
+ PMU_EVENT_FILE_SCALE,
+ PMU_EVENT_FILE_PER_PKG,
+ PMU_EVENT_FILE_SNAPSHOT,
+ PMU_EVENT_FILE_SCALE_UNIT,
+ PMU_EVENT_FILE_DESC,
+ PMU_EVENT_FILE_LONG_DESC,
+ PMU_EVENT_FILE_TOPIC,
+ PMU_EVENT_FILE_COMPAT,
+ PMU_EVENT_FILE_DEPRECATED,
+ PMU_EVENT_FILE_LEGACY_TERMS,
+ PMU_EVENT_FILE_RETIRE_LAT_MEAN,
+ PMU_EVENT_FILE_RETIRE_LAT_MIN,
+ PMU_EVENT_FILE_RETIRE_LAT_MAX,
+};
+
+/**
+ * struct perf_pmu_alias - Internal resolved event entry cached on
+ * struct perf_pmu->aliases (shared only between pmu.c and pmu-layer.c).
+ */
+struct perf_pmu_alias {
+ char *name;
+ char *desc;
+ char *long_desc;
+ char *topic;
+ char *terms;
+ char *legacy_terms;
+ char *pmu_name;
+ char *compat;
+ char unit[UNIT_MAX_LEN + 1];
+ double scale;
+ double retirement_latency_mean;
+ double retirement_latency_min;
+ double retirement_latency_max;
+ bool per_pkg;
+ bool snapshot;
+ bool deprecated;
+ bool legacy_deprecated_checked;
+ bool from_sysfs;
+ bool info_loaded;
+ bool resolved;
+ bool terms_from_legacy;
+ bool has_scale;
+ bool has_unit;
+ bool has_per_pkg;
+ bool has_snapshot;
+ bool has_deprecated;
+ bool has_retire_lat_mean;
+ bool has_retire_lat_min;
+ bool has_retire_lat_max;
+};
+
+enum pmu_metric_file_kind {
+ PMU_METRIC_FILE_BASE,
+ PMU_METRIC_FILE_SCALE_UNIT,
+ PMU_METRIC_FILE_DESC,
+ PMU_METRIC_FILE_LONG_DESC,
+ PMU_METRIC_FILE_GROUP,
+ PMU_METRIC_FILE_NO_GROUP,
+ PMU_METRIC_FILE_DEFAULT_GROUP,
+ PMU_METRIC_FILE_THRESHOLD,
+ PMU_METRIC_FILE_COMPAT,
+ PMU_METRIC_FILE_AGGR_MODE,
+ PMU_METRIC_FILE_CONSTRAINT,
+ PMU_METRIC_FILE_SHOW_EVENTS,
+};
+
+/**
+ * struct perf_pmu_metric - Internal resolved metric entry cached on
+ * struct perf_pmu->metrics (shared only between pmu.c and pmu-layer.c).
+ */
+struct perf_pmu_metric {
+ char *pmu;
+ char *metric_name;
+ char *metric_group;
+ char *metric_expr;
+ char *metric_threshold;
+ char *unit;
+ char *compat;
+ char *desc;
+ char *long_desc;
+ char *metricgroup_no_group;
+ char *default_metricgroup_name;
+ enum aggr_mode_class aggr_mode;
+ enum metric_event_groups event_grouping;
+ bool default_show_events;
+ bool has_aggr_mode;
+ bool has_event_grouping;
+ bool has_default_show_events;
+};
+
+/**
+ * struct perf_pmu_metricgroup - Internal metricgroup description cached on
+ * struct perf_pmu->metricgroups.
+ */
+struct perf_pmu_metricgroup {
+ char *name;
+ char *desc;
+ bool from_builtin;
+};
+
+enum pmu_layer_kind {
+ PMU_LAYER_USER_DIR,
+ PMU_LAYER_BUILTIN_CPU,
+ PMU_LAYER_SYSFS,
+ PMU_LAYER_BUILTIN_COMMON,
+};
+
+struct pmu_layer_ops {
+ int (*find_event)(const struct pmu_layer *layer, struct perf_pmu *pmu,
+ const char *name, struct perf_pmu_alias *alias);
+ int (*for_each_event)(const struct pmu_layer *layer,
+ struct perf_pmu *pmu);
+
+ int (*find_metric)(const struct pmu_layer *layer, struct perf_pmu *pmu,
+ const char *name, struct perf_pmu_metric *metric);
+ int (*for_each_metric)(const struct pmu_layer *layer,
+ struct perf_pmu *pmu);
+
+ int (*load_metricgroup)(const struct pmu_layer *layer,
+ struct perf_pmu *pmu,
+ const char *group,
+ struct perf_pmu_metricgroup *mg);
+};
+
+struct pmu_layer {
+ enum pmu_layer_kind kind;
+ const struct pmu_layer_ops *ops;
+ char *root_path;
+};
+
+void pmu_layers__exit(void);
+
+enum pmu_event_file_kind pmu_event_file_kind(const char *name, size_t *base_len);
+enum pmu_metric_file_kind pmu_metric_file_kind(const char *name, size_t *base_len);
+void perf_pmu_free_alias(struct perf_pmu_alias *alias);
+void perf_pmu_free_metric(struct perf_pmu_metric *metric);
+void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg);
+void perf_pmu_metric__to_pmu_metric(const struct perf_pmu_metric *src,
+ struct pmu_metric *dst);
+
+struct perf_pmu_alias *pmu_layer__find_event(struct perf_pmu *pmu,
+ const char *name);
+int pmu_layer__load_events(struct perf_pmu *pmu);
+int pmu_layer__merge_event_table(struct perf_pmu *pmu,
+ const struct pmu_events_table *table);
+
+struct perf_pmu_metric *pmu_layer__find_metric(struct perf_pmu *pmu,
+ const char *name);
+int pmu_layer__load_metrics(struct perf_pmu *pmu);
+
+const char *pmu_layer__describe_metricgroup(struct perf_pmu *pmu,
+ const char *group);
+
+#endif /* __PMU_LAYER_H */
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index 8baeba49d2e8..685c61935f50 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -26,6 +26,7 @@
#include "debug.h"
#include "evsel.h"
#include "pmu.h"
+#include "pmu-layer.h"
#include "drm_pmu.h"
#include "hwmon_pmu.h"
#include "pmus.h"
@@ -43,8 +44,6 @@
#include "util/evsel_config.h"
#include <regex.h>
-#define UNIT_MAX_LEN 31 /* max length for event unit name */
-
enum event_source {
/* An event loaded from /sys/bus/event_source/devices/<pmu>/events. */
EVENT_SRC_SYSFS,
@@ -57,162 +56,6 @@ enum event_source {
EVENT_SRC_SYS_JSON,
};
-enum pmu_event_file_kind {
- PMU_EVENT_FILE_BASE,
- PMU_EVENT_FILE_UNIT,
- PMU_EVENT_FILE_SCALE,
- PMU_EVENT_FILE_PER_PKG,
- PMU_EVENT_FILE_SNAPSHOT,
- PMU_EVENT_FILE_SCALE_UNIT,
- PMU_EVENT_FILE_DESC,
- PMU_EVENT_FILE_LONG_DESC,
- PMU_EVENT_FILE_TOPIC,
- PMU_EVENT_FILE_COMPAT,
- PMU_EVENT_FILE_DEPRECATED,
- PMU_EVENT_FILE_LEGACY_TERMS,
- PMU_EVENT_FILE_RETIRE_LAT_MEAN,
- PMU_EVENT_FILE_RETIRE_LAT_MIN,
- PMU_EVENT_FILE_RETIRE_LAT_MAX,
-};
-
-/**
- * struct perf_pmu_alias - An event either read from sysfs, a directory
- * overlay, or builtin in pmu-events.c, created by parsing the pmu-events json
- * files.
- */
-struct perf_pmu_alias {
- /** @name: Name of the event like "mem-loads". */
- char *name;
- /** @desc: Optional short description of the event. */
- char *desc;
- /** @long_desc: Optional long description. */
- char *long_desc;
- /**
- * @topic: Optional topic such as cache or pipeline, particularly for
- * json events.
- */
- char *topic;
- /** @terms: Owned copy of the event terms. */
- char *terms;
- /**
- * @legacy_terms: If the event aliases a legacy event, holds a copy
- * of the legacy event string.
- */
- char *legacy_terms;
- /**
- * @pmu_name: The name copied from the json struct pmu_event. This can
- * differ from the PMU name as it won't have suffixes.
- */
- char *pmu_name;
- /** @compat: Optional uncore/system PMU identifier match pattern. */
- char *compat;
- /** @unit: Units for the event, such as bytes or cache lines. */
- char unit[UNIT_MAX_LEN + 1];
- /** @scale: Value to scale read counter values by. */
- double scale;
- /** @retirement_latency_mean: Value to be given for unsampled retirement latency mean. */
- double retirement_latency_mean;
- /** @retirement_latency_min: Value to be given for unsampled retirement latency min. */
- double retirement_latency_min;
- /** @retirement_latency_max: Value to be given for unsampled retirement latency max. */
- double retirement_latency_max;
- /**
- * @per_pkg: Does the file
- * <sysfs>/bus/event_source/devices/<pmu_name>/events/<name>.per-pkg or
- * equivalent json value exist and have the value 1.
- */
- bool per_pkg;
- /**
- * @snapshot: Does the file
- * <sysfs>/bus/event_source/devices/<pmu_name>/events/<name>.snapshot
- * exist and have the value 1.
- */
- bool snapshot;
- /**
- * @deprecated: Is the event hidden and so not shown in perf list by
- * default.
- */
- bool deprecated;
- /**
- * @legacy_deprecated_checked: Legacy events may not be supported by the
- * PMU and need to be checked. If they aren't supported they are marked
- * deprecated.
- */
- bool legacy_deprecated_checked;
- /** @from_sysfs: Was the alias from sysfs or a json event? */
- bool from_sysfs;
- /** @info_loaded: Have the scale, unit and other values been read from disk? */
- bool info_loaded;
- /** @resolved: Fully resolved across all PMU layers. */
- bool resolved;
- /** @terms_from_legacy: @terms was promoted from @legacy_terms. */
- bool terms_from_legacy;
- /** @has_scale: Has scale been explicitly set by a layer? */
- bool has_scale;
- /** @has_unit: Has unit been explicitly set by a layer? */
- bool has_unit;
- /** @has_per_pkg: Has per_pkg been explicitly set by a layer? */
- bool has_per_pkg;
- /** @has_snapshot: Has snapshot been explicitly set by a layer? */
- bool has_snapshot;
- /** @has_deprecated: Has deprecated been explicitly set by a layer? */
- bool has_deprecated;
- /** @has_retire_lat_mean: Has retirement_latency_mean been explicitly set? */
- bool has_retire_lat_mean;
- /** @has_retire_lat_min: Has retirement_latency_min been explicitly set? */
- bool has_retire_lat_min;
- /** @has_retire_lat_max: Has retirement_latency_max been explicitly set? */
- bool has_retire_lat_max;
-};
-
-enum pmu_metric_file_kind {
- PMU_METRIC_FILE_BASE,
- PMU_METRIC_FILE_SCALE_UNIT,
- PMU_METRIC_FILE_DESC,
- PMU_METRIC_FILE_LONG_DESC,
- PMU_METRIC_FILE_GROUP,
- PMU_METRIC_FILE_NO_GROUP,
- PMU_METRIC_FILE_DEFAULT_GROUP,
- PMU_METRIC_FILE_THRESHOLD,
- PMU_METRIC_FILE_COMPAT,
- PMU_METRIC_FILE_AGGR_MODE,
- PMU_METRIC_FILE_CONSTRAINT,
- PMU_METRIC_FILE_SHOW_EVENTS,
-};
-
-/**
- * struct perf_pmu_metric - Owned metric entry cached on struct perf_pmu.
- */
-struct perf_pmu_metric {
- char *pmu;
- char *metric_name;
- char *metric_group;
- char *metric_expr;
- char *metric_threshold;
- char *unit;
- char *compat;
- char *desc;
- char *long_desc;
- char *metricgroup_no_group;
- char *default_metricgroup_name;
- enum aggr_mode_class aggr_mode;
- enum metric_event_groups event_grouping;
- bool default_show_events;
- bool has_aggr_mode;
- bool has_event_grouping;
- bool has_default_show_events;
-};
-
-/**
- * struct perf_pmu_metricgroup - Owned metricgroup description cached on
- * struct perf_pmu.
- */
-struct perf_pmu_metricgroup {
- char *name;
- char *desc;
- bool from_builtin;
-};
-
static int pmu_aliases_parse(struct perf_pmu *pmu);
static struct perf_pmu_format *perf_pmu__new_format(struct list_head *list, char *name)
@@ -408,7 +251,7 @@ static const struct {
{ ".retire-lat-mean", 16, PMU_EVENT_FILE_RETIRE_LAT_MEAN },
};
-static enum pmu_event_file_kind pmu_event_file_kind(const char *name, size_t *base_len)
+enum pmu_event_file_kind pmu_event_file_kind(const char *name, size_t *base_len)
{
size_t len = strlen(name);
@@ -449,8 +292,7 @@ static const struct {
{ ".default-group", 14, PMU_METRIC_FILE_DEFAULT_GROUP },
};
-static __maybe_unused enum pmu_metric_file_kind
-pmu_metric_file_kind(const char *name, size_t *base_len)
+enum pmu_metric_file_kind pmu_metric_file_kind(const char *name, size_t *base_len)
{
size_t len = strlen(name);
@@ -583,7 +425,7 @@ static void read_alias_info_at(int events_dir_fd, struct perf_pmu_alias *alias)
}
/* Delete an alias entry. */
-static void perf_pmu_free_alias(struct perf_pmu_alias *alias)
+void perf_pmu_free_alias(struct perf_pmu_alias *alias)
{
if (!alias)
return;
@@ -614,7 +456,7 @@ static void perf_pmu__del_aliases(struct perf_pmu *pmu)
pmu->aliases = NULL;
}
-static void perf_pmu_free_metric(struct perf_pmu_metric *metric)
+void perf_pmu_free_metric(struct perf_pmu_metric *metric)
{
if (!metric)
return;
@@ -633,7 +475,7 @@ static void perf_pmu_free_metric(struct perf_pmu_metric *metric)
free(metric);
}
-static void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg)
+void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg)
{
if (!mg)
return;
@@ -643,9 +485,8 @@ static void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg)
free(mg);
}
-static __maybe_unused void
-perf_pmu_metric__to_pmu_metric(const struct perf_pmu_metric *src,
- struct pmu_metric *dst)
+void perf_pmu_metric__to_pmu_metric(const struct perf_pmu_metric *src,
+ struct pmu_metric *dst)
{
*dst = (struct pmu_metric){
.pmu = src->pmu,
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 04/11] perf pmu-layer: Add openat-based directory layer with literal CPUID/symlink lookup
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (2 preceding siblings ...)
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 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 05/11] perf config: Add pmu-events.dir configuration option Ian Rogers
` (6 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Add the filesystem-backed PMU layer backends (user_dir_layer_ops and
sysfs_layer_ops) in util/pmu-layer.c using openat() and io_dir.
A user or system overlay directory <root> supports both portable
multi-CPU trees (keyed by CPUID or uncore identifier) and host-direct
PMU overrides without requiring a runtime mapfile.csv or regex engine:
<root>/
|-- cpus/
| |-- GenuineIntel-6-4E -> ../models/x86/skylake
| |-- GenuineIntel-6-5E -> ../models/x86/skylake
| |-- GenuineIntel-6-8F -> ../models/x86/sapphirerapids
| |-- GenuineIntel-6-8F-8/ # stepping-specific override
| | `-- cpu/events/inst_retired.any.desc
| `-- 0x00000000410fd490 -> ../models/arm64/arm/neoverse-n2
|-- sys/
| `-- 43401 -> ../models/arm64/arm/cmn
|-- models/
| |-- x86/skylake/
| | |-- cpu/{events,metrics}/...
| | `-- uncore_imc/{events,metrics}/...
| `-- arm64/arm/neoverse-n2/
| `-- armv8_pmuv3/{events,metrics}/...
|-- default_core/ # cross-CPU or host-local core
| `-- metrics/my_ipc{,.desc}
|-- <pmu>/{events,metrics}/... # host-direct PMU overrides
`-- metricgroups/<group>.desc
Within each <root>, dir_for_each_scope() probes three scopes in order:
1. CPUID scope (<root>/cpus/<cpuid>/):
Queries the PMU's CPUID via get_cpuid_allow_env_override() (using
perf_cpu_map__min(pmu->cpus) so heterogeneous core PMUs such as
cpu_core and cpu_atom resolve against their own CPU's CPUID, and
honoring PERF_CPUID). build_cpuid_candidates() probes the exact
CPUID first, followed by deterministic base CPUIDs stripped of
stepping or revision fields:
- '-' or ',' delimited CPUIDs (x86, s390, RISC-V): strips trailing
fields down to two components, e.g.:
x86: GenuineIntel-6-8F-8 -> GenuineIntel-6-8F
s390: IBM,8561,703 -> IBM,8561
RISC-V: 0x489-0x8000000000000007-0x1 -> 0x489-0x8000000000000007
- ARM64 64-bit hex MIDR (0x + 16 hex digits): masks Variant[23:20]
and Revision[3:0] to zero, e.g.:
0x00000000411fd493 -> 0x00000000410fd490
- PowerPC 32-bit hex PVR (0x + 8 hex digits): masks Revision[15:0]
to zero, e.g.:
0x00800200 -> 0x00800000
Each <root> stops at the first matching cpus/<candidate> directory
so a stepping directory (such as cpus/GenuineIntel-6-8F-8/) does
not merge coarser model events from cpus/GenuineIntel-6-8F/ in the
same <root>, while unset sidecars and unoverridden events still fall
through to subsequent directories in pmu-events.dir, inbuilt JSON,
and sysfs. Because openat() traverses POSIX symlinks in the kernel
VFS, multiple CPUIDs sharing a microarchitecture can symlink to a
single canonical models/<arch>/<model>/ directory.
2. System/uncore identifier scope (<root>/sys/<id>/):
Probes pmu->id read from /sys/bus/event_source/devices/<pmu>/identifier
(supporting literal directories or POSIX symlinks).
3. Host-direct scope (<root>/):
Probes PMU subdirectories directly under <root>/ plus
<root>/metricgroups/<group>.desc.
Within each matched scope, dir_for_each_pmu_subdir() checks the exact
PMU name (e.g. cpu, cpu_core, uncore_imc_0), pmu->alias_name,
default_core for core PMUs, and suffix-stripped uncore prefixes (e.g.
uncore_imc matching uncore_imc_0).
Also parse extended event sidecars (.unit, .scale, .scale-unit,
.per-pkg, .snapshot, .desc, .long-desc, .topic, .compat, .deprecated,
.legacy-terms, and .retire-lat-{mean,min,max}), metric sidecars
(<pmu>/metrics/*), and metricgroup descriptions with %2F escaping for
'/' in names, and register PMU_LAYER_SYSFS between
PMU_LAYER_BUILTIN_CPU and PMU_LAYER_BUILTIN_COMMON so single-event
sysfs lookups resolve lazily via openat() without readdir().
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/pmu-layer.c | 1351 ++++++++++++++++++++++++++++++++++-
tools/perf/util/pmu-layer.h | 6 +
2 files changed, 1350 insertions(+), 7 deletions(-)
diff --git a/tools/perf/util/pmu-layer.c b/tools/perf/util/pmu-layer.c
index e4c50cc6d1ea..e54d7e8f487f 100644
--- a/tools/perf/util/pmu-layer.c
+++ b/tools/perf/util/pmu-layer.c
@@ -1,23 +1,39 @@
// SPDX-License-Identifier: GPL-2.0
+#include <ctype.h>
+#include <dirent.h>
#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
#include <stdbool.h>
#include <stddef.h>
+#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <api/fs/fs.h>
+#include <api/io.h>
+#include <api/io_dir.h>
#include <linux/compiler.h>
#include <linux/err.h>
+#include <linux/string.h>
#include <linux/zalloc.h>
+#include "cpumap.h"
#include "debug.h"
#include "hashmap.h"
+#include "header.h"
#include "pmu.h"
+#include "pmus.h"
#include "pmu-layer.h"
#include "pmu-events/pmu-events.h"
static struct pmu_layer *pmu_layers;
static size_t nr_pmu_layers;
static bool pmu_layers_initialized;
+static int active_sysfs_override_fd = -1;
static struct perf_pmu_alias *pmu_layer_alloc_alias(const char *name)
{
@@ -265,12 +281,40 @@ struct builtin_find_event_ctx {
bool is_common;
};
+static bool pmu_layer_alias_is_empty(const struct perf_pmu_alias *alias)
+{
+ return !alias->terms && !alias->legacy_terms &&
+ !alias->desc && !alias->long_desc && !alias->topic &&
+ !alias->pmu_name && !alias->compat && !alias->has_scale &&
+ !alias->has_unit && !alias->has_per_pkg &&
+ !alias->has_snapshot && !alias->has_deprecated &&
+ !alias->has_retire_lat_mean && !alias->has_retire_lat_min &&
+ !alias->has_retire_lat_max;
+}
+
+static void pmu_layer_canon_alias_name(struct perf_pmu_alias *alias,
+ const char *canon)
+{
+ char *dup_name;
+
+ if (!canon || !canon[0] || !pmu_layer_alias_is_empty(alias) ||
+ !strcmp(alias->name, canon))
+ return;
+
+ dup_name = strdup(canon);
+ if (dup_name) {
+ free(alias->name);
+ alias->name = dup_name;
+ }
+}
+
static int builtin_find_event_cb(const struct pmu_event *pe,
const struct pmu_events_table *table __maybe_unused,
void *vdata)
{
struct builtin_find_event_ctx *ctx = vdata;
+ pmu_layer_canon_alias_name(ctx->alias, pe->name);
return pmu_layer_merge_pe(ctx->alias, pe, ctx->is_common);
}
@@ -288,6 +332,7 @@ static int builtin_find_sys_event_cb(const struct pmu_event *pe,
return 0;
if (!pmu_uncore_identifier_match(pe->compat, ctx->pmu->id))
return 0;
+ pmu_layer_canon_alias_name(ctx->alias, pe->name);
return pmu_layer_merge_pe(ctx->alias, pe, /*is_common=*/false);
}
@@ -678,6 +723,1264 @@ static const struct pmu_layer_ops builtin_common_layer_ops = {
.load_metricgroup = builtin_common_load_metricgroup,
};
+int pmu_layer__encode_filename(const char *name, char *buf, size_t size)
+{
+ size_t out = 0;
+
+ if (!name || size == 0)
+ return -EINVAL;
+
+ for (size_t i = 0; name[i] != '\0'; i++) {
+ if (name[i] == '/') {
+ if (out + 3 >= size)
+ return -ENAMETOOLONG;
+ buf[out++] = '%';
+ buf[out++] = '2';
+ buf[out++] = 'F';
+ } else {
+ if (out + 1 >= size)
+ return -ENAMETOOLONG;
+ buf[out++] = name[i];
+ }
+ }
+ buf[out] = '\0';
+ return 0;
+}
+
+int pmu_layer__decode_filename(const char *encoded, size_t len,
+ char *buf, size_t size)
+{
+ size_t out = 0;
+
+ if (!encoded || size == 0)
+ return -EINVAL;
+
+ for (size_t i = 0; i < len; i++) {
+ if (i + 2 < len && encoded[i] == '%' && encoded[i + 1] == '2' &&
+ (encoded[i + 2] == 'F' || encoded[i + 2] == 'f')) {
+ if (out + 1 >= size)
+ return -ENAMETOOLONG;
+ buf[out++] = '/';
+ i += 2;
+ } else {
+ if (out + 1 >= size)
+ return -ENAMETOOLONG;
+ buf[out++] = encoded[i];
+ }
+ }
+ buf[out] = '\0';
+ return 0;
+}
+
+static int dir_read_file_at(int dirfd, const char *filename, char **out_str)
+{
+ struct stat st;
+ char *buf = NULL;
+ size_t cap = 256, len = 0;
+ int fd, ret = 0;
+
+ fd = openat(dirfd, filename, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ if (fstat(fd, &st) < 0) {
+ ret = -errno;
+ close(fd);
+ return ret;
+ }
+ if (S_ISDIR(st.st_mode)) {
+ close(fd);
+ return -EISDIR;
+ }
+
+ buf = malloc(cap);
+ if (!buf) {
+ close(fd);
+ return -ENOMEM;
+ }
+
+ for (;;) {
+ ssize_t n;
+
+ if (len + 1 >= cap) {
+ char *next;
+
+ cap *= 2;
+ next = realloc(buf, cap);
+ if (!next) {
+ free(buf);
+ close(fd);
+ return -ENOMEM;
+ }
+ buf = next;
+ }
+ n = read(fd, buf + len, cap - len - 1);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ ret = -errno;
+ free(buf);
+ close(fd);
+ return ret;
+ }
+ if (n == 0)
+ break;
+ len += (size_t)n;
+ }
+ close(fd);
+
+ if (len > 0 && buf[len - 1] == '\n') {
+ len--;
+ if (len > 0 && buf[len - 1] == '\r')
+ len--;
+ }
+ buf[len] = '\0';
+ *out_str = buf;
+ return 0;
+}
+
+static int parse_bool_str(char *str, bool *out_val)
+{
+ size_t len = strlen(str);
+
+ while (len > 0 && isspace((unsigned char)str[len - 1]))
+ str[--len] = '\0';
+
+ if (!strcmp(str, "1") || !strcasecmp(str, "true") || !strcasecmp(str, "yes")) {
+ *out_val = true;
+ return 0;
+ }
+ if (!strcmp(str, "0") || !strcasecmp(str, "false") || !strcasecmp(str, "no")) {
+ *out_val = false;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int dir_apply_event_file(int events_fd, const char *filename,
+ enum pmu_event_file_kind kind,
+ const char *pmu_dir_name,
+ bool is_sysfs,
+ struct perf_pmu_alias *alias,
+ bool *matched)
+{
+ char *val = NULL;
+ bool bval;
+ int r = -ENOENT;
+
+ switch (kind) {
+ case PMU_EVENT_FILE_BASE:
+ if (!alias->terms) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ alias->terms = val;
+ if (is_sysfs) {
+ alias->from_sysfs = true;
+ alias->info_loaded = true;
+ }
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_LEGACY_TERMS:
+ if (!alias->legacy_terms) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0)
+ alias->legacy_terms = val;
+ }
+ break;
+ case PMU_EVENT_FILE_UNIT:
+ if (!alias->has_unit) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ snprintf(alias->unit, sizeof(alias->unit), "%s", val);
+ alias->has_unit = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_SCALE:
+ if (!alias->has_scale) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (perf_pmu__convert_scale(val, NULL, &alias->scale) == 0)
+ alias->has_scale = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_SCALE_UNIT:
+ if (!alias->has_scale || !alias->has_unit) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ char *unit_str = NULL;
+ double sval = 1.0;
+
+ if (perf_pmu__convert_scale(val, &unit_str, &sval) == 0) {
+ if (!alias->has_scale) {
+ alias->scale = sval;
+ alias->has_scale = true;
+ }
+ if (!alias->has_unit && unit_str) {
+ snprintf(alias->unit, sizeof(alias->unit),
+ "%s", unit_str);
+ alias->has_unit = true;
+ }
+ }
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_PER_PKG:
+ if (!alias->has_per_pkg) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (parse_bool_str(val, &bval) == 0) {
+ alias->per_pkg = bval;
+ alias->has_per_pkg = true;
+ }
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_SNAPSHOT:
+ if (!alias->has_snapshot) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (parse_bool_str(val, &bval) == 0) {
+ alias->snapshot = bval;
+ alias->has_snapshot = true;
+ }
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_DEPRECATED:
+ if (!alias->has_deprecated) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (parse_bool_str(val, &bval) == 0) {
+ alias->deprecated = bval;
+ alias->has_deprecated = true;
+ }
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_DESC:
+ if (!alias->desc) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0)
+ alias->desc = val;
+ }
+ break;
+ case PMU_EVENT_FILE_LONG_DESC:
+ if (!alias->long_desc) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0)
+ alias->long_desc = val;
+ }
+ break;
+ case PMU_EVENT_FILE_TOPIC:
+ if (!alias->topic) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0)
+ alias->topic = val;
+ }
+ break;
+ case PMU_EVENT_FILE_COMPAT:
+ if (!alias->compat) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0)
+ alias->compat = val;
+ }
+ break;
+ case PMU_EVENT_FILE_RETIRE_LAT_MEAN:
+ if (!alias->has_retire_lat_mean) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (perf_pmu__convert_scale(val, NULL,
+ &alias->retirement_latency_mean) == 0)
+ alias->has_retire_lat_mean = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_RETIRE_LAT_MIN:
+ if (!alias->has_retire_lat_min) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (perf_pmu__convert_scale(val, NULL,
+ &alias->retirement_latency_min) == 0)
+ alias->has_retire_lat_min = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_EVENT_FILE_RETIRE_LAT_MAX:
+ if (!alias->has_retire_lat_max) {
+ r = dir_read_file_at(events_fd, filename, &val);
+ if (r == 0) {
+ if (perf_pmu__convert_scale(val, NULL,
+ &alias->retirement_latency_max) == 0)
+ alias->has_retire_lat_max = true;
+ free(val);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (r == 0) {
+ if (matched)
+ *matched = true;
+ if (!is_sysfs && !alias->pmu_name && pmu_dir_name &&
+ strcmp(pmu_dir_name, "default_core") != 0) {
+ alias->pmu_name = strdup(pmu_dir_name);
+ if (!alias->pmu_name)
+ return -ENOMEM;
+ }
+ }
+ return r == -ENOMEM ? -ENOMEM : 0;
+}
+
+static int parse_aggr_mode_str(const char *str, enum aggr_mode_class *out)
+{
+ if (!strcasecmp(str, "PerChip") || !strcmp(str, "1")) {
+ *out = PerChip;
+ return 0;
+ }
+ if (!strcasecmp(str, "PerCore") || !strcmp(str, "2")) {
+ *out = PerCore;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int parse_constraint_str(const char *str, enum metric_event_groups *out)
+{
+ if (!strcasecmp(str, "GROUP_EVENTS") || !strcmp(str, "0")) {
+ *out = MetricGroupEvents;
+ return 0;
+ }
+ if (!strcasecmp(str, "NO_GROUP_EVENTS") || !strcmp(str, "1")) {
+ *out = MetricNoGroupEvents;
+ return 0;
+ }
+ if (!strcasecmp(str, "NO_GROUP_EVENTS_NMI") ||
+ !strcasecmp(str, "NO_NMI_WATCHDOG") || !strcmp(str, "2")) {
+ *out = MetricNoGroupEventsNmi;
+ return 0;
+ }
+ if (!strcasecmp(str, "NO_GROUP_EVENTS_SMT") || !strcmp(str, "3")) {
+ *out = MetricNoGroupEventsSmt;
+ return 0;
+ }
+ if (!strcasecmp(str, "NO_THRESHOLD_AND_NMI") || !strcmp(str, "4")) {
+ *out = MetricNoGroupEventsThresholdAndNmi;
+ return 0;
+ }
+ return -EINVAL;
+}
+
+static int dir_apply_metric_file(int metrics_fd, const char *filename,
+ enum pmu_metric_file_kind kind,
+ const char *pmu_dir_name,
+ struct perf_pmu_metric *metric,
+ bool *matched)
+{
+ char *val = NULL;
+ bool bval;
+ int r = -ENOENT;
+
+ switch (kind) {
+ case PMU_METRIC_FILE_BASE:
+ if (!metric->metric_expr) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->metric_expr = val;
+ } else if (faccessat(metrics_fd, filename, F_OK, 0) == 0) {
+ r = 0;
+ }
+ break;
+ case PMU_METRIC_FILE_SCALE_UNIT:
+ if (!metric->unit) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->unit = val;
+ }
+ break;
+ case PMU_METRIC_FILE_DESC:
+ if (!metric->desc) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->desc = val;
+ }
+ break;
+ case PMU_METRIC_FILE_LONG_DESC:
+ if (!metric->long_desc) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->long_desc = val;
+ }
+ break;
+ case PMU_METRIC_FILE_GROUP:
+ if (!metric->metric_group) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->metric_group = val;
+ }
+ break;
+ case PMU_METRIC_FILE_NO_GROUP:
+ if (!metric->metricgroup_no_group) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->metricgroup_no_group = val;
+ }
+ break;
+ case PMU_METRIC_FILE_DEFAULT_GROUP:
+ if (!metric->default_metricgroup_name) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->default_metricgroup_name = val;
+ }
+ break;
+ case PMU_METRIC_FILE_THRESHOLD:
+ if (!metric->metric_threshold) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->metric_threshold = val;
+ }
+ break;
+ case PMU_METRIC_FILE_COMPAT:
+ if (!metric->compat) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0)
+ metric->compat = val;
+ }
+ break;
+ case PMU_METRIC_FILE_AGGR_MODE:
+ if (!metric->has_aggr_mode) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0) {
+ if (parse_aggr_mode_str(val, &metric->aggr_mode) == 0)
+ metric->has_aggr_mode = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_METRIC_FILE_CONSTRAINT:
+ if (!metric->has_event_grouping) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0) {
+ if (parse_constraint_str(val, &metric->event_grouping) == 0)
+ metric->has_event_grouping = true;
+ free(val);
+ }
+ }
+ break;
+ case PMU_METRIC_FILE_SHOW_EVENTS:
+ if (!metric->has_default_show_events) {
+ r = dir_read_file_at(metrics_fd, filename, &val);
+ if (r == 0) {
+ if (parse_bool_str(val, &bval) == 0) {
+ metric->default_show_events = bval;
+ metric->has_default_show_events = true;
+ }
+ free(val);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (r == 0) {
+ if (matched)
+ *matched = true;
+ if (!metric->pmu && pmu_dir_name) {
+ metric->pmu = strdup(pmu_dir_name);
+ if (!metric->pmu)
+ return -ENOMEM;
+ }
+ }
+ return r == -ENOMEM ? -ENOMEM : 0;
+}
+
+static size_t build_cpuid_candidates(const char *cpuid,
+ char candidates[][128],
+ size_t max_candidates)
+{
+ size_t n = 0, len;
+
+ if (!cpuid || !cpuid[0] || max_candidates == 0)
+ return 0;
+
+ len = strlen(cpuid);
+ if (len >= sizeof(candidates[0]))
+ return 0;
+
+ snprintf(candidates[n++], sizeof(candidates[0]), "%s", cpuid);
+
+ if (strchr(cpuid, '-') || strchr(cpuid, ',')) {
+ char sep = strchr(cpuid, '-') ? '-' : ',';
+ char work[128];
+
+ snprintf(work, sizeof(work), "%s", cpuid);
+ while (n < max_candidates) {
+ char *last = strrchr(work, sep);
+
+ if (!last || last == work || !strchr(work, sep) ||
+ strchr(work, sep) == last)
+ break;
+ *last = '\0';
+ snprintf(candidates[n++], sizeof(candidates[0]), "%s", work);
+ }
+ } else if (!strncmp(cpuid, "0x", 2) || !strncmp(cpuid, "0X", 2)) {
+ char *end = NULL;
+ unsigned long long val = strtoull(cpuid, &end, 16);
+
+ if (end && *end == '\0' && n < max_candidates) {
+ if (len == 18) {
+ /* ARM64 MIDR: zero Variant[23:20] and Revision[3:0]. */
+ unsigned long long masked = val & ~0x00f0000fULL;
+
+ if (masked != val)
+ snprintf(candidates[n++], sizeof(candidates[0]),
+ "0x%016llx", masked);
+ } else if (len == 10) {
+ /* PowerPC PVR: zero Revision[15:0]. */
+ unsigned long long masked = val & ~0xffffULL;
+
+ if (masked != val)
+ snprintf(candidates[n++], sizeof(candidates[0]),
+ "0x%08llx", masked);
+ }
+ }
+ }
+ return n;
+}
+
+typedef int (*dir_scope_cb)(int scope_fd, struct perf_pmu *pmu, void *ctx);
+
+static int dir_for_each_scope(const struct pmu_layer *layer,
+ struct perf_pmu *pmu,
+ dir_scope_cb cb, void *ctx)
+{
+ int root_fd, cpus_fd, ret = 0;
+
+ if (!layer->root_path || !layer->root_path[0])
+ return 0;
+
+ root_fd = open(layer->root_path, O_RDONLY | O_DIRECTORY);
+ if (root_fd < 0)
+ return 0;
+
+ /* 1. Exact and deterministic base CPUID scopes under <root>/cpus/. */
+ cpus_fd = openat(root_fd, "cpus", O_RDONLY | O_DIRECTORY);
+ if (cpus_fd >= 0) {
+ struct perf_cpu cpu = pmu ? perf_cpu_map__min(pmu->cpus)
+ : (struct perf_cpu){ -1 };
+ char *cpuid = get_cpuid_allow_env_override(cpu);
+
+ if (!cpuid && pmu)
+ cpuid = get_cpuid_allow_env_override((struct perf_cpu){ -1 });
+ if (cpuid) {
+ char cands[6][128];
+ size_t nr_cands = build_cpuid_candidates(cpuid, cands,
+ ARRAY_SIZE(cands));
+
+ for (size_t i = 0; i < nr_cands; i++) {
+ int scope_fd = openat(cpus_fd, cands[i],
+ O_RDONLY | O_DIRECTORY);
+
+ if (scope_fd < 0)
+ continue;
+ ret = cb(scope_fd, pmu, ctx);
+ close(scope_fd);
+ break;
+ }
+ free(cpuid);
+ }
+ close(cpus_fd);
+ if (ret) {
+ close(root_fd);
+ return ret;
+ }
+ }
+
+ /* 2. Uncore/system PMU identifier scope under <root>/sys/<pmu->id>/. */
+ if (pmu && pmu->id && pmu->id[0]) {
+ int sys_fd = openat(root_fd, "sys", O_RDONLY | O_DIRECTORY);
+
+ if (sys_fd >= 0) {
+ int scope_fd = openat(sys_fd, pmu->id, O_RDONLY | O_DIRECTORY);
+
+ if (scope_fd >= 0) {
+ ret = cb(scope_fd, pmu, ctx);
+ close(scope_fd);
+ }
+ close(sys_fd);
+ if (ret) {
+ close(root_fd);
+ return ret;
+ }
+ }
+ }
+
+ /* 3. Host-direct layout directly under <root>/. */
+ ret = cb(root_fd, pmu, ctx);
+ close(root_fd);
+ return ret;
+}
+
+typedef int (*dir_pmu_cb)(int pmu_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu, void *ctx);
+
+static bool seen_pmu_dir(const char *seen[], size_t nr_seen, const char *name)
+{
+ for (size_t i = 0; i < nr_seen; i++) {
+ if (!strcmp(seen[i], name))
+ return true;
+ }
+ return false;
+}
+
+static int dir_for_each_pmu_subdir(int scope_fd, struct perf_pmu *pmu,
+ dir_pmu_cb cb, void *ctx)
+{
+ const char *cands[4];
+ char no_suffix[MAX_PMU_NAME_LEN];
+ size_t nr_cands = 0;
+ int ret = 0;
+
+ if (pmu->name && pmu->name[0])
+ cands[nr_cands++] = pmu->name;
+
+ if (pmu->alias_name && pmu->alias_name[0] &&
+ !seen_pmu_dir(cands, nr_cands, pmu->alias_name))
+ cands[nr_cands++] = pmu->alias_name;
+
+ if (pmu->is_core && !seen_pmu_dir(cands, nr_cands, "default_core"))
+ cands[nr_cands++] = "default_core";
+
+ if (pmu->is_uncore && pmu->name) {
+ size_t base_len = pmu_name_len_no_suffix(pmu->name);
+
+ if (base_len > 0 && base_len < sizeof(no_suffix) &&
+ base_len != strlen(pmu->name)) {
+ memcpy(no_suffix, pmu->name, base_len);
+ no_suffix[base_len] = '\0';
+ if (!seen_pmu_dir(cands, nr_cands, no_suffix))
+ cands[nr_cands++] = no_suffix;
+ }
+ }
+
+ for (size_t i = 0; i < nr_cands; i++) {
+ int pmu_fd = openat(scope_fd, cands[i], O_RDONLY | O_DIRECTORY);
+
+ if (pmu_fd < 0)
+ continue;
+ ret = cb(pmu_fd, cands[i], pmu, ctx);
+ close(pmu_fd);
+ if (ret)
+ return ret;
+ }
+
+ if (pmu->is_uncore) {
+ int dup_fd = dup(scope_fd);
+
+ if (dup_fd >= 0) {
+ struct io_dir dir;
+ struct io_dirent64 *dent;
+
+ io_dir__init(&dir, dup_fd);
+ while ((dent = io_dir__readdir(&dir)) != NULL) {
+ int pmu_fd;
+
+ if (dent->d_name[0] == '.' ||
+ seen_pmu_dir(cands, nr_cands, dent->d_name) ||
+ !strcmp(dent->d_name, "cpus") ||
+ !strcmp(dent->d_name, "sys") ||
+ !strcmp(dent->d_name, "models") ||
+ !strcmp(dent->d_name, "metricgroups"))
+ continue;
+ if (!perf_pmu__name_wildcard_match(pmu, dent->d_name))
+ continue;
+ pmu_fd = openat(scope_fd, dent->d_name,
+ O_RDONLY | O_DIRECTORY);
+ if (pmu_fd < 0)
+ continue;
+ ret = cb(pmu_fd, dent->d_name, pmu, ctx);
+ close(pmu_fd);
+ if (ret)
+ break;
+ }
+ close(dup_fd);
+ }
+ }
+
+ return ret;
+}
+
+static int dir_scan_events_fd(int events_fd, const char *pmu_dir_name,
+ bool is_sysfs, struct perf_pmu *pmu)
+{
+ int dup_fd = dup(events_fd);
+ struct io_dir dir;
+ struct io_dirent64 *dent;
+ int ret = 0;
+
+ if (dup_fd < 0)
+ return -errno;
+
+ io_dir__init(&dir, dup_fd);
+ while ((dent = io_dir__readdir(&dir)) != NULL) {
+ size_t base_len = 0;
+ enum pmu_event_file_kind kind;
+ char decoded[NAME_MAX];
+ struct perf_pmu_alias *alias;
+
+ if (dent->d_name[0] == '.')
+ continue;
+
+ kind = pmu_event_file_kind(dent->d_name, &base_len);
+ if (base_len == 0 ||
+ pmu_layer__decode_filename(dent->d_name, base_len,
+ decoded, sizeof(decoded)) != 0)
+ continue;
+
+ alias = pmu_layer_get_or_create_alias(pmu, decoded);
+ if (!alias) {
+ ret = -ENOMEM;
+ break;
+ }
+ if (alias->resolved && active_sysfs_override_fd < 0)
+ continue;
+
+ ret = dir_apply_event_file(events_fd, dent->d_name, kind,
+ pmu_dir_name, is_sysfs, alias, NULL);
+ if (ret)
+ break;
+ }
+ close(dup_fd);
+ return ret;
+}
+
+static int dir_scan_metrics_fd(int metrics_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu)
+{
+ int dup_fd = dup(metrics_fd);
+ struct io_dir dir;
+ struct io_dirent64 *dent;
+ int ret = 0;
+
+ if (dup_fd < 0)
+ return -errno;
+
+ io_dir__init(&dir, dup_fd);
+ while ((dent = io_dir__readdir(&dir)) != NULL) {
+ size_t base_len = 0;
+ enum pmu_metric_file_kind kind;
+ char decoded[NAME_MAX];
+ struct perf_pmu_metric *metric;
+
+ if (dent->d_name[0] == '.')
+ continue;
+
+ kind = pmu_metric_file_kind(dent->d_name, &base_len);
+ if (base_len == 0 ||
+ pmu_layer__decode_filename(dent->d_name, base_len,
+ decoded, sizeof(decoded)) != 0)
+ continue;
+
+ metric = pmu_layer_get_or_create_metric(pmu, decoded);
+ if (!metric) {
+ ret = -ENOMEM;
+ break;
+ }
+
+ ret = dir_apply_metric_file(metrics_fd, dent->d_name, kind,
+ pmu_dir_name, metric, NULL);
+ if (ret)
+ break;
+ }
+ close(dup_fd);
+ return ret;
+}
+
+struct dir_find_event_ctx {
+ const char *name;
+ struct perf_pmu_alias *alias;
+};
+
+static int dir_probe_user_event_base(int events_fd, const char *enc_base,
+ const char *canon_name,
+ const char *pmu_dir_name,
+ struct perf_pmu_alias *alias)
+{
+ static const struct {
+ const char *suffix;
+ enum pmu_event_file_kind kind;
+ } probes[] = {
+ { "", PMU_EVENT_FILE_BASE },
+ { ".legacy-terms", PMU_EVENT_FILE_LEGACY_TERMS },
+ { ".unit", PMU_EVENT_FILE_UNIT },
+ { ".scale", PMU_EVENT_FILE_SCALE },
+ { ".scale-unit", PMU_EVENT_FILE_SCALE_UNIT },
+ { ".per-pkg", PMU_EVENT_FILE_PER_PKG },
+ { ".snapshot", PMU_EVENT_FILE_SNAPSHOT },
+ { ".desc", PMU_EVENT_FILE_DESC },
+ { ".long-desc", PMU_EVENT_FILE_LONG_DESC },
+ { ".topic", PMU_EVENT_FILE_TOPIC },
+ { ".compat", PMU_EVENT_FILE_COMPAT },
+ { ".deprecated", PMU_EVENT_FILE_DEPRECATED },
+ { ".retire-lat-mean", PMU_EVENT_FILE_RETIRE_LAT_MEAN },
+ { ".retire-lat-min", PMU_EVENT_FILE_RETIRE_LAT_MIN },
+ { ".retire-lat-max", PMU_EVENT_FILE_RETIRE_LAT_MAX },
+ };
+ char filename[NAME_MAX + 32];
+ bool was_empty = pmu_layer_alias_is_empty(alias);
+ bool matched_any = false;
+
+ for (size_t i = 0; i < ARRAY_SIZE(probes); i++) {
+ bool matched = false;
+ int ret;
+
+ snprintf(filename, sizeof(filename), "%s%s", enc_base, probes[i].suffix);
+ ret = dir_apply_event_file(events_fd, filename, probes[i].kind,
+ pmu_dir_name, /*is_sysfs=*/false,
+ alias, &matched);
+ if (ret)
+ return ret;
+ if (matched)
+ matched_any = true;
+ }
+
+ if (was_empty && matched_any && canon_name && strcmp(alias->name, canon_name) != 0) {
+ char *dup_name = strdup(canon_name);
+
+ if (!dup_name)
+ return -ENOMEM;
+ free(alias->name);
+ alias->name = dup_name;
+ }
+ return 0;
+}
+
+static int user_dir_find_event_pmu_cb(int pmu_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu __maybe_unused,
+ void *vctx)
+{
+ struct dir_find_event_ctx *ctx = vctx;
+ char lower[NAME_MAX], upper[NAME_MAX];
+ char enc_exact[NAME_MAX], enc_lower[NAME_MAX], enc_upper[NAME_MAX];
+ const char *enc_cands[3], *raw_cands[3];
+ size_t nr_cands = 0, len = strlen(ctx->name);
+ const char *pmu_label;
+ int events_fd;
+
+ if (len == 0 || len >= NAME_MAX || ctx->name[0] == '.' ||
+ pmu_event_file_kind(ctx->name, NULL) != PMU_EVENT_FILE_BASE)
+ return 0;
+
+ events_fd = openat(pmu_fd, "events", O_RDONLY | O_DIRECTORY);
+ if (events_fd < 0)
+ return 0;
+
+ for (size_t i = 0; i < len; i++) {
+ lower[i] = tolower((unsigned char)ctx->name[i]);
+ upper[i] = toupper((unsigned char)ctx->name[i]);
+ }
+ lower[len] = '\0';
+ upper[len] = '\0';
+
+ if (pmu_layer__encode_filename(ctx->name, enc_exact, sizeof(enc_exact)) == 0) {
+ enc_cands[nr_cands] = enc_exact;
+ raw_cands[nr_cands++] = ctx->name;
+ }
+ if (strcmp(lower, ctx->name) != 0 &&
+ pmu_layer__encode_filename(lower, enc_lower, sizeof(enc_lower)) == 0) {
+ enc_cands[nr_cands] = enc_lower;
+ raw_cands[nr_cands++] = lower;
+ }
+ if (strcmp(upper, ctx->name) != 0 && strcmp(upper, lower) != 0 &&
+ pmu_layer__encode_filename(upper, enc_upper, sizeof(enc_upper)) == 0) {
+ enc_cands[nr_cands] = enc_upper;
+ raw_cands[nr_cands++] = upper;
+ }
+
+ pmu_label = !strcmp(pmu_dir_name, "default_core") ? NULL : pmu_dir_name;
+
+ for (size_t i = 0; i < nr_cands; i++) {
+ int ret = dir_probe_user_event_base(events_fd, enc_cands[i], raw_cands[i],
+ pmu_label, ctx->alias);
+ if (ret) {
+ close(events_fd);
+ return ret;
+ }
+ }
+
+ close(events_fd);
+ return 0;
+}
+
+static int user_dir_find_event_scope_cb(int scope_fd, struct perf_pmu *pmu, void *vctx)
+{
+ return dir_for_each_pmu_subdir(scope_fd, pmu, user_dir_find_event_pmu_cb, vctx);
+}
+
+static int user_dir_find_event(const struct pmu_layer *layer,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_alias *alias)
+{
+ struct dir_find_event_ctx ctx = {
+ .name = name,
+ .alias = alias,
+ };
+ int ret = dir_for_each_scope(layer, pmu, user_dir_find_event_scope_cb, &ctx);
+
+ return ret < 0 ? ret : 0;
+}
+
+static int user_dir_for_each_event_pmu_cb(int pmu_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu, void *vctx __maybe_unused)
+{
+ const char *pmu_label;
+ int events_fd, ret;
+
+ events_fd = openat(pmu_fd, "events", O_RDONLY | O_DIRECTORY);
+ if (events_fd < 0)
+ return 0;
+
+ pmu_label = !strcmp(pmu_dir_name, "default_core") ? NULL : pmu_dir_name;
+ ret = dir_scan_events_fd(events_fd, pmu_label, /*is_sysfs=*/false, pmu);
+ close(events_fd);
+ return ret;
+}
+
+static int user_dir_for_each_event_scope_cb(int scope_fd, struct perf_pmu *pmu, void *vctx)
+{
+ return dir_for_each_pmu_subdir(scope_fd, pmu, user_dir_for_each_event_pmu_cb, vctx);
+}
+
+static int user_dir_for_each_event(const struct pmu_layer *layer,
+ struct perf_pmu *pmu)
+{
+ return dir_for_each_scope(layer, pmu, user_dir_for_each_event_scope_cb, NULL);
+}
+
+struct dir_find_metric_ctx {
+ const char *name;
+ struct perf_pmu_metric *metric;
+};
+
+static int user_dir_find_metric_pmu_cb(int pmu_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu __maybe_unused,
+ void *vctx)
+{
+ static const struct {
+ const char *suffix;
+ enum pmu_metric_file_kind kind;
+ } probes[] = {
+ { "", PMU_METRIC_FILE_BASE },
+ { ".scale-unit", PMU_METRIC_FILE_SCALE_UNIT },
+ { ".desc", PMU_METRIC_FILE_DESC },
+ { ".long-desc", PMU_METRIC_FILE_LONG_DESC },
+ { ".group", PMU_METRIC_FILE_GROUP },
+ { ".no-group", PMU_METRIC_FILE_NO_GROUP },
+ { ".default-group", PMU_METRIC_FILE_DEFAULT_GROUP },
+ { ".threshold", PMU_METRIC_FILE_THRESHOLD },
+ { ".compat", PMU_METRIC_FILE_COMPAT },
+ { ".aggr-mode", PMU_METRIC_FILE_AGGR_MODE },
+ { ".constraint", PMU_METRIC_FILE_CONSTRAINT },
+ { ".show-events", PMU_METRIC_FILE_SHOW_EVENTS },
+ };
+ struct dir_find_metric_ctx *ctx = vctx;
+ char enc_name[NAME_MAX];
+ char filename[NAME_MAX + 32];
+ int metrics_fd;
+
+ if (!ctx->name[0] || ctx->name[0] == '.' ||
+ pmu_metric_file_kind(ctx->name, NULL) != PMU_METRIC_FILE_BASE ||
+ pmu_layer__encode_filename(ctx->name, enc_name, sizeof(enc_name)) != 0)
+ return 0;
+
+ metrics_fd = openat(pmu_fd, "metrics", O_RDONLY | O_DIRECTORY);
+ if (metrics_fd < 0)
+ return 0;
+
+ for (size_t i = 0; i < ARRAY_SIZE(probes); i++) {
+ int ret;
+
+ snprintf(filename, sizeof(filename), "%s%s", enc_name, probes[i].suffix);
+ ret = dir_apply_metric_file(metrics_fd, filename, probes[i].kind,
+ pmu_dir_name, ctx->metric, NULL);
+ if (ret) {
+ close(metrics_fd);
+ return ret;
+ }
+ }
+
+ close(metrics_fd);
+ return 0;
+}
+
+static int user_dir_find_metric_scope_cb(int scope_fd, struct perf_pmu *pmu, void *vctx)
+{
+ return dir_for_each_pmu_subdir(scope_fd, pmu, user_dir_find_metric_pmu_cb, vctx);
+}
+
+static int user_dir_find_metric(const struct pmu_layer *layer,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_metric *metric)
+{
+ struct dir_find_metric_ctx ctx = {
+ .name = name,
+ .metric = metric,
+ };
+ int ret = dir_for_each_scope(layer, pmu, user_dir_find_metric_scope_cb, &ctx);
+
+ return ret < 0 ? ret : 0;
+}
+
+static int user_dir_for_each_metric_pmu_cb(int pmu_fd, const char *pmu_dir_name,
+ struct perf_pmu *pmu,
+ void *vctx __maybe_unused)
+{
+ int metrics_fd, ret;
+
+ metrics_fd = openat(pmu_fd, "metrics", O_RDONLY | O_DIRECTORY);
+ if (metrics_fd < 0)
+ return 0;
+
+ ret = dir_scan_metrics_fd(metrics_fd, pmu_dir_name, pmu);
+ close(metrics_fd);
+ return ret;
+}
+
+static int user_dir_for_each_metric_scope_cb(int scope_fd, struct perf_pmu *pmu, void *vctx)
+{
+ return dir_for_each_pmu_subdir(scope_fd, pmu, user_dir_for_each_metric_pmu_cb, vctx);
+}
+
+static int user_dir_for_each_metric(const struct pmu_layer *layer,
+ struct perf_pmu *pmu)
+{
+ return dir_for_each_scope(layer, pmu, user_dir_for_each_metric_scope_cb, NULL);
+}
+
+struct dir_metricgroup_ctx {
+ const char *group;
+ struct perf_pmu_metricgroup *mg;
+};
+
+static int dir_probe_metricgroups_fd(int mg_fd, const char *enc_group,
+ struct perf_pmu_metricgroup *mg)
+{
+ char filename[NAME_MAX + 16];
+ char *val = NULL;
+
+ snprintf(filename, sizeof(filename), "%s.desc", enc_group);
+ if (dir_read_file_at(mg_fd, filename, &val) == 0) {
+ mg->desc = val;
+ return 1;
+ }
+ return 0;
+}
+
+static int user_dir_metricgroup_pmu_cb(int pmu_fd, const char *pmu_dir_name __maybe_unused,
+ struct perf_pmu *pmu __maybe_unused, void *vctx)
+{
+ struct dir_metricgroup_ctx *ctx = vctx;
+ char enc_group[NAME_MAX];
+ int mg_fd, ret = 0;
+
+ if (pmu_layer__encode_filename(ctx->group, enc_group, sizeof(enc_group)) != 0)
+ return 0;
+
+ mg_fd = openat(pmu_fd, "metricgroups", O_RDONLY | O_DIRECTORY);
+ if (mg_fd < 0)
+ return 0;
+
+ ret = dir_probe_metricgroups_fd(mg_fd, enc_group, ctx->mg);
+ close(mg_fd);
+ return ret;
+}
+
+static int user_dir_metricgroup_scope_cb(int scope_fd, struct perf_pmu *pmu, void *vctx)
+{
+ struct dir_metricgroup_ctx *ctx = vctx;
+ char enc_group[NAME_MAX];
+ int mg_fd, ret = 0;
+
+ if (pmu_layer__encode_filename(ctx->group, enc_group, sizeof(enc_group)) != 0)
+ return 0;
+
+ mg_fd = openat(scope_fd, "metricgroups", O_RDONLY | O_DIRECTORY);
+ if (mg_fd >= 0) {
+ ret = dir_probe_metricgroups_fd(mg_fd, enc_group, ctx->mg);
+ close(mg_fd);
+ if (ret)
+ return ret;
+ }
+ if (pmu)
+ return dir_for_each_pmu_subdir(scope_fd, pmu,
+ user_dir_metricgroup_pmu_cb, vctx);
+ return 0;
+}
+
+static int user_dir_load_metricgroup(const struct pmu_layer *layer,
+ struct perf_pmu *pmu,
+ const char *group,
+ struct perf_pmu_metricgroup *mg)
+{
+ struct dir_metricgroup_ctx ctx = {
+ .group = group,
+ .mg = mg,
+ };
+ int ret;
+
+ if (mg->desc)
+ return 0;
+
+ ret = dir_for_each_scope(layer, pmu, user_dir_metricgroup_scope_cb, &ctx);
+ return ret < 0 ? ret : 0;
+}
+
+static const struct pmu_layer_ops __maybe_unused user_dir_layer_ops = {
+ .find_event = user_dir_find_event,
+ .for_each_event = user_dir_for_each_event,
+ .find_metric = user_dir_find_metric,
+ .for_each_metric = user_dir_for_each_metric,
+ .load_metricgroup = user_dir_load_metricgroup,
+};
+
+static int sysfs_open_events_dir(struct perf_pmu *pmu)
+{
+ char path[PATH_MAX];
+ size_t len;
+
+ if (active_sysfs_override_fd >= 0) {
+ scnprintf(path, sizeof(path), "%s/events", pmu->name);
+ return openat(active_sysfs_override_fd, path, O_RDONLY | O_DIRECTORY);
+ }
+
+ if (pmu->sysfs_aliases_loaded)
+ return -1;
+
+ len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
+ if (!len)
+ return -1;
+ scnprintf(path + len, sizeof(path) - len, "%s/events", pmu->name);
+ return open(path, O_RDONLY | O_DIRECTORY);
+}
+
+static int sysfs_layer_find_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu, const char *name,
+ struct perf_pmu_alias *alias)
+{
+ char lower[NAME_MAX], upper[NAME_MAX];
+ const char *cands[3];
+ size_t nr_cands = 0, len = strlen(name);
+ int events_fd;
+
+ if (pmu->eager_sysfs_loaded)
+ return 0;
+
+ if (len == 0 || len >= NAME_MAX || name[0] == '.' || strchr(name, '/') ||
+ pmu_event_file_kind(name, NULL) != PMU_EVENT_FILE_BASE)
+ return 0;
+
+ events_fd = sysfs_open_events_dir(pmu);
+ if (events_fd < 0) {
+ if (errno == ENOENT)
+ pmu->sysfs_aliases_loaded = true;
+ return 0;
+ }
+
+ cands[nr_cands++] = name;
+ for (size_t i = 0; i < len; i++) {
+ lower[i] = tolower((unsigned char)name[i]);
+ upper[i] = toupper((unsigned char)name[i]);
+ }
+ lower[len] = '\0';
+ upper[len] = '\0';
+
+ if (strcmp(lower, name) != 0)
+ cands[nr_cands++] = lower;
+ if (strcmp(upper, name) != 0 && strcmp(upper, lower) != 0)
+ cands[nr_cands++] = upper;
+
+ for (size_t i = 0; i < nr_cands; i++) {
+ struct stat st;
+ char filename[NAME_MAX + 16];
+ int fd = openat(events_fd, cands[i], O_RDONLY);
+
+ if (fd < 0)
+ continue;
+ if (fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode)) {
+ static const struct {
+ const char *suffix;
+ enum pmu_event_file_kind kind;
+ } sysfs_probes[] = {
+ { "", PMU_EVENT_FILE_BASE },
+ { ".unit", PMU_EVENT_FILE_UNIT },
+ { ".scale", PMU_EVENT_FILE_SCALE },
+ { ".per-pkg", PMU_EVENT_FILE_PER_PKG },
+ { ".snapshot", PMU_EVENT_FILE_SNAPSHOT },
+ };
+ close(fd);
+ pmu_layer_canon_alias_name(alias, cands[i]);
+ for (size_t j = 0; j < ARRAY_SIZE(sysfs_probes); j++) {
+ int ret;
+
+ snprintf(filename, sizeof(filename), "%s%s",
+ cands[i], sysfs_probes[j].suffix);
+ ret = dir_apply_event_file(events_fd, filename,
+ sysfs_probes[j].kind, NULL,
+ /*is_sysfs=*/true,
+ alias, NULL);
+ if (ret) {
+ close(events_fd);
+ return ret;
+ }
+ }
+ break;
+ }
+ close(fd);
+ }
+
+ close(events_fd);
+ return 0;
+}
+
+static int sysfs_layer_for_each_event(const struct pmu_layer *layer __maybe_unused,
+ struct perf_pmu *pmu)
+{
+ int events_fd, ret;
+
+ if (pmu->eager_sysfs_loaded && active_sysfs_override_fd < 0)
+ return 0;
+
+ events_fd = sysfs_open_events_dir(pmu);
+ if (events_fd < 0) {
+ pmu->sysfs_aliases_loaded = true;
+ return 0;
+ }
+
+ ret = dir_scan_events_fd(events_fd, NULL, /*is_sysfs=*/true, pmu);
+ close(events_fd);
+ if (!ret)
+ pmu->sysfs_aliases_loaded = true;
+ return ret;
+}
+
+static const struct pmu_layer_ops sysfs_layer_ops = {
+ .find_event = sysfs_layer_find_event,
+ .for_each_event = sysfs_layer_for_each_event,
+};
+
static int pmu_layers_append(enum pmu_layer_kind kind,
const struct pmu_layer_ops *ops,
const char *root_path)
@@ -725,6 +2028,10 @@ static int pmu_layers_ensure_init(void)
if (ret)
goto err;
+ ret = pmu_layers_append(PMU_LAYER_SYSFS, &sysfs_layer_ops, NULL);
+ if (ret)
+ goto err;
+
ret = pmu_layers_append(PMU_LAYER_BUILTIN_COMMON, &builtin_common_layer_ops, NULL);
if (ret)
goto err;
@@ -786,7 +2093,8 @@ struct perf_pmu_alias *pmu_layer__find_event(struct perf_pmu *pmu,
if (hashmap__find(pmu->aliases, name, &alias))
return alias->terms ? alias : NULL;
- if (pmu->aliases_loaded)
+ if (pmu->aliases_loaded ||
+ (pmu->cpu_aliases_added && pmu->sysfs_aliases_loaded))
return NULL;
if (pmu_layers_ensure_init())
@@ -828,17 +2136,21 @@ struct perf_pmu_alias *pmu_layer__find_event(struct perf_pmu *pmu,
return alias;
}
-int pmu_layer__load_events(struct perf_pmu *pmu)
+int pmu_layer__load_events_at(struct perf_pmu *pmu, int sysfs_fd)
{
int ret;
- if (!pmu || pmu->aliases_loaded)
+ if (!pmu || pmu->aliases_loaded ||
+ (pmu->cpu_aliases_added && pmu->sysfs_aliases_loaded))
return 0;
ret = pmu_layers_ensure_init();
if (ret)
return ret;
+ if (sysfs_fd >= 0)
+ active_sysfs_override_fd = sysfs_fd;
+
/*
* Any single-event entries cached before full enumeration remain in
* pmu->aliases; top-down iteration fills any missing events cleanly.
@@ -849,17 +2161,28 @@ int pmu_layer__load_events(struct perf_pmu *pmu)
if (!layer->ops->for_each_event)
continue;
ret = layer->ops->for_each_event(layer, pmu);
- if (ret) {
- pmu_layer_finalize_aliases(pmu, /*promote_legacy=*/false);
- return ret;
- }
+ if (ret)
+ break;
+ }
+
+ active_sysfs_override_fd = -1;
+ if (ret) {
+ pmu_layer_finalize_aliases(pmu, /*promote_legacy=*/false);
+ return ret;
}
pmu_layer_finalize_aliases(pmu, /*promote_legacy=*/true);
+ if (sysfs_fd >= 0)
+ pmu->eager_sysfs_loaded = true;
pmu->aliases_loaded = true;
return 0;
}
+int pmu_layer__load_events(struct perf_pmu *pmu)
+{
+ return pmu_layer__load_events_at(pmu, -1);
+}
+
int pmu_layer__merge_event_table(struct perf_pmu *pmu,
const struct pmu_events_table *table)
{
@@ -876,6 +2199,20 @@ int pmu_layer__merge_event_table(struct perf_pmu *pmu,
builtin_for_each_event_cb, &ctx);
}
+int pmu_layer__merge_sys_events(struct perf_pmu *pmu)
+{
+ struct builtin_iter_event_ctx ctx = {
+ .pmu = pmu,
+ .is_common = false,
+ .override_existing = true,
+ };
+
+ if (!pmu || !pmu->id)
+ return 0;
+
+ return pmu_for_each_sys_event(builtin_for_each_sys_event_cb, &ctx);
+}
+
struct perf_pmu_metric *pmu_layer__find_metric(struct perf_pmu *pmu,
const char *name)
{
diff --git a/tools/perf/util/pmu-layer.h b/tools/perf/util/pmu-layer.h
index 330af9415448..5d8a33481410 100644
--- a/tools/perf/util/pmu-layer.h
+++ b/tools/perf/util/pmu-layer.h
@@ -154,11 +154,17 @@ void perf_pmu_free_metricgroup(struct perf_pmu_metricgroup *mg);
void perf_pmu_metric__to_pmu_metric(const struct perf_pmu_metric *src,
struct pmu_metric *dst);
+int pmu_layer__encode_filename(const char *name, char *buf, size_t size);
+int pmu_layer__decode_filename(const char *encoded, size_t len,
+ char *buf, size_t size);
+
struct perf_pmu_alias *pmu_layer__find_event(struct perf_pmu *pmu,
const char *name);
int pmu_layer__load_events(struct perf_pmu *pmu);
+int pmu_layer__load_events_at(struct perf_pmu *pmu, int sysfs_fd);
int pmu_layer__merge_event_table(struct perf_pmu *pmu,
const struct pmu_events_table *table);
+int pmu_layer__merge_sys_events(struct perf_pmu *pmu);
struct perf_pmu_metric *pmu_layer__find_metric(struct perf_pmu *pmu,
const char *name);
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 05/11] perf config: Add pmu-events.dir configuration option
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (3 preceding siblings ...)
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 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 06/11] perf pmu: Switch alias resolution and enumeration to layered PMU stack Ian Rogers
` (5 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Add [pmu-events] dir to perf_config() (~/.perfconfig,
$(sysconfdir)/perfconfig, or PERF_CONFIG) to configure one or more
colon-separated user or system PMU overlay directories (ordered from
highest to lowest priority) with leading ~/ home expansion.
For example, configuring:
[pmu-events]
dir = ~/.perf-events/local:/opt/pmu-events
allows a developer or fleet administrator to layer host-local or
stepping-specific patches in ~/.perf-events/local ahead of a shared
multi-CPU directory tree in /opt/pmu-events:
~/.perf-events/local/
|-- cpus/GenuineIntel-6-8F-8/cpu/events/
| `-- inst_retired.any.desc # stepping-specific fixup
`-- default_core/metrics/
|-- my_ipc # host-local custom metric
`-- my_ipc.desc
/opt/pmu-events/
|-- cpus/
| |-- GenuineIntel-6-8F -> ../models/x86/sapphirerapids
| `-- 0x00000000410fd490 -> ../models/arm64/arm/neoverse-n2
`-- models/...
On a Sapphire Rapids stepping 8 host (GenuineIntel-6-8F-8), perf first
matches ~/.perf-events/local/cpus/GenuineIntel-6-8F-8/ (overriding only
inst_retired.any.desc while inheriting event encoding terms from lower
layers) and ~/.perf-events/local/default_core/, then falls back to
/opt/pmu-events/cpus/GenuineIntel-6-8F -> ../models/x86/sapphirerapids,
and finally to compiled-in tables and kernel sysfs.
When pmu-events.dir is unset, PMU_LAYER_USER_DIR is omitted from the
layer stack so runtime overhead is zero extra syscalls. Reset the layer
stack in perf_config__exit() and perf_pmus__destroy() so process
teardown and perf test config isolation reload cleanly, and document
pmu-events.dir in Documentation/perf-config.txt.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-config.txt | 42 ++++++++++++++++++
tools/perf/util/config.c | 15 +++++++
tools/perf/util/pmu-layer.c | 55 +++++++++++++++++++++++-
tools/perf/util/pmus.c | 2 +
4 files changed, 113 insertions(+), 1 deletion(-)
diff --git a/tools/perf/Documentation/perf-config.txt b/tools/perf/Documentation/perf-config.txt
index 6cff6f26af8d..e81c33717688 100644
--- a/tools/perf/Documentation/perf-config.txt
+++ b/tools/perf/Documentation/perf-config.txt
@@ -94,6 +94,10 @@ Given a $HOME/.perfconfig like this:
# Default, disable using /dev/null
dir = ~/.debug
+ [pmu-events]
+ # Colon-separated sysfs-style PMU event/metric overlay directories
+ dir = ~/.perf-events
+
[annotate]
# Defaults
hide_src_code = false
@@ -259,6 +263,44 @@ buildid-cache.*::
buildid-cache.debuginfod=http://192.168.122.174:8002
+pmu-events.*::
+ pmu-events.dir::
+ Specify a directory, or a colon-separated list of directories
+ (ordered from highest to lowest priority), containing sysfs-style
+ PMU event, metric, and metricgroup definitions. Leading '~/' is
+ expanded to '$HOME/'.
++
+Overlay directories have higher priority than inbuilt JSON tables and kernel
+sysfs ('pmu-events.dir > inbuilt CPU/SoC > sysfs > inbuilt common'). Attributes
+merge top-down per event or metric so individual sidecar files (such as
+'<pmu>/events/<event>.desc' or '<pmu>/metrics/<metric>.threshold') can override
+single fields while preserving lower-layer definitions.
++
+Within each '<dir>', perf probes three scopes in order:
+'<dir>/cpus/<cpuid>/<pmu>/', '<dir>/sys/<id>/<pmu>/', and host-direct
+'<dir>/<pmu>/' (matching exact PMU names, 'default_core' for core PMUs, and
+suffix-stripped uncore prefixes, plus '<dir>/metricgroups/<group>.desc'). Under
+'<dir>/cpus/', perf tries the exact CPUID first and then deterministic base
+CPUIDs stripped of stepping or revision fields (for example
+'GenuineIntel-6-8F-8' then 'GenuineIntel-6-8F' on x86, '0x00000000411fd493'
+then '0x00000000410fd490' on ARM64, '0x00800200' then '0x00800000' on PowerPC,
+'IBM,8561,703' then 'IBM,8561' on s390, and '0x489-0x8000000000000007-0x1' then
+'0x489-0x8000000000000007' on RISC-V), stopping at the first matching
+'cpus/<candidate>' in that '<dir>' and following POSIX symlinks (such as
+'cpus/GenuineIntel-6-4E -> ../models/x86/skylake') without requiring a
+'mapfile.csv'.
++
+For example, to layer local overrides ahead of a shared directory in
+'$HOME/.perfconfig' and define a custom metric:
+
+ [pmu-events]
+ dir = ~/.perf-events/local:/opt/pmu-events
+
+ $ mkdir -p ~/.perf-events/local/default_core/metrics
+ $ 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
+
annotate.*::
These are in control of addresses, jump function, source code
in lines of assembly code from a specific program.
diff --git a/tools/perf/util/config.c b/tools/perf/util/config.c
index 287401f159e6..c9e68f0c6163 100644
--- a/tools/perf/util/config.c
+++ b/tools/perf/util/config.c
@@ -33,6 +33,7 @@
#include "header.h"
#include "mutex.h"
#include "path.h"
+#include "pmu-layer.h"
#include "srcline.h"
#include "unwind.h"
#include "util/event.h" /* proc_map_timeout */
@@ -518,6 +519,16 @@ static int perf_stat_config(const char *var, const char *value)
return 0;
}
+static int perf_pmu_events_config(const char *var, const char *value)
+{
+ if (!strcmp(var, "pmu-events.dir")) {
+ if (!value)
+ return config_error_nonbool(var);
+ }
+
+ return 0;
+}
+
int perf_default_config(const char *var, const char *value,
void *dummy __maybe_unused)
{
@@ -536,6 +547,9 @@ int perf_default_config(const char *var, const char *value,
if (strstarts(var, "buildid."))
return perf_buildid_config(var, value);
+ if (strstarts(var, "pmu-events."))
+ return perf_pmu_events_config(var, value);
+
if (strstarts(var, "stat."))
return perf_stat_config(var, value);
@@ -917,6 +931,7 @@ void perf_config__exit(void)
perf_config_set__delete(config_set);
config_set = NULL;
mutex_unlock(&config_set_mutex);
+ pmu_layers__exit();
}
int perf_config_set__write(struct perf_config_set *set,
diff --git a/tools/perf/util/pmu-layer.c b/tools/perf/util/pmu-layer.c
index e54d7e8f487f..2c60fc1e3285 100644
--- a/tools/perf/util/pmu-layer.c
+++ b/tools/perf/util/pmu-layer.c
@@ -21,6 +21,7 @@
#include <linux/err.h>
#include <linux/string.h>
#include <linux/zalloc.h>
+#include "config.h"
#include "cpumap.h"
#include "debug.h"
#include "hashmap.h"
@@ -1848,7 +1849,7 @@ static int user_dir_load_metricgroup(const struct pmu_layer *layer,
return ret < 0 ? ret : 0;
}
-static const struct pmu_layer_ops __maybe_unused user_dir_layer_ops = {
+static const struct pmu_layer_ops user_dir_layer_ops = {
.find_event = user_dir_find_event,
.for_each_event = user_dir_for_each_event,
.find_metric = user_dir_find_metric,
@@ -2017,13 +2018,65 @@ void pmu_layers__exit(void)
pmu_layers_initialized = false;
}
+static int pmu_layers_add_user_dirs(const char *cfg)
+{
+ const char *p = cfg;
+
+ if (!cfg || !cfg[0])
+ return 0;
+
+ while (*p) {
+ const char *colon = strchr(p, ':');
+ size_t seg_len = colon ? (size_t)(colon - p) : strlen(p);
+
+ if (seg_len > 0 && seg_len < PATH_MAX) {
+ char raw[PATH_MAX];
+ char expanded[PATH_MAX];
+ const char *use_path = raw;
+ int ret;
+
+ memcpy(raw, p, seg_len);
+ raw[seg_len] = '\0';
+
+ if (raw[0] == '~' && (raw[1] == '\0' || raw[1] == '/')) {
+ const char *home = getenv("HOME");
+
+ if (home && home[0]) {
+ int n = snprintf(expanded, sizeof(expanded),
+ "%s%s", home, raw + 1);
+
+ if (n < 0 || (size_t)n >= sizeof(expanded))
+ return -ENAMETOOLONG;
+ use_path = expanded;
+ }
+ }
+
+ ret = pmu_layers_append(PMU_LAYER_USER_DIR,
+ &user_dir_layer_ops, use_path);
+ if (ret)
+ return ret;
+ }
+
+ if (!colon)
+ break;
+ p = colon + 1;
+ }
+ return 0;
+}
+
static int pmu_layers_ensure_init(void)
{
+ const char *user_dirs;
int ret;
if (pmu_layers_initialized)
return 0;
+ user_dirs = perf_config_get("pmu-events.dir");
+ ret = pmu_layers_add_user_dirs(user_dirs);
+ if (ret)
+ goto err;
+
ret = pmu_layers_append(PMU_LAYER_BUILTIN_CPU, &builtin_cpu_layer_ops, NULL);
if (ret)
goto err;
diff --git a/tools/perf/util/pmus.c b/tools/perf/util/pmus.c
index 1355b317f3ed..6ecea8f0d382 100644
--- a/tools/perf/util/pmus.c
+++ b/tools/perf/util/pmus.c
@@ -17,6 +17,7 @@
#include "evsel.h"
#include "pmus.h"
#include "pmu.h"
+#include "pmu-layer.h"
#include "hwmon_pmu.h"
#include "tool_pmu.h"
#include "print-events.h"
@@ -124,6 +125,7 @@ void perf_pmus__destroy(void)
perf_pmu__delete(pmu);
}
read_pmu_types = 0;
+ pmu_layers__exit();
}
static struct perf_pmu *pmu_find(const char *name)
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 06/11] perf pmu: Switch alias resolution and enumeration to layered PMU stack
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (4 preceding siblings ...)
2026-11-13 21:10 ` [PATCH 05/11] perf config: Add pmu-events.dir configuration option Ian Rogers
@ 2026-11-13 21:10 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 07/11] perf metricgroup: Route metric and metricgroup lookups through struct perf_pmu Ian Rogers
` (4 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Route perf_pmu__find_alias(), pmu_find_alias(),
perf_pmu__have_event(), perf_pmu__num_events(),
perf_pmu__for_each_event(), and perf_pmu__name_from_config() through
pmu_layer__find_event() and pmu_layer__load_events().
This replaces the inverted EVENT_SRC_SYSFS + update_alias() in-place
mutation pipeline with uniform top-down attribute merging across the
four-tier stack:
1. pmu-events.dir user/system overlay directories (when configured)
2. Compiled-in CPUID and system/SoC JSON tables (PMU_LAYER_BUILTIN_CPU)
3. Kernel sysfs /sys/bus/event_source/devices/<pmu>/ (PMU_LAYER_SYSFS)
4. Compiled-in cross-CPU common JSON tables (PMU_LAYER_BUILTIN_COMMON)
Keeping compiled-in CPUID/sys JSON ahead of kernel sysfs and kernel
sysfs ahead of common fallback JSON matches existing upstream perf
precedence (for example, per-CPU JSON enriches or overrides same-named
sysfs events while sysfs events take priority over arch/common
fallbacks), and preserves single-event lazy openat() sysfs lookups
without full readdir().
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/util/pmu.c | 665 ++----------------------------------------
tools/perf/util/pmu.h | 11 -
2 files changed, 32 insertions(+), 644 deletions(-)
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index 685c61935f50..a3e1630c5a84 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -44,20 +44,6 @@
#include "util/evsel_config.h"
#include <regex.h>
-enum event_source {
- /* An event loaded from /sys/bus/event_source/devices/<pmu>/events. */
- EVENT_SRC_SYSFS,
- /* An event loaded from a CPUID matched json file. */
- EVENT_SRC_CPU_JSON,
- /*
- * An event loaded from a /sys/bus/event_source/devices/<pmu>/identifier matched json
- * file.
- */
- EVENT_SRC_SYS_JSON,
-};
-
-static int pmu_aliases_parse(struct perf_pmu *pmu);
-
static struct perf_pmu_format *perf_pmu__new_format(struct list_head *list, char *name)
{
struct perf_pmu_format *format;
@@ -269,11 +255,6 @@ enum pmu_event_file_kind pmu_event_file_kind(const char *name, size_t *base_len)
return PMU_EVENT_FILE_BASE;
}
-static inline bool pmu_alias_info_file(const char *name)
-{
- return pmu_event_file_kind(name, NULL) != PMU_EVENT_FILE_BASE;
-}
-
static const struct {
const char *suffix;
size_t len;
@@ -310,120 +291,6 @@ enum pmu_metric_file_kind pmu_metric_file_kind(const char *name, size_t *base_le
return PMU_METRIC_FILE_BASE;
}
-static int perf_pmu__parse_scale_at(int events_dir_fd, const char *event_name,
- struct perf_pmu_alias *alias)
-{
- ssize_t sret;
- char scale[128];
- char filename[NAME_MAX + 16];
- int fd, ret = -1;
-
- scnprintf(filename, sizeof(filename), "%s.scale", event_name);
- fd = openat(events_dir_fd, filename, O_RDONLY);
- if (fd == -1)
- return -1;
-
- sret = read(fd, scale, sizeof(scale) - 1);
- if (sret <= 0)
- goto error;
-
- if (scale[sret - 1] == '\n')
- scale[sret - 1] = '\0';
- else
- scale[sret] = '\0';
-
- ret = perf_pmu__convert_scale(scale, NULL, &alias->scale);
- if (!ret)
- alias->has_scale = true;
-error:
- close(fd);
- return ret;
-}
-
-static int perf_pmu__parse_unit_at(int events_dir_fd, const char *event_name,
- struct perf_pmu_alias *alias)
-{
- char filename[NAME_MAX + 16];
- ssize_t sret;
- int fd;
-
- scnprintf(filename, sizeof(filename), "%s.unit", event_name);
- fd = openat(events_dir_fd, filename, O_RDONLY);
- if (fd == -1)
- return -1;
-
- sret = read(fd, alias->unit, UNIT_MAX_LEN);
- if (sret <= 0)
- goto error;
-
- close(fd);
-
- if (alias->unit[sret - 1] == '\n')
- alias->unit[sret - 1] = '\0';
- else
- alias->unit[sret] = '\0';
-
- alias->has_unit = true;
- return 0;
-error:
- close(fd);
- alias->unit[0] = '\0';
- return -1;
-}
-
-static int perf_pmu__parse_bool_at(int events_dir_fd, const char *event_name,
- const char *suffix, bool *out_val)
-{
- char filename[NAME_MAX + 32];
- char buf[16];
- ssize_t sret;
- int fd;
-
- scnprintf(filename, sizeof(filename), "%s.%s", event_name, suffix);
- fd = openat(events_dir_fd, filename, O_RDONLY);
- if (fd == -1)
- return -ENOENT;
-
- sret = read(fd, buf, sizeof(buf) - 1);
- close(fd);
- if (sret <= 0)
- return -EINVAL;
-
- while (sret > 0 && isspace(buf[sret - 1]))
- sret--;
- buf[sret] = '\0';
-
- if (!strcmp(buf, "1") || !strcasecmp(buf, "true") || !strcasecmp(buf, "yes")) {
- *out_val = true;
- return 0;
- }
- if (!strcmp(buf, "0") || !strcasecmp(buf, "false") || !strcasecmp(buf, "no")) {
- *out_val = false;
- return 0;
- }
- return -EINVAL;
-}
-
-static void read_alias_info_at(int events_dir_fd, struct perf_pmu_alias *alias)
-{
- bool val;
-
- if (!alias->from_sysfs || alias->info_loaded || events_dir_fd < 0)
- return;
-
- perf_pmu__parse_unit_at(events_dir_fd, alias->name, alias);
- perf_pmu__parse_scale_at(events_dir_fd, alias->name, alias);
- if (perf_pmu__parse_bool_at(events_dir_fd, alias->name, "per-pkg", &val) == 0) {
- alias->per_pkg = val;
- alias->has_per_pkg = true;
- }
- if (perf_pmu__parse_bool_at(events_dir_fd, alias->name, "snapshot", &val) == 0) {
- alias->snapshot = val;
- alias->has_snapshot = true;
- }
- alias->info_loaded = true;
-}
-
/* Delete an alias entry. */
void perf_pmu_free_alias(struct perf_pmu_alias *alias)
{
@@ -536,393 +403,22 @@ static void perf_pmu__del_metricgroups(struct perf_pmu *pmu)
pmu->metricgroups = NULL;
}
-static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
- const char *desc, const char *val, int val_fd,
- const struct pmu_event *pe, enum event_source src,
- int events_dir_fd);
-
static struct perf_pmu_alias *perf_pmu__find_alias(struct perf_pmu *pmu,
const char *name,
bool load)
{
struct perf_pmu_alias *alias = NULL;
- char path[PATH_MAX];
- char lower_name[NAME_MAX];
- char upper_name[NAME_MAX];
- const char *candidates[3];
- size_t nr_candidates = 0, name_len, path_len;
- int events_dir_fd;
-
- if (hashmap__find(pmu->aliases, name, &alias))
- return alias;
-
- if (!load || pmu->sysfs_aliases_loaded)
- return NULL;
-
- name_len = strlen(name);
- if (name_len == 0 || name_len >= NAME_MAX || name[0] == '.' ||
- strchr(name, '/') || pmu_alias_info_file(name))
- return NULL;
- path_len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!path_len)
+ if (!pmu || !name)
return NULL;
- scnprintf(path + path_len, sizeof(path) - path_len, "%s/events", pmu->name);
- events_dir_fd = open(path, O_DIRECTORY | O_RDONLY);
- if (events_dir_fd == -1) {
- if (errno == ENOENT)
- pmu->sysfs_aliases_loaded = true;
+ if (!load) {
+ if (hashmap__find(pmu->aliases, name, &alias))
+ return alias->terms ? alias : NULL;
return NULL;
}
- candidates[nr_candidates++] = name;
- for (size_t i = 0; i < name_len; i++) {
- lower_name[i] = tolower((unsigned char)name[i]);
- upper_name[i] = toupper((unsigned char)name[i]);
- }
- lower_name[name_len] = '\0';
- upper_name[name_len] = '\0';
-
- if (strcmp(lower_name, name) != 0)
- candidates[nr_candidates++] = lower_name;
- if (strcmp(upper_name, name) != 0 && strcmp(upper_name, lower_name) != 0)
- candidates[nr_candidates++] = upper_name;
-
- /*
- * Lazily open only the requested event and its sidecars via openat()
- * rather than scanning every file in sysfs/events/.
- */
- for (size_t i = 0; i < nr_candidates; i++) {
- struct stat st;
- int fd = openat(events_dir_fd, candidates[i], O_RDONLY);
-
- if (fd < 0)
- continue;
- if (fstat(fd, &st) == 0 && !S_ISDIR(st.st_mode)) {
- if (perf_pmu__new_alias(pmu, candidates[i], /*desc=*/NULL,
- /*val=*/NULL, fd, /*pe=*/NULL,
- EVENT_SRC_SYSFS, events_dir_fd) == 0 &&
- hashmap__find(pmu->aliases, name, &alias)) {
- read_alias_info_at(events_dir_fd, alias);
- close(fd);
- break;
- }
- }
- close(fd);
- }
-
- close(events_dir_fd);
- return alias;
-}
-
-static bool assign_str(const char *name, const char *field, char **old_str,
- const char *new_str)
-{
- if (!*old_str && new_str) {
- *old_str = strdup(new_str);
- return true;
- }
-
- if (!new_str || !strcasecmp(*old_str, new_str))
- return false; /* Nothing to update. */
-
- pr_debug("alias %s differs in field '%s' ('%s' != '%s')\n",
- name, field, *old_str, new_str);
- zfree(old_str);
- *old_str = strdup(new_str);
- return true;
-}
-
-static void read_alias_info(struct perf_pmu *pmu, struct perf_pmu_alias *alias)
-{
- char path[PATH_MAX];
- size_t len;
- int events_dir_fd;
-
- if (!alias->from_sysfs || alias->info_loaded)
- return;
-
- len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!len)
- return;
- scnprintf(path + len, sizeof(path) - len, "%s/events", pmu->name);
-
- events_dir_fd = open(path, O_DIRECTORY | O_RDONLY);
- if (events_dir_fd == -1)
- return;
-
- read_alias_info_at(events_dir_fd, alias);
- close(events_dir_fd);
-}
-
-struct update_alias_data {
- struct perf_pmu *pmu;
- struct perf_pmu_alias *alias;
- int events_dir_fd;
- bool legacy;
-};
-
-static int update_alias(const struct pmu_event *pe,
- const struct pmu_events_table *table __maybe_unused,
- void *vdata)
-{
- struct update_alias_data *data = vdata;
- int ret = 0;
-
- if (data->events_dir_fd >= 0)
- read_alias_info_at(data->events_dir_fd, data->alias);
- else
- read_alias_info(data->pmu, data->alias);
- assign_str(pe->name, "desc", &data->alias->desc, pe->desc);
- assign_str(pe->name, "long_desc", &data->alias->long_desc, pe->long_desc);
- assign_str(pe->name, "topic", &data->alias->topic, pe->topic);
- data->alias->per_pkg = pe->perpkg;
- if (pe->event) {
- if (data->legacy) {
- zfree(&data->alias->legacy_terms);
- data->alias->legacy_terms = strdup(pe->event);
- } else {
- zfree(&data->alias->terms);
- data->alias->terms = strdup(pe->event);
- }
- }
- if (!ret && pe->unit) {
- char *unit;
-
- ret = perf_pmu__convert_scale(pe->unit, &unit, &data->alias->scale);
- if (!ret)
- snprintf(data->alias->unit, sizeof(data->alias->unit), "%s", unit);
- }
- if (!ret && pe->retirement_latency_mean) {
- ret = parse_double(pe->retirement_latency_mean, NULL,
- &data->alias->retirement_latency_mean);
- }
- if (!ret && pe->retirement_latency_min) {
- ret = parse_double(pe->retirement_latency_min, NULL,
- &data->alias->retirement_latency_min);
- }
- if (!ret && pe->retirement_latency_max) {
- ret = parse_double(pe->retirement_latency_max, NULL,
- &data->alias->retirement_latency_max);
- }
- return ret;
-}
-
-static int perf_pmu__new_alias(struct perf_pmu *pmu, const char *name,
- const char *desc, const char *val, int val_fd,
- const struct pmu_event *pe, enum event_source src,
- int events_dir_fd)
-{
- struct perf_pmu_alias *alias, *old_alias;
- int ret = 0;
- const char *long_desc = NULL, *topic = NULL, *unit = NULL, *pmu_name = NULL;
- bool deprecated = false, perpkg = false;
-
- if (perf_pmu__find_alias(pmu, name, /*load=*/ false)) {
- /* Alias was already created/loaded. */
- return 0;
- }
-
- if (pe) {
- long_desc = pe->long_desc;
- topic = pe->topic;
- unit = pe->unit;
- perpkg = pe->perpkg;
- deprecated = pe->deprecated;
- if (pe->pmu && strcmp(pe->pmu, "default_core"))
- pmu_name = pe->pmu;
- }
-
- alias = zalloc(sizeof(*alias));
- if (!alias)
- return -ENOMEM;
-
- alias->scale = 1.0;
- alias->unit[0] = '\0';
- alias->per_pkg = perpkg;
- alias->snapshot = false;
- alias->deprecated = deprecated;
- alias->retirement_latency_mean = 0.0;
- alias->retirement_latency_min = 0.0;
- alias->retirement_latency_max = 0.0;
-
- if (!ret && pe && pe->retirement_latency_mean) {
- ret = parse_double(pe->retirement_latency_mean, NULL,
- &alias->retirement_latency_mean);
- }
- if (!ret && pe && pe->retirement_latency_min) {
- ret = parse_double(pe->retirement_latency_min, NULL,
- &alias->retirement_latency_min);
- }
- if (!ret && pe && pe->retirement_latency_max) {
- ret = parse_double(pe->retirement_latency_max, NULL,
- &alias->retirement_latency_max);
- }
- if (ret) {
- perf_pmu_free_alias(alias);
- return ret;
- }
-
- if (val_fd < 0) {
- alias->terms = strdup(val);
- } else {
- char buf[256];
- struct io io;
- size_t line_len;
-
- io__init(&io, val_fd, buf, sizeof(buf));
- ret = io__getline(&io, &alias->terms, &line_len) < 0 ? -errno : 0;
- if (ret) {
- pr_err("Failed to read alias %s\n", name);
- perf_pmu_free_alias(alias);
- return ret;
- }
- if (line_len >= 1 && alias->terms[line_len - 1] == '\n')
- alias->terms[line_len - 1] = '\0';
- }
- alias->name = strdup(name);
- alias->desc = desc ? strdup(desc) : NULL;
- alias->long_desc = long_desc ? strdup(long_desc) : NULL;
- alias->topic = topic ? strdup(topic) : NULL;
- alias->pmu_name = pmu_name ? strdup(pmu_name) : NULL;
- alias->compat = (pe && pe->compat) ? strdup(pe->compat) : NULL;
- if (unit) {
- if (perf_pmu__convert_scale(unit, (char **)&unit, &alias->scale) < 0) {
- perf_pmu_free_alias(alias);
- return -1;
- }
- snprintf(alias->unit, sizeof(alias->unit), "%s", unit);
- }
- switch (src) {
- default:
- case EVENT_SRC_SYSFS:
- alias->from_sysfs = true;
- if (pmu->events_table || pmu->is_core) {
- /* Update an event from sysfs with json data. */
- struct update_alias_data data = {
- .pmu = pmu,
- .alias = alias,
- .events_dir_fd = events_dir_fd,
- .legacy = false,
- };
- if ((pmu_events_table__find_event(pmu->events_table, pmu, name,
- update_alias, &data) == 0)) {
- /*
- * Override sysfs encodings with json encodings
- * specific to the cpuid.
- */
- pmu->cpu_common_json_aliases++;
- }
- if (pmu->is_core) {
- /* Add in legacy encodings. */
- data.legacy = true;
- if (pmu_events_table__find_event(
- perf_pmu__default_core_events_table(),
- pmu, name, update_alias, &data) == 0)
- pmu->cpu_common_json_aliases++;
- }
- }
- pmu->sysfs_aliases++;
- break;
- case EVENT_SRC_CPU_JSON:
- pmu->cpu_json_aliases++;
- break;
- case EVENT_SRC_SYS_JSON:
- pmu->sys_json_aliases++;
- break;
-
- }
- hashmap__set(pmu->aliases, alias->name, alias, /*old_key=*/ NULL, &old_alias);
- perf_pmu_free_alias(old_alias);
- return 0;
-}
-
-/*
- * Reading the pmu event aliases definition, which should be located at:
- * /sys/bus/event_source/devices/<dev>/events as sysfs group attributes.
- */
-static int __pmu_aliases_parse(struct perf_pmu *pmu, int events_dir_fd, bool eager_info)
-{
- struct io_dirent64 *evt_ent;
- struct io_dir event_dir;
-
- io_dir__init(&event_dir, events_dir_fd);
-
- while ((evt_ent = io_dir__readdir(&event_dir))) {
- char *name = evt_ent->d_name;
- int fd;
-
- if (!strcmp(name, ".") || !strcmp(name, "..") || name[0] == '.')
- continue;
-
- /*
- * skip info files parsed in perf_pmu__new_alias()
- */
- if (pmu_alias_info_file(name))
- continue;
-
- fd = openat(events_dir_fd, name, O_RDONLY);
- if (fd == -1) {
- pr_debug("Cannot open %s\n", name);
- continue;
- }
-
- if (perf_pmu__new_alias(pmu, name, /*desc=*/ NULL,
- /*val=*/ NULL, fd, /*pe=*/ NULL,
- EVENT_SRC_SYSFS, events_dir_fd) < 0)
- pr_debug("Cannot set up %s\n", name);
- else if (eager_info) {
- struct perf_pmu_alias *alias;
-
- if (hashmap__find(pmu->aliases, name, &alias))
- read_alias_info_at(events_dir_fd, alias);
- }
- close(fd);
- }
-
- pmu->sysfs_aliases_loaded = true;
- return 0;
-}
-
-static int pmu_aliases_parse(struct perf_pmu *pmu)
-{
- char path[PATH_MAX];
- size_t len;
- int events_dir_fd, ret;
-
- if (pmu->sysfs_aliases_loaded)
- return 0;
-
- len = perf_pmu__event_source_devices_scnprintf(path, sizeof(path));
- if (!len)
- return 0;
- scnprintf(path + len, sizeof(path) - len, "%s/events", pmu->name);
-
- events_dir_fd = open(path, O_DIRECTORY);
- if (events_dir_fd == -1) {
- pmu->sysfs_aliases_loaded = true;
- return 0;
- }
- ret = __pmu_aliases_parse(pmu, events_dir_fd, /*eager_info=*/false);
- close(events_dir_fd);
- return ret;
-}
-
-static int pmu_aliases_parse_eager(struct perf_pmu *pmu, int sysfs_fd)
-{
- char path[NAME_MAX + 8];
- int ret, events_dir_fd;
-
- scnprintf(path, sizeof(path), "%s/events", pmu->name);
- events_dir_fd = openat(sysfs_fd, path, O_DIRECTORY, 0);
- if (events_dir_fd == -1) {
- pmu->sysfs_aliases_loaded = true;
- return 0;
- }
- ret = __pmu_aliases_parse(pmu, events_dir_fd, /*eager_info=*/true);
- close(events_dir_fd);
- return ret;
+ return pmu_layer__find_event(pmu, name);
}
static int pmu_alias_terms(struct perf_pmu_alias *alias, struct list_head *terms)
@@ -1205,78 +701,18 @@ bool pmu_uncore_identifier_match(const char *compat, const char *id)
return match;
}
-static int pmu_add_cpu_aliases_map_callback(const struct pmu_event *pe,
- const struct pmu_events_table *table __maybe_unused,
- void *vdata)
-{
- struct perf_pmu *pmu = vdata;
-
- perf_pmu__new_alias(pmu, pe->name, pe->desc, pe->event, /*val_fd=*/ -1,
- pe, EVENT_SRC_CPU_JSON, /*events_dir_fd=*/ -1);
- return 0;
-}
-
/*
* From the pmu_events_table, find the events that correspond to the given
- * PMU and add them to the list 'head'.
+ * PMU and merge them into pmu->aliases.
*/
void pmu_add_cpu_aliases_table(struct perf_pmu *pmu, const struct pmu_events_table *table)
{
- pmu_events_table__for_each_event(table, pmu, pmu_add_cpu_aliases_map_callback, pmu);
-}
-
-static void pmu_add_cpu_aliases(struct perf_pmu *pmu)
-{
- if (!pmu->events_table && !pmu->is_core)
- return;
-
- if (pmu->cpu_aliases_added)
- return;
-
- pmu_add_cpu_aliases_table(pmu, pmu->events_table);
- if (pmu->is_core)
- pmu_add_cpu_aliases_table(pmu, perf_pmu__default_core_events_table());
-
- pmu->cpu_aliases_added = true;
-}
-
-static int pmu_add_sys_aliases_iter_fn(const struct pmu_event *pe,
- const struct pmu_events_table *table __maybe_unused,
- void *vdata)
-{
- struct perf_pmu *pmu = vdata;
-
- if (!pe->compat || !pe->pmu) {
- /* No data to match. */
- return 0;
- }
-
- if (!perf_pmu__match_wildcard_uncore(pmu->name, pe->pmu) &&
- !perf_pmu__match_wildcard_uncore(pmu->alias_name, pe->pmu)) {
- /* PMU name/alias_name don't match. */
- return 0;
- }
-
- if (pmu_uncore_identifier_match(pe->compat, pmu->id)) {
- /* Id matched. */
- perf_pmu__new_alias(pmu,
- pe->name,
- pe->desc,
- pe->event,
- /*val_fd=*/ -1,
- pe,
- EVENT_SRC_SYS_JSON,
- /*events_dir_fd=*/ -1);
- }
- return 0;
+ pmu_layer__merge_event_table(pmu, table);
}
void pmu_add_sys_aliases(struct perf_pmu *pmu)
{
- if (!pmu->id)
- return;
-
- pmu_for_each_sys_event(pmu_add_sys_aliases_iter_fn, pmu);
+ pmu_layer__merge_sys_events(pmu);
}
static char *pmu_find_alias_name(struct perf_pmu *pmu, int dirfd)
@@ -1466,21 +902,12 @@ struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char
pmu->alias_name = pmu_find_alias_name(pmu, dirfd);
pmu->events_table = perf_pmu__find_events_table(pmu);
pmu->metrics_table = perf_pmu__lookup_metrics_table(pmu);
- /*
- * Load the sys json events/aliases when loading the PMU as each event
- * may have a different compat regular expression. We therefore can't
- * know the number of sys json events/aliases without computing the
- * regular expressions for them all.
- */
- pmu_add_sys_aliases(pmu);
list_add_tail(&pmu->list, pmus);
perf_pmu__arch_init(pmu);
- if (eager_load) {
- pmu_aliases_parse_eager(pmu, dirfd);
- pmu->eager_sysfs_loaded = true;
- }
+ if (eager_load)
+ pmu_layer__load_events_at(pmu, dirfd);
return pmu;
}
@@ -1917,7 +1344,6 @@ int perf_pmu__config(struct perf_pmu *pmu, struct perf_event_attr *attr,
static struct perf_pmu_alias *pmu_find_alias(struct perf_pmu *pmu,
struct parse_events_term *term)
{
- struct perf_pmu_alias *alias;
const char *name;
if (parse_events__is_hardcoded_term(term))
@@ -1938,32 +1364,16 @@ static struct perf_pmu_alias *pmu_find_alias(struct perf_pmu *pmu,
return NULL;
}
- alias = perf_pmu__find_alias(pmu, name, /*load=*/ true);
- if (alias || pmu->cpu_aliases_added)
- return alias;
-
- /* Alias doesn't exist, try to get it from the json events. */
- if ((pmu_events_table__find_event(pmu->events_table, pmu, name,
- pmu_add_cpu_aliases_map_callback,
- pmu) == 0) ||
- (pmu->is_core &&
- pmu_events_table__find_event(perf_pmu__default_core_events_table(),
- pmu, name,
- pmu_add_cpu_aliases_map_callback,
- pmu) == 0)) {
- alias = perf_pmu__find_alias(pmu, name, /*load=*/ false);
- }
- return alias;
+ return perf_pmu__find_alias(pmu, name, /*load=*/true);
}
-static int check_info_data(struct perf_pmu *pmu,
+static int check_info_data(struct perf_pmu *pmu __maybe_unused,
struct perf_pmu_alias *alias,
struct perf_pmu_info *info,
struct parse_events_error *err,
int column)
{
- read_alias_info(pmu, alias);
/*
* Only one term in event definition can
* define unit, scale and snapshot, fail
@@ -2311,20 +1721,13 @@ bool perf_pmu__have_event(struct perf_pmu *pmu, const char *name)
return hwmon_pmu__have_event(pmu, name);
if (perf_pmu__is_drm(pmu))
return drm_pmu__have_event(pmu, name);
- if (perf_pmu__find_alias(pmu, name, /*load=*/ true) != NULL)
- return true;
- if (pmu->cpu_aliases_added || (!pmu->events_table && !pmu->is_core))
- return false;
- if (pmu_events_table__find_event(pmu->events_table, pmu, name, NULL, NULL) == 0)
- return true;
- return pmu->is_core &&
- pmu_events_table__find_event(perf_pmu__default_core_events_table(),
- pmu, name, NULL, NULL) == 0;
+ return perf_pmu__find_alias(pmu, name, /*load=*/true) != NULL;
}
size_t perf_pmu__num_events(struct perf_pmu *pmu)
{
- size_t nr;
+ struct hashmap_entry *entry;
+ size_t bkt, nr = 0;
if (perf_pmu__is_tracepoint(pmu))
return tp_pmu__num_events(pmu);
@@ -2333,25 +1736,17 @@ size_t perf_pmu__num_events(struct perf_pmu *pmu)
if (perf_pmu__is_drm(pmu))
return drm_pmu__num_events(pmu);
- pmu_aliases_parse(pmu);
- nr = pmu->sysfs_aliases + pmu->sys_json_aliases;
+ pmu_layer__load_events(pmu);
+ hashmap__for_each_entry(pmu->aliases, entry, bkt) {
+ struct perf_pmu_alias *event = entry->pvalue;
- if (pmu->cpu_aliases_added) {
- nr += pmu->cpu_json_aliases;
- } else if (pmu->events_table || pmu->is_core) {
- nr += pmu_events_table__num_events(pmu->events_table, pmu);
- if (pmu->is_core) {
- nr += pmu_events_table__num_events(
- perf_pmu__default_core_events_table(), pmu);
- }
- nr -= pmu->cpu_common_json_aliases;
- } else {
- assert(pmu->cpu_json_aliases == 0 && pmu->cpu_common_json_aliases == 0);
+ if (!event->terms)
+ continue;
+ if (perf_pmu__is_tool(pmu) && tool_pmu__skip_event(event->name))
+ continue;
+ nr++;
}
- if (perf_pmu__is_tool(pmu))
- nr -= tool_pmu__num_skip_events();
-
return pmu->selectable ? nr + 1 : nr;
}
@@ -2469,12 +1864,13 @@ int perf_pmu__for_each_event(struct perf_pmu *pmu, bool skip_duplicate_pmus,
if (perf_pmu__is_drm(pmu))
return drm_pmu__for_each_event(pmu, state, cb);
- pmu_aliases_parse(pmu);
- pmu_add_cpu_aliases(pmu);
+ pmu_layer__load_events(pmu);
hashmap__for_each_entry(pmu->aliases, entry, bkt) {
struct perf_pmu_alias *event = entry->pvalue;
size_t buf_used, pmu_name_len;
+ if (!event->terms)
+ continue;
if (perf_pmu__is_tool(pmu) && tool_pmu__skip_event(event->name))
continue;
@@ -2999,12 +2395,15 @@ const char *perf_pmu__name_from_config(struct perf_pmu *pmu, u64 config)
if (!pmu)
return NULL;
- pmu_aliases_parse(pmu);
- pmu_add_cpu_aliases(pmu);
+ pmu_layer__load_events(pmu);
hashmap__for_each_entry(pmu->aliases, entry, bkt) {
struct perf_pmu_alias *event = entry->pvalue;
struct perf_event_attr attr = {.config = 0,};
- int ret = perf_pmu__parse_terms_to_attr(pmu, event->terms, &attr);
+ int ret;
+
+ if (!event->terms)
+ continue;
+ ret = perf_pmu__parse_terms_to_attr(pmu, event->terms, &attr);
if (ret == 0 && config == attr.config)
return event->name;
diff --git a/tools/perf/util/pmu.h b/tools/perf/util/pmu.h
index 2587279fa0ce..42a0f5655b1c 100644
--- a/tools/perf/util/pmu.h
+++ b/tools/perf/util/pmu.h
@@ -160,17 +160,6 @@ struct perf_pmu {
* @metrics_table: The metrics table for json metrics in pmu-events.c.
*/
const struct pmu_metrics_table *metrics_table;
- /** @sysfs_aliases: Number of sysfs aliases loaded. */
- uint32_t sysfs_aliases;
- /** @cpu_json_aliases: Number of json event aliases loaded specific to the CPUID. */
- uint32_t cpu_json_aliases;
- /** @sys_json_aliases: Number of json event aliases loaded matching the PMU's identifier. */
- uint32_t sys_json_aliases;
- /**
- * @cpu_common_json_aliases: Number of json events that overlapped with sysfs when
- * loading all sysfs events.
- */
- uint32_t cpu_common_json_aliases;
/** @sysfs_aliases_loaded: Are sysfs aliases loaded from disk? */
bool sysfs_aliases_loaded;
/**
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 07/11] perf metricgroup: Route metric and metricgroup lookups through struct perf_pmu
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (5 preceding siblings ...)
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 ` Ian Rogers
2026-11-13 21:10 ` [PATCH 08/11] perf list, jevents: Add --export-dir and --output-dir sysfs-style exporters Ian Rogers
` (3 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Expose metric and metricgroup lookup and enumeration helpers on struct
perf_pmu and perf_pmus (perf_pmu__find_metric(),
perf_pmu__for_each_metric(), perf_pmu__describe_metricgroup(),
perf_pmus__find_metric(), perf_pmus__find_metric_pmu(),
perf_pmus__for_each_metric(), perf_pmus__for_each_pmu_metric(), and
perf_pmus__describe_metricgroup()) backed by the layered PMU VFS stack,
using struct pmu_metric at the public boundary so struct perf_pmu_metric
remains private.
Scan all core and uncore PMUs without suffix collapsing so heterogeneous
PMUs are preserved, deduplicating identical shared metrics across PMU
instances while resolving sub-metrics and thresholds on their
originating struct perf_pmu and letting user metricgroup overrides on
any PMU take precedence over compiled-in defaults.
Update live callers in metricgroup.c, print-events.c, builtin-list.c,
builtin-script.c, and util/python.c to query metrics and metricgroup
descriptions through struct perf_pmu rather than reading compiled-in
pmu_metrics_table arrays directly, while retaining explicit table
overrides for fake_pmu unit tests.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/builtin-list.c | 2 +-
tools/perf/builtin-script.c | 2 +-
tools/perf/util/metricgroup.c | 369 +++++++++++++++++++++++++++------
tools/perf/util/pmu.c | 65 +++++-
tools/perf/util/pmu.h | 7 +
tools/perf/util/pmus.c | 237 ++++++++++++++++++++-
tools/perf/util/pmus.h | 13 ++
tools/perf/util/print-events.c | 4 +-
tools/perf/util/python.c | 5 +-
9 files changed, 628 insertions(+), 76 deletions(-)
diff --git a/tools/perf/builtin-list.c b/tools/perf/builtin-list.c
index 47668d66c395..53e1834233c8 100644
--- a/tools/perf/builtin-list.c
+++ b/tools/perf/builtin-list.c
@@ -232,7 +232,7 @@ static void default_print_metric(void *ps,
fprintf(fp, "%s ", group);
} else {
const char *gdesc = print_state->desc
- ? describe_metricgroup(group)
+ ? perf_pmus__describe_metricgroup(group)
: NULL;
const char *print_colon = "";
diff --git a/tools/perf/builtin-script.c b/tools/perf/builtin-script.c
index b6691ebb1b4e..f4c61172eb84 100644
--- a/tools/perf/builtin-script.c
+++ b/tools/perf/builtin-script.c
@@ -2546,7 +2546,7 @@ static void perf_sample__fprint_metric(struct thread *thread,
/*needs_sort=*/false);
}
- metricgroup__for_each_metric(pmu_metrics_table__find(), script_find_metrics, &args);
+ metricgroup__for_each_metric(/*table=*/NULL, script_find_metrics, &args);
init_metrics = true;
}
diff --git a/tools/perf/util/metricgroup.c b/tools/perf/util/metricgroup.c
index 779fe56fe8d0..ebe9873d273f 100644
--- a/tools/perf/util/metricgroup.c
+++ b/tools/perf/util/metricgroup.c
@@ -128,6 +128,7 @@ struct metric {
*/
struct expr_parse_ctx *pctx;
const char *pmu;
+ struct perf_pmu *source_pmu;
/** The name of the metric such as "IPC". */
const char *metric_name;
/** Modifier on the metric such as "u" or NULL for none. */
@@ -219,6 +220,7 @@ static void metric__free(struct metric *m)
}
static struct metric *metric__new(const struct pmu_metric *pm,
+ struct perf_pmu *source_pmu,
const char *modifier,
bool metric_no_group,
bool metric_no_threshold,
@@ -237,7 +239,9 @@ static struct metric *metric__new(const struct pmu_metric *pm,
if (!m->pctx)
goto out_err;
- m->pmu = pm->pmu ?: "cpu";
+ m->pmu = (source_pmu && source_pmu->is_core && (!pm->pmu || !pm->pmu[0]))
+ ? "default_core" : (pm->pmu ?: "cpu");
+ m->source_pmu = source_pmu;
m->metric_name = pm->metric_name;
m->default_metricgroup_name = pm->default_metricgroup_name ?: "";
m->modifier = NULL;
@@ -281,6 +285,16 @@ static bool contains_metric_id(struct evsel **metric_events, int num_events,
return false;
}
+static bool is_core_pmu_name(const char *pmu)
+{
+ struct perf_pmu *perf_pmu;
+
+ if (is_pmu_core(pmu))
+ return true;
+ perf_pmu = perf_pmus__find(pmu);
+ return perf_pmu && perf_pmu->is_core;
+}
+
/**
* setup_metric_events - Find a group of events in metric_evlist that correspond
* to the IDs from a parsed metric expression.
@@ -298,7 +312,7 @@ static int setup_metric_events(const char *pmu, struct hashmap *ids,
struct evsel *ev;
size_t ids_size, matched_events, i;
bool all_pmus = !strcmp(pmu, "all") || !strcmp(pmu, "default_core") ||
- perf_pmus__num_core_pmus() == 1 || !is_pmu_core(pmu);
+ perf_pmus__num_core_pmus() == 1 || !is_core_pmu_name(pmu);
*out_metric_events = NULL;
ids_size = hashmap__size(ids);
@@ -359,6 +373,10 @@ static int setup_metric_events(const char *pmu, struct hashmap *ids,
*/
metric_id = evsel__metric_id(ev);
evlist__for_each_entry_continue(metric_evlist, ev) {
+ /* Don't match events for the wrong hybrid PMU. */
+ if (!all_pmus && ev->pmu && evsel__is_hybrid(ev) &&
+ strcmp(ev->pmu->name, pmu))
+ continue;
if (!strcmp(evsel__metric_id(ev), metric_id))
ev->metric_leader = metric_events[i];
}
@@ -386,18 +404,32 @@ static bool match_metric_or_groups(const char *metric_or_groups, const char *sou
return m && match_metric_or_groups(m + 1, sought);
}
-static bool match_pm_metric_or_groups(const struct pmu_metric *pm, const char *pmu,
+static bool match_pm_metric_or_groups(const struct pmu_metric *pm,
+ struct perf_pmu *source_pmu,
+ const char *pmu,
const char *metric_or_groups)
{
const char *pm_pmu = pm->pmu ?: "cpu";
struct perf_pmu *perf_pmu = NULL;
- if (pm->pmu)
- perf_pmu = perf_pmus__find(pm->pmu);
-
- if (strcmp(pmu, "all") && strcmp(pm_pmu, pmu) &&
- (perf_pmu && !perf_pmu__name_wildcard_match(perf_pmu, pmu)))
- return false;
+ if (strcmp(pmu, "all")) {
+ if (source_pmu) {
+ if (!perf_pmu__name_wildcard_match(source_pmu, pmu) &&
+ !perf_pmu__wildcard_match(source_pmu, pmu))
+ return false;
+ } else if (!strcmp(pm_pmu, "default_core") && is_core_pmu_name(pmu)) {
+ /* default_core matches any core PMU filter. */
+ } else {
+ if (pm->pmu)
+ perf_pmu = perf_pmus__find(pm->pmu);
+
+ if (strcmp(pm_pmu, pmu) &&
+ (!perf_pmu ||
+ (!perf_pmu__name_wildcard_match(perf_pmu, pmu) &&
+ !perf_pmu__wildcard_match(perf_pmu, pmu))))
+ return false;
+ }
+ }
return match_metric_or_groups(pm->metric_group, metric_or_groups) ||
match_metric_or_groups(pm->metric_name, metric_or_groups);
@@ -435,6 +467,9 @@ int metricgroup__for_each_metric(const struct pmu_metrics_table *table, pmu_metr
pmu_metrics_table__default(),
};
+ if (!table)
+ return perf_pmus__for_each_metric(fn, data);
+
for (size_t i = 0; i < ARRAY_SIZE(tables); i++) {
int ret;
@@ -696,6 +731,7 @@ struct metricgroup_add_iter_data {
static int add_metric(struct list_head *metric_list,
const struct pmu_metric *pm,
+ struct perf_pmu *source_pmu,
const char *modifier,
bool metric_no_group,
bool metric_no_threshold,
@@ -720,11 +756,13 @@ static int metricgroup__find_metric_callback(const struct pmu_metric *pm,
* resolve_metric - Locate metrics within the root metric and recursively add
* references to them.
* @metric_list: The list the metric is added to.
- * @pmu: The PMU name to resolve metrics on, or "all" for all PMUs.
+ * @pm_pmu: The PMU name pattern to resolve metrics on when @pmu is NULL.
+ * @pmu: Originating PMU struct when known, or target PMU for @fake_pmu.
* @modifier: if non-null event modifiers like "u".
* @metric_no_group: Should events written to events be grouped "{}" or
* global. Grouping is the default but due to multiplexing the
* user may override.
+ * @metric_no_threshold: Should threshold expressions be ignored?
* @user_requested_cpu_list: Command line specified CPUs to record on.
* @system_wide: Are events for all processes recorded.
* @fake_pmu: Are PMUs and events being faked for testing?
@@ -737,6 +775,7 @@ static int metricgroup__find_metric_callback(const struct pmu_metric *pm,
* architecture perf is running upon.
*/
static int resolve_metric(struct list_head *metric_list,
+ const char *pm_pmu,
struct perf_pmu *pmu,
const char *modifier,
bool metric_no_group,
@@ -767,10 +806,18 @@ static int resolve_metric(struct list_head *metric_list,
*/
hashmap__for_each_entry(root_metric->pctx->ids, cur, bkt) {
struct pmu_metric pm;
+ bool found;
- if (pmu_metrics_table__find_metric(table, pmu, cur->pkey,
- metricgroup__find_metric_callback,
- &pm) != PMU_METRICS__NOT_FOUND) {
+ if (fake_pmu && table) {
+ found = pmu_metrics_table__find_metric(table, pmu, cur->pkey,
+ metricgroup__find_metric_callback,
+ &pm) != PMU_METRICS__NOT_FOUND;
+ } else {
+ found = pmu ? perf_pmu__find_metric(pmu, cur->pkey, &pm)
+ : perf_pmus__find_metric_pmu(pm_pmu, cur->pkey, &pm);
+ }
+
+ if (found) {
pending = realloc(pending,
(pending_cnt + 1) * sizeof(struct to_resolve));
if (!pending)
@@ -791,8 +838,9 @@ static int resolve_metric(struct list_head *metric_list,
* context.
*/
for (i = 0; i < pending_cnt; i++) {
- ret = add_metric(metric_list, &pending[i].pm, modifier, metric_no_group,
- metric_no_threshold, user_requested_cpu_list, system_wide,
+ ret = add_metric(metric_list, &pending[i].pm, pmu, modifier,
+ metric_no_group, metric_no_threshold,
+ user_requested_cpu_list, system_wide,
fake_pmu, root_metric, visited, table);
if (ret)
break;
@@ -806,6 +854,7 @@ static int resolve_metric(struct list_head *metric_list,
* __add_metric - Add a metric to metric_list.
* @metric_list: The list the metric is added to.
* @pm: The pmu_metric containing the metric to be added.
+ * @source_pmu: Originating PMU struct when known, or NULL.
* @modifier: if non-null event modifiers like "u".
* @metric_no_group: Should events written to events be grouped "{}" or
* global. Grouping is the default but due to multiplexing the
@@ -825,6 +874,7 @@ static int resolve_metric(struct list_head *metric_list,
*/
static int __add_metric(struct list_head *metric_list,
const struct pmu_metric *pm,
+ struct perf_pmu *source_pmu,
const char *modifier,
bool metric_no_group,
bool metric_no_threshold,
@@ -857,8 +907,10 @@ static int __add_metric(struct list_head *metric_list,
* This metric is the root of a tree and may reference other
* metrics that are added recursively.
*/
- root_metric = metric__new(pm, modifier, metric_no_group, metric_no_threshold,
- runtime, user_requested_cpu_list, system_wide, fake_pmu);
+ root_metric = metric__new(pm, source_pmu, modifier,
+ metric_no_group, metric_no_threshold,
+ runtime, user_requested_cpu_list,
+ system_wide, fake_pmu);
if (!root_metric)
return -ENOMEM;
@@ -922,14 +974,17 @@ static int __add_metric(struct list_head *metric_list,
if (!ret) {
/* Resolve referenced metrics. */
- struct perf_pmu *pmu;
+ const char *pm_pmu = (pm->pmu && pm->pmu[0]) ? pm->pmu : "default_core";
+ struct perf_pmu *pmu = source_pmu;
- if (pm->pmu && pm->pmu[0] != '\0')
- pmu = perf_pmus__find(pm->pmu);
- else
- pmu = perf_pmus__scan_core(/*pmu=*/ NULL);
+ if (!pmu && fake_pmu && table) {
+ if (pm->pmu && pm->pmu[0] != '\0')
+ pmu = perf_pmus__find(pm->pmu);
+ else
+ pmu = perf_pmus__scan_core(/*pmu=*/NULL);
+ }
- ret = resolve_metric(metric_list, pmu, modifier, metric_no_group,
+ ret = resolve_metric(metric_list, pm_pmu, pmu, modifier, metric_no_group,
metric_no_threshold, user_requested_cpu_list,
system_wide, fake_pmu, root_metric, &visited_node,
table);
@@ -946,6 +1001,7 @@ static int __add_metric(struct list_head *metric_list,
static int add_metric(struct list_head *metric_list,
const struct pmu_metric *pm,
+ struct perf_pmu *source_pmu,
const char *modifier,
bool metric_no_group,
bool metric_no_threshold,
@@ -961,9 +1017,10 @@ static int add_metric(struct list_head *metric_list,
pr_debug("metric expr %s for %s\n", pm->metric_expr, pm->metric_name);
if (!strstr(pm->metric_expr, "?")) {
- ret = __add_metric(metric_list, pm, modifier, metric_no_group,
- metric_no_threshold, 0, user_requested_cpu_list,
- system_wide, fake_pmu, root_metric, visited, table);
+ ret = __add_metric(metric_list, pm, source_pmu, modifier,
+ metric_no_group, metric_no_threshold, 0,
+ user_requested_cpu_list, system_wide,
+ fake_pmu, root_metric, visited, table);
} else {
int j, count;
@@ -975,9 +1032,10 @@ static int add_metric(struct list_head *metric_list,
*/
for (j = 0; j < count && !ret; j++)
- ret = __add_metric(metric_list, pm, modifier, metric_no_group,
- metric_no_threshold, j, user_requested_cpu_list,
- system_wide, fake_pmu, root_metric, visited, table);
+ ret = __add_metric(metric_list, pm, source_pmu, modifier,
+ metric_no_group, metric_no_threshold, j,
+ user_requested_cpu_list, system_wide,
+ fake_pmu, root_metric, visited, table);
}
return ret;
@@ -1042,26 +1100,141 @@ struct metricgroup__add_metric_data {
bool has_match;
};
-static int metricgroup__add_metric_callback(const struct pmu_metric *pm,
- const struct pmu_metrics_table *table,
- void *vdata)
+static size_t metric_refs_count(const struct metric_ref *refs)
+{
+ size_t n = 0;
+
+ if (!refs)
+ return 0;
+ while (refs[n].metric_name)
+ n++;
+ return n;
+}
+
+static bool metric__same_definition(const struct metric *a, const struct metric *b)
+{
+ size_t na, nb;
+
+ if (strcmp(a->metric_name ?: "", b->metric_name ?: "") ||
+ strcmp(a->modifier ?: "", b->modifier ?: "") ||
+ strcmp(a->metric_expr ?: "", b->metric_expr ?: "") ||
+ strcmp(a->metric_threshold ?: "", b->metric_threshold ?: "") ||
+ a->pctx->sctx.runtime != b->pctx->sctx.runtime)
+ return false;
+
+ na = metric_refs_count(a->metric_refs);
+ nb = metric_refs_count(b->metric_refs);
+ if (na != nb)
+ return false;
+
+ for (size_t i = 0; i < na; i++) {
+ bool found = false;
+
+ for (size_t j = 0; j < nb; j++) {
+ if (!strcmp(a->metric_refs[i].metric_name,
+ b->metric_refs[j].metric_name) &&
+ !strcmp(a->metric_refs[i].metric_expr ?: "",
+ b->metric_refs[j].metric_expr ?: "")) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+ }
+ return true;
+}
+
+static void metricgroup__dedup_and_pin_metrics(struct list_head *list)
+{
+ struct metric *m, *other, *tmp;
+ int total_core_pmus = perf_pmus__num_core_pmus();
+
+ if (total_core_pmus > 1) {
+ list_for_each_entry(m, list, nd) {
+ size_t core_matches = 0;
+ bool uniform = true;
+
+ if (!m->source_pmu || !m->source_pmu->is_core ||
+ strcmp(m->pmu, "default_core") != 0)
+ continue;
+
+ list_for_each_entry(other, list, nd) {
+ if (!other->source_pmu || !other->source_pmu->is_core ||
+ strcmp(other->pmu, "default_core") != 0)
+ continue;
+ if (strcmp(m->metric_name ?: "", other->metric_name ?: "") ||
+ strcmp(m->modifier ?: "", other->modifier ?: "") ||
+ m->pctx->sctx.runtime != other->pctx->sctx.runtime)
+ continue;
+ core_matches++;
+ if (!metric__same_definition(m, other))
+ uniform = false;
+ }
+ if (!uniform || core_matches < (size_t)total_core_pmus) {
+ list_for_each_entry(other, list, nd) {
+ if (!other->source_pmu ||
+ !other->source_pmu->is_core ||
+ strcmp(other->pmu, "default_core") != 0)
+ continue;
+ if (strcmp(m->metric_name ?: "",
+ other->metric_name ?: "") ||
+ strcmp(m->modifier ?: "",
+ other->modifier ?: "") ||
+ m->pctx->sctx.runtime !=
+ other->pctx->sctx.runtime)
+ continue;
+ other->pmu = other->source_pmu->name;
+ }
+ }
+ }
+ }
+
+ list_for_each_entry_safe(m, tmp, list, nd) {
+ list_for_each_entry(other, list, nd) {
+ if (other == m)
+ break;
+ if (!strcmp(other->pmu ?: "", m->pmu ?: "") &&
+ metric__same_definition(other, m)) {
+ list_del_init(&m->nd);
+ metric__free(m);
+ break;
+ }
+ }
+ }
+}
+
+static int metricgroup__add_pmu_metric_callback(struct perf_pmu *pmu,
+ const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table,
+ void *vdata)
{
struct metricgroup__add_metric_data *data = vdata;
int ret = 0;
- if (pm->metric_expr && match_pm_metric_or_groups(pm, data->pmu, data->metric_name)) {
+ if (pm->metric_expr &&
+ match_pm_metric_or_groups(pm, pmu, data->pmu, data->metric_name)) {
bool metric_no_group = data->metric_no_group ||
match_metric_or_groups(pm->metricgroup_no_group, data->metric_name);
data->has_match = true;
- ret = add_metric(data->list, pm, data->modifier, metric_no_group,
- data->metric_no_threshold, data->user_requested_cpu_list,
- data->system_wide, data->fake_pmu, /*root_metric=*/NULL,
+ ret = add_metric(data->list, pm, pmu, data->modifier,
+ metric_no_group, data->metric_no_threshold,
+ data->user_requested_cpu_list,
+ data->system_wide, data->fake_pmu,
+ /*root_metric=*/NULL,
/*visited_metrics=*/NULL, table);
}
return ret;
}
+static int metricgroup__add_metric_callback(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table,
+ void *vdata)
+{
+ return metricgroup__add_pmu_metric_callback(/*pmu=*/NULL, pm, table, vdata);
+}
+
/**
* metricgroup__add_metric - Find and add a metric, or a metric group.
* @pmu: The PMU name to search for metrics on, or "all" for all PMUs.
@@ -1105,7 +1278,16 @@ static int metricgroup__add_metric(const char *pmu, const char *metric_name, con
* Iterate over all metrics seeing if metric matches either the
* name or group. When it does add the metric to the list.
*/
- ret = metricgroup__for_each_metric(table, metricgroup__add_metric_callback, &data);
+ if (!table) {
+ ret = perf_pmus__for_each_pmu_metric(metricgroup__add_pmu_metric_callback,
+ &data);
+ if (!ret)
+ metricgroup__dedup_and_pin_metrics(&list);
+ } else {
+ ret = metricgroup__for_each_metric(table,
+ metricgroup__add_metric_callback,
+ &data);
+ }
if (!ret && !data.has_match)
ret = -ENOENT;
@@ -1219,14 +1401,68 @@ static void find_tool_events(const struct list_head *metric_list,
}
}
+static bool metric__has_unqualified_id(const struct metric *m)
+{
+ struct hashmap_entry *cur;
+ size_t bkt;
+
+ hashmap__for_each_entry(m->pctx->ids, cur, bkt) {
+ const char *id = cur->pkey;
+
+ if (tool_pmu__str_to_event(id) == TOOL_PMU__EVENT_NONE &&
+ !strchr(id, '@'))
+ return true;
+ }
+ return false;
+}
+
+static bool pmu_filter_allows_other_core_pmu(const char *pmu_filter,
+ const char *target_pmu)
+{
+ struct perf_pmu *core_pmu = NULL;
+
+ while ((core_pmu = perf_pmus__scan_core(core_pmu)) != NULL) {
+ if (perf_pmu__name_wildcard_match(core_pmu, target_pmu))
+ continue;
+ if (!pmu_filter || !strcmp(pmu_filter, "all") ||
+ perf_pmu__wildcard_match(core_pmu, pmu_filter) ||
+ perf_pmu__name_wildcard_match(core_pmu, pmu_filter))
+ return true;
+ }
+ return false;
+}
+
+static bool metric__needs_per_pmu_evlist(const struct metric *m,
+ const char *pmu,
+ bool fake_pmu)
+{
+ if (fake_pmu || !m->pmu || !strcmp(m->pmu, "all") ||
+ !strcmp(m->pmu, "default_core") || !is_core_pmu_name(m->pmu))
+ return false;
+
+ return pmu_filter_allows_other_core_pmu(pmu, m->pmu) &&
+ metric__has_unqualified_id(m);
+}
+
+static bool metric__can_use_combined_evlist(const struct metric *m,
+ const char *pmu,
+ bool fake_pmu)
+{
+ return !m->group_events && !m->modifier &&
+ !metric__needs_per_pmu_evlist(m, pmu, fake_pmu);
+}
+
/**
* build_combined_expr_ctx - Make an expr_parse_ctx with all !group_events
* metric IDs, as the IDs are held in a set,
* duplicates will be removed.
* @metric_list: List to take metrics from.
+ * @pmu: Top-level PMU filter, or "all".
+ * @fake_pmu: Are PMUs and events being faked for testing?
* @combined: Out argument for result.
*/
static int build_combined_expr_ctx(const struct list_head *metric_list,
+ const char *pmu, bool fake_pmu,
struct expr_parse_ctx **combined)
{
struct hashmap_entry *cur;
@@ -1240,7 +1476,7 @@ static int build_combined_expr_ctx(const struct list_head *metric_list,
return -ENOMEM;
list_for_each_entry(m, metric_list, nd) {
- if (!m->group_events && !m->modifier) {
+ if (metric__can_use_combined_evlist(m, pmu, fake_pmu)) {
hashmap__for_each_entry(m->pctx->ids, cur, bkt) {
dup = strdup(cur->pkey);
if (!dup) {
@@ -1430,7 +1666,8 @@ static int parse_groups(struct evlist *perf_evlist,
find_tool_events(&metric_list, tool_events);
- ret = build_combined_expr_ctx(&metric_list, &combined);
+ ret = build_combined_expr_ctx(&metric_list, pmu, fake_pmu,
+ &combined);
if (!ret && combined && hashmap__size(combined->ids)) {
ret = parse_ids(metric_no_merge, fake_pmu, combined,
@@ -1458,7 +1695,8 @@ static int parse_groups(struct evlist *perf_evlist,
struct metric *n;
struct metric_expr *expr;
- if (combined_evlist && !m->group_events) {
+ if (combined_evlist &&
+ metric__can_use_combined_evlist(m, pmu, fake_pmu)) {
metric_evlist = combined_evlist;
} else if (!metric_no_merge) {
/*
@@ -1493,10 +1731,23 @@ static int parse_groups(struct evlist *perf_evlist,
}
}
if (!metric_evlist) {
+ const char *m_filter_pmu =
+ (pmu && strcmp(pmu, "all") != 0) ? pmu : NULL;
+ bool m_cputype_filter = cputype_filter;
+
+ if (!fake_pmu && m->pmu &&
+ strcmp(m->pmu, "all") != 0 &&
+ strcmp(m->pmu, "default_core") != 0 &&
+ is_core_pmu_name(m->pmu) &&
+ pmu_filter_allows_other_core_pmu(pmu, m->pmu)) {
+ if (!m_filter_pmu)
+ m_cputype_filter = true;
+ m_filter_pmu = m->pmu;
+ }
+
ret = parse_ids(metric_no_merge, fake_pmu, m->pctx, m->modifier,
m->group_events, tool_events, &m->evlist,
- (pmu && strcmp(pmu, "all") == 0) ? NULL : pmu,
- cputype_filter);
+ m_filter_pmu, m_cputype_filter);
if (ret)
goto out;
@@ -1589,14 +1840,12 @@ int metricgroup__parse_groups(struct evlist *perf_evlist,
bool system_wide,
bool hardware_aware_grouping)
{
- const struct pmu_metrics_table *table = pmu_metrics_table__find();
-
if (hardware_aware_grouping)
pr_debug("Use hardware aware grouping instead of traditional metric grouping method\n");
return parse_groups(perf_evlist, pmu, cputype_filter, str, metric_no_group, metric_no_merge,
metric_no_threshold, user_requested_cpu_list, system_wide,
- /*fake_pmu=*/false, table);
+ /*fake_pmu=*/false, /*table=*/NULL);
}
int metricgroup__parse_groups_test(struct evlist *evlist,
@@ -1617,28 +1866,30 @@ struct metricgroup__has_metric_data {
const char *pmu;
const char *metric_or_groups;
};
-static int metricgroup__has_metric_or_groups_callback(const struct pmu_metric *pm,
- const struct pmu_metrics_table *table
- __maybe_unused,
- void *vdata)
+static int metricgroup__has_pmu_metric_or_groups_callback(struct perf_pmu *pmu,
+ const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table
+ __maybe_unused,
+ void *vdata)
{
struct metricgroup__has_metric_data *data = vdata;
- return match_pm_metric_or_groups(pm, data->pmu, data->metric_or_groups) ? 1 : 0;
+ if (!pm->metric_expr)
+ return 0;
+
+ return match_pm_metric_or_groups(pm, pmu, data->pmu,
+ data->metric_or_groups) ? 1 : 0;
}
bool metricgroup__has_metric_or_groups(const char *pmu, const char *metric_or_groups)
{
- const struct pmu_metrics_table *table = pmu_metrics_table__find();
struct metricgroup__has_metric_data data = {
.pmu = pmu,
.metric_or_groups = metric_or_groups,
};
- return metricgroup__for_each_metric(table,
- metricgroup__has_metric_or_groups_callback,
- &data)
- ? true : false;
+ return perf_pmus__for_each_pmu_metric(metricgroup__has_pmu_metric_or_groups_callback,
+ &data) > 0;
}
static int metricgroup__topdown_max_level_callback(const struct pmu_metric *pm,
@@ -1662,13 +1913,9 @@ static int metricgroup__topdown_max_level_callback(const struct pmu_metric *pm,
unsigned int metricgroups__topdown_max_level(void)
{
unsigned int max_level = 0;
- const struct pmu_metrics_table *table = pmu_metrics_table__find();
-
- if (!table)
- return false;
- pmu_metrics_table__for_each_metric(table, metricgroup__topdown_max_level_callback,
- &max_level);
+ perf_pmus__for_each_metric(metricgroup__topdown_max_level_callback,
+ &max_level);
return max_level;
}
diff --git a/tools/perf/util/pmu.c b/tools/perf/util/pmu.c
index a3e1630c5a84..9713d266393e 100644
--- a/tools/perf/util/pmu.c
+++ b/tools/perf/util/pmu.c
@@ -421,6 +421,53 @@ static struct perf_pmu_alias *perf_pmu__find_alias(struct perf_pmu *pmu,
return pmu_layer__find_event(pmu, name);
}
+bool perf_pmu__find_metric(struct perf_pmu *pmu, const char *name,
+ struct pmu_metric *out)
+{
+ struct perf_pmu_metric *m = pmu_layer__find_metric(pmu, name);
+
+ if (!m)
+ return false;
+ if (out)
+ perf_pmu_metric__to_pmu_metric(m, out);
+ return true;
+}
+
+int perf_pmu__for_each_metric(struct perf_pmu *pmu, pmu_metric_iter_fn fn,
+ void *data)
+{
+ struct hashmap_entry *entry;
+ size_t bkt;
+ int ret;
+
+ if (!pmu)
+ return 0;
+
+ ret = pmu_layer__load_metrics(pmu);
+ if (ret)
+ return ret;
+
+ hashmap__for_each_entry(pmu->metrics, entry, bkt) {
+ const struct perf_pmu_metric *m = entry->pvalue;
+ struct pmu_metric pm;
+
+ if (!m->metric_expr)
+ continue;
+
+ perf_pmu_metric__to_pmu_metric(m, &pm);
+ ret = fn(&pm, pmu->metrics_table, data);
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+const char *perf_pmu__describe_metricgroup(struct perf_pmu *pmu,
+ const char *group)
+{
+ return pmu_layer__describe_metricgroup(pmu, group);
+}
+
static int pmu_alias_terms(struct perf_pmu_alias *alias, struct list_head *terms)
{
struct parse_events_terms alias_terms;
@@ -855,6 +902,22 @@ static __u32 wellknown_pmu_type(const char *pmu_name)
return PERF_TYPE_MAX;
}
+bool perf_pmu__is_core_at(int dirfd, const char *name)
+{
+ int fd;
+
+ if (is_pmu_core(name))
+ return true;
+ if (dirfd < 0)
+ return false;
+
+ fd = perf_pmu__pathname_fd(dirfd, name, "cpus", O_RDONLY);
+ if (fd < 0)
+ return false;
+ close(fd);
+ return true;
+}
+
struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char *name,
bool eager_load)
{
@@ -892,7 +955,7 @@ struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char
return NULL;
}
- pmu->is_core = is_pmu_core(name);
+ pmu->is_core = perf_pmu__is_core_at(dirfd, name);
pmu->cpus = pmu_cpumask(dirfd, name, pmu->is_core);
pmu->is_uncore = pmu_is_uncore(dirfd, name);
diff --git a/tools/perf/util/pmu.h b/tools/perf/util/pmu.h
index 42a0f5655b1c..ef437cc4c7e6 100644
--- a/tools/perf/util/pmu.h
+++ b/tools/perf/util/pmu.h
@@ -270,6 +270,12 @@ typedef int (*pmu_event_callback)(void *state, struct pmu_event_info *info);
typedef int (*pmu_format_callback)(void *state, const char *name, int config,
const unsigned long *bits);
+bool perf_pmu__find_metric(struct perf_pmu *pmu, const char *name,
+ struct pmu_metric *out);
+int perf_pmu__for_each_metric(struct perf_pmu *pmu, pmu_metric_iter_fn fn,
+ void *data);
+const char *perf_pmu__describe_metricgroup(struct perf_pmu *pmu,
+ const char *group);
void pmu_add_sys_aliases(struct perf_pmu *pmu);
int perf_pmu__config(struct perf_pmu *pmu, struct perf_event_attr *attr,
struct parse_events_terms *head_terms,
@@ -353,6 +359,7 @@ int perf_pmu__event_source_devices_fd(void);
int perf_pmu__pathname_fd(int dirfd, const char *pmu_name, const char *filename, int flags);
int perf_pmu__init(struct perf_pmu *pmu, __u32 type, const char *name);
+bool perf_pmu__is_core_at(int dirfd, const char *name);
struct perf_pmu *perf_pmu__lookup(struct list_head *pmus, int dirfd, const char *lookup_name,
bool eager_load);
struct perf_pmu *perf_pmu__create_placeholder_core_pmu(struct list_head *core_pmus);
diff --git a/tools/perf/util/pmus.c b/tools/perf/util/pmus.c
index 6ecea8f0d382..928dfc80f8db 100644
--- a/tools/perf/util/pmus.c
+++ b/tools/perf/util/pmus.c
@@ -7,7 +7,9 @@
#include <subcmd/pager.h>
#include <sys/types.h>
#include <ctype.h>
+#include <errno.h>
#include <pthread.h>
+#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
@@ -15,6 +17,7 @@
#include "debug.h"
#include "drm_pmu.h"
#include "evsel.h"
+#include "hashmap.h"
#include "pmus.h"
#include "pmu.h"
#include "pmu-layer.h"
@@ -23,6 +26,7 @@
#include "print-events.h"
#include "strbuf.h"
#include "string2.h"
+#include "strlist.h"
/*
* core_pmus: A PMU belongs to core_pmus if it's name is "cpu" or it's sysfs
@@ -61,6 +65,7 @@ enum perf_tool_pmu_type {
PERF_TOOL_PMU_TYPE_DRM_MASK)
};
static unsigned int read_pmu_types;
+static int core_pmus_count;
static void pmu_read_sysfs(unsigned int to_read_pmus);
@@ -125,6 +130,7 @@ void perf_pmus__destroy(void)
perf_pmu__delete(pmu);
}
read_pmu_types = 0;
+ core_pmus_count = 0;
pmu_layers__exit();
}
@@ -491,6 +497,31 @@ static struct perf_pmu *perf_pmus__scan_skip_duplicates(struct perf_pmu *pmu)
return NULL;
}
+struct perf_pmu *perf_pmus__scan_for_metrics(struct perf_pmu *pmu)
+{
+ bool use_core_pmus = !pmu || pmu->is_core;
+
+ if (!pmu) {
+ const unsigned int to_read_pmus =
+ PERF_TOOL_PMU_TYPE_PE_CORE_MASK |
+ PERF_TOOL_PMU_TYPE_PE_OTHER_MASK |
+ PERF_TOOL_PMU_TYPE_TOOL_MASK;
+
+ pmu_read_sysfs(to_read_pmus);
+ pmu = list_prepare_entry(pmu, &core_pmus, list);
+ }
+ if (use_core_pmus) {
+ list_for_each_entry_continue(pmu, &core_pmus, list)
+ return pmu;
+
+ pmu = NULL;
+ pmu = list_prepare_entry(pmu, &other_pmus, list);
+ }
+ list_for_each_entry_continue(pmu, &other_pmus, list)
+ return pmu;
+ return NULL;
+}
+
struct perf_pmu *perf_pmus__scan_for_uncore_id(struct perf_pmu *pmu, const char *compat)
{
if (!pmu) {
@@ -805,17 +836,204 @@ bool perf_pmus__have_event(const char *pname, const char *name)
return pmu && perf_pmu__have_event(pmu, name);
}
-int perf_pmus__num_core_pmus(void)
+bool perf_pmus__find_metric_pmu(const char *pmu_name, const char *name,
+ struct pmu_metric *out)
+{
+ struct perf_pmu *pmu = NULL;
+
+ if (!name || !name[0])
+ return false;
+
+ if (pmu_name && pmu_name[0] && strcmp(pmu_name, "all") != 0) {
+ struct pmu_metric fallback = {};
+ bool have_fallback = false;
+
+ if (!strcmp(pmu_name, "default_core")) {
+ while ((pmu = perf_pmus__scan_core(pmu)) != NULL) {
+ struct pmu_metric pm;
+
+ if (!perf_pmu__find_metric(pmu, name, &pm))
+ continue;
+ if (!pm.pmu || !pm.pmu[0] ||
+ !strcmp(pm.pmu, "default_core")) {
+ if (out)
+ *out = pm;
+ return true;
+ }
+ if (!have_fallback) {
+ fallback = pm;
+ have_fallback = true;
+ }
+ }
+ if (have_fallback && out)
+ *out = fallback;
+ return have_fallback;
+ }
+
+ pmu = perf_pmus__find(pmu_name);
+ if (pmu)
+ return perf_pmu__find_metric(pmu, name, out);
+
+ while ((pmu = perf_pmus__scan_for_metrics(pmu)) != NULL) {
+ struct pmu_metric pm;
+ const char *m_pmu;
+
+ if (!perf_pmu__name_wildcard_match(pmu, pmu_name) &&
+ !perf_pmu__wildcard_match(pmu, pmu_name))
+ continue;
+ if (!perf_pmu__find_metric(pmu, name, &pm))
+ continue;
+ m_pmu = (pm.pmu && pm.pmu[0]) ? pm.pmu :
+ (pmu->is_core ? "default_core" : pmu->name);
+ if (!strcmp(m_pmu, pmu_name)) {
+ if (out)
+ *out = pm;
+ return true;
+ }
+ if (!have_fallback) {
+ fallback = pm;
+ have_fallback = true;
+ }
+ }
+ if (have_fallback && out)
+ *out = fallback;
+ return have_fallback;
+ }
+
+ while ((pmu = perf_pmus__scan_for_metrics(pmu)) != NULL) {
+ if (perf_pmu__find_metric(pmu, name, out))
+ return true;
+ }
+ return false;
+}
+
+bool perf_pmus__find_metric(const char *name, struct pmu_metric *out)
+{
+ return perf_pmus__find_metric_pmu(NULL, name, out);
+}
+
+struct pmus_pmu_metric_iter_ctx {
+ struct perf_pmu *pmu;
+ pmus_metric_iter_fn fn;
+ void *data;
+};
+
+static int pmus_for_each_pmu_metric_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table,
+ void *vdata)
+{
+ struct pmus_pmu_metric_iter_ctx *ctx = vdata;
+
+ return ctx->fn(ctx->pmu, pm, table, ctx->data);
+}
+
+int perf_pmus__for_each_pmu_metric(pmus_metric_iter_fn fn, void *data)
{
- static int count;
+ struct pmus_pmu_metric_iter_ctx ctx = {
+ .fn = fn,
+ .data = data,
+ };
+ int ret = 0;
+
+ while ((ctx.pmu = perf_pmus__scan_for_metrics(ctx.pmu)) != NULL) {
+ ret = perf_pmu__for_each_metric(ctx.pmu,
+ pmus_for_each_pmu_metric_cb,
+ &ctx);
+ if (ret)
+ break;
+ }
+ return ret;
+}
+
+struct pmus_metric_iter_adapter_ctx {
+ pmu_metric_iter_fn fn;
+ void *data;
+ struct strlist *seen;
+};
+
+static int pmus_metric_iter_adapter_cb(struct perf_pmu *pmu __maybe_unused,
+ const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table,
+ void *vdata)
+{
+ struct pmus_metric_iter_adapter_ctx *ctx = vdata;
+ char *key = NULL;
+ int ret;
+
+ if (asprintf(&key, "%s\x1f%s\x1f%s\x1f%s",
+ (pm->pmu && pm->pmu[0]) ? pm->pmu : "default_core",
+ pm->metric_name ?: "",
+ pm->metric_expr ?: "",
+ pm->metric_threshold ?: "") < 0)
+ return -ENOMEM;
+
+ if (strlist__has_entry(ctx->seen, key)) {
+ free(key);
+ return 0;
+ }
+ ret = strlist__add(ctx->seen, key);
+ free(key);
+ if (ret)
+ return ret;
+
+ return ctx->fn(pm, table, ctx->data);
+}
- if (!count) {
+int perf_pmus__for_each_metric(pmu_metric_iter_fn fn, void *data)
+{
+ struct pmus_metric_iter_adapter_ctx ctx = {
+ .fn = fn,
+ .data = data,
+ .seen = strlist__new(NULL, NULL),
+ };
+ int ret;
+
+ if (!ctx.seen)
+ return -ENOMEM;
+
+ ret = perf_pmus__for_each_pmu_metric(pmus_metric_iter_adapter_cb, &ctx);
+ strlist__delete(ctx.seen);
+ return ret;
+}
+
+const char *perf_pmus__describe_metricgroup(const char *group)
+{
+ struct perf_pmu *pmu = NULL;
+ const char *builtin_desc = NULL;
+ bool saw_core_pmu = false;
+
+ if (!group || !group[0])
+ return NULL;
+
+ while ((pmu = perf_pmus__scan_for_metrics(pmu)) != NULL) {
+ const char *desc = perf_pmu__describe_metricgroup(pmu, group);
+ struct perf_pmu_metricgroup *mg = NULL;
+
+ if (pmu->is_core)
+ saw_core_pmu = true;
+ if (!desc)
+ continue;
+ if (pmu->metricgroups)
+ hashmap__find(pmu->metricgroups, group, &mg);
+ if (mg && !mg->from_builtin)
+ return desc;
+ if (!builtin_desc)
+ builtin_desc = desc;
+ }
+ if (saw_core_pmu)
+ return builtin_desc;
+ return describe_metricgroup(group);
+}
+
+int perf_pmus__num_core_pmus(void)
+{
+ if (!core_pmus_count) {
struct perf_pmu *pmu = NULL;
while ((pmu = perf_pmus__scan_core(pmu)) != NULL)
- count++;
+ core_pmus_count++;
}
- return count;
+ return core_pmus_count;
}
static bool __perf_pmus__supports_extended_type(void)
@@ -893,12 +1111,19 @@ struct perf_pmu *perf_pmus__find_core_pmu(void)
struct perf_pmu *perf_pmus__add_test_pmu(int test_sysfs_dirfd, const char *name)
{
+ bool is_core = perf_pmu__is_core_at(test_sysfs_dirfd, name);
+ struct perf_pmu *pmu;
+
/*
* Some PMU functions read from the sysfs mount point, so care is
* needed, hence passing the eager_load flag to load things like the
* format files.
*/
- return perf_pmu__lookup(&other_pmus, test_sysfs_dirfd, name, /*eager_load=*/true);
+ pmu = perf_pmu__lookup(is_core ? &core_pmus : &other_pmus,
+ test_sysfs_dirfd, name, /*eager_load=*/true);
+ if (pmu && is_core)
+ core_pmus_count = 0;
+ return pmu;
}
struct perf_pmu *perf_pmus__add_test_hwmon_pmu(const char *hwmon_dir,
diff --git a/tools/perf/util/pmus.h b/tools/perf/util/pmus.h
index 0d55edb3f2fc..f4295526b2ae 100644
--- a/tools/perf/util/pmus.h
+++ b/tools/perf/util/pmus.h
@@ -4,6 +4,7 @@
#include <stdbool.h>
#include <stddef.h>
+#include "pmu-events/pmu-events.h"
struct perf_event_attr;
struct perf_pmu;
@@ -23,6 +24,7 @@ struct perf_pmu *perf_pmus__scan(struct perf_pmu *pmu);
struct perf_pmu *perf_pmus__scan_core(struct perf_pmu *pmu);
struct perf_pmu *perf_pmus__scan_for_event(struct perf_pmu *pmu, const char *event);
struct perf_pmu *perf_pmus__scan_matching_wildcard(struct perf_pmu *pmu, const char *wildcard);
+struct perf_pmu *perf_pmus__scan_for_metrics(struct perf_pmu *pmu);
struct perf_pmu *perf_pmus__scan_for_uncore_id(struct perf_pmu *pmu, const char *compat);
const struct perf_pmu *perf_pmus__pmu_for_pmu_filter(const char *str);
@@ -30,6 +32,17 @@ const struct perf_pmu *perf_pmus__pmu_for_pmu_filter(const char *str);
void perf_pmus__print_pmu_events(const struct print_callbacks *print_cb, void *print_state);
void perf_pmus__print_raw_pmu_events(const struct print_callbacks *print_cb, void *print_state);
bool perf_pmus__have_event(const char *pname, const char *name);
+typedef int (*pmus_metric_iter_fn)(struct perf_pmu *pmu,
+ const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table,
+ void *data);
+
+bool perf_pmus__find_metric_pmu(const char *pmu_name, const char *name,
+ struct pmu_metric *out);
+bool perf_pmus__find_metric(const char *name, struct pmu_metric *out);
+int perf_pmus__for_each_pmu_metric(pmus_metric_iter_fn fn, void *data);
+int perf_pmus__for_each_metric(pmu_metric_iter_fn fn, void *data);
+const char *perf_pmus__describe_metricgroup(const char *group);
int perf_pmus__num_core_pmus(void);
bool perf_pmus__supports_extended_type(void);
diff --git a/tools/perf/util/print-events.c b/tools/perf/util/print-events.c
index 77428d9e73c4..eb44026972b9 100644
--- a/tools/perf/util/print-events.c
+++ b/tools/perf/util/print-events.c
@@ -289,14 +289,14 @@ void metricgroup__print(const struct print_callbacks *print_cb, void *print_stat
{
struct rblist groups;
struct rb_node *node, *next;
- const struct pmu_metrics_table *table = pmu_metrics_table__find();
rblist__init(&groups);
groups.node_new = mep_new;
groups.node_cmp = mep_cmp;
groups.node_delete = mep_delete;
- metricgroup__for_each_metric(table, metricgroup__add_to_mep_groups_callback, &groups);
+ metricgroup__for_each_metric(/*table=*/NULL,
+ metricgroup__add_to_mep_groups_callback, &groups);
for (node = rb_first_cached(&groups.entries); node; node = next) {
struct mep *me = container_of(node, struct mep, nd);
diff --git a/tools/perf/util/python.c b/tools/perf/util/python.c
index 42c587cd54a5..9002288d24f4 100644
--- a/tools/perf/util/python.c
+++ b/tools/perf/util/python.c
@@ -3953,16 +3953,13 @@ static int pyrf__metrics_cb(const struct pmu_metric *pm,
static PyObject *pyrf__metrics(PyObject *self, PyObject *args)
{
- const struct pmu_metrics_table *table = pmu_metrics_table__find();
PyObject *list = PyList_New(/*len=*/0);
int ret;
if (!list)
return NULL;
- ret = pmu_metrics_table__for_each_metric(table, pyrf__metrics_cb, list);
- if (!ret)
- ret = pmu_for_each_sys_metric(pyrf__metrics_cb, list);
+ ret = metricgroup__for_each_metric(/*table=*/NULL, pyrf__metrics_cb, list);
if (ret) {
Py_DECREF(list);
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 08/11] perf list, jevents: Add --export-dir and --output-dir sysfs-style exporters
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (6 preceding siblings ...)
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
2026-11-13 21:11 ` [PATCH 09/11] perf jevents: Add compiled-in model and sys table iterators Ian Rogers
` (2 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:10 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
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
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 09/11] perf jevents: Add compiled-in model and sys table iterators
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (7 preceding siblings ...)
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 ` 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
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:11 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Record canonical model relative paths in struct pmu_events_map and
architecture/model paths in struct pmu_sys_events during jevents.py C
code generation, and expose pmu_events_map__for_each() and
pmu_sys_events__for_each() in pmu-events.h.
This keeps struct pmu_events_map and struct pmu_sys_events private to
pmu-events.c while allowing callers such as 'perf list --export-dir
--export-cpuid' to enumerate compiled-in CPU and system PMU tables
across all architectures without requiring an unbuilt JSON source tree.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/pmu-events/empty-pmu-events.c | 36 +++++++++++++++
tools/perf/pmu-events/jevents.py | 57 ++++++++++++++++++++++++
tools/perf/pmu-events/pmu-events.h | 15 +++++++
3 files changed, 108 insertions(+)
diff --git a/tools/perf/pmu-events/empty-pmu-events.c b/tools/perf/pmu-events/empty-pmu-events.c
index f4009e5c51ee..116ce8e39d5d 100644
--- a/tools/perf/pmu-events/empty-pmu-events.c
+++ b/tools/perf/pmu-events/empty-pmu-events.c
@@ -5419,6 +5419,7 @@ struct pmu_metrics_table {
struct pmu_events_map {
const char *arch;
const char *cpuid;
+ const char *model;
struct pmu_events_table event_table;
struct pmu_metrics_table metric_table;
};
@@ -5431,6 +5432,7 @@ static const struct pmu_events_map pmu_events_map[] = {
{
.arch = "common",
.cpuid = "common",
+ .model = "common",
.event_table = {
.pmus = pmu_events__common,
.num_pmus = ARRAY_SIZE(pmu_events__common),
@@ -5444,6 +5446,7 @@ static const struct pmu_events_map pmu_events_map[] = {
{
.arch = "testarch",
.cpuid = "testcpu",
+ .model = "test_soc/cpu",
.event_table = {
.pmus = pmu_events__test_soc_cpu,
.num_pmus = ARRAY_SIZE(pmu_events__test_soc_cpu),
@@ -5457,6 +5460,7 @@ static const struct pmu_events_map pmu_events_map[] = {
{
.arch = 0,
.cpuid = 0,
+ .model = 0,
.event_table = { 0, 0 },
.metric_table = { 0 },
}
@@ -5464,6 +5468,8 @@ static const struct pmu_events_map pmu_events_map[] = {
struct pmu_sys_events {
const char *name;
+ const char *arch;
+ const char *model;
struct pmu_events_table event_table;
struct pmu_metrics_table metric_table;
};
@@ -5475,6 +5481,8 @@ static const struct pmu_sys_events pmu_sys_event_tables[] = {
.num_pmus = ARRAY_SIZE(pmu_events__test_soc_sys)
},
.name = "pmu_events__test_soc_sys",
+ .arch = "testarch",
+ .model = "test_soc/sys",
},
{
.event_table = { 0, 0 },
@@ -6041,6 +6049,34 @@ int pmu_metrics_table__iterate_tables(pmu_metrics_table_iter_t fn, void *data)
return 0;
}
+int pmu_events_map__for_each(pmu_events_map_iter_fn fn, void *data)
+{
+ for (size_t i = 0; pmu_events_map[i].arch; i++) {
+ const struct pmu_events_map *m = &pmu_events_map[i];
+ const struct pmu_events_table *et = m->event_table.pmus ? &m->event_table : NULL;
+ const struct pmu_metrics_table *mt = m->metric_table.pmus ? &m->metric_table : NULL;
+ int ret = fn(m->arch, m->cpuid, m->model, et, mt, data);
+
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
+int pmu_sys_events__for_each(pmu_sys_events_iter_fn fn, void *data)
+{
+ for (size_t i = 0; pmu_sys_event_tables[i].name; i++) {
+ const struct pmu_sys_events *s = &pmu_sys_event_tables[i];
+ const struct pmu_events_table *et = s->event_table.pmus ? &s->event_table : NULL;
+ const struct pmu_metrics_table *mt = s->metric_table.pmus ? &s->metric_table : NULL;
+ int ret = fn(s->arch, s->model, et, mt, data);
+
+ if (ret)
+ return ret;
+ }
+ return 0;
+}
+
static const uint32_t metricgroups[][2] = {
{ 130139, 128811 }, /* group1\000 => Test metricgroup 1\000 */
{ 130146, 128830 }, /* group1A\000 => Test metricgroup 1A\000 */
diff --git a/tools/perf/pmu-events/jevents.py b/tools/perf/pmu-events/jevents.py
index 8c2f6531b0c5..972356ea84c3 100755
--- a/tools/perf/pmu-events/jevents.py
+++ b/tools/perf/pmu-events/jevents.py
@@ -25,6 +25,10 @@ _metric_tables: list[str] = []
_sys_metric_tables: list[str] = []
# Mapping between sys event table names and sys metric table names.
_sys_event_table_to_metric_table_mapping: Dict[str, str] = {}
+# Mapping from sys event/metric table names to (arch, model_rel_path).
+_sys_table_arch_model: Dict[str, Tuple[str, str]] = {}
+# Architecture directory currently being walked during code generation.
+_cur_arch: str = ''
# Map from an event name to an architecture standard
# JsonEvent. Architecture standard events are in json files in the top
# f'{_args.starting_dir}/{_args.arch}' directory.
@@ -1309,6 +1313,10 @@ def process_one_file(parents: Sequence[str], item: os.DirEntry) -> None:
if item.name == 'sys':
_sys_event_table_to_metric_table_mapping[_pending_events_tblname] = _pending_metrics_tblname
+ c_arch = 'testarch' if _cur_arch == 'test' else _cur_arch
+ model_rel = '/'.join(list(parents) + [item.name])
+ _sys_table_arch_model[_pending_events_tblname] = (c_arch, model_rel)
+ _sys_table_arch_model[_pending_metrics_tblname] = (c_arch, model_rel)
return
# base dir or too deep
@@ -1352,6 +1360,7 @@ struct pmu_metrics_table {
struct pmu_events_map {
\tconst char *arch;
\tconst char *cpuid;
+\tconst char *model;
\tstruct pmu_events_table event_table;
\tstruct pmu_metrics_table metric_table;
};
@@ -1367,6 +1376,7 @@ static const struct pmu_events_map pmu_events_map[] = {
_args.output_file.write("""{
\t.arch = "testarch",
\t.cpuid = "testcpu",
+\t.model = "test_soc/cpu",
\t.event_table = {
\t\t.pmus = pmu_events__test_soc_cpu,
\t\t.num_pmus = ARRAY_SIZE(pmu_events__test_soc_cpu),
@@ -1382,6 +1392,7 @@ static const struct pmu_events_map pmu_events_map[] = {
_args.output_file.write("""{
\t.arch = "common",
\t.cpuid = "common",
+\t.model = "common",
\t.event_table = {
\t\t.pmus = pmu_events__common,
\t\t.num_pmus = ARRAY_SIZE(pmu_events__common),
@@ -1400,6 +1411,7 @@ static const struct pmu_events_map pmu_events_map[] = {
for row in table:
# Skip the first row or any row beginning with #.
if not first and len(row) > 0 and not row[0].startswith('#'):
+ model_rel = row[2].strip('/')
event_tblname = file_name_to_table_name('pmu_events_', [], row[2].replace('/', '_'))
if event_tblname in _event_tables:
event_size = f'ARRAY_SIZE({event_tblname})'
@@ -1417,9 +1429,11 @@ static const struct pmu_events_map pmu_events_map[] = {
if event_size == '0' and metric_size == '0':
continue
cpuid = row[0].replace('\\', '\\\\')
+ model_c = bytes_to_c_literal(model_rel.encode('utf-8'))
_args.output_file.write(f"""{{
\t.arch = "{arch}",
\t.cpuid = "{cpuid}",
+\t.model = "{model_c}",
\t.event_table = {{
\t\t.pmus = {event_tblname},
\t\t.num_pmus = {event_size}
@@ -1436,6 +1450,7 @@ static const struct pmu_events_map pmu_events_map[] = {
_args.output_file.write("""{
\t.arch = 0,
\t.cpuid = 0,
+\t.model = 0,
\t.event_table = { 0, 0 },
\t.metric_table = { 0 },
}
@@ -1479,6 +1494,34 @@ int pmu_metrics_table__iterate_tables(pmu_metrics_table_iter_t fn, void *data)
\t\tif (!pmu_sys_event_tables[i].metric_table.pmus)
\t\t\tcontinue;
\t\tret = fn(&pmu_sys_event_tables[i].metric_table, data);
+\t\tif (ret)
+\t\t\treturn ret;
+\t}
+\treturn 0;
+}
+
+int pmu_events_map__for_each(pmu_events_map_iter_fn fn, void *data)
+{
+\tfor (size_t i = 0; pmu_events_map[i].arch; i++) {
+\t\tconst struct pmu_events_map *m = &pmu_events_map[i];
+\t\tconst struct pmu_events_table *et = m->event_table.pmus ? &m->event_table : NULL;
+\t\tconst struct pmu_metrics_table *mt = m->metric_table.pmus ? &m->metric_table : NULL;
+\t\tint ret = fn(m->arch, m->cpuid, m->model, et, mt, data);
+
+\t\tif (ret)
+\t\t\treturn ret;
+\t}
+\treturn 0;
+}
+
+int pmu_sys_events__for_each(pmu_sys_events_iter_fn fn, void *data)
+{
+\tfor (size_t i = 0; pmu_sys_event_tables[i].name; i++) {
+\t\tconst struct pmu_sys_events *s = &pmu_sys_event_tables[i];
+\t\tconst struct pmu_events_table *et = s->event_table.pmus ? &s->event_table : NULL;
+\t\tconst struct pmu_metrics_table *mt = s->metric_table.pmus ? &s->metric_table : NULL;
+\t\tint ret = fn(s->arch, s->model, et, mt, data);
+
\t\tif (ret)
\t\t\treturn ret;
\t}
@@ -1493,6 +1536,8 @@ def print_system_mapping_table() -> None:
_args.output_file.write("""
struct pmu_sys_events {
\tconst char *name;
+\tconst char *arch;
+\tconst char *model;
\tstruct pmu_events_table event_table;
\tstruct pmu_metrics_table metric_table;
};
@@ -1501,6 +1546,9 @@ static const struct pmu_sys_events pmu_sys_event_tables[] = {
""")
printed_metric_tables = []
for tblname in _sys_event_tables:
+ sys_arch, sys_model = _sys_table_arch_model.get(tblname, ('', ''))
+ sys_arch_c = bytes_to_c_literal(sys_arch.encode('utf-8'))
+ sys_model_c = bytes_to_c_literal(sys_model.encode('utf-8'))
_args.output_file.write(f"""\t{{
\t\t.event_table = {{
\t\t\t.pmus = {tblname},
@@ -1517,11 +1565,16 @@ static const struct pmu_sys_events pmu_sys_event_tables[] = {
printed_metric_tables.append(metric_tblname)
_args.output_file.write(f"""
\t\t.name = \"{tblname}\",
+\t\t.arch = \"{sys_arch_c}\",
+\t\t.model = \"{sys_model_c}\",
\t}},
""")
for tblname in _sys_metric_tables:
if tblname in printed_metric_tables:
continue
+ sys_arch, sys_model = _sys_table_arch_model.get(tblname, ('', ''))
+ sys_arch_c = bytes_to_c_literal(sys_arch.encode('utf-8'))
+ sys_model_c = bytes_to_c_literal(sys_model.encode('utf-8'))
_args.output_file.write(f"""\t{{
\t\t.metric_table = {{
\t\t\t.name = "{tblname.replace('pmu_metrics__', '')}",
@@ -1529,6 +1582,8 @@ static const struct pmu_sys_events pmu_sys_event_tables[] = {
\t\t\t.num_pmus = ARRAY_SIZE({tblname})
\t\t}},
\t\t.name = \"{tblname}\",
+\t\t.arch = \"{sys_arch_c}\",
+\t\t.model = \"{sys_model_c}\",
\t}},
""")
_args.output_file.write("""\t{
@@ -2568,7 +2623,9 @@ struct pmu_table_entry {
}
""")
_args.output_file.write('const char *pmu_events__get_string(uint32_t offset);\n\n')
+ global _cur_arch
for arch in archs:
+ _cur_arch = arch
arch_path = f'{_args.starting_dir}/{arch}'
ftw(arch_path, [], process_one_file)
print_pending_events()
diff --git a/tools/perf/pmu-events/pmu-events.h b/tools/perf/pmu-events/pmu-events.h
index cb55c9fbca43..77a58e238723 100644
--- a/tools/perf/pmu-events/pmu-events.h
+++ b/tools/perf/pmu-events/pmu-events.h
@@ -94,6 +94,19 @@ typedef int (*pmu_metric_iter_fn)(const struct pmu_metric *pm,
typedef int (*pmu_metrics_table_iter_t)(const struct pmu_metrics_table *table,
void *data);
+typedef int (*pmu_events_map_iter_fn)(const char *arch,
+ const char *cpuid,
+ const char *model,
+ const struct pmu_events_table *events_table,
+ const struct pmu_metrics_table *metrics_table,
+ void *data);
+
+typedef int (*pmu_sys_events_iter_fn)(const char *arch,
+ const char *model,
+ const struct pmu_events_table *events_table,
+ const struct pmu_metrics_table *metrics_table,
+ void *data);
+
int pmu_events_table__for_each_event(const struct pmu_events_table *table,
struct perf_pmu *pmu,
pmu_event_iter_fn fn,
@@ -143,6 +156,8 @@ const struct pmu_events_table *find_sys_events_table(const char *name);
const struct pmu_metrics_table *find_sys_metrics_table(const char *name);
int pmu_for_each_sys_event(pmu_event_iter_fn fn, void *data);
int pmu_for_each_sys_metric(pmu_metric_iter_fn fn, void *data);
+int pmu_events_map__for_each(pmu_events_map_iter_fn fn, void *data);
+int pmu_sys_events__for_each(pmu_sys_events_iter_fn fn, void *data);
const char *describe_metricgroup(const char *group);
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 10/11] perf list: Add --export-cpuid to export compiled-in CPU models
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (8 preceding siblings ...)
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
2026-11-13 21:11 ` [PATCH 11/11] perf tests: Add round-trip export, CPUID symlink, and pmu-events.dir overlay tests Ian Rogers
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:11 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
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
^ permalink raw reply [flat|nested] 12+ messages in thread* [PATCH 11/11] perf tests: Add round-trip export, CPUID symlink, and pmu-events.dir overlay tests
2026-11-13 21:10 [PATCH 00/11] perf pmu: Layered sysfs-style PMU event and metric directories Ian Rogers
` (9 preceding siblings ...)
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 ` Ian Rogers
10 siblings, 0 replies; 12+ messages in thread
From: Ian Rogers @ 2026-11-13 21:11 UTC (permalink / raw)
To: Peter Zijlstra, Ingo Molnar, Arnaldo Carvalho de Melo, Namhyung Kim
Cc: Jiri Olsa, Adrian Hunter, James Clark, linux-perf-users,
linux-kernel, Ian Rogers
Add C unit and CLI shell tests exercising the layered PMU directory VFS
stack and sysfs-style export tools through public pmu, pmus, and
parse-events APIs (keeping struct perf_pmu_alias and struct
perf_pmu_metric encapsulated):
- In tools/perf/tests/pmu.c (Layered PMU directory overlays):
Test colon-separated pmu-events.dir overlays via perfconfig, base
(cpus/GenuineIntel-6-99 -> ../models/x86/mock_arch) and
stepping-specific (cpus/GenuineIntel-6-99-4 ->
../models/x86/mock_arch_step4) CPUID POSIX symlinks via PERF_CPUID
without mapfile.csv, single-file attribute overrides preserving
lower-layer sysfs event terms, multi-directory priority overrides
across events, metrics, and metricgroups, extended event and metric
sidecars, %2F filename encoding, and heterogeneous multi-instance PMU
sub-metric resolution.
- In tools/perf/tests/shell/pmu_events_dir.sh:
Test lossless perf list --export-dir JSON round-tripping via
PERF_CONFIG, custom user overlay events and metrics executed with perf
stat -e / -M, file removal restoring lower-layer defaults,
perf list --export-cpuid (architecture, model, and stepping CPUID
export, metric filtering, and error handling), and jevents.py
--output-dir literal CPUID and uncore symlink generation.
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/tests/pmu.c | 959 +++++++++++++++++++++++
tools/perf/tests/shell/pmu_events_dir.sh | 236 ++++++
2 files changed, 1195 insertions(+)
create mode 100755 tools/perf/tests/shell/pmu_events_dir.sh
diff --git a/tools/perf/tests/pmu.c b/tools/perf/tests/pmu.c
index f208932590df..fc2a92dbb2b3 100644
--- a/tools/perf/tests/pmu.c
+++ b/tools/perf/tests/pmu.c
@@ -1,9 +1,13 @@
// SPDX-License-Identifier: GPL-2.0
+#include "builtin.h"
+#include "config.h"
#include "evlist.h"
#include "evsel.h"
+#include "metricgroup.h"
#include "parse-events.h"
#include "pmu.h"
#include "pmus.h"
+#include "pmu-events/pmu-events.h"
#include "tests.h"
#include "debug.h"
#include "fncache.h"
@@ -12,8 +16,10 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
+#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
+#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
@@ -628,6 +634,958 @@ static int test__pmu_match(struct test_suite *test __maybe_unused, int subtest _
return TEST_OK;
}
+static int overlay_mkdir_p(const char *path)
+{
+ char tmp[PATH_MAX];
+ size_t len = strlen(path);
+
+ if (len == 0 || len >= sizeof(tmp))
+ return -ENAMETOOLONG;
+
+ memcpy(tmp, path, len + 1);
+ 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 && errno != EEXIST)
+ return -errno;
+ return 0;
+}
+
+static int overlay_write_file(const char *root, const char *relpath,
+ const char *content)
+{
+ char full[PATH_MAX];
+ char *slash;
+ size_t len = strlen(content);
+ int fd;
+
+ if (snprintf(full, sizeof(full), "%s/%s", root, relpath) >= (int)sizeof(full))
+ return -ENAMETOOLONG;
+
+ slash = strrchr(full, '/');
+ if (slash) {
+ *slash = '\0';
+ if (overlay_mkdir_p(full) < 0)
+ return -errno;
+ *slash = '/';
+ }
+
+ fd = open(full, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0)
+ return -errno;
+
+ if (len > 0 && write(fd, content, len) != (ssize_t)len) {
+ int err = errno ? -errno : -EIO;
+
+ close(fd);
+ return err;
+ }
+ close(fd);
+ return 0;
+}
+
+static int overlay_symlink(const char *root, const char *relpath,
+ const char *target)
+{
+ char full[PATH_MAX];
+ char *slash;
+
+ if (snprintf(full, sizeof(full), "%s/%s", root, relpath) >= (int)sizeof(full))
+ return -ENAMETOOLONG;
+
+ slash = strrchr(full, '/');
+ if (slash) {
+ *slash = '\0';
+ if (overlay_mkdir_p(full) < 0)
+ return -errno;
+ *slash = '/';
+ }
+ if (symlink(target, full) < 0)
+ return -errno;
+ return 0;
+}
+
+static void overlay_rm_rf_at(int parent_fd, const char *name)
+{
+ struct stat st;
+
+ if (fstatat(parent_fd, name, &st, AT_SYMLINK_NOFOLLOW) < 0)
+ return;
+
+ if (S_ISDIR(st.st_mode)) {
+ int fd = openat(parent_fd, name,
+ O_RDONLY | O_DIRECTORY | O_NOFOLLOW);
+
+ if (fd >= 0) {
+ DIR *dir = fdopendir(fd);
+
+ if (dir) {
+ struct dirent *dent;
+
+ while ((dent = readdir(dir)) != NULL) {
+ if (!strcmp(dent->d_name, ".") ||
+ !strcmp(dent->d_name, ".."))
+ continue;
+ overlay_rm_rf_at(fd, dent->d_name);
+ }
+ closedir(dir);
+ } else {
+ close(fd);
+ }
+ }
+ unlinkat(parent_fd, name, AT_REMOVEDIR);
+ return;
+ }
+
+ unlinkat(parent_fd, name, 0);
+}
+
+static bool overlay_expect_file(const char *root, const char *relpath,
+ const char *expected)
+{
+ char full[PATH_MAX];
+ char buf[256];
+ ssize_t n;
+ int fd;
+
+ if (snprintf(full, sizeof(full), "%s/%s", root, relpath) >= (int)sizeof(full))
+ return false;
+
+ fd = open(full, O_RDONLY);
+ if (fd < 0)
+ return false;
+
+ n = read(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return false;
+
+ buf[n] = '\0';
+ return strcmp(buf, expected) == 0;
+}
+
+struct overlay_event_snapshot {
+ const char *target;
+ bool found;
+ char str[128];
+ char desc[128];
+ char topic[64];
+ char pmu_name[64];
+};
+
+static int snapshot_event_cb(void *state, struct pmu_event_info *info)
+{
+ struct overlay_event_snapshot *snap = state;
+ bool match = info->name && !strcmp(info->name, snap->target);
+
+ if (!match && info->name) {
+ const char *slash = strchr(info->name, '/');
+ const char *end = slash ? strrchr(slash + 1, '/') : NULL;
+ size_t tlen = strlen(snap->target);
+
+ if (slash && end && (size_t)(end - (slash + 1)) == tlen &&
+ !strncmp(slash + 1, snap->target, tlen))
+ match = true;
+ }
+ if (!match)
+ return 0;
+
+ snap->found = true;
+ snprintf(snap->str, sizeof(snap->str), "%s", info->str ?: "");
+ snprintf(snap->desc, sizeof(snap->desc), "%s", info->desc ?: "");
+ snprintf(snap->topic, sizeof(snap->topic), "%s", info->topic ?: "");
+ snprintf(snap->pmu_name, sizeof(snap->pmu_name), "%s", info->pmu_name ?: "");
+ return 0;
+}
+
+static bool overlay_find_event(struct perf_pmu *pmu, const char *event,
+ struct overlay_event_snapshot *snap)
+{
+ memset(snap, 0, sizeof(*snap));
+ snap->target = event;
+ if (perf_pmu__for_each_event(pmu, /*skip_duplicate_pmus=*/false,
+ snap, snapshot_event_cb))
+ return false;
+ return snap->found;
+}
+
+static int count_metrics_cb(const struct pmu_metric *pm __maybe_unused,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *data)
+{
+ int *cnt = data;
+
+ (*cnt)++;
+ return 0;
+}
+
+static int run_pmu_layer_overlay_checks(const char *overlay_root,
+ char *config_path, size_t config_path_sz,
+ char *sysfs_dir, size_t sysfs_dir_sz,
+ struct perf_pmu **pmu_out)
+{
+ char config_body[PATH_MAX * 2];
+ struct perf_pmu *pmu;
+ struct overlay_event_snapshot ev_snap;
+ struct pmu_metric metric;
+ struct evlist *evlist;
+ struct evsel *first_ev;
+ struct parse_events_error err;
+ const char *mg_desc;
+ double parsed_scale = 0.0, parsed_retire_lat = 0.0;
+ char parsed_unit[32] = "";
+ int parse_ret, metric_cnt = 0;
+ u64 parsed_config;
+
+ /*
+ * Populate dir2 (lower-priority overlay) using a canonical model tree
+ * linked via a base-CPUID POSIX symlink (no mapfile.csv or regexes).
+ */
+ TEST_ASSERT_VAL("write base-cpuid-event",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/base-cpuid-event",
+ "krava01=1\n") == 0);
+ TEST_ASSERT_VAL("write base-cpuid-event.desc",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/base-cpuid-event.desc",
+ "From base CPUID model\n") == 0);
+ TEST_ASSERT_VAL("write base-cpuid-event.topic",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/base-cpuid-event.topic",
+ "custom\n") == 0);
+ TEST_ASSERT_VAL("write base-cpuid-event.scale-unit",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/base-cpuid-event.scale-unit",
+ "64Bytes\n") == 0);
+ TEST_ASSERT_VAL("write base-cpuid-event.retire-lat-mean",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/base-cpuid-event.retire-lat-mean",
+ "12.5\n") == 0);
+ TEST_ASSERT_VAL("write dir2 overridden-event",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/overridden-event",
+ "krava01=2\n") == 0);
+ TEST_ASSERT_VAL("write slash%2Fevent",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/slash%2Fevent",
+ "krava01=3\n") == 0);
+ TEST_ASSERT_VAL("write slash%2Fevent.desc",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/events/slash%2Fevent.desc",
+ "Encoded slash event\n") == 0);
+
+ TEST_ASSERT_VAL("write base_metric",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric",
+ "test-event * 2\n") == 0);
+ TEST_ASSERT_VAL("write base_metric.scale-unit",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric.scale-unit",
+ "100%\n") == 0);
+ TEST_ASSERT_VAL("write base_metric.desc",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric.desc",
+ "Base metric desc\n") == 0);
+ TEST_ASSERT_VAL("write base_metric.group",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric.group",
+ "TestOverlayGroup\n") == 0);
+ TEST_ASSERT_VAL("write base_metric.aggr-mode",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric.aggr-mode",
+ "PerCore\n") == 0);
+ TEST_ASSERT_VAL("write base_metric.constraint",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/base_metric.constraint",
+ "NO_GROUP_EVENTS\n") == 0);
+ TEST_ASSERT_VAL("write dir2 overridden_metric",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/perf-pmu-test/metrics/overridden_metric",
+ "test-event + 1\n") == 0);
+
+ TEST_ASSERT_VAL("write dir2 TestOverlayGroup.desc",
+ overlay_write_file(overlay_root,
+ "dir2/models/testmodel/metricgroups/TestOverlayGroup.desc",
+ "Dir2 group description\n") == 0);
+
+ TEST_ASSERT_VAL("symlink base CPUID",
+ overlay_symlink(overlay_root,
+ "dir2/cpus/GenuineTest-6-AA",
+ "../models/testmodel") == 0);
+
+ /*
+ * Populate dir1 (higher-priority overlay) with base and single-attribute
+ * overrides.
+ */
+ TEST_ASSERT_VAL("write dir1 test-event.desc override",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/test-event.desc",
+ "Overridden sysfs description\n") == 0);
+ TEST_ASSERT_VAL("write dir1 test-event.legacy-terms fallback",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/test-event.legacy-terms",
+ "krava01=9\n") == 0);
+ TEST_ASSERT_VAL("write dir1 legacy-only-event.legacy-terms",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/legacy-only-event.legacy-terms",
+ "krava01=7\n") == 0);
+ TEST_ASSERT_VAL("write dir1 malformed per-pkg boolean sidecar",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/base-cpuid-event.per-pkg",
+ "garbage\n") == 0);
+ TEST_ASSERT_VAL("write dir1 default_core bpu-refs.legacy-terms override",
+ overlay_write_file(overlay_root,
+ "dir1/default_core/events/bpu-refs.legacy-terms",
+ "legacy-cache-config=1\n") == 0);
+ TEST_ASSERT_VAL("write dir1 .hidden dotfile",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/.hidden",
+ "krava01=6\n") == 0);
+ TEST_ASSERT_VAL("write dir1 overridden-event over dir2",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/events/overridden-event",
+ "krava01=5\n") == 0);
+ TEST_ASSERT_VAL("write dir1 base_metric.threshold override",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/metrics/base_metric.threshold",
+ "base_metric > 10\n") == 0);
+ TEST_ASSERT_VAL("write dir1 malformed show-events boolean sidecar",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/metrics/base_metric.show-events",
+ "garbage\n") == 0);
+ TEST_ASSERT_VAL("write dir1 overridden_metric over dir2",
+ overlay_write_file(overlay_root,
+ "dir1/perf-pmu-test/metrics/overridden_metric",
+ "test-event * 4\n") == 0);
+ TEST_ASSERT_VAL("write dir1 TestOverlayGroup.desc override",
+ overlay_write_file(overlay_root,
+ "dir1/metricgroups/TestOverlayGroup.desc",
+ "Dir1 winning group description\n") == 0);
+ TEST_ASSERT_VAL("write dir1 tool TopdownL1.desc override",
+ overlay_write_file(overlay_root,
+ "dir1/tool/metricgroups/TopdownL1.desc",
+ "Overridden TopdownL1 on tool PMU\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v1 het_sub",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v1/het_pmu/metrics/het_sub",
+ "duration_time\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v1 het_parent",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v1/het_pmu/metrics/het_parent",
+ "het_sub + 1\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v2 het_sub",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v2/het_pmu/metrics/het_sub",
+ "user_time\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v2 het_parent",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v2/het_pmu/metrics/het_parent",
+ "het_sub + 1\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v2 het_soc2_only",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v2/het_pmu/metrics/het_soc2_only",
+ "user_time + 2\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v1 het_filt_m",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v1/het_pmu/metrics/het_filt_m",
+ "het_pmu@het_ev_0@ + 1\n") == 0);
+ TEST_ASSERT_VAL("write dir1 soc_v2 het_filt_m",
+ overlay_write_file(overlay_root,
+ "dir1/sys/soc_v2/het_pmu/metrics/het_filt_m",
+ "het_pmu@het_ev_1@ + 1\n") == 0);
+ TEST_ASSERT_VAL("write dir1 het_pmu_0 het_coexist_m",
+ overlay_write_file(overlay_root,
+ "dir1/het_pmu_0/metrics/het_coexist_m",
+ "duration_time + 10\n") == 0);
+ TEST_ASSERT_VAL("write dir2 het_pmu het_coexist_m",
+ overlay_write_file(overlay_root,
+ "dir2/het_pmu/metrics/het_coexist_m",
+ "duration_time + 20\n") == 0);
+ TEST_ASSERT_VAL("write dir1 het_pmu_0 het_coexist_ev",
+ overlay_write_file(overlay_root,
+ "dir1/het_pmu_0/events/het_coexist_ev",
+ "config=0x31\n") == 0);
+ TEST_ASSERT_VAL("write dir2 het_pmu het_coexist_ev",
+ overlay_write_file(overlay_root,
+ "dir2/het_pmu/events/het_coexist_ev",
+ "config=0x32\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_0 type",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_0/type", "9997\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_0 cpumask",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_0/cpumask", "0\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_0 format config",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_0/format/config", "config:0-63\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_0 het_ev_0",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_0/events/het_ev_0", "config=0x1\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_1 type",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_1/type", "9998\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_1 cpumask",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_1/cpumask", "0\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_1 format config",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_1/format/config", "config:0-63\n") == 0);
+ TEST_ASSERT_VAL("write het_pmu_1 het_ev_1",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_pmu_1/events/het_ev_1", "config=0x2\n") == 0);
+ TEST_ASSERT_VAL("write dir1 aa_het_core_0 het_coexist_m",
+ overlay_write_file(overlay_root,
+ "dir1/aa_het_core_0/metrics/het_coexist_m",
+ "duration_time + 30\n") == 0);
+ TEST_ASSERT_VAL("write dir2 default_core het_coexist_m",
+ overlay_write_file(overlay_root,
+ "dir2/default_core/metrics/het_coexist_m",
+ "duration_time + 40\n") == 0);
+ TEST_ASSERT_VAL("write aa_het_core_0 type",
+ overlay_write_file(overlay_root,
+ "het_sysfs/aa_het_core_0/type", "9994\n") == 0);
+ TEST_ASSERT_VAL("write aa_het_core_0 cpus",
+ overlay_write_file(overlay_root,
+ "het_sysfs/aa_het_core_0/cpus", "0\n") == 0);
+ TEST_ASSERT_VAL("write dir1 default_core het_core_m.desc",
+ overlay_write_file(overlay_root,
+ "dir1/default_core/metrics/het_core_m.desc",
+ "Heterogeneous core metric\n") == 0);
+ TEST_ASSERT_VAL("write dir2 het_core_0 het_core_m",
+ overlay_write_file(overlay_root,
+ "dir2/het_core_0/metrics/het_core_m",
+ "het_core_ev + 1\n") == 0);
+ TEST_ASSERT_VAL("write dir2 het_core_1 het_core_m",
+ overlay_write_file(overlay_root,
+ "dir2/het_core_1/metrics/het_core_m",
+ "het_core_ev + 2\n") == 0);
+ TEST_ASSERT_VAL("write dir1 default_core het_core0_only_m.desc",
+ overlay_write_file(overlay_root,
+ "dir1/default_core/metrics/het_core0_only_m.desc",
+ "Single core PMU metric\n") == 0);
+ TEST_ASSERT_VAL("write dir2 het_core_0 het_core0_only_m",
+ overlay_write_file(overlay_root,
+ "dir2/het_core_0/metrics/het_core0_only_m",
+ "het_core_ev + 3\n") == 0);
+ TEST_ASSERT_VAL("write het_core_0 type",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_0/type", "9995\n") == 0);
+ TEST_ASSERT_VAL("write het_core_0 cpus",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_0/cpus", "0\n") == 0);
+ TEST_ASSERT_VAL("write het_core_0 het_core_ev",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_0/events/het_core_ev", "config=0x11\n") == 0);
+ TEST_ASSERT_VAL("write het_core_1 type",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_1/type", "9996\n") == 0);
+ TEST_ASSERT_VAL("write het_core_1 cpus",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_1/cpus", "0\n") == 0);
+ TEST_ASSERT_VAL("write het_core_1 het_core_ev",
+ overlay_write_file(overlay_root,
+ "het_sysfs/het_core_1/events/het_core_ev", "config=0x22\n") == 0);
+
+ TEST_ASSERT_VAL("format perfconfig",
+ snprintf(config_body, sizeof(config_body),
+ "[pmu-events]\n\tdir = %s/dir1:%s/dir2\n",
+ overlay_root, overlay_root) < (int)sizeof(config_body));
+ TEST_ASSERT_VAL("write perfconfig",
+ overlay_write_file(overlay_root, "perfconfig", config_body) == 0);
+ TEST_ASSERT_VAL("format config_path",
+ snprintf(config_path, config_path_sz, "%s/perfconfig",
+ overlay_root) < (int)config_path_sz);
+
+ config_exclusive_filename = config_path;
+ TEST_ASSERT_VAL("setenv PERF_CONFIG",
+ setenv("PERF_CONFIG", config_path, 1) == 0);
+ TEST_ASSERT_VAL("setenv PERF_CPUID",
+ setenv("PERF_CPUID", "GenuineTest-6-AA-1", 1) == 0);
+ perf_config__exit();
+ perf_pmus__destroy();
+
+ pmu = test_pmu_get(sysfs_dir, sysfs_dir_sz);
+ *pmu_out = pmu;
+ TEST_ASSERT_VAL("test_pmu_get", pmu != NULL);
+
+ /*
+ * 1-3. Exercise lazy single-event lookup (before perf_pmu__for_each_event()
+ * enumerates all events), then verify full enumeration metadata.
+ */
+ TEST_ASSERT_VAL("reject lowercase event sidecar lookup",
+ !perf_pmu__have_event(pmu, "test-event.desc"));
+ TEST_ASSERT_VAL("reject uppercase event sidecar lookup",
+ !perf_pmu__have_event(pmu, "TEST-EVENT.DESC"));
+ TEST_ASSERT_VAL("reject dotfile event lookup",
+ !perf_pmu__have_event(pmu, ".hidden"));
+ TEST_ASSERT_VAL("have test-event lazily",
+ perf_pmu__have_event(pmu, "test-event"));
+
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new", evlist != NULL);
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, "perf-pmu-test/test-event/", &err);
+ parse_events_error__exit(&err);
+ parsed_config = parse_ret == 0 ? evlist__first(evlist)->core.attr.config : 0;
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse test-event", parse_ret, 0);
+ TEST_ASSERT_VAL("test-event sysfs config preserved",
+ parsed_config == 0xc00000000002a823);
+
+ /* 2. Base CPUID symlink + extended sidecars + %2F decoding. */
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new base-cpuid-event", evlist != NULL);
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, "perf-pmu-test/base-cpuid-event/", &err);
+ parse_events_error__exit(&err);
+ if (parse_ret == 0) {
+ first_ev = evlist__first(evlist);
+ parsed_config = first_ev->core.attr.config;
+ parsed_scale = first_ev->scale;
+ snprintf(parsed_unit, sizeof(parsed_unit), "%s", first_ev->unit ?: "");
+ parsed_retire_lat = first_ev->retirement_latency.mean;
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse base-cpuid-event via base CPUID symlink", parse_ret, 0);
+ TEST_ASSERT_VAL("base-cpuid-event config", parsed_config == 1);
+ TEST_ASSERT_VAL("base-cpuid-event scale", parsed_scale == 64.0);
+ TEST_ASSERT_VAL("base-cpuid-event unit", !strcmp(parsed_unit, "Bytes"));
+ TEST_ASSERT_VAL("base-cpuid-event retire_lat_mean", parsed_retire_lat == 12.5);
+
+ TEST_ASSERT_VAL("have %2F-encoded slash/event lazily",
+ perf_pmu__have_event(pmu, "slash/event"));
+
+ /* 3. Multi-directory priority overrides, legacy promotion, and full scan. */
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new overridden-event", evlist != NULL);
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, "perf-pmu-test/overridden-event/", &err);
+ parse_events_error__exit(&err);
+ parsed_config = parse_ret == 0 ? evlist__first(evlist)->core.attr.config : 0;
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse overridden-event", parse_ret, 0);
+ TEST_ASSERT_VAL("dir1 event beats dir2 event",
+ parsed_config == ((1ULL << 62) | 1ULL));
+
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new legacy-only-event", evlist != NULL);
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, "perf-pmu-test/legacy-only-event/", &err);
+ parse_events_error__exit(&err);
+ parsed_config = parse_ret == 0 ? evlist__first(evlist)->core.attr.config : 0;
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse legacy-only-event", parse_ret, 0);
+ TEST_ASSERT_VAL("legacy-terms promoted when no base event exists",
+ parsed_config == ((1ULL << 62) | 3ULL));
+
+ TEST_ASSERT_VAL("full event scan succeeds despite malformed boolean sidecar",
+ perf_pmu__num_events(pmu) >= 4);
+
+ TEST_ASSERT_VAL("find test-event info",
+ overlay_find_event(pmu, "test-event", &ev_snap) &&
+ !strcmp(ev_snap.desc, "Overridden sysfs description"));
+ TEST_ASSERT_VAL("find base-cpuid-event info",
+ overlay_find_event(pmu, "base-cpuid-event", &ev_snap) &&
+ !strcmp(ev_snap.str, "krava01=1") &&
+ !strcmp(ev_snap.desc, "From base CPUID model") &&
+ !strcmp(ev_snap.topic, "custom"));
+ TEST_ASSERT_VAL("find slash/event info",
+ overlay_find_event(pmu, "slash/event", &ev_snap) &&
+ !strcmp(ev_snap.str, "krava01=3") &&
+ !strcmp(ev_snap.desc, "Encoded slash event"));
+ TEST_ASSERT_VAL("find overridden-event info",
+ overlay_find_event(pmu, "overridden-event", &ev_snap) &&
+ !strcmp(ev_snap.str, "krava01=5"));
+ TEST_ASSERT_VAL("legacy-only-event str preserved after load_events",
+ overlay_find_event(pmu, "legacy-only-event", &ev_snap) &&
+ !strcmp(ev_snap.str, "krava01=7"));
+
+ if (perf_pmus__scan_core(NULL) != NULL) {
+ struct perf_pmu *core_pmu = perf_pmus__scan_core(NULL);
+ u32 bpu_type = 0;
+
+ TEST_ASSERT_VAL("have core bpu-refs lazily",
+ perf_pmu__have_event(core_pmu, "bpu-refs"));
+ (void)perf_pmu__num_events(core_pmu);
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new bpu-refs", evlist != NULL);
+ parse_events_error__init(&err);
+ parse_ret = parse_events(evlist, "bpu-refs", &err);
+ parse_events_error__exit(&err);
+ if (parse_ret == 0) {
+ first_ev = evlist__first(evlist);
+ bpu_type = first_ev->core.attr.type;
+ parsed_config = first_ev->core.attr.config & 0xffffffULL;
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse overridden bpu-refs after load_events",
+ parse_ret, 0);
+ TEST_ASSERT_VAL("user .legacy-terms beats builtin common table",
+ bpu_type == PERF_TYPE_HW_CACHE && parsed_config == 1);
+ }
+
+ /* 4. Metric merge and priority across dir1 + dir2. */
+ memset(&metric, 0, sizeof(metric));
+ TEST_ASSERT_VAL("find base_metric",
+ perf_pmu__find_metric(pmu, "base_metric", &metric));
+ TEST_ASSERT_VAL("base_metric expr",
+ metric.metric_expr && !strcmp(metric.metric_expr, "test-event * 2"));
+ TEST_ASSERT_VAL("base_metric threshold from dir1",
+ metric.metric_threshold &&
+ !strcmp(metric.metric_threshold, "base_metric > 10"));
+ TEST_ASSERT_VAL("base_metric scale-unit",
+ metric.unit && !strcmp(metric.unit, "100%"));
+ TEST_ASSERT_VAL("base_metric desc",
+ metric.desc && !strcmp(metric.desc, "Base metric desc"));
+ TEST_ASSERT_VAL("base_metric group",
+ metric.metric_group && !strcmp(metric.metric_group, "TestOverlayGroup"));
+ TEST_ASSERT_VAL("base_metric aggr_mode", metric.aggr_mode == PerCore);
+ TEST_ASSERT_VAL("base_metric constraint",
+ metric.event_grouping == MetricNoGroupEvents);
+ TEST_ASSERT_VAL("perf_pmus__find_metric base_metric",
+ perf_pmus__find_metric("base_metric", &metric));
+ TEST_ASSERT_VAL("default_core metric lookup does not match non-core PMU",
+ !perf_pmus__find_metric_pmu("default_core", "base_metric", NULL));
+ TEST_ASSERT_VAL("full metric scan succeeds despite malformed boolean sidecar",
+ perf_pmu__for_each_metric(pmu, count_metrics_cb, &metric_cnt) == 0 &&
+ metric_cnt == 2);
+ memset(&metric, 0, sizeof(metric));
+ TEST_ASSERT_VAL("dir1 metric beats dir2 metric",
+ perf_pmu__find_metric(pmu, "overridden_metric", &metric) &&
+ metric.metric_expr && !strcmp(metric.metric_expr, "test-event * 4"));
+ TEST_ASSERT_VAL("reject lowercase metric sidecar lookup",
+ !perf_pmu__find_metric(pmu, "base_metric.desc", &metric));
+ TEST_ASSERT_VAL("reject uppercase metric sidecar lookup",
+ !perf_pmu__find_metric(pmu, "base_metric.DESC", &metric));
+
+ /* 5. Metricgroup description precedence. */
+ mg_desc = perf_pmu__describe_metricgroup(pmu, "TestOverlayGroup");
+ TEST_ASSERT_VAL("TestOverlayGroup dir1 precedence",
+ mg_desc && !strcmp(mg_desc, "Dir1 winning group description"));
+ mg_desc = perf_pmus__describe_metricgroup("TestOverlayGroup");
+ TEST_ASSERT_VAL("perf_pmus__describe_metricgroup TestOverlayGroup",
+ mg_desc && !strcmp(mg_desc, "Dir1 winning group description"));
+ mg_desc = perf_pmus__describe_metricgroup("TopdownL1");
+ TEST_ASSERT_VAL("per-PMU user TopdownL1.desc beats compiled-in",
+ mg_desc && !strcmp(mg_desc, "Overridden TopdownL1 on tool PMU"));
+
+ /*
+ * 5b. Sub-metric resolution on heterogeneous/multi-instance PMUs sharing
+ * the same pm->pmu prefix resolves against each originating struct perf_pmu.
+ */
+ {
+ char het_sysfs_path[PATH_MAX];
+ char export_coexist_dir[PATH_MAX];
+ const char *export_argv[5];
+ struct perf_pmu *het0 = NULL, *het1 = NULL;
+ struct perf_pmu *aa_c0 = NULL, *het_c0 = NULL, *het_c1 = NULL;
+ struct pmu_metric found_m;
+ struct evsel *pos;
+ bool saw_duration = false, saw_user = false;
+ bool filt_ev0 = false, filt_ev1 = false;
+ bool saw_core0_expr = false, saw_core1_expr = false;
+ int nr_core0 = 0, nr_core1 = 0;
+ int only_core0 = 0, only_core1 = 0;
+ int het_fd;
+
+ TEST_ASSERT_VAL("perf_pmu__is_core_at matches cpu name without dirfd",
+ perf_pmu__is_core_at(-1, "cpu") &&
+ !perf_pmu__is_core_at(-1, "het_pmu_0"));
+ TEST_ASSERT_VAL("format het_sysfs_path",
+ snprintf(het_sysfs_path, sizeof(het_sysfs_path),
+ "%s/het_sysfs", overlay_root) < (int)sizeof(het_sysfs_path));
+ het_fd = open(het_sysfs_path, O_RDONLY | O_DIRECTORY);
+ TEST_ASSERT_VAL("open het_sysfs", het_fd >= 0);
+ het0 = perf_pmus__add_test_pmu(het_fd, "het_pmu_0");
+ het1 = perf_pmus__add_test_pmu(het_fd, "het_pmu_1");
+ aa_c0 = perf_pmus__add_test_pmu(het_fd, "aa_het_core_0");
+ het_c0 = perf_pmus__add_test_pmu(het_fd, "het_core_0");
+ het_c1 = perf_pmus__add_test_pmu(het_fd, "het_core_1");
+ close(het_fd);
+ TEST_ASSERT_VAL("add het_pmu_0 and het_pmu_1", het0 && het1);
+ TEST_ASSERT_VAL("add aa_het_core_0, het_core_0 and het_core_1",
+ aa_c0 && het_c0 && het_c1 &&
+ aa_c0->is_core && het_c0->is_core && het_c1->is_core);
+ het0->is_uncore = true;
+ free((char *)het0->id);
+ het0->id = strdup("soc_v1");
+ het1->is_uncore = true;
+ free((char *)het1->id);
+ het1->id = strdup("soc_v2");
+ TEST_ASSERT_VAL("set het_pmu ids", het0->id && het1->id);
+
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new het_parent", evlist != NULL);
+ parse_ret = metricgroup__parse_groups(evlist, "all",
+ /*cputype_filter=*/false,
+ "het_parent",
+ /*metric_no_group=*/true,
+ /*metric_no_merge=*/false,
+ /*metric_no_threshold=*/false,
+ /*user_requested_cpu_list=*/NULL,
+ /*system_wide=*/false,
+ /*hardware_aware_grouping=*/false);
+ evlist__for_each_entry(evlist, pos) {
+ const char *ev_name = evsel__name(pos);
+
+ if (ev_name && strstr(ev_name, "duration_time"))
+ saw_duration = true;
+ if (ev_name && strstr(ev_name, "user_time"))
+ saw_user = true;
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse het_parent across het_pmu_0 and het_pmu_1",
+ parse_ret, 0);
+ TEST_ASSERT_VAL("het_parent resolved sub-metrics per originating PMU",
+ saw_duration && saw_user);
+
+ TEST_ASSERT_VAL("uncore has_metric_or_groups filters by originating PMU",
+ metricgroup__has_metric_or_groups("het_pmu_1",
+ "het_soc2_only") &&
+ !metricgroup__has_metric_or_groups("het_pmu_0",
+ "het_soc2_only") &&
+ !metricgroup__has_metric_or_groups("het_core_0",
+ "het_soc2_only"));
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new filtered het_filt_m", evlist != NULL);
+ parse_ret = metricgroup__parse_groups(evlist, "het_pmu_0",
+ /*cputype_filter=*/false,
+ "het_filt_m",
+ /*metric_no_group=*/true,
+ /*metric_no_merge=*/false,
+ /*metric_no_threshold=*/false,
+ /*user_requested_cpu_list=*/NULL,
+ /*system_wide=*/false,
+ /*hardware_aware_grouping=*/false);
+ evlist__for_each_entry(evlist, pos) {
+ if (!pos->pmu)
+ continue;
+ if (!strcmp(pos->pmu->name, "het_pmu_0"))
+ filt_ev0 = true;
+ if (!strcmp(pos->pmu->name, "het_pmu_1"))
+ filt_ev1 = true;
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse het_filt_m filtered to het_pmu_0",
+ parse_ret, 0);
+ TEST_ASSERT_VAL("het_filt_m filter excludes het_pmu_1 metric",
+ filt_ev0 && !filt_ev1);
+
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new het_core_m", evlist != NULL);
+ parse_ret = metricgroup__parse_groups(evlist, "all",
+ /*cputype_filter=*/false,
+ "het_core_m",
+ /*metric_no_group=*/true,
+ /*metric_no_merge=*/false,
+ /*metric_no_threshold=*/false,
+ /*user_requested_cpu_list=*/NULL,
+ /*system_wide=*/false,
+ /*hardware_aware_grouping=*/false);
+ evlist__for_each_entry(evlist, pos) {
+ struct metric_event *me;
+ struct metric_expr *mexp;
+
+ if (!pos->pmu)
+ continue;
+ if (!strcmp(pos->pmu->name, "het_core_0"))
+ nr_core0++;
+ if (!strcmp(pos->pmu->name, "het_core_1"))
+ nr_core1++;
+ me = metricgroup__lookup(evlist__metric_events(evlist),
+ pos, /*create=*/false);
+ if (!me)
+ continue;
+ list_for_each_entry(mexp, &me->head, nd) {
+ if (!strcmp(pos->pmu->name, "het_core_0") &&
+ mexp->metric_expr &&
+ !strcmp(mexp->metric_expr, "het_core_ev + 1") &&
+ mexp->metric_events[0] == pos)
+ saw_core0_expr = true;
+ if (!strcmp(pos->pmu->name, "het_core_1") &&
+ mexp->metric_expr &&
+ !strcmp(mexp->metric_expr, "het_core_ev + 2") &&
+ mexp->metric_events[0] == pos)
+ saw_core1_expr = true;
+ }
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse het_core_m across het_core_0 and het_core_1",
+ parse_ret, 0);
+ TEST_ASSERT_VAL("het_core_m bound default_core metrics per core PMU",
+ saw_core0_expr && saw_core1_expr &&
+ nr_core0 == 1 && nr_core1 == 1);
+
+ TEST_ASSERT_VAL("has_metric_or_groups matches het_core_0 only",
+ metricgroup__has_metric_or_groups("het_core_0",
+ "het_core0_only_m") &&
+ !metricgroup__has_metric_or_groups("het_core_1",
+ "het_core0_only_m"));
+ evlist = evlist__new();
+ TEST_ASSERT_VAL("evlist__new het_core0_only_m", evlist != NULL);
+ parse_ret = metricgroup__parse_groups(evlist, "all",
+ /*cputype_filter=*/false,
+ "het_core0_only_m",
+ /*metric_no_group=*/true,
+ /*metric_no_merge=*/false,
+ /*metric_no_threshold=*/false,
+ /*user_requested_cpu_list=*/NULL,
+ /*system_wide=*/false,
+ /*hardware_aware_grouping=*/false);
+ evlist__for_each_entry(evlist, pos) {
+ if (!pos->pmu)
+ continue;
+ if (!strcmp(pos->pmu->name, "het_core_0"))
+ only_core0++;
+ if (!strcmp(pos->pmu->name, "het_core_1"))
+ only_core1++;
+ }
+ evlist__put(evlist);
+ TEST_ASSERT_EQUAL("parse het_core0_only_m", parse_ret, 0);
+ TEST_ASSERT_VAL("het_core0_only_m does not expand onto het_core_1",
+ only_core0 == 1 && only_core1 == 0);
+
+ TEST_ASSERT_VAL("find_metric_pmu default_core skips per-PMU override",
+ perf_pmus__find_metric_pmu("default_core", "het_coexist_m", &found_m) &&
+ found_m.metric_expr &&
+ !strcmp(found_m.metric_expr, "duration_time + 40"));
+ TEST_ASSERT_VAL("find_metric_pmu wildcard skips per-instance override",
+ perf_pmus__find_metric_pmu("het_pmu", "het_coexist_m", &found_m) &&
+ found_m.metric_expr &&
+ !strcmp(found_m.metric_expr, "duration_time + 20"));
+
+ TEST_ASSERT_VAL("het0 override event preserves pmu_name across layers",
+ overlay_find_event(het0, "het_coexist_ev", &ev_snap) &&
+ !strcmp(ev_snap.str, "config=0x31") &&
+ !strcmp(ev_snap.pmu_name, "het_pmu_0"));
+ TEST_ASSERT_VAL("het1 wildcard event keeps shared pmu_name",
+ overlay_find_event(het1, "het_coexist_ev", &ev_snap) &&
+ !strcmp(ev_snap.str, "config=0x32") &&
+ !strcmp(ev_snap.pmu_name, "het_pmu"));
+
+ TEST_ASSERT_VAL("format export_coexist_dir",
+ snprintf(export_coexist_dir, sizeof(export_coexist_dir),
+ "%s/export_coexist", overlay_root) <
+ (int)sizeof(export_coexist_dir));
+ export_argv[0] = "list";
+ export_argv[1] = "--export-dir";
+ export_argv[2] = export_coexist_dir;
+ export_argv[3] = "metric";
+ export_argv[4] = NULL;
+ TEST_ASSERT_EQUAL("cmd_list --export-dir metric",
+ cmd_list(4, export_argv), 0);
+ TEST_ASSERT_VAL("exported aa_het_core_0 override metric",
+ overlay_expect_file(overlay_root,
+ "export_coexist/aa_het_core_0/metrics/het_coexist_m",
+ "duration_time + 30\n"));
+ TEST_ASSERT_VAL("exported shared default_core metric",
+ overlay_expect_file(overlay_root,
+ "export_coexist/default_core/metrics/het_coexist_m",
+ "duration_time + 40\n"));
+ TEST_ASSERT_VAL("exported het_pmu_0 override metric",
+ overlay_expect_file(overlay_root,
+ "export_coexist/het_pmu_0/metrics/het_coexist_m",
+ "duration_time + 10\n"));
+ TEST_ASSERT_VAL("exported shared het_pmu wildcard metric",
+ overlay_expect_file(overlay_root,
+ "export_coexist/het_pmu/metrics/het_coexist_m",
+ "duration_time + 20\n"));
+ }
+
+ test_pmu_put(sysfs_dir, pmu);
+ *pmu_out = NULL;
+ sysfs_dir[0] = '\0';
+
+ /*
+ * 6. Stepping-specific full CPUID symlink takes precedence over base
+ * CPUID in the same layer without merging the coarser model tree.
+ */
+ TEST_ASSERT_VAL("write stepping-event",
+ overlay_write_file(overlay_root,
+ "dir2/models/stepping1/perf-pmu-test/events/stepping-event",
+ "krava01=2\n") == 0);
+ TEST_ASSERT_VAL("symlink exact stepping CPUID",
+ overlay_symlink(overlay_root,
+ "dir2/cpus/GenuineTest-6-AA-1",
+ "../models/stepping1") == 0);
+
+ perf_config__exit();
+ perf_pmus__destroy();
+
+ pmu = test_pmu_get(sysfs_dir, sysfs_dir_sz);
+ *pmu_out = pmu;
+ TEST_ASSERT_VAL("test_pmu_get stepping", pmu != NULL);
+ TEST_ASSERT_VAL("stepping-event present",
+ perf_pmu__have_event(pmu, "stepping-event"));
+ TEST_ASSERT_VAL("base-cpuid-event not merged when exact stepping matched",
+ !perf_pmu__have_event(pmu, "base-cpuid-event"));
+
+ return TEST_OK;
+}
+
+static int test__pmu_layer_overlay(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ char overlay_root[PATH_MAX] = "/tmp/perf-pmu-layer-XXXXXX";
+ char config_path[PATH_MAX] = "";
+ char sysfs_dir[PATH_MAX] = "";
+ char *orig_config = NULL, *orig_cpuid = NULL;
+ const char *orig_exclusive = config_exclusive_filename;
+ const char *env_val;
+ struct perf_pmu *pmu = NULL;
+ int ret = TEST_FAIL;
+
+ env_val = getenv("PERF_CONFIG");
+ if (env_val) {
+ orig_config = strdup(env_val);
+ if (!orig_config)
+ return TEST_FAIL;
+ }
+ env_val = getenv("PERF_CPUID");
+ if (env_val) {
+ orig_cpuid = strdup(env_val);
+ if (!orig_cpuid) {
+ free(orig_config);
+ return TEST_FAIL;
+ }
+ }
+
+ if (mkdtemp(overlay_root)) {
+ ret = run_pmu_layer_overlay_checks(overlay_root,
+ config_path, sizeof(config_path),
+ sysfs_dir, sizeof(sysfs_dir),
+ &pmu);
+ } else {
+ overlay_root[0] = '\0';
+ }
+
+ if (pmu) {
+ test_pmu_put(sysfs_dir, pmu);
+ pmu = NULL;
+ }
+ config_exclusive_filename = orig_exclusive;
+ if (orig_config)
+ setenv("PERF_CONFIG", orig_config, 1);
+ else
+ unsetenv("PERF_CONFIG");
+ if (orig_cpuid)
+ setenv("PERF_CPUID", orig_cpuid, 1);
+ else
+ unsetenv("PERF_CPUID");
+ free(orig_config);
+ free(orig_cpuid);
+ perf_config__exit();
+ perf_pmus__destroy();
+ if (overlay_root[0])
+ overlay_rm_rf_at(AT_FDCWD, overlay_root);
+ return ret;
+}
+
static struct test_case tests__pmu[] = {
TEST_CASE("Parsing with PMU format directory", pmu_format),
TEST_CASE("Parsing with PMU event", pmu_events),
@@ -636,6 +1594,7 @@ static struct test_case tests__pmu[] = {
TEST_CASE("PMU name comparison", name_cmp),
TEST_CASE("PMU cmdline match", pmu_match),
TEST_CASE("PMU config helpers", pmu_config_helpers),
+ TEST_CASE("Layered PMU directory overlays", pmu_layer_overlay),
{ .name = NULL, }
};
diff --git a/tools/perf/tests/shell/pmu_events_dir.sh b/tools/perf/tests/shell/pmu_events_dir.sh
new file mode 100755
index 000000000000..ffca18e674fb
--- /dev/null
+++ b/tools/perf/tests/shell/pmu_events_dir.sh
@@ -0,0 +1,236 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# pmu-events.dir overlay and export tests (no_mlock)
+
+set -e
+
+shelldir=$(dirname "$0")
+tmpdir=$(mktemp -d /tmp/__perf_test.pmu_events_dir.XXXXX)
+
+cleanup() {
+ rm -rf "${tmpdir}"
+ trap - EXIT TERM INT
+}
+
+trap_cleanup() {
+ cleanup
+ exit 1
+}
+trap trap_cleanup EXIT TERM INT
+
+test_export_roundtrip() {
+ echo "perf list --export-dir round-trip test"
+ perf list -j -o "${tmpdir}/baseline.json" sw metric metricgroup
+ perf list --export-dir "${tmpdir}/export" sw metric metricgroup
+ [ -d "${tmpdir}/export" ]
+
+ cat > "${tmpdir}/export_config" <<EOF
+[pmu-events]
+ dir = ${tmpdir}/export
+EOF
+
+ PERF_CONFIG="${tmpdir}/export_config" \
+ perf list -j -o "${tmpdir}/roundtrip.json" sw metric metricgroup
+ diff -u "${tmpdir}/baseline.json" "${tmpdir}/roundtrip.json"
+
+ perf list --export-dir "${tmpdir}/tp_export" tracepoint
+ [ -d "${tmpdir}/tp_export" ]
+ [ -z "$(ls -A "${tmpdir}/tp_export")" ]
+ echo "perf list --export-dir round-trip test [Success]"
+}
+
+test_user_overlay() {
+ echo "pmu-events.dir user overlay test"
+ ov_ev="${tmpdir}/overlay/tool/events"
+ ov_met="${tmpdir}/overlay/default_core/metrics"
+ ov_mg="${tmpdir}/overlay/metricgroups"
+ mkdir -p "${ov_ev}" "${ov_met}" "${ov_mg}"
+
+ printf 'config=0x1\n' > "${ov_ev}/overlay_duration"
+ printf 'Overlay custom duration event\n' > "${ov_ev}/overlay_duration.desc"
+ printf 'Overridden duration_time description\n' > "${ov_ev}/duration_time.desc"
+
+ printf 'duration_time * 2\n' > "${ov_met}/overlay_double_duration"
+ printf '1ns\n' > "${ov_met}/overlay_double_duration.scale-unit"
+ printf 'Custom doubled duration metric\n' > "${ov_met}/overlay_double_duration.desc"
+ printf 'OverlayTestGroup\n' > "${ov_met}/overlay_double_duration.group"
+
+ printf 'Custom overlay metric group\n' > "${ov_mg}/OverlayTestGroup.desc"
+
+ cat > "${tmpdir}/overlay_config" <<EOF
+[pmu-events]
+ dir = ${tmpdir}/overlay
+EOF
+
+ PERF_CONFIG="${tmpdir}/overlay_config" perf list > "${tmpdir}/list_all.txt"
+ grep -q "overlay_duration" "${tmpdir}/list_all.txt"
+ grep -q "Overlay custom duration event" "${tmpdir}/list_all.txt"
+ grep -q "Overridden duration_time description" "${tmpdir}/list_all.txt"
+
+ PERF_CONFIG="${tmpdir}/overlay_config" perf list metric > "${tmpdir}/list_metric.txt"
+ grep -q "overlay_double_duration" "${tmpdir}/list_metric.txt"
+ grep -q "Custom doubled duration metric" "${tmpdir}/list_metric.txt"
+
+ PERF_CONFIG="${tmpdir}/overlay_config" perf list metricgroup > "${tmpdir}/list_mg.txt"
+ grep -q "OverlayTestGroup" "${tmpdir}/list_mg.txt"
+ grep -q "Custom overlay metric group" "${tmpdir}/list_mg.txt"
+
+ PERF_CONFIG="${tmpdir}/overlay_config" perf stat -e overlay_duration true >/dev/null 2>&1
+ PERF_CONFIG="${tmpdir}/overlay_config" perf stat -M overlay_double_duration true >/dev/null 2>&1
+
+ rm -f "${ov_ev}/duration_time.desc" \
+ "${ov_ev}/overlay_duration" "${ov_ev}/overlay_duration.desc" \
+ "${ov_met}/overlay_double_duration" "${ov_met}/overlay_double_duration.scale-unit" \
+ "${ov_met}/overlay_double_duration.desc" "${ov_met}/overlay_double_duration.group" \
+ "${ov_mg}/OverlayTestGroup.desc"
+
+ PERF_CONFIG="${tmpdir}/overlay_config" perf list > "${tmpdir}/list_after_rm.txt"
+ if grep -q "overlay_duration" "${tmpdir}/list_after_rm.txt"; then
+ echo "Deleting overlay_duration failed to remove custom event"
+ exit 1
+ fi
+ if grep -q "Overridden duration_time description" "${tmpdir}/list_after_rm.txt"; then
+ echo "Deleting duration_time.desc failed to restore default description"
+ exit 1
+ fi
+ grep -q "duration_time" "${tmpdir}/list_after_rm.txt"
+ echo "pmu-events.dir user overlay test [Success]"
+}
+
+test_jevents_output_dir() {
+ jevents="${shelldir}/../../pmu-events/jevents.py"
+ archdir="${shelldir}/../../pmu-events/arch"
+ if [ ! -f "${jevents}" ] || [ ! -d "${archdir}" ]; then
+ echo "jevents.py --output-dir test [Skipped: source tree not present]"
+ return 0
+ fi
+
+ py="${PYTHON:-}"
+ if [ -z "${py}" ]; then
+ python3 --version >/dev/null 2>&1 && py=python3
+ fi
+ if [ -z "${py}" ]; then
+ python --version >/dev/null 2>&1 && py=python
+ fi
+ if [ -z "${py}" ]; then
+ echo "jevents.py --output-dir test [Skipped: python not found]"
+ return 0
+ fi
+
+ echo "jevents.py --output-dir CPUID symlink test"
+ "${py}" "${jevents}" --output-dir "${tmpdir}/jevents-test" test all "${archdir}"
+ [ -L "${tmpdir}/jevents-test/cpus/testcpu" ]
+ [ -L "${tmpdir}/jevents-test/sys/v8" ]
+ [ -z "$(find "${tmpdir}/jevents-test" -name 'mapfile.csv')" ]
+
+ cat > "${tmpdir}/jevents_config" <<EOF
+[pmu-events]
+ dir = ${tmpdir}/jevents-test
+EOF
+
+ PERF_CPUID=testcpu PERF_CONFIG="${tmpdir}/jevents_config" \
+ perf list > "${tmpdir}/jevents_list.txt" 2>/dev/null
+ grep -q "bp_l1_btb_correct" "${tmpdir}/jevents_list.txt"
+ grep -q "dcache_miss_cpi" "${tmpdir}/jevents_list.txt"
+
+ "${py}" "${jevents}" --output-dir "${tmpdir}/jevents-common" common all "${archdir}"
+ [ -f "${tmpdir}/jevents-common/default_core/events/cycles.legacy-terms" ]
+ [ ! -e "${tmpdir}/jevents-common/default_core/events/cycles" ]
+
+ synth_soc="${tmpdir}/synth_arch/arm64/vendor/soc"
+ mkdir -p "${synth_soc}/sys"
+ cat > "${tmpdir}/synth_arch/arm64/mapfile.csv" <<'EOF'
+Family-model,Version,Filename,EventType
+0x00000000480fd010,v1,vendor/soc,core
+EOF
+ cat > "${synth_soc}/branch.json" <<'EOF'
+[
+ {
+ "EventName": "before_sys_ev",
+ "EventCode": "0x11",
+ "BriefDescription": "Event sorting before sys/"
+ }
+]
+EOF
+ cat > "${synth_soc}/sys/uncore.json" <<'EOF'
+[
+ {
+ "Unit": "hisi_sccl,ddrc",
+ "EventName": "sys_uncore_ev",
+ "EventCode": "0x22",
+ "Compat": "0x00000030",
+ "BriefDescription": "System PMU event inside sys/"
+ }
+]
+EOF
+ cat > "${synth_soc}/virtual-memory.json" <<'EOF'
+[
+ {
+ "EventName": "after_sys_ev",
+ "EventCode": "0x33",
+ "BriefDescription": "Event sorting after sys/"
+ }
+]
+EOF
+
+ "${py}" "${jevents}" --output-dir "${tmpdir}/jevents-synth" arm64 all "${tmpdir}/synth_arch"
+ [ -f "${tmpdir}/jevents-synth/models/arm64/vendor/soc/default_core/events/before_sys_ev" ]
+ [ -f "${tmpdir}/jevents-synth/models/arm64/vendor/soc/default_core/events/after_sys_ev" ]
+ [ ! -e "${tmpdir}/jevents-synth/models/arm64/vendor/soc/sys/default_core/events/after_sys_ev" ]
+ [ -f "${tmpdir}/jevents-synth/models/arm64/vendor/soc/sys/hisi_sccl,ddrc/events/sys_uncore_ev" ]
+ [ -L "${tmpdir}/jevents-synth/cpus/0x00000000480fd010" ]
+ [ -L "${tmpdir}/jevents-synth/sys/0x00000030" ]
+ echo "jevents.py --output-dir CPUID symlink test [Success]"
+}
+
+test_export_cpuid() {
+ echo "perf list --export-cpuid test"
+ if perf list --export-cpuid testcpu >/dev/null 2>&1; then
+ echo "--export-cpuid without --export-dir unexpectedly succeeded"
+ exit 1
+ fi
+ if perf list --export-dir "${tmpdir}/bad_cpuid" \
+ --export-cpuid no_such_cpuid_xyz >/dev/null 2>&1; then
+ echo "--export-cpuid with unknown CPUID unexpectedly succeeded"
+ exit 1
+ fi
+
+ perf list --export-dir "${tmpdir}/cpuid_test" --export-cpuid testarch
+ [ -L "${tmpdir}/cpuid_test/cpus/testcpu" ]
+ [ -L "${tmpdir}/cpuid_test/sys/v8" ]
+ [ -f "${tmpdir}/cpuid_test/models/testarch/test_soc/cpu/default_core/events/bp_l1_btb_correct" ]
+ [ -f "${tmpdir}/cpuid_test/models/testarch/test_soc/cpu/default_core/metrics/dcache_miss_cpi" ]
+
+ printf 'Migrated testcpu metric description\n' \
+ > "${tmpdir}/cpuid_test/models/testarch/test_soc/cpu/default_core/metrics/dcache_miss_cpi.desc"
+ cat > "${tmpdir}/cpuid_test_config" <<EOF
+[pmu-events]
+ dir = ${tmpdir}/cpuid_test
+EOF
+ PERF_CPUID=testcpu PERF_CONFIG="${tmpdir}/cpuid_test_config" \
+ perf list metric > "${tmpdir}/cpuid_test_metric.txt" 2>/dev/null
+ grep -q "Migrated testcpu metric description" "${tmpdir}/cpuid_test_metric.txt"
+
+ perf list --export-dir "${tmpdir}/cpuid_common" --export-cpuid common
+ [ -f "${tmpdir}/cpuid_common/default_core/events/cycles.legacy-terms" ]
+
+ if perf list --export-dir "${tmpdir}/cpuid_n2" \
+ --export-cpuid 0x00000000411fd493 metric metricgroup >/dev/null 2>&1; then
+ [ -L "${tmpdir}/cpuid_n2/cpus/0x00000000410fd490" ]
+ [ -d "${tmpdir}/cpuid_n2/models/arm64/arm/neoverse-n2-v2/default_core/metrics" ]
+ [ ! -d "${tmpdir}/cpuid_n2/models/arm64/arm/neoverse-n2-v2/default_core/events" ]
+ fi
+ if perf list --export-dir "${tmpdir}/cpuid_skl" \
+ --export-cpuid x86/skylake metric >/dev/null 2>&1; then
+ [ -L "${tmpdir}/cpuid_skl/cpus/GenuineIntel-6-4E" ]
+ [ -L "${tmpdir}/cpuid_skl/cpus/GenuineIntel-6-5E" ]
+ fi
+ echo "perf list --export-cpuid test [Success]"
+}
+
+test_export_roundtrip
+test_user_overlay
+test_jevents_output_dir
+test_export_cpuid
+cleanup
+exit 0
--
2.56.1.703.gf848ed807c-goog
^ permalink raw reply [flat|nested] 12+ messages in thread