Page MenuHomeFreeBSD

D57638.diff
No OneTemporary

D57638.diff

diff --git a/tools/regression/pmc/Makefile b/tools/regression/pmc/Makefile
new file mode 100644
--- /dev/null
+++ b/tools/regression/pmc/Makefile
@@ -0,0 +1,75 @@
+# Build the hwpmc grouping/multiplex regression tests.
+#
+# Usage from this directory:
+# make # builds all three test binaries
+# make check # userland-only test (no root needed)
+# make check-root # in-kernel tests (need hwpmc(4) + root)
+# make clean
+#
+# Plain Makefile (no <bsd.prog.mk>) so it works the same in-tree and
+# out-of-tree. libpmcstat is INTERNALLIB on FreeBSD - it has no
+# installed .so/.a - so test_libpmcstat_group compiles libpmcstat_group.c
+# directly rather than -lpmcstat.
+
+# Disable bmake's auto-objdir; build in-place next to the sources so
+# the recipes can find pmc_group_test.c et al with relative paths and
+# the resulting binaries land where `make check` expects them.
+.OBJDIR: ${.CURDIR}
+
+CC?= cc
+
+# Auto-detects when this directory still lives at tools/regression/pmc
+# in a FreeBSD source tree; override on the command line if copied.
+SRCROOT?= ${.CURDIR}/../../..
+
+LIBPMCSTAT_SRC= ${SRCROOT}/lib/libpmcstat
+LIBPMCSTAT_INC= -I${LIBPMCSTAT_SRC}
+
+CFLAGS+= -O -g -Wall
+
+# Kernel ABI tests (need hwpmc(4) loaded and root to run).
+KTESTS= pmc_group_test pmc_mux_test pmc_mux_works_test
+
+# Userland-only test (parser unit test, no PMU access).
+UTESTS= test_libpmcstat_group
+
+ALL= ${KTESTS} ${UTESTS}
+
+.PHONY: all check check-root clean
+
+all: ${ALL}
+
+pmc_group_test: ${.CURDIR}/pmc_group_test.c
+ ${CC} ${CFLAGS} -o ${.TARGET} ${.ALLSRC} -lpmc
+
+pmc_mux_test: ${.CURDIR}/pmc_mux_test.c
+ ${CC} ${CFLAGS} -o ${.TARGET} ${.ALLSRC} -lpmc
+
+pmc_mux_works_test: ${.CURDIR}/pmc_mux_works_test.c
+ ${CC} ${CFLAGS} -o ${.TARGET} ${.ALLSRC} -lpmc
+
+# libpmcstat is INTERNALLIB (no .so installed), so build the parser TU
+# directly into the test binary.
+test_libpmcstat_group: ${.CURDIR}/test_libpmcstat_group.c \
+ ${LIBPMCSTAT_SRC}/libpmcstat_group.c
+ ${CC} ${CFLAGS} ${LIBPMCSTAT_INC} -o ${.TARGET} ${.ALLSRC} -lpmc
+
+# Userland-only check: brace-list parser, no PMU.
+check: test_libpmcstat_group
+ ./test_libpmcstat_group
+
+# In-kernel check: requires `kldload hwpmc` and root.
+check-root: ${KTESTS}
+ @echo "==> pmc_group_test"
+ ./pmc_group_test
+ @echo "==> pmc_mux_test"
+ ./pmc_mux_test
+ @echo "==> pmc_mux_works_test"
+ ./pmc_mux_works_test
+ @if [ -r ${.CURDIR}/pmcstat_test.sh ]; then \
+ echo "==> pmcstat(8) shell tests"; \
+ sh ${.CURDIR}/pmcstat_test.sh; \
+ fi
+
+clean:
+ rm -f ${ALL} *.o *.core
diff --git a/tools/regression/pmc/pmc_group_test.c b/tools/regression/pmc/pmc_group_test.c
new file mode 100644
--- /dev/null
+++ b/tools/regression/pmc/pmc_group_test.c
@@ -0,0 +1,251 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Regression test for hwpmc grouping (phase 1).
+ *
+ * Build: cc -o pmc_group_test pmc_group_test.c -lpmc
+ * Run: sudo ./pmc_group_test (requires hwpmc loaded, AMD CPU)
+ */
+
+#include <sys/types.h>
+#include <sys/sysctl.h>
+
+#include <err.h>
+#include <errno.h>
+#include <pmc.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static int
+is_amd(void)
+{
+ char buf[64];
+ size_t s = sizeof(buf);
+
+ if (sysctlbyname("kern.hwpmc.cpuid", buf, &s, NULL, 0) != 0)
+ return (0);
+ return (strstr(buf, "AuthenticAMD") != NULL ||
+ strstr(buf, "HygonGenuine") != NULL);
+}
+
+/*
+ * Probe how many process-mode core PMCs we can simultaneously allocate
+ * with the canonical "instructions" event. Returns the count without
+ * leaving any allocations behind. Used to size the rest of the test
+ * dynamically per-CPU instead of relying on pmc_npmc(0), which sums
+ * SOFT/TSC/K8/IBS classes and is therefore meaningless for sizing a
+ * single core-class group.
+ */
+static int
+probe_core_pmcs(void)
+{
+ pmc_id_t ids[64];
+ int n = 0;
+
+ while (n < (int)(sizeof(ids) / sizeof(ids[0]))) {
+ if (pmc_allocate("instructions", PMC_MODE_TC, 0,
+ PMC_CPU_ANY, &ids[n], 0) < 0)
+ break;
+ n++;
+ }
+ for (int i = 0; i < n; i++)
+ (void)pmc_release(ids[i]);
+ return (n);
+}
+
+static int
+test_basic_group(void)
+{
+ uint32_t gid;
+ pmc_id_t pmc0, pmc1, pmc2;
+ pmc_value_t v0, v1, v2;
+ const char *ev0 = "instructions";
+ const char *ev1 = "unhalted-cycles";
+ const char *ev2 = "branches";
+ volatile uint64_t spin;
+
+ if (pmc_group_create(&gid) < 0) {
+ warn("pmc_group_create");
+ return (1);
+ }
+ if (pmc_allocate_group(ev0, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &pmc0, 0) < 0) {
+ warn("allocate %s", ev0);
+ return (1);
+ }
+ if (pmc_allocate_group(ev1, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &pmc1, 0) < 0) {
+ warn("allocate %s", ev1);
+ return (1);
+ }
+ if (pmc_allocate_group(ev2, PMC_MODE_TC, 0, PMC_CPU_ANY,
+ &pmc2, 0) < 0) {
+ warn("allocate %s", ev2);
+ return (1);
+ }
+ if (pmc_group_add(gid, pmc0, 1) < 0) {
+ warn("group_add leader");
+ return (1);
+ }
+ if (pmc_group_add(gid, pmc1, 0) < 0) {
+ warn("group_add sibling 1");
+ return (1);
+ }
+ if (pmc_group_add(gid, pmc2, 0) < 0) {
+ warn("group_add sibling 2");
+ return (1);
+ }
+ if (pmc_group_commit(gid) < 0) {
+ warn("group_commit");
+ return (1);
+ }
+
+ /*
+ * Process-mode PMCs require an explicit attach before start.
+ * pmu_group_target_proc() now insists on an attached target proc
+ * (the owner is intentionally NOT used as a fallback because that
+ * confused csw_in/out under the multiplex rework), so calling
+ * pmc_start without first pmc_attach'ing to ourselves now returns
+ * EINVAL. Attach every sibling so the whole group can bind.
+ */
+ if (pmc_attach(pmc0, getpid()) < 0) {
+ warn("pmc_attach pmc0");
+ return (1);
+ }
+ if (pmc_attach(pmc1, getpid()) < 0) {
+ warn("pmc_attach pmc1");
+ return (1);
+ }
+ if (pmc_attach(pmc2, getpid()) < 0) {
+ warn("pmc_attach pmc2");
+ return (1);
+ }
+
+ if (pmc_start(pmc0) < 0) {
+ warn("pmc_start leader");
+ return (1);
+ }
+ if (pmc_start(pmc1) < 0) {
+ warn("pmc_start sibling 1");
+ return (1);
+ }
+ if (pmc_start(pmc2) < 0) {
+ warn("pmc_start sibling 2");
+ return (1);
+ }
+
+ for (spin = 0; spin < 100000000ULL; spin++)
+ ;
+
+ if (pmc_read(pmc0, &v0) < 0 || pmc_read(pmc1, &v1) < 0 ||
+ pmc_read(pmc2, &v2) < 0) {
+ warn("pmc_read");
+ return (1);
+ }
+ (void)pmc_stop(pmc0);
+ (void)pmc_stop(pmc1);
+ (void)pmc_stop(pmc2);
+ printf("3-event group: pmc0=%ju pmc1=%ju pmc2=%ju\n",
+ (uintmax_t)v0, (uintmax_t)v1, (uintmax_t)v2);
+ if (v0 == 0 || v1 == 0 || v2 == 0) {
+ fprintf(stderr, "FAIL: at least one sibling never counted\n");
+ (void)pmc_release(pmc0);
+ (void)pmc_release(pmc1);
+ (void)pmc_release(pmc2);
+ return (1);
+ }
+ (void)pmc_release(pmc0);
+ (void)pmc_release(pmc1);
+ (void)pmc_release(pmc2);
+ return (0);
+}
+
+/*
+ * The atomic-group scheduler must reject a single group whose event
+ * count exceeds the core HW counter pool. Within-group placement is
+ * all-or-none, so a group that cannot fit at commit MUST fail rather
+ * than silently get split across rotation windows.
+ *
+ * Sizing: query the actual per-class core count via probe_core_pmcs()
+ * (Zen5 = 6, Zen3/4 = 6, Zen6 = up to 12, EPYC = vendor-dependent),
+ * then attempt a group of (core + 2) events. Hard-coding 16 missed
+ * Zen6 and any future generation that exceeds it.
+ */
+static int
+test_oversubscription_rejected(void)
+{
+ uint32_t gid;
+ pmc_id_t *ids;
+ int core, target, i, allocated, err;
+
+ core = probe_core_pmcs();
+ if (core <= 0) {
+ printf("SKIP: probe_core_pmcs returned %d\n", core);
+ return (0);
+ }
+ target = core + 2;
+ ids = calloc(target, sizeof(*ids));
+ if (ids == NULL)
+ return (1);
+
+ if (pmc_group_create(&gid) < 0) {
+ free(ids);
+ return (1);
+ }
+ allocated = 0;
+ for (i = 0; i < target; i++) {
+ if (pmc_allocate_group("instructions", PMC_MODE_TC, 0,
+ PMC_CPU_ANY, &ids[i], 0) < 0)
+ break;
+ if (pmc_group_add(gid, ids[i], i == 0) < 0)
+ break;
+ allocated++;
+ }
+ if (allocated <= core) {
+ fprintf(stderr,
+ "SKIP: only allocated %d events for a %d-counter pool; "
+ "cannot oversubscribe\n", allocated, core);
+ for (i = 0; i < allocated; i++)
+ (void)pmc_release(ids[i]);
+ free(ids);
+ return (0);
+ }
+ err = pmc_group_commit(gid);
+ for (i = 0; i < allocated; i++)
+ (void)pmc_release(ids[i]);
+ free(ids);
+ if (err == 0) {
+ fprintf(stderr,
+ "FAIL: oversubscribed commit succeeded (allocated=%d "
+ "core_pool=%d)\n", allocated, core);
+ return (1);
+ }
+ if (errno != ENOSPC && errno != EOPNOTSUPP) {
+ fprintf(stderr,
+ "FAIL: expected ENOSPC/EOPNOTSUPP got errno=%d\n", errno);
+ return (1);
+ }
+ printf("oversubscription rejected (allocated=%d core_pool=%d "
+ "errno=%d)\n", allocated, core, errno);
+ return (0);
+}
+
+int
+main(void)
+{
+ if (pmc_init() < 0)
+ err(1, "pmc_init");
+ if (!is_amd()) {
+ printf("SKIP: non-AMD CPU\n");
+ return (77);
+ }
+ if (test_basic_group() != 0)
+ return (1);
+ if (test_oversubscription_rejected() != 0)
+ return (1);
+ printf("pmc_group_test: OK\n");
+ return (0);
+}
diff --git a/tools/regression/pmc/pmcstat_test.sh b/tools/regression/pmc/pmcstat_test.sh
new file mode 100644
--- /dev/null
+++ b/tools/regression/pmc/pmcstat_test.sh
@@ -0,0 +1,271 @@
+#!/bin/sh
+#
+# pmcstat(8) regression tests for -b / {a,b,c} group syntax.
+#
+# Must run as root with hwpmc(4) loaded on an AMD CPU. A workload
+# program is needed; we use /usr/bin/yes piped to /dev/null so each
+# test has a few seconds of busy CPU to collect counts on.
+#
+# Each test prints a "PASS:" or "FAIL:" line and an exit-status row
+# at the end. The script returns 0 only if every case passes.
+#
+# Usage:
+# sudo sh tools/regression/pmc/pmcstat_test.sh
+#
+
+set -u
+
+PMCSTAT=${PMCSTAT:-/usr/sbin/pmcstat}
+WORKLOAD_SECS=${WORKLOAD_SECS:-3}
+LOGDIR=$(mktemp -d -t pmcstat_test) || exit 1
+SUMMARY="${LOGDIR}/summary.log"
+PASS=0
+FAIL=0
+
+trap "rm -rf ${LOGDIR}" EXIT
+
+note() { printf '%s\n' "$*" | tee -a "${SUMMARY}"; }
+fail() { note "FAIL: $*"; FAIL=$((FAIL + 1)); }
+pass() { note "PASS: $*"; PASS=$((PASS + 1)); }
+
+# Run a workload that produces a few hundred million instructions so
+# every counter in the group has something to read.
+busy_workload() {
+ # `yes` is CPU-bound; redirect its stdout/stderr.
+ yes >/dev/null 2>&1 &
+ pid=$!
+ sleep "${WORKLOAD_SECS}"
+ kill -9 "${pid}" 2>/dev/null
+ wait "${pid}" 2>/dev/null
+}
+
+require_root() {
+ if [ "$(id -u)" -ne 0 ]; then
+ echo "ERROR: must run as root" >&2
+ exit 2
+ fi
+}
+
+require_hwpmc() {
+ if ! kldstat -q -m hwpmc; then
+ if ! kldload hwpmc 2>/dev/null; then
+ echo "ERROR: hwpmc(4) is not loaded and could not be loaded" >&2
+ exit 2
+ fi
+ fi
+}
+
+require_amd() {
+ cpuid=$(sysctl -n kern.hwpmc.cpuid 2>/dev/null || echo unknown)
+ case "${cpuid}" in
+ *AuthenticAMD*|*HygonGenuine*) ;;
+ *) echo "SKIP: AMD-only test, cpuid=${cpuid}"; exit 77 ;;
+ esac
+}
+
+# t_basic_group: three events, one group, process-counting mode.
+# Verifies -b accepts {ev1,ev2,ev3} and pmcstat exits 0 with non-empty output.
+t_basic_group() {
+ out="${LOGDIR}/basic.out"
+ ${PMCSTAT} -b \
+ -p '{instructions,unhalted-cycles,ls_dispatch.all}' \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "basic_group: pmcstat exit=${rc}; see ${out}"
+ return
+ fi
+ if ! grep -q '[0-9]' "${out}"; then
+ fail "basic_group: no numeric counter output in ${out}"
+ return
+ fi
+ pass "basic_group"
+}
+
+# t_two_groups: two independent process-counting groups.
+t_two_groups() {
+ out="${LOGDIR}/two.out"
+ ${PMCSTAT} -b \
+ -p '{instructions,unhalted-cycles}' \
+ -p '{ls_alloc_mab_count, ls_not_halted_cyc, ls_dispatch.all}' \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "two_groups: pmcstat exit=${rc}; see ${out}"
+ return
+ fi
+ pass "two_groups"
+}
+
+# t_oversubscribe: 7 events on a typical 6-counter Zen forces failure.
+# Requires PMC_F_GROUP_MUX support in the kernel (commit fails otherwise).
+t_oversubscribe() {
+ out="${LOGDIR}/mux.out"
+ ${PMCSTAT} -b -O /dev/null \
+ -p '{instructions,unhalted-cycles,ls_alloc_mab_count, ls_not_halted_cyc, ls_dispatch.all,ls_stlf, ls_int_taken}' \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -eq 0 ]; then
+ fail "oversubscribe: should have rejected"
+ return
+ fi
+ if [ "${rc}" -ge 128 ]; then
+ fail "oversubscribe: signal exit=${rc}"
+ return
+ fi
+ pass "oversubscribe"
+}
+
+# t_system_mode: system-wide counting, single CPU.
+t_system_mode() {
+ out="${LOGDIR}/sys.out"
+ ${PMCSTAT} -b -c 0 \
+ -s '{instructions,unhalted-cycles}' \
+ -- sleep "${WORKLOAD_SECS}" \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "system_mode: pmcstat exit=${rc}; see ${out}"
+ return
+ fi
+ pass "system_mode"
+}
+
+# t_sampling_group: process sampling with a group leader.
+t_sampling_group() {
+ log="${LOGDIR}/samples.log"
+ out="${LOGDIR}/sampling.out"
+ ${PMCSTAT} -b \
+ -P '{instructions,unhalted-cycles,ls_dispatch.all}' \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "sampling_group: pmcstat exit=${rc}; see ${out}"
+ return
+ fi
+ if [ ! -s "${log}" ]; then
+ fail "sampling_group: empty log ${log}"
+ return
+ fi
+ pass "sampling_group"
+}
+
+# t_compat_no_b: without -b, brace text is treated as a literal event
+# spec by pmc_allocate which should reject it; we expect non-zero
+# exit and a useful message, never a kernel panic / segfault.
+t_compat_no_b() {
+ out="${LOGDIR}/compat_no_b.out"
+ ${PMCSTAT} \
+ -p '{instructions,unhalted-cycles}' \
+ -- /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ # Without -b the literal "{instructions,...}" string is invalid
+ # event-spec syntax, so pmc_allocate must fail; pmcstat reports it
+ # via err(EX_OSERR, ...). Exit must be non-zero, NOT a SIGSEGV.
+ if [ "${rc}" -eq 0 ]; then
+ fail "compat_no_b: should have rejected brace without -b"
+ return
+ fi
+ if [ "${rc}" -ge 128 ]; then
+ fail "compat_no_b: pmcstat killed by signal (exit=${rc})"
+ return
+ fi
+ pass "compat_no_b"
+}
+
+# t_compat_legacy: classic non-grouped pmcstat invocation must still work.
+t_compat_legacy() {
+ out="${LOGDIR}/legacy.out"
+ ${PMCSTAT} \
+ -p instructions -p unhalted-cycles \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "compat_legacy: exit=${rc}; see ${out}"
+ return
+ fi
+ pass "compat_legacy"
+}
+
+# t_single_brace: {ev} (one element) must fall through to single-event
+# behaviour without erroring.
+t_single_brace() {
+ out="${LOGDIR}/single.out"
+ ${PMCSTAT} -b \
+ -p '{instructions}' \
+ -- /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "single_brace: exit=${rc}; see ${out}"
+ return
+ fi
+ pass "single_brace"
+}
+
+# t_malformed_brace: missing closing '}' must be rejected with EX_USAGE
+# (64), never a crash.
+t_malformed_brace() {
+ out="${LOGDIR}/malformed.out"
+ ${PMCSTAT} -b \
+ -p '{instructions,unhalted-cycles' \
+ -- /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -eq 0 ]; then
+ fail "malformed_brace: should have rejected"
+ return
+ fi
+ if [ "${rc}" -ge 128 ]; then
+ fail "malformed_brace: signal exit=${rc}"
+ return
+ fi
+ pass "malformed_brace"
+}
+
+# t_mixed: one grouped -p and one ungrouped -p in the same invocation.
+t_mixed() {
+ out="${LOGDIR}/mixed.out"
+ ${PMCSTAT} -b \
+ -p '{instructions,unhalted-cycles}' \
+ -p ls_dispatch.all \
+ -- sh -c /usr/bin/true \
+ >"${out}" 2>&1
+ rc=$?
+ if [ "${rc}" -ne 0 ]; then
+ fail "mixed: exit=${rc}; see ${out}"
+ return
+ fi
+ pass "mixed"
+}
+
+main() {
+ require_root
+ require_hwpmc
+ require_amd
+
+ note "pmcstat(8) regression tests; logdir=${LOGDIR}"
+
+ t_compat_legacy
+ t_basic_group
+ t_two_groups
+ t_system_mode
+ t_sampling_group
+ t_single_brace
+ t_mixed
+ t_compat_no_b
+ t_malformed_brace
+ t_oversubscribe
+
+ note "------------------------------------"
+ note "PASS: ${PASS} FAIL: ${FAIL}"
+ [ "${FAIL}" -eq 0 ]
+}
+
+main "$@"

File Metadata

Mime Type
text/plain
Expires
Mon, Jun 29, 1:25 AM (11 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
34448025
Default Alt Text
D57638.diff (15 KB)

Event Timeline