All of lore.kernel.org
 help / color / mirror / Atom feed
From: Samir Mulani <samir@linux.ibm.com>
To: ltp@lists.linux.it
Cc: Samir Mulani <samir@linux.ibm.com>
Subject: [LTP] [PATCH v1 4/4] sched/psi: Add psi_accounting01 test
Date: Sat, 24 Oct 2026 04:30:44 +0200	[thread overview]
Message-ID: <20261024023044.80055-5-samir@linux.ibm.com> (raw)
In-Reply-To: <20261024023044.80055-1-samir@linux.ibm.com>

Add a test case to verify that PSI correctly records stall time when
tasks experience CPU, memory, and I/O pressure.

The test creates a cgroup v2 to isolate the workload and checks that:
- Running two CPU-heavy tasks on a single CPU increases cpu "some" total.
- Touching memory above memory.high triggers reclaim and increases
  memory "some" total.
- Doing synchronous O_DIRECT writes to a block device increases io "some"
  total.

If the system cannot create enough memory or I/O pressure (for example, on
very fast storage or systems without a test block device), the test cleanly
reports TCONF instead of failing.

The test requires cgroup v2 with memory controller enabled.

Signed-off-by: Samir Mulani <samir@linux.ibm.com>
---
 runtest/sched                                 |   1 +
 testcases/kernel/sched/psi/.gitignore         |   1 +
 testcases/kernel/sched/psi/psi_accounting01.c | 356 ++++++++++++++++++
 3 files changed, 358 insertions(+)
 create mode 100644 testcases/kernel/sched/psi/psi_accounting01.c

diff --git a/runtest/sched b/runtest/sched
index 0651fafd9..b6021411c 100644
--- a/runtest/sched
+++ b/runtest/sched
@@ -24,3 +24,4 @@ sched_football sched_football
 psi_invalid_trigger01 psi_invalid_trigger01
 psi_multiple_trigger01 psi_multiple_trigger01
 psi_proc_format01 psi_proc_format01
+psi_accounting01 psi_accounting01
diff --git a/testcases/kernel/sched/psi/.gitignore b/testcases/kernel/sched/psi/.gitignore
index 8d1083ff2..67407cc1c 100644
--- a/testcases/kernel/sched/psi/.gitignore
+++ b/testcases/kernel/sched/psi/.gitignore
@@ -1,3 +1,4 @@
 /psi_invalid_trigger01
 /psi_multiple_trigger01
 /psi_proc_format01
