Verify that CPU latency, wakeup latency, and per-CPU resume latency
QoS ceilings prevent governors from selecting idle states whose exit
latency exceeds the constraint, by comparing cpuidle state usage
deltas under each QoS path.

USE:
sudo make -C tools/testing/selftests TARGETS=cpuidle run_tests

Signed-off-by: Yaxiong Tian <[email protected]>
---
 tools/testing/selftests/Makefile              |   1 +
 tools/testing/selftests/cpuidle/Makefile      |   6 +
 tools/testing/selftests/cpuidle/config        |   3 +
 .../cpuidle/cpuidle_latency_req_qos.py        | 331 ++++++++++++++++++
 tools/testing/selftests/cpuidle/settings      |   2 +
 5 files changed, 343 insertions(+)
 create mode 100644 tools/testing/selftests/cpuidle/Makefile
 create mode 100644 tools/testing/selftests/cpuidle/config
 create mode 100755 tools/testing/selftests/cpuidle/cpuidle_latency_req_qos.py
 create mode 100644 tools/testing/selftests/cpuidle/settings

diff --git a/tools/testing/selftests/Makefile b/tools/testing/selftests/Makefile
index 8189f333814c..a8fb620bebd1 100644
--- a/tools/testing/selftests/Makefile
+++ b/tools/testing/selftests/Makefile
@@ -12,6 +12,7 @@ TARGETS += clone3
 TARGETS += connector
 TARGETS += core
 TARGETS += cpufreq
+TARGETS += cpuidle
 TARGETS += cpu-hotplug
 TARGETS += damon
 TARGETS += devices/error_logs
