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()

