Add damos_split.py, which allocates a MADV_HUGEPAGE-backed region in a
child process and runs a DAMON/DAMOS scheme with action 'split' and
target_order 0 against it, then verifies the huge pages are split into
base pages (the child's AnonHugePages drops).

Extend the _damon_sysfs.py Damos helper with the target_order parameter
so a scheme's split target can be expressed.



Co-developed-by: Kunwu Chan <[email protected]>
Signed-off-by: Kunwu Chan <[email protected]>
Signed-off-by: Lian Wang (Processmission) <[email protected]>
---
 tools/testing/selftests/damon/Makefile        |   1 +
 tools/testing/selftests/damon/_damon_sysfs.py |   9 +-
 tools/testing/selftests/damon/damos_split.py  | 125 ++++++++++++++++++
 3 files changed, 134 insertions(+), 1 deletion(-)
 create mode 100644 tools/testing/selftests/damon/damos_split.py

diff --git a/tools/testing/selftests/damon/Makefile 
b/tools/testing/selftests/damon/Makefile
index ece244e5c5b9..a623c271355f 100644
--- a/tools/testing/selftests/damon/Makefile
+++ b/tools/testing/selftests/damon/Makefile
@@ -12,6 +12,7 @@ TEST_PROGS += sysfs.sh
 TEST_PROGS += sysfs.py
 TEST_PROGS += sysfs_update_schemes_tried_regions_wss_estimation.py
 TEST_PROGS += damos_quota.py damos_quota_goal.py damos_apply_interval.py
+TEST_PROGS += damos_split.py
 TEST_PROGS += damos_tried_regions.py damon_nr_regions.py
 TEST_PROGS += sysfs_refresh.py
 TEST_PROGS += reclaim.sh lru_sort.sh
diff --git a/tools/testing/selftests/damon/_damon_sysfs.py 
b/tools/testing/selftests/damon/_damon_sysfs.py
index e6a2265d721e..5a01f31ad9f2 100644
--- a/tools/testing/selftests/damon/_damon_sysfs.py
+++ b/tools/testing/selftests/damon/_damon_sysfs.py
@@ -419,6 +419,7 @@ class Damos:
     filters = None
     apply_interval_us = None
     target_nid = None
+    target_order = None
     dests = None
     idx = None
     context = None
@@ -429,7 +430,7 @@ class Damos:
     def __init__(self, action='stat', access_pattern=DamosAccessPattern(),
                  quota=DamosQuota(), watermarks=DamosWatermarks(),
                  core_filters=[], ops_filters=[], filters=[], target_nid=0,
-                 dests=DamosDests(), apply_interval_us=0):
+                 target_order=0, dests=DamosDests(), apply_interval_us=0):
         self.action = action
         self.access_pattern = access_pattern
         self.access_pattern.scheme = self
@@ -448,6 +449,7 @@ class Damos:
         self.filters.scheme = self
 
         self.target_nid = target_nid
+        self.target_order = target_order
         self.dests = dests
         self.dests.scheme = self
 
@@ -492,6 +494,11 @@ class Damos:
         if err is not None:
             return err
 
+        err = write_file(os.path.join(self.sysfs_dir(), 'target_order'), '%d' %
+                         self.target_order)
+        if err is not None:
+            return err
+
         err = self.dests.stage()
         if err is not None:
             return err
