[PATCH 09/10] perf tests pmu-events: Test metricgroup descriptions and concurrent lookups

From: Ian Rogers

Date: Fri Nov 13 2026 - 16:10:11 EST


Add metricgroups.json to arch/test/test_soc/cpu/ with prefix-overlapping
keys ("group1" and "group1A"), regenerate empty-pmu-events.c, and add a
"metricgroups" subtest to "PMU JSON event tests".

The new subtest verifies exact and prefix-overlapping metricgroup binary
searches via describe_metricgroup(), validates metricgroup descriptions
across all core and system metrics, and launches 8 concurrent worker
threads scanning core/sys events and metrics to stress-test lock-free
block decompression and shared dictionary initialization.

Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
.../arch/test/test_soc/cpu/metricgroups.json | 4 +
tools/perf/pmu-events/empty-pmu-events.c | 84 +++----
tools/perf/tests/pmu-events.c | 209 +++++++++++++++++-
3 files changed, 259 insertions(+), 38 deletions(-)
create mode 100644 tools/perf/pmu-events/arch/test/test_soc/cpu/metricgroups.json

diff --git a/tools/perf/pmu-events/arch/test/test_soc/cpu/metricgroups.json b/tools/perf/pmu-events/arch/test/test_soc/cpu/metricgroups.json
new file mode 100644
index 000000000000..2bf7e4fbff3c
--- /dev/null
+++ b/tools/perf/pmu-events/arch/test/test_soc/cpu/metricgroups.json
@@ -0,0 +1,4 @@
+{
+ "group1": "Test metricgroup 1",
+ "group1A": "Test metricgroup 1A"
+}
diff --git a/tools/perf/pmu-events/empty-pmu-events.c b/tools/perf/pmu-events/empty-pmu-events.c
index 46a32a4cb9d6..f4009e5c51ee 100644
--- a/tools/perf/pmu-events/empty-pmu-events.c
+++ b/tools/perf/pmu-events/empty-pmu-events.c
@@ -2605,42 +2605,50 @@ static const char big_c_string[] =
/* offset=128790 */
"M3\000\0001 / M3\000\000\000\000\000\000\000\000000"
/* offset=128811 */
+"Test metricgroup 1\000"
+/* offset=128830 */
+"Test metricgroup 1A\000"
+/* offset=128850 */
"backend_cycles_idle\000Default\000(stalled\\-cycles\\-backend / cpu\\-cycles if has_event(stalled\\-cycles\\-backend) else 0)\000backend_cycles_idle > 0.2\000Backend stalls per cycle\000\000\000\000\000\000001"
-/* offset=128985 */
+/* offset=129024 */
"branch_frequency\000Default\000branches / (software@cpu\\-clock\\,name\\=cpu\\-clock@ if #target_cpu else software@task\\-clock\\,name\\=task\\-clock@)\000\000Branches per CPU second\000\0001000M/sec\000\000\000\000011"
-/* offset=129165 */
+/* offset=129204 */
"branch_miss_rate\000Default\000branch\\-misses / branches\000branch_miss_rate > 0.05\000Branch miss rate\000\000100%\000\000\000\000001"
-/* offset=129269 */
+/* offset=129308 */
"cache_miss_cycles\000group1\000dcache_miss_cpi + icache_miss_cycles\000\000\000\000\000\000\000\000000"
-/* offset=129341 */
+/* offset=129380 */
"cs_per_second\000Default\000software@context\\-switches\\,name\\=context\\-switches@ * 1e9 / (software@cpu\\-clock\\,name\\=cpu\\-clock@ if #target_cpu else software@task\\-clock\\,name\\=task\\-clock@)\000\000Context switches per CPU second\000\0001cs/sec\000\000\000\000011"
-/* offset=129574 */
+/* offset=129613 */
"cycles_frequency\000Default\000cpu\\-cycles / (software@cpu\\-clock\\,name\\=cpu\\-clock@ if #target_cpu else software@task\\-clock\\,name\\=task\\-clock@)\000\000Cycles per CPU second\000\0001GHz\000\000\000\000011"
-/* offset=129750 */
+/* offset=129789 */
"dcache_miss_cpi\000\000l1d\\-loads\\-misses / inst_retired.any\000\000\000\000\000\000\000\000000"
-/* offset=129815 */
+/* offset=129854 */
"dtlb_miss_rate\000Default3\000dTLB\\-load\\-misses / dTLB\\-loads\000dtlb_miss_rate > 0.05\000dTLB miss rate\000\000100%\000\000\000\000001"
-/* offset=129921 */
+/* offset=129960 */
"frontend_cycles_idle\000Default\000(stalled\\-cycles\\-frontend / cpu\\-cycles if has_event(stalled\\-cycles\\-frontend) else 0)\000frontend_cycles_idle > 0.1\000Frontend stalls per cycle\000\000\000\000\000\000001"
-/* offset=130100 */
+/* offset=130139 */
+"group1\000"
+/* offset=130146 */
+"group1A\000"
+/* offset=130154 */
"icache_miss_cycles\000\000l1i\\-loads\\-misses / inst_retired.any\000\000\000\000\000\000\000\000000"
-/* offset=130168 */
+/* offset=130222 */
"insn_per_cycle\000Default\000instructions / cpu\\-cycles\000insn_per_cycle < 1\000Instructions Per Cycle\000\0001instructions\000\000\000\000001"
-/* offset=130281 */
+/* offset=130335 */
"itlb_miss_rate\000Default3\000iTLB\\-load\\-misses / iTLB\\-loads\000itlb_miss_rate > 0.05\000iTLB miss rate\000\000100%\000\000\000\000001"
-/* offset=130387 */
+/* offset=130441 */
"l1_prefetch_miss_rate\000Default4\000L1\\-dcache\\-prefetch\\-misses / L1\\-dcache\\-prefetches\000l1_prefetch_miss_rate > 0.05\000L1 prefetch miss rate\000\000100%\000\000\000\000001"
-/* offset=130535 */
+/* offset=130589 */
"l1d_miss_rate\000Default2\000L1\\-dcache\\-load\\-misses / L1\\-dcache\\-loads\000l1d_miss_rate > 0.05\000L1D miss rate\000\000100%\000\000\000\000001"
-/* offset=130651 */
+/* offset=130705 */
"l1i_miss_rate\000Default3\000L1\\-icache\\-load\\-misses / L1\\-icache\\-loads\000l1i_miss_rate > 0.05\000L1I miss rate\000\000100%\000\000\000\000001"
-/* offset=130766 */
+/* offset=130820 */
"llc_miss_rate\000Default2\000LLC\\-load\\-misses / LLC\\-loads\000llc_miss_rate > 0.05\000LLC miss rate\000\000100%\000\000\000\000001"
-/* offset=130867 */
+/* offset=130921 */
"migrations_per_second\000Default\000software@cpu\\-migrations\\,name\\=cpu\\-migrations@ * 1e9 / (software@cpu\\-clock\\,name\\=cpu\\-clock@ if #target_cpu else software@task\\-clock\\,name\\=task\\-clock@)\000\000Process migrations to a new CPU per CPU second\000\0001migrations/sec\000\000\000\000011"
-/* offset=131127 */
+/* offset=131181 */
"page_faults_per_second\000Default\000software@page\\-faults\\,name\\=page\\-faults@ * 1e9 / (software@cpu\\-clock\\,name\\=cpu\\-clock@ if #target_cpu else software@task\\-clock\\,name\\=task\\-clock@)\000\000Page faults per CPU second\000\0001faults/sec\000\000\000\000011"
-/* offset=131358 */
+/* offset=131412 */
"stalled_cycles_per_instruction\000Default\000(max(stalled\\-cycles\\-frontend, stalled\\-cycles\\-backend) / instructions if has_event(stalled\\-cycles\\-frontend) & has_event(stalled\\-cycles\\-backend) else (stalled\\-cycles\\-frontend / instructions if has_event(stalled\\-cycles\\-frontend) else (stalled\\-cycles\\-backend / instructions if has_event(stalled\\-cycles\\-backend) else 0)))\000\000Max front or backend stalls per instruction\000\000\000\000\000\000001"
;

