Linux Test Project
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox