From: "Kiryl Shutsemau (Meta)" <[email protected]>

The khugepaged tests attribute per-attempt outcomes through the
huge_memory tracepoints. The anon-path events carry no virtual address,
but an attempt is identifiable anyway: mm_collapse_huge_page_isolate()
reports a source folio PFN and order, which the test matches against the
PFNs it recorded from pagemap before the pass. Count an attempt from
whichever attribution signal fires, so the check does not depend on which
of the anon tracepoints a given kernel emits.

Add minimal tracefs helpers to vm_util (enable/clear one event subsystem,
open the trace buffer) and a khugepaged_sync_check test: per step, prepare
one aligned window, record its source PFNs, run one khugepaged_full_pass()
barrier and require the window collapsed with exactly one attributed
attempt. Five steps; scan_sleep_millisecs is set high so the test only
completes inside its timeout if the sysfs store really wakes the daemon.

Passes 5/5 on x86-64 4K and arm64 64K.

Assisted-by: Claude-Code:claude-opus-5
Signed-off-by: Kiryl Shutsemau (Meta) <[email protected]>
---
 tools/testing/selftests/mm/.gitignore         |   1 +
 tools/testing/selftests/mm/Makefile           |   1 +
 .../selftests/mm/khugepaged_sync_check.c      | 198 ++++++++++++++++++
 tools/testing/selftests/mm/run_vmtests.sh     |   2 +
 tools/testing/selftests/mm/vm_util.c          |  49 +++++
 tools/testing/selftests/mm/vm_util.h          |   3 +
 6 files changed, 254 insertions(+)
 create mode 100644 tools/testing/selftests/mm/khugepaged_sync_check.c

diff --git a/tools/testing/selftests/mm/.gitignore 
b/tools/testing/selftests/mm/.gitignore
index b3b26447cd23..616043a0fcd2 100644
--- a/tools/testing/selftests/mm/.gitignore
+++ b/tools/testing/selftests/mm/.gitignore
@@ -67,4 +67,5 @@ prctl_thp_disable
 rmap
 folio_split_race_test
 folio_order_check
+khugepaged_sync_check
 khugepaged_race
diff --git a/tools/testing/selftests/mm/Makefile 
b/tools/testing/selftests/mm/Makefile
index 046bae8d1eff..026d4b61414d 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -105,6 +105,7 @@ TEST_GEN_FILES += merge
 TEST_GEN_FILES += rmap
 TEST_GEN_FILES += folio_split_race_test
 TEST_GEN_FILES += folio_order_check
+TEST_GEN_FILES += khugepaged_sync_check
 TEST_GEN_FILES += khugepaged_race
 
 ifneq ($(ARCH),arm64)
diff --git a/tools/testing/selftests/mm/khugepaged_sync_check.c 
b/tools/testing/selftests/mm/khugepaged_sync_check.c
new file mode 100644
index 000000000000..13215cb370ce
--- /dev/null
+++ b/tools/testing/selftests/mm/khugepaged_sync_check.c
@@ -0,0 +1,198 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Synchronous khugepaged driving check.
+ *
+ * Race tests drive khugepaged through the existing sysfs controls: a
+ * store to scan_sleep_millisecs wakes the daemon, and full_scans
+ * advancing by two is a completion barrier for one full pass that
+ * started after setup (khugepaged_full_pass()). Verify the pair gives
+ * deterministic, attributable results: one barrier step over one
+ * prepared window produces exactly one collapse attempt on that
+ * window's source pages — mm_collapse_huge_page_isolate events
+ * filtered by source PFN and order — and the window is collapsed
+ * afterwards, repeatably.
+ *
+ * scan_sleep_millisecs is set to 60s to prove the wake path: without
+ * the wake, one barrier step would sleep multiples of that and blow
+ * the timeout. It also keeps the daemon from free-running between
+ * steps, per the khugepaged_full_pass() discipline.
+ */
+#define _GNU_SOURCE
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "vm_util.h"
+#include "hugepage_settings.h"
+
+#define BASE_ADDR ((void *)(1UL << 30))
+#define TARGET_ORDER 2 /* smallest order khugepaged considers */
+#define NR_ITERATIONS 5
+
+static int pagemap_fd;
+static int kpageflags_fd;
+static unsigned long hpage_pmd_size;
+
+/*
+ * Count collapse attempts attributable to our window: legacy-engine
+ * isolate events whose scan_pfn is one of the window's source PFNs,
+ * plus batch-engine per-candidate install events at the window's
+ * address. Either engine reports exactly once per attempt.
+ */
+static int count_attributed(unsigned long *pfns, int nr_pfns,
+                           unsigned long addr, unsigned int order)
+{
+       char line[1024];
+       int count = 0;
+       FILE *fp;
+
+       fp = tracing_open_trace();
+       if (!fp)
+               ksft_exit_fail_msg("Cannot open trace buffer\n");
+
+       while (fgets(line, sizeof(line), fp)) {
+               char *s;
+               unsigned long val;
+               unsigned int ord;
+               char *o;
+               int i;
+
+               s = strstr(line, "mm_collapse_huge_page_isolate:");
+               if (s) {
+                       if (sscanf(s, "mm_collapse_huge_page_isolate: 
scan_pfn=0x%lx",
+                                  &val) != 1)
+                               continue;
+                       o = strstr(s, "order=");
+                       if (!o || sscanf(o, "order=%u", &ord) != 1 ||
+                           ord != order)
+                               continue;
+                       for (i = 0; i < nr_pfns; i++) {
+                               if (val == pfns[i]) {
+                                       count++;
+                                       break;
+                               }
+                       }
+                       continue;
+               }
+
+               s = strstr(line, "mm_collapse_candidate:");
+               if (s) {
+                       if (!strstr(s, "pass=install") ||
+                           !strstr(s, "result=succeeded"))
+                               continue;
+                       o = strstr(s, "addr=");
+                       if (!o || sscanf(o, "addr=0x%lx", &val) != 1 ||
+                           val != addr)
+                               continue;
+                       o = strstr(s, "order=");
+                       if (!o || sscanf(o, "order=%u", &ord) != 1 ||
+                           ord != order)
+                               continue;
+                       count++;
+               }
+       }
+       fclose(fp);
+       return count;
+}
+
+static void one_step(int iteration)
+{
+       const size_t window = getpagesize() << TARGET_ORDER;
+       const int nr_pages = 1 << TARGET_ORDER;
+       unsigned long pfns[1 << TARGET_ORDER];
+       bool collapsed;
+       int attributed;
+       char *p;
+       int i;
+
+       p = mmap(BASE_ADDR, hpage_pmd_size, PROT_READ | PROT_WRITE,
+                MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
+       if (p != BASE_ADDR)
+               ksft_exit_fail_msg("Failed to allocate VMA at %p\n",
+                                  BASE_ADDR);
+
+       /* Prepare one window; record its source PFNs. */
+       for (i = 0; i < nr_pages; i++) {
+               p[i * getpagesize()] = i + 1;
+               pfns[i] = pagemap_get_pfn(pagemap_fd, p + i * getpagesize());
+               if (pfns[i] == -1UL)
+                       ksft_exit_fail_msg("Source page not present\n");
+       }
+
+       if (tracing_events_start("huge_memory"))
+               ksft_exit_fail_msg("Cannot enable huge_memory events\n");
+
+       madvise(p, hpage_pmd_size, MADV_HUGEPAGE);
+       /* Wait up to 120 seconds for the pass to complete. */
+       if (!khugepaged_full_pass(120))
+               ksft_exit_fail_msg("khugepaged did not complete a full pass\n");
+
+       tracing_events_stop("huge_memory");
+
+       collapsed = is_range_backed_by_folio_orders(p, window, TARGET_ORDER,
+                                                   pagemap_fd, kpageflags_fd);
+       attributed = count_attributed(pfns, nr_pages, (unsigned long)p,
+                                     TARGET_ORDER);
+
+       ksft_test_result(collapsed && attributed == 1,
+                        "step %d: window collapsed, %d attributed result(s)\n",
+                        iteration, attributed);
+
+       munmap(p, hpage_pmd_size);
+}
+
+int main(void)
+{
+       struct thp_settings settings;
+       int i;
+
+       ksft_print_header();
+
+       if (!thp_available())
+               ksft_exit_skip("Transparent Hugepages not available\n");
+       if (!(thp_supported_orders() & (1UL << TARGET_ORDER)))
+               ksft_exit_skip("Order %d is not a supported anon THP order\n",
+                              TARGET_ORDER);
+
+       hpage_pmd_size = read_pmd_pagesize();
+       if (!hpage_pmd_size)
+               ksft_exit_fail_msg("Reading PMD pagesize failed\n");
+       pagemap_fd = open("/proc/self/pagemap", O_RDONLY);
+       if (pagemap_fd < 0)
+               ksft_exit_fail_perror("open(\"/proc/self/pagemap\")");
+       kpageflags_fd = open("/proc/kpageflags", O_RDONLY);
+       if (kpageflags_fd < 0)
+               ksft_exit_skip("open(\"/proc/kpageflags\") requires root\n");
+       if (tracing_events_start("huge_memory"))
+               ksft_exit_skip("tracefs unavailable\n");
+       tracing_events_stop("huge_memory");
+
+       ksft_set_plan(NR_ITERATIONS);
+
+       thp_save_settings();
+       thp_read_settings(&settings);
+       settings.thp_enabled = THP_MADVISE;
+       settings.thp_defrag = THP_DEFRAG_ALWAYS;
+       settings.khugepaged.defrag = 1;
+       settings.khugepaged.scan_sleep_millisecs = 60000;
+       settings.khugepaged.alloc_sleep_millisecs = 60000;
+       settings.khugepaged.max_ptes_none = (hpage_pmd_size / getpagesize()) - 
1;
+       /* One wake must complete one full pass; see khugepaged_full_pass(). */
+       settings.khugepaged.pages_to_scan = 1UL << 24;
+       for (i = 0; i < NR_ORDERS; i++)
+               settings.hugepages[i].enabled = THP_NEVER;
+       settings.hugepages[TARGET_ORDER].enabled = THP_INHERIT;
+       /* Base of the settings stack; the bottom entry is never popped. */
+       thp_push_settings(&settings);
+
+       for (i = 0; i < NR_ITERATIONS; i++)
+               one_step(i);
+
+       thp_restore_settings();
+
+       ksft_finished();
+}
diff --git a/tools/testing/selftests/mm/run_vmtests.sh 
b/tools/testing/selftests/mm/run_vmtests.sh
index a8b6b839cb97..f61ec76d8e00 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -405,6 +405,8 @@ CATEGORY="cow" run_test ./cow
 CATEGORY="thp" run_test ./folio_order_check
 
 
+CATEGORY="thp" run_test ./khugepaged_sync_check
+
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m stepped
 
 CATEGORY="thp" run_test ./khugepaged_race -d 5 -m free
diff --git a/tools/testing/selftests/mm/vm_util.c 
b/tools/testing/selftests/mm/vm_util.c
index 793342095420..886fb3de9c01 100644
--- a/tools/testing/selftests/mm/vm_util.c
+++ b/tools/testing/selftests/mm/vm_util.c
@@ -411,6 +411,55 @@ bool is_range_backed_by_folio_orders(char *start, size_t 
len, int order,
        return true;
 }
 
+#define TRACEFS_ROOT "/sys/kernel/tracing"
+
+static int tracing_events_write(const char *subsys, const char *val)
+{
+       char path[256];
+       int fd;
+
+       snprintf(path, sizeof(path), TRACEFS_ROOT "/events/%s/enable",
+                subsys);
+       fd = open(path, O_WRONLY);
+       if (fd < 0)
+               return -1;
+       if (write(fd, val, 1) != 1) {
+               close(fd);
+               return -1;
+       }
+       close(fd);
+       return 0;
+}
+
+/*
+ * Enable one ftrace event subsystem (e.g. "huge_memory") and clear the
+ * trace buffer. Returns -1 if tracefs is unavailable; parse the results
+ * via tracing_open_trace() after tracing_events_stop().
+ */
+int tracing_events_start(const char *subsys)
+{
+       int fd;
+
+       if (tracing_events_write(subsys, "1"))
+               return -1;
+
+       fd = open(TRACEFS_ROOT "/trace", O_WRONLY | O_TRUNC);
+       if (fd < 0)
+               return -1;
+       close(fd);
+       return 0;
+}
+
+int tracing_events_stop(const char *subsys)
+{
+       return tracing_events_write(subsys, "0");
+}
+
+FILE *tracing_open_trace(void)
+{
+       return fopen(TRACEFS_ROOT "/trace", "r");
+}
+
 /* If `ioctls' non-NULL, the allowed ioctls will be returned into the var */
 int uffd_register_with_ioctls(int uffd, void *addr, uint64_t len,
                              bool miss, bool wp, bool minor, uint64_t *ioctls)
diff --git a/tools/testing/selftests/mm/vm_util.h 
b/tools/testing/selftests/mm/vm_util.h
index 76e9938a908e..4ff5a1c5ab8e 100644
--- a/tools/testing/selftests/mm/vm_util.h
+++ b/tools/testing/selftests/mm/vm_util.h
@@ -115,6 +115,9 @@ int close_procmap(struct procmap_fd *procmap);
 int write_sysfs(const char *file_path, unsigned long val);
 int read_sysfs(const char *file_path, unsigned long *val);
 bool softdirty_supported(void);
+int tracing_events_start(const char *subsys);
+int tracing_events_stop(const char *subsys);
+FILE *tracing_open_trace(void);
 
 static inline int open_self_procmap(struct procmap_fd *procmap_out)
 {
-- 
2.54.0


Reply via email to