@@ -5197,37 +5205,37 @@ static const struct compact_pmu_event pmu_metrics__common_default_core[] = {
/* CPUs_utilized\000Default\000(software@cpu\\-clock\\,name\\=cpu\\-clock@ if #targ... */
{ 127820 },
/* backend_cycles_idle\000Default\000(stalled\\-cycles\\-backend / cpu\\-cycles if ... */
- { 128811 },
+ { 128850 },
/* branch_frequency\000Default\000branches / (software@cpu\\-clock\\,name\\=cpu\\-c... */
- { 128985 },
+ { 129024 },
/* branch_miss_rate\000Default\000branch\\-misses / branches\000branch_miss_rate > ... */
- { 129165 },
+ { 129204 },
/* cs_per_second\000Default\000software@context\\-switches\\,name\\=context\\-switc... */
- { 129341 },
+ { 129380 },
/* cycles_frequency\000Default\000cpu\\-cycles / (software@cpu\\-clock\\,name\\=cpu... */
- { 129574 },
+ { 129613 },
/* dtlb_miss_rate\000Default3\000dTLB\\-load\\-misses / dTLB\\-loads\000dtlb_miss_r... */
- { 129815 },
+ { 129854 },
/* frontend_cycles_idle\000Default\000(stalled\\-cycles\\-frontend / cpu\\-cycles i... */
- { 129921 },
+ { 129960 },
/* insn_per_cycle\000Default\000instructions / cpu\\-cycles\000insn_per_cycle < 1\0... */
- { 130168 },
+ { 130222 },
/* itlb_miss_rate\000Default3\000iTLB\\-load\\-misses / iTLB\\-loads\000itlb_miss_r... */
- { 130281 },
+ { 130335 },
/* l1_prefetch_miss_rate\000Default4\000L1\\-dcache\\-prefetch\\-misses / L1\\-dcac... */
- { 130387 },
+ { 130441 },
/* l1d_miss_rate\000Default2\000L1\\-dcache\\-load\\-misses / L1\\-dcache\\-loads\0... */
- { 130535 },
+ { 130589 },
/* l1i_miss_rate\000Default3\000L1\\-icache\\-load\\-misses / L1\\-icache\\-loads\0... */
- { 130651 },
+ { 130705 },
/* llc_miss_rate\000Default2\000LLC\\-load\\-misses / LLC\\-loads\000llc_miss_rate ... */
- { 130766 },
+ { 130820 },
/* migrations_per_second\000Default\000software@cpu\\-migrations\\,name\\=cpu\\-mig... */
- { 130867 },
+ { 130921 },
/* page_faults_per_second\000Default\000software@page\\-faults\\,name\\=page\\-faul... */
- { 131127 },
+ { 131181 },
/* stalled_cycles_per_instruction\000Default\000(max(stalled\\-cycles\\-frontend, s... */
- { 131358 },
+ { 131412 },

};

@@ -5338,11 +5346,11 @@ static const struct compact_pmu_event pmu_metrics__test_soc_cpu_default_core[] =
/* M3\000\0001 / M3\000\000\000\000\000\000\000\000000 */
{ 128790 },
/* cache_miss_cycles\000group1\000dcache_miss_cpi + icache_miss_cycles\000\000\000\... */
- { 129269 },
+ { 129308 },
/* dcache_miss_cpi\000\000l1d\\-loads\\-misses / inst_retired.any\000\000\000\000\0... */
- { 129750 },
+ { 129789 },
/* icache_miss_cycles\000\000l1i\\-loads\\-misses / inst_retired.any\000\000\000\00... */
- { 130100 },
+ { 130154 },

};

@@ -6034,6 +6042,8 @@ int pmu_metrics_table__iterate_tables(pmu_metrics_table_iter_t fn, void *data)
}

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/tests/pmu-events.c b/tools/perf/tests/pmu-events.c
index e6e4f4cf1fa2..a06525cafb48 100644
--- a/tools/perf/tests/pmu-events.c
+++ b/tools/perf/tests/pmu-events.c
@@ -5,7 +5,12 @@
#include "pmus.h"
#include "tests.h"
#include <errno.h>
+#include <pthread.h>
+#include <stdbool.h>
+#include <stddef.h>
#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
#include <linux/kernel.h>
#include <linux/zalloc.h>
#include "debug.h"
@@ -1094,6 +1099,202 @@ static int test__parsing_threshold(struct test_suite *test __maybe_unused,
return pmu_for_each_sys_metric(test__parsing_threshold_callback, NULL);
}