+/psi_accounting01
diff --git a/testcases/kernel/sched/psi/psi_accounting01.c b/testcases/kernel/sched/psi/psi_accounting01.c
new file mode 100644
index 000000000..8b08f5a14
--- /dev/null
+++ b/testcases/kernel/sched/psi/psi_accounting01.c
@@ -0,0 +1,356 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 IBM Corp.
+ * Author: Samir Mulani <samir@linux.ibm.com>
+ */
+
+/*\
+ * Verify that the PSI counters of a cgroup grow when its tasks are stalled.
+ *
+ * The test reads cpu.pressure, memory.pressure and io.pressure of a cgroup of
+ * its own, so load generated by the rest of the system does not influence the
+ * result. For every resource a workload is run by tasks inside the cgroup and
+ * the "some" total is expected to grow:
+ *
+ * - cpu: two busy tasks are pinned to the same CPU, so one of them is always
+ *   runnable but not running. The total has to grow by at least 50ms.
+ * - memory: a task keeps touching fresh anonymous pages above memory.high of
+ *   the cgroup, which makes it reclaim and get throttled. If memory.events
+ *   does not show any new "high" event the workload did not create the stall
+ *   and the test reports TCONF. Otherwise the task has been accounted as
+ *   stalled, so the total has to grow.
+ * - io: a task does O_DIRECT writes to a block device and has to wait for
+ *   their completion. If the writes did not block long enough the test reports
+ *   TCONF. Otherwise the total has to grow by a quarter of the time that the
+ *   writes were blocked.
+ *
+ * The format of the files in /proc/pressure/ is checked by psi_proc_format01.
+ */
+
+#define _GNU_SOURCE
+
+#include <stdio.h>
+#include <string.h>
+#include <sched.h>
+#include <sys/mman.h>
+
+#include "tst_test.h"
+#include "tst_timer.h"
+#include "tst_clocks.h"
+#include "tst_safe_file_at.h"
+#include "lapi/cpuset.h"
+
+#define PSI_PROBE "/proc/pressure/cpu"
+#define MNTPOINT "mntpoint"
+#define IO_FILE MNTPOINT "/psi_io"
+
+#define WORK_MS 2000
+#define CPU_MIN_DELTA_US 50000ULL
+#define MIN_IO_WAIT_US 200000LL
+/* Share of the measured IO wait that has to show up in io.pressure */
+#define IO_DELTA_DIVISOR 4
+
+#define MEM_HIGH (16 * 1024 * 1024)
+#define MEM_REGION (24 * 1024 * 1024)
+/*
+ * Memory above memory.high is throttled for up to two seconds after every
+ * batch of 64 pages (256KB), so the clock has to be checked after each one.
+ */
+#define MEM_STEP (256 * 1024)
+
+#define IO_SIZE (256 * 1024)
+
+static struct tst_cg_group *cg_psi;
+static int cg_fd = -1;
+static int pin_cpu;
+static long long *io_wait_us;
+
+static char io_buf[IO_SIZE] __attribute__((aligned(4096)));
+
+static unsigned long long read_some_total(const char *file)
+{
+	char buf[256];
+	unsigned long long total;
+
+	SAFE_FILE_READAT(cg_fd, file, buf, sizeof(buf));
+
+	if (sscanf(buf, "some avg10=%*f avg60=%*f avg300=%*f total=%llu",
+		   &total) != 1)
+		tst_brk(TBROK, "Cannot parse %s: '%s'", file, buf);
+
+	return total;
+}
+
+static void check_delta(const char *file, unsigned long long before,
+			unsigned long long min_delta)
+{
+	unsigned long long after = read_some_total(file);
+
+	if (after < before) {
+		tst_res(TFAIL, "%s some total decreased: %llu -> %llu",
+			file, before, after);
+		return;
+	}
+
+	if (after - before >= min_delta) {
+		tst_res(TPASS, "%s some total grew by %llu us",
+			file, after - before);
+	} else {
+		tst_res(TFAIL,
+			"%s some total grew only by %llu us, expected at least %llu us",
+			file, after - before, min_delta);
+	}
+}
+
+static void pin_to_cpu(int cpu)
+{
+	long ncpus = tst_ncpus_max();
+	size_t size = CPU_ALLOC_SIZE(ncpus);
+	cpu_set_t *mask = CPU_ALLOC(ncpus);
+
+	if (!mask)
+		tst_brk(TBROK | TERRNO, "CPU_ALLOC() failed");
+
+	CPU_ZERO_S(size, mask);
+	CPU_SET_S(cpu, size, mask);
+
+	if (sched_setaffinity(0, size, mask))
+		tst_brk(TBROK | TERRNO, "sched_setaffinity(%d) failed", cpu);
+
+	CPU_FREE(mask);
+}
+
+static void join_cgroup(void)
+{
+	SAFE_CG_PRINTF(cg_psi, "cgroup.procs", "%d", getpid());
+}
+
+static void verify_cpu(void)
+{
+	unsigned long long before = read_some_total("cpu.pressure");
+	int i;
+
+	for (i = 0; i < 2; i++) {
+		if (!SAFE_FORK()) {
+			join_cgroup();
+			pin_to_cpu(pin_cpu);
+
+			tst_timer_start(CLOCK_MONOTONIC);
+			while (!tst_timer_expired_ms(WORK_MS))
+				;
+
+			exit(0);
+		}
+	}
+
+	tst_reap_children();
+	check_delta("cpu.pressure", before, CPU_MIN_DELTA_US);
+}
+
+static void verify_memory(void)
+{
+	unsigned long long before = read_some_total("memory.pressure");
+	long high_before, high_after;
+
+	/* The limit is only wanted here, it would throttle the other tests */
+	SAFE_CG_PRINTF(cg_psi, "memory.high", "%d", MEM_HIGH);
+	SAFE_CG_LINES_SCANF(cg_psi, "memory.events", "high %ld", &high_before);
+
+	if (!SAFE_FORK()) {
+		size_t off;
+		char *ptr;
+
+		join_cgroup();
+
+		ptr = SAFE_MMAP(NULL, MEM_REGION, PROT_READ | PROT_WRITE,
+				MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+
+		tst_timer_start(CLOCK_MONOTONIC);
+
+		while (!tst_timer_expired_ms(WORK_MS)) {
+			for (off = 0; off < MEM_REGION; off += MEM_STEP) {
+				memset(ptr + off, 0xab, MEM_STEP);
+
+				if (tst_timer_expired_ms(WORK_MS))
+					break;
+			}
+
+			/* Drop the pages so that they have to be charged again */
+			if (madvise(ptr, MEM_REGION, MADV_DONTNEED))
+				tst_brk(TBROK | TERRNO, "madvise() failed");
+		}
+
+		exit(0);
+	}
+
+	tst_reap_children();
+
+	SAFE_CG_LINES_SCANF(cg_psi, "memory.events", "high %ld", &high_after);
+	SAFE_CG_PRINT(cg_psi, "memory.high", "max");
+
+	if (high_after <= high_before) {
+		tst_res(TCONF, "Workload did not exceed memory.high (high=%ld)",
+			high_after);
+		return;
+	}
+
+	/*
+	 * A "high" event is raised right before the task enters the memory
+	 * stall accounting, so any growth is enough at this point.
+	 */
+	check_delta("memory.pressure", before, 1);
+}
+
+static void verify_io(void)
+{
+	unsigned long long before = read_some_total("io.pressure");
+
+	*io_wait_us = 0;
+
+	if (!SAFE_FORK()) {
+		struct timespec start, end;
+		int fd;
+
+		join_cgroup();
+
+		memset(io_buf, 0xcd, sizeof(io_buf));
+		fd = SAFE_OPEN(IO_FILE, O_WRONLY | O_CREAT | O_DIRECT, 0600);
+
+		tst_timer_start(CLOCK_MONOTONIC);
+
+		while (!tst_timer_expired_ms(WORK_MS)) {
+			tst_clock_gettime(CLOCK_MONOTONIC, &start);
+
+			if (pwrite(fd, io_buf, IO_SIZE, 0) != IO_SIZE)
+				tst_brk(TBROK | TERRNO, "pwrite() failed");
+
+			tst_clock_gettime(CLOCK_MONOTONIC, &end);
+			*io_wait_us += tst_timespec_diff_us(end, start);
+		}
+
+		SAFE_CLOSE(fd);
+		SAFE_UNLINK(IO_FILE);
+		exit(0);
+	}
+
+	tst_reap_children();
+
+	if (*io_wait_us < MIN_IO_WAIT_US) {
+		tst_res(TCONF, "The workload was blocked on IO for only %lld us",
+			*io_wait_us);
+		return;
+	}
+
+	check_delta("io.pressure", before, *io_wait_us / IO_DELTA_DIVISOR);
+}
+
+static void (*const tests[])(void) = {
+	verify_cpu,
+	verify_memory,
+	verify_io,
+};
+
+static void run(unsigned int n)
+{
+	tests[n]();
+}
+
+static void check_pressure_file(const char *file)
+{
+	if (faccessat(cg_fd, file, R_OK, 0))
+		tst_brk(TCONF | TERRNO, "%s is not available in the cgroup", file);
+}
+
+static void setup(void)
+{
+	char buf[64];
+	long i, ncpus = tst_ncpus_max();
+	size_t size = CPU_ALLOC_SIZE(ncpus);
+	cpu_set_t *mask = CPU_ALLOC(ncpus);
+	int fd;
+
+	if (!mask)
+		tst_brk(TBROK | TERRNO, "CPU_ALLOC() failed");
+
+	fd = SAFE_OPEN(PSI_PROBE, O_RDONLY);
+
+	/* psi=0 on the kernel command line makes the interface return EOPNOTSUPP */
+	if (read(fd, buf, sizeof(buf)) < 0) {
+		if (errno == EOPNOTSUPP)
+			tst_brk(TCONF, "PSI is disabled (psi=0 on kernel command line)");
+
+		tst_brk(TBROK | TERRNO, "read(%s) failed", PSI_PROBE);
+	}
+
+	SAFE_CLOSE(fd);
+
+	/* Pin the busy tasks to a CPU we are actually allowed to use */
+	if (sched_getaffinity(0, size, mask))
+		tst_brk(TBROK | TERRNO, "sched_getaffinity() failed");
+
+	for (i = 0; i < ncpus; i++) {
+		if (CPU_ISSET_S(i, size, mask))
+			break;
+	}
+
+	if (i == ncpus)
+		tst_brk(TBROK, "No usable CPU found");
+
+	pin_cpu = i;
+	CPU_FREE(mask);
+
+	cg_psi = tst_cg_group_mk(tst_cg, "psi");
+	cg_fd = tst_cg_group_unified_dir_fd(cg_psi);
+
+	if (cg_fd < 0)
+		tst_brk(TBROK, "Cannot get the directory of the cgroup");
+
+	check_pressure_file("cpu.pressure");
+	check_pressure_file("memory.pressure");
+	check_pressure_file("io.pressure");
+
+	/* Not every filesystem supports O_DIRECT */
+	fd = open(IO_FILE, O_WRONLY | O_CREAT | O_DIRECT, 0600);
+	if (fd < 0) {
+		if (errno == EINVAL)
+			tst_brk(TCONF, "O_DIRECT is not supported by the filesystem");
+
+		tst_brk(TBROK | TERRNO, "open(%s) failed", IO_FILE);
+	}
+	SAFE_CLOSE(fd);
+	SAFE_UNLINK(IO_FILE);
+
+	io_wait_us = SAFE_MMAP(NULL, sizeof(*io_wait_us), PROT_READ | PROT_WRITE,
+			       MAP_SHARED | MAP_ANONYMOUS, -1, 0);
+}
+
+static void cleanup(void)
+{
+	if (io_wait_us)
+		SAFE_MUNMAP(io_wait_us, sizeof(*io_wait_us));
+
+	if (cg_psi)
+		cg_psi = tst_cg_group_rm(cg_psi);
+}
+
+static struct tst_test test = {
+	.tcnt = ARRAY_SIZE(tests),
+	.test = run,
+	.setup = setup,
+	.cleanup = cleanup,
+	.forks_child = 1,
+	.needs_root = 1,
+	.timeout = 60,
+	.min_kver = "4.20",
+	.min_mem_avail = 256,
+	.needs_device = 1,
+	.mount_device = 1,
+	.mntpoint = MNTPOINT,
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const[]) {"memory", NULL},
+	.needs_kconfigs = (const char *const[]) {
+		"CONFIG_PSI=y",
+		NULL
+	},
+};
+
-- 
2.53.0


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

      parent reply	other threads:[~2026-10-23 13:46 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-24  2:30 [LTP] [PATCH v1 0/4] sched/psi: Add tests for PSI interface Samir Mulani
2026-10-24  2:30 ` [LTP] [PATCH v1 1/4] sched/psi: Add psi_invalid_trigger01 test Samir Mulani
2026-10-23 17:55   ` [LTP] " linuxtestproject.agent
2026-10-24  2:30 ` [LTP] [PATCH v1 2/4] sched/psi: Add psi_multiple_trigger01 test Samir Mulani
2026-10-24  2:30 ` [LTP] [PATCH v1 3/4] sched/psi: Add psi_proc_format01 test Samir Mulani
2026-10-24  2:30 ` Samir Mulani [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261024023044.80055-5-samir@linux.ibm.com \
    --to=samir@linux.ibm.com \
    --cc=ltp@lists.linux.it \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.