* [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test Thomas Hellström
` (6 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add igt_cgroup, a library module providing helpers to create and manage
cgroup v2 sub-cgroups from IGT tests, with support for the dmem
controller that governs device memory (e.g. GPU VRAM) limits.
The API covers:
- igt_cgroup_new() / igt_cgroup_free(): create and destroy a named
sub-cgroup under the unified cgroupv2 hierarchy, enabling the dmem
controller automatically.
- igt_cgroup_move_current(): move the calling process into a cgroup.
- igt_cgroup_dmem_set/get_max/min/low(): write and read dmem.max,
dmem.min and dmem.low for a named device memory region.
- igt_cgroup_dmem_get_current(): read current per-cgroup device memory
usage.
- igt_cgroup_dmem_get_system_current(): read system-wide device memory
usage from the root cgroup.
- igt_cgroup_dmem_get_capacity(): read total region capacity from the
root cgroup's dmem.capacity file.
- igt_cgroup_dmem_regions() / igt_cgroup_dmem_regions_free(): enumerate
all registered device memory regions.
All public API functions that can fail use igt_assert internally rather
than returning error codes, following the IGT convention.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/igt.h | 1 +
lib/igt_cgroup.c | 638 +++++++++++++++++++++++++++++++++++++++++++++++
lib/igt_cgroup.h | 56 +++++
lib/meson.build | 1 +
4 files changed, 696 insertions(+)
create mode 100644 lib/igt_cgroup.c
create mode 100644 lib/igt_cgroup.h
diff --git a/lib/igt.h b/lib/igt.h
index 173ca70bf..d8e5de7dc 100644
--- a/lib/igt.h
+++ b/lib/igt.h
@@ -27,6 +27,7 @@
#include "drmtest.h"
#include "i915_3d.h"
#include "igt_aux.h"
+#include "igt_cgroup.h"
#include "igt_configfs.h"
#include "igt_core.h"
#include "igt_debugfs.h"
diff --git a/lib/igt_cgroup.c b/lib/igt_cgroup.c
new file mode 100644
index 000000000..60586ccc4
--- /dev/null
+++ b/lib/igt_cgroup.c
@@ -0,0 +1,638 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+/**
+ * SECTION:igt_cgroup
+ * @short_description: cgroup v2 helpers for IGT tests
+ * @title: cgroup
+ * @include: igt_cgroup.h
+ *
+ * This library provides helpers for creating and managing cgroup v2
+ * sub-cgroups from IGT tests, including support for the dmem controller
+ * which governs device memory (e.g. GPU VRAM) limits.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "igt_cgroup.h"
+#include "igt_core.h"
+#include "igt_fs.h"
+
+#ifndef CGROUP2_SUPER_MAGIC
+#define CGROUP2_SUPER_MAGIC 0x63677270
+#endif
+
+/**
+ * struct igt_cgroup - Opaque handle to a cgroup v2 sub-cgroup.
+ * @dirfd: File descriptor for the cgroup directory.
+ * @path: Absolute path to the cgroup directory.
+ * @parent_path: Absolute path to the parent cgroup directory.
+ *
+ * Allocated by igt_cgroup_new() and freed by igt_cgroup_free().
+ */
+struct igt_cgroup {
+ int dirfd;
+ char *path;
+ char *parent_path;
+};
+
+static const char *cgroupv2_mount(void)
+{
+ static const char *path;
+ static const char * const candidates[] = {
+ "/sys/fs/cgroup",
+ "/sys/fs/cgroup/unified",
+ NULL,
+ };
+ struct statfs st;
+ int i;
+
+ if (path)
+ return path;
+
+ for (i = 0; candidates[i]; i++) {
+ if (statfs(candidates[i], &st) == 0 &&
+ (unsigned long)st.f_type == CGROUP2_SUPER_MAGIC) {
+ path = candidates[i];
+ return path;
+ }
+ }
+
+ return NULL;
+}
+
+/*
+ * Write "+controller" to @cgroup_path/cgroup.subtree_control to enable
+ * the named controller for children of that cgroup.
+ */
+static int enable_controller(const char *cgroup_path, const char *controller)
+{
+ char path[PATH_MAX];
+ char cmd[64];
+ ssize_t ret;
+ int fd;
+
+ snprintf(path, sizeof(path), "%s/cgroup.subtree_control", cgroup_path);
+ snprintf(cmd, sizeof(cmd), "+%s", controller);
+
+ fd = open(path, O_WRONLY);
+ if (fd < 0)
+ return -errno;
+
+ ret = write(fd, cmd, strlen(cmd));
+ close(fd);
+
+ return (ret < 0) ? -errno : 0;
+}
+
+/*
+ * Move every PID listed in @cgroup_path/cgroup.procs to
+ * @parent_path/cgroup.procs. Silently ignores individual failures
+ * (a PID may have exited between reading and writing).
+ */
+static void drain_procs_to_parent(const char *cgroup_path,
+ const char *parent_path)
+{
+ char proc_path[PATH_MAX];
+ char parent_procs[PATH_MAX];
+ int parent_fd;
+ FILE *f;
+ int pid;
+
+ snprintf(proc_path, sizeof(proc_path), "%s/cgroup.procs", cgroup_path);
+ snprintf(parent_procs, sizeof(parent_procs), "%s/cgroup.procs", parent_path);
+
+ parent_fd = open(parent_procs, O_WRONLY);
+ if (parent_fd < 0)
+ return;
+
+ f = fopen(proc_path, "r");
+ if (f) {
+ while (fscanf(f, "%d", &pid) == 1) {
+ char pidbuf[32];
+ ssize_t len = snprintf(pidbuf, sizeof(pidbuf), "%d", pid);
+
+ write(parent_fd, pidbuf, len);
+ }
+ fclose(f);
+ }
+
+ close(parent_fd);
+}
+
+/**
+ * igt_cgroup_new() - Create a new cgroup v2 sub-cgroup.
+ * @name: Name for the new cgroup directory.
+ *
+ * Creates a sub-cgroup named @name under the system's unified cgroupv2
+ * hierarchy. The dmem controller is enabled in the parent's
+ * subtree_control so that igt_cgroup_dmem_set_max() and friends take effect
+ * immediately.
+ *
+ * Return: Pointer to an &struct igt_cgroup on success, %NULL on failure.
+ */
+struct igt_cgroup *igt_cgroup_new(const char *name)
+{
+ struct igt_cgroup *cg;
+ const char *mount;
+ int ret;
+
+ mount = cgroupv2_mount();
+ if (!mount) {
+ igt_debug("cgroup v2 not found\n");
+ return NULL;
+ }
+
+ cg = calloc(1, sizeof(*cg));
+ if (!cg)
+ return NULL;
+
+ cg->parent_path = strdup(mount);
+ if (!cg->parent_path)
+ goto err_free;
+
+ if (asprintf(&cg->path, "%s/%s", mount, name) < 0) {
+ cg->path = NULL;
+ goto err_parent;
+ }
+
+ /*
+ * Try to enable the dmem controller in the parent's subtree_control.
+ * Ignore EINVAL which the kernel returns when the controller is already
+ * listed (i.e. already enabled).
+ */
+ ret = enable_controller(mount, "dmem");
+ if (ret < 0 && ret != -EINVAL)
+ igt_debug("Failed to enable dmem controller in %s: %d\n",
+ mount, ret);
+
+ if (mkdir(cg->path, 0755) < 0 && errno != EEXIST) {
+ igt_debug("Failed to create cgroup %s: %m\n", cg->path);
+ goto err_path;
+ }
+
+ cg->dirfd = open(cg->path, O_RDONLY | O_DIRECTORY);
+ if (cg->dirfd < 0) {
+ igt_debug("Failed to open cgroup dir %s: %m\n", cg->path);
+ goto err_rmdir;
+ }
+
+ return cg;
+
+err_rmdir:
+ rmdir(cg->path);
+err_path:
+ free(cg->path);
+err_parent:
+ free(cg->parent_path);
+err_free:
+ free(cg);
+ return NULL;
+}
+
+/**
+ * igt_cgroup_free() - Destroy a cgroup and release its resources.
+ * @cg: The cgroup to destroy.
+ *
+ * Moves any processes still running inside @cg back to the parent cgroup,
+ * removes the cgroup directory, and frees all associated memory.
+ * After this call @cg must not be used.
+ */
+void igt_cgroup_free(struct igt_cgroup *cg)
+{
+ if (!cg)
+ return;
+
+ drain_procs_to_parent(cg->path, cg->parent_path);
+
+ close(cg->dirfd);
+
+ if (rmdir(cg->path) < 0)
+ igt_debug("Failed to remove cgroup %s: %m\n", cg->path);
+
+ free(cg->path);
+ free(cg->parent_path);
+ free(cg);
+}
+
+/**
+ * igt_cgroup_move_current() - Move the calling process into a cgroup.
+ * @cg: Target cgroup.
+ *
+ * Writes the calling process's PID to @cg's cgroup.procs file, transferring
+ * it into the cgroup. All threads of the process move together.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_move_current(struct igt_cgroup *cg)
+{
+ char pidbuf[32];
+ ssize_t len;
+ int fd, ret;
+
+ len = snprintf(pidbuf, sizeof(pidbuf), "%d", (int)getpid());
+
+ fd = openat(cg->dirfd, "cgroup.procs", O_WRONLY);
+ igt_assert_f(fd >= 0, "Failed to open cgroup.procs: %m\n");
+
+ ret = write(fd, pidbuf, len);
+ close(fd);
+
+ igt_assert_f(ret == len, "Failed to write PID to cgroup.procs: %m\n");
+}
+
+/*
+ * Parse a single dmem interface file line of the form "region_name value\n"
+ * where value is either a decimal byte count or the string "max".
+ * Returns 0 and writes to *out on success, -EINVAL on parse error.
+ */
+static int dmem_parse_line(char *line, const char *region, uint64_t *out)
+{
+ char *space = strchr(line, ' ');
+
+ if (!space)
+ return -EINVAL;
+
+ *space = '\0';
+ if (strcmp(line, region) != 0)
+ return -ENOENT;
+
+ if (strcmp(space + 1, "max") == 0) {
+ *out = IGT_CGROUP_DMEM_MAX;
+ return 0;
+ }
+
+ errno = 0;
+ *out = strtoull(space + 1, &space, 10);
+ if (errno || *space != '\0')
+ return -EINVAL;
+
+ return 0;
+}
+
+/*
+ * Read a dmem interface file opened relative to @dirfd, searching for
+ * @region. On success writes the region's value to @out and returns 0.
+ * Returns -ENOENT when @region is absent, or a negative errno otherwise.
+ */
+static int dmem_read_region(int dirfd, const char *file,
+ const char *region, uint64_t *out)
+{
+ char buf[4096];
+ char *line, *saveptr;
+ ssize_t n;
+ int fd;
+
+ fd = openat(dirfd, file, O_RDONLY);
+ if (fd < 0)
+ return -errno;
+
+ n = igt_readn(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n < 0)
+ return (int)n;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ int ret = dmem_parse_line(line, region, out);
+
+ if (ret != -ENOENT)
+ return ret;
+ }
+
+ return -ENOENT;
+}
+
+/*
+ * Write "region_name value" (or "region_name max") to the dmem interface
+ * file @file opened relative to @dirfd.
+ * If @nonblock is true the file is opened with O_NONBLOCK, causing any
+ * eviction triggered by the limit change to be skipped rather than waited
+ * for; the write still succeeds (returns 0).
+ * Returns 0 on success, negative errno on failure.
+ */
+static int dmem_write_region(int dirfd, const char *file,
+ const char *region, uint64_t bytes, bool nonblock)
+{
+ char buf[PATH_MAX + 64];
+ ssize_t len;
+ int fd, ret;
+ int flags = O_WRONLY;
+
+ if (bytes == IGT_CGROUP_DMEM_MAX)
+ len = snprintf(buf, sizeof(buf), "%s max", region);
+ else
+ len = snprintf(buf, sizeof(buf), "%s %" PRIu64, region, bytes);
+
+ if (nonblock)
+ flags |= O_NONBLOCK;
+
+ fd = openat(dirfd, file, flags);
+ if (fd < 0)
+ return -errno;
+
+ do {
+ ret = write(fd, buf, len);
+ if (ret < 0 && errno == EINTR)
+ igt_debug("dmem cgroup write interrupted by signal, retrying\n");
+ } while (ret < 0 && errno == EINTR);
+ close(fd);
+
+ return (ret < 0) ? -errno : 0;
+}
+
+/**
+ * igt_cgroup_dmem_set_max() - Set the hard device memory limit for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name (e.g. "drm/0000:03:00.0/vram0").
+ * @bytes: Hard limit in bytes. Use %IGT_CGROUP_DMEM_MAX for no limit.
+ * @nonblock: If true, open the file with O_NONBLOCK so that eviction
+ * triggered by the limit change is skipped rather than awaited.
+ *
+ * Writes @bytes to dmem.max for @region inside @cg. Allocation attempts
+ * that would push usage past this limit fail with -EAGAIN in the kernel.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_max(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes, bool nonblock)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.max", region, bytes,
+ nonblock) == 0,
+ "Failed to set dmem.max for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_set_min() - Set the hard protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @bytes: Hard protection threshold in bytes. Pass 0 to disable.
+ *
+ * Writes @bytes to dmem.min for @region inside @cg. Device memory below
+ * this threshold is never reclaimed regardless of system pressure.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_min(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.min", region, bytes,
+ false) == 0,
+ "Failed to set dmem.min for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_set_low() - Set the soft protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @bytes: Soft protection threshold in bytes. Pass 0 to disable.
+ *
+ * Writes @bytes to dmem.low for @region inside @cg. Device memory below
+ * this threshold is only reclaimed when no unprotected memory remains.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_set_low(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes)
+{
+ igt_assert_f(dmem_write_region(cg->dirfd, "dmem.low", region, bytes,
+ false) == 0,
+ "Failed to set dmem.low for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_current() - Read current device memory usage for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the current usage in bytes.
+ *
+ * Reads dmem.current from @cg and returns the usage for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_current(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.current", region, out) == 0,
+ "Failed to read dmem.current for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_max() - Read the configured hard limit for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the limit in bytes, or %IGT_CGROUP_DMEM_MAX if unset.
+ *
+ * Reads dmem.max from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_max(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.max", region, out) == 0,
+ "Failed to read dmem.max for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_min() - Read the configured hard protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the threshold in bytes.
+ *
+ * Reads dmem.min from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_min(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.min", region, out) == 0,
+ "Failed to read dmem.min for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_low() - Read the configured soft protection threshold for a region.
+ * @cg: Target cgroup.
+ * @region: Device memory region name.
+ * @out: Receives the threshold in bytes.
+ *
+ * Reads dmem.low from @cg for @region.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_low(struct igt_cgroup *cg, const char *region,
+ uint64_t *out)
+{
+ igt_assert_f(dmem_read_region(cg->dirfd, "dmem.low", region, out) == 0,
+ "Failed to read dmem.low for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_available() - Check if the dmem cgroup controller is available.
+ *
+ * Probes the cgroup v2 hierarchy for the presence of a dmem.capacity file at
+ * the root, indicating that the kernel dmem controller is compiled in and at
+ * least one device memory region has been registered.
+ *
+ * Return: %true if the dmem controller is available, %false otherwise.
+ */
+bool igt_cgroup_dmem_available(void)
+{
+ char **regions = igt_cgroup_dmem_regions();
+
+ if (!regions)
+ return false;
+
+ igt_cgroup_dmem_regions_free(regions);
+ return true;
+}
+
+/**
+ * igt_cgroup_dmem_regions() - Enumerate all registered device memory regions.
+ *
+ * Reads the root cgroup's dmem.capacity file and returns a NULL-terminated
+ * array of region name strings. Each name can be passed directly to
+ * igt_cgroup_dmem_get_capacity(), igt_cgroup_dmem_get_current(), and the
+ * igt_cgroup_dmem_set_*() / igt_cgroup_dmem_get_*() family.
+ *
+ * Free the returned array with igt_cgroup_dmem_regions_free().
+ *
+ * Return: A NULL-terminated array of strings on success, %NULL if cgroupv2
+ * is unavailable or no regions are registered.
+ */
+char **igt_cgroup_dmem_regions(void)
+{
+ char buf[4096];
+ char *line, *saveptr, *space, *name;
+ char **regions = NULL, **tmp;
+ int count = 0;
+ const char *mount;
+ ssize_t n;
+ int dirfd, fd;
+
+ mount = cgroupv2_mount();
+ if (!mount)
+ return NULL;
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ if (dirfd < 0)
+ return NULL;
+
+ fd = openat(dirfd, "dmem.capacity", O_RDONLY);
+ close(dirfd);
+ if (fd < 0)
+ return NULL;
+
+ n = igt_readn(fd, buf, sizeof(buf) - 1);
+ close(fd);
+ if (n <= 0)
+ return NULL;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ space = strchr(line, ' ');
+
+ if (!space)
+ continue;
+ *space = '\0';
+
+ name = strdup(line);
+ if (!name)
+ goto err;
+
+ tmp = realloc(regions, (count + 2) * sizeof(*regions));
+ if (!tmp) {
+ free(name);
+ goto err;
+ }
+ regions = tmp;
+ regions[count++] = name;
+ regions[count] = NULL;
+ }
+
+ return regions;
+
+err:
+ igt_cgroup_dmem_regions_free(regions);
+ return NULL;
+}
+
+/**
+ * igt_cgroup_dmem_regions_free() - Free a region list returned by igt_cgroup_dmem_regions().
+ * @regions: NULL-terminated array returned by igt_cgroup_dmem_regions().
+ *
+ * Frees each string in @regions and the array itself. Safe to call with
+ * %NULL.
+ */
+void igt_cgroup_dmem_regions_free(char **regions)
+{
+ int i;
+
+ if (!regions)
+ return;
+
+ for (i = 0; regions[i]; i++)
+ free(regions[i]);
+
+ free(regions);
+}
+
+/**
+ * igt_cgroup_dmem_get_capacity() - Read total device memory capacity for a region.
+ * @region: Device memory region name.
+ * @out: Receives the total capacity in bytes.
+ *
+ * Reads dmem.capacity from the root cgroup and returns the capacity for
+ * @region. This reflects the maximum allocatable bytes, excluding memory
+ * reserved by the kernel for internal use.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_capacity(const char *region, uint64_t *out)
+{
+ const char *mount;
+ int dirfd, ret;
+
+ mount = cgroupv2_mount();
+ igt_assert_f(mount, "cgroup v2 not available\n");
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ igt_assert_f(dirfd >= 0, "Failed to open cgroup root: %m\n");
+
+ ret = dmem_read_region(dirfd, "dmem.capacity", region, out);
+ close(dirfd);
+
+ igt_assert_f(ret == 0, "Failed to read dmem.capacity for region %s\n", region);
+}
+
+/**
+ * igt_cgroup_dmem_get_system_current() - Read system-wide device memory usage for a region.
+ * @region: Device memory region name.
+ * @out: Receives the total system-wide usage in bytes.
+ *
+ * Reads dmem.current from the root cgroup for @region. This reflects the
+ * aggregate device memory usage across all cgroups on the system.
+ * Fails the test via igt_assert on error.
+ */
+void igt_cgroup_dmem_get_system_current(const char *region, uint64_t *out)
+{
+ const char *mount;
+ int dirfd, ret;
+
+ mount = cgroupv2_mount();
+ igt_assert_f(mount, "cgroup v2 not available\n");
+
+ dirfd = open(mount, O_RDONLY | O_DIRECTORY);
+ igt_assert_f(dirfd >= 0, "Failed to open cgroup root: %m\n");
+
+ ret = dmem_read_region(dirfd, "dmem.current", region, out);
+ close(dirfd);
+
+ igt_assert_f(ret == 0, "Failed to read root dmem.current for region %s\n", region);
+}
diff --git a/lib/igt_cgroup.h b/lib/igt_cgroup.h
new file mode 100644
index 000000000..379de457a
--- /dev/null
+++ b/lib/igt_cgroup.h
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+#ifndef __IGT_CGROUP_H__
+#define __IGT_CGROUP_H__
+
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+ * IGT_CGROUP_DMEM_MAX - Sentinel value meaning "no device memory limit".
+ *
+ * Pass this to igt_cgroup_dmem_set_max() to remove a previously set limit,
+ * equivalent to writing "max" to the dmem.max interface file.
+ */
+#define IGT_CGROUP_DMEM_MAX UINT64_MAX
+
+/**
+ * struct igt_cgroup - Opaque handle to a cgroup v2 sub-cgroup.
+ *
+ * Allocated by igt_cgroup_new() and freed by igt_cgroup_free().
+ * All other functions in this module take a pointer to this type.
+ */
+struct igt_cgroup;
+
+struct igt_cgroup *igt_cgroup_new(const char *name);
+void igt_cgroup_free(struct igt_cgroup *cg);
+
+void igt_cgroup_move_current(struct igt_cgroup *cg);
+
+void igt_cgroup_dmem_set_max(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes, bool nonblock);
+void igt_cgroup_dmem_set_min(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes);
+void igt_cgroup_dmem_set_low(struct igt_cgroup *cg, const char *region,
+ uint64_t bytes);
+
+void igt_cgroup_dmem_get_max(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_min(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_low(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+
+void igt_cgroup_dmem_get_current(struct igt_cgroup *cg, const char *region,
+ uint64_t *out);
+void igt_cgroup_dmem_get_capacity(const char *region, uint64_t *out);
+void igt_cgroup_dmem_get_system_current(const char *region, uint64_t *out);
+
+bool igt_cgroup_dmem_available(void);
+char **igt_cgroup_dmem_regions(void);
+void igt_cgroup_dmem_regions_free(char **regions);
+
+#endif /* __IGT_CGROUP_H__ */
diff --git a/lib/meson.build b/lib/meson.build
index 0e7efadf3..fb4679ffd 100644
--- a/lib/meson.build
+++ b/lib/meson.build
@@ -18,6 +18,7 @@ lib_sources = [
'i915/i915_crc.c',
'igt_collection.c',
'igt_color_encoding.c',
+ 'igt_cgroup.c',
'igt_configfs.c',
'igt_facts.c',
'igt_crc.c',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper Thomas Hellström
` (5 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add a test that exercises the cgroup v2 dmem controller interface using
the new igt_cgroup library.
The test uses igt_simple_main and:
- Skips if no dmem regions are registered (no cgroup v2 or no
dmem-capable device).
- Creates a sub-cgroup and moves the test process into it.
- Enumerates all registered device memory regions and prints their
capacity, system-wide current usage, per-cgroup current usage, and
configured min, low and max limits.
- Destroys the cgroup on completion.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/cgroup_dmem.c | 92 +++++++++++++++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 93 insertions(+)
create mode 100644 tests/cgroup_dmem.c
diff --git a/tests/cgroup_dmem.c b/tests/cgroup_dmem.c
new file mode 100644
index 000000000..442c965f9
--- /dev/null
+++ b/tests/cgroup_dmem.c
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2025 Intel Corporation
+ */
+
+/**
+ * TEST: cgroup dmem
+ * Description: Exercises the cgroup v2 dmem controller interface. Creates a
+ * cgroup, moves the process into it, enumerates all dmem regions,
+ * prints their capacity, system-wide current usage, per-cgroup
+ * current usage and configured limits, then destroys the cgroup.
+ * Category: Core
+ * Mega feature: General Core features
+ * Sub-category: uapi
+ * Functionality: cgroup
+ * Feature: dmem
+ * Test category: uapi
+ */
+
+#include <inttypes.h>
+
+#include "igt.h"
+#include "igt_cgroup.h"
+
+IGT_TEST_DESCRIPTION("Exercises the cgroup v2 dmem controller interface.");
+
+static void fmt_bytes(uint64_t v, char *buf, size_t len)
+{
+ if (v == IGT_CGROUP_DMEM_MAX)
+ snprintf(buf, len, "max");
+ else
+ snprintf(buf, len, "%" PRIu64, v);
+}
+
+int igt_simple_main()
+{
+ struct igt_cgroup *cg;
+ const char *region;
+ char **regions;
+ uint64_t capacity, sys_current, cg_current, min, low, max;
+ char cap_s[32], sys_s[32], cg_s[32];
+ char min_s[32], low_s[32], max_s[32];
+ int i;
+
+ igt_require_f(igt_cgroup_dmem_available(),
+ "No dmem regions found; is cgroup v2 with the "
+ "dmem controller available?\n");
+
+ cg = igt_cgroup_new("igt-cgroup-dmem-test");
+ igt_assert_f(cg, "Failed to create cgroup\n");
+
+ igt_cgroup_move_current(cg);
+
+ regions = igt_cgroup_dmem_regions();
+ igt_assert_f(regions, "Failed to enumerate dmem regions\n");
+
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ "region", "capacity", "system-current",
+ "cgroup-current", "min", "low", "max");
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ "------", "--------", "--------------",
+ "--------------", "---", "---", "---");
+
+ for (i = 0; regions[i]; i++) {
+ region = regions[i];
+
+ igt_cgroup_dmem_get_capacity(region, &capacity);
+ fmt_bytes(capacity, cap_s, sizeof(cap_s));
+
+ igt_cgroup_dmem_get_system_current(region, &sys_current);
+ fmt_bytes(sys_current, sys_s, sizeof(sys_s));
+
+ igt_cgroup_dmem_get_current(cg, region, &cg_current);
+ fmt_bytes(cg_current, cg_s, sizeof(cg_s));
+
+ igt_cgroup_dmem_get_min(cg, region, &min);
+ fmt_bytes(min, min_s, sizeof(min_s));
+
+ igt_cgroup_dmem_get_low(cg, region, &low);
+ fmt_bytes(low, low_s, sizeof(low_s));
+
+ igt_cgroup_dmem_get_max(cg, region, &max);
+ fmt_bytes(max, max_s, sizeof(max_s));
+
+ igt_info("%-40s %16s %16s %16s %16s %16s %16s\n",
+ region, cap_s, sys_s, cg_s,
+ min_s, low_s, max_s);
+ }
+
+ igt_cgroup_dmem_regions_free(regions);
+ igt_cgroup_free(cg);
+}
diff --git a/tests/meson.build b/tests/meson.build
index 60cea3aa8..02fbb8c4e 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -1,4 +1,5 @@
test_progs = [
+ 'cgroup_dmem',
'core_auth',
'core_debugfs',
'core_getclient',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 1/6] lib/igt_cgroup: add cgroup v2 and dmem controller helpers Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 2/6] tests/cgroup_dmem: add dmem cgroup controller test Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync() Thomas Hellström
` (4 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add xe_cgroup_region_name(fd, region) which constructs the dmem cgroup
region path for a given xe memory region. The returned string has the
form "drm/<pci-slot>/<region>" (e.g. "drm/0000:03:00.0/vram0"),
matching the name registered by the kernel via drmm_cgroup_register_region().
Only VRAM regions are tracked by the dmem controller; system and stolen
memory regions return NULL.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/xe/xe_query.c | 32 ++++++++++++++++++++++++++++++++
lib/xe/xe_query.h | 2 ++
2 files changed, 34 insertions(+)
diff --git a/lib/xe/xe_query.c b/lib/xe/xe_query.c
index 3286a3b37..ad8b16f74 100644
--- a/lib/xe/xe_query.c
+++ b/lib/xe/xe_query.c
@@ -7,6 +7,7 @@
*/
#include <fcntl.h>
+#include <limits.h>
#include <stdlib.h>
#include <pthread.h>
@@ -21,6 +22,7 @@
#include "drmtest.h"
#include "igt_debugfs.h"
+#include "igt_device.h"
#include "ioctl_wrappers.h"
#include "igt_map.h"
#include "intel_pat.h"
@@ -1270,6 +1272,36 @@ int xe_query_eu_thread_count(int fd, int gt)
xe_hwconfig_lookup_value_u32(fd, INTEL_HWCONFIG_NUM_THREADS_PER_EU);
}
+/**
+ * xe_cgroup_region_name() - Build the dmem cgroup region name for an xe memory region.
+ * @fd: xe device fd.
+ * @region: Region mask (as used by xe_mem_region(), xe_region_name(), etc.).
+ *
+ * Constructs the full dmem cgroup region path for @region on the device
+ * identified by @fd. The returned string has the form
+ * ``drm/<pci-slot>/<region>`` (e.g. ``drm/0000:03:00.0/vram0``), matching
+ * the name registered by the kernel driver via drmm_cgroup_register_region().
+ *
+ * Only VRAM regions are registered with the dmem controller; passing a
+ * system-memory region returns %NULL.
+ *
+ * Return: A newly allocated string that the caller must free(), or %NULL if
+ * @region is not tracked by the dmem cgroup controller.
+ */
+char *xe_cgroup_region_name(int fd, uint64_t region)
+{
+ char pci_slot[NAME_MAX];
+ char *name;
+
+ if (xe_region_class(fd, region) != DRM_XE_MEM_REGION_CLASS_VRAM)
+ return NULL;
+
+ igt_device_get_pci_slot_name(fd, pci_slot);
+
+ igt_assert(asprintf(&name, "drm/%s/%s", pci_slot, xe_region_name(region)) > 0);
+ return name;
+}
+
igt_constructor
{
xe_device_cache_init();
diff --git a/lib/xe/xe_query.h b/lib/xe/xe_query.h
index 8815c6c66..b01a5dac1 100644
--- a/lib/xe/xe_query.h
+++ b/lib/xe/xe_query.h
@@ -204,4 +204,6 @@ void xe_device_put(int fd);
int xe_query_eu_count(int fd, int gt);
int xe_query_eu_thread_count(int fd, int gt);
+char *xe_cgroup_region_name(int fd, uint64_t region);
+
#endif /* XE_QUERY_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync()
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (2 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 3/6] lib/xe: add xe_cgroup_region_name() helper Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test Thomas Hellström
` (3 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Sobin Thomas, Thomas Hellström
From: Sobin Thomas <sobin.thomas@intel.com>
Add __xe_vm_bind_lr_sync helper function which returns standard error
codes instead of asserting on failure. This allows calling function
to handle VM bind failures explicitly while preserving the existing
xe_vm_bind_lr_sync() wrapper for tests. This enables callers that
expect bind / overcommit failures.
v7: Introduced xe_vm_bind_lr_sync_failable (Thomas)
v8: Modified xe_vm_bind_lr_sync_failable and xe_vm_bind_lr_sync to call
__xe_vm_bind_lr_sync
v9: Removed redundant typecast and removed xe_vm_bind_lr_sync_failable
Signed-off-by: Sobin Thomas <sobin.thomas@intel.com>
Reviewed-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
lib/xe/xe_ioctl.c | 35 +++++++++++++++++++++++++----------
lib/xe/xe_ioctl.h | 2 ++
2 files changed, 27 insertions(+), 10 deletions(-)
diff --git a/lib/xe/xe_ioctl.c b/lib/xe/xe_ioctl.c
index 1dae56444..e13195e16 100644
--- a/lib/xe/xe_ioctl.c
+++ b/lib/xe/xe_ioctl.c
@@ -860,23 +860,38 @@ uint32_t xe_vm_madvise_purgeable(int fd, uint32_t vm_id, uint64_t start,
}
#define BIND_SYNC_VAL 0x686868
-void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
- uint64_t addr, uint64_t size, uint32_t flags)
+int __xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags)
{
- volatile uint64_t *sync_addr = malloc(sizeof(*sync_addr));
+ uint64_t *sync_addr = malloc(sizeof(*sync_addr));
struct drm_xe_sync sync = {
.flags = DRM_XE_SYNC_FLAG_SIGNAL,
.type = DRM_XE_SYNC_TYPE_USER_FENCE,
- .addr = to_user_pointer((uint64_t *)sync_addr),
+ .addr = to_user_pointer(sync_addr),
.timeline_value = BIND_SYNC_VAL,
};
-
- igt_assert(!!sync_addr);
- xe_vm_bind_async_flags(fd, vm, 0, bo, 0, addr, size, &sync, 1, flags);
- if (*sync_addr != BIND_SYNC_VAL)
- xe_wait_ufence(fd, (uint64_t *)sync_addr, BIND_SYNC_VAL, 0, NSEC_PER_SEC * 10);
+ int ret = 0;
+
+ if (!sync_addr)
+ return -ENOMEM;
+ WRITE_ONCE(*sync_addr, 0);
+ ret = __xe_vm_bind(fd, vm, 0, bo, offset, addr, size, DRM_XE_VM_BIND_OP_MAP, flags,
+ &sync, 1, 0, DEFAULT_PAT_INDEX, 0);
+ if (ret)
+ goto out;
+
+ if (READ_ONCE(*sync_addr) != BIND_SYNC_VAL)
+ xe_wait_ufence(fd, sync_addr, BIND_SYNC_VAL, 0, NSEC_PER_SEC * 10);
/* Only free if the wait succeeds */
- free((void *)sync_addr);
+out:
+ free(sync_addr);
+ return ret;
+}
+
+void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags)
+{
+ igt_assert_eq(__xe_vm_bind_lr_sync(fd, vm, bo, offset, addr, size, flags), 0);
}
void xe_vm_unbind_lr_sync(int fd, uint32_t vm, uint64_t offset,
diff --git a/lib/xe/xe_ioctl.h b/lib/xe/xe_ioctl.h
index ceb380685..768f77246 100644
--- a/lib/xe/xe_ioctl.h
+++ b/lib/xe/xe_ioctl.h
@@ -120,6 +120,8 @@ struct drm_xe_mem_range_attr
void xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo,
uint64_t offset, uint64_t addr,
uint64_t size, uint32_t flags);
+int __xe_vm_bind_lr_sync(int fd, uint32_t vm, uint32_t bo, uint64_t offset,
+ uint64_t addr, uint64_t size, uint32_t flags);
void xe_vm_unbind_lr_sync(int fd, uint32_t vm, uint64_t offset,
uint64_t addr, uint64_t size);
#endif /* XE_IOCTL_H */
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (3 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 4/6] lib/xe: Add failable variant of xe_vm_bind_lr_sync() Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 6:54 ` [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest Thomas Hellström
` (2 subsequent siblings)
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add xe_cgroups, a test exercising the dmem cgroup controller on xe
devices.
The write_eviction subtest:
- Skips if the dmem cgroup controller is not available.
- Skips if no VRAM region is registered with the dmem controller.
- Creates a sub-cgroup and moves the test process into it.
- Sets a dmem.max limit on the first VRAM region (up to 4 GiB, or
the full capacity if smaller).
- Creates an LR VM and fills VRAM by repeatedly creating BOs with
DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING and binding them via
__xe_vm_bind_lr_sync() until -ENOMEM or -ENOSPC is returned.
- Verifies that cgroup current usage is within the expected range when
the limit is hit.
- Lowers dmem.max in 256 MiB steps, waiting for usage to follow each
reduction.
The write_eviction_interruptible subtest runs the same test with
SIGCONT signals injected via igt_fork_signal_helper() and reports the
number of signals received. When a signal interrupts kernel-side
eviction, a small BO allocation is used to re-trigger it.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/intel/xe_cgroups.c | 276 +++++++++++++++++++++++++++++++++++++++
tests/meson.build | 1 +
2 files changed, 277 insertions(+)
create mode 100644 tests/intel/xe_cgroups.c
diff --git a/tests/intel/xe_cgroups.c b/tests/intel/xe_cgroups.c
new file mode 100644
index 000000000..8b0f4381f
--- /dev/null
+++ b/tests/intel/xe_cgroups.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: MIT
+/*
+ * Copyright © 2026 Intel Corporation
+ */
+
+/**
+ * TEST: xe_cgroups
+ * DESCRIPTION: Tests exercising the dmem cgroup controller on xe devices.
+ * Category: Core
+ * Mega feature: General Core features
+ * Sub-category: cgroup
+ * FUNCTIONALITY: cgroup dmem controller
+ * SUBSETS: xe
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "drmtest.h"
+#include "igt.h"
+#include "igt_aux.h"
+#include "igt_cgroup.h"
+#include "xe_drm.h"
+#include "xe/xe_ioctl.h"
+#include "xe/xe_query.h"
+
+#define BO_SIZE SZ_128M
+#define MAX_LIMIT ((uint64_t)4 * SZ_1G)
+#define EVICT_STEP SZ_256M
+#define BIND_BASE 0x100000000ULL /* 4 GiB VA base */
+
+#define TEST_INTERRUPTIBLE (1 << 0)
+
+/**
+ * SUBTEST: write_eviction
+ * DESCRIPTION:
+ * Create a dmem cgroup, move the current process into it and set the max
+ * device memory limit for the first VRAM region to 4 GiB. Then fill VRAM
+ * by creating BOs with %DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING (so that the
+ * physical allocation is deferred until VM_BIND) and binding them into an
+ * LR VM until the cgroup limit is hit. Verify that the reported cgroup
+ * current usage is within the expected range when the error occurs.
+ * Finally lower the max limit in 256 MiB steps and verify that the cgroup
+ * usage follows.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
+/**
+ * SUBTEST: write_eviction_interruptible
+ * DESCRIPTION:
+ * Same as write_eviction but with SIGCONT signals injected throughout via
+ * igt_fork_signal_helper() to verify that the dmem.max write path handles
+ * signal interruption correctly. A signal handler counts received signals
+ * and the count is reported as debug output at the end of the test.
+ * A signal interrupts the set-time eviction, and further eviction can be
+ * triggered by an explicit allocation.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
+static atomic_int signal_count;
+static struct sigaction sigcont_oldact;
+
+static void sigcont_handler(int sig)
+{
+ atomic_fetch_add(&signal_count, 1);
+
+ /* Chain to the previous handler (IGT's dummy sig_handler) */
+ if (sigcont_oldact.sa_handler &&
+ sigcont_oldact.sa_handler != SIG_IGN &&
+ sigcont_oldact.sa_handler != SIG_DFL)
+ sigcont_oldact.sa_handler(sig);
+}
+
+static void install_sigcont_counter(void)
+{
+ struct sigaction sa;
+
+ atomic_store(&signal_count, 0);
+ igt_fork_signal_helper();
+ /*
+ * Install the counter after igt_fork_signal_helper() so our handler
+ * is not overwritten. Save the old handler so we can chain to it.
+ */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sigcont_handler;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGCONT, &sa, &sigcont_oldact);
+}
+
+static int fill_vram(int fd, uint32_t vm, uint64_t vram_region,
+ uint32_t *handles, int max_bo)
+{
+ uint32_t handle;
+ uint64_t addr = BIND_BASE;
+ int n_bo, err = 0;
+
+ for (n_bo = 0; n_bo < max_bo; n_bo++) {
+ err = __xe_bo_create(fd, 0, BO_SIZE, vram_region,
+ DRM_XE_GEM_CREATE_FLAG_DEFER_BACKING,
+ NULL, &handle);
+ if (err)
+ break;
+
+ handles[n_bo] = handle;
+
+ err = __xe_vm_bind_lr_sync(fd, vm, handle, 0, addr, BO_SIZE, 0);
+ if (err)
+ break;
+
+ addr += BO_SIZE;
+ }
+
+ igt_assert_f(err == -ENOMEM || err == -ENOSPC,
+ "Expected -ENOMEM or -ENOSPC, got %d (%s)\n",
+ err, strerror(-err));
+
+ return n_bo;
+}
+
+static void unfill_vram(int fd, uint32_t vm, uint32_t *handles, int n_bo)
+{
+ uint64_t addr = BIND_BASE;
+ int i;
+
+ for (i = 0; i < n_bo; i++) {
+ if (handles[i]) {
+ xe_vm_unbind_lr_sync(fd, vm, 0, addr, BO_SIZE);
+ gem_close(fd, handles[i]);
+ }
+ addr += BO_SIZE;
+ }
+ free(handles);
+}
+
+static void test_write_eviction(int fd, unsigned int flags)
+{
+ struct igt_cgroup *cg;
+ char *cg_region;
+ uint32_t vm;
+ uint64_t vram_region = 0;
+ uint64_t region;
+ uint32_t *handles = NULL;
+ int n_bo = 0, max_bo;
+ uint64_t current, capacity, cg_max, limit, after;
+
+ /* Check dmem cgroup controller is available before doing anything else */
+ igt_require_f(igt_cgroup_dmem_available(),
+ "dmem cgroup controller not available (no cgroup v2 or no registered regions)\n");
+
+ /* Find first VRAM region */
+ xe_for_each_mem_region(fd, all_memory_regions(fd), region) {
+ if (xe_region_class(fd, region) == DRM_XE_MEM_REGION_CLASS_VRAM) {
+ vram_region = region;
+ break;
+ }
+ }
+ igt_require_f(vram_region, "No VRAM region found on this device\n");
+
+ cg_region = xe_cgroup_region_name(fd, vram_region);
+ igt_require_f(cg_region, "Region not tracked by dmem cgroup controller\n");
+
+ igt_cgroup_dmem_get_capacity(cg_region, &capacity);
+ igt_require_f(capacity >= 4 * BO_SIZE,
+ "VRAM capacity (%"PRIu64" MiB) too small to test\n",
+ capacity / SZ_1M);
+
+ /*
+ * Use up to 4 GiB, or the full capacity if the device has less.
+ * Leave one BO_SIZE worth of headroom so the device isn't completely
+ * exhausted before the cgroup limit is hit.
+ */
+ cg_max = min(MAX_LIMIT, capacity - BO_SIZE);
+ cg_max = ALIGN_DOWN(cg_max, EVICT_STEP);
+
+ if (flags & TEST_INTERRUPTIBLE)
+ install_sigcont_counter();
+
+ /* Create cgroup and move into it */
+ cg = igt_cgroup_new("xe_cgroups_test");
+ igt_cgroup_move_current(cg);
+ igt_cgroup_dmem_set_max(cg, cg_region, cg_max, false);
+
+ vm = xe_vm_create(fd, DRM_XE_VM_CREATE_FLAG_LR_MODE, 0);
+
+ max_bo = (cg_max / BO_SIZE) + 8; /* headroom for overcommit */
+ handles = calloc(max_bo, sizeof(*handles));
+ igt_assert(handles);
+
+ n_bo = fill_vram(fd, vm, vram_region, handles, max_bo);
+
+ igt_cgroup_dmem_get_current(cg, cg_region, ¤t);
+ igt_debug("After fill: cgroup current = %"PRIu64" MiB, "
+ "max = %"PRIu64" MiB\n",
+ current / SZ_1M, cg_max / SZ_1M);
+
+ igt_assert_f(current <= cg_max,
+ "Usage %"PRIu64" MiB exceeds max %"PRIu64" MiB + slack\n",
+ current / SZ_1M, cg_max / SZ_1M);
+
+ /* Phase 2: lower max in 256 MiB steps, verify usage follows */
+ limit = cg_max;
+ while (limit >= EVICT_STEP) {
+
+ limit -= EVICT_STEP;
+ igt_cgroup_dmem_set_max(cg, cg_region, limit, false);
+
+ igt_cgroup_dmem_get_current(cg, cg_region, &after);
+ igt_debug("Lowered max to %"PRIu64" MiB: usage = %"PRIu64" MiB\n",
+ limit / SZ_1M, after / SZ_1M);
+
+ if (limit > EVICT_STEP) {
+ if ((flags & TEST_INTERRUPTIBLE) && after > limit) {
+ uint32_t handle;
+
+ /* Let a new bo creation trigger eviction. */
+ handle = xe_bo_create(fd, 0, BO_SIZE / 8, vram_region, 0);
+ gem_close(fd, handle);
+
+ igt_cgroup_dmem_get_current(cg, cg_region, &after);
+ igt_debug("Forced eviction max is %"PRIu64
+ " MiB: usage = %"PRIu64" MiB\n",
+ limit / SZ_1M, after / SZ_1M);
+ }
+
+ igt_assert_f(after <= limit,
+ "Usage %"PRIu64" MiB did not follow max %"PRIu64" MiB\n",
+ after / SZ_1M, limit / SZ_1M);
+ }
+ }
+
+ if (flags & TEST_INTERRUPTIBLE) {
+ igt_stop_signal_helper();
+ igt_info("Signals received during test: %d\n",
+ atomic_load(&signal_count));
+ }
+
+ /* Cleanup */
+ igt_cgroup_dmem_set_max(cg, cg_region, IGT_CGROUP_DMEM_MAX, false);
+ unfill_vram(fd, vm, handles, n_bo);
+ handles = NULL;
+ xe_vm_destroy(fd, vm);
+ free(cg_region);
+ igt_cgroup_free(cg);
+}
+
+static const struct {
+ const char *name;
+ unsigned int flags;
+} subtests[] = {
+ { "write_eviction", 0 },
+ { "write_eviction_interruptible", TEST_INTERRUPTIBLE },
+ { }
+};
+
+int igt_main()
+{
+ int fd = -1;
+
+ igt_fixture() {
+ fd = drm_open_driver(DRIVER_XE);
+ igt_require_f(getuid() == 0, "Test requires root\n");
+ }
+
+ for (int i = 0; subtests[i].name; i++)
+ igt_subtest(subtests[i].name)
+ test_write_eviction(fd, subtests[i].flags);
+
+ igt_fixture() {
+ drm_close_driver(fd);
+ }
+}
diff --git a/tests/meson.build b/tests/meson.build
index 02fbb8c4e..eb1158510 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -292,6 +292,7 @@ intel_xe_progs = [
'xe_dma_buf_sync',
'xe_drm_fdinfo',
'xe_eu_stall',
+ 'xe_cgroups',
'xe_evict',
'xe_evict_ccs',
'xe_exec_atomic',
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (4 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 5/6] tests/xe_cgroups: add dmem cgroup eviction test Thomas Hellström
@ 2026-04-28 6:54 ` Thomas Hellström
2026-04-28 7:59 ` ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2) Patchwork
2026-04-28 8:19 ` ✗ i915.CI.BAT: failure " Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Thomas Hellström @ 2026-04-28 6:54 UTC (permalink / raw)
To: igt-dev; +Cc: maarten.lankhorst, Thomas Hellström
Add write_eviction_nonblock to exercise the O_NONBLOCK path of the dmem
cgroup max interface. After filling VRAM to the cgroup limit, each
limit-lowering step writes dmem.max with O_NONBLOCK so that synchronous
eviction is skipped. The test then verifies that usage has not yet
dropped below the new limit, allocates a small BO to trigger eviction
explicitly, and finally confirms that usage falls within bounds.
Assisted-by: GitHub Copilot:claude-sonnet-4.6
Signed-off-by: Thomas Hellström <thomas.hellstrom@linux.intel.com>
---
tests/intel/xe_cgroups.c | 43 ++++++++++++++++++++++++++++++++++------
1 file changed, 37 insertions(+), 6 deletions(-)
diff --git a/tests/intel/xe_cgroups.c b/tests/intel/xe_cgroups.c
index 8b0f4381f..aa7a8c3b2 100644
--- a/tests/intel/xe_cgroups.c
+++ b/tests/intel/xe_cgroups.c
@@ -35,6 +35,7 @@
#define BIND_BASE 0x100000000ULL /* 4 GiB VA base */
#define TEST_INTERRUPTIBLE (1 << 0)
+#define TEST_NONBLOCK (1 << 1)
/**
* SUBTEST: write_eviction
@@ -62,6 +63,18 @@
* REQUIREMENTS: must run as root; xe device with at least one VRAM region
*/
+/**
+ * SUBTEST: write_eviction_nonblock
+ * DESCRIPTION:
+ * Same fill phase as write_eviction. In the limit-lowering phase dmem.max
+ * is written with O_NONBLOCK, which causes the kernel to skip synchronous
+ * eviction. After each nonblock write the test verifies that usage has not
+ * yet dropped below the new limit, then triggers eviction explicitly by
+ * allocating a small BO. Finally verifies that usage falls within bounds
+ * after the forced eviction.
+ * REQUIREMENTS: must run as root; xe device with at least one VRAM region
+ */
+
static atomic_int signal_count;
static struct sigaction sigcont_oldact;
@@ -146,7 +159,7 @@ static void test_write_eviction(int fd, unsigned int flags)
uint64_t region;
uint32_t *handles = NULL;
int n_bo = 0, max_bo;
- uint64_t current, capacity, cg_max, limit, after;
+ uint64_t current, capacity, cg_max, limit, after, before;
/* Check dmem cgroup controller is available before doing anything else */
igt_require_f(igt_cgroup_dmem_available(),
@@ -207,18 +220,35 @@ static void test_write_eviction(int fd, unsigned int flags)
while (limit >= EVICT_STEP) {
limit -= EVICT_STEP;
- igt_cgroup_dmem_set_max(cg, cg_region, limit, false);
+
+ if (flags & TEST_NONBLOCK)
+ igt_cgroup_dmem_get_current(cg, cg_region, &before);
+
+ igt_cgroup_dmem_set_max(cg, cg_region, limit,
+ !!(flags & TEST_NONBLOCK));
igt_cgroup_dmem_get_current(cg, cg_region, &after);
igt_debug("Lowered max to %"PRIu64" MiB: usage = %"PRIu64" MiB\n",
limit / SZ_1M, after / SZ_1M);
+ if (flags & TEST_NONBLOCK) {
+ /*
+ * O_NONBLOCK skips eviction: verify usage has not
+ * dropped below the new limit yet.
+ */
+ igt_assert_f(after == before,
+ "Expected no eviction with O_NONBLOCK, but "
+ "usage dropped from %"PRIu64" MiB to %"PRIu64" MiB "
+ "(limit %"PRIu64" MiB)\n",
+ before / SZ_1M, after / SZ_1M, limit / SZ_1M);
+ }
+
if (limit > EVICT_STEP) {
- if ((flags & TEST_INTERRUPTIBLE) && after > limit) {
+ if ((flags & (TEST_INTERRUPTIBLE | TEST_NONBLOCK)) && after > limit) {
uint32_t handle;
- /* Let a new bo creation trigger eviction. */
- handle = xe_bo_create(fd, 0, BO_SIZE / 8, vram_region, 0);
+ handle = xe_bo_create(fd, 0, BO_SIZE / 8,
+ vram_region, 0);
gem_close(fd, handle);
igt_cgroup_dmem_get_current(cg, cg_region, &after);
@@ -252,8 +282,9 @@ static const struct {
const char *name;
unsigned int flags;
} subtests[] = {
- { "write_eviction", 0 },
+ { "write_eviction", 0 },
{ "write_eviction_interruptible", TEST_INTERRUPTIBLE },
+ { "write_eviction_nonblock", TEST_NONBLOCK },
{ }
};
--
2.53.0
^ permalink raw reply related [flat|nested] 9+ messages in thread* ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2)
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (5 preceding siblings ...)
2026-04-28 6:54 ` [PATCH i-g-t 6/6] tests/xe_cgroups: add write_eviction_nonblock subtest Thomas Hellström
@ 2026-04-28 7:59 ` Patchwork
2026-04-28 8:19 ` ✗ i915.CI.BAT: failure " Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Patchwork @ 2026-04-28 7:59 UTC (permalink / raw)
To: Thomas Hellström; +Cc: igt-dev
[-- Attachment #1: Type: text/plain, Size: 1060 bytes --]
== Series Details ==
Series: Initial dmem cgroup support (rev2)
URL : https://patchwork.freedesktop.org/series/163935/
State : success
== Summary ==
CI Bug Log - changes from XEIGT_8874_BAT -> XEIGTPW_15072_BAT
====================================================
Summary
-------
**SUCCESS**
No regressions found.
Participating hosts (13 -> 13)
------------------------------
No changes in participating hosts
Changes
-------
No changes found
Build changes
-------------
* IGT: IGT_8874 -> IGTPW_15072
* Linux: xe-4940-b6f6b69b2dffa9ad1c43b2149786b4630d41acbf -> xe-4944-aea2c496abcf55b647c14fe720bfc4ea555aac6a
IGTPW_15072: 15072
IGT_8874: 4568b2c141ab630c34f8eb2b9afab8cbf8f3ce9e @ https://gitlab.freedesktop.org/drm/igt-gpu-tools.git
xe-4940-b6f6b69b2dffa9ad1c43b2149786b4630d41acbf: b6f6b69b2dffa9ad1c43b2149786b4630d41acbf
xe-4944-aea2c496abcf55b647c14fe720bfc4ea555aac6a: aea2c496abcf55b647c14fe720bfc4ea555aac6a
== Logs ==
For more details see: https://intel-gfx-ci.01.org/tree/intel-xe/IGTPW_15072/index.html
[-- Attachment #2: Type: text/html, Size: 1619 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread* ✗ i915.CI.BAT: failure for Initial dmem cgroup support (rev2)
2026-04-28 6:54 [PATCH i-g-t 0/6] Initial dmem cgroup support Thomas Hellström
` (6 preceding siblings ...)
2026-04-28 7:59 ` ✓ Xe.CI.BAT: success for Initial dmem cgroup support (rev2) Patchwork
@ 2026-04-28 8:19 ` Patchwork
7 siblings, 0 replies; 9+ messages in thread
From: Patchwork @ 2026-04-28 8:19 UTC (permalink / raw)
To: Thomas Hellström; +Cc: igt-dev
[-- Attachment #1: Type: text/plain, Size: 3213 bytes --]
== Series Details ==
Series: Initial dmem cgroup support (rev2)
URL : https://patchwork.freedesktop.org/series/163935/
State : failure
== Summary ==
CI Bug Log - changes from IGT_8874 -> IGTPW_15072
====================================================
Summary
-------
**FAILURE**
Serious unknown changes coming with IGTPW_15072 absolutely need to be
verified manually.
If you think the reported changes have nothing to do with the changes
introduced in IGTPW_15072, please notify your bug team (I915-ci-infra@lists.freedesktop.org) to allow them
to document this new failure mode, which will reduce false positives in CI.
External URL: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/index.html
Participating hosts (42 -> 40)
------------------------------
Missing (2): bat-dg2-13 fi-snb-2520m
Possible new issues
-------------------
Here are the unknown changes that may have been introduced in IGTPW_15072:
### IGT changes ###
#### Possible regressions ####
* igt@i915_selftest@live@workarounds:
- bat-arlh-3: [PASS][1] -> [INCOMPLETE][2]
[1]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-arlh-3/igt@i915_selftest@live@workarounds.html
[2]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-arlh-3/igt@i915_selftest@live@workarounds.html
Known issues
------------
Here are the changes found in IGTPW_15072 that come from known issues:
### IGT changes ###
#### Issues hit ####
* igt@i915_selftest@live:
- bat-arlh-3: [PASS][3] -> [INCOMPLETE][4] ([i915#15622])
[3]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-arlh-3/igt@i915_selftest@live.html
[4]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-arlh-3/igt@i915_selftest@live.html
* igt@i915_selftest@live@workarounds:
- bat-mtlp-9: [PASS][5] -> [DMESG-FAIL][6] ([i915#12061]) +1 other test dmesg-fail
[5]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-mtlp-9/igt@i915_selftest@live@workarounds.html
[6]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-mtlp-9/igt@i915_selftest@live@workarounds.html
#### Possible fixes ####
* igt@i915_selftest@live:
- bat-dg2-8: [DMESG-FAIL][7] ([i915#12061]) -> [PASS][8] +1 other test pass
[7]: https://intel-gfx-ci.01.org/tree/drm-tip/IGT_8874/bat-dg2-8/igt@i915_selftest@live.html
[8]: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/bat-dg2-8/igt@i915_selftest@live.html
[i915#12061]: https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/12061
[i915#15622]: https://gitlab.freedesktop.org/drm/i915/kernel/-/issues/15622
Build changes
-------------
* CI: CI-20190529 -> None
* IGT: IGT_8874 -> IGTPW_15072
* Linux: CI_DRM_18369 -> CI_DRM_18373
CI-20190529: 20190529
CI_DRM_18369: b6f6b69b2dffa9ad1c43b2149786b4630d41acbf @ git://anongit.freedesktop.org/gfx-ci/linux
CI_DRM_18373: aea2c496abcf55b647c14fe720bfc4ea555aac6a @ git://anongit.freedesktop.org/gfx-ci/linux
IGTPW_15072: 15072
IGT_8874: 4568b2c141ab630c34f8eb2b9afab8cbf8f3ce9e @ https://gitlab.freedesktop.org/drm/igt-gpu-tools.git
== Logs ==
For more details see: https://intel-gfx-ci.01.org/tree/drm-tip/IGTPW_15072/index.html
[-- Attachment #2: Type: text/html, Size: 3994 bytes --]
^ permalink raw reply [flat|nested] 9+ messages in thread