+static int check_event_strings_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *data __maybe_unused)
+{
+ if (!pe->pmu || !pe->name || (!pe->event && !pe->desc))
+ return TEST_FAIL;
+ return 0;
+}
+
+static int check_find_btb_cb(const struct pmu_event *pe,
+ const struct pmu_events_table *table __maybe_unused,
+ void *data)
+{
+ int *matched = data;
+
+ if (!pe->event || strcmp(pe->event, "event=0x8a"))
+ return TEST_FAIL;
+ (*matched)++;
+ return 0;
+}
+
+static int check_find_ipc_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *data)
+{
+ int *matched = data;
+
+ if (!pm->metric_group || strcmp(pm->metric_group, "group1"))
+ return TEST_FAIL;
+ (*matched)++;
+ return 0;
+}
+
+static int check_metricgroup_strings_cb(const struct pmu_metric *pm,
+ const struct pmu_metrics_table *table __maybe_unused,
+ void *data __maybe_unused)
+{
+ char *dup, *group, *saveptr = NULL;
+
+ if (!pm->pmu || !pm->metric_expr)
+ return TEST_FAIL;
+ if (!pm->metric_group)
+ return 0;
+
+ dup = strdup(pm->metric_group);
+ if (!dup)
+ return -ENOMEM;
+
+ for (group = strtok_r(dup, ";", &saveptr); group;
+ group = strtok_r(NULL, ";", &saveptr)) {
+ const char *desc = describe_metricgroup(group);
+
+ if (desc && desc[0] == '\0') {
+ free(dup);
+ return TEST_FAIL;
+ }
+ if (!strcmp(group, "group1") &&
+ (!desc || strcmp(desc, "Test metricgroup 1"))) {
+ free(dup);
+ return TEST_FAIL;
+ }
+ }
+ free(dup);
+ return 0;
+}
+
+struct concurrent_lookup_gate {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ bool start;
+};
+
+struct concurrent_lookup_worker {
+ struct concurrent_lookup_gate *gate;
+ int err;
+};
+
+static int run_lookup_checks(void)
+{
+ const struct pmu_events_table *etest;
+ const struct pmu_metrics_table *mtest;
+ const char *desc;
+ int matched, err;
+
+ desc = describe_metricgroup("group1");
+ if (!desc || strcmp(desc, "Test metricgroup 1")) {
+ pr_err("Unexpected group1 description: %s\n", desc ?: "(null)");
+ return TEST_FAIL;
+ }
+
+ desc = describe_metricgroup("group1A");
+ if (!desc || strcmp(desc, "Test metricgroup 1A")) {
+ pr_err("Unexpected group1A description: %s\n", desc ?: "(null)");
+ return TEST_FAIL;
+ }
+
+ if (describe_metricgroup("group1_nonexistent") || describe_metricgroup("")) {
+ pr_err("Unexpected match for nonexistent metricgroup\n");
+ return TEST_FAIL;
+ }
+
+ if (describe_metricgroup("BadSpec") && !describe_metricgroup("Bad")) {
+ pr_err("Prefix metricgroup 'Bad' missing when 'BadSpec' is present\n");
+ return TEST_FAIL;
+ }
+
+ etest = find_core_events_table("testarch", "testcpu");
+ mtest = find_core_metrics_table("testarch", "testcpu");
+ if (!etest || !mtest)
+ return TEST_FAIL;
+
+ matched = 0;
+ err = pmu_events_table__find_event(etest, NULL, "bp_l1_btb_correct",
+ check_find_btb_cb, &matched);
+ if (err || matched != 1)
+ return TEST_FAIL;
+
+ matched = 0;
+ err = pmu_metrics_table__find_metric(mtest, NULL, "IPC",
+ check_find_ipc_cb, &matched);
+ if (err || matched != 1)
+ return TEST_FAIL;
+
+ err = pmu_for_each_core_event(check_event_strings_cb, NULL);
+ if (err)
+ return err;
+
+ err = pmu_for_each_sys_event(check_event_strings_cb, NULL);
+ if (err)
+ return err;
+
+ err = pmu_for_each_core_metric(check_metricgroup_strings_cb, NULL);
+ if (err)
+ return err;
+
+ return pmu_for_each_sys_metric(check_metricgroup_strings_cb, NULL);
+}
+
+static void *concurrent_lookup_thread(void *arg)
+{
+ struct concurrent_lookup_worker *worker = arg;
+
+ pthread_mutex_lock(&worker->gate->mutex);
+ while (!worker->gate->start)
+ pthread_cond_wait(&worker->gate->cond, &worker->gate->mutex);
+ pthread_mutex_unlock(&worker->gate->mutex);
+
+ worker->err = run_lookup_checks();
+ return NULL;
+}
+
+/*
+ * Verify metricgroup binary search and release 8 worker threads simultaneously
+ * to stress-test lock-free on-demand decompression of ~8 KiB string blocks in
+ * pmu_events__get_string() (big_c_string_cache[]) and shared dictionary setup.
+ */
+static int test__metricgroups(struct test_suite *test __maybe_unused,
+ int subtest __maybe_unused)
+{
+ struct concurrent_lookup_gate gate = {
+ .mutex = PTHREAD_MUTEX_INITIALIZER,
+ .cond = PTHREAD_COND_INITIALIZER,
+ .start = false,
+ };
+ struct concurrent_lookup_worker workers[8];
+ pthread_t threads[ARRAY_SIZE(workers)];
+ size_t created = 0;
+ int ret = TEST_OK;
+
+ for (size_t i = 0; i < ARRAY_SIZE(workers); i++) {
+ workers[i].gate = &gate;
+ workers[i].err = 0;
+ if (pthread_create(&threads[i], NULL, concurrent_lookup_thread,
+ &workers[i]) != 0) {
+ ret = TEST_FAIL;
+ break;
+ }
+ created++;
+ }
+
+ pthread_mutex_lock(&gate.mutex);
+ gate.start = true;
+ pthread_cond_broadcast(&gate.cond);
+ pthread_mutex_unlock(&gate.mutex);
+
+ for (size_t i = 0; i < created; i++) {
+ pthread_join(threads[i], NULL);
+ if (workers[i].err && ret == TEST_OK)
+ ret = workers[i].err;
+ }
+
+ pthread_cond_destroy(&gate.cond);
+ pthread_mutex_destroy(&gate.mutex);
+ return ret;
+}
+
struct populate_cb_data {
struct test_case *test_cases;
size_t curr;
@@ -1145,7 +1346,7 @@ static struct test_case pmu_events_tests[];
static int setup_pmu_events_suite(struct test_suite *suite)
{
size_t num_tables = 0;
- size_t num_fixed_tests = 4;
+ size_t num_fixed_tests = 5;
size_t tests_per_table = 2;
size_t total_tests;
struct test_case *test_cases;
@@ -1171,6 +1372,11 @@ static int setup_pmu_events_suite(struct test_suite *suite)
.desc = "PMU event table sanity",
.run_case = test__pmu_event_table,
};
+ test_cases[curr++] = (struct test_case){
+ .name = "metricgroups",
+ .desc = "PMU metricgroup descriptions and concurrent lookups",
+ .run_case = test__metricgroups,
+ };
test_cases[curr++] = (struct test_case){
.name = "aliases",
.desc = "PMU event map aliases",
@@ -1208,6 +1414,7 @@ static int setup_pmu_events_suite(struct test_suite *suite)

static struct test_case pmu_events_tests[] = {
TEST_CASE("PMU event table sanity", pmu_event_table),
+ TEST_CASE("PMU metricgroup descriptions and concurrent lookups", metricgroups),
TEST_CASE("PMU event map aliases", aliases),
TEST_CASE_REASON("Parsing of PMU event table metrics", parsing,
"some metrics failed"),
--
2.56.1.703.gf848ed807c-goog