[RFC PATCH v3 3/3] selftests/damon: add functional test for DAMOS_SPLIT

Lian Wang <[email protected]>
Newsgroups dev.linux.lists.damon,org.kernel.vger.linux-doc,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest,org.kvack.linux-mm
Message-ID <[email protected]>
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()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.