diff --git a/tools/testing/selftests/cpuidle/Makefile 
b/tools/testing/selftests/cpuidle/Makefile
new file mode 100644
index 000000000000..f960d72c3a65
--- /dev/null
+++ b/tools/testing/selftests/cpuidle/Makefile
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0
+all:
+
+TEST_PROGS := cpuidle_latency_req_qos.py
+
+include ../lib.mk
diff --git a/tools/testing/selftests/cpuidle/config 
b/tools/testing/selftests/cpuidle/config
new file mode 100644
index 000000000000..86e8f87d4e62
--- /dev/null
+++ b/tools/testing/selftests/cpuidle/config
@@ -0,0 +1,3 @@
+CONFIG_CPU_IDLE=y
+CONFIG_CPU_IDLE_GOV_MENU=y
+CONFIG_PM_QOS_CPU_SYSTEM_WAKEUP=y
diff --git a/tools/testing/selftests/cpuidle/cpuidle_latency_req_qos.py 
b/tools/testing/selftests/cpuidle/cpuidle_latency_req_qos.py
new file mode 100755
index 000000000000..875b623da530
--- /dev/null
+++ b/tools/testing/selftests/cpuidle/cpuidle_latency_req_qos.py
@@ -0,0 +1,331 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""
+cpuidle: verify latency_req QoS ceilings restrict idle-state selection.
+
+Constrains exit latency via each of the three QoS inputs and checks that
+cpuidle states whose exit latency exceeds the ceiling do not gain usage:
+
+  1) /dev/cpu_dma_latency
+  2) /dev/cpu_wakeup_latency
+  3) /sys/devices/system/cpu/cpuN/power/pm_qos_resume_latency_us
+"""
+
+from __future__ import annotations
+
+import glob
+import os
+import struct
+import sys
+import time
+from dataclasses import dataclass
+from typing import Dict, List, Optional, Tuple
+
+# Source tree: tools/testing/selftests/cpuidle/../kselftest
+# Install tree: kselftest_install/cpuidle/../kselftest
+sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)),
+                               "..", "kselftest"))
+import ksft
+
+
+CPUIDLE_BASE = "/sys/devices/system/cpu"
+DMA_LAT_DEV = "/dev/cpu_dma_latency"
+WAKEUP_LAT_DEV = "/dev/cpu_wakeup_latency"
+
+# Keep windows short so the whole collection fits under settings timeout.
+IDLE_SEC = 2.0
+CPU = 0
+
+
+@dataclass
+class IdleState:
+       index: int
+       name: str
+       latency_us: int
+       residency_us: int
+       usage: int
+       time_us: int
+
+
+def read_states(cpu: int) -> Dict[int, IdleState]:
+       base = f"{CPUIDLE_BASE}/cpu{cpu}/cpuidle"
+       states: Dict[int, IdleState] = {}
+       paths = sorted(
+               glob.glob(f"{base}/state*"),
+               key=lambda p: int(os.path.basename(p).replace("state", "")),
+       )
+       for path in paths:
+               idx = int(os.path.basename(path).replace("state", ""))
+               states[idx] = IdleState(
+                       index=idx,
+                       name=open(f"{path}/name").read().strip(),
+                       latency_us=int(open(f"{path}/latency").read()),
+                       residency_us=int(open(f"{path}/residency").read()),
+                       usage=int(open(f"{path}/usage").read()),
+                       time_us=int(open(f"{path}/time").read()),
+               )
+       return states
+
+
+def usage_delta(before: Dict[int, IdleState],
+               after: Dict[int, IdleState]) -> Dict[int, int]:
+       return {i: after[i].usage - before[i].usage for i in before}
+
+
+def pick_ceilings(states: Dict[int, IdleState]) -> List[int]:
+       nonzero = sorted({s.latency_us for s in states.values() if s.latency_us 
> 0})
+       out: List[int] = []
+       if nonzero:
+               out.append(nonzero[0])
+       if len(nonzero) >= 2:
+               mid = (nonzero[0] + nonzero[1]) // 2
+               out.append(mid if mid > nonzero[0] else max(1, nonzero[1] - 1))
+       if len(nonzero) >= 3:
+               out.append((nonzero[1] + nonzero[2]) // 2)
+       # Dedup, keep order
+       seen = set()
+       uniq = []
+       for c in out:
+               if c not in seen and c >= 1:
+                       seen.add(c)
+                       uniq.append(c)
+       return uniq or [1]
+
+
+def idle_on_cpu(seconds: float) -> None:
+       end = time.monotonic() + seconds
+       burn_end = time.monotonic() + min(0.1, seconds / 10)
+       while time.monotonic() < burn_end:
+               pass
+       while time.monotonic() < end:
+               time.sleep(0.05)
+
+
+def allowed_states(states: Dict[int, IdleState], ceiling: int) -> List[int]:
+       return [i for i, s in states.items() if s.latency_us <= ceiling]
+
+
+def forbidden_states(states: Dict[int, IdleState], ceiling: int) -> List[int]:
+       return [i for i, s in states.items() if s.latency_us > ceiling]
+
+
+def fmt_state_list(states: Dict[int, IdleState], idxs: List[int]) -> str:
+       if not idxs:
+               return "(none)"
+       return ", ".join(
+               f"state{i}:{states[i].name}(lat={states[i].latency_us})"
+               for i in idxs
+       )
+
+
+def forbidden_violations(states: Dict[int, IdleState], udelta: Dict[int, int],
+                        ceiling: int) -> List[str]:
+       """States that must not be entered but still gained usage."""
+       bad = []
+       for i in forbidden_states(states, ceiling):
+               if udelta[i] > 0:
+                       s = states[i]
+                       bad.append(
+                               f"state{i}({s.name},lat={s.latency_us}) 
usage+={udelta[i]}"
+                       )
+       return bad
+
+
+def print_usage_table(states: Dict[int, IdleState], udelta: Dict[int, int],
+                     ceiling: int) -> None:
+       ksft.print_msg(
+               f"{'idx':>3} {'name':<12} {'lat':>6} {'d_usage':>8} 
{'expect':>8}"
+       )
+       for i in sorted(states):
+               s = states[i]
+               expect = "allow" if s.latency_us <= ceiling else "forbid"
+               ksft.print_msg(
+                       f"{i:3d} {s.name:<12} {s.latency_us:6d} {udelta[i]:8d} 
{expect:>8}"
+               )
+
+
+class DmaLatencyGuard:
+       def __init__(self, latency_us: int):
+               self.latency_us = latency_us
+               self.fd = -1
+
+       def __enter__(self):
+               self.fd = os.open(DMA_LAT_DEV, os.O_RDWR)
+               os.write(self.fd, struct.pack("i", int(self.latency_us)))
+               return self
+
+       def __exit__(self, *args):
+               if self.fd >= 0:
+                       os.close(self.fd)
+                       self.fd = -1
+
+
+class WakeupLatencyGuard:
+       def __init__(self, latency_us: int):
+               self.latency_us = latency_us
+               self.fd = -1
+
+       def __enter__(self):
+               self.fd = os.open(WAKEUP_LAT_DEV, os.O_RDWR)
+               os.write(self.fd, struct.pack("i", int(self.latency_us)))
+               return self
+
+       def __exit__(self, *args):
+               if self.fd >= 0:
+                       os.close(self.fd)
+                       self.fd = -1
+
+
+class ResumeLatencyGuard:
+       def __init__(self, cpu: int, latency_us: int):
+               self.path = 
f"{CPUIDLE_BASE}/cpu{cpu}/power/pm_qos_resume_latency_us"
+               self.latency_us = latency_us
+               self.prev: Optional[str] = None
+
+       def __enter__(self):
+               self.prev = open(self.path).read().strip()
+               with open(self.path, "w") as f:
+                       f.write(f"{int(self.latency_us)}\n")
+               return self
+
+       def __exit__(self, *args):
+               restore = "0" if self.prev in ("0", "n/a", None) else self.prev
+               with open(self.path, "w") as f:
+                       f.write(f"{restore}\n")
+
+
+def run_case(desc: str, states: Dict[int, IdleState], ceiling: int,
+            guard) -> None:
+       allow = allowed_states(states, ceiling)
+       forbid = forbidden_states(states, ceiling)
+
+       ksft.print_msg(f"=== {desc} ===")
+       ksft.print_msg(f"ceiling={ceiling}us")
+       ksft.print_msg(f"allowed:   {fmt_state_list(states, allow)}")
+       ksft.print_msg(f"forbidden: {fmt_state_list(states, forbid)}")
+
+       with guard:
+               time.sleep(0.05)
+               before = read_states(CPU)
+               idle_on_cpu(IDLE_SEC)
+               after = read_states(CPU)
+
+       ud = usage_delta(before, after)
+       total = sum(ud.values())
+       violations = forbidden_violations(states, ud, ceiling)
+
+       print_usage_table(states, ud, ceiling)
+       ksft.print_msg(
+               f"total_usage+={total} "
+               f"violations={violations if violations else 'none'}"
+       )
+
+       if total <= 0:
+               ksft.test_result_fail(f"{desc}: too little idle activity 
({total})")
+               return
+       if violations:
+               ksft.test_result_fail(f"{desc}: {'; '.join(violations)}")
+               return
+       ksft.test_result_pass(desc)
+
+
+def build_plan(states: Dict[int, IdleState],
+              ceilings: List[int]) -> List[Tuple[str, int, object]]:
+       """Return list of (description, ceiling, context-manager factory 
args)."""
+       cases: List[Tuple[str, int, object]] = []
+
+       have_dma = os.path.exists(DMA_LAT_DEV)
+       have_wakeup = os.path.exists(WAKEUP_LAT_DEV)
+       resume_path = f"{CPUIDLE_BASE}/cpu{CPU}/power/pm_qos_resume_latency_us"
+       have_resume = os.path.exists(resume_path)
+
+       for ceiling in ceilings:
+               if have_dma:
+                       cases.append(
+                               (f"cpu_dma_latency ceiling={ceiling}", ceiling,
+                                ("dma", ceiling))
+                       )
+               else:
+                       cases.append(
+                               (f"cpu_dma_latency ceiling={ceiling}", ceiling,
+                                ("skip", "missing /dev/cpu_dma_latency"))
+                       )
+
+               if have_wakeup:
+                       cases.append(
+                               (f"cpu_wakeup_latency ceiling={ceiling}", 
ceiling,
+                                ("wakeup", ceiling))
+                       )
+               else:
+                       cases.append(
+                               (f"cpu_wakeup_latency ceiling={ceiling}", 
ceiling,
+                                ("skip", "missing /dev/cpu_wakeup_latency"))
+                       )
+
+               if have_resume:
+                       cases.append(
+                               (f"pm_qos_resume_latency_us ceiling={ceiling}",
+                                ceiling, ("resume", ceiling))
+                       )
+               else:
+                       cases.append(
+                               (f"pm_qos_resume_latency_us ceiling={ceiling}",
+                                ceiling, ("skip", f"missing {resume_path}"))
+                       )
+
+       return cases
+
+
+def main() -> None:
+       ksft.print_header()
+
+       if os.geteuid() != 0:
+               ksft.set_plan(1)
+               ksft.test_result_skip("must run as root")
+               ksft.finished()
+
+       cpuidle_dir = f"{CPUIDLE_BASE}/cpu{CPU}/cpuidle"
+       if not os.path.isdir(cpuidle_dir):
+               ksft.set_plan(1)
+               ksft.test_result_skip(f"no cpuidle sysfs at {cpuidle_dir}")
+               ksft.finished()
+
+       states = read_states(CPU)
+       if not states:
+               ksft.set_plan(1)
+               ksft.test_result_skip("no cpuidle states")
+               ksft.finished()
+
+       gov_path = f"{CPUIDLE_BASE}/cpuidle/current_governor"
+       gov = open(gov_path).read().strip() if os.path.exists(gov_path) else "?"
+       ksft.print_msg(f"governor={gov} cpu={CPU}")
+       for i in sorted(states):
+               s = states[i]
+               ksft.print_msg(
+                       f"state{i}: {s.name} latency={s.latency_us}us "
+                       f"residency={s.residency_us}us"
+               )
+
+       ceilings = pick_ceilings(states)
+       ksft.print_msg(f"ceilings_us={ceilings}")
+       cases = build_plan(states, ceilings)
+       ksft.set_plan(len(cases))
+
+       for desc, ceiling, kind in cases:
+               tag, arg = kind[0], kind[1]
+               if tag == "skip":
+                       ksft.test_result_skip(f"{desc}: {arg}")
+                       continue
+               if tag == "dma":
+                       guard = DmaLatencyGuard(arg)
+               elif tag == "wakeup":
+                       guard = WakeupLatencyGuard(arg)
+               else:
+                       guard = ResumeLatencyGuard(CPU, arg)
+               run_case(desc, states, ceiling, guard)
+
+       ksft.finished()
+
+
+if __name__ == "__main__":
+       main()
diff --git a/tools/testing/selftests/cpuidle/settings 
b/tools/testing/selftests/cpuidle/settings
new file mode 100644
index 000000000000..5b445e716562
--- /dev/null
+++ b/tools/testing/selftests/cpuidle/settings
@@ -0,0 +1,2 @@
+# Multiple QoS paths x several latency ceilings x idle windows.
+timeout=180
-- 
2.43.0


Reply via email to