diff --git a/tools/testing/selftests/damon/damos_split.py 
b/tools/testing/selftests/damon/damos_split.py
new file mode 100644
index 000000000000..089fbe0f0d4d
--- /dev/null
+++ b/tools/testing/selftests/damon/damos_split.py
@@ -0,0 +1,125 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+#
+# Functional test for the DAMOS_SPLIT action.
+#
+# A child process allocates a MADV_HUGEPAGE-backed anonymous region and
+# faults it in so that it is backed by (m)THPs.  The parent then runs a
+# DAMON/DAMOS scheme with action 'split' and target_order 0 against the
+# child and checks that the huge pages are split into base pages, i.e. the
+# child's AnonHugePages (as reported by /proc/<pid>/smaps) drops.
+
+import ctypes
+import os
+import signal
+import sys
+import time
+
+import _damon_sysfs
+
+PMD_SIZE = 2 * 1024 * 1024
+MADV_HUGEPAGE = 14
+PROT_READ_WRITE = 0x1 | 0x2
+MAP_PRIVATE_ANON = 0x2 | 0x20
+REGION_SIZE = 32 * PMD_SIZE
+
+def child_workload():
+    '''Allocate a PMD-aligned, THP-backed region, fault it in, then idle.'''
+    libc = ctypes.CDLL('libc.so.6', use_errno=True)
+    libc.mmap.restype = ctypes.c_void_p
+    libc.mmap.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int,
+                          ctypes.c_int, ctypes.c_int, ctypes.c_long]
+    libc.madvise.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_int]
+
+    # Over-allocate so that a PMD-aligned window is available.
+    raw = libc.mmap(None, REGION_SIZE + PMD_SIZE, PROT_READ_WRITE,
+                    MAP_PRIVATE_ANON, -1, 0)
+    if raw is None or raw == ctypes.c_void_p(-1).value:
+        os._exit(2)
+    base = (raw + PMD_SIZE - 1) & ~(PMD_SIZE - 1)
+    libc.madvise(ctypes.c_void_p(base), REGION_SIZE, MADV_HUGEPAGE)
+
+    buf = (ctypes.c_char * REGION_SIZE).from_address(base)
+    for off in range(0, REGION_SIZE, 4096):
+        buf[off] = 1
+
+    # Ready; idle until the parent tears us down.
+    signal.pause()
+
+def anon_huge_kb(pid):
+    total = 0
+    try:
+        with open('/proc/%d/smaps' % pid) as f:
+            for line in f:
+                if line.startswith('AnonHugePages:'):
+                    total += int(line.split()[1])
+    except FileNotFoundError:
+        return -1
+    return total
+
+def main():
+    if not os.path.exists('/sys/kernel/mm/transparent_hugepage/enabled'):
+        print('SKIP: transparent hugepage is not available')
+        exit(0)
+
+    pid = os.fork()
+    if pid == 0:
+        child_workload()
+        os._exit(0)
+
+    try:
+        # Give the child time to fault in its huge pages.
+        time.sleep(2)
+        before = anon_huge_kb(pid)
+        if before <= 0:
+            print('SKIP: workload did not get any THP (AnonHugePages=%d)'
+                  % before)
+            os.kill(pid, signal.SIGKILL)
+            exit(0)
+
+        # Split every large folio in the target down to order-0 base pages.
+        kdamonds = _damon_sysfs.Kdamonds([_damon_sysfs.Kdamond(
+            contexts=[_damon_sysfs.DamonCtx(
+                ops='vaddr',
+                targets=[_damon_sysfs.DamonTarget(pid=pid)],
+                schemes=[_damon_sysfs.Damos(
+                    action='split',
+                    target_order=0,
+                    # match every region regardless of access/age/size, so
+                    # the ARM64 stale-TLB blind spot cannot mask the target
+                    access_pattern=_damon_sysfs.DamosAccessPattern(
+                        size=[0, 2**64 - 1],
+                        nr_accesses=[0, 2**64 - 1],
+                        age=[0, 2**64 - 1]),
+                    apply_interval_us=0)])])])
+        err = kdamonds.start()
+        if err is not None:
+            print('kdamonds start failed: %s' % err)
+            os.kill(pid, signal.SIGKILL)
+            exit(1)
+
+        # Let the scheme find and split the regions.
+        after = before
+        for _ in range(50):
+            time.sleep(0.2)
+            after = anon_huge_kb(pid)
+            if after == 0:
+                break
+
+        kdamonds.stop()
+        os.kill(pid, signal.SIGKILL)
+
+        if after >= before:
+            print('FAIL: AnonHugePages did not shrink: before=%d KiB '
+                  'after=%d KiB' % (before, after))
+            exit(1)
+        print('PASS: AnonHugePages %d KiB -> %d KiB after DAMOS_SPLIT'
+              % (before, after))
+    finally:
+        try:
+            os.kill(pid, signal.SIGKILL)
+        except ProcessLookupError:
+            pass
+
+if __name__ == '__main__':
+    main()

Reply via email to