* [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest
@ 2026-11-12 9:02 Sarthak Sharma
2026-11-12 9:02 ` [PATCH 1/4] selftests/mm: remove transhuge-stress Sarthak Sharma
` (3 more replies)
0 siblings, 4 replies; 6+ messages in thread
From: Sarthak Sharma @ 2026-11-12 9:02 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm,
linux-kselftest, linux-kernel, Sarthak Sharma
transhuge-stress is a stressor tool that repeatedly
allocates and splits THPs till a specified duration. If the
duration elapses, the test passes and fails only if a setup or
system call fails.
This is not how a test residing in selftests/mm is supposed to
behave. So remove it and add a new functional test called
thp-compaction.c, that allocates THPs as much as it can,
splits those THPs keeping one page retained and discarding
others, and then retries allocating THPs, which will test
if THP compaction is working fine. If we are able to recover
more than half of the initial THPs, the test passes. Otherwise
it fails.
This was suggested by David earlier [1].
Patch 1 removes the current transhuge-stress. Patch 2 adds a check to see
if PFNs are being masked due to no CAP_SYS_ADMIN in the initial user namespace.
Patch 3 factors out THP split helpers from split_huge_page_test.c to vm_util.c.
Patch 4 adds the
new test.
[1] https://lore.kernel.org/all/94ace42d-8d60-4475-953e-74fb3dc23774@kernel.org/
Sarthak Sharma (4):
selftests/mm: remove transhuge-stress
selftests/mm: detect masked PFNs in allocate_transhuge()
selftests/mm: factor out THP split helpers
selftests/mm: add a THP compaction selftest
MAINTAINERS | 2 +-
tools/testing/selftests/mm/Makefile | 2 +-
tools/testing/selftests/mm/ksm_tests.c | 26 ++-
tools/testing/selftests/mm/run_vmtests.sh | 2 +-
.../selftests/mm/split_huge_page_test.c | 45 +---
tools/testing/selftests/mm/thp-compaction.c | 215 ++++++++++++++++++
tools/testing/selftests/mm/transhuge-stress.c | 146 ------------
tools/testing/selftests/mm/vm_util.c | 65 ++++++
tools/testing/selftests/mm/vm_util.h | 6 +
9 files changed, 313 insertions(+), 196 deletions(-)
create mode 100644 tools/testing/selftests/mm/thp-compaction.c
delete mode 100644 tools/testing/selftests/mm/transhuge-stress.c
--
2.53.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH 1/4] selftests/mm: remove transhuge-stress
2026-11-12 9:02 [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest Sarthak Sharma
@ 2026-11-12 9:02 ` Sarthak Sharma
2026-11-12 9:02 ` [PATCH 2/4] selftests/mm: detect masked PFNs in allocate_transhuge() Sarthak Sharma
` (2 subsequent siblings)
3 siblings, 0 replies; 6+ messages in thread
From: Sarthak Sharma @ 2026-11-12 9:02 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm,
linux-kselftest, linux-kernel, Sarthak Sharma
transhuge-stress currently behaves like a stressor which takes arguments
like duration and size. By default, it tries to allocate and split a
THP at every hugepage sized offset in a range equal to RAM. It counts
successful and failed allocations and prints them. The test passes if
the specified duration has elapsed and fails only if a setup or system
call fails.
This is not how a selftest is supposed to behave. Remove
transhuge-stress from selftests/mm and remove its references from
Makefile, run_vmtests.sh and MAINTAINERS. A subsequent patch adds a
selftest for THP compaction.
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
MAINTAINERS | 1 -
tools/testing/selftests/mm/Makefile | 1 -
tools/testing/selftests/mm/run_vmtests.sh | 2 -
tools/testing/selftests/mm/transhuge-stress.c | 146 ------------------
4 files changed, 150 deletions(-)
delete mode 100644 tools/testing/selftests/mm/transhuge-stress.c
diff --git a/MAINTAINERS b/MAINTAINERS
index ce05a3253baa..4d74ad879ecf 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17946,7 +17946,6 @@ F: mm/mm_slot.h
F: tools/mm/thpmaps
F: tools/testing/selftests/mm/khugepaged.c
F: tools/testing/selftests/mm/split_huge_page_test.c
-F: tools/testing/selftests/mm/transhuge-stress.c
MEMORY MANAGEMENT - USERFAULTFD
M: Andrew Morton <akpm@linux-foundation.org>
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 675ec57d47de..06d3d0350a9a 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -90,7 +90,6 @@ TEST_GEN_FILES += pfnmap
TEST_GEN_FILES += process_madv
TEST_GEN_FILES += prctl_thp_disable
TEST_GEN_FILES += thuge-gen
-TEST_GEN_FILES += transhuge-stress
TEST_GEN_FILES += uffd-stress
TEST_GEN_FILES += uffd-unit-tests
TEST_GEN_FILES += uffd-wp-mremap
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index 973cb715b326..cb5c7cccb585 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -448,8 +448,6 @@ if [ -n "${MOUNTED_XFS}" ]; then
rm -f ${XFS_IMG}
fi
-CATEGORY="thp" run_test ./transhuge-stress -d 20
-
CATEGORY="thp" run_test ./folio_split_race_test
CATEGORY="migration" run_test ./migration
diff --git a/tools/testing/selftests/mm/transhuge-stress.c b/tools/testing/selftests/mm/transhuge-stress.c
deleted file mode 100644
index ad2831c864dc..000000000000
--- a/tools/testing/selftests/mm/transhuge-stress.c
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * Stress test for transparent huge pages, memory compaction and migration.
- *
- * Authors: Konstantin Khlebnikov <koct9i@gmail.com>
- *
- * This is free and unencumbered software released into the public domain.
- */
-
-#include <stdlib.h>
-#include <stdio.h>
-#include <stdint.h>
-#include <err.h>
-#include <time.h>
-#include <unistd.h>
-#include <fcntl.h>
-#include <string.h>
-#include <sys/mman.h>
-#include "vm_util.h"
-#include "kselftest.h"
-
-int backing_fd = -1;
-int mmap_flags = MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE;
-#define PROT_RW (PROT_READ | PROT_WRITE)
-
-int main(int argc, char **argv)
-{
- size_t ram, len;
- void *ptr, *p;
- struct timespec start, a, b;
- int i = 0;
- char *name = NULL;
- double s;
- uint8_t *map;
- size_t map_len;
- int pagemap_fd;
- int duration = 0;
- const uint64_t hpage_size = pmd_psize();
- const uint64_t hpage_shift = pmd_pshift();
-
- ksft_print_header();
-
- if (!thp_is_enabled())
- ksft_exit_skip("Transparent Hugepages not available\n");
-
- if (!hpage_size || !hpage_shift)
- ksft_exit_fail_msg("reading hpage_size failed\n");
-
- ram = sysconf(_SC_PHYS_PAGES);
- if (ram > SIZE_MAX / psize() / 4)
- ram = SIZE_MAX / 4;
- else
- ram *= psize();
- len = ram;
-
- while (++i < argc) {
- if (!strcmp(argv[i], "-h"))
- ksft_exit_fail_msg("usage: %s [-f <filename>] [-d <duration>] [size in MiB]\n",
- argv[0]);
- else if (!strcmp(argv[i], "-f"))
- name = argv[++i];
- else if (!strcmp(argv[i], "-d"))
- duration = atoi(argv[++i]);
- else
- len = atoll(argv[i]) << 20;
- }
-
- ksft_set_plan(1);
-
- if (name) {
- backing_fd = open(name, O_RDWR);
- if (backing_fd == -1)
- ksft_exit_fail_msg("open %s\n", name);
- mmap_flags = MAP_SHARED;
- }
-
- warnx("allocate %zd transhuge pages, using %zd MiB virtual memory"
- " and %zd MiB of ram", len >> hpage_shift, len >> 20,
- ram >> (20 + hpage_shift - pshift() - 1));
-
- pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
- if (pagemap_fd < 0)
- ksft_exit_fail_msg("open pagemap\n");
-
- len -= len % hpage_size;
- ptr = mmap(NULL, len + hpage_size, PROT_RW, mmap_flags, backing_fd, 0);
- if (ptr == MAP_FAILED)
- ksft_exit_fail_msg("initial mmap");
- ptr += hpage_size - (uintptr_t)ptr % hpage_size;
-
- if (madvise(ptr, len, MADV_HUGEPAGE))
- ksft_exit_fail_msg("MADV_HUGEPAGE");
-
- map_len = ram >> (hpage_shift - 1);
- map = malloc(map_len);
- if (!map)
- ksft_exit_fail_msg("map malloc\n");
-
- clock_gettime(CLOCK_MONOTONIC, &start);
-
- while (1) {
- int nr_succeed = 0, nr_failed = 0, nr_pages = 0;
-
- memset(map, 0, map_len);
-
- clock_gettime(CLOCK_MONOTONIC, &a);
- for (p = ptr; p < ptr + len; p += hpage_size) {
- int64_t pfn;
-
- pfn = allocate_transhuge(p, pagemap_fd);
-
- if (pfn < 0) {
- nr_failed++;
- } else {
- size_t idx = pfn >> (hpage_shift - pshift());
-
- nr_succeed++;
- if (idx >= map_len) {
- map = realloc(map, idx + 1);
- if (!map)
- ksft_exit_fail_msg("map realloc\n");
- memset(map + map_len, 0, idx + 1 - map_len);
- map_len = idx + 1;
- }
- if (!map[idx])
- nr_pages++;
- map[idx] = 1;
- }
-
- /* split transhuge page, keep last page */
- if (madvise(p, hpage_size - psize(), MADV_DONTNEED))
- ksft_exit_fail_msg("MADV_DONTNEED");
- }
- clock_gettime(CLOCK_MONOTONIC, &b);
- s = b.tv_sec - a.tv_sec + (b.tv_nsec - a.tv_nsec) / 1000000000.;
-
- ksft_print_msg("%.3f s/loop, %.3f ms/page, %10.3f MiB/s\t"
- "%4d succeed, %4d failed, %4d different pages\n",
- s, s * 1000 / (len >> hpage_shift), len / s / (1 << 20),
- nr_succeed, nr_failed, nr_pages);
-
- if (duration > 0 && b.tv_sec - start.tv_sec >= duration) {
- ksft_test_result_pass("Completed\n");
- ksft_finished();
- }
- }
-}
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 2/4] selftests/mm: detect masked PFNs in allocate_transhuge()
2026-11-12 9:02 [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest Sarthak Sharma
2026-11-12 9:02 ` [PATCH 1/4] selftests/mm: remove transhuge-stress Sarthak Sharma
@ 2026-11-12 9:02 ` Sarthak Sharma
2026-11-12 9:02 ` [PATCH 3/4] selftests/mm: factor out THP split helpers Sarthak Sharma
2026-11-12 9:02 ` [PATCH 4/4] selftests/mm: add a THP compaction selftest Sarthak Sharma
3 siblings, 0 replies; 6+ messages in thread
From: Sarthak Sharma @ 2026-11-12 9:02 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm,
linux-kselftest, linux-kernel, Sarthak Sharma
allocate_transhuge() uses pagemap PFNs to determine whether a THP
was allocated. But pagemap PFN returns zero PFNs when the caller
is not allowed to read them (without CAP_SYS_ADMIN). Currently
this looks like a normal THP allocation failure.
When both pagemap entries are present but their PFNs are zero,
return -EACCES since the THP size and alignment cannot be verified.
In case of a base page fallback, the second entry won't be present
and thus it will still return -1 indicating a THP allocation failure.
Update ksm_tests.c to skip the test when pagemap PFNs are
unavailable. While here, use a common cleanup path and
add the missing close(pagemap_fd).
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
tools/testing/selftests/mm/ksm_tests.c | 26 +++++++++++++++++---------
tools/testing/selftests/mm/vm_util.c | 9 +++++++++
2 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/tools/testing/selftests/mm/ksm_tests.c b/tools/testing/selftests/mm/ksm_tests.c
index 9e6e1633d0b5..9f97ce28e72a 100644
--- a/tools/testing/selftests/mm/ksm_tests.c
+++ b/tools/testing/selftests/mm/ksm_tests.c
@@ -10,6 +10,7 @@
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
+#include <errno.h>
#include <err.h>
#include "kselftest.h"
@@ -521,6 +522,8 @@ static int ksm_merge_hugepages_time(int merge_type, int mapping, int prot,
unsigned long scan_time_ns;
int pagemap_fd, n_normal_pages, n_huge_pages;
const uint64_t hpage_size = pmd_psize();
+ int status = KSFT_FAIL;
+ int64_t pfn;
if (!thp_is_enabled()) {
ksft_print_msg("Transparent Hugepages not available\n");
@@ -551,7 +554,13 @@ static int ksm_merge_hugepages_time(int merge_type, int mapping, int prot,
n_normal_pages = 0;
n_huge_pages = 0;
for (void *p = map_ptr; p < map_ptr + len; p += hpage_size) {
- if (allocate_transhuge(p, pagemap_fd) < 0)
+ pfn = allocate_transhuge(p, pagemap_fd);
+ if (pfn == -EACCES) {
+ ksft_print_msg("Pagemap PFNs are unavailable\n");
+ status = KSFT_SKIP;
+ goto out;
+ }
+ if (pfn < 0)
n_normal_pages++;
else
n_huge_pages++;
@@ -563,13 +572,13 @@ static int ksm_merge_hugepages_time(int merge_type, int mapping, int prot,
if (clock_gettime(CLOCK_MONOTONIC_RAW, &start_time)) {
ksft_perror("clock_gettime");
- goto err_out;
+ goto out;
}
if (ksm_merge_pages(merge_type, map_ptr, map_size, start_time, timeout))
- goto err_out;
+ goto out;
if (clock_gettime(CLOCK_MONOTONIC_RAW, &end_time)) {
ksft_perror("clock_gettime");
- goto err_out;
+ goto out;
}
scan_time_ns = (end_time.tv_sec - start_time.tv_sec) * NSEC_PER_SEC +
@@ -581,12 +590,11 @@ static int ksm_merge_hugepages_time(int merge_type, int mapping, int prot,
ksft_print_msg("Average speed: %.3f MiB/s\n", (map_size / MB) /
((double)scan_time_ns / NSEC_PER_SEC));
+ status = KSFT_PASS;
+out:
+ close(pagemap_fd);
munmap(map_ptr_orig, len + hpage_size);
- return KSFT_PASS;
-
-err_out:
- munmap(map_ptr_orig, len + hpage_size);
- return KSFT_FAIL;
+ return status;
}
static int ksm_merge_time(int merge_type, int mapping, int prot, int timeout, size_t map_size)
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 8ef226107d78..9942bbea7837 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -534,6 +534,15 @@ int64_t allocate_transhuge(void *ptr, int pagemap_fd)
(uintptr_t)ptr >> (pshift() - 3)) != sizeof(ent))
ksft_exit_fail_msg("read pagemap\n");
+ /*
+ * Both entries present means that a THP has been allocated. But if
+ * their PFNs are masked, we cannot verify the size and alignment of
+ * the THP
+ */
+ if (PAGEMAP_PRESENT(ent[0]) && PAGEMAP_PRESENT(ent[1]) &&
+ !PAGEMAP_PFN(ent[0]) && !PAGEMAP_PFN(ent[1]))
+ return -EACCES;
+
if (PAGEMAP_PRESENT(ent[0]) && PAGEMAP_PRESENT(ent[1]) &&
PAGEMAP_PFN(ent[0]) + 1 == PAGEMAP_PFN(ent[1]) &&
!(PAGEMAP_PFN(ent[0]) & ((1 << (hpage_shift - pshift())) - 1)))
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 3/4] selftests/mm: factor out THP split helpers
2026-11-12 9:02 [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest Sarthak Sharma
2026-11-12 9:02 ` [PATCH 1/4] selftests/mm: remove transhuge-stress Sarthak Sharma
2026-11-12 9:02 ` [PATCH 2/4] selftests/mm: detect masked PFNs in allocate_transhuge() Sarthak Sharma
@ 2026-11-12 9:02 ` Sarthak Sharma
2026-11-12 9:02 ` [PATCH 4/4] selftests/mm: add a THP compaction selftest Sarthak Sharma
3 siblings, 0 replies; 6+ messages in thread
From: Sarthak Sharma @ 2026-11-12 9:02 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm,
linux-kselftest, linux-kernel, Sarthak Sharma
split_huge_page_test.c contains helpers to split huge pages using
the split_huge_pages debugfs interface. A later patch adds a THP
compaction selftest and needs the same functionality.
Move write_debugfs() to vm_util.c. Add wrappers for splitting
process mappings, mappings with an offset within folio and file
backed folios. Also add a helper to check if the debugfs interface
is available.
Update split_huge_page_test.c to use the shared helpers and
also remove the unused SMAP_PATH definition.
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
.../selftests/mm/split_huge_page_test.c | 45 +++------------
tools/testing/selftests/mm/vm_util.c | 56 +++++++++++++++++++
tools/testing/selftests/mm/vm_util.h | 6 ++
3 files changed, 69 insertions(+), 38 deletions(-)
diff --git a/tools/testing/selftests/mm/split_huge_page_test.c b/tools/testing/selftests/mm/split_huge_page_test.c
index 33a2473327bb..330a453e6402 100644
--- a/tools/testing/selftests/mm/split_huge_page_test.c
+++ b/tools/testing/selftests/mm/split_huge_page_test.c
@@ -8,7 +8,6 @@
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
-#include <stdarg.h>
#include <unistd.h>
#include <inttypes.h>
#include <string.h>
@@ -28,14 +27,8 @@ uint64_t pmd_pagesize;
unsigned int pmd_order;
int *expected_orders;
-#define SPLIT_DEBUGFS "/sys/kernel/debug/split_huge_pages"
-#define SMAP_PATH "/proc/self/smaps"
#define INPUT_MAX 80
-#define PID_FMT "%d,0x%lx,0x%lx,%d"
-#define PID_FMT_OFFSET "%d,0x%lx,0x%lx,%d,%d"
-#define PATH_FMT "%s,0x%lx,0x%lx,%d"
-
const char *pagemap_proc = "/proc/self/pagemap";
const char *kpageflags_proc = "/proc/kpageflags";
int pagemap_fd;
@@ -70,25 +63,6 @@ static int check_after_split_folio_orders(char *vaddr_start, size_t len,
return status;
}
-static void write_debugfs(const char *fmt, ...)
-{
- char input[INPUT_MAX];
- int ret;
- va_list argp;
-
- va_start(argp, fmt);
- ret = vsnprintf(input, INPUT_MAX, fmt, argp);
- va_end(argp);
-
- if (ret >= INPUT_MAX)
- ksft_exit_fail_msg("%s: Debugfs input is too long\n", __func__);
-
- ret = write_file(SPLIT_DEBUGFS, input, ret + 1);
- if (ret)
- ksft_exit_fail_msg("write_file(%s): %s\n", SPLIT_DEBUGFS,
- strerror(-ret));
-}
-
static char *allocate_zero_filled_hugepage(size_t len)
{
char *result;
@@ -132,8 +106,7 @@ static void verify_rss_anon_split_huge_page_all_zeroes(char *one_page, int nr_hp
disable_khugepaged(one_page, len);
/* split all THPs */
- write_debugfs(PID_FMT, getpid(), (uint64_t)one_page,
- (uint64_t)one_page + len, 0);
+ split_huge_pages(one_page, len, 0);
for (i = 0; i < len; i++)
if (one_page[i] != (char)0)
@@ -181,8 +154,7 @@ static void split_pmd_thp_to_order(int order)
disable_khugepaged(one_page, len);
/* split all THPs */
- write_debugfs(PID_FMT, getpid(), (uint64_t)one_page,
- (uint64_t)one_page + len, order);
+ split_huge_pages(one_page, len, order);
for (i = 0; i < len; i++)
if (one_page[i] != (char)i)
@@ -269,8 +241,7 @@ static void split_pte_mapped_thp(void)
}
/* Split all THPs through the remapped pages. */
- write_debugfs(PID_FMT, getpid(), (uint64_t)page_area,
- (uint64_t)page_area + page_area_size, 0);
+ split_huge_pages(page_area, page_area_size, 0);
/* Corruption during mremap or split? */
for (i = 0; i < page_area_size; i++) {
@@ -306,7 +277,7 @@ static void split_file_backed_thp(int order)
char opts[64];
ssize_t num_written, num_read;
char *file_buf1 = NULL, *file_buf2 = NULL;
- uint64_t pgoff_start = 0, pgoff_end = 1024;
+ unsigned long pgoff_start = 0, pgoff_end = 1024;
int i;
ksft_print_msg("Please enable pr_debug in split_huge_pages_in_file() for more info.\n");
@@ -355,7 +326,7 @@ static void split_file_backed_thp(int order)
}
/* split the file-backed THP */
- write_debugfs(PATH_FMT, testfile, pgoff_start, pgoff_end, order);
+ split_huge_pages_file(testfile, pgoff_start, pgoff_end, order);
/* check file content after split */
status = lseek(fd, 0, SEEK_SET);
@@ -536,16 +507,14 @@ static void split_thp_in_pagecache_to_order_at(size_t fd_size,
*/
if (offset == -1) {
for (split_addr = addr; split_addr < addr + fd_size; split_addr += pmd_pagesize)
- write_debugfs(PID_FMT, getpid(), (uint64_t)split_addr,
- (uint64_t)split_addr + pagesize, order);
+ split_huge_pages(split_addr, pagesize, order);
expected_orders[order] = fd_size / (pagesize << order);
} else {
int times = fd_size / pmd_pagesize;
for (split_addr = addr; split_addr < addr + fd_size; split_addr += pmd_pagesize)
- write_debugfs(PID_FMT_OFFSET, getpid(), (uint64_t)split_addr,
- (uint64_t)split_addr + pagesize, order, offset);
+ split_huge_pages_with_offset(split_addr, pagesize, order, offset);
for (i = order + 1; i < pmd_order; i++)
expected_orders[i] = times;
diff --git a/tools/testing/selftests/mm/vm_util.c b/tools/testing/selftests/mm/vm_util.c
index 9942bbea7837..b64c651f753e 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -18,6 +18,12 @@
#define PAGEMAP_PATH "/proc/self/pagemap"
#define KPAGEFLAGS_PATH "/proc/kpageflags"
#define MAX_NR_ORDERS 20
+#define SPLIT_DEBUGFS "/sys/kernel/debug/split_huge_pages"
+#define INPUT_MAX 80
+
+#define PID_FMT "%d,0x%lx,0x%lx,%d"
+#define PID_FMT_OFFSET "%d,0x%lx,0x%lx,%d,%d"
+#define PATH_FMT "%s,0x%lx,0x%lx,%d"
unsigned int __page_size;
unsigned int __page_shift;
@@ -1105,6 +1111,56 @@ int unpoison_memory(unsigned long pfn)
return ret > 0 ? 0 : -errno;
}
+static void write_debugfs(const char *fmt, ...)
+{
+ char input[INPUT_MAX];
+ int ret;
+ va_list argp;
+
+ va_start(argp, fmt);
+ ret = vsnprintf(input, INPUT_MAX, fmt, argp);
+ va_end(argp);
+
+ if (ret >= INPUT_MAX)
+ ksft_exit_fail_msg("%s: Debugfs input is too long\n", __func__);
+
+ ret = write_file(SPLIT_DEBUGFS, input, ret + 1);
+ if (ret)
+ ksft_exit_fail_msg("write_file(%s): %s\n", SPLIT_DEBUGFS,
+ strerror(-ret));
+}
+
+bool split_huge_pages_supported(void)
+{
+ int fd;
+
+ fd = open(SPLIT_DEBUGFS, O_WRONLY);
+ if (fd < 0)
+ return false;
+
+ close(fd);
+ return true;
+}
+
+void split_huge_pages(void *addr, size_t len, int order)
+{
+ write_debugfs(PID_FMT, getpid(), (unsigned long)addr,
+ (unsigned long)addr + len, order);
+}
+
+void split_huge_pages_with_offset(void *addr, size_t len, int order,
+ int offset)
+{
+ write_debugfs(PID_FMT_OFFSET, getpid(), (unsigned long)addr,
+ (unsigned long)addr + len, order, offset);
+}
+
+void split_huge_pages_file(const char *path, unsigned long start,
+ unsigned long end, int order)
+{
+ write_debugfs(PATH_FMT, path, start, end, order);
+}
+
static unsigned long shmall, shmmax;
void __shm_limits_restore(void)
diff --git a/tools/testing/selftests/mm/vm_util.h b/tools/testing/selftests/mm/vm_util.h
index ef8fe76d4c61..efd3a2be945d 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -118,6 +118,12 @@ bool is_backed_by_folio(char *vaddr, int order, int pagemap_fd,
int kpageflags_fd);
bool is_range_backed_by_order(char *start, size_t len, int order,
int pagemap_fd, int kpageflags_fd);
+bool split_huge_pages_supported(void);
+void split_huge_pages(void *addr, size_t len, int order);
+void split_huge_pages_with_offset(void *addr, size_t len, int order,
+ int offset);
+void split_huge_pages_file(const char *path, unsigned long start,
+ unsigned long end, int order);
int uffd_register(int uffd, void *addr, uint64_t len,
bool miss, bool wp, bool minor);
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* [PATCH 4/4] selftests/mm: add a THP compaction selftest
2026-11-12 9:02 [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest Sarthak Sharma
` (2 preceding siblings ...)
2026-11-12 9:02 ` [PATCH 3/4] selftests/mm: factor out THP split helpers Sarthak Sharma
@ 2026-11-12 9:02 ` Sarthak Sharma
2026-11-13 9:03 ` sashiko-bot
3 siblings, 1 reply; 6+ messages in thread
From: Sarthak Sharma @ 2026-11-12 9:02 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm,
linux-kselftest, linux-kernel, Sarthak Sharma
Add a selftest to check whether THPs can be recovered through
memory compaction after fragmenting memory.
Read the PMD size during runtime. Use a fixed max test size
of 32 GiB and calculate the number of max THPs based on PMD size.
Save existing THP settings and restore them at exit. Configure THP
for MADV_HUGEPAGE mappings, enable only the PMD sized order and set
defrag to madvise so that synchronous compaction is performed for the
test mappings.
Allocate as many THPs as possible, until allocation falls back
to base pages or the max test size is reached. If the max test
size limit is reached, skip the test because retry allocations
could use free memory and not recover memory through compaction.
Record the starting PFN of every allocated THP. Split the allocated
THPs to order 0 using the split_huge_pages debugfs interface and
verify the split using pagemap and kpageflags. Keep the final base
page from each THP mapped and discard the rest.
Retry the same number of successful THP allocations in a new mapping,
while keeping the retained pages mapped. Count a THP as recovered
through compaction if it uses a PFN which was previously used in the
initial THP allocation. After retrying, check if every retained page
is still resident. If not, skip the test since the range could have
been freed through swapping and not through compaction. If more than
half of the initial THP allocations are recovered, pass the test.
Otherwise fail the test.
Skip the test if PMD sized THPs are unavailable, the test is not
running as root, split_huge_pages interface is unavailable,
the pagemap PFNs are masked or the mapping is too large for the
address space.
Add the test to Makefile, run_vmtests.sh and list it in MAINTAINERS.
Suggested-by: David Hildenbrand (Arm) <david@kernel.org>
Signed-off-by: Sarthak Sharma <sarthak.sharma@arm.com>
---
MAINTAINERS | 1 +
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/run_vmtests.sh | 2 +
tools/testing/selftests/mm/thp-compaction.c | 215 ++++++++++++++++++++
4 files changed, 219 insertions(+)
create mode 100644 tools/testing/selftests/mm/thp-compaction.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 4d74ad879ecf..eb0b46a361dc 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -17946,6 +17946,7 @@ F: mm/mm_slot.h
F: tools/mm/thpmaps
F: tools/testing/selftests/mm/khugepaged.c
F: tools/testing/selftests/mm/split_huge_page_test.c
+F: tools/testing/selftests/mm/thp-compaction.c
MEMORY MANAGEMENT - USERFAULTFD
M: Andrew Morton <akpm@linux-foundation.org>
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 06d3d0350a9a..ac39e2afd59f 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -90,6 +90,7 @@ TEST_GEN_FILES += pfnmap
TEST_GEN_FILES += process_madv
TEST_GEN_FILES += prctl_thp_disable
TEST_GEN_FILES += thuge-gen
+TEST_GEN_FILES += thp-compaction
TEST_GEN_FILES += uffd-stress
TEST_GEN_FILES += uffd-unit-tests
TEST_GEN_FILES += uffd-wp-mremap
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index cb5c7cccb585..a3fdc23c9cfe 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -448,6 +448,8 @@ if [ -n "${MOUNTED_XFS}" ]; then
rm -f ${XFS_IMG}
fi
+CATEGORY="thp" run_test ./thp-compaction
+
CATEGORY="thp" run_test ./folio_split_race_test
CATEGORY="migration" run_test ./migration
diff --git a/tools/testing/selftests/mm/thp-compaction.c b/tools/testing/selftests/mm/thp-compaction.c
new file mode 100644
index 000000000000..8935f1968a6d
--- /dev/null
+++ b/tools/testing/selftests/mm/thp-compaction.c
@@ -0,0 +1,215 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <sys/mman.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <mm/hugepage_settings.h>
+#include "vm_util.h"
+#include "kselftest.h"
+
+int mmap_flags = MAP_ANONYMOUS | MAP_NORESERVE | MAP_PRIVATE;
+#define PROT_RW (PROT_READ | PROT_WRITE)
+#define MAX_TEST_SIZE (32ULL << 30)
+#define MAX_THP_RETRIES 3
+
+size_t pmd_size;
+
+static int compare_pfns(const void *a, const void *b)
+{
+ uint64_t pfn_a = *(const uint64_t *)a;
+ uint64_t pfn_b = *(const uint64_t *)b;
+
+ return (pfn_a > pfn_b) - (pfn_a < pfn_b);
+}
+
+static void *mmap_hpage_aligned(size_t len)
+{
+ void *ptr;
+
+ ptr = mmap(NULL, len + pmd_size, PROT_RW, mmap_flags, -1, 0);
+ if (ptr == MAP_FAILED) {
+ if (errno == ENOMEM) {
+ ksft_test_result_skip("Cannot reserve enough virtual address space\n");
+ ksft_finished();
+ }
+ ksft_exit_fail_perror("mmap");
+ }
+ ptr += pmd_size - (uintptr_t)ptr % pmd_size;
+
+ return ptr;
+}
+
+int main(void)
+{
+ size_t len, max_thps, nr_initial = 0;
+ size_t nr_recovered = 0;
+ int pagemap_fd, kpageflags_fd, pmd_order;
+ void *initial, *retry, *p, *last_page;
+ struct thp_settings settings;
+ uint64_t *initial_pfns;
+ int64_t ret;
+
+ ksft_print_header();
+
+ if (!thp_available())
+ ksft_exit_skip("Transparent Hugepages not available\n");
+
+ if (geteuid())
+ ksft_exit_skip("Run this test as root\n");
+
+ pmd_size = pmd_psize();
+ if (!pmd_size)
+ ksft_exit_skip("Reading PMD pagesize failed\n");
+
+ if (SIZE_MAX - pmd_size < MAX_TEST_SIZE)
+ ksft_exit_skip("Required mapping is too large for this architecture\n");
+
+ pmd_order = sz2ord(pmd_size, psize());
+ if (!(thp_supported_orders() & (1UL << pmd_order)))
+ ksft_exit_skip("PMD sized THPs are not supported\n");
+
+ if (!split_huge_pages_supported())
+ ksft_exit_skip("split_huge_pages debugfs interface unavailable\n");
+
+ thp_save_settings();
+ thp_read_settings(&settings);
+
+ settings.thp_enabled = THP_MADVISE;
+ settings.thp_defrag = THP_DEFRAG_MADVISE;
+
+ for (int i = 0; i < NR_ORDERS; i++)
+ settings.hugepages[i].enabled = THP_NEVER;
+
+ settings.hugepages[pmd_order].enabled = THP_INHERIT;
+ thp_push_settings(&settings);
+
+ pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+ if (pagemap_fd < 0)
+ ksft_exit_skip("Unable to open pagemap\n");
+
+ kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+ if (kpageflags_fd < 0) {
+ close(pagemap_fd);
+ ksft_exit_skip("Unable to open kpageflags\n");
+ }
+
+ ksft_set_plan(1);
+
+ len = (size_t)MAX_TEST_SIZE;
+ max_thps = len / pmd_size;
+ initial = mmap_hpage_aligned(len);
+ initial_pfns = calloc(max_thps, sizeof(*initial_pfns));
+ if (!initial_pfns)
+ ksft_exit_fail_msg("Couldn't allocate initial PFNs array\n");
+
+ /*
+ * Try to allocate as many THPs as possible and keep them
+ * allocated
+ */
+ for (nr_initial = 0; nr_initial < max_thps; nr_initial++) {
+ p = initial + nr_initial * pmd_size;
+
+ for (int i = 0; i < MAX_THP_RETRIES; i++) {
+ ret = allocate_transhuge(p, pagemap_fd);
+ if (ret == -EACCES) {
+ ksft_test_result_skip("Pagemap PFNs are unavailable\n");
+ ksft_finished();
+ }
+ if (ret >= 0)
+ break;
+ }
+ if (ret < 0) {
+ if (madvise(p, pmd_size, MADV_DONTNEED))
+ ksft_exit_fail_perror("MADV_DONTNEED");
+ break;
+ }
+ }
+
+ if (!nr_initial) {
+ ksft_test_result_skip("Failed to allocate any THPs\n");
+ ksft_finished();
+ }
+
+ /*
+ * If no fallback is observed, retry allocations could use untouched
+ * free memory instead of recovering memory through compaction.
+ */
+ if (nr_initial == max_thps) {
+ ksft_test_result_skip("Reached max allocation without failure\n");
+ ksft_finished();
+ }
+
+ /*
+ * Split all allocated THPs to order 0, keeping only the final base page
+ * from each THP
+ */
+ for (size_t i = 0; i < nr_initial; i++) {
+ p = initial + i * pmd_size;
+ last_page = p + pmd_size - psize();
+
+ if (!is_backed_by_folio(p, pmd_order, pagemap_fd, kpageflags_fd)) {
+ ksft_test_result_skip(
+ "Initial range is not backed by a resident PMD sized folio\n");
+ ksft_finished();
+ }
+
+ initial_pfns[i] = pagemap_get_pfn(pagemap_fd, p);
+
+ split_huge_pages(p, psize(), 0);
+ if (!is_backed_by_folio(last_page, 0, pagemap_fd, kpageflags_fd)) {
+ ksft_test_result_fail("Failed to split THP to order 0\n");
+ ksft_finished();
+ }
+ if (madvise(p, pmd_size - psize(), MADV_DONTNEED))
+ ksft_exit_fail_perror("MADV_DONTNEED");
+ }
+
+ qsort(initial_pfns, nr_initial, sizeof(*initial_pfns), compare_pfns);
+
+ /*
+ * Retry to allocate THPs in a separate mapping
+ */
+ retry = mmap_hpage_aligned(nr_initial * pmd_size);
+ for (size_t i = 0; i < nr_initial; i++) {
+ p = retry + i * pmd_size;
+
+ ret = allocate_transhuge(p, pagemap_fd);
+ if (ret == -EACCES) {
+ ksft_test_result_skip("Pagemap PFNs are unavailable\n");
+ ksft_finished();
+ }
+ if (ret >= 0) {
+ uint64_t pfn = ret;
+
+ if (bsearch(&pfn, initial_pfns, nr_initial,
+ sizeof(*initial_pfns), compare_pfns))
+ nr_recovered++;
+ } else if (madvise(p, pmd_size, MADV_DONTNEED)) {
+ ksft_exit_fail_perror("MADV_DONTNEED");
+ }
+ }
+
+ /* Check if the last page left from each initial THP is resident */
+ for (size_t i = 0; i < nr_initial; i++) {
+ uint64_t entry;
+
+ last_page = initial + (i + 1) * pmd_size - psize();
+ entry = pagemap_get_entry(pagemap_fd, last_page);
+
+ if (!PAGEMAP_PRESENT(entry)) {
+ ksft_test_result_skip("Retained page is not resident\n");
+ ksft_finished();
+ }
+ }
+
+ close(pagemap_fd);
+ close(kpageflags_fd);
+ free(initial_pfns);
+ ksft_test_result(nr_recovered > nr_initial / 2,
+ "Recovered %zu of %zu THP ranges after splitting\n",
+ nr_recovered, nr_initial);
+ ksft_finished();
+}
--
2.53.0
^ permalink raw reply related [flat|nested] 6+ messages in thread
* Re: [PATCH 4/4] selftests/mm: add a THP compaction selftest
2026-11-12 9:02 ` [PATCH 4/4] selftests/mm: add a THP compaction selftest Sarthak Sharma
@ 2026-11-13 9:03 ` sashiko-bot
0 siblings, 0 replies; 6+ messages in thread
From: sashiko-bot @ 2026-11-13 9:03 UTC (permalink / raw)
To: Sarthak Sharma, Andrew Morton, David Hildenbrand
Cc: Lorenzo Stoakes, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, Zi Yan, Baolin Wang,
Nico Pache, Ryan Roberts, Dev Jain, Anshuman Khandual, Barry Song,
Lance Yang, Usama Arif, Kiryl Shutsemau, linux-mm
Thank you for your contribution! Sashiko AI review found 2 potential
issues to consider:
- [Medium] selftests/mm: unwritten last_page freed via zero page on
split in main()
- [Low] selftests/mm: missing MADV_NOHUGEPAGE before split_huge_pages()
in main()
--
commit 8fc76b939a1f641b9678f19aaded037c1f80ae5b
Author: Sarthak Sharma <sarthak.sharma@arm.com>
selftests/mm: add a THP compaction selftest
This commit adds a selftest that allocates PMD-sized THPs, splits them to
order 0 while retaining one base page per range, and verifies that a
subsequent mapping can recover THPs via memory compaction.
> diff --git a/tools/testing/selftests/mm/thp-compaction.c b/tools/testing/selftests/mm/thp-compaction.c
> new file mode 100644
> index 0000000000000..8935f1968a6da
> --- /dev/null
> +++ b/tools/testing/selftests/mm/thp-compaction.c
> @@ -0,0 +1,215 @@
[ ... ]
> +int main(void)
> +{
[ ... ]
> + /*
> + * Split all allocated THPs to order 0, keeping only the final base page
> + * from each THP
> + */
> + for (size_t i = 0; i < nr_initial; i++) {
> + p = initial + i * pmd_size;
> + last_page = p + pmd_size - psize();
> +
> + if (!is_backed_by_folio(p, pmd_order, pagemap_fd, kpageflags_fd)) {
> + ksft_test_result_skip(
> + "Initial range is not backed by a resident PMD sized folio\n");
> + ksft_finished();
> + }
> +
> + initial_pfns[i] = pagemap_get_pfn(pagemap_fd, p);
> +
> + split_huge_pages(p, psize(), 0);
> + if (!is_backed_by_folio(last_page, 0, pagemap_fd, kpageflags_fd)) {
> + ksft_test_result_fail("Failed to split THP to order 0\n");
> + ksft_finished();
> + }
> + if (madvise(p, pmd_size - psize(), MADV_DONTNEED))
> + ksft_exit_fail_perror("MADV_DONTNEED");
> + }
[Severity: Medium]
Does split_huge_pages() replace last_page with the shared zero page and
free its physical page?
allocate_transhuge() only writes to the first word at p, leaving last_page
zero-filled. When splitting an anonymous THP to order 0,
__folio_freeze_split_anon() passes TTU_USE_SHARED_ZEROPAGE to
remap_anon_folio(), which remaps zero-filled tail pages to the shared zero
page and frees their physical folios. madvise(MADV_DONTNEED) then frees
subpage 0, leaving the entire PMD range free in the buddy allocator rather
than fragmented. Because the shared zero page is present in pagemap and
has no compound flags in kpageflags, both is_backed_by_folio() and
PAGEMAP_PRESENT() succeed, so the retry phase allocates from free buddy
pages without exercising compaction.
Writing non-zero data to last_page before split_huge_pages() will keep the
physical tail page mapped.
[Severity: Low]
Could khugepaged collapse ranges in initial back into THPs during the split
and retry loops?
allocate_transhuge() calls madvise(MADV_HUGEPAGE) on each range in initial,
registering the VMA with khugepaged. Without clearing VM_HUGEPAGE before
splitting, khugepaged can collapse p before is_backed_by_folio() checks the
order-0 split, or collapse the single-page-retained ranges (which have
HPAGE_PMD_NR - 1 pte_none entries, matching the default max_ptes_none) and
consume compacted THPs meant for the retry mapping. Calling
madvise(MADV_NOHUGEPAGE) on initial before the split loop would avoid this.
--
Sashiko AI review · https://sashiko.dev/#/patchset/20261112090250.334344-1-sarthak.sharma@arm.com?part=4
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-11-13 9:03 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-12 9:02 [PATCH 0/4] selftests/mm: replace transhuge-stress with a THP compaction selftest Sarthak Sharma
2026-11-12 9:02 ` [PATCH 1/4] selftests/mm: remove transhuge-stress Sarthak Sharma
2026-11-12 9:02 ` [PATCH 2/4] selftests/mm: detect masked PFNs in allocate_transhuge() Sarthak Sharma
2026-11-12 9:02 ` [PATCH 3/4] selftests/mm: factor out THP split helpers Sarthak Sharma
2026-11-12 9:02 ` [PATCH 4/4] selftests/mm: add a THP compaction selftest Sarthak Sharma
2026-11-13 9:03 ` sashiko-bot
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.