* [LTP] [PATCH v5 1/3] lib: add tst_test.needs_debugfs flag
2026-10-21 8:59 [LTP] [PATCH v5 0/3] pmc_core: cover the PkgC LTR blocker counters Piotr Kubaj
@ 2026-10-21 8:59 ` Piotr Kubaj
2026-10-21 13:39 ` [LTP] " linuxtestproject.agent
2026-10-21 8:59 ` [LTP] [PATCH v5 2/3] pmc_core: add test for pkgc_ltr_blocker_show Piotr Kubaj
2026-10-21 8:59 ` [LTP] [PATCH v5 3/3] pmc_core: add ltr_counter test Piotr Kubaj
2 siblings, 1 reply; 6+ messages in thread
From: Piotr Kubaj @ 2026-10-21 8:59 UTC (permalink / raw)
To: ltp; +Cc: helena.anna.dubel, tomasz.ossowski, rafael.j.wysocki,
daniel.niestepski
Tests reading debugfs files each carry the same statfs() and mount()
dance, because /sys/kernel/debug exists as a plain sysfs directory even
when debugfs is not mounted on it, so checking for the path is not
enough.
With needs_debugfs set the library mounts debugfs during setup, exits
with TCONF if it cannot, and unmounts it at the end only if it did the
mount. The flag implies needs_root. Add TST_DEBUGFS_PATH for tests to
build their paths from, register the flag in the metadata parser and add
a lib/newlib_tests self-test.
Signed-off-by: Piotr Kubaj <piotr.kubaj@intel.com>
---
doc/developers/writing_tests.rst | 3 +++
include/tst_fs.h | 6 +++++
include/tst_test.h | 8 ++++++
lib/newlib_tests/.gitignore | 1 +
lib/newlib_tests/runtest.sh | 1 +
lib/newlib_tests/tst_needs_debugfs.c | 32 +++++++++++++++++++++++
lib/tst_test.c | 38 ++++++++++++++++++++++++++++
metadata/metaparse.c | 1 +
8 files changed, 90 insertions(+)
create mode 100644 lib/newlib_tests/tst_needs_debugfs.c
diff --git a/doc/developers/writing_tests.rst b/doc/developers/writing_tests.rst
index 9a56c0265..5d08cd907 100644
--- a/doc/developers/writing_tests.rst
+++ b/doc/developers/writing_tests.rst
@@ -448,6 +448,9 @@ shell API. For tests using the new shell loader library, see
* - .needs_cpu_vendor
- \-
+ * - .needs_debugfs
+ - \-
+
* - .needs_devfs
- \-
diff --git a/include/tst_fs.h b/include/tst_fs.h
index 80eb1292e..278bbab17 100644
--- a/include/tst_fs.h
+++ b/include/tst_fs.h
@@ -8,6 +8,7 @@
/* man 2 statfs or kernel-source/include/uapi/linux/magic.h */
#define TST_BTRFS_MAGIC 0x9123683E
+#define TST_DEBUGFS_MAGIC 0x64626720
#define TST_NFS_MAGIC 0x6969
#define TST_RAMFS_MAGIC 0x858458f6
#define TST_TMPFS_MAGIC 0x01021994
@@ -58,6 +59,11 @@ enum {
#define OVL_WORK OVL_BASE_MNTPOINT"/work"
#define OVL_MNT OVL_BASE_MNTPOINT"/ovl"
+/**
+ * TST_DEBUGFS_PATH - Where debugfs is mounted by tst_test.needs_debugfs.
+ */
+#define TST_DEBUGFS_PATH "/sys/kernel/debug"
+
/*
* @path: path is the pathname of any file within the mounted file system
* @mult: mult should be TST_KB, TST_MB or TST_GB
diff --git a/include/tst_test.h b/include/tst_test.h
index 80b46f9b1..9e464ea85 100644
--- a/include/tst_test.h
+++ b/include/tst_test.h
@@ -358,6 +358,13 @@ struct tst_fs {
* needed for tests that need to create device files since tmpfs
* at /tmp is usually mounted with 'nodev' option.
*
+ * @needs_debugfs: If set debugfs is mounted at TST_DEBUGFS_PATH i.e.
+ * /sys/kernel/debug, unless it is mounted there already, and
+ * the test exits with TCONF if it cannot be. The library
+ * unmounts it at the end of the test if it mounted it. Both
+ * mounting debugfs and reading its content require root, so
+ * the flag implies tst_test.needs_root.
+ *
* @restore_wallclock: Saves wall clock at the start of the test and restores
* it at the end with the help of monotonic timers.
* Testcases that modify system wallclock use this to
@@ -576,6 +583,7 @@ struct tst_fs {
unsigned int child_needs_reinit:1;
unsigned int runs_script:1;
unsigned int needs_devfs:1;
+ unsigned int needs_debugfs:1;
unsigned int restore_wallclock:1;
unsigned int all_filesystems:1;
diff --git a/lib/newlib_tests/.gitignore b/lib/newlib_tests/.gitignore
index 3a6e1643c..24008fa5c 100644
--- a/lib/newlib_tests/.gitignore
+++ b/lib/newlib_tests/.gitignore
@@ -59,6 +59,7 @@ tst_needs_cmds05
tst_needs_cmds06
tst_needs_cmds07
tst_needs_cmds08
+tst_needs_debugfs
test_runtime01
test_runtime02
test_children_cleanup
diff --git a/lib/newlib_tests/runtest.sh b/lib/newlib_tests/runtest.sh
index 71808ef8b..1a51d8b7a 100755
--- a/lib/newlib_tests/runtest.sh
+++ b/lib/newlib_tests/runtest.sh
@@ -27,6 +27,7 @@ tst_expiration_timer
tst_filesystems01
tst_fuzzy_sync0[1-3]
tst_needs_cmds0[1-36-8]
+tst_needs_debugfs
tst_res_hexd
tst_safe_sscanf
tst_strstatus}"
diff --git a/lib/newlib_tests/tst_needs_debugfs.c b/lib/newlib_tests/tst_needs_debugfs.c
new file mode 100644
index 000000000..7689665a4
--- /dev/null
+++ b/lib/newlib_tests/tst_needs_debugfs.c
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2026 Piotr Kubaj <piotr.kubaj@intel.com>
+ */
+
+/*
+ * Test that tst_test.needs_debugfs gets debugfs mounted for the test.
+ *
+ * Run it both with debugfs mounted and with it unmounted, it has to pass either
+ * way. In the latter case the library reports mounting debugfs and has to leave
+ * it unmounted once the test is over.
+ */
+
+#include <sys/vfs.h>
+#include "tst_test.h"
+
+static void run(void)
+{
+ struct statfs sfs;
+
+ SAFE_STATFS(TST_DEBUGFS_PATH, &sfs);
+
+ if (sfs.f_type == TST_DEBUGFS_MAGIC)
+ tst_res(TPASS, "debugfs is mounted at %s", TST_DEBUGFS_PATH);
+ else
+ tst_res(TFAIL, "debugfs is not mounted at %s", TST_DEBUGFS_PATH);
+}
+
+static struct tst_test test = {
+ .needs_debugfs = 1,
+ .test_all = run,
+};
diff --git a/lib/tst_test.c b/lib/tst_test.c
index 10ff73397..a42d3f462 100644
--- a/lib/tst_test.c
+++ b/lib/tst_test.c
@@ -15,6 +15,7 @@
#include <errno.h>
#include <sys/mount.h>
#include <sys/types.h>
+#include <sys/vfs.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <math.h>
@@ -81,6 +82,7 @@ struct context {
tst_atomic_t abort_flag;
uint32_t mntpoint_mounted:1;
uint32_t ovl_mounted:1;
+ uint32_t debugfs_mounted:1;
uint32_t tdebug;
};
@@ -1224,6 +1226,31 @@ static void prepare_and_mount_dev_fs(const char *mntpoint)
}
}
+static void mount_debugfs(void)
+{
+ struct statfs sfs;
+
+ /*
+ * TST_DEBUGFS_PATH exists as a plain sysfs directory whenever
+ * CONFIG_DEBUG_FS is enabled, whether or not debugfs is mounted on it,
+ * so the filesystem type has to be checked rather than the directory
+ * being present.
+ */
+ if (statfs(TST_DEBUGFS_PATH, &sfs))
+ tst_brk(TCONF | TERRNO, "Can't statfs %s", TST_DEBUGFS_PATH);
+
+ if (sfs.f_type == TST_DEBUGFS_MAGIC)
+ return;
+
+ if (mount("debugfs", TST_DEBUGFS_PATH, "debugfs", 0, NULL)) {
+ tst_brk(TCONF | TERRNO, "Can't mount debugfs at %s",
+ TST_DEBUGFS_PATH);
+ }
+
+ tst_res(TINFO, "Mounted debugfs at %s", TST_DEBUGFS_PATH);
+ context->debugfs_mounted = 1;
+}
+
static void prepare_and_mount_hugetlb_fs(void)
{
if (access(PATH_MM_HUGEPAGES, F_OK))
@@ -1474,6 +1501,9 @@ static void do_setup(int argc, char *argv[])
if (tst_test->needs_kconfigs && tst_kconfig_check(tst_test->needs_kconfigs))
tst_brk(TCONF, "Aborting due to unsuitable kernel config, see above!");
+ if (tst_test->needs_debugfs)
+ tst_test->needs_root = 1;
+
if (tst_test->needs_root && geteuid() != 0)
tst_brk(TCONF, "Test needs to be run as root");
@@ -1565,6 +1595,9 @@ static void do_setup(int argc, char *argv[])
"Two or more of needs_{rofs, devfs, device, hugetlbfs} are set");
}
+ if (tst_test->needs_debugfs)
+ mount_debugfs();
+
if (tst_test->needs_devfs)
prepare_and_mount_dev_fs(tst_test->mntpoint);
@@ -1676,6 +1709,11 @@ static void do_cleanup(void)
if (context->mntpoint_mounted)
tst_umount(tst_test->mntpoint);
+ if (context->debugfs_mounted) {
+ tst_umount(TST_DEBUGFS_PATH);
+ context->debugfs_mounted = 0;
+ }
+
if (tst_test->needs_device && tdev.dev)
tst_release_device(tdev.dev);
diff --git a/metadata/metaparse.c b/metadata/metaparse.c
index cb141c383..7f2c09037 100644
--- a/metadata/metaparse.c
+++ b/metadata/metaparse.c
@@ -1169,6 +1169,7 @@ static struct typemap tst_test_typemap[] = {
{.id = "child_needs_reinit", .type = DATA_BOOL},
{.id = "runs_script", .type = DATA_BOOL},
{.id = "needs_devfs", .type = DATA_BOOL},
+ {.id = "needs_debugfs", .type = DATA_BOOL},
{.id = "restore_wallclock", .type = DATA_BOOL},
{.id = "all_filesystems", .type = DATA_BOOL},
{.id = "skip_in_lockdown", .type = DATA_BOOL},
--
2.47.3
---------------------------------------------------------------------
Intel Technology Poland sp. z o.o.
ul. Slowackiego 173 | 80-298 Gdansk | Sad Rejonowy Gdansk Polnoc | VII Wydzial Gospodarczy Krajowego Rejestru Sadowego - KRS 101882 | NIP 957-07-52-316 | Kapital zakladowy 200.000 PLN.
Spolka oswiadcza, ze posiada status duzego przedsiebiorcy w rozumieniu ustawy z dnia 8 marca 2013 r. o przeciwdzialaniu nadmiernym opoznieniom w transakcjach handlowych.
Ta wiadomosc wraz z zalacznikami jest przeznaczona dla okreslonego adresata i moze zawierac informacje poufne. W razie przypadkowego otrzymania tej wiadomosci, prosimy o powiadomienie nadawcy oraz trwale jej usuniecie; jakiekolwiek przegladanie lub rozpowszechnianie jest zabronione.
This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). If you are not the intended recipient, please contact the sender and delete all copies; any review or distribution by others is strictly prohibited.
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [LTP] lib: add tst_test.needs_debugfs flag
2026-10-21 8:59 ` [LTP] [PATCH v5 1/3] lib: add tst_test.needs_debugfs flag Piotr Kubaj
@ 2026-10-21 13:39 ` linuxtestproject.agent
0 siblings, 0 replies; 6+ messages in thread
From: linuxtestproject.agent @ 2026-10-21 13:39 UTC (permalink / raw)
To: Piotr Kubaj; +Cc: ltp
Hi Piotr,
On 21 October 2026, Piotr Kubaj wrote:
> lib: add tst_test.needs_debugfs flag
--- [PATCH 1/3] ---
> diff --git a/metadata/metaparse.c b/metadata/metaparse.c
> + {.id = "needs_debugfs", .type = DATA_BOOL},
Medium: Add needs_debugfs -> needs_root to implies[] and cover it with a
metadata fixture. Parsing tst_needs_debugfs.c currently emits
needs_debugfs:true without needs_root:true, although the same executable
rejects non-root execution, so metadata consumers miss its root requirement.
--- [PATCH 2/3] ---
> diff --git a/testcases/kernel/power_management/pkgc_ltr_blocker_show.c b/testcases/kernel/power_management/pkgc_ltr_blocker_show.c
> + * arbitrary u32 read out of the wrong offset averages 2^31, orders of
> + * magnitude past what the bound allows.
Low: This concerns the absolute value, not the difference between reads
from the same wrong offset. Replaying 3735928559 -> 3735928560 passes the
actual delta check, as does an unchanged arbitrary value. Narrow the
description and commit-message coverage claim to format/rate validation,
or provide an independent oracle for the offset regression.
--- [PATCH 3/3] ---
> diff --git a/testcases/kernel/power_management/ltr_counter.c b/testcases/kernel/power_management/ltr_counter.c
> + if (tst_timespec_diff_ms(now, last_change) >= QUIET_WINDOW * 1000) {
> + tst_res(TPASS,
> + "%d counters held still over %ds with every CPU busy",
> + cnt, QUIET_WINDOW);
> + return;
Medium: Replaying a counter that updates every 6.5 seconds satisfies the
idle-advance prerequisite, then passes after six seconds of load before
the next update; LOAD_WINDOW never protects this path. Establish a
conservative refresh bound with margin before declaring the counters
quiet. Reconcile the timing assumptions across the series: patch 2 calls
four seconds several refresh periods, while this patch reports roughly
six seconds per refresh.
> + if (sched_setaffinity(0, sizeof(one), &one)) {
> + tst_brk(TBROK | TERRNO,
> + "sched_setaffinity(CPU %d) failed", cpu);
> + }
> +
> + spin();
Medium: Affinity does not establish continuous load when a CPU-bandwidth
cgroup quota throttles all spinners together. During those idle gaps,
correct counters can advance and be reported as failures after the load
window. Detect throttling, including inherited limits, and report the
configuration as unsupported rather than blaming the counters.
> + cpu_set_t mask;
> + int nr_cpus;
> + int cpu = 0;
> +
> + if (sched_getaffinity(0, sizeof(mask), &mask))
> + tst_brk(TBROK | TERRNO, "sched_getaffinity() failed");
Medium: Both glibc and musl cpu_set_t hold only 1024 CPUs, so this call
fails with EINVAL when the kernel's possible-CPU mask is larger, even with
few CPUs online. Use CPU_ALLOC(tst_ncpus_max()), CPU_ALLOC_SIZE(), and the
*_S macros for both masks, with cleanup, following lib/tst_cpu.c.
Verdict - Needs revision
---
Note:
The agent can sometimes produce false positives although often its
findings are genuine. If you find issues with the review, please
comment this email or ignore the suggestions.
Regards,
LTP AI Reviewer
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply [flat|nested] 6+ messages in thread
* [LTP] [PATCH v5 2/3] pmc_core: add test for pkgc_ltr_blocker_show
2026-10-21 8:59 [LTP] [PATCH v5 0/3] pmc_core: cover the PkgC LTR blocker counters Piotr Kubaj
2026-10-21 8:59 ` [LTP] [PATCH v5 1/3] lib: add tst_test.needs_debugfs flag Piotr Kubaj
@ 2026-10-21 8:59 ` Piotr Kubaj
2026-10-21 8:59 ` [LTP] [PATCH v5 3/3] pmc_core: add ltr_counter test Piotr Kubaj
2 siblings, 0 replies; 6+ messages in thread
From: Piotr Kubaj @ 2026-10-21 8:59 UTC (permalink / raw)
To: ltp; +Cc: helena.anna.dubel, tomasz.ossowski, rafael.j.wysocki,
daniel.niestepski
Cover the pkgc_ltr_blocker_show debugfs file added in kernel commit
38c79dd63b72 ("platform/x86/intel/pmc: Enable PkgC LTR blocking
counter"), released in v7.2. It reports how many times an LTR posted by
each source blocked a package C-state entry.
The counter set is platform specific, so rather than matching a list of
names the test samples the file twice and checks that it reads to EOF,
that every line is a PKGC_PREVENT_LTR_<SOURCE> token followed by one u32
value, that the counter set is stable, and that no counter advances by
more entries than the package could have attempted over the interval.
The last check is what catches a telemetry region read at the wrong
offset: such a value overshoots the bound by orders of magnitude.
Tested on Nova Lake running 7.3.0-rc1.
Signed-off-by: Piotr Kubaj <piotr.kubaj@intel.com>
---
runtest/power_management_tests | 1 +
testcases/kernel/power_management/.gitignore | 1 +
.../power_management/pkgc_ltr_blocker_show.c | 311 ++++++++++++++++++
3 files changed, 313 insertions(+)
create mode 100644 testcases/kernel/power_management/pkgc_ltr_blocker_show.c
diff --git a/runtest/power_management_tests b/runtest/power_management_tests
index 5a22283d6..7708d3fd4 100644
--- a/runtest/power_management_tests
+++ b/runtest/power_management_tests
@@ -2,6 +2,7 @@
cpufreq_intel cpufreq_intel
rfim01 rfim01
high_freq_hwp_cap_cppc high_freq_hwp_cap_cppc
+pkgc_ltr_blocker_show pkgc_ltr_blocker_show
runpwtests03 runpwtests03.sh
runpwtests04 runpwtests04.sh
runpwtests06 runpwtests06.sh
diff --git a/testcases/kernel/power_management/.gitignore b/testcases/kernel/power_management/.gitignore
index 766f07901..b96084eca 100644
--- a/testcases/kernel/power_management/.gitignore
+++ b/testcases/kernel/power_management/.gitignore
@@ -1,3 +1,4 @@
high_freq_hwp_cap_cppc
cpufreq_intel
rfim01
+pkgc_ltr_blocker_show
diff --git a/testcases/kernel/power_management/pkgc_ltr_blocker_show.c b/testcases/kernel/power_management/pkgc_ltr_blocker_show.c
new file mode 100644
index 000000000..49366b6bf
--- /dev/null
+++ b/testcases/kernel/power_management/pkgc_ltr_blocker_show.c
@@ -0,0 +1,311 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Copyright (C) 2026 Piotr Kubaj <piotr.kubaj@intel.com>
+ */
+
+/*\
+ * Verify that pkgc_ltr_blocker_show reports well-formed package C-state LTR
+ * blocker counters that advance at a plausible rate.
+ *
+ * The intel_pmc_core driver prints one line per counter as "%-30s %-30u",
+ * where the name is a PKGC_PREVENT_LTR_<SOURCE> token and the value counts how
+ * many times an LTR posted by <SOURCE> blocked a package C-state entry.
+ *
+ * The counter set is platform specific, so instead of checking a hardcoded
+ * list the test samples the file twice and verifies that
+ *
+ * - the file can be read in full, so that a failing telemetry read in the
+ * driver is not mistaken for a short counter list,
+ * - every line is a PKGC_PREVENT_LTR_<SOURCE> token followed by exactly one
+ * unsigned value that fits in u32,
+ * - the set of counters does not change between the two reads,
+ * - no counter advances by more entries than the package could have attempted
+ * over the interval.
+ *
+ * The last check is the one that catches a telemetry region read at the wrong
+ * offset. The values are u32 and wrap, so the advance has to be evaluated
+ * modulo 2^32, and a wrap is then indistinguishable from a bogus read unless
+ * the delta is bounded by the time the counter had to advance over. An
+ * arbitrary u32 read out of the wrong offset averages 2^31, orders of
+ * magnitude past what the bound allows.
+ *
+ * The debugfs file is only created for a PMC that exposes a package C-state
+ * telemetry endpoint (pc_guid in the driver's pmc_dev_info), so on platforms
+ * without one the test is not applicable.
+ *
+ * The test needs root because /sys/kernel/debug is mode 0700 and owned by
+ * root, so the counter file cannot be opened otherwise, and because the test
+ * library mounts debugfs if it is not mounted already.
+ */
+
+#include <time.h>
+#include "tst_clocks.h"
+#include "tst_safe_stdio.h"
+#include "tst_test.h"
+#include "tst_timer.h"
+
+#define PATH TST_DEBUGFS_PATH "/pmc_core/pkgc_ltr_blocker_show"
+#define PREFIX "PKGC_PREVENT_LTR_"
+
+/* Nova Lake reports 5 counters, leave room for future platforms */
+#define MAX_COUNTERS 64
+#define NAME_LEN 64
+
+/*
+ * Cap, in seconds, on how long to poll for a second sample that differs from
+ * the first one. On an idle Nova Lake the counters advance in bursts a few
+ * seconds apart, because a blocked entry needs the package to attempt a C-state
+ * entry in the first place, so a window of a few seconds is what it takes for
+ * the delta check to have anything to look at. TST_RETRY_FN_EXP_BACKOFF() can
+ * end up sleeping up to about twice the cap in total, hence the modest value
+ * here.
+ */
+#define MAX_SAMPLE_DELAY 4
+
+/*
+ * Allowance for how stale the first sample can be. The PMC refreshes the
+ * telemetry region behind these counters asynchronously, so the first read can
+ * land just before a refresh and return a snapshot that is almost a whole
+ * refresh period old. The retry then observes a period worth of blocked entries
+ * after only microseconds of measured time, which the measured interval on its
+ * own does not account for.
+ *
+ * The allowance is the same window the test is willing to poll a refresh out
+ * of, which on Nova Lake is several times the observed refresh period.
+ */
+#define REFRESH_AGE_US (MAX_SAMPLE_DELAY * 1000000ULL)
+
+/*
+ * Blocked entries per microsecond the bound allows. Each count stands for one
+ * package C-state entry that was blocked, and entries can only be attempted as
+ * fast as the package can cycle between idle and running, which takes
+ * microseconds. Ten per microsecond is therefore already past what the hardware
+ * can produce - the counters were observed advancing by about 6 per second on
+ * an idle Nova Lake - and the headroom costs the check nothing, because the
+ * bogus read it exists to catch overshoots it by orders of magnitude anyway.
+ */
+#define MAX_BLOCKS_PER_US 10
+
+struct snapshot {
+ char names[MAX_COUNTERS][NAME_LEN];
+ uint32_t values[MAX_COUNTERS];
+ int cnt;
+};
+
+static struct snapshot first, second;
+static int fails;
+
+/*
+ * Largest advance a counter can plausibly show over two samples taken
+ * @elapsed_us microseconds apart.
+ *
+ * The interval the counter had to advance over is the measured one widened by
+ * REFRESH_AGE_US, because the sampled values come from a telemetry snapshot
+ * that is already up to a refresh period old, not from the instants the file
+ * was read.
+ */
+static unsigned long long max_plausible_delta(unsigned long long elapsed_us)
+{
+ return (elapsed_us + REFRESH_AGE_US) * MAX_BLOCKS_PER_US;
+}
+
+static void read_snapshot(struct snapshot *snap)
+{
+ char line[256];
+ FILE *fp;
+
+ snap->cnt = 0;
+ fp = SAFE_FOPEN(PATH, "r");
+
+ while (fgets(line, sizeof(line), fp)) {
+ char name[NAME_LEN], value[NAME_LEN], extra;
+ unsigned long long parsed;
+
+ line[strcspn(line, "\n")] = '\0';
+
+ /*
+ * The value is scanned as a string rather than with %u so that
+ * a signed or out of range value is rejected instead of being
+ * quietly converted. The trailing %c rejects a third token; the
+ * space in front of it skips the padding the driver emits.
+ */
+ if (sscanf(line, "%63s %63s %c", name, value, &extra) != 2) {
+ tst_res(TFAIL, "malformed counter line: '%s'", line);
+ fails++;
+ continue;
+ }
+
+ if (strncmp(name, PREFIX, sizeof(PREFIX) - 1) ||
+ !name[sizeof(PREFIX) - 1]) {
+ tst_res(TFAIL, "counter '%s' is not a " PREFIX "<SOURCE> token",
+ name);
+ fails++;
+ continue;
+ }
+
+ if (value[strspn(value, "0123456789")]) {
+ tst_res(TFAIL, "counter '%s' has a non-numeric value '%s'",
+ name, value);
+ fails++;
+ continue;
+ }
+
+ parsed = strtoull(value, NULL, 10);
+
+ if (parsed > UINT32_MAX) {
+ tst_res(TFAIL, "counter '%s' value '%s' does not fit in u32",
+ name, value);
+ fails++;
+ continue;
+ }
+
+ if (snap->cnt == MAX_COUNTERS)
+ tst_brk(TBROK, "more than %d counters reported", MAX_COUNTERS);
+
+ strcpy(snap->names[snap->cnt], name);
+ snap->values[snap->cnt] = parsed;
+ snap->cnt++;
+ }
+
+ /*
+ * A telemetry read that fails in the driver aborts the seq_file read
+ * instead of ending it at EOF, so without this the counters lost to the
+ * error would look like a platform reporting fewer of them.
+ */
+ if (ferror(fp))
+ tst_brk(TFAIL, "reading " PATH " failed after %d counters", snap->cnt);
+
+ SAFE_FCLOSE(fp);
+}
+
+static void setup(void)
+{
+ if (access(PATH, R_OK))
+ tst_brk(TCONF | TERRNO, "%s not available", PATH);
+}
+
+/*
+ * Take the second sample, reporting whether it differs from the first one.
+ *
+ * A blocked entry is a sporadic event and the counters are backed by a PMT
+ * telemetry region that the PMC refreshes asynchronously, so the second sample
+ * is retried until it observes a change instead of being taken after a fixed
+ * delay. A differing counter count also counts as a difference, so that the
+ * mismatch is reported rather than polled over.
+ */
+static int second_sample_differs(void)
+{
+ read_snapshot(&second);
+
+ if (first.cnt != second.cnt)
+ return 1;
+
+ for (int i = 0; i < first.cnt; i++) {
+ if (first.values[i] != second.values[i])
+ return 1;
+ }
+
+ return 0;
+}
+
+static void run(void)
+{
+ struct timespec start, end;
+ unsigned long long elapsed_us, max_delta;
+ int differs;
+
+ fails = 0;
+
+ tst_clock_gettime(CLOCK_MONOTONIC, &start);
+ read_snapshot(&first);
+
+ /*
+ * The retry below re-reads the file until a counter moves, so a format
+ * problem has to stop the test here rather than be reported again on
+ * every retry, and the delta check has nothing to say about lines that
+ * do not parse anyway.
+ */
+ if (fails)
+ tst_brk(TFAIL, "%d malformed counter line(s), skipping the delta check",
+ fails);
+
+ if (!first.cnt)
+ tst_brk(TFAIL, "no LTR blocker counters reported");
+
+ differs = TST_RETRY_FN_EXP_BACKOFF(second_sample_differs(),
+ TST_RETVAL_NOTNULL, MAX_SAMPLE_DELAY);
+
+ tst_clock_gettime(CLOCK_MONOTONIC, &end);
+
+ elapsed_us = tst_timespec_diff_us(end, start);
+ max_delta = max_plausible_delta(elapsed_us);
+
+ if (!differs) {
+ tst_res(TINFO, "no counter changed over %lluus, no entry was blocked",
+ elapsed_us);
+ }
+
+ if (first.cnt != second.cnt) {
+ tst_brk(TFAIL, "counter count changed between reads: %d -> %d",
+ first.cnt, second.cnt);
+ }
+
+ for (int i = 0; i < first.cnt; i++) {
+ uint32_t delta;
+
+ if (strcmp(first.names[i], second.names[i])) {
+ tst_res(TFAIL, "counter %d renamed between reads: %s -> %s",
+ i, first.names[i], second.names[i]);
+ fails++;
+ continue;
+ }
+
+ tst_res(TDEBUG, "%s: %u -> %u", first.names[i],
+ first.values[i], second.values[i]);
+
+ /*
+ * The values are u32 and wrap, so the advance has to be
+ * computed with modular arithmetic rather than by comparing the
+ * second value against the first one. A delta of 0 is normal:
+ * it means no entry was blocked by that source over the
+ * interval, and most counters on an idle system read 0
+ * permanently.
+ */
+ delta = second.values[i] - first.values[i];
+
+ if (delta > max_delta) {
+ tst_res(TFAIL,
+ "%s advanced by %u over %lluus, at most %llu plausible: %u -> %u",
+ first.names[i], delta, elapsed_us, max_delta,
+ first.values[i], second.values[i]);
+ fails++;
+ }
+ }
+
+ if (!fails) {
+ tst_res(TPASS, "%d LTR blocker counters well-formed, advancing plausibly",
+ first.cnt);
+ }
+}
+
+static struct tst_test test = {
+ .min_kver = "7.2",
+ .needs_debugfs = 1,
+ .needs_kconfigs = (const char *const []) {
+ "CONFIG_DEBUG_FS",
+ "CONFIG_INTEL_PMC_CORE",
+ NULL
+ },
+ .supported_archs = (const char *const []) {
+ "x86",
+ "x86_64",
+ NULL
+ },
+ .tags = (const struct tst_tag[]) {
+ {"linux-git", "38c79dd63b72e36919ef097d4e5025ca0fa17f34"},
+ {}
+ },
+ .setup = setup,
+ .test_all = run
+};
--
2.47.3
---------------------------------------------------------------------
Intel Technology Poland sp. z o.o.
ul. Slowackiego 173 | 80-298 Gdansk | Sad Rejonowy Gdansk Polnoc | VII Wydzial Gospodarczy Krajowego Rejestru Sadowego - KRS 101882 | NIP 957-07-52-316 | Kapital zakladowy 200.000 PLN.
Spolka oswiadcza, ze posiada status duzego przedsiebiorcy w rozumieniu ustawy z dnia 8 marca 2013 r. o przeciwdzialaniu nadmiernym opoznieniom w transakcjach handlowych.
Ta wiadomosc wraz z zalacznikami jest przeznaczona dla okreslonego adresata i moze zawierac informacje poufne. W razie przypadkowego otrzymania tej wiadomosci, prosimy o powiadomienie nadawcy oraz trwale jej usuniecie; jakiekolwiek przegladanie lub rozpowszechnianie jest zabronione.
This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). If you are not the intended recipient, please contact the sender and delete all copies; any review or distribution by others is strictly prohibited.
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread* [LTP] [PATCH v5 3/3] pmc_core: add ltr_counter test
2026-10-21 8:59 [LTP] [PATCH v5 0/3] pmc_core: cover the PkgC LTR blocker counters Piotr Kubaj
2026-10-21 8:59 ` [LTP] [PATCH v5 1/3] lib: add tst_test.needs_debugfs flag Piotr Kubaj
2026-10-21 8:59 ` [LTP] [PATCH v5 2/3] pmc_core: add test for pkgc_ltr_blocker_show Piotr Kubaj
@ 2026-10-21 8:59 ` Piotr Kubaj
2 siblings, 0 replies; 6+ messages in thread
From: Piotr Kubaj @ 2026-10-21 8:59 UTC (permalink / raw)
To: ltp; +Cc: helena.anna.dubel, tomasz.ossowski, rafael.j.wysocki,
daniel.niestepski
Test the invariant behind the pkgc_ltr_blocker_show counters: they only
advance while a package C-state entry can be attempted, which needs every
CPU in the package idle. A counter that keeps advancing with every CPU
busy is not counting blocked entries, which is what a telemetry region
read at the wrong offset looks like.
The test idles until it sees a counter advance, so that a stuck counter
cannot pass for free, then pins a busy loop to every CPU and requires the
counters to hold still. The PMC refreshes the telemetry region in bursts,
roughly six seconds apart on Nova Lake, so the load phase waits for the
counters to go quiet rather than sampling after a fixed delay.
Tested on Nova Lake running 7.3.0-rc1.
Signed-off-by: Piotr Kubaj <piotr.kubaj@intel.com>
---
runtest/power_management_tests | 1 +
testcases/kernel/power_management/.gitignore | 1 +
.../kernel/power_management/ltr_counter.c | 377 ++++++++++++++++++
3 files changed, 379 insertions(+)
create mode 100644 testcases/kernel/power_management/ltr_counter.c
diff --git a/runtest/power_management_tests b/runtest/power_management_tests
index 7708d3fd4..c4f661257 100644
--- a/runtest/power_management_tests
+++ b/runtest/power_management_tests
@@ -3,6 +3,7 @@ cpufreq_intel cpufreq_intel
rfim01 rfim01
high_freq_hwp_cap_cppc high_freq_hwp_cap_cppc
pkgc_ltr_blocker_show pkgc_ltr_blocker_show
+ltr_counter ltr_counter
runpwtests03 runpwtests03.sh
runpwtests04 runpwtests04.sh
runpwtests06 runpwtests06.sh
diff --git a/testcases/kernel/power_management/.gitignore b/testcases/kernel/power_management/.gitignore
index b96084eca..f54cec368 100644
--- a/testcases/kernel/power_management/.gitignore
+++ b/testcases/kernel/power_management/.gitignore
@@ -2,3 +2,4 @@ high_freq_hwp_cap_cppc
cpufreq_intel
rfim01
pkgc_ltr_blocker_show
+ltr_counter
diff --git a/testcases/kernel/power_management/ltr_counter.c b/testcases/kernel/power_management/ltr_counter.c
new file mode 100644
index 000000000..50d9c574f
--- /dev/null
+++ b/testcases/kernel/power_management/ltr_counter.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+/*
+ * Copyright (C) 2026 Intel - http://www.intel.com/
+ */
+
+/*\
+ * Verify that the package C-state LTR blocker counters only advance while a
+ * package C-state entry can be attempted.
+ *
+ * The pkgc_ltr_blocker_show debugfs file reports, per source, how many times an
+ * LTR posted by that source blocked a package C-state entry. Such an entry is
+ * only attempted once every CPU in the package has gone idle, so a workload
+ * that keeps every CPU busy has to stop the counters. A counter that keeps
+ * advancing under full load is not counting blocked package C-state entries,
+ * which is what a telemetry region read at the wrong offset looks like.
+ *
+ * The test first lets the system idle until it sees a counter advance, so that
+ * a counter stuck at a constant value cannot pass the load phase for free. It
+ * then pins a busy loop to every CPU and requires the counters to hold still.
+ *
+ * The counters are backed by a PMT telemetry region that the PMC refreshes
+ * asynchronously and in bursts - roughly six seconds apart on Nova Lake - so
+ * the load phase waits for the counters to go quiet rather than sampling them
+ * after a fixed delay. Entries blocked before the load took hold surface in a
+ * later refresh, while a counter that is not tracking blocked entries never
+ * goes quiet at all and runs the window out.
+ *
+ * The test needs root because /sys/kernel/debug is mode 0700 and owned by
+ * root, so the counter file cannot be opened otherwise, and because the test
+ * library mounts debugfs if it is not mounted already.
+ */
+
+#define _GNU_SOURCE
+
+#include <time.h>
+#include "tst_clocks.h"
+#include "tst_safe_stdio.h"
+#include "tst_test.h"
+#include "tst_timer.h"
+
+#define PATH TST_DEBUGFS_PATH "/pmc_core/pkgc_ltr_blocker_show"
+
+/* Nova Lake reports 5 counters, leave room for future platforms */
+#define MAX_COUNTERS 64
+#define NAME_LEN 64
+
+#define POLL_MS 500
+
+/*
+ * Seconds to wait for a counter to advance while the system idles. The bursts
+ * are about six seconds apart on Nova Lake, so this leaves room for two of
+ * them before giving up on the platform blocking a package C-state entry at
+ * all.
+ */
+#define IDLE_WINDOW 15
+
+/*
+ * How long, in seconds, the counters have to hold still under load, and the cap
+ * on how long to wait for that. One burst period of silence is what tells a
+ * stopped counter from one that is merely between refreshes, and the cap has to
+ * be long enough on top of that for a burst counted before the load took hold
+ * to arrive and drain.
+ *
+ * Both are sized for the roughly six second refresh cadence seen on Nova Lake.
+ * A platform whose PMC refreshes the region less often than QUIET_WINDOW needs
+ * it raised, otherwise a counter that is not tracking blocked entries at all
+ * sits still between two refreshes and passes the check.
+ */
+#define QUIET_WINDOW 6
+#define LOAD_WINDOW 20
+
+static char names[MAX_COUNTERS][NAME_LEN];
+static int cnt;
+static pid_t *spinners;
+static int nr_spinners;
+static volatile unsigned long long burn;
+
+/*
+ * Read the counter values, checking that the counter set itself has not changed
+ * since setup(). Only the values are of interest here - the format of the file
+ * is what the pkgc_ltr_blocker_show test covers - so anything unexpected about
+ * the lines makes this test unable to run rather than failing it.
+ */
+static void read_counters(uint32_t *values)
+{
+ char line[256];
+ FILE *fp;
+ int n = 0;
+
+ fp = SAFE_FOPEN(PATH, "r");
+
+ while (fgets(line, sizeof(line), fp)) {
+ char name[NAME_LEN];
+ unsigned int value;
+
+ line[strcspn(line, "\n")] = '\0';
+
+ if (n == MAX_COUNTERS)
+ tst_brk(TBROK, "more than %d counters reported", MAX_COUNTERS);
+
+ if (sscanf(line, "%63s %u", name, &value) != 2)
+ tst_brk(TBROK, "unexpected line format: '%s'", line);
+
+ values[n] = value;
+
+ if (cnt && strcmp(name, names[n])) {
+ tst_brk(TBROK, "counter %d renamed during the test: %s -> %s",
+ n, names[n], name);
+ }
+
+ if (!cnt)
+ strcpy(names[n], name);
+
+ n++;
+ }
+
+ /*
+ * A telemetry read that fails in the driver aborts the seq_file read
+ * instead of ending it at EOF, so without this the counters lost to the
+ * error would look like counters that stopped advancing.
+ */
+ if (ferror(fp))
+ tst_brk(TBROK, "reading " PATH " failed after %d counters", n);
+
+ SAFE_FCLOSE(fp);
+
+ if (cnt && n != cnt)
+ tst_brk(TBROK, "counter count changed during the test: %d -> %d", cnt, n);
+
+ cnt = n;
+}
+
+static void spin(void)
+{
+ /* volatile so that the loop survives optimization */
+ for (;;)
+ burn++;
+}
+
+/*
+ * Keep every CPU the test may run on busy, so that the package cannot go idle
+ * and no package C-state entry can be attempted.
+ *
+ * The spinners are pinned one per CPU taken from the test's own affinity mask,
+ * rather than left to the load balancer, so that a CPU is not left idle by a
+ * scheduling decision. Offline CPUs and a restricted mask are handled by
+ * construction.
+ */
+static void start_load(void)
+{
+ cpu_set_t mask;
+ int nr_cpus;
+ int cpu = 0;
+
+ if (sched_getaffinity(0, sizeof(mask), &mask))
+ tst_brk(TBROK | TERRNO, "sched_getaffinity() failed");
+
+ nr_cpus = CPU_COUNT(&mask);
+ spinners = SAFE_MALLOC(nr_cpus * sizeof(*spinners));
+
+ for (int i = 0; i < nr_cpus; i++) {
+ pid_t pid;
+
+ while (!CPU_ISSET(cpu, &mask))
+ cpu++;
+
+ pid = SAFE_FORK();
+
+ if (!pid) {
+ cpu_set_t one;
+
+ CPU_ZERO(&one);
+ CPU_SET(cpu, &one);
+
+ if (sched_setaffinity(0, sizeof(one), &one)) {
+ tst_brk(TBROK | TERRNO,
+ "sched_setaffinity(CPU %d) failed", cpu);
+ }
+
+ spin();
+ }
+
+ /*
+ * Counted only once the child exists, so that a fork() that
+ * fails half way through does not leave stop_load() killing
+ * pids that were never written. A zeroed entry would mean
+ * kill() signalling the whole process group.
+ */
+ spinners[nr_spinners++] = pid;
+ cpu++;
+ }
+
+ tst_res(TINFO, "keeping %d CPUs busy", nr_spinners);
+}
+
+static void stop_load(void)
+{
+ if (!spinners)
+ return;
+
+ for (int i = 0; i < nr_spinners; i++) {
+ SAFE_KILL(spinners[i], SIGKILL);
+ SAFE_WAITPID(spinners[i], NULL, 0);
+ }
+
+ free(spinners);
+ spinners = NULL;
+ nr_spinners = 0;
+}
+
+/*
+ * Wait for a counter to advance while the system idles, reporting how many did.
+ *
+ * This is what tells a counter that stops under load from one that never moves.
+ * On an idle system most counters read 0 permanently, because nothing posts an
+ * LTR that would block a package C-state entry, so a single counter advancing
+ * is enough.
+ */
+static int wait_for_advance(const uint32_t *baseline)
+{
+ uint32_t values[MAX_COUNTERS];
+ struct timespec start, now;
+ int advanced = 0;
+
+ tst_clock_gettime(CLOCK_MONOTONIC, &start);
+
+ for (;;) {
+ read_counters(values);
+
+ for (int i = 0; i < cnt; i++) {
+ if (values[i] == baseline[i])
+ continue;
+
+ tst_res(TINFO, "%s advanced by %u while idle", names[i],
+ values[i] - baseline[i]);
+ advanced++;
+ }
+
+ if (advanced)
+ return advanced;
+
+ tst_clock_gettime(CLOCK_MONOTONIC, &now);
+
+ if (tst_timespec_diff_ms(now, start) >= IDLE_WINDOW * 1000)
+ return 0;
+
+ usleep(POLL_MS * 1000);
+ }
+}
+
+/*
+ * Require the counters to hold still for QUIET_WINDOW seconds of full load.
+ */
+static void check_counters_stop(void)
+{
+ uint32_t first[MAX_COUNTERS], prev[MAX_COUNTERS], values[MAX_COUNTERS];
+ struct timespec changed_at[MAX_COUNTERS];
+ struct timespec start, last_change, now;
+
+ read_counters(first);
+ memcpy(prev, first, sizeof(prev));
+
+ tst_clock_gettime(CLOCK_MONOTONIC, &start);
+ last_change = start;
+
+ for (int i = 0; i < cnt; i++)
+ changed_at[i] = start;
+
+ for (;;) {
+ usleep(POLL_MS * 1000);
+
+ read_counters(values);
+ tst_clock_gettime(CLOCK_MONOTONIC, &now);
+
+ for (int i = 0; i < cnt; i++) {
+ if (values[i] == prev[i])
+ continue;
+
+ changed_at[i] = now;
+ last_change = now;
+ }
+
+ memcpy(prev, values, sizeof(prev));
+
+ if (tst_timespec_diff_ms(now, last_change) >= QUIET_WINDOW * 1000) {
+ tst_res(TPASS,
+ "%d counters held still over %ds with every CPU busy",
+ cnt, QUIET_WINDOW);
+ return;
+ }
+
+ if (tst_timespec_diff_ms(now, start) < LOAD_WINDOW * 1000)
+ continue;
+
+ /*
+ * A counter that last changed a quiet window ago or more was
+ * only draining entries blocked before the load took hold. The
+ * ones that changed since are the ones that never stopped.
+ */
+ for (int i = 0; i < cnt; i++) {
+ if (tst_timespec_diff_ms(now, changed_at[i]) >=
+ QUIET_WINDOW * 1000)
+ continue;
+
+ tst_res(TFAIL,
+ "%s kept advancing over %ds with every CPU busy: %u -> %u",
+ names[i], LOAD_WINDOW, first[i], values[i]);
+ }
+
+ return;
+ }
+}
+
+static void setup(void)
+{
+ uint32_t values[MAX_COUNTERS];
+
+ if (access(PATH, R_OK))
+ tst_brk(TCONF | TERRNO, "%s not available", PATH);
+
+ /* Learns the counter set the rest of the test compares against */
+ read_counters(values);
+
+ if (!cnt)
+ tst_brk(TBROK, "no LTR blocker counters reported");
+}
+
+static void cleanup(void)
+{
+ stop_load();
+}
+
+static void run(void)
+{
+ uint32_t baseline[MAX_COUNTERS];
+
+ read_counters(baseline);
+
+ /*
+ * Without a counter that moves while idle there is nothing to stop
+ * under load, a stuck counter would look exactly the same.
+ */
+ if (!wait_for_advance(baseline)) {
+ tst_brk(TCONF,
+ "no LTR blocked a package C-state entry over %ds of idling",
+ IDLE_WINDOW);
+ }
+
+ start_load();
+ check_counters_stop();
+ stop_load();
+}
+
+static struct tst_test test = {
+ .min_kver = "7.2",
+ .timeout = 90,
+ .needs_debugfs = 1,
+ .forks_child = 1,
+ .needs_kconfigs = (const char *const []) {
+ "CONFIG_DEBUG_FS",
+ "CONFIG_INTEL_PMC_CORE",
+ NULL
+ },
+ .supported_archs = (const char *const []) {
+ "x86",
+ "x86_64",
+ NULL
+ },
+ .tags = (const struct tst_tag[]) {
+ {"linux-git", "38c79dd63b72e36919ef097d4e5025ca0fa17f34"},
+ {}
+ },
+ .setup = setup,
+ .cleanup = cleanup,
+ .test_all = run
+};
--
2.47.3
---------------------------------------------------------------------
Intel Technology Poland sp. z o.o.
ul. Slowackiego 173 | 80-298 Gdansk | Sad Rejonowy Gdansk Polnoc | VII Wydzial Gospodarczy Krajowego Rejestru Sadowego - KRS 101882 | NIP 957-07-52-316 | Kapital zakladowy 200.000 PLN.
Spolka oswiadcza, ze posiada status duzego przedsiebiorcy w rozumieniu ustawy z dnia 8 marca 2013 r. o przeciwdzialaniu nadmiernym opoznieniom w transakcjach handlowych.
Ta wiadomosc wraz z zalacznikami jest przeznaczona dla okreslonego adresata i moze zawierac informacje poufne. W razie przypadkowego otrzymania tej wiadomosci, prosimy o powiadomienie nadawcy oraz trwale jej usuniecie; jakiekolwiek przegladanie lub rozpowszechnianie jest zabronione.
This e-mail and any attachments may contain confidential material for the sole use of the intended recipient(s). If you are not the intended recipient, please contact the sender and delete all copies; any review or distribution by others is strictly prohibited.
--
Mailing list info: https://lists.linux.it/listinfo/ltp
^ permalink raw reply related [flat|nested] 6+ messages in thread