Page MenuHomeFreeBSD

D59549.1794764609.diff
No OneTemporary

Authored By
Unknown
Size
76 KB
Referenced Files
None
Subscribers
None

D59549.1794764609.diff

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

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)

Event Timeline