Page Menu
Home
FreeBSD
Search
Configure Global Search
Log In
Files
F177569596
D59549.1794764609.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
76 KB
Referenced Files
None
Subscribers
None
D59549.1794764609.diff
View Options
diff --git a/etc/mtree/BSD.tests.dist b/etc/mtree/BSD.tests.dist
--- a/etc/mtree/BSD.tests.dist
+++ b/etc/mtree/BSD.tests.dist
@@ -988,6 +988,8 @@
..
..
pmc
+ amd
+ ..
..
posixshm
..
diff --git a/tests/sys/pmc/Makefile b/tests/sys/pmc/Makefile
--- a/tests/sys/pmc/Makefile
+++ b/tests/sys/pmc/Makefile
@@ -4,15 +4,30 @@
ATF_TESTS_C+= pmc_credexec_test
ATF_TESTS_C+= pmc_detach_test
ATF_TESTS_C+= pmc_exec_test
+ATF_TESTS_C+= pmc_lifecycle_test
+ATF_TESTS_C+= pmc_log_test
+ATF_TESTS_C+= pmc_pidtrack_test
+ATF_TESTS_C+= pmc_wrap_test
+
+ATF_TESTS_SH+= pmc_callchain_ur_test
+ATF_TESTS_SH+= pmc_cli_test
+
.if ${MACHINE_ARCH} == "amd64"
ATF_TESTS_C+= pmc_exterr_test
ATF_TESTS_C+= pmc_mapfile_test
+ATF_TESTS_C+= pmc_rapl_test
+
+ATF_TESTS_SH+= pmcstat_tsc_test
+
+TESTS_SUBDIRS+= amd
.endif
-ATF_TESTS_C+= pmc_lifecycle_test
-ATF_TESTS_C+= pmc_log_test
-ATF_TESTS_C+= pmc_wrap_test
TEST_METADATA.pmc_exterr_test+= required_user="root"
+TEST_METADATA.pmc_callchain_ur_test+= is_exclusive="true"
+TEST_METADATA.pmc_cli_test+= is_exclusive="true"
+TEST_METADATA.pmc_pidtrack_test+= is_exclusive="true"
+TEST_METADATA.pmc_rapl_test+= is_exclusive="true"
+TEST_METADATA.pmcstat_tsc_test+= required_kmods="hwpmc"
LIBADD+= pmc
LIBADD.pmc_detach_test+= pthread
diff --git a/tests/sys/pmc/amd/Makefile b/tests/sys/pmc/amd/Makefile
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/Makefile
@@ -0,0 +1,18 @@
+TESTSDIR= ${TESTSBASE}/sys/pmc/amd
+
+ATF_TESTS_C+= pmu_detect_test
+ATF_TESTS_C+= pmu_event_neg_test
+ATF_TESTS_C+= pmu_event_test
+ATF_TESTS_C+= pmu_mux_test
+ATF_TESTS_C+= rapl_domains_test
+ATF_TESTS_SH+= pmc_frontend_test
+
+# These need the counters of cpu0 to themselves.
+TEST_METADATA.pmu_event_neg_test+= is_exclusive="true"
+TEST_METADATA.pmu_event_test+= is_exclusive="true"
+TEST_METADATA.pmu_mux_test+= is_exclusive="true"
+TEST_METADATA.rapl_domains_test+= is_exclusive="true"
+
+LIBADD+= pmc
+
+.include <bsd.test.mk>
diff --git a/tests/sys/pmc/amd/amd_pmc_test_common.h b/tests/sys/pmc/amd/amd_pmc_test_common.h
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/amd_pmc_test_common.h
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Author: Davi C.A <davi.chavesazevedo@amd.com>
+ */
+
+/*
+ * Common helpers for the AMD hwpmc tests.
+ */
+
+#ifndef _AMD_PMC_TEST_COMMON_H_
+#define _AMD_PMC_TEST_COMMON_H_
+
+#include <sys/param.h>
+#include <sys/cpuset.h>
+#include <machine/cpufunc.h>
+#include <machine/specialreg.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define AMD_CPUID_VENDOR_EBX 0x68747541U /* "Auth" */
+#define AMD_CPUID_VENDOR_EDX 0x69746e65U /* "enti" */
+#define AMD_CPUID_VENDOR_ECX 0x444d4163U /* "cAMD" */
+
+/* Ordered oldest to newest so that generations can be compared. */
+enum amd_zen {
+ AMD_ZEN_PRE_ZEN = 0,
+ AMD_ZEN_1,
+ AMD_ZEN_PLUS,
+ AMD_ZEN_2,
+ AMD_ZEN_3,
+ AMD_ZEN_3_PLUS,
+ AMD_ZEN_4,
+ AMD_ZEN_5,
+ AMD_ZEN_6,
+ AMD_ZEN_FUTURE,
+ AMD_ZEN_UNKNOWN
+};
+
+struct amd_test_cpu {
+ uint32_t family;
+ uint32_t model;
+ enum amd_zen zen;
+};
+
+static inline const char *
+amd_test_zen_name(enum amd_zen zen)
+{
+ switch (zen) {
+ case AMD_ZEN_PRE_ZEN:
+ return ("pre-Zen");
+ case AMD_ZEN_1:
+ return ("Zen 1");
+ case AMD_ZEN_PLUS:
+ return ("Zen+");
+ case AMD_ZEN_2:
+ return ("Zen 2");
+ case AMD_ZEN_3:
+ return ("Zen 3");
+ case AMD_ZEN_3_PLUS:
+ return ("Zen 3+");
+ case AMD_ZEN_4:
+ return ("Zen 4");
+ case AMD_ZEN_5:
+ return ("Zen 5");
+ case AMD_ZEN_6:
+ return ("Zen 6");
+ case AMD_ZEN_FUTURE:
+ return ("post-Zen 6");
+ default:
+ return ("unknown");
+ }
+}
+
+static inline enum amd_zen
+amd_test_map_zen(uint32_t family, uint32_t model)
+{
+ if (family < 0x17)
+ return (AMD_ZEN_PRE_ZEN);
+ if (family > 0x1a)
+ return (AMD_ZEN_FUTURE);
+
+ switch (family) {
+ case 0x17:
+ if (model >= 0x08 && model <= 0x0f) /* Pinnacle Ridge */
+ return (AMD_ZEN_PLUS);
+ if ((model >= 0x01 && model <= 0x07) || /* Summit Ridge */
+ (model >= 0x10 && model <= 0x17) || /* Raven Ridge */
+ (model >= 0x20 && model <= 0x2f)) /* Dali */
+ return (AMD_ZEN_1);
+ if (model >= 0x18 && model <= 0x1f) /* Picasso */
+ return (AMD_ZEN_PLUS);
+ if ((model >= 0x31 && model <= 0x3f) || /* Rome */
+ (model >= 0x60 && model <= 0x6f) || /* Renoir */
+ (model >= 0x71 && model <= 0x7f) || /* Matisse */
+ (model >= 0x90 && model <= 0x9f) || /* Van Gogh */
+ (model >= 0xa0 && model <= 0xaf)) /* Mendocino */
+ return (AMD_ZEN_2);
+ break;
+ case 0x19:
+ if ((model >= 0x10 && model <= 0x1f) || /* Genoa, Storm Peak */
+ (model >= 0x60 && model <= 0x7f) || /* Raphael, Phoenix */
+ (model >= 0xa0 && model <= 0xaf)) /* Bergamo, Siena */
+ return (AMD_ZEN_4);
+ if (model >= 0x40 && model <= 0x4f) /* Rembrandt */
+ return (AMD_ZEN_3_PLUS);
+ if (model <= 0x0f || /* Milan */
+ (model >= 0x20 && model <= 0x2f) || /* Vermeer */
+ (model >= 0x50 && model <= 0x5f)) /* Cezanne */
+ return (AMD_ZEN_3);
+ break;
+ case 0x1a:
+ if ((model >= 0x50 && model <= 0x5f) || /* Medusa */
+ (model >= 0x80 && model <= 0xaf) ||
+ (model >= 0xc0 && model <= 0xcf) ||
+ (model >= 0xd8 && model <= 0xef))
+ return (AMD_ZEN_6);
+ /* Turin, Strix, Krackan, Fire Range */
+ if (model <= 0x2f || (model >= 0x40 && model <= 0x4f) ||
+ (model >= 0x60 && model <= 0x7f) ||
+ (model >= 0xd0 && model <= 0xd7))
+ return (AMD_ZEN_5);
+ break;
+ }
+
+ return (AMD_ZEN_UNKNOWN);
+}
+
+static inline bool
+amd_test_is_amd(void)
+{
+ u_int regs[4];
+
+ do_cpuid(0, regs);
+ return (regs[1] == AMD_CPUID_VENDOR_EBX &&
+ regs[3] == AMD_CPUID_VENDOR_EDX &&
+ regs[2] == AMD_CPUID_VENDOR_ECX);
+}
+
+static inline void
+amd_test_skip_unless_amd(void)
+{
+ if (!amd_test_is_amd())
+ atf_tc_skip("CPU vendor is not AuthenticAMD");
+}
+
+/* Skip unless this is an AMD CPU of a known Zen generation. */
+static inline void
+amd_test_skip_unless_zen(struct amd_test_cpu *cpu)
+{
+ u_int regs[4];
+
+ amd_test_skip_unless_amd();
+
+ do_cpuid(1, regs);
+ cpu->family = (regs[0] >> 8) & 0x0f;
+ cpu->model = (regs[0] >> 4) & 0x0f;
+ if (cpu->family == 0x0f) {
+ cpu->family += (regs[0] >> 20) & 0xff;
+ cpu->model |= ((regs[0] >> 16) & 0x0f) << 4;
+ }
+ cpu->zen = amd_test_map_zen(cpu->family, cpu->model);
+
+ if (cpu->zen == AMD_ZEN_PRE_ZEN)
+ atf_tc_skip("AMD family %02xh predates Zen", cpu->family);
+ if (cpu->zen == AMD_ZEN_FUTURE || cpu->zen == AMD_ZEN_UNKNOWN)
+ atf_tc_skip("AMD family %02xh model %02xh is not in the "
+ "Zen map", cpu->family, cpu->model);
+}
+
+/* Without PerfCtrExtCore (e.g. some guests) hwpmc exposes 4 core counters. */
+static inline void
+amd_test_skip_unless_perfctr_extcore(void)
+{
+ u_int regs[4];
+
+ do_cpuid(0x80000001, regs);
+ if ((regs[2] & AMDID2_PCXC) == 0)
+ atf_tc_skip("CPU lacks PerfCtrExtCore; only 4 core counters");
+}
+
+/*
+ * The number of core counters, computed as hwpmc_amd.c does: Fn8000_0022
+ * EBX[3:0] when that leaf reports counters, otherwise 6 with PerfCtrExtCore
+ * and 4 without.
+ */
+static inline int
+amd_test_core_counters(void)
+{
+ u_int regs[4];
+ int n;
+
+ do_cpuid(0x80000001, regs);
+ n = (regs[2] & AMDID2_PCXC) != 0 ? 6 : 4;
+ do_cpuid(0x80000000, regs);
+ if (regs[0] >= 0x80000022) {
+ do_cpuid(0x80000022, regs);
+ if (regs[1] != 0)
+ n = regs[1] & 0x0f;
+ }
+ return (n);
+}
+
+static inline void
+amd_test_skip_unless_hwpmc(void)
+{
+ if (pmc_init() < 0)
+ atf_tc_skip("pmc_init() failed: %s", strerror(errno));
+}
+
+/* Bind the calling thread to cpu0, where the system-mode PMCs count. */
+static inline void
+amd_test_bind_cpu0(void)
+{
+ cpuset_t mask;
+
+ CPU_ZERO(&mask);
+ CPU_SET(0, &mask);
+ if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1,
+ sizeof(mask), &mask) != 0)
+ atf_tc_skip("cannot bind to cpu0: %s", strerror(errno));
+}
+
+static inline const struct pmc_classinfo *
+amd_test_find_classinfo(const struct pmc_cpuinfo *cpuinfo, enum pmc_class cl)
+{
+ uint32_t i;
+
+ for (i = 0; i < cpuinfo->pm_nclass; i++)
+ if (cpuinfo->pm_classes[i].pm_class == cl)
+ return (&cpuinfo->pm_classes[i]);
+
+ return (NULL);
+}
+
+static inline size_t
+amd_test_count_pmc_rows_with_prefix(int cpu, const char *prefix)
+{
+ struct pmc_pmcinfo *pmcinfo;
+ size_t count;
+ int i, npmc;
+
+ count = 0;
+ ATF_REQUIRE_MSG(pmc_pmcinfo(cpu, &pmcinfo) == 0,
+ "pmc_pmcinfo(%d) failed: %s", cpu, strerror(errno));
+
+ npmc = pmc_npmc(cpu);
+ if (npmc < 0) {
+ free(pmcinfo);
+ atf_tc_fail("pmc_npmc(%d) failed: %s", cpu, strerror(errno));
+ }
+
+ for (i = 0; i < npmc; i++) {
+ if (strncmp(pmcinfo->pm_pmcs[i].pm_name, prefix,
+ strlen(prefix)) == 0)
+ count++;
+ }
+
+ free(pmcinfo);
+ return (count);
+}
+
+/*
+ * Count the AMD core counter rows ("K8-<n>") on a CPU. The L3, DF and UMC
+ * rows share the K8 class and prefix ("K8-L3-", "K8-DF-", "K8-UMC") and are
+ * not core counters. With free_only, count only rows that are free.
+ */
+static inline int
+amd_test_count_core_rows(int cpu, bool free_only)
+{
+ struct pmc_pmcinfo *pmcinfo;
+ const struct pmc_info *p;
+ int count, i, npmc;
+
+ ATF_REQUIRE_MSG(pmc_pmcinfo(cpu, &pmcinfo) == 0,
+ "pmc_pmcinfo(%d) failed: %s", cpu, strerror(errno));
+
+ npmc = pmc_npmc(cpu);
+ if (npmc < 0) {
+ free(pmcinfo);
+ atf_tc_fail("pmc_npmc(%d) failed: %s", cpu, strerror(errno));
+ }
+
+ count = 0;
+ for (i = 0; i < npmc; i++) {
+ p = &pmcinfo->pm_pmcs[i];
+ if (strncmp(p->pm_name, "K8-", 3) != 0 ||
+ p->pm_name[3] < '0' || p->pm_name[3] > '9')
+ continue;
+ if (free_only && p->pm_rowdisp != PMC_DISP_FREE)
+ continue;
+ count++;
+ }
+
+ free(pmcinfo);
+ return (count);
+}
+
+static inline void
+amd_test_release_pmc(pmc_id_t pmcid)
+{
+ if (pmcid != PMC_ID_INVALID)
+ ATF_REQUIRE_MSG(pmc_release(pmcid) == 0,
+ "pmc_release(%u) failed: %s", pmcid, strerror(errno));
+}
+
+#endif /* _AMD_PMC_TEST_COMMON_H_ */
diff --git a/tests/sys/pmc/amd/pmc_frontend_test.sh b/tests/sys/pmc/amd/pmc_frontend_test.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/pmc_frontend_test.sh
@@ -0,0 +1,54 @@
+#
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+#
+# pmc(8) frontend study, which records AMD IBS fetch samples.
+#
+
+atf_test_case pmc_frontend_ibs
+pmc_frontend_ibs_head()
+{
+ atf_set "descr" \
+ "pmc record -s frontend captures IBS fetch samples and " \
+ "pmc frontend analyzes the log"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmc pmccontrol"
+ atf_set "timeout" "60"
+}
+pmc_frontend_ibs_body()
+{
+ local load rc
+
+ sysctl -n kern.hwpmc.cpuid > /dev/null 2>&1 || \
+ atf_skip "hwpmc(4) is not loaded"
+ pmccontrol -l | grep -q 'IBS-FETCH' || \
+ atf_skip "hwpmc registered no IBS fetch row on this host"
+
+ # Keep a CPU busy so that samples are taken while recording.
+ sh -c 'i=0; while [ $i -lt 3000000 ]; do i=$((i + 1)); done' &
+ load=$!
+ # pmc record returns kevent()'s result (1) when the -t timer ends
+ # the capture and 0 when it is interrupted.
+ pmc record -s frontend -t 3 frontend.pmc > record.out 2>&1
+ rc=$?
+ kill "$load" 2>/dev/null
+ wait
+ [ "$rc" -le 1 ] || \
+ atf_fail "pmc record -s frontend failed: $(cat record.out)"
+
+ # Each decoded sample row of the analysis carries miss percentages.
+ atf_check -s exit:0 -o save:frontend.txt -e ignore \
+ pmc frontend frontend.pmc
+ grep -q '[0-9]%' frontend.txt || \
+ atf_fail "pmc frontend decoded no IBS fetch sample"
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case pmc_frontend_ibs
+}
diff --git a/tests/sys/pmc/amd/pmu_detect_test.c b/tests/sys/pmc/amd/pmu_detect_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/pmu_detect_test.c
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Checks that hwpmc registers the AMD core PMU on Zen CPUs: the K8 class is
+ * present with 48-bit counters and at least six core rows, alongside the
+ * TSC class.
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+
+/* Every Zen generation has at least six core counters. */
+#define AMD_CORE_PMC_MIN 6
+
+ATF_TC(pmu_detect_k8_class_present);
+ATF_TC_HEAD(pmu_detect_k8_class_present, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that PMC_CLASS_K8 is listed in pmc_cpuinfo() on Zen.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_k8_class_present, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ struct amd_test_cpu cpu;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+ ATF_CHECK_MSG(amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8) != NULL,
+ "PMC_CLASS_K8 not registered on %s", amd_test_zen_name(cpu.zen));
+}
+
+ATF_TC(pmu_detect_counter_width);
+ATF_TC_HEAD(pmu_detect_counter_width, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that the AMD core counters are reported as 48 bits wide.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_counter_width, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ const struct pmc_classinfo *ci;
+ struct amd_test_cpu cpu;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+ ci = amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8);
+ ATF_REQUIRE_MSG(ci != NULL, "PMC_CLASS_K8 not registered");
+
+ ATF_CHECK_MSG(ci->pm_width == 48,
+ "AMD core counter width is %u bits, expected 48", ci->pm_width);
+}
+
+ATF_TC(pmu_detect_core_pmc_count);
+ATF_TC_HEAD(pmu_detect_core_pmc_count, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that cpu0 exposes at least "
+ __XSTRING(AMD_CORE_PMC_MIN) " AMD core counter rows.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_core_pmc_count, tc)
+{
+ struct amd_test_cpu cpu;
+ int n;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_perfctr_extcore();
+ amd_test_skip_unless_hwpmc();
+
+ n = amd_test_count_core_rows(0, false);
+ ATF_CHECK_MSG(n >= AMD_CORE_PMC_MIN,
+ "expected at least %d core counter rows on cpu0 (%s), got %d",
+ AMD_CORE_PMC_MIN, amd_test_zen_name(cpu.zen), n);
+}
+
+ATF_TC(pmu_detect_tsc_class_coexists);
+ATF_TC_HEAD(pmu_detect_tsc_class_coexists, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Verify that PMC_CLASS_TSC and PMC_CLASS_K8 are both registered.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_detect_tsc_class_coexists, tc)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ struct amd_test_cpu cpu;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+
+ ATF_REQUIRE_MSG(pmc_cpuinfo(&cpuinfo) == 0,
+ "pmc_cpuinfo() failed: %s", strerror(errno));
+ ATF_CHECK_MSG(amd_test_find_classinfo(cpuinfo, PMC_CLASS_K8) != NULL,
+ "PMC_CLASS_K8 not registered");
+ ATF_CHECK_MSG(amd_test_find_classinfo(cpuinfo, PMC_CLASS_TSC) != NULL,
+ "PMC_CLASS_TSC not registered");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_detect_k8_class_present);
+ ATF_TP_ADD_TC(tp, pmu_detect_counter_width);
+ ATF_TP_ADD_TC(tp, pmu_detect_core_pmc_count);
+ ATF_TP_ADD_TC(tp, pmu_detect_tsc_class_coexists);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/amd/pmu_event_neg_test.c b/tests/sys/pmc/amd/pmu_event_neg_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/pmu_event_neg_test.c
@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * These tests issue PMC_OP_PMCALLOCATE directly, as libpmc does, with
+ * config words that libpmc would never build, to exercise the config
+ * validation in hwpmc_amd.c.
+ *
+ * Kernel validation path, amd_can_assign_pmc():
+ * config = a->pm_md.pm_amd.pm_amd_config;
+ * if ((config & ~amd_config_mask(subclass, caps)) != 0)
+ * return (EINVAL);
+ *
+ * For the CORE sub-class amd_config_mask() is AMD_VALID_BITS (CounterMask,
+ * Invert, Enable, Int, PC, Edge, OS, USR, UnitMask, EventMask) on every
+ * family, plus PRECISERETIRE (bit 43) only with PMC_CAP_PRECISE, plus the
+ * kern.hwpmc.amd_core_extra_mask tunable. HOST (bit 41), GUEST (bit 40)
+ * and bits 44 and up must therefore be rejected.
+ */
+
+#include <sys/param.h>
+#include <sys/module.h>
+#include <sys/pmc.h>
+#include <sys/sysctl.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "amd_pmc_test_common.h"
+
+/* Find the hwpmc syscall number the same way pmc_init() does. */
+static int
+neg_get_pmc_syscall(void)
+{
+ struct module_stat ms;
+ int mod_id;
+
+ mod_id = modfind(PMC_MODULE_NAME);
+ if (mod_id < 0)
+ return (-1);
+
+ ms.version = sizeof(ms);
+ if (modstat(mod_id, &ms) < 0)
+ return (-1);
+
+ return (ms.data.intval);
+}
+
+static int
+neg_raw_pmcallocate(int pmc_sc, struct pmc_op_pmcallocate *op)
+{
+ return (syscall(pmc_sc, PMC_OP_PMCALLOCATE, op));
+}
+
+static void
+neg_raw_pmcrelease(int pmc_sc, pmc_id_t pmcid)
+{
+ struct pmc_op_simple op;
+
+ memset(&op, 0, sizeof(op));
+ op.pm_pmcid = pmcid;
+ (void)syscall(pmc_sc, PMC_OP_PMCRELEASE, &op);
+}
+
+/*
+ * Skip unless the raw allocation can be tested, and return the syscall
+ * number together with a valid system-mode request for ex_ret_instr on
+ * cpu0 (the event is in every amdzen* table).
+ */
+static void
+neg_setup(int *sc, struct pmc_op_pmcallocate *op)
+{
+ struct amd_test_cpu cpu;
+ uint64_t extra;
+ size_t len;
+ int error;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+
+ len = sizeof(extra);
+ if (sysctlbyname("kern.hwpmc.amd_core_extra_mask", &extra, &len,
+ NULL, 0) == 0 && extra != 0)
+ atf_tc_skip("kern.hwpmc.amd_core_extra_mask=%#jx widens the "
+ "accepted CORE bits", (uintmax_t)extra);
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+
+ *sc = neg_get_pmc_syscall();
+ if (*sc < 0)
+ atf_tc_skip("cannot find the hwpmc syscall: %s",
+ strerror(errno));
+
+ memset(op, 0, sizeof(*op));
+ error = pmc_pmu_pmcallocate("ex_ret_instr", op);
+ if (error != 0)
+ atf_tc_skip("ex_ret_instr not resolvable: %s",
+ strerror(error));
+ op->pm_mode = PMC_MODE_SC;
+ op->pm_flags = PMC_F_EV_PMU;
+ op->pm_cpu = 0;
+ op->pm_pmcid = PMC_ID_INVALID;
+}
+
+/*
+ * Check that the request with bits added to its config word is refused
+ * with EINVAL. hwpmc also answers EINVAL when no row is free, so the
+ * unmodified request must be accepted first.
+ */
+static void
+neg_expect_rejected(uint64_t bits, const char *what)
+{
+ struct pmc_op_pmcallocate op, valid;
+ int sc;
+
+ neg_setup(&sc, &valid);
+
+ op = valid;
+ if (neg_raw_pmcallocate(sc, &op) != 0)
+ atf_tc_skip("the unmodified request is refused: %s",
+ strerror(errno));
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+
+ op = valid;
+ op.pm_md.pm_amd.pm_amd_config |= bits;
+ if (neg_raw_pmcallocate(sc, &op) == 0) {
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+ atf_tc_fail("config word with %s (0x%016jx) was accepted",
+ what, (uintmax_t)op.pm_md.pm_amd.pm_amd_config);
+ }
+ ATF_CHECK_MSG(errno == EINVAL,
+ "config word with %s: expected EINVAL, got %d (%s)",
+ what, errno, strerror(errno));
+}
+
+ATF_TC(pmu_neg_valid_config_accepted);
+ATF_TC_HEAD(pmu_neg_valid_config_accepted, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG/SANITY] The unmodified ex_ret_instr request is accepted by "
+ "the raw PMC_OP_PMCALLOCATE path used by the rejection tests.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_valid_config_accepted, tc)
+{
+ struct pmc_op_pmcallocate op;
+ int sc;
+
+ neg_setup(&sc, &op);
+ if (amd_test_count_core_rows(0, true) == 0)
+ atf_tc_skip("no free core counter on cpu0");
+
+ ATF_REQUIRE_MSG(neg_raw_pmcallocate(sc, &op) == 0,
+ "valid config word 0x%016jx was refused: %s",
+ (uintmax_t)op.pm_md.pm_amd.pm_amd_config, strerror(errno));
+ neg_raw_pmcrelease(sc, op.pm_pmcid);
+}
+
+ATF_TC(pmu_neg_host_bit_rejected);
+ATF_TC_HEAD(pmu_neg_host_bit_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] A CORE config word with HOST (bit 41) set is refused "
+ "with EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_host_bit_rejected, tc)
+{
+ neg_expect_rejected(AMD_PMC_HOST, "HOST (bit 41)");
+}
+
+ATF_TC(pmu_neg_guest_bit_rejected);
+ATF_TC_HEAD(pmu_neg_guest_bit_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] A CORE config word with GUEST (bit 40) set is refused "
+ "with EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_guest_bit_rejected, tc)
+{
+ neg_expect_rejected(AMD_PMC_GUEST, "GUEST (bit 40)");
+}
+
+ATF_TC(pmu_neg_host_and_guest_bits_rejected);
+ATF_TC_HEAD(pmu_neg_host_and_guest_bits_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] A CORE config word with both HOST and GUEST set is "
+ "refused with EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_host_and_guest_bits_rejected, tc)
+{
+ neg_expect_rejected(AMD_PMC_HOST | AMD_PMC_GUEST, "HOST|GUEST");
+}
+
+ATF_TC(pmu_neg_reserved_bits_rejected);
+ATF_TC_HEAD(pmu_neg_reserved_bits_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] A CORE config word with reserved bit 44 set is refused "
+ "with EINVAL.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_reserved_bits_rejected, tc)
+{
+ neg_expect_rejected(1ULL << 44, "reserved bit 44");
+}
+
+ATF_TC(pmu_neg_preciseretire_without_cap_rejected);
+ATF_TC_HEAD(pmu_neg_preciseretire_without_cap_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[NEG] A CORE config word with PRECISERETIRE (bit 43) set is "
+ "refused with EINVAL when PMC_CAP_PRECISE is not requested.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_neg_preciseretire_without_cap_rejected, tc)
+{
+ /* libpmc requests only PMC_CAP_READ | PMC_CAP_WRITE. */
+ neg_expect_rejected(AMD_PMC_PRECISERETIRE,
+ "PRECISERETIRE (bit 43) without PMC_CAP_PRECISE");
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_neg_valid_config_accepted);
+ ATF_TP_ADD_TC(tp, pmu_neg_host_bit_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_guest_bit_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_host_and_guest_bits_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_reserved_bits_rejected);
+ ATF_TP_ADD_TC(tp, pmu_neg_preciseretire_without_cap_rejected);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/amd/pmu_event_test.c b/tests/sys/pmc/amd/pmu_event_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/pmu_event_test.c
@@ -0,0 +1,481 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ * Author: Davi C.A <davi.chavesazevedo@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * [UNIT] No hardware: AMD_PMC_TO_EVENTMASK splits a 12-bit EventSelect
+ * into config word bits 7:0 and 35:32.
+ *
+ * [SMOKE] pmc_pmu_pmcallocate() lookup by name for well-known core
+ * events of the running Zen generation. Verifies EventSelect
+ * bits 7:0 + 35:32, UnitMask bits 15:8, and OsUser bits 17:16
+ * in the returned config word.
+ *
+ * [E2E] Count ex_ret_instr on cpu0 across a known loop.
+ *
+ * Reference:
+ * AMD PPR for Family 19h Model 11h: PERF_CTL MSR bit layout:
+ * bits 7:0 EventSelect[7:0]
+ * bits 15:8 UnitMask[7:0]
+ * bit 16 USR (count in user mode)
+ * bit 17 OS (count in OS/kernel mode)
+ * bits 35:32 EventSelect[11:8]
+ * bit 40 GUEST (count in guest mode)
+ * bit 41 HOST (count in host mode)
+ *
+ * hwpmc macros (sys/dev/hwpmc/hwpmc_amd.h):
+ * AMD_PMC_TO_EVENTMASK(x) = ((x) & 0xFF) | (((uint64_t)(x) & 0xF00) << 24)
+ * AMD_PMC_TO_UNITMASK(x) = ((x) << 8) & 0xFF00
+ * AMD_PMC_USR = (1 << 16)
+ * AMD_PMC_OS = (1 << 17)
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <dev/hwpmc/hwpmc_amd.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+
+/* EventSelect[7:0] from config word bits 7:0 */
+#define EVTEST_EVSEL_LO(cfg) ((uint32_t)((cfg) & 0xFFU))
+/* EventSelect[11:8] from config word bits 35:32 */
+#define EVTEST_EVSEL_HI(cfg) ((uint32_t)(((cfg) >> 24) & 0xF00U))
+/* Full 12-bit EventSelect */
+#define EVTEST_EVSEL(cfg) (EVTEST_EVSEL_LO(cfg) | EVTEST_EVSEL_HI(cfg))
+/* UnitMask[7:0] from config word bits 15:8 */
+#define EVTEST_UMASK(cfg) ((uint32_t)(((cfg) >> 8) & 0xFFU))
+/* USR bit (bit 16) */
+#define EVTEST_USR(cfg) (((cfg) >> 16) & 1U)
+/* OS bit (bit 17) */
+#define EVTEST_OS(cfg) (((cfg) >> 17) & 1U)
+
+/*
+ * Core events and the EventCode/UMask the pmu-events tables in
+ * lib/libpmc/pmu-events/arch/x86/amdzen* give for them. umask 0 means
+ * the table has no UMask for the event. The tables differ between
+ * generations (e.g. ls_mab_alloc.all_allocations is 0x7f on Zen 3/4,
+ * 0x0f on Zen 5 and absent on Zen 1/2), so expectations are per table.
+ */
+struct evtest_event {
+ const char *name;
+ uint32_t event_code;
+ uint32_t umask;
+};
+
+/* amdzen1, amdzen2 */
+static const struct evtest_event zen1_events[] = {
+ { "ex_ret_instr", 0xc0, 0x00 },
+ { "ls_not_halted_cyc", 0x76, 0x00 },
+ { "ex_ret_brn", 0xc2, 0x00 },
+ { "ex_ret_brn_misp", 0xc3, 0x00 },
+ { NULL, 0, 0 }
+};
+
+/* amdzen3 */
+static const struct evtest_event zen3_events[] = {
+ { "ex_ret_instr", 0xc0, 0x00 },
+ { "ls_not_halted_cyc", 0x76, 0x00 },
+ { "ex_ret_brn", 0xc2, 0x00 },
+ { "ex_ret_brn_misp", 0xc3, 0x00 },
+ { "ls_mab_alloc.all_allocations", 0x41, 0x7f },
+ { "ex_ret_msprd_brnch_instr_dir_msmtch", 0x1c7, 0x00 },
+ { NULL, 0, 0 }
+};
+
+/* amdzen4 */
+static const struct evtest_event zen4_events[] = {
+ { "ex_ret_instr", 0xc0, 0x00 },
+ { "ls_not_halted_cyc", 0x76, 0x00 },
+ { "ex_ret_brn", 0xc2, 0x00 },
+ { "ex_ret_brn_misp", 0xc3, 0x00 },
+ { "ls_mab_alloc.all_allocations", 0x41, 0x7f },
+ { "l2_request_g1.all", 0x60, 0xff },
+ { "ex_ret_msprd_brnch_instr_dir_msmtch", 0x1c7, 0x00 },
+ { NULL, 0, 0 }
+};
+
+/*
+ * amdzen5 and amdzen6. mapfile.csv sends some Zen 5 models to amdzen6
+ * (D0h-D7h, and 00h-0Fh in older versions), so only events with the same
+ * values in both tables are listed.
+ */
+static const struct evtest_event zen5_events[] = {
+ { "ex_ret_instr", 0xc0, 0x00 },
+ { "ls_not_halted_cyc", 0x76, 0x00 },
+ { "ex_ret_brn", 0xc2, 0x00 },
+ { "ex_ret_brn_misp", 0xc3, 0x00 },
+ { "l2_request_g1.all", 0x60, 0xf7 },
+ { NULL, 0, 0 }
+};
+
+static const struct evtest_event *
+evtest_events_for(enum amd_zen zen)
+{
+ switch (zen) {
+ case AMD_ZEN_1:
+ case AMD_ZEN_PLUS:
+ case AMD_ZEN_2:
+ return (zen1_events);
+ case AMD_ZEN_3:
+ case AMD_ZEN_3_PLUS:
+ return (zen3_events);
+ case AMD_ZEN_4:
+ return (zen4_events);
+ case AMD_ZEN_5:
+ case AMD_ZEN_6:
+ return (zen5_events);
+ default:
+ return (NULL);
+ }
+}
+
+/*
+ * Skip unless this is a Zen CPU with hwpmc and pmu-events available, and
+ * return the expected core events for it.
+ */
+static const struct evtest_event *
+evtest_skip_unless_ready(struct amd_test_cpu *cpu)
+{
+ const struct evtest_event *events;
+
+ amd_test_skip_unless_zen(cpu);
+ amd_test_skip_unless_hwpmc();
+
+ events = evtest_events_for(cpu->zen);
+ if (events == NULL)
+ atf_tc_skip("no core event expectations for %s",
+ amd_test_zen_name(cpu->zen));
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+ return (events);
+}
+
+/* ================================================================
+ * UNIT TESTS: no hardware, no pmc_init()
+ * ================================================================ */
+
+ATF_TC(pmu_event_unit_evsel_hi_encoding);
+ATF_TC_HEAD(pmu_event_unit_evsel_hi_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[UNIT] Verify AMD_PMC_TO_EVENTMASK splits a 12-bit EventSelect "
+ "into config word bits 7:0 and 35:32.");
+}
+
+ATF_TC_BODY(pmu_event_unit_evsel_hi_encoding, tc)
+{
+ uint64_t cfg;
+
+ cfg = AMD_PMC_TO_EVENTMASK(0x123);
+ ATF_CHECK_MSG(EVTEST_EVSEL_LO(cfg) == 0x23,
+ "EventSelect[7:0] of 0x123 is 0x%02x, expected 0x23",
+ EVTEST_EVSEL_LO(cfg));
+ ATF_CHECK_MSG(EVTEST_EVSEL_HI(cfg) == 0x100,
+ "EventSelect[11:8] of 0x123 is 0x%03x, expected 0x100",
+ EVTEST_EVSEL_HI(cfg));
+ ATF_CHECK_MSG((cfg & ~(0xffULL | 0xfULL << 32)) == 0,
+ "0x123 sets bits outside 7:0 and 35:32 (config=0x%016jx)",
+ (uintmax_t)cfg);
+}
+
+/* ================================================================
+ * SMOKE TESTS: require AMD CPU, hwpmc loaded, pmu-events JSON
+ * ================================================================ */
+
+ATF_TC(pmu_event_smoke_pmu_enabled);
+ATF_TC_HEAD(pmu_event_smoke_pmu_enabled, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify pmc_pmu_enabled() returns true on Zen CPUs, "
+ "which all have a pmu-events table in libpmc.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_pmu_enabled, tc)
+{
+ struct amd_test_cpu cpu;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+
+ ATF_CHECK_MSG(pmc_pmu_enabled(),
+ "pmc_pmu_enabled() is false on %s (family %02xh model %02xh)",
+ amd_test_zen_name(cpu.zen), cpu.family, cpu.model);
+}
+
+ATF_TC(pmu_event_smoke_lookup_by_name);
+ATF_TC_HEAD(pmu_event_smoke_lookup_by_name, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify that every expected core event name for this "
+ "Zen generation resolves via pmc_pmu_pmcallocate().");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_lookup_by_name, tc)
+{
+ const struct evtest_event *events, *ev;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_test_cpu cpu;
+ int error;
+
+ events = evtest_skip_unless_ready(&cpu);
+
+ for (ev = events; ev->name != NULL; ev++) {
+ memset(&cfg, 0, sizeof(cfg));
+ error = pmc_pmu_pmcallocate(ev->name, &cfg);
+ ATF_CHECK_MSG(error == 0,
+ "%s: pmc_pmu_pmcallocate() failed on %s: %s",
+ ev->name, amd_test_zen_name(cpu.zen), strerror(error));
+ }
+}
+
+ATF_TC(pmu_event_smoke_evsel_encoding);
+ATF_TC_HEAD(pmu_event_smoke_evsel_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify EventSelect bits 7:0 and 35:32 in the hwpmc "
+ "config word match the pmu-events table event codes.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_evsel_encoding, tc)
+{
+ const struct evtest_event *events, *ev;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_test_cpu cpu;
+ uint64_t raw;
+ bool any_checked;
+
+ events = evtest_skip_unless_ready(&cpu);
+
+ any_checked = false;
+ for (ev = events; ev->name != NULL; ev++) {
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate(ev->name, &cfg) != 0)
+ continue;
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ any_checked = true;
+ ATF_CHECK_MSG(EVTEST_EVSEL(raw) == ev->event_code,
+ "%s: EventSelect mismatch: got 0x%03x expected 0x%03x "
+ "(config=0x%016jx)",
+ ev->name, EVTEST_EVSEL(raw), ev->event_code,
+ (uintmax_t)raw);
+ }
+
+ if (!any_checked)
+ atf_tc_fail("no expected core event resolved on %s",
+ amd_test_zen_name(cpu.zen));
+}
+
+ATF_TC(pmu_event_smoke_umask_encoding);
+ATF_TC_HEAD(pmu_event_smoke_umask_encoding, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify UnitMask bits 15:8 in the hwpmc config word "
+ "match the pmu-events table unit mask values.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_umask_encoding, tc)
+{
+ const struct evtest_event *events, *ev;
+ struct pmc_op_pmcallocate cfg;
+ struct amd_test_cpu cpu;
+ uint64_t raw;
+ bool any_checked;
+
+ events = evtest_skip_unless_ready(&cpu);
+
+ any_checked = false;
+ for (ev = events; ev->name != NULL; ev++) {
+ /* Events without a UMask in the table have nothing to check. */
+ if (ev->umask == 0x00)
+ continue;
+
+ memset(&cfg, 0, sizeof(cfg));
+ if (pmc_pmu_pmcallocate(ev->name, &cfg) != 0)
+ continue;
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ any_checked = true;
+ ATF_CHECK_MSG(EVTEST_UMASK(raw) == ev->umask,
+ "%s: UnitMask mismatch: got 0x%02x expected 0x%02x "
+ "(config=0x%016jx)",
+ ev->name, EVTEST_UMASK(raw), ev->umask, (uintmax_t)raw);
+ }
+
+ if (!any_checked)
+ atf_tc_skip("no expected core event with a UnitMask on %s",
+ amd_test_zen_name(cpu.zen));
+}
+
+ATF_TC(pmu_event_smoke_osuser_bits);
+ATF_TC_HEAD(pmu_event_smoke_osuser_bits, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify OsUser bits 17:16 (AMD_PMC_OS | AMD_PMC_USR) "
+ "are set in the default config word from pmc_pmu_pmcallocate(). "
+ "libpmc sets both bits when no explicit mode is requested.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_osuser_bits, tc)
+{
+ struct pmc_op_pmcallocate cfg;
+ struct amd_test_cpu cpu;
+ uint64_t raw;
+ int error;
+
+ (void)evtest_skip_unless_ready(&cpu);
+
+ memset(&cfg, 0, sizeof(cfg));
+ error = pmc_pmu_pmcallocate("ex_ret_instr", &cfg);
+ ATF_REQUIRE_MSG(error == 0, "ex_ret_instr not resolved: %s",
+ strerror(error));
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ ATF_CHECK_MSG(EVTEST_USR(raw) != 0,
+ "USR bit (bit 16) not set in default pmc_pmu_pmcallocate() config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+ ATF_CHECK_MSG(EVTEST_OS(raw) != 0,
+ "OS bit (bit 17) not set in default pmc_pmu_pmcallocate() config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+}
+
+ATF_TC(pmu_event_smoke_host_guest_bits_default);
+ATF_TC_HEAD(pmu_event_smoke_host_guest_bits_default, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[SMOKE] Verify HOST/GUEST bits 41:40 are NOT automatically set "
+ "in the default config word from pmc_pmu_pmcallocate(). "
+ "libpmc does not set these bits without an explicit request.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_smoke_host_guest_bits_default, tc)
+{
+ struct pmc_op_pmcallocate cfg;
+ struct amd_test_cpu cpu;
+ uint64_t raw;
+ int error;
+
+ (void)evtest_skip_unless_ready(&cpu);
+
+ memset(&cfg, 0, sizeof(cfg));
+ error = pmc_pmu_pmcallocate("ex_ret_instr", &cfg);
+ ATF_REQUIRE_MSG(error == 0, "ex_ret_instr not resolved: %s",
+ strerror(error));
+
+ raw = cfg.pm_md.pm_amd.pm_amd_config;
+ ATF_CHECK_MSG((raw & AMD_PMC_HOST) == 0,
+ "HOST bit (bit 41) unexpectedly set in default config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+ ATF_CHECK_MSG((raw & AMD_PMC_GUEST) == 0,
+ "GUEST bit (bit 40) unexpectedly set in default config "
+ "(config=0x%016jx)", (uintmax_t)raw);
+}
+
+/* ================================================================
+ * E2E TESTS: allocate, start, count, stop, verify counter moved
+ * ================================================================ */
+
+ATF_TC(pmu_event_e2e_counter_counts);
+ATF_TC_HEAD(pmu_event_e2e_counter_counts, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "[E2E] Bind to cpu0, count ex_ret_instr with a system-mode PMC "
+ "on cpu0 across a 1M-iteration loop, and verify at least 1M "
+ "retired instructions: confirms the event encoding reaches "
+ "the hardware.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_event_e2e_counter_counts, tc)
+{
+ struct amd_test_cpu cpu;
+ pmc_value_t before, after;
+ pmc_id_t pmcid;
+ volatile uint64_t sink;
+ uint64_t i;
+
+ (void)evtest_skip_unless_ready(&cpu);
+ amd_test_bind_cpu0();
+
+ /* hwpmc reports a full CPU as EINVAL, like a rejected encoding. */
+ if (amd_test_count_core_rows(0, true) == 0)
+ atf_tc_skip("no free core counter on cpu0");
+ pmcid = PMC_ID_INVALID;
+ if (pmc_allocate("ex_ret_instr", PMC_MODE_SC, 0, 0, &pmcid, 1) != 0)
+ atf_tc_fail("pmc_allocate(ex_ret_instr, SC) failed: %s",
+ strerror(errno));
+
+ if (pmc_start(pmcid) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_start() failed: %s", strerror(errno));
+ }
+
+ if (pmc_read(pmcid, &before) != 0) {
+ (void)pmc_stop(pmcid);
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(before) failed: %s", strerror(errno));
+ }
+
+ sink = 0;
+ for (i = 0; i < 1000000UL; i++)
+ sink += i * 3 + 7;
+
+ if (pmc_stop(pmcid) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_stop() failed: %s", strerror(errno));
+ }
+
+ if (pmc_read(pmcid, &after) != 0) {
+ amd_test_release_pmc(pmcid);
+ atf_tc_fail("pmc_read(after) failed: %s", strerror(errno));
+ }
+
+ amd_test_release_pmc(pmcid);
+
+ ATF_CHECK_MSG(after >= before && after - before >= 1000000,
+ "ex_ret_instr counted fewer than 1M instructions over a "
+ "1M-iteration loop: before=%ju after=%ju",
+ (uintmax_t)before, (uintmax_t)after);
+}
+
+/* ================================================================
+ * Test program entry
+ * ================================================================ */
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_event_unit_evsel_hi_encoding);
+
+ /* Smoke tests: require AMD + hwpmc + pmu-events */
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_pmu_enabled);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_lookup_by_name);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_evsel_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_umask_encoding);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_osuser_bits);
+ ATF_TP_ADD_TC(tp, pmu_event_smoke_host_guest_bits_default);
+
+ /* E2E tests: allocate counter, count instructions, verify */
+ ATF_TP_ADD_TC(tp, pmu_event_e2e_counter_counts);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/amd/pmu_mux_test.c b/tests/sys/pmc/amd/pmu_mux_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/pmu_mux_test.c
@@ -0,0 +1,280 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Core counter allocation: hwpmc exposes as many core counters as CPUID
+ * reports, all of them can be held at once, one more allocation is refused
+ * (hwpmc does not multiplex), and a released counter can be allocated
+ * again. Every counter uses ex_ret_instr, which all Zen event tables have.
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+
+#define CORE_EVENT "ex_ret_instr"
+/* CPUID Fn8000_0022 EBX[3:0] reports at most 15 core counters. */
+#define CORE_PMCS_MAX 15
+
+/* Skip unless the cases can run; return the number of core counters. */
+static int
+core_counters_or_skip(void)
+{
+ struct amd_test_cpu cpu;
+ int n;
+
+ amd_test_skip_unless_zen(&cpu);
+ amd_test_skip_unless_hwpmc();
+ if (!pmc_pmu_enabled())
+ atf_tc_skip("pmu-events not enabled");
+ n = amd_test_core_counters();
+ if (n == 0)
+ atf_tc_skip("CPUID reports no core counters");
+
+ return (n);
+}
+
+static void
+release_all(pmc_id_t *pmcids, int n)
+{
+ int i;
+
+ for (i = 0; i < n; i++) {
+ if (pmcids[i] != PMC_ID_INVALID) {
+ (void)pmc_release(pmcids[i]);
+ pmcids[i] = PMC_ID_INVALID;
+ }
+ }
+}
+
+/*
+ * Allocate n system-mode PMCs on cpu0. Skips unless all core rows are free;
+ * once they are, any failure to allocate is an error.
+ */
+static void
+hold_core_slots(pmc_id_t *pmcids, int n)
+{
+ int slot;
+
+ for (slot = 0; slot < n; slot++)
+ pmcids[slot] = PMC_ID_INVALID;
+
+ if (amd_test_count_core_rows(0, true) < n)
+ atf_tc_skip("not all core counters on cpu0 are free");
+
+ for (slot = 0; slot < n; slot++) {
+ if (pmc_allocate(CORE_EVENT, PMC_MODE_SC, 0, 0, &pmcids[slot],
+ 1) != 0) {
+ release_all(pmcids, slot);
+ atf_tc_fail("allocation %d of %d failed: %s", slot, n,
+ strerror(errno));
+ }
+ }
+}
+
+static void
+spin(uint64_t iters)
+{
+ volatile uint64_t sink;
+ uint64_t i;
+
+ sink = 0;
+ for (i = 0; i < iters; i++)
+ sink += i * 3 + 7;
+}
+
+ATF_TC(pmu_mux_slot_count);
+ATF_TC_HEAD(pmu_mux_slot_count, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "pmc_pmcinfo() reports as many core counter rows as CPUID");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_slot_count, tc)
+{
+ int n, rows;
+
+ n = core_counters_or_skip();
+
+ rows = amd_test_count_core_rows(0, false);
+ ATF_CHECK_MSG(rows == n,
+ "CPUID reports %d core counters, pmc_pmcinfo() has %d rows", n,
+ rows);
+}
+
+ATF_TC(pmu_mux_fill_all_slots);
+ATF_TC_HEAD(pmu_mux_fill_all_slots, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "With every core counter held on cpu0, a held counter still "
+ "counts a known loop, and releasing them frees every row");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_fill_all_slots, tc)
+{
+ pmc_id_t pmcids[CORE_PMCS_MAX];
+ pmc_value_t before, after;
+ int free_after, n, slot;
+
+ n = core_counters_or_skip();
+ amd_test_bind_cpu0();
+
+ hold_core_slots(pmcids, n);
+
+ for (slot = 0; slot < n; slot++) {
+ if (pmc_start(pmcids[slot]) != 0) {
+ release_all(pmcids, n);
+ atf_tc_fail("pmc_start(slot %d) failed: %s",
+ slot, strerror(errno));
+ }
+ }
+
+ before = after = 0;
+ if (pmc_read(pmcids[0], &before) != 0) {
+ release_all(pmcids, n);
+ atf_tc_fail("pmc_read(before) failed: %s", strerror(errno));
+ }
+ spin(2000000);
+ if (pmc_read(pmcids[0], &after) != 0) {
+ release_all(pmcids, n);
+ atf_tc_fail("pmc_read(after) failed: %s", strerror(errno));
+ }
+
+ for (slot = 0; slot < n; slot++) {
+ if (pmc_stop(pmcids[slot]) != 0) {
+ release_all(pmcids, n);
+ atf_tc_fail("pmc_stop(slot %d) failed: %s",
+ slot, strerror(errno));
+ }
+ }
+ release_all(pmcids, n);
+
+ ATF_CHECK_MSG(after >= before && after - before >= 2000000,
+ "%s counted fewer than 2M instructions over a 2M-iteration loop "
+ "with all %d core counters held: before=%ju after=%ju",
+ CORE_EVENT, n, (uintmax_t)before, (uintmax_t)after);
+
+ free_after = amd_test_count_core_rows(0, true);
+ ATF_CHECK_MSG(free_after == n,
+ "core rows not freed: expected %d free, got %d", n, free_after);
+}
+
+ATF_TC(pmu_mux_over_allocation_rejected);
+ATF_TC_HEAD(pmu_mux_over_allocation_rejected, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "With every core counter held, one more allocation is refused "
+ "with EINVAL and the held counters still start and stop");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_over_allocation_rejected, tc)
+{
+ pmc_id_t pmcids[CORE_PMCS_MAX];
+ pmc_id_t extra;
+ int n, slot;
+
+ n = core_counters_or_skip();
+ hold_core_slots(pmcids, n);
+
+ extra = PMC_ID_INVALID;
+ if (pmc_allocate(CORE_EVENT, PMC_MODE_SC, 0, 0, &extra, 1) == 0) {
+ (void)pmc_release(extra);
+ atf_tc_fail_nonfatal("one more core counter was allocated "
+ "while all %d were held", n);
+ } else {
+ /* hwpmc reports "no PMC row accepted the request" as EINVAL. */
+ ATF_CHECK_MSG(errno == EINVAL,
+ "the extra allocation failed with unexpected errno %d (%s)",
+ errno, strerror(errno));
+ }
+
+ for (slot = 0; slot < n; slot++) {
+ ATF_CHECK_MSG(pmc_start(pmcids[slot]) == 0,
+ "held slot %d failed pmc_start: %s",
+ slot, strerror(errno));
+ ATF_CHECK_MSG(pmc_stop(pmcids[slot]) == 0,
+ "held slot %d failed pmc_stop: %s",
+ slot, strerror(errno));
+ }
+ release_all(pmcids, n);
+}
+
+ATF_TC(pmu_mux_sequential_reuse);
+ATF_TC_HEAD(pmu_mux_sequential_reuse, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "With every core counter held, a released counter can be "
+ "allocated again and counts a known loop");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(pmu_mux_sequential_reuse, tc)
+{
+ pmc_id_t pmcids[CORE_PMCS_MAX];
+ pmc_id_t reused;
+ pmc_value_t val;
+ int n;
+
+ n = core_counters_or_skip();
+ amd_test_bind_cpu0();
+
+ hold_core_slots(pmcids, n);
+
+ ATF_REQUIRE_MSG(pmc_release(pmcids[0]) == 0,
+ "pmc_release(slot 0) failed: %s", strerror(errno));
+ pmcids[0] = PMC_ID_INVALID;
+
+ reused = PMC_ID_INVALID;
+ if (pmc_allocate(CORE_EVENT, PMC_MODE_SC, 0, 0, &reused, 1) != 0) {
+ release_all(pmcids, n);
+ atf_tc_fail("reallocating the released counter failed: %s",
+ strerror(errno));
+ }
+
+ val = 0;
+ if (pmc_start(reused) != 0) {
+ (void)pmc_release(reused);
+ release_all(pmcids, n);
+ atf_tc_fail("pmc_start(reused) failed: %s", strerror(errno));
+ }
+ spin(1000000);
+ if (pmc_stop(reused) != 0 || pmc_read(reused, &val) != 0) {
+ (void)pmc_release(reused);
+ release_all(pmcids, n);
+ atf_tc_fail("stop/read of the reused counter failed: %s",
+ strerror(errno));
+ }
+
+ (void)pmc_release(reused);
+ release_all(pmcids, n);
+
+ ATF_CHECK_MSG(val >= 1000000,
+ "reused counter counted %ju instructions over a 1M-iteration "
+ "loop", (uintmax_t)val);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pmu_mux_slot_count);
+ ATF_TP_ADD_TC(tp, pmu_mux_fill_all_slots);
+ ATF_TP_ADD_TC(tp, pmu_mux_over_allocation_rejected);
+ ATF_TP_ADD_TC(tp, pmu_mux_sequential_reuse);
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/amd/rapl_domains_test.c b/tests/sys/pmc/amd/rapl_domains_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/amd/rapl_domains_test.c
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * AMD RAPL domain layout: AMD has package and core energy MSRs
+ * (MSR_AMD_PKG_ENERGY_STATUS, MSR_AMD_CORE_ENERGY_STATUS) but no DRAM
+ * domain, so hwpmc_rapl.c registers at most the PKG and CORES rows. The
+ * vendor-neutral RAPL tests are in ../pmc_rapl_test.c.
+ */
+
+#include <sys/param.h>
+#include <sys/pmc.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <string.h>
+
+#include "amd_pmc_test_common.h"
+
+/* CPU the SC-mode RAPL PMCs are allocated on. */
+#define RAPL_CPU 0
+
+/* Return the PMC_CLASS_RAPL classinfo, or NULL if it is not registered. */
+static const struct pmc_classinfo *
+rapl_classinfo(void)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+
+ if (pmc_cpuinfo(&cpuinfo) != 0)
+ return (NULL);
+
+ return (amd_test_find_classinfo(cpuinfo, PMC_CLASS_RAPL));
+}
+
+/* Skip unless this is AMD with hwpmc and a registered RAPL class. */
+static void
+skip_unless_rapl(void)
+{
+ amd_test_skip_unless_amd();
+ amd_test_skip_unless_hwpmc();
+
+ if (rapl_classinfo() == NULL)
+ atf_tc_skip("PMC_CLASS_RAPL not registered: the RAPL energy "
+ "MSRs did not respond (VM or unsupported CPU)");
+}
+
+ATF_TC(rapl_metadata_names);
+ATF_TC_HEAD(rapl_metadata_names, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "On AMD the RAPL class has at most two rows, PKG and CORES "
+ "(there is no DRAM domain), all with the RAPL prefix.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_metadata_names, tc)
+{
+ const struct pmc_classinfo *ci;
+ size_t nrows;
+
+ skip_unless_rapl();
+
+ ci = rapl_classinfo();
+ ATF_REQUIRE(ci != NULL);
+ /* The driver registers only the rows whose MSR responds. */
+ ATF_CHECK_MSG(ci->pm_num >= 1 && ci->pm_num <= 2,
+ "AMD RAPL expected 1 or 2 rows (PKG, CORES), got %u",
+ ci->pm_num);
+
+ nrows = amd_test_count_pmc_rows_with_prefix(RAPL_CPU, "RAPL");
+ ATF_CHECK_MSG(nrows == (size_t)ci->pm_num,
+ "expected %u RAPL-prefixed rows in pmc_pmcinfo, got %zu",
+ ci->pm_num, nrows);
+}
+
+ATF_TC(rapl_no_dram_on_amd);
+ATF_TC_HEAD(rapl_no_dram_on_amd, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "AMD has no RAPL DRAM domain: allocating rapl-energy-dram fails "
+ "while rapl-energy-pkg can be allocated.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_no_dram_on_amd, tc)
+{
+ pmc_id_t id;
+
+ skip_unless_rapl();
+ if (amd_test_count_pmc_rows_with_prefix(RAPL_CPU,
+ "RAPL_ENERGY_PKG") == 0)
+ atf_tc_skip("the driver registered no RAPL_ENERGY_PKG row");
+
+ /* Show that RAPL allocation works before expecting a refusal. */
+ id = PMC_ID_INVALID;
+ if (pmc_allocate("rapl-energy-pkg", PMC_MODE_SC, 0, RAPL_CPU, &id,
+ 1) != 0)
+ atf_tc_fail("pmc_allocate(rapl-energy-pkg) failed: %s",
+ strerror(errno));
+ amd_test_release_pmc(id);
+
+ /*
+ * Either libpmc (no DRAM row on AMD) or the driver (wrong event for
+ * the row) may refuse the request; only the refusal is asserted.
+ */
+ id = PMC_ID_INVALID;
+ if (pmc_allocate("rapl-energy-dram", PMC_MODE_SC, 0, RAPL_CPU, &id,
+ 1) == 0) {
+ amd_test_release_pmc(id);
+ atf_tc_fail("pmc_allocate(rapl-energy-dram) succeeded on AMD");
+ }
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, rapl_metadata_names);
+ ATF_TP_ADD_TC(tp, rapl_no_dram_on_amd);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/pmc_callchain_ur_test.sh b/tests/sys/pmc/pmc_callchain_ur_test.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_callchain_ur_test.sh
@@ -0,0 +1,100 @@
+#
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+#
+# Regression test for two KASSERTs in pmc_capture_user_callchain() that
+# fired on valid samples: the TDP_CALLCHAIN check did not allow for the
+# PMC_UR ring, and the pm_runcount check ran before verifying that the
+# sample had not been dropped in the meantime. Both are reachable only
+# through the PMC_UR ring, which hwpmc uses for samples of
+# PMC_F_USERCALLCHAIN PMCs (pmcstat -U) taken while the thread is in the
+# kernel. An INVARIANTS kernel without the fixes panics on the first such
+# sample; on a kernel without INVARIANTS the test passes without proving
+# anything.
+#
+# A panic cannot be reported from inside the test, so the test asserts that
+# pmcstat -U exits cleanly and that kern.hwpmc.stats.merges, which counts
+# user callchains merged from the PMC_UR ring, increased during the run.
+#
+
+# Candidate sampling events, tried in order.
+UR_EVENTS="instructions unhalted-core-cycles ex_ret_instr ls_not_halted_cyc"
+
+# Sampling rate: small enough to collect many samples in a short workload.
+UR_RATE=100000
+
+# Write a CPU-bound workload that runs for the given number of seconds.
+ur_write_load()
+{
+ cat > ur_load.sh <<'EOF'
+#!/bin/sh
+end=$(($(date +%s) + ${1:-4}))
+x=0
+while [ $(date +%s) -lt $end ]; do
+ i=0
+ while [ $i -lt 100000 ]; do
+ x=$((x + i * 3 + 7))
+ i=$((i + 1))
+ done
+done
+EOF
+ chmod +x ur_load.sh
+}
+
+# Print the first event in UR_EVENTS that pmcstat can sample in process mode.
+# The probe runs without -U so that a kernel lacking the fixes is not
+# crashed before the real test starts.
+ur_pick_event()
+{
+ local ev
+
+ for ev in $UR_EVENTS; do
+ if pmcstat -P "$ev" -n "$UR_RATE" -O ur_probe.pmc \
+ -- /usr/bin/true >/dev/null 2>&1; then
+ printf '%s\n' "$ev"
+ return 0
+ fi
+ done
+ return 1
+}
+
+atf_test_case pmcstat_U_survives_and_samples
+pmcstat_U_survives_and_samples_head()
+{
+ atf_set "descr" \
+ "pmcstat -U collects user callchains through the PMC_UR ring " \
+ "without tripping the pmc_capture_user_callchain() KASSERTs"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmcstat"
+ atf_set "timeout" "60"
+}
+pmcstat_U_survives_and_samples_body()
+{
+ local after before ev
+
+ before=$(sysctl -n kern.hwpmc.stats.merges 2>/dev/null) || \
+ atf_skip "hwpmc(4) is not loaded"
+ ur_write_load
+ ev=$(ur_pick_event) || \
+ atf_skip "no process-mode sampling event allocatable" \
+ "(tried: $UR_EVENTS)"
+
+ atf_check -s exit:0 -o ignore -e ignore \
+ pmcstat -U -P "$ev" -n "$UR_RATE" -O ur_capture.pmc \
+ -- ./ur_load.sh 4
+
+ after=$(sysctl -n kern.hwpmc.stats.merges)
+ [ "$after" -gt "$before" ] || \
+ atf_fail "no user callchain went through the PMC_UR ring" \
+ "(kern.hwpmc.stats.merges stayed at $before)"
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case pmcstat_U_survives_and_samples
+}
diff --git a/tests/sys/pmc/pmc_cli_test.sh b/tests/sys/pmc/pmc_cli_test.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_cli_test.sh
@@ -0,0 +1,102 @@
+#
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+#
+# Smoke tests for the pmc(8) front end: error handling for a missing log,
+# list-events, and a record/info round trip with the "instructions" alias.
+# pmc(8) initializes libpmc before running any subcommand, so every case
+# needs hwpmc(4).
+#
+
+# Skip unless hwpmc(4) is loaded and libpmc has a pmu-events table for
+# this CPU.
+pmc_cli_setup()
+{
+ sysctl -n kern.hwpmc.cpuid > /dev/null 2>&1 || \
+ atf_skip "hwpmc(4) is not loaded"
+ [ -n "$(pmc list-events 2>/dev/null | head -1)" ] || \
+ atf_skip "libpmc has no pmu-events table for this CPU"
+}
+
+# Run pmc record, saving its output in record.out. pmc record returns
+# kevent()'s result (1) when the -t timer ends the capture and 0 when it is
+# interrupted; any other status means the capture failed.
+pmc_record()
+{
+ pmc record "$@" > record.out 2>&1
+ case $? in
+ 0|1) return 0 ;;
+ *) return 1 ;;
+ esac
+}
+
+atf_test_case pmc_info_missing_file
+pmc_info_missing_file_head()
+{
+ atf_set "descr" \
+ "pmc info on a missing file exits with EX_OSERR (71) and a " \
+ "'Cannot open' diagnostic"
+ atf_set "require.progs" "pmc"
+}
+pmc_info_missing_file_body()
+{
+ pmc_cli_setup
+ atf_check -s exit:71 -o ignore -e match:"Cannot open" \
+ pmc info ./nonexistent.pmc
+}
+
+atf_test_case pmc_list_events_present
+pmc_list_events_present_head()
+{
+ atf_set "descr" \
+ "pmc list-events prints the pmu-events table for this CPU"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmc"
+}
+pmc_list_events_present_body()
+{
+ pmc_cli_setup
+ atf_check -s exit:0 -o save:events.txt -e ignore pmc list-events
+ atf_check test "$(wc -l < events.txt)" -gt 10
+}
+
+atf_test_case pmc_record_then_info
+pmc_record_then_info_head()
+{
+ atf_set "descr" \
+ "pmc record samples the instructions event and pmc info " \
+ "reports a non-zero sample count for it"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmc"
+ atf_set "timeout" "30"
+}
+pmc_record_then_info_body()
+{
+ pmc_cli_setup
+
+ if ! pmc_record -c instructions -t 2 record.pmc; then
+ grep -q "Cannot allocate event" record.out && \
+ atf_skip "no instructions event on this CPU"
+ atf_fail "pmc record failed: $(cat record.out)"
+ fi
+
+ atf_check -s exit:0 -o save:info.txt -e ignore pmc info record.pmc
+ # The "Counter | Counts" table follows its header line; the alias is
+ # listed under the name libpmc resolved it to.
+ awk '/^Counter/ { t = 1; next }
+ t && $NF ~ /^[0-9]+$/ && $NF > 0 { found = 1 }
+ END { exit(found ? 0 : 1) }' info.txt || \
+ atf_fail "pmc info shows no samples: $(tail -5 info.txt)"
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case pmc_info_missing_file
+ atf_add_test_case pmc_list_events_present
+ atf_add_test_case pmc_record_then_info
+}
diff --git a/tests/sys/pmc/pmc_pidtrack_test.c b/tests/sys/pmc/pmc_pidtrack_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_pidtrack_test.c
@@ -0,0 +1,399 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Per-process tracking tests for process-mode (PMC_MODE_TC) counting PMCs.
+ * A process-scope counter must follow a single PID:
+ * - a process attached to its own PMC counts its own work, readable live
+ * via self-read (pidtrack_self_counts);
+ * - an idle process sharing a CPU with a busy sibling does not pick up the
+ * sibling's work (pidtrack_no_leakage);
+ * - the count is preserved and keeps advancing across the target being
+ * switched out and back in (pidtrack_ctxswitch).
+ *
+ * pmc_read() of a process-mode PMC attached to another live process only
+ * returns the value saved at the target's last switch-out, so each measured
+ * process attaches its own PMC and reports its own delta. The
+ * controller-attaches-child pattern is covered by pmc_wrap_test.
+ *
+ * Each test skips when no process-mode counting event can be allocated
+ * (hwpmc(4) not loaded, or a VM without a vPMU).
+ */
+
+#include <sys/param.h>
+#include <sys/cpuset.h>
+#include <sys/wait.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+static const char *const counting_events[] = {
+ "instructions",
+ "cycles",
+ "branches",
+ "unhalted-core-cycles",
+ "inst_retired.any",
+ "cpu_clk_unhalted.thread",
+ "ls_not_halted_cyc",
+ NULL
+};
+
+/*
+ * Instruction-retired events only. An idle process must not accumulate
+ * the event used by the no-leakage test, and cycle events tick while a
+ * process merely waits to run.
+ */
+static const char *const retired_events[] = {
+ "instructions",
+ "inst_retired.any",
+ "ex_ret_instr", /* AMD */
+ NULL
+};
+
+/* Spin iterations: a large, unambiguous count, still well under a second. */
+#define SPIN_ITERS ((uint64_t)200 * 1000 * 1000)
+
+/* Active work must move the counter by at least this much. */
+#define MIN_ACTIVE ((uint64_t)1 << 16)
+
+/* Return the name of the first allocatable event in list, or NULL. */
+static const char *
+find_event_in(const char *const *list)
+{
+ pmc_id_t id;
+ int i;
+
+ for (i = 0; list[i] != NULL; i++) {
+ id = PMC_ID_INVALID;
+ if (pmc_allocate(list[i], PMC_MODE_TC, 0,
+ PMC_CPU_ANY, &id, 0) == 0) {
+ (void)pmc_release(id);
+ return (list[i]);
+ }
+ }
+ return (NULL);
+}
+
+/* Skip unless hwpmc(4) works and an event from list can be allocated. */
+static const char *
+skip_unless_event(const char *const *list)
+{
+ const char *event;
+
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ event = find_event_in(list);
+ if (event == NULL)
+ atf_tc_skip("no allocatable process-mode counting event");
+ return (event);
+}
+
+static void __noinline
+spin_workload(void)
+{
+ volatile uint64_t sink;
+ uint64_t i;
+
+ sink = 0;
+ for (i = 0; i < SPIN_ITERS; i++)
+ sink += i * 3 + 7;
+}
+
+/* Set up a process-mode PMC for event on the calling process. */
+static int
+self_attach(const char *event, pmc_id_t *id)
+{
+ *id = PMC_ID_INVALID;
+ if (pmc_init() != 0 ||
+ pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY, id, 0) != 0 ||
+ pmc_attach(*id, 0) != 0 || pmc_start(*id) != 0)
+ return (-1);
+ return (0);
+}
+
+static void
+self_detach(pmc_id_t id)
+{
+ (void)pmc_stop(id);
+ (void)pmc_detach(id, 0);
+ (void)pmc_release(id);
+}
+
+ATF_TC(pidtrack_self_counts);
+ATF_TC_HEAD(pidtrack_self_counts, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "A process-mode PMC attached to the calling process counts that "
+ "process' own work, observable via a live self-read");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_self_counts, tc)
+{
+ const char *event;
+ pmc_id_t id;
+ pmc_value_t v0, v1;
+
+ event = skip_unless_event(counting_events);
+
+ ATF_REQUIRE(pmc_allocate(event, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &id, 0) == 0);
+ ATF_REQUIRE(pmc_attach(id, 0) == 0);
+ ATF_REQUIRE(pmc_start(id) == 0);
+
+ ATF_REQUIRE(pmc_read(id, &v0) == 0);
+ spin_workload();
+ ATF_REQUIRE(pmc_read(id, &v1) == 0);
+
+ ATF_REQUIRE(pmc_stop(id) == 0);
+ ATF_REQUIRE(pmc_detach(id, 0) == 0);
+ ATF_REQUIRE(pmc_release(id) == 0);
+
+ ATF_CHECK_MSG(v1 >= v0 && v1 - v0 >= MIN_ACTIVE,
+ "self-counter did not advance over the workload: %ju -> %ju "
+ "(expected a delta >= %ju)", (uintmax_t)v0, (uintmax_t)v1,
+ (uintmax_t)MIN_ACTIVE);
+}
+
+/* Exit status of a child whose PMC could not be set up. */
+#define CHILD_NO_PMC 70
+
+/* Skip if a child could not set up its PMC; fail if it failed otherwise. */
+static void
+check_child(const char *name, int status)
+{
+ if (WIFEXITED(status) && WEXITSTATUS(status) == CHILD_NO_PMC)
+ atf_tc_skip("the %s child could not set up its own PMC", name);
+ ATF_REQUIRE_MSG(WIFEXITED(status) && WEXITSTATUS(status) == 0,
+ "the %s child failed: status %#x", name, status);
+}
+
+/*
+ * Child for pidtrack_no_leakage. Both children are bound to cpu0 and
+ * self-attach their own PMC. The idle child takes its first reading, tells
+ * the busy child to start and blocks until the busy child has finished its
+ * spin, so the idle counter is live for the whole spin on the same CPU.
+ * Each child writes its delta to resfd and exits 0.
+ */
+static void __dead2
+child_selfmeasure(const char *event, bool busy, int waitfd, int notifyfd,
+ int resfd)
+{
+ cpuset_t mask;
+ pmc_id_t id;
+ pmc_value_t v0, v1;
+ uint64_t delta;
+ char c;
+
+ CPU_ZERO(&mask);
+ CPU_SET(0, &mask);
+ if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1,
+ sizeof(mask), &mask) != 0 || self_attach(event, &id) != 0)
+ _exit(CHILD_NO_PMC);
+
+ if (busy)
+ (void)read(waitfd, &c, 1);
+ if (pmc_read(id, &v0) != 0)
+ _exit(1);
+ if (busy) {
+ spin_workload();
+ (void)write(notifyfd, "d", 1);
+ } else {
+ (void)write(notifyfd, "r", 1);
+ (void)read(waitfd, &c, 1);
+ }
+ if (pmc_read(id, &v1) != 0)
+ _exit(1);
+ self_detach(id);
+
+ delta = v1 >= v0 ? v1 - v0 : 0;
+ (void)write(resfd, &delta, sizeof(delta));
+ _exit(0);
+}
+
+ATF_TC(pidtrack_no_leakage);
+ATF_TC_HEAD(pidtrack_no_leakage, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Two processes bound to one CPU, each with its own process-mode "
+ "PMC: the idle one's counter stays near zero while the busy one "
+ "runs, i.e. no cross-PID counter leakage");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_no_leakage, tc)
+{
+ const char *event;
+ pid_t busy, idle;
+ int busyfd[2], donefd[2], idlefd[2], readyfd[2];
+ int bstatus, istatus;
+ uint64_t busy_delta, idle_delta;
+
+ event = skip_unless_event(retired_events);
+
+ /* Notifying a child that already exited must not kill the test. */
+ (void)signal(SIGPIPE, SIG_IGN);
+ ATF_REQUIRE(pipe(busyfd) == 0);
+ ATF_REQUIRE(pipe(donefd) == 0);
+ ATF_REQUIRE(pipe(idlefd) == 0);
+ ATF_REQUIRE(pipe(readyfd) == 0);
+
+ busy = fork();
+ ATF_REQUIRE(busy >= 0);
+ if (busy == 0) {
+ close(busyfd[0]);
+ close(donefd[0]);
+ close(idlefd[0]);
+ close(idlefd[1]);
+ close(readyfd[1]);
+ child_selfmeasure(event, true, readyfd[0], donefd[1],
+ busyfd[1]);
+ }
+ idle = fork();
+ if (idle < 0) {
+ (void)kill(busy, SIGKILL);
+ (void)waitpid(busy, NULL, 0);
+ atf_tc_fail("fork(idle) failed: %s", strerror(errno));
+ }
+ if (idle == 0) {
+ close(busyfd[0]);
+ close(busyfd[1]);
+ close(donefd[1]);
+ close(idlefd[0]);
+ close(readyfd[0]);
+ child_selfmeasure(event, false, donefd[0], readyfd[1],
+ idlefd[1]);
+ }
+ close(busyfd[1]);
+ close(donefd[0]);
+ close(donefd[1]);
+ close(idlefd[1]);
+ close(readyfd[0]);
+ close(readyfd[1]);
+
+ ATF_REQUIRE(waitpid(busy, &bstatus, 0) == busy);
+ ATF_REQUIRE(waitpid(idle, &istatus, 0) == idle);
+ check_child("busy", bstatus);
+ check_child("idle", istatus);
+
+ ATF_REQUIRE(read(busyfd[0], &busy_delta, sizeof(busy_delta)) ==
+ (ssize_t)sizeof(busy_delta));
+ ATF_REQUIRE(read(idlefd[0], &idle_delta, sizeof(idle_delta)) ==
+ (ssize_t)sizeof(idle_delta));
+ close(busyfd[0]);
+ close(idlefd[0]);
+
+ ATF_CHECK_MSG(busy_delta >= MIN_ACTIVE,
+ "busy child's own counter did not advance: %ju",
+ (uintmax_t)busy_delta);
+ ATF_CHECK_MSG(idle_delta < MIN_ACTIVE,
+ "idle child's counter advanced by %ju while its sibling ran on "
+ "the same CPU (expected < %ju)",
+ (uintmax_t)idle_delta, (uintmax_t)MIN_ACTIVE);
+}
+
+ATF_TC(pidtrack_ctxswitch);
+ATF_TC_HEAD(pidtrack_ctxswitch, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "A process-mode PMC's accumulated count is preserved and keeps "
+ "advancing across the target being switched out and in");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+ATF_TC_BODY(pidtrack_ctxswitch, tc)
+{
+ const char *event;
+ pmc_id_t id;
+ pmc_value_t b, e, s2;
+ pid_t child;
+ uint64_t out[3];
+ int pfd[2], rdyfd[2], resfd[2], status;
+ char c;
+
+ event = skip_unless_event(counting_events);
+
+ (void)signal(SIGPIPE, SIG_IGN);
+ ATF_REQUIRE(pipe(pfd) == 0);
+ ATF_REQUIRE(pipe(rdyfd) == 0);
+ ATF_REQUIRE(pipe(resfd) == 0);
+
+ child = fork();
+ ATF_REQUIRE(child >= 0);
+ if (child == 0) {
+ /*
+ * Report three absolute reads so the parent can check both
+ * that the value survived the switch (r2_start >= r1_end)
+ * and that counting resumed afterwards (r2_end > r2_start).
+ */
+ close(pfd[1]);
+ close(rdyfd[0]);
+ close(resfd[0]);
+ if (self_attach(event, &id) != 0)
+ _exit(CHILD_NO_PMC);
+
+ spin_workload();
+ if (pmc_read(id, &b) != 0)
+ _exit(1);
+
+ /* Report round 1 done, then block: switched out and in. */
+ (void)write(rdyfd[1], "r", 1);
+ (void)read(pfd[0], &c, 1);
+
+ if (pmc_read(id, &s2) != 0)
+ _exit(1);
+ spin_workload();
+ if (pmc_read(id, &e) != 0)
+ _exit(1);
+ self_detach(id);
+
+ out[0] = b;
+ out[1] = s2;
+ out[2] = e;
+ (void)write(resfd[1], out, sizeof(out));
+ _exit(0);
+ }
+ close(pfd[0]);
+ close(rdyfd[1]);
+ close(resfd[1]);
+
+ /*
+ * Once round 1 is done, give the child time to block in read(2)
+ * before releasing it. EOF means the child exited early.
+ */
+ if (read(rdyfd[0], &c, 1) == 1)
+ usleep(200 * 1000);
+ (void)write(pfd[1], "g", 1);
+ ATF_REQUIRE(waitpid(child, &status, 0) == child);
+ check_child("measuring", status);
+ ATF_REQUIRE(read(resfd[0], out, sizeof(out)) == (ssize_t)sizeof(out));
+ close(pfd[1]);
+ close(rdyfd[0]);
+ close(resfd[0]);
+
+ ATF_CHECK_MSG(out[0] >= MIN_ACTIVE,
+ "round-1 count too low: %ju", (uintmax_t)out[0]);
+ ATF_CHECK_MSG(out[1] >= out[0],
+ "counter went backwards across the context switch: r1_end=%ju "
+ "r2_start=%ju", (uintmax_t)out[0], (uintmax_t)out[1]);
+ /* Checked separately so that a backwards step cannot wrap. */
+ ATF_CHECK_MSG(out[2] >= out[1] && out[2] - out[1] >= MIN_ACTIVE,
+ "counter did not advance after switch-in: r2_start=%ju "
+ "r2_end=%ju", (uintmax_t)out[1], (uintmax_t)out[2]);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, pidtrack_self_counts);
+ ATF_TP_ADD_TC(tp, pidtrack_no_leakage);
+ ATF_TP_ADD_TC(tp, pidtrack_ctxswitch);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/pmc_rapl_test.c b/tests/sys/pmc/pmc_rapl_test.c
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmc_rapl_test.c
@@ -0,0 +1,260 @@
+/*
+ * Copyright (c) 2026 Advanced Micro Devices, Inc.
+ *
+ * Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+/*
+ * Tests for the hwpmc RAPL energy class (hwpmc_rapl.c), which the AMD and
+ * Intel backends both register. The driver only adds a row when its MSR
+ * responds, so every case skips when the class or the row it needs is
+ * absent (VM, unsupported CPU). Values are cumulative microjoules and
+ * RAPL PMCs are system-scope only; see pmc.rapl(3).
+ */
+
+#include <sys/param.h>
+#include <sys/cpuset.h>
+#include <sys/pmc.h>
+
+#include <atf-c.h>
+
+#include <errno.h>
+#include <pmc.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+
+#define RAPL_EV_PKG "rapl-energy-pkg"
+#define RAPL_EV_CORES "rapl-energy-cores"
+
+/* CPU the SC-mode RAPL PMCs are allocated on. */
+#define RAPL_CPU 0
+
+/* Return the PMC_CLASS_RAPL classinfo, or NULL if it is not registered. */
+static const struct pmc_classinfo *
+rapl_classinfo(void)
+{
+ const struct pmc_cpuinfo *cpuinfo;
+ uint32_t i;
+
+ if (pmc_cpuinfo(&cpuinfo) != 0)
+ return (NULL);
+ for (i = 0; i < cpuinfo->pm_nclass; i++)
+ if (cpuinfo->pm_classes[i].pm_class == PMC_CLASS_RAPL)
+ return (&cpuinfo->pm_classes[i]);
+ return (NULL);
+}
+
+/* Skip unless hwpmc is loaded and the RAPL class is registered. */
+static void
+skip_unless_rapl(void)
+{
+ if (pmc_init() != 0)
+ atf_tc_skip("hwpmc(4) is not available: %s", strerror(errno));
+ if (rapl_classinfo() == NULL)
+ atf_tc_skip("PMC_CLASS_RAPL not registered: the RAPL energy "
+ "MSRs did not respond (VM or unsupported CPU)");
+}
+
+/* Return true if RAPL_CPU has a PMC row with the given name. */
+static bool
+rapl_has_row(const char *row)
+{
+ struct pmc_pmcinfo *pmcinfo;
+ bool found;
+ int i, npmc;
+
+ ATF_REQUIRE_MSG(pmc_pmcinfo(RAPL_CPU, &pmcinfo) == 0,
+ "pmc_pmcinfo(%d) failed: %s", RAPL_CPU, strerror(errno));
+ npmc = pmc_npmc(RAPL_CPU);
+ found = false;
+ for (i = 0; i < npmc; i++)
+ if (strcmp(pmcinfo->pm_pmcs[i].pm_name, row) == 0)
+ found = true;
+ free(pmcinfo);
+ return (found);
+}
+
+/* Busy-spin for at least ms milliseconds so the core consumes energy. */
+static void
+cpu_spin_ms(unsigned ms)
+{
+ struct timespec start, now;
+ volatile uint64_t sink;
+ uint64_t i;
+ int64_t elapsed_ms;
+ unsigned k;
+
+ ATF_REQUIRE_MSG(clock_gettime(CLOCK_MONOTONIC, &start) == 0,
+ "clock_gettime failed: %s", strerror(errno));
+
+ sink = 0;
+ i = 0;
+ for (;;) {
+ for (k = 0; k < 200000; k++)
+ sink += (i++ * 2654435761UL) ^ (sink >> 3);
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
+ break;
+ elapsed_ms = (int64_t)(now.tv_sec - start.tv_sec) * 1000 +
+ (now.tv_nsec - start.tv_nsec) / 1000000;
+ if (elapsed_ms >= (int64_t)ms)
+ break;
+ }
+}
+
+/*
+ * Bind to RAPL_CPU and read the event of the given row across a 500 ms
+ * spin there. Skips if the driver did not register the row; any other
+ * failure is an error.
+ */
+static void
+rapl_delta_under_load(const char *event, const char *row,
+ pmc_value_t *before, pmc_value_t *after)
+{
+ cpuset_t mask;
+ pmc_id_t id;
+ int error;
+
+ if (!rapl_has_row(row))
+ atf_tc_skip("the driver registered no %s row", row);
+
+ CPU_ZERO(&mask);
+ CPU_SET(RAPL_CPU, &mask);
+ if (cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_TID, -1,
+ sizeof(mask), &mask) != 0)
+ atf_tc_skip("cannot bind to cpu%d: %s", RAPL_CPU,
+ strerror(errno));
+
+ id = PMC_ID_INVALID;
+ if (pmc_allocate(event, PMC_MODE_SC, 0, RAPL_CPU, &id, 1) != 0)
+ atf_tc_fail("pmc_allocate(%s) failed: %s", event,
+ strerror(errno));
+
+ error = 0;
+ if (pmc_read(id, before) != 0) {
+ error = errno;
+ } else {
+ cpu_spin_ms(500);
+ if (pmc_read(id, after) != 0)
+ error = errno;
+ }
+ ATF_REQUIRE_MSG(pmc_release(id) == 0, "pmc_release failed: %s",
+ strerror(errno));
+ if (error != 0)
+ atf_tc_fail("pmc_read(%s) failed: %s", event, strerror(error));
+}
+
+ATF_TC(rapl_class_present);
+ATF_TC_HEAD(rapl_class_present, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "PMC_CLASS_RAPL is registered with 64-bit counters.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_class_present, tc)
+{
+ const struct pmc_classinfo *ci;
+
+ skip_unless_rapl();
+
+ ci = rapl_classinfo();
+ ATF_REQUIRE(ci != NULL);
+ ATF_CHECK_MSG(ci->pm_width == 64,
+ "RAPL counter width expected 64, got %u", ci->pm_width);
+}
+
+ATF_TC(rapl_reject_process_mode);
+ATF_TC_HEAD(rapl_reject_process_mode, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "Allocating a RAPL event in process mode (PMC_MODE_TC) fails "
+ "with EINVAL: RAPL is system-scope only.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+}
+
+ATF_TC_BODY(rapl_reject_process_mode, tc)
+{
+ pmc_id_t id;
+
+ skip_unless_rapl();
+ if (!rapl_has_row("RAPL_ENERGY_PKG"))
+ atf_tc_skip("the driver registered no RAPL_ENERGY_PKG row");
+
+ id = PMC_ID_INVALID;
+ if (pmc_allocate(RAPL_EV_PKG, PMC_MODE_TC, 0, PMC_CPU_ANY, &id,
+ 1) == 0) {
+ (void)pmc_release(id);
+ atf_tc_fail("pmc_allocate(%s, PMC_MODE_TC) succeeded",
+ RAPL_EV_PKG);
+ }
+ ATF_CHECK_MSG(errno == EINVAL,
+ "expected EINVAL for a process-mode RAPL PMC, got %d (%s)",
+ errno, strerror(errno));
+}
+
+ATF_TC(rapl_pkg_delta_under_load);
+ATF_TC_HEAD(rapl_pkg_delta_under_load, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "rapl-energy-pkg increases across a 500 ms CPU workload.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "timeout", "30");
+}
+
+ATF_TC_BODY(rapl_pkg_delta_under_load, tc)
+{
+ pmc_value_t before, after;
+
+ skip_unless_rapl();
+
+ before = after = 0;
+ rapl_delta_under_load(RAPL_EV_PKG, "RAPL_ENERGY_PKG", &before,
+ &after);
+ ATF_CHECK_MSG(after > before,
+ "package energy did not increase under load: before=%ju "
+ "after=%ju", (uintmax_t)before, (uintmax_t)after);
+}
+
+ATF_TC(rapl_cores_delta_under_load);
+ATF_TC_HEAD(rapl_cores_delta_under_load, tc)
+{
+ atf_tc_set_md_var(tc, "descr",
+ "rapl-energy-cores on cpu0 increases across a 500 ms workload "
+ "running on cpu0.");
+ atf_tc_set_md_var(tc, "require.user", "root");
+ atf_tc_set_md_var(tc, "timeout", "30");
+}
+
+ATF_TC_BODY(rapl_cores_delta_under_load, tc)
+{
+ pmc_value_t before, after;
+
+ skip_unless_rapl();
+
+ before = after = 0;
+ rapl_delta_under_load(RAPL_EV_CORES, "RAPL_ENERGY_CORES", &before,
+ &after);
+ /* Some Intel server parts register a PP0 row that never counts. */
+ if (before == 0 && after == 0)
+ atf_tc_skip("RAPL_ENERGY_CORES reads 0: the domain is not "
+ "implemented");
+ ATF_CHECK_MSG(after > before,
+ "core energy did not increase under load on cpu0: before=%ju "
+ "after=%ju", (uintmax_t)before, (uintmax_t)after);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, rapl_class_present);
+ ATF_TP_ADD_TC(tp, rapl_reject_process_mode);
+ ATF_TP_ADD_TC(tp, rapl_pkg_delta_under_load);
+ ATF_TP_ADD_TC(tp, rapl_cores_delta_under_load);
+
+ return (atf_no_error());
+}
diff --git a/tests/sys/pmc/pmcstat_tsc_test.sh b/tests/sys/pmc/pmcstat_tsc_test.sh
new file mode 100644
--- /dev/null
+++ b/tests/sys/pmc/pmcstat_tsc_test.sh
@@ -0,0 +1,122 @@
+#
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+#
+# Author: Osvaldo Janeri Filho <ojanerif@amd.com>
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+
+#
+# Checks the TSC information in pmc logs: the tsc_freq field of the initlog
+# record, the raw TSC column printed by pmcstat -R, the unsigned "tsc"
+# values emitted by pmc filter -j, and that pmcstat -G still renders a
+# callgraph from a log with callchains.
+#
+
+# Capture a 1-second system-wide "instructions" sampling log into $1.
+# Only a PMC that cannot be allocated is a reason to skip.
+pmc_capture()
+{
+ if ! pmcstat -S instructions -O "$1" sleep 1 > /dev/null \
+ 2> pmcstat.err; then
+ grep -q "Cannot allocate system-mode pmc" pmcstat.err && \
+ atf_skip "cannot allocate a system-mode instructions PMC"
+ atf_fail "pmcstat failed: $(cat pmcstat.err)"
+ fi
+ [ -s "$1" ] || atf_fail "pmcstat produced an empty log"
+}
+
+atf_test_case tsc_freq_initlog
+tsc_freq_initlog_head()
+{
+ atf_set "descr" "pmcstat -R prints tsc_freq=<hz> in the initlog record"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmcstat"
+ atf_set "timeout" "120"
+}
+tsc_freq_initlog_body()
+{
+ local line
+
+ pmc_capture test.pmc
+
+ line=$(pmcstat -R test.pmc 2>/dev/null | grep '^initlog')
+ [ -n "$line" ] || atf_fail "no initlog record in pmcstat -R output"
+ echo "$line" | grep -qE 'tsc_freq=[1-9][0-9]*' || \
+ atf_fail "initlog record lacks tsc_freq: $line"
+}
+
+atf_test_case tsc_column_format
+tsc_column_format_head()
+{
+ atf_set "descr" "every pmcstat -R line ends with the raw TSC value"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmcstat"
+ atf_set "timeout" "120"
+}
+tsc_column_format_body()
+{
+ local bad decoded first
+
+ pmc_capture test.pmc
+
+ decoded=$(pmcstat -R test.pmc 2>/dev/null | grep -v '^[[:space:]]*$')
+ [ -n "$decoded" ] || atf_fail "pmcstat -R produced no output"
+
+ bad=$(echo "$decoded" | grep -cvE '[0-9]{10,}[[:space:]]*$')
+ if [ "$bad" -gt 0 ]; then
+ first=$(echo "$decoded" | grep -vE '[0-9]{10,}[[:space:]]*$' |
+ head -1)
+ atf_fail "$bad line(s) without a TSC column; first: $first"
+ fi
+}
+
+atf_test_case json_tsc_unsigned
+json_tsc_unsigned_head()
+{
+ atf_set "descr" \
+ "every record from pmc filter -j carries a decimal tsc string"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmc pmcstat"
+ atf_set "timeout" "120"
+}
+json_tsc_unsigned_body()
+{
+ local bad total
+
+ pmc_capture test.pmc
+
+ atf_check -s exit:0 -o ignore -e ignore pmc filter -j test.pmc test.json
+ [ -s test.json ] || atf_fail "pmc filter -j produced no output"
+ grep -oE '"tsc": "[^"]*"' test.json > tsc.txt
+ total=$(wc -l < tsc.txt)
+ bad=$(grep -cvE '"tsc": "[0-9]+"' tsc.txt)
+ [ "$total" -gt 0 ] || atf_fail "no \"tsc\" key in JSON output"
+ [ "$bad" -eq 0 ] || \
+ atf_fail "$bad of $total \"tsc\" values are not unsigned decimals"
+}
+
+atf_test_case callgraph_render
+callgraph_render_head()
+{
+ atf_set "descr" "pmcstat -G renders a callgraph from a sampling log"
+ atf_set "require.user" "root"
+ atf_set "require.progs" "pmcstat"
+ atf_set "timeout" "120"
+}
+callgraph_render_body()
+{
+ pmc_capture test.pmc
+
+ atf_check -s exit:0 -o ignore -e ignore \
+ pmcstat -R test.pmc -G callgraph.txt
+ [ -s callgraph.txt ] || atf_fail "pmcstat -G wrote an empty callgraph"
+}
+
+atf_init_test_cases()
+{
+ atf_add_test_case tsc_freq_initlog
+ atf_add_test_case tsc_column_format
+ atf_add_test_case json_tsc_unsigned
+ atf_add_test_case callgraph_render
+}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sun, Nov 15, 5:43 PM (19 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
41746280
Default Alt Text
D59549.1794764609.diff (76 KB)
Attached To
Mode
D59549: tests/sys/pmc: add pidtrack, callchain, pmc(8), RAPL and AMD tests
Attached
Detach File
Event Timeline