* [PATCH] selftests/mm: add THP sysfs interface test
@ 2026-03-09 12:00 Breno Leitao
2026-03-16 12:55 ` Lorenzo Stoakes (Oracle)
0 siblings, 1 reply; 8+ messages in thread
From: Breno Leitao @ 2026-03-09 12:00 UTC (permalink / raw)
To: Andrew Morton, David Hildenbrand, Lorenzo Stoakes,
Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan
Cc: linux-kernel, linux-mm, linux-kselftest, kernel-team,
Breno Leitao
Add a shell-based selftest that exercises the full set of THP sysfs
knobs: enabled (global and per-size anon), defrag, use_zero_page,
hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
khugepaged/ tunables, and per-size stats files.
Each writable knob is tested for valid writes, invalid-input rejection,
idempotent writes, and mode transitions where applicable. All original
values are saved before testing and restored afterwards.
The test uses the kselftest KTAP framework (ktap_helpers.sh) for
structured TAP 13 output, making results parseable by the kselftest
harness. The test plan is printed at the end since the number of test
points is dynamic (depends on available hugepage sizes and sysfs files).
This is particularly useful for validating the refactoring of
enabled_store() and anon_enabled_store() to use sysfs_match_string()
and the new change_enabled()/change_anon_orders() helpers.
Signed-off-by: Breno Leitao <leitao@debian.org>
---
While working on the THP sysfs interface, I noticed there were no
existing tests covering this functionality. This patch adds test
coverage that may be useful for upstream !?
---
tools/testing/selftests/mm/Makefile | 1 +
tools/testing/selftests/mm/run_vmtests.sh | 2 +
tools/testing/selftests/mm/thp_sysfs_test.sh | 666 +++++++++++++++++++++++++++
3 files changed, 669 insertions(+)
diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
index 7a5de4e9bf520..90fcca53a561b 100644
--- a/tools/testing/selftests/mm/Makefile
+++ b/tools/testing/selftests/mm/Makefile
@@ -180,6 +180,7 @@ TEST_FILES += charge_reserved_hugetlb.sh
TEST_FILES += hugetlb_reparenting_test.sh
TEST_FILES += test_page_frag.sh
TEST_FILES += run_vmtests.sh
+TEST_FILES += thp_sysfs_test.sh
# required by charge_reserved_hugetlb.sh
TEST_FILES += write_hugetlb_memory.sh
diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
index afdcfd0d7cef7..9e0e2089214a3 100755
--- a/tools/testing/selftests/mm/run_vmtests.sh
+++ b/tools/testing/selftests/mm/run_vmtests.sh
@@ -491,6 +491,8 @@ CATEGORY="thp" run_test ./khugepaged -s 4 all:shmem
CATEGORY="thp" run_test ./transhuge-stress -d 20
+CATEGORY="thp" run_test ./thp_sysfs_test.sh
+
# Try to create XFS if not provided
if [ -z "${SPLIT_HUGE_PAGE_TEST_XFS_PATH}" ]; then
if [ "${HAVE_HUGEPAGES}" = "1" ]; then
diff --git a/tools/testing/selftests/mm/thp_sysfs_test.sh b/tools/testing/selftests/mm/thp_sysfs_test.sh
new file mode 100755
index 0000000000000..407f306a0989f
--- /dev/null
+++ b/tools/testing/selftests/mm/thp_sysfs_test.sh
@@ -0,0 +1,666 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Test THP sysfs interface.
+#
+# Exercises the full set of THP sysfs knobs: enabled (global and
+# per-size anon), defrag, use_zero_page, hpage_pmd_size, shmem_enabled
+# (global and per-size), shrink_underused, khugepaged/ tunables, and
+# per-size stats files. Each writable knob is tested for valid writes,
+# invalid-input rejection, idempotent writes, and mode transitions
+# where applicable. All original values are saved before testing and
+# restored afterwards.
+#
+# Author: Breno Leitao <leitao@debian.org>
+
+DIR="$(dirname "$(readlink -f "$0")")"
+. "${DIR}"/../kselftest/ktap_helpers.sh
+
+THP_SYSFS=/sys/kernel/mm/transparent_hugepage
+
+# Read the currently active mode from a sysfs enabled file.
+# The active mode is enclosed in brackets, e.g. "always [madvise] never"
+get_active_mode() {
+ local path="$1"
+ local content
+
+ content=$(cat "$path")
+ echo "$content" | grep -o '\[.*\]' | tr -d '[]'
+}
+
+# Test that writing a mode and reading it back gives the expected result.
+test_mode() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+ local active
+
+ if ! echo "$mode" > "$path" 2>/dev/null; then
+ ktap_test_fail "$label: write '$mode'"
+ return
+ fi
+
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$mode" ]; then
+ ktap_test_pass "$label: write '$mode'"
+ else
+ ktap_test_fail "$label: write '$mode', read back '$active'"
+ fi
+}
+
+# Test that writing an invalid mode is rejected.
+test_invalid() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+ local saved
+
+ saved=$(get_active_mode "$path")
+
+ if echo "$mode" > "$path" 2>/dev/null; then
+ # Write succeeded -- check if mode actually changed (it shouldn't)
+ local active
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$saved" ]; then
+ # Some shells don't propagate the error, but mode unchanged
+ ktap_test_pass "$label: reject '$mode'"
+ else
+ ktap_test_fail "$label: '$mode' should have been rejected but mode changed to '$active'"
+ fi
+ else
+ ktap_test_pass "$label: reject '$mode'"
+ fi
+}
+
+# Test that writing the same mode twice doesn't crash or change state.
+test_idempotent() {
+ local path="$1"
+ local mode="$2"
+ local label="$3"
+
+ echo "$mode" > "$path" 2>/dev/null
+ echo "$mode" > "$path" 2>/dev/null
+ local active
+ active=$(get_active_mode "$path")
+ if [ "$active" = "$mode" ]; then
+ ktap_test_pass "$label: idempotent '$mode'"
+ else
+ ktap_test_fail "$label: idempotent '$mode', got '$active'"
+ fi
+}
+
+# Write a numeric value, read it back, verify match.
+test_numeric() {
+ local path="$1"
+ local value="$2"
+ local label="$3"
+ local readback
+
+ if ! echo "$value" > "$path" 2>/dev/null; then
+ ktap_test_fail "$label: write '$value'"
+ return
+ fi
+
+ readback=$(cat "$path" 2>/dev/null)
+ if [ "$readback" = "$value" ]; then
+ ktap_test_pass "$label: write '$value'"
+ else
+ ktap_test_fail "$label: write '$value', read back '$readback'"
+ fi
+}
+
+# Verify that an out-of-range or invalid numeric value is rejected.
+test_numeric_invalid() {
+ local path="$1"
+ local value="$2"
+ local label="$3"
+ local saved readback
+
+ saved=$(cat "$path" 2>/dev/null)
+
+ if echo "$value" > "$path" 2>/dev/null; then
+ readback=$(cat "$path" 2>/dev/null)
+ if [ "$readback" = "$saved" ]; then
+ ktap_test_pass "$label: reject '$value'"
+ else
+ ktap_test_fail "$label: '$value' should have been rejected but value changed to '$readback'"
+ fi
+ else
+ ktap_test_pass "$label: reject '$value'"
+ fi
+}
+
+# Verify a read-only file: readable, returns a numeric value, rejects writes.
+test_readonly() {
+ local path="$1"
+ local label="$2"
+ local val
+
+ val=$(cat "$path" 2>/dev/null)
+ if [ -z "$val" ]; then
+ ktap_test_fail "$label: read returned empty"
+ return
+ fi
+
+ if ! echo "$val" | grep -qE '^[0-9]+$'; then
+ ktap_test_fail "$label: expected numeric, got '$val'"
+ return
+ fi
+
+ if echo "0" > "$path" 2>/dev/null; then
+ local after
+ after=$(cat "$path" 2>/dev/null)
+ if [ "$after" = "$val" ]; then
+ ktap_test_pass "$label: read-only (value=$val)"
+ else
+ ktap_test_fail "$label: write should have been rejected but value changed"
+ fi
+ else
+ ktap_test_pass "$label: read-only (value=$val)"
+ fi
+}
+
+# --- Precondition checks ---
+
+ktap_print_header
+
+if [ ! -d "$THP_SYSFS" ]; then
+ ktap_skip_all "THP sysfs not found at $THP_SYSFS"
+ exit "$KSFT_SKIP"
+fi
+
+if [ "$(id -u)" -ne 0 ]; then
+ ktap_skip_all "must be run as root"
+ exit "$KSFT_SKIP"
+fi
+
+# --- Test global THP enabled ---
+
+GLOBAL_ENABLED="$THP_SYSFS/enabled"
+
+if [ ! -f "$GLOBAL_ENABLED" ]; then
+ ktap_test_skip "global enabled file not found"
+else
+ ktap_print_msg "Testing global THP enabled ($GLOBAL_ENABLED)"
+
+ # Save current setting
+ saved_global=$(get_active_mode "$GLOBAL_ENABLED")
+
+ # Valid modes for global
+ test_mode "$GLOBAL_ENABLED" "always" "global"
+ test_mode "$GLOBAL_ENABLED" "madvise" "global"
+ test_mode "$GLOBAL_ENABLED" "never" "global"
+
+ # "inherit" is not valid for global THP
+ test_invalid "$GLOBAL_ENABLED" "inherit" "global"
+
+ # Invalid strings
+ test_invalid "$GLOBAL_ENABLED" "bogus" "global"
+ test_invalid "$GLOBAL_ENABLED" "" "global (empty)"
+
+ # Idempotent writes
+ test_idempotent "$GLOBAL_ENABLED" "always" "global"
+ test_idempotent "$GLOBAL_ENABLED" "never" "global"
+
+ # Restore
+ echo "$saved_global" > "$GLOBAL_ENABLED" 2>/dev/null
+fi
+
+# --- Test global defrag ---
+
+GLOBAL_DEFRAG="$THP_SYSFS/defrag"
+
+if [ ! -f "$GLOBAL_DEFRAG" ]; then
+ ktap_test_skip "defrag file not found"
+else
+ ktap_print_msg "Testing global THP defrag ($GLOBAL_DEFRAG)"
+
+ saved_defrag=$(get_active_mode "$GLOBAL_DEFRAG")
+
+ # Valid modes
+ test_mode "$GLOBAL_DEFRAG" "always" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "defer" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "madvise" "defrag"
+ test_mode "$GLOBAL_DEFRAG" "never" "defrag"
+
+ # Invalid
+ test_invalid "$GLOBAL_DEFRAG" "bogus" "defrag"
+ test_invalid "$GLOBAL_DEFRAG" "" "defrag (empty)"
+ test_invalid "$GLOBAL_DEFRAG" "inherit" "defrag"
+
+ # Idempotent
+ test_idempotent "$GLOBAL_DEFRAG" "always" "defrag"
+ test_idempotent "$GLOBAL_DEFRAG" "never" "defrag"
+
+ # Mode transitions: cycle through all 5
+ echo "always" > "$GLOBAL_DEFRAG"
+ test_mode "$GLOBAL_DEFRAG" "defer" "defrag (always->defer)"
+ test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag (defer->defer+madvise)"
+ test_mode "$GLOBAL_DEFRAG" "madvise" "defrag (defer+madvise->madvise)"
+ test_mode "$GLOBAL_DEFRAG" "never" "defrag (madvise->never)"
+ test_mode "$GLOBAL_DEFRAG" "always" "defrag (never->always)"
+
+ # Restore
+ echo "$saved_defrag" > "$GLOBAL_DEFRAG" 2>/dev/null
+fi
+
+# --- Test use_zero_page ---
+
+USE_ZERO_PAGE="$THP_SYSFS/use_zero_page"
+
+if [ ! -f "$USE_ZERO_PAGE" ]; then
+ ktap_test_skip "use_zero_page file not found"
+else
+ ktap_print_msg "Testing use_zero_page ($USE_ZERO_PAGE)"
+
+ saved_uzp=$(cat "$USE_ZERO_PAGE" 2>/dev/null)
+
+ # Valid values
+ test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page"
+ test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page"
+
+ # Invalid values
+ test_numeric_invalid "$USE_ZERO_PAGE" "2" "use_zero_page"
+ test_numeric_invalid "$USE_ZERO_PAGE" "-1" "use_zero_page"
+ test_numeric_invalid "$USE_ZERO_PAGE" "bogus" "use_zero_page"
+
+ # Idempotent
+ echo "1" > "$USE_ZERO_PAGE" 2>/dev/null
+ test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page (idempotent)"
+ echo "0" > "$USE_ZERO_PAGE" 2>/dev/null
+ test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page (idempotent)"
+
+ # Restore
+ echo "$saved_uzp" > "$USE_ZERO_PAGE" 2>/dev/null
+fi
+
+# --- Test hpage_pmd_size ---
+
+HPAGE_PMD_SIZE_FILE="$THP_SYSFS/hpage_pmd_size"
+
+if [ ! -f "$HPAGE_PMD_SIZE_FILE" ]; then
+ ktap_test_skip "hpage_pmd_size file not found"
+else
+ ktap_print_msg "Testing hpage_pmd_size ($HPAGE_PMD_SIZE_FILE)"
+
+ test_readonly "$HPAGE_PMD_SIZE_FILE" "hpage_pmd_size"
+fi
+
+# --- Test global shmem_enabled ---
+
+SHMEM_ENABLED="$THP_SYSFS/shmem_enabled"
+
+if [ ! -f "$SHMEM_ENABLED" ]; then
+ ktap_test_skip "shmem_enabled file not found (CONFIG_SHMEM not set?)"
+else
+ ktap_print_msg "Testing global shmem_enabled ($SHMEM_ENABLED)"
+
+ saved_shmem=$(get_active_mode "$SHMEM_ENABLED")
+
+ # Valid modes
+ test_mode "$SHMEM_ENABLED" "always" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "never" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled"
+ test_mode "$SHMEM_ENABLED" "force" "shmem_enabled"
+
+ # Invalid
+ test_invalid "$SHMEM_ENABLED" "bogus" "shmem_enabled"
+ test_invalid "$SHMEM_ENABLED" "inherit" "shmem_enabled"
+ test_invalid "$SHMEM_ENABLED" "" "shmem_enabled (empty)"
+
+ # Idempotent
+ test_idempotent "$SHMEM_ENABLED" "always" "shmem_enabled"
+ test_idempotent "$SHMEM_ENABLED" "never" "shmem_enabled"
+
+ # Mode transitions: cycle through all 6
+ echo "always" > "$SHMEM_ENABLED"
+ test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled (always->within_size)"
+ test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled (within_size->advise)"
+ test_mode "$SHMEM_ENABLED" "never" "shmem_enabled (advise->never)"
+ test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled (never->deny)"
+ test_mode "$SHMEM_ENABLED" "force" "shmem_enabled (deny->force)"
+ test_mode "$SHMEM_ENABLED" "always" "shmem_enabled (force->always)"
+
+ # Restore
+ echo "$saved_shmem" > "$SHMEM_ENABLED" 2>/dev/null
+fi
+
+# --- Test shrink_underused ---
+
+SHRINK_UNDERUSED="$THP_SYSFS/shrink_underused"
+
+if [ ! -f "$SHRINK_UNDERUSED" ]; then
+ ktap_test_skip "shrink_underused file not found"
+else
+ ktap_print_msg "Testing shrink_underused ($SHRINK_UNDERUSED)"
+
+ saved_shrink=$(cat "$SHRINK_UNDERUSED" 2>/dev/null)
+
+ # Valid values
+ test_numeric "$SHRINK_UNDERUSED" "0" "shrink_underused"
+ test_numeric "$SHRINK_UNDERUSED" "1" "shrink_underused"
+
+ # Invalid values
+ test_numeric_invalid "$SHRINK_UNDERUSED" "2" "shrink_underused"
+ test_numeric_invalid "$SHRINK_UNDERUSED" "bogus" "shrink_underused"
+
+ # Restore
+ echo "$saved_shrink" > "$SHRINK_UNDERUSED" 2>/dev/null
+fi
+
+# --- Test per-size anon THP enabled ---
+
+found_anon=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+
+ ANON_ENABLED="$dir/enabled"
+ [ -f "$ANON_ENABLED" ] || continue
+
+ found_anon=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size anon THP enabled ($size)"
+
+ # Save current setting
+ saved_anon=$(get_active_mode "$ANON_ENABLED")
+
+ # Valid modes for per-size anon (includes inherit)
+ test_mode "$ANON_ENABLED" "always" "$size"
+ test_mode "$ANON_ENABLED" "inherit" "$size"
+ test_mode "$ANON_ENABLED" "madvise" "$size"
+ test_mode "$ANON_ENABLED" "never" "$size"
+
+ # Invalid strings
+ test_invalid "$ANON_ENABLED" "bogus" "$size"
+
+ # Idempotent writes
+ test_idempotent "$ANON_ENABLED" "always" "$size"
+ test_idempotent "$ANON_ENABLED" "inherit" "$size"
+ test_idempotent "$ANON_ENABLED" "never" "$size"
+
+ # Mode transitions: verify each mode clears the others
+ echo "always" > "$ANON_ENABLED"
+ test_mode "$ANON_ENABLED" "madvise" "$size (always->madvise)"
+ test_mode "$ANON_ENABLED" "inherit" "$size (madvise->inherit)"
+ test_mode "$ANON_ENABLED" "never" "$size (inherit->never)"
+ test_mode "$ANON_ENABLED" "always" "$size (never->always)"
+
+ # Restore
+ echo "$saved_anon" > "$ANON_ENABLED" 2>/dev/null
+
+ # Only test one size in detail to keep output manageable,
+ # but do a quick smoke test on the rest
+ break
+done
+
+if [ $found_anon -eq 0 ]; then
+ ktap_test_skip "no per-size anon THP directories found"
+fi
+
+# Quick smoke test: all other sizes accept valid modes
+first=1
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ ANON_ENABLED="$dir/enabled"
+ [ -f "$ANON_ENABLED" ] || continue
+
+ # Skip the first one (already tested in detail)
+ if [ $first -eq 1 ]; then
+ first=0
+ continue
+ fi
+
+ size=$(basename "$dir")
+ saved=$(get_active_mode "$ANON_ENABLED")
+
+ smoke_failed=0
+ for mode in always inherit madvise never; do
+ echo "$mode" > "$ANON_ENABLED" 2>/dev/null
+ active=$(get_active_mode "$ANON_ENABLED")
+ if [ "$active" != "$mode" ]; then
+ ktap_test_fail "$size: smoke test '$mode' got '$active'"
+ smoke_failed=1
+ break
+ fi
+ done
+ [ $smoke_failed -eq 0 ] && ktap_test_pass "$size: smoke test all modes"
+
+ echo "$saved" > "$ANON_ENABLED" 2>/dev/null
+done
+
+# --- Test per-size shmem_enabled ---
+
+found_shmem=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+
+ SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
+ [ -f "$SHMEM_SIZE_ENABLED" ] || continue
+
+ found_shmem=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size shmem_enabled ($size)"
+
+ # Save current setting
+ saved_shmem_size=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+
+ # Valid modes for per-size shmem
+ test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem"
+ test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem"
+
+ # Invalid: deny and force are not valid for per-size
+ test_invalid "$SHMEM_SIZE_ENABLED" "bogus" "$size shmem"
+ test_invalid "$SHMEM_SIZE_ENABLED" "deny" "$size shmem"
+ test_invalid "$SHMEM_SIZE_ENABLED" "force" "$size shmem"
+
+ # Mode transitions
+ echo "always" > "$SHMEM_SIZE_ENABLED"
+ test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem (always->inherit)"
+ test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem (inherit->within_size)"
+ test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem (within_size->advise)"
+ test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem (advise->never)"
+ test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem (never->always)"
+
+ # Restore
+ echo "$saved_shmem_size" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+
+ # Only test one size in detail
+ break
+done
+
+if [ $found_shmem -eq 0 ]; then
+ ktap_test_skip "no per-size shmem_enabled files found"
+fi
+
+# Quick smoke test: remaining sizes with shmem_enabled
+first=1
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
+ [ -f "$SHMEM_SIZE_ENABLED" ] || continue
+
+ if [ $first -eq 1 ]; then
+ first=0
+ continue
+ fi
+
+ size=$(basename "$dir")
+ saved=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+
+ smoke_failed=0
+ for mode in always inherit within_size advise never; do
+ echo "$mode" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+ active=$(get_active_mode "$SHMEM_SIZE_ENABLED")
+ if [ "$active" != "$mode" ]; then
+ ktap_test_fail "$size shmem: smoke test '$mode' got '$active'"
+ smoke_failed=1
+ break
+ fi
+ done
+ [ $smoke_failed -eq 0 ] && ktap_test_pass "$size shmem: smoke test all modes"
+
+ echo "$saved" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
+done
+
+# --- Test khugepaged tunables ---
+
+KHUGEPAGED="$THP_SYSFS/khugepaged"
+
+if [ ! -d "$KHUGEPAGED" ]; then
+ ktap_test_skip "khugepaged directory not found"
+else
+ ktap_print_msg "Testing khugepaged tunables ($KHUGEPAGED)"
+
+ # Compute HPAGE_PMD_NR for boundary tests
+ pmd_size=$(cat "$HPAGE_PMD_SIZE_FILE" 2>/dev/null)
+ page_size=$(getconf PAGE_SIZE)
+ if [ -n "$pmd_size" ] && [ -n "$page_size" ] && [ "$page_size" -gt 0 ]; then
+ hpage_pmd_nr=$((pmd_size / page_size))
+ else
+ hpage_pmd_nr=512
+ fi
+
+ # Save all tunable values
+ saved_khp_defrag=$(cat "$KHUGEPAGED/defrag" 2>/dev/null)
+ saved_khp_max_ptes_none=$(cat "$KHUGEPAGED/max_ptes_none" 2>/dev/null)
+ saved_khp_max_ptes_swap=$(cat "$KHUGEPAGED/max_ptes_swap" 2>/dev/null)
+ saved_khp_max_ptes_shared=$(cat "$KHUGEPAGED/max_ptes_shared" 2>/dev/null)
+ saved_khp_pages_to_scan=$(cat "$KHUGEPAGED/pages_to_scan" 2>/dev/null)
+ saved_khp_scan_sleep=$(cat "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null)
+ saved_khp_alloc_sleep=$(cat "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null)
+
+ # khugepaged/defrag (0/1 flag)
+ if [ -f "$KHUGEPAGED/defrag" ]; then
+ test_numeric "$KHUGEPAGED/defrag" "0" "khugepaged/defrag"
+ test_numeric "$KHUGEPAGED/defrag" "1" "khugepaged/defrag"
+ test_numeric_invalid "$KHUGEPAGED/defrag" "2" "khugepaged/defrag"
+ test_numeric_invalid "$KHUGEPAGED/defrag" "bogus" "khugepaged/defrag"
+ fi
+
+ # khugepaged/max_ptes_none (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_none" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_none" "0" "khugepaged/max_ptes_none"
+ test_numeric "$KHUGEPAGED/max_ptes_none" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_none"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_none" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_none (boundary)"
+ fi
+
+ # khugepaged/max_ptes_swap (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_swap" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_swap" "0" "khugepaged/max_ptes_swap"
+ test_numeric "$KHUGEPAGED/max_ptes_swap" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_swap"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_swap" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_swap (boundary)"
+ fi
+
+ # khugepaged/max_ptes_shared (0 .. HPAGE_PMD_NR-1)
+ if [ -f "$KHUGEPAGED/max_ptes_shared" ]; then
+ test_numeric "$KHUGEPAGED/max_ptes_shared" "0" "khugepaged/max_ptes_shared"
+ test_numeric "$KHUGEPAGED/max_ptes_shared" "$((hpage_pmd_nr - 1))" \
+ "khugepaged/max_ptes_shared"
+ test_numeric_invalid "$KHUGEPAGED/max_ptes_shared" "$hpage_pmd_nr" \
+ "khugepaged/max_ptes_shared (boundary)"
+ fi
+
+ # khugepaged/pages_to_scan (1 .. UINT_MAX, 0 rejected)
+ if [ -f "$KHUGEPAGED/pages_to_scan" ]; then
+ test_numeric "$KHUGEPAGED/pages_to_scan" "1" "khugepaged/pages_to_scan"
+ test_numeric "$KHUGEPAGED/pages_to_scan" "8" "khugepaged/pages_to_scan"
+ test_numeric_invalid "$KHUGEPAGED/pages_to_scan" "0" \
+ "khugepaged/pages_to_scan (reject 0)"
+ fi
+
+ # khugepaged/scan_sleep_millisecs
+ if [ -f "$KHUGEPAGED/scan_sleep_millisecs" ]; then
+ test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "0" \
+ "khugepaged/scan_sleep_millisecs"
+ test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "1000" \
+ "khugepaged/scan_sleep_millisecs"
+ fi
+
+ # khugepaged/alloc_sleep_millisecs
+ if [ -f "$KHUGEPAGED/alloc_sleep_millisecs" ]; then
+ test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "0" \
+ "khugepaged/alloc_sleep_millisecs"
+ test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "1000" \
+ "khugepaged/alloc_sleep_millisecs"
+ fi
+
+ # khugepaged/pages_collapsed (read-only)
+ if [ -f "$KHUGEPAGED/pages_collapsed" ]; then
+ test_readonly "$KHUGEPAGED/pages_collapsed" "khugepaged/pages_collapsed"
+ fi
+
+ # khugepaged/full_scans (read-only)
+ if [ -f "$KHUGEPAGED/full_scans" ]; then
+ test_readonly "$KHUGEPAGED/full_scans" "khugepaged/full_scans"
+ fi
+
+ # Restore all values
+ [ -n "$saved_khp_defrag" ] && \
+ echo "$saved_khp_defrag" > "$KHUGEPAGED/defrag" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_none" ] && \
+ echo "$saved_khp_max_ptes_none" > "$KHUGEPAGED/max_ptes_none" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_swap" ] && \
+ echo "$saved_khp_max_ptes_swap" > "$KHUGEPAGED/max_ptes_swap" 2>/dev/null
+ [ -n "$saved_khp_max_ptes_shared" ] && \
+ echo "$saved_khp_max_ptes_shared" > "$KHUGEPAGED/max_ptes_shared" 2>/dev/null
+ [ -n "$saved_khp_pages_to_scan" ] && \
+ echo "$saved_khp_pages_to_scan" > "$KHUGEPAGED/pages_to_scan" 2>/dev/null
+ [ -n "$saved_khp_scan_sleep" ] && \
+ echo "$saved_khp_scan_sleep" > "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null
+ [ -n "$saved_khp_alloc_sleep" ] && \
+ echo "$saved_khp_alloc_sleep" > "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null
+fi
+
+# --- Test per-size stats files ---
+
+found_stats=0
+for dir in "$THP_SYSFS"/hugepages-*; do
+ [ -d "$dir" ] || continue
+ [ -d "$dir/stats" ] || continue
+
+ found_stats=1
+ size=$(basename "$dir")
+ ktap_print_msg "Testing per-size stats ($size)"
+
+ for stat_file in "$dir"/stats/*; do
+ [ -f "$stat_file" ] || continue
+ stat_name=$(basename "$stat_file")
+ val=$(cat "$stat_file" 2>/dev/null)
+
+ if [ -z "$val" ]; then
+ ktap_test_fail "$size/stats/$stat_name: read returned empty"
+ continue
+ fi
+
+ if echo "$val" | grep -qE '^[0-9]+$'; then
+ ktap_test_pass "$size/stats/$stat_name: readable (value=$val)"
+ else
+ ktap_test_fail "$size/stats/$stat_name: expected numeric, got '$val'"
+ fi
+ done
+
+ # Only test one size
+ break
+done
+
+if [ $found_stats -eq 0 ]; then
+ ktap_test_skip "no per-size stats directories found"
+fi
+
+# --- Done ---
+
+# The test count is dynamic (depends on available sysfs files and hugepage
+# sizes), so print the plan at the end. TAP 13 allows trailing plans.
+KSFT_NUM_TESTS=$((KTAP_CNT_PASS + KTAP_CNT_FAIL + KTAP_CNT_SKIP))
+echo "1..$KSFT_NUM_TESTS"
+ktap_finished
---
base-commit: dc420ab7b435928a467b8d03fd3ed80e7d01d720
change-id: 20260309-thp_selftest_v2-4d32eba70441
Best regards,
--
Breno Leitao <leitao@debian.org>
^ permalink raw reply related [flat|nested] 8+ messages in thread* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-09 12:00 [PATCH] selftests/mm: add THP sysfs interface test Breno Leitao
@ 2026-03-16 12:55 ` Lorenzo Stoakes (Oracle)
2026-03-16 13:47 ` Breno Leitao
0 siblings, 1 reply; 8+ messages in thread
From: Lorenzo Stoakes (Oracle) @ 2026-03-16 12:55 UTC (permalink / raw)
To: Breno Leitao
Cc: Andrew Morton, David Hildenbrand, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Shuah Khan, linux-kernel, linux-mm, linux-kselftest, kernel-team
On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> Add a shell-based selftest that exercises the full set of THP sysfs
> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> khugepaged/ tunables, and per-size stats files.
>
> Each writable knob is tested for valid writes, invalid-input rejection,
> idempotent writes, and mode transitions where applicable. All original
> values are saved before testing and restored afterwards.
>
> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> structured TAP 13 output, making results parseable by the kselftest
> harness. The test plan is printed at the end since the number of test
> points is dynamic (depends on available hugepage sizes and sysfs files).
>
> This is particularly useful for validating the refactoring of
> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> and the new change_enabled()/change_anon_orders() helpers.
>
> Signed-off-by: Breno Leitao <leitao@debian.org>
The test is broken locally for me, returning error code 127.
I do appreciate the effort here, so I'm sorry to push back negatively, but I
feel a bash script here is pretty janky, and frankly if any of these interfaces
were as broken as this it'd be a major failure that would surely get picked up
far sooner elsewhere.
So while I think this might be useful as a local test for your sysfs interface
changes, I don't think this is really suited to the mm selftests.
Andrew - can we drop this change please? Thanks!
Thanks, Lorenzo
> ---
> While working on the THP sysfs interface, I noticed there were no
> existing tests covering this functionality. This patch adds test
> coverage that may be useful for upstream !?
> ---
> tools/testing/selftests/mm/Makefile | 1 +
> tools/testing/selftests/mm/run_vmtests.sh | 2 +
> tools/testing/selftests/mm/thp_sysfs_test.sh | 666 +++++++++++++++++++++++++++
> 3 files changed, 669 insertions(+)
>
> diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile
> index 7a5de4e9bf520..90fcca53a561b 100644
> --- a/tools/testing/selftests/mm/Makefile
> +++ b/tools/testing/selftests/mm/Makefile
> @@ -180,6 +180,7 @@ TEST_FILES += charge_reserved_hugetlb.sh
> TEST_FILES += hugetlb_reparenting_test.sh
> TEST_FILES += test_page_frag.sh
> TEST_FILES += run_vmtests.sh
> +TEST_FILES += thp_sysfs_test.sh
>
> # required by charge_reserved_hugetlb.sh
> TEST_FILES += write_hugetlb_memory.sh
> diff --git a/tools/testing/selftests/mm/run_vmtests.sh b/tools/testing/selftests/mm/run_vmtests.sh
> index afdcfd0d7cef7..9e0e2089214a3 100755
> --- a/tools/testing/selftests/mm/run_vmtests.sh
> +++ b/tools/testing/selftests/mm/run_vmtests.sh
> @@ -491,6 +491,8 @@ CATEGORY="thp" run_test ./khugepaged -s 4 all:shmem
>
> CATEGORY="thp" run_test ./transhuge-stress -d 20
>
> +CATEGORY="thp" run_test ./thp_sysfs_test.sh
> +
> # Try to create XFS if not provided
> if [ -z "${SPLIT_HUGE_PAGE_TEST_XFS_PATH}" ]; then
> if [ "${HAVE_HUGEPAGES}" = "1" ]; then
> diff --git a/tools/testing/selftests/mm/thp_sysfs_test.sh b/tools/testing/selftests/mm/thp_sysfs_test.sh
> new file mode 100755
> index 0000000000000..407f306a0989f
> --- /dev/null
> +++ b/tools/testing/selftests/mm/thp_sysfs_test.sh
> @@ -0,0 +1,666 @@
> +#!/bin/bash
> +# SPDX-License-Identifier: GPL-2.0
> +#
> +# Test THP sysfs interface.
> +#
> +# Exercises the full set of THP sysfs knobs: enabled (global and
> +# per-size anon), defrag, use_zero_page, hpage_pmd_size, shmem_enabled
> +# (global and per-size), shrink_underused, khugepaged/ tunables, and
> +# per-size stats files. Each writable knob is tested for valid writes,
> +# invalid-input rejection, idempotent writes, and mode transitions
> +# where applicable. All original values are saved before testing and
> +# restored afterwards.
> +#
> +# Author: Breno Leitao <leitao@debian.org>
> +
> +DIR="$(dirname "$(readlink -f "$0")")"
> +. "${DIR}"/../kselftest/ktap_helpers.sh
> +
> +THP_SYSFS=/sys/kernel/mm/transparent_hugepage
> +
> +# Read the currently active mode from a sysfs enabled file.
> +# The active mode is enclosed in brackets, e.g. "always [madvise] never"
> +get_active_mode() {
> + local path="$1"
> + local content
> +
> + content=$(cat "$path")
> + echo "$content" | grep -o '\[.*\]' | tr -d '[]'
> +}
> +
> +# Test that writing a mode and reading it back gives the expected result.
> +test_mode() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> + local active
> +
> + if ! echo "$mode" > "$path" 2>/dev/null; then
> + ktap_test_fail "$label: write '$mode'"
> + return
> + fi
> +
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$mode" ]; then
> + ktap_test_pass "$label: write '$mode'"
> + else
> + ktap_test_fail "$label: write '$mode', read back '$active'"
> + fi
> +}
> +
> +# Test that writing an invalid mode is rejected.
> +test_invalid() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> + local saved
> +
> + saved=$(get_active_mode "$path")
> +
> + if echo "$mode" > "$path" 2>/dev/null; then
> + # Write succeeded -- check if mode actually changed (it shouldn't)
> + local active
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$saved" ]; then
> + # Some shells don't propagate the error, but mode unchanged
> + ktap_test_pass "$label: reject '$mode'"
> + else
> + ktap_test_fail "$label: '$mode' should have been rejected but mode changed to '$active'"
> + fi
> + else
> + ktap_test_pass "$label: reject '$mode'"
> + fi
> +}
> +
> +# Test that writing the same mode twice doesn't crash or change state.
> +test_idempotent() {
> + local path="$1"
> + local mode="$2"
> + local label="$3"
> +
> + echo "$mode" > "$path" 2>/dev/null
> + echo "$mode" > "$path" 2>/dev/null
> + local active
> + active=$(get_active_mode "$path")
> + if [ "$active" = "$mode" ]; then
> + ktap_test_pass "$label: idempotent '$mode'"
> + else
> + ktap_test_fail "$label: idempotent '$mode', got '$active'"
> + fi
> +}
> +
> +# Write a numeric value, read it back, verify match.
> +test_numeric() {
> + local path="$1"
> + local value="$2"
> + local label="$3"
> + local readback
> +
> + if ! echo "$value" > "$path" 2>/dev/null; then
> + ktap_test_fail "$label: write '$value'"
> + return
> + fi
> +
> + readback=$(cat "$path" 2>/dev/null)
> + if [ "$readback" = "$value" ]; then
> + ktap_test_pass "$label: write '$value'"
> + else
> + ktap_test_fail "$label: write '$value', read back '$readback'"
> + fi
> +}
> +
> +# Verify that an out-of-range or invalid numeric value is rejected.
> +test_numeric_invalid() {
> + local path="$1"
> + local value="$2"
> + local label="$3"
> + local saved readback
> +
> + saved=$(cat "$path" 2>/dev/null)
> +
> + if echo "$value" > "$path" 2>/dev/null; then
> + readback=$(cat "$path" 2>/dev/null)
> + if [ "$readback" = "$saved" ]; then
> + ktap_test_pass "$label: reject '$value'"
> + else
> + ktap_test_fail "$label: '$value' should have been rejected but value changed to '$readback'"
> + fi
> + else
> + ktap_test_pass "$label: reject '$value'"
> + fi
> +}
> +
> +# Verify a read-only file: readable, returns a numeric value, rejects writes.
> +test_readonly() {
> + local path="$1"
> + local label="$2"
> + local val
> +
> + val=$(cat "$path" 2>/dev/null)
> + if [ -z "$val" ]; then
> + ktap_test_fail "$label: read returned empty"
> + return
> + fi
> +
> + if ! echo "$val" | grep -qE '^[0-9]+$'; then
> + ktap_test_fail "$label: expected numeric, got '$val'"
> + return
> + fi
> +
> + if echo "0" > "$path" 2>/dev/null; then
> + local after
> + after=$(cat "$path" 2>/dev/null)
> + if [ "$after" = "$val" ]; then
> + ktap_test_pass "$label: read-only (value=$val)"
> + else
> + ktap_test_fail "$label: write should have been rejected but value changed"
> + fi
> + else
> + ktap_test_pass "$label: read-only (value=$val)"
> + fi
> +}
> +
> +# --- Precondition checks ---
> +
> +ktap_print_header
> +
> +if [ ! -d "$THP_SYSFS" ]; then
> + ktap_skip_all "THP sysfs not found at $THP_SYSFS"
> + exit "$KSFT_SKIP"
> +fi
> +
> +if [ "$(id -u)" -ne 0 ]; then
> + ktap_skip_all "must be run as root"
> + exit "$KSFT_SKIP"
> +fi
> +
> +# --- Test global THP enabled ---
> +
> +GLOBAL_ENABLED="$THP_SYSFS/enabled"
> +
> +if [ ! -f "$GLOBAL_ENABLED" ]; then
> + ktap_test_skip "global enabled file not found"
> +else
> + ktap_print_msg "Testing global THP enabled ($GLOBAL_ENABLED)"
> +
> + # Save current setting
> + saved_global=$(get_active_mode "$GLOBAL_ENABLED")
> +
> + # Valid modes for global
> + test_mode "$GLOBAL_ENABLED" "always" "global"
> + test_mode "$GLOBAL_ENABLED" "madvise" "global"
> + test_mode "$GLOBAL_ENABLED" "never" "global"
> +
> + # "inherit" is not valid for global THP
> + test_invalid "$GLOBAL_ENABLED" "inherit" "global"
> +
> + # Invalid strings
> + test_invalid "$GLOBAL_ENABLED" "bogus" "global"
> + test_invalid "$GLOBAL_ENABLED" "" "global (empty)"
> +
> + # Idempotent writes
> + test_idempotent "$GLOBAL_ENABLED" "always" "global"
> + test_idempotent "$GLOBAL_ENABLED" "never" "global"
> +
> + # Restore
> + echo "$saved_global" > "$GLOBAL_ENABLED" 2>/dev/null
> +fi
> +
> +# --- Test global defrag ---
> +
> +GLOBAL_DEFRAG="$THP_SYSFS/defrag"
> +
> +if [ ! -f "$GLOBAL_DEFRAG" ]; then
> + ktap_test_skip "defrag file not found"
> +else
> + ktap_print_msg "Testing global THP defrag ($GLOBAL_DEFRAG)"
> +
> + saved_defrag=$(get_active_mode "$GLOBAL_DEFRAG")
> +
> + # Valid modes
> + test_mode "$GLOBAL_DEFRAG" "always" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "defer" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "madvise" "defrag"
> + test_mode "$GLOBAL_DEFRAG" "never" "defrag"
> +
> + # Invalid
> + test_invalid "$GLOBAL_DEFRAG" "bogus" "defrag"
> + test_invalid "$GLOBAL_DEFRAG" "" "defrag (empty)"
> + test_invalid "$GLOBAL_DEFRAG" "inherit" "defrag"
> +
> + # Idempotent
> + test_idempotent "$GLOBAL_DEFRAG" "always" "defrag"
> + test_idempotent "$GLOBAL_DEFRAG" "never" "defrag"
> +
> + # Mode transitions: cycle through all 5
> + echo "always" > "$GLOBAL_DEFRAG"
> + test_mode "$GLOBAL_DEFRAG" "defer" "defrag (always->defer)"
> + test_mode "$GLOBAL_DEFRAG" "defer+madvise" "defrag (defer->defer+madvise)"
> + test_mode "$GLOBAL_DEFRAG" "madvise" "defrag (defer+madvise->madvise)"
> + test_mode "$GLOBAL_DEFRAG" "never" "defrag (madvise->never)"
> + test_mode "$GLOBAL_DEFRAG" "always" "defrag (never->always)"
> +
> + # Restore
> + echo "$saved_defrag" > "$GLOBAL_DEFRAG" 2>/dev/null
> +fi
> +
> +# --- Test use_zero_page ---
> +
> +USE_ZERO_PAGE="$THP_SYSFS/use_zero_page"
> +
> +if [ ! -f "$USE_ZERO_PAGE" ]; then
> + ktap_test_skip "use_zero_page file not found"
> +else
> + ktap_print_msg "Testing use_zero_page ($USE_ZERO_PAGE)"
> +
> + saved_uzp=$(cat "$USE_ZERO_PAGE" 2>/dev/null)
> +
> + # Valid values
> + test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page"
> + test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page"
> +
> + # Invalid values
> + test_numeric_invalid "$USE_ZERO_PAGE" "2" "use_zero_page"
> + test_numeric_invalid "$USE_ZERO_PAGE" "-1" "use_zero_page"
> + test_numeric_invalid "$USE_ZERO_PAGE" "bogus" "use_zero_page"
> +
> + # Idempotent
> + echo "1" > "$USE_ZERO_PAGE" 2>/dev/null
> + test_numeric "$USE_ZERO_PAGE" "1" "use_zero_page (idempotent)"
> + echo "0" > "$USE_ZERO_PAGE" 2>/dev/null
> + test_numeric "$USE_ZERO_PAGE" "0" "use_zero_page (idempotent)"
> +
> + # Restore
> + echo "$saved_uzp" > "$USE_ZERO_PAGE" 2>/dev/null
> +fi
> +
> +# --- Test hpage_pmd_size ---
> +
> +HPAGE_PMD_SIZE_FILE="$THP_SYSFS/hpage_pmd_size"
> +
> +if [ ! -f "$HPAGE_PMD_SIZE_FILE" ]; then
> + ktap_test_skip "hpage_pmd_size file not found"
> +else
> + ktap_print_msg "Testing hpage_pmd_size ($HPAGE_PMD_SIZE_FILE)"
> +
> + test_readonly "$HPAGE_PMD_SIZE_FILE" "hpage_pmd_size"
> +fi
> +
> +# --- Test global shmem_enabled ---
> +
> +SHMEM_ENABLED="$THP_SYSFS/shmem_enabled"
> +
> +if [ ! -f "$SHMEM_ENABLED" ]; then
> + ktap_test_skip "shmem_enabled file not found (CONFIG_SHMEM not set?)"
> +else
> + ktap_print_msg "Testing global shmem_enabled ($SHMEM_ENABLED)"
> +
> + saved_shmem=$(get_active_mode "$SHMEM_ENABLED")
> +
> + # Valid modes
> + test_mode "$SHMEM_ENABLED" "always" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "never" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled"
> + test_mode "$SHMEM_ENABLED" "force" "shmem_enabled"
> +
> + # Invalid
> + test_invalid "$SHMEM_ENABLED" "bogus" "shmem_enabled"
> + test_invalid "$SHMEM_ENABLED" "inherit" "shmem_enabled"
> + test_invalid "$SHMEM_ENABLED" "" "shmem_enabled (empty)"
> +
> + # Idempotent
> + test_idempotent "$SHMEM_ENABLED" "always" "shmem_enabled"
> + test_idempotent "$SHMEM_ENABLED" "never" "shmem_enabled"
> +
> + # Mode transitions: cycle through all 6
> + echo "always" > "$SHMEM_ENABLED"
> + test_mode "$SHMEM_ENABLED" "within_size" "shmem_enabled (always->within_size)"
> + test_mode "$SHMEM_ENABLED" "advise" "shmem_enabled (within_size->advise)"
> + test_mode "$SHMEM_ENABLED" "never" "shmem_enabled (advise->never)"
> + test_mode "$SHMEM_ENABLED" "deny" "shmem_enabled (never->deny)"
> + test_mode "$SHMEM_ENABLED" "force" "shmem_enabled (deny->force)"
> + test_mode "$SHMEM_ENABLED" "always" "shmem_enabled (force->always)"
> +
> + # Restore
> + echo "$saved_shmem" > "$SHMEM_ENABLED" 2>/dev/null
> +fi
> +
> +# --- Test shrink_underused ---
> +
> +SHRINK_UNDERUSED="$THP_SYSFS/shrink_underused"
> +
> +if [ ! -f "$SHRINK_UNDERUSED" ]; then
> + ktap_test_skip "shrink_underused file not found"
> +else
> + ktap_print_msg "Testing shrink_underused ($SHRINK_UNDERUSED)"
> +
> + saved_shrink=$(cat "$SHRINK_UNDERUSED" 2>/dev/null)
> +
> + # Valid values
> + test_numeric "$SHRINK_UNDERUSED" "0" "shrink_underused"
> + test_numeric "$SHRINK_UNDERUSED" "1" "shrink_underused"
> +
> + # Invalid values
> + test_numeric_invalid "$SHRINK_UNDERUSED" "2" "shrink_underused"
> + test_numeric_invalid "$SHRINK_UNDERUSED" "bogus" "shrink_underused"
> +
> + # Restore
> + echo "$saved_shrink" > "$SHRINK_UNDERUSED" 2>/dev/null
> +fi
> +
> +# --- Test per-size anon THP enabled ---
> +
> +found_anon=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> +
> + ANON_ENABLED="$dir/enabled"
> + [ -f "$ANON_ENABLED" ] || continue
> +
> + found_anon=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size anon THP enabled ($size)"
> +
> + # Save current setting
> + saved_anon=$(get_active_mode "$ANON_ENABLED")
> +
> + # Valid modes for per-size anon (includes inherit)
> + test_mode "$ANON_ENABLED" "always" "$size"
> + test_mode "$ANON_ENABLED" "inherit" "$size"
> + test_mode "$ANON_ENABLED" "madvise" "$size"
> + test_mode "$ANON_ENABLED" "never" "$size"
> +
> + # Invalid strings
> + test_invalid "$ANON_ENABLED" "bogus" "$size"
> +
> + # Idempotent writes
> + test_idempotent "$ANON_ENABLED" "always" "$size"
> + test_idempotent "$ANON_ENABLED" "inherit" "$size"
> + test_idempotent "$ANON_ENABLED" "never" "$size"
> +
> + # Mode transitions: verify each mode clears the others
> + echo "always" > "$ANON_ENABLED"
> + test_mode "$ANON_ENABLED" "madvise" "$size (always->madvise)"
> + test_mode "$ANON_ENABLED" "inherit" "$size (madvise->inherit)"
> + test_mode "$ANON_ENABLED" "never" "$size (inherit->never)"
> + test_mode "$ANON_ENABLED" "always" "$size (never->always)"
> +
> + # Restore
> + echo "$saved_anon" > "$ANON_ENABLED" 2>/dev/null
> +
> + # Only test one size in detail to keep output manageable,
> + # but do a quick smoke test on the rest
> + break
> +done
> +
> +if [ $found_anon -eq 0 ]; then
> + ktap_test_skip "no per-size anon THP directories found"
> +fi
> +
> +# Quick smoke test: all other sizes accept valid modes
> +first=1
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + ANON_ENABLED="$dir/enabled"
> + [ -f "$ANON_ENABLED" ] || continue
> +
> + # Skip the first one (already tested in detail)
> + if [ $first -eq 1 ]; then
> + first=0
> + continue
> + fi
> +
> + size=$(basename "$dir")
> + saved=$(get_active_mode "$ANON_ENABLED")
> +
> + smoke_failed=0
> + for mode in always inherit madvise never; do
> + echo "$mode" > "$ANON_ENABLED" 2>/dev/null
> + active=$(get_active_mode "$ANON_ENABLED")
> + if [ "$active" != "$mode" ]; then
> + ktap_test_fail "$size: smoke test '$mode' got '$active'"
> + smoke_failed=1
> + break
> + fi
> + done
> + [ $smoke_failed -eq 0 ] && ktap_test_pass "$size: smoke test all modes"
> +
> + echo "$saved" > "$ANON_ENABLED" 2>/dev/null
> +done
> +
> +# --- Test per-size shmem_enabled ---
> +
> +found_shmem=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> +
> + SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
> + [ -f "$SHMEM_SIZE_ENABLED" ] || continue
> +
> + found_shmem=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size shmem_enabled ($size)"
> +
> + # Save current setting
> + saved_shmem_size=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> +
> + # Valid modes for per-size shmem
> + test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem"
> + test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem"
> +
> + # Invalid: deny and force are not valid for per-size
> + test_invalid "$SHMEM_SIZE_ENABLED" "bogus" "$size shmem"
> + test_invalid "$SHMEM_SIZE_ENABLED" "deny" "$size shmem"
> + test_invalid "$SHMEM_SIZE_ENABLED" "force" "$size shmem"
> +
> + # Mode transitions
> + echo "always" > "$SHMEM_SIZE_ENABLED"
> + test_mode "$SHMEM_SIZE_ENABLED" "inherit" "$size shmem (always->inherit)"
> + test_mode "$SHMEM_SIZE_ENABLED" "within_size" "$size shmem (inherit->within_size)"
> + test_mode "$SHMEM_SIZE_ENABLED" "advise" "$size shmem (within_size->advise)"
> + test_mode "$SHMEM_SIZE_ENABLED" "never" "$size shmem (advise->never)"
> + test_mode "$SHMEM_SIZE_ENABLED" "always" "$size shmem (never->always)"
> +
> + # Restore
> + echo "$saved_shmem_size" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> +
> + # Only test one size in detail
> + break
> +done
> +
> +if [ $found_shmem -eq 0 ]; then
> + ktap_test_skip "no per-size shmem_enabled files found"
> +fi
> +
> +# Quick smoke test: remaining sizes with shmem_enabled
> +first=1
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + SHMEM_SIZE_ENABLED="$dir/shmem_enabled"
> + [ -f "$SHMEM_SIZE_ENABLED" ] || continue
> +
> + if [ $first -eq 1 ]; then
> + first=0
> + continue
> + fi
> +
> + size=$(basename "$dir")
> + saved=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> +
> + smoke_failed=0
> + for mode in always inherit within_size advise never; do
> + echo "$mode" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> + active=$(get_active_mode "$SHMEM_SIZE_ENABLED")
> + if [ "$active" != "$mode" ]; then
> + ktap_test_fail "$size shmem: smoke test '$mode' got '$active'"
> + smoke_failed=1
> + break
> + fi
> + done
> + [ $smoke_failed -eq 0 ] && ktap_test_pass "$size shmem: smoke test all modes"
> +
> + echo "$saved" > "$SHMEM_SIZE_ENABLED" 2>/dev/null
> +done
> +
> +# --- Test khugepaged tunables ---
> +
> +KHUGEPAGED="$THP_SYSFS/khugepaged"
> +
> +if [ ! -d "$KHUGEPAGED" ]; then
> + ktap_test_skip "khugepaged directory not found"
> +else
> + ktap_print_msg "Testing khugepaged tunables ($KHUGEPAGED)"
> +
> + # Compute HPAGE_PMD_NR for boundary tests
> + pmd_size=$(cat "$HPAGE_PMD_SIZE_FILE" 2>/dev/null)
> + page_size=$(getconf PAGE_SIZE)
> + if [ -n "$pmd_size" ] && [ -n "$page_size" ] && [ "$page_size" -gt 0 ]; then
> + hpage_pmd_nr=$((pmd_size / page_size))
> + else
> + hpage_pmd_nr=512
> + fi
> +
> + # Save all tunable values
> + saved_khp_defrag=$(cat "$KHUGEPAGED/defrag" 2>/dev/null)
> + saved_khp_max_ptes_none=$(cat "$KHUGEPAGED/max_ptes_none" 2>/dev/null)
> + saved_khp_max_ptes_swap=$(cat "$KHUGEPAGED/max_ptes_swap" 2>/dev/null)
> + saved_khp_max_ptes_shared=$(cat "$KHUGEPAGED/max_ptes_shared" 2>/dev/null)
> + saved_khp_pages_to_scan=$(cat "$KHUGEPAGED/pages_to_scan" 2>/dev/null)
> + saved_khp_scan_sleep=$(cat "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null)
> + saved_khp_alloc_sleep=$(cat "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null)
> +
> + # khugepaged/defrag (0/1 flag)
> + if [ -f "$KHUGEPAGED/defrag" ]; then
> + test_numeric "$KHUGEPAGED/defrag" "0" "khugepaged/defrag"
> + test_numeric "$KHUGEPAGED/defrag" "1" "khugepaged/defrag"
> + test_numeric_invalid "$KHUGEPAGED/defrag" "2" "khugepaged/defrag"
> + test_numeric_invalid "$KHUGEPAGED/defrag" "bogus" "khugepaged/defrag"
> + fi
> +
> + # khugepaged/max_ptes_none (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_none" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_none" "0" "khugepaged/max_ptes_none"
> + test_numeric "$KHUGEPAGED/max_ptes_none" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_none"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_none" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_none (boundary)"
> + fi
> +
> + # khugepaged/max_ptes_swap (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_swap" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_swap" "0" "khugepaged/max_ptes_swap"
> + test_numeric "$KHUGEPAGED/max_ptes_swap" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_swap"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_swap" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_swap (boundary)"
> + fi
> +
> + # khugepaged/max_ptes_shared (0 .. HPAGE_PMD_NR-1)
> + if [ -f "$KHUGEPAGED/max_ptes_shared" ]; then
> + test_numeric "$KHUGEPAGED/max_ptes_shared" "0" "khugepaged/max_ptes_shared"
> + test_numeric "$KHUGEPAGED/max_ptes_shared" "$((hpage_pmd_nr - 1))" \
> + "khugepaged/max_ptes_shared"
> + test_numeric_invalid "$KHUGEPAGED/max_ptes_shared" "$hpage_pmd_nr" \
> + "khugepaged/max_ptes_shared (boundary)"
> + fi
> +
> + # khugepaged/pages_to_scan (1 .. UINT_MAX, 0 rejected)
> + if [ -f "$KHUGEPAGED/pages_to_scan" ]; then
> + test_numeric "$KHUGEPAGED/pages_to_scan" "1" "khugepaged/pages_to_scan"
> + test_numeric "$KHUGEPAGED/pages_to_scan" "8" "khugepaged/pages_to_scan"
> + test_numeric_invalid "$KHUGEPAGED/pages_to_scan" "0" \
> + "khugepaged/pages_to_scan (reject 0)"
> + fi
> +
> + # khugepaged/scan_sleep_millisecs
> + if [ -f "$KHUGEPAGED/scan_sleep_millisecs" ]; then
> + test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "0" \
> + "khugepaged/scan_sleep_millisecs"
> + test_numeric "$KHUGEPAGED/scan_sleep_millisecs" "1000" \
> + "khugepaged/scan_sleep_millisecs"
> + fi
> +
> + # khugepaged/alloc_sleep_millisecs
> + if [ -f "$KHUGEPAGED/alloc_sleep_millisecs" ]; then
> + test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "0" \
> + "khugepaged/alloc_sleep_millisecs"
> + test_numeric "$KHUGEPAGED/alloc_sleep_millisecs" "1000" \
> + "khugepaged/alloc_sleep_millisecs"
> + fi
> +
> + # khugepaged/pages_collapsed (read-only)
> + if [ -f "$KHUGEPAGED/pages_collapsed" ]; then
> + test_readonly "$KHUGEPAGED/pages_collapsed" "khugepaged/pages_collapsed"
> + fi
> +
> + # khugepaged/full_scans (read-only)
> + if [ -f "$KHUGEPAGED/full_scans" ]; then
> + test_readonly "$KHUGEPAGED/full_scans" "khugepaged/full_scans"
> + fi
> +
> + # Restore all values
> + [ -n "$saved_khp_defrag" ] && \
> + echo "$saved_khp_defrag" > "$KHUGEPAGED/defrag" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_none" ] && \
> + echo "$saved_khp_max_ptes_none" > "$KHUGEPAGED/max_ptes_none" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_swap" ] && \
> + echo "$saved_khp_max_ptes_swap" > "$KHUGEPAGED/max_ptes_swap" 2>/dev/null
> + [ -n "$saved_khp_max_ptes_shared" ] && \
> + echo "$saved_khp_max_ptes_shared" > "$KHUGEPAGED/max_ptes_shared" 2>/dev/null
> + [ -n "$saved_khp_pages_to_scan" ] && \
> + echo "$saved_khp_pages_to_scan" > "$KHUGEPAGED/pages_to_scan" 2>/dev/null
> + [ -n "$saved_khp_scan_sleep" ] && \
> + echo "$saved_khp_scan_sleep" > "$KHUGEPAGED/scan_sleep_millisecs" 2>/dev/null
> + [ -n "$saved_khp_alloc_sleep" ] && \
> + echo "$saved_khp_alloc_sleep" > "$KHUGEPAGED/alloc_sleep_millisecs" 2>/dev/null
> +fi
> +
> +# --- Test per-size stats files ---
> +
> +found_stats=0
> +for dir in "$THP_SYSFS"/hugepages-*; do
> + [ -d "$dir" ] || continue
> + [ -d "$dir/stats" ] || continue
> +
> + found_stats=1
> + size=$(basename "$dir")
> + ktap_print_msg "Testing per-size stats ($size)"
> +
> + for stat_file in "$dir"/stats/*; do
> + [ -f "$stat_file" ] || continue
> + stat_name=$(basename "$stat_file")
> + val=$(cat "$stat_file" 2>/dev/null)
> +
> + if [ -z "$val" ]; then
> + ktap_test_fail "$size/stats/$stat_name: read returned empty"
> + continue
> + fi
> +
> + if echo "$val" | grep -qE '^[0-9]+$'; then
> + ktap_test_pass "$size/stats/$stat_name: readable (value=$val)"
> + else
> + ktap_test_fail "$size/stats/$stat_name: expected numeric, got '$val'"
> + fi
> + done
> +
> + # Only test one size
> + break
> +done
> +
> +if [ $found_stats -eq 0 ]; then
> + ktap_test_skip "no per-size stats directories found"
> +fi
> +
> +# --- Done ---
> +
> +# The test count is dynamic (depends on available sysfs files and hugepage
> +# sizes), so print the plan at the end. TAP 13 allows trailing plans.
> +KSFT_NUM_TESTS=$((KTAP_CNT_PASS + KTAP_CNT_FAIL + KTAP_CNT_SKIP))
> +echo "1..$KSFT_NUM_TESTS"
> +ktap_finished
>
> ---
> base-commit: dc420ab7b435928a467b8d03fd3ed80e7d01d720
> change-id: 20260309-thp_selftest_v2-4d32eba70441
>
> Best regards,
> --
> Breno Leitao <leitao@debian.org>
>
^ permalink raw reply [flat|nested] 8+ messages in thread* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-16 12:55 ` Lorenzo Stoakes (Oracle)
@ 2026-03-16 13:47 ` Breno Leitao
2026-03-16 14:44 ` David Hildenbrand (Arm)
0 siblings, 1 reply; 8+ messages in thread
From: Breno Leitao @ 2026-03-16 13:47 UTC (permalink / raw)
To: Lorenzo Stoakes (Oracle)
Cc: Andrew Morton, David Hildenbrand, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Shuah Khan, linux-kernel, linux-mm, linux-kselftest, kernel-team
On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> > Add a shell-based selftest that exercises the full set of THP sysfs
> > knobs: enabled (global and per-size anon), defrag, use_zero_page,
> > hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> > khugepaged/ tunables, and per-size stats files.
> >
> > Each writable knob is tested for valid writes, invalid-input rejection,
> > idempotent writes, and mode transitions where applicable. All original
> > values are saved before testing and restored afterwards.
> >
> > The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> > structured TAP 13 output, making results parseable by the kselftest
> > harness. The test plan is printed at the end since the number of test
> > points is dynamic (depends on available hugepage sizes and sysfs files).
> >
> > This is particularly useful for validating the refactoring of
> > enabled_store() and anon_enabled_store() to use sysfs_match_string()
> > and the new change_enabled()/change_anon_orders() helpers.
> >
> > Signed-off-by: Breno Leitao <leitao@debian.org>
>
> The test is broken locally for me, returning error code 127.
>
> I do appreciate the effort here, so I'm sorry to push back negatively, but I
> feel a bash script here is pretty janky, and frankly if any of these interfaces
> were as broken as this it'd be a major failure that would surely get picked up
> far sooner elsewhere.
>
> So while I think this might be useful as a local test for your sysfs interface
> changes, I don't think this is really suited to the mm selftests.
That is totally fine. This test is what I have been using to test the
changes, and I decide to share it in case someone find it useful.
Let's drop it.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-16 13:47 ` Breno Leitao
@ 2026-03-16 14:44 ` David Hildenbrand (Arm)
2026-03-16 16:02 ` Breno Leitao
0 siblings, 1 reply; 8+ messages in thread
From: David Hildenbrand (Arm) @ 2026-03-16 14:44 UTC (permalink / raw)
To: Breno Leitao, Lorenzo Stoakes (Oracle)
Cc: Andrew Morton, Liam R. Howlett, Vlastimil Babka, Mike Rapoport,
Suren Baghdasaryan, Michal Hocko, Shuah Khan, linux-kernel,
linux-mm, linux-kselftest, kernel-team
On 3/16/26 14:47, Breno Leitao wrote:
> On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
>> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
>>> Add a shell-based selftest that exercises the full set of THP sysfs
>>> knobs: enabled (global and per-size anon), defrag, use_zero_page,
>>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
>>> khugepaged/ tunables, and per-size stats files.
>>>
>>> Each writable knob is tested for valid writes, invalid-input rejection,
>>> idempotent writes, and mode transitions where applicable. All original
>>> values are saved before testing and restored afterwards.
>>>
>>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
>>> structured TAP 13 output, making results parseable by the kselftest
>>> harness. The test plan is printed at the end since the number of test
>>> points is dynamic (depends on available hugepage sizes and sysfs files).
>>>
>>> This is particularly useful for validating the refactoring of
>>> enabled_store() and anon_enabled_store() to use sysfs_match_string()
>>> and the new change_enabled()/change_anon_orders() helpers.
>>>
>>> Signed-off-by: Breno Leitao <leitao@debian.org>
>>
>> The test is broken locally for me, returning error code 127.
>>
>> I do appreciate the effort here, so I'm sorry to push back negatively, but I
>> feel a bash script here is pretty janky, and frankly if any of these interfaces
>> were as broken as this it'd be a major failure that would surely get picked up
>> far sooner elsewhere.
>>
>> So while I think this might be useful as a local test for your sysfs interface
>> changes, I don't think this is really suited to the mm selftests.
>
> That is totally fine. This test is what I have been using to test the
> changes, and I decide to share it in case someone find it useful.
>
> Let's drop it.
Out of interest, to we know why the test is failing for Lorenzo?
I agree that the test is a bit excessive, in particular when it comes to
invalid/idempotent values etc. I could see some value for testing
whether setting the modes keeps working, but also then I wonder if that
is really something we'll be changing frequently (and that breaks easily).
--
Cheers,
David
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-16 14:44 ` David Hildenbrand (Arm)
@ 2026-03-16 16:02 ` Breno Leitao
2026-03-16 19:53 ` Lorenzo Stoakes (Oracle)
0 siblings, 1 reply; 8+ messages in thread
From: Breno Leitao @ 2026-03-16 16:02 UTC (permalink / raw)
To: David Hildenbrand (Arm)
Cc: Lorenzo Stoakes (Oracle), Andrew Morton, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Shuah Khan, linux-kernel, linux-mm, linux-kselftest, kernel-team
On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote:
> On 3/16/26 14:47, Breno Leitao wrote:
> > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
> >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> >>> Add a shell-based selftest that exercises the full set of THP sysfs
> >>> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> >>> khugepaged/ tunables, and per-size stats files.
> >>>
> >>> Each writable knob is tested for valid writes, invalid-input rejection,
> >>> idempotent writes, and mode transitions where applicable. All original
> >>> values are saved before testing and restored afterwards.
> >>>
> >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> >>> structured TAP 13 output, making results parseable by the kselftest
> >>> harness. The test plan is printed at the end since the number of test
> >>> points is dynamic (depends on available hugepage sizes and sysfs files).
> >>>
> >>> This is particularly useful for validating the refactoring of
> >>> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> >>> and the new change_enabled()/change_anon_orders() helpers.
> >>>
> >>> Signed-off-by: Breno Leitao <leitao@debian.org>
> >>
> >> The test is broken locally for me, returning error code 127.
> >>
> >> I do appreciate the effort here, so I'm sorry to push back negatively, but I
> >> feel a bash script here is pretty janky, and frankly if any of these interfaces
> >> were as broken as this it'd be a major failure that would surely get picked up
> >> far sooner elsewhere.
> >>
> >> So while I think this might be useful as a local test for your sysfs interface
> >> changes, I don't think this is really suited to the mm selftests.
> >
> > That is totally fine. This test is what I have been using to test the
> > changes, and I decide to share it in case someone find it useful.
> >
> > Let's drop it.
>
> Out of interest, to we know why the test is failing for Lorenzo?
I really don't know, but, it sounds like ktap was not found?
Then the first early-exit path hit:
ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP"
# expands to: exit "" → exits with last $? = 127
> I agree that the test is a bit excessive, in particular when it comes to
> invalid/idempotent values etc. I could see some value for testing
> whether setting the modes keeps working, but also then I wonder if that
> is really something we'll be changing frequently (and that breaks easily).
yea, I make it very excessive, because there were some intrinsics in
those sysfs that I was gettingit wrong when doing the intial conversion.
So, the test is something that I trust now, and I found it useful when
finding regressiosn.
Is is something that will chagne frequently? probably not!
That said, would you like to have a simplified/different version of this
test?
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-16 16:02 ` Breno Leitao
@ 2026-03-16 19:53 ` Lorenzo Stoakes (Oracle)
2026-03-17 5:43 ` Mike Rapoport
0 siblings, 1 reply; 8+ messages in thread
From: Lorenzo Stoakes (Oracle) @ 2026-03-16 19:53 UTC (permalink / raw)
To: Breno Leitao
Cc: David Hildenbrand (Arm), Andrew Morton, Liam R. Howlett,
Vlastimil Babka, Mike Rapoport, Suren Baghdasaryan, Michal Hocko,
Shuah Khan, linux-kernel, linux-mm, linux-kselftest, kernel-team
On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote:
> On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote:
> > On 3/16/26 14:47, Breno Leitao wrote:
> > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
> > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> > >>> Add a shell-based selftest that exercises the full set of THP sysfs
> > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> > >>> khugepaged/ tunables, and per-size stats files.
> > >>>
> > >>> Each writable knob is tested for valid writes, invalid-input rejection,
> > >>> idempotent writes, and mode transitions where applicable. All original
> > >>> values are saved before testing and restored afterwards.
> > >>>
> > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> > >>> structured TAP 13 output, making results parseable by the kselftest
> > >>> harness. The test plan is printed at the end since the number of test
> > >>> points is dynamic (depends on available hugepage sizes and sysfs files).
> > >>>
> > >>> This is particularly useful for validating the refactoring of
> > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> > >>> and the new change_enabled()/change_anon_orders() helpers.
> > >>>
> > >>> Signed-off-by: Breno Leitao <leitao@debian.org>
> > >>
> > >> The test is broken locally for me, returning error code 127.
> > >>
> > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I
> > >> feel a bash script here is pretty janky, and frankly if any of these interfaces
> > >> were as broken as this it'd be a major failure that would surely get picked up
> > >> far sooner elsewhere.
> > >>
> > >> So while I think this might be useful as a local test for your sysfs interface
> > >> changes, I don't think this is really suited to the mm selftests.
> > >
> > > That is totally fine. This test is what I have been using to test the
> > > changes, and I decide to share it in case someone find it useful.
> > >
> > > Let's drop it.
> >
> > Out of interest, to we know why the test is failing for Lorenzo?
>
> I really don't know, but, it sounds like ktap was not found?
Yeah CONFIG_KUNIT is not set so could be :)
>
> Then the first early-exit path hit:
> ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP"
> # expands to: exit "" → exits with last $? = 127
>
> > I agree that the test is a bit excessive, in particular when it comes to
> > invalid/idempotent values etc. I could see some value for testing
> > whether setting the modes keeps working, but also then I wonder if that
> > is really something we'll be changing frequently (and that breaks easily).
>
> yea, I make it very excessive, because there were some intrinsics in
> those sysfs that I was gettingit wrong when doing the intial conversion.
>
> So, the test is something that I trust now, and I found it useful when
> finding regressiosn.
>
> Is is something that will chagne frequently? probably not!
>
> That said, would you like to have a simplified/different version of this
> test?
In an ideal world we'd use kunit or something to assert it internal to the
kernel I guess, but if we do have something scaled down it'd at least be nice to
have in C? :)
I am not sure how useful it'd be though overall, I don't see us changing this
too often and really we're more interested in asserting behaviour.
Sadly THP is inherently tricky to test generally because of its very nature, I
wish we could have better test isolation etc.
See tools/testing/vma for a forlorn dream of kernel code being run in userland
(but oh how the stubs/duplicate declarations/etc. are a pain).
I suspect THP could never be given the same treatment though! :)
Cheers, Lorenzo
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-16 19:53 ` Lorenzo Stoakes (Oracle)
@ 2026-03-17 5:43 ` Mike Rapoport
2026-03-17 8:45 ` Lorenzo Stoakes (Oracle)
0 siblings, 1 reply; 8+ messages in thread
From: Mike Rapoport @ 2026-03-17 5:43 UTC (permalink / raw)
To: Lorenzo Stoakes (Oracle)
Cc: Breno Leitao, David Hildenbrand (Arm), Andrew Morton,
Liam R. Howlett, Vlastimil Babka, Suren Baghdasaryan,
Michal Hocko, Shuah Khan, linux-kernel, linux-mm, linux-kselftest,
kernel-team
On Mon, Mar 16, 2026 at 07:53:46PM +0000, Lorenzo Stoakes (Oracle) wrote:
> On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote:
> > On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote:
> > > On 3/16/26 14:47, Breno Leitao wrote:
> > > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
> > > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> > > >>> Add a shell-based selftest that exercises the full set of THP sysfs
> > > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> > > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> > > >>> khugepaged/ tunables, and per-size stats files.
> > > >>>
> > > >>> Each writable knob is tested for valid writes, invalid-input rejection,
> > > >>> idempotent writes, and mode transitions where applicable. All original
> > > >>> values are saved before testing and restored afterwards.
> > > >>>
> > > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> > > >>> structured TAP 13 output, making results parseable by the kselftest
> > > >>> harness. The test plan is printed at the end since the number of test
> > > >>> points is dynamic (depends on available hugepage sizes and sysfs files).
> > > >>>
> > > >>> This is particularly useful for validating the refactoring of
> > > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> > > >>> and the new change_enabled()/change_anon_orders() helpers.
> > > >>>
> > > >>> Signed-off-by: Breno Leitao <leitao@debian.org>
> > > >>
> > > >> The test is broken locally for me, returning error code 127.
> > > >>
> > > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I
> > > >> feel a bash script here is pretty janky, and frankly if any of these interfaces
> > > >> were as broken as this it'd be a major failure that would surely get picked up
> > > >> far sooner elsewhere.
> > > >>
> > > >> So while I think this might be useful as a local test for your sysfs interface
> > > >> changes, I don't think this is really suited to the mm selftests.
> > > >
> > > > That is totally fine. This test is what I have been using to test the
> > > > changes, and I decide to share it in case someone find it useful.
> > > >
> > > > Let's drop it.
> > >
> > > Out of interest, to we know why the test is failing for Lorenzo?
> >
> > I really don't know, but, it sounds like ktap was not found?
>
> Yeah CONFIG_KUNIT is not set so could be :)
Nah, CONFIG_KUNIT has nothing to do with ktap_helpers.sh, probably your
environment does not bring in tools/testing/selftests/kselftest
> >
> > Then the first early-exit path hit:
> > ktap_skip_all "..." # undefined → returns 127 exit "$KSFT_SKIP"
> > # expands to: exit "" → exits with last $? = 127
> >
> > > I agree that the test is a bit excessive, in particular when it comes to
> > > invalid/idempotent values etc. I could see some value for testing
> > > whether setting the modes keeps working, but also then I wonder if that
> > > is really something we'll be changing frequently (and that breaks easily).
> >
> > yea, I make it very excessive, because there were some intrinsics in
> > those sysfs that I was gettingit wrong when doing the intial conversion.
> >
> > So, the test is something that I trust now, and I found it useful when
> > finding regressiosn.
> >
> > Is is something that will chagne frequently? probably not!
> >
> > That said, would you like to have a simplified/different version of this
> > test?
>
> In an ideal world we'd use kunit or something to assert it internal to the
> kernel I guess, but if we do have something scaled down it'd at least be nice to
> have in C? :)
>
> I am not sure how useful it'd be though overall, I don't see us changing this
> too often and really we're more interested in asserting behaviour.
>
> Sadly THP is inherently tricky to test generally because of its very nature, I
> wish we could have better test isolation etc.
>
> See tools/testing/vma for a forlorn dream of kernel code being run in userland
> (but oh how the stubs/duplicate declarations/etc. are a pain).
>
> I suspect THP could never be given the same treatment though! :)
>
> Cheers, Lorenzo
--
Sincerely yours,
Mike.
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH] selftests/mm: add THP sysfs interface test
2026-03-17 5:43 ` Mike Rapoport
@ 2026-03-17 8:45 ` Lorenzo Stoakes (Oracle)
0 siblings, 0 replies; 8+ messages in thread
From: Lorenzo Stoakes (Oracle) @ 2026-03-17 8:45 UTC (permalink / raw)
To: Mike Rapoport
Cc: Breno Leitao, David Hildenbrand (Arm), Andrew Morton,
Liam R. Howlett, Vlastimil Babka, Suren Baghdasaryan,
Michal Hocko, Shuah Khan, linux-kernel, linux-mm, linux-kselftest,
kernel-team
On Tue, Mar 17, 2026 at 07:43:41AM +0200, Mike Rapoport wrote:
> On Mon, Mar 16, 2026 at 07:53:46PM +0000, Lorenzo Stoakes (Oracle) wrote:
> > On Mon, Mar 16, 2026 at 09:02:33AM -0700, Breno Leitao wrote:
> > > On Mon, Mar 16, 2026 at 03:44:14PM +0100, David Hildenbrand (Arm) wrote:
> > > > On 3/16/26 14:47, Breno Leitao wrote:
> > > > > On Mon, Mar 16, 2026 at 12:55:13PM +0000, Lorenzo Stoakes (Oracle) wrote:
> > > > >> On Mon, Mar 09, 2026 at 05:00:34AM -0700, Breno Leitao wrote:
> > > > >>> Add a shell-based selftest that exercises the full set of THP sysfs
> > > > >>> knobs: enabled (global and per-size anon), defrag, use_zero_page,
> > > > >>> hpage_pmd_size, shmem_enabled (global and per-size), shrink_underused,
> > > > >>> khugepaged/ tunables, and per-size stats files.
> > > > >>>
> > > > >>> Each writable knob is tested for valid writes, invalid-input rejection,
> > > > >>> idempotent writes, and mode transitions where applicable. All original
> > > > >>> values are saved before testing and restored afterwards.
> > > > >>>
> > > > >>> The test uses the kselftest KTAP framework (ktap_helpers.sh) for
> > > > >>> structured TAP 13 output, making results parseable by the kselftest
> > > > >>> harness. The test plan is printed at the end since the number of test
> > > > >>> points is dynamic (depends on available hugepage sizes and sysfs files).
> > > > >>>
> > > > >>> This is particularly useful for validating the refactoring of
> > > > >>> enabled_store() and anon_enabled_store() to use sysfs_match_string()
> > > > >>> and the new change_enabled()/change_anon_orders() helpers.
> > > > >>>
> > > > >>> Signed-off-by: Breno Leitao <leitao@debian.org>
> > > > >>
> > > > >> The test is broken locally for me, returning error code 127.
> > > > >>
> > > > >> I do appreciate the effort here, so I'm sorry to push back negatively, but I
> > > > >> feel a bash script here is pretty janky, and frankly if any of these interfaces
> > > > >> were as broken as this it'd be a major failure that would surely get picked up
> > > > >> far sooner elsewhere.
> > > > >>
> > > > >> So while I think this might be useful as a local test for your sysfs interface
> > > > >> changes, I don't think this is really suited to the mm selftests.
> > > > >
> > > > > That is totally fine. This test is what I have been using to test the
> > > > > changes, and I decide to share it in case someone find it useful.
> > > > >
> > > > > Let's drop it.
> > > >
> > > > Out of interest, to we know why the test is failing for Lorenzo?
> > >
> > > I really don't know, but, it sounds like ktap was not found?
> >
> > Yeah CONFIG_KUNIT is not set so could be :)
>
> Nah, CONFIG_KUNIT has nothing to do with ktap_helpers.sh, probably your
> environment does not bring in tools/testing/selftests/kselftest
Yeah I copy the mm tests to /mnt in vng before running to avoid ro
filesystem issues.
Not a fan of us relying on the entire tree structure being there either
then :)
Cheers, Lorenzo
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-03-17 8:45 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-09 12:00 [PATCH] selftests/mm: add THP sysfs interface test Breno Leitao
2026-03-16 12:55 ` Lorenzo Stoakes (Oracle)
2026-03-16 13:47 ` Breno Leitao
2026-03-16 14:44 ` David Hildenbrand (Arm)
2026-03-16 16:02 ` Breno Leitao
2026-03-16 19:53 ` Lorenzo Stoakes (Oracle)
2026-03-17 5:43 ` Mike Rapoport
2026-03-17 8:45 ` Lorenzo Stoakes (Oracle)
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox