* [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint
@ 2026-07-21 9:23 Yaxiong Tian
2026-07-21 9:25 ` [PATCH 1/5] pm: qos: add notifiers for CPU latency and wakeup latency QoS Yaxiong Tian
` (4 more replies)
0 siblings, 5 replies; 8+ messages in thread
From: Yaxiong Tian @ 2026-07-21 9:23 UTC (permalink / raw)
To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah
Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian
cpuidle_governor_latency_req() is evaluated on every idle-state
selection. It aggregates the per-CPU resume latency with the global
CPU latency and wakeup latency QoS limits by repeatedly calling
get_cpu_device() and pm_qos_read_value() cpu_latency_qos_limit()
cpu_wakeup_latency_qos_limit().
Use ftrace's function_graph, we can see:
parent: do_idle
parent_total_ns: 36671010505
parent_count: 5994
SYMBOL TIME_NS %ROOT %PARENT COUNT
--------------------------------------------------------------------------------------------------
do_idle 36671010505 100.00% 100.00% 5994
cpuidle_idle_call 35566528731 96.99% 96.99% 8570
call_cpuidle 35476606844 96.74% 99.75% 8561
cpuidle_enter 35472932468 96.73% 99.99% 8526
cpuidle_select 52097031 0.14% 0.15% 8580
menu_select 49555181 0.14% 95.12% 8580
tick_nohz_get_sleep_length 28843887 0.08% 58.21% 8570
cpuidle_governor_latency_req 9567488 0.03% 19.31% 8580
tick_nohz_tick_stopped 2057031 0.01% 4.15% 15695
cpuidle_reflect 11427201 0.03% 0.03% 8561
menu_reflect 6579506 0.02% 57.58% 8526
tick_nohz_idle_got_tick 2231559 0.01% 33.92% 8526
__sysvec_apic_timer_interrupt 105520 0.00% 0.92% 3
tick_nohz_idle_stop_tick 8279641 0.02% 0.02% 1475
---- skip
The majority of the time spent in cpuidle_enter for CPUs entering
idle state has already been charged to the idle path. Among the
remaining contributors, cpuidle_governor_latency_req() accounts
for a non-negligible portion of the overall latency.
Under the menu governor this shows up hot: ftrace data shows,
cpuidle_governor_latency_req() accounts for about 19.9% of
menu_select() time (~1.9 us/call). After caching the aggregated
value per CPU and invalidating via QoS notifiers, that share drops to
about 4.2% (~0.3 us/call), roughly a 6x reduction on this path.
The ftrace data before and after the optimization is shown below:
1) original
parent: menu_select
parent_total_ns: 160492937
parent_count: 16718
SYMBOL TIME_NS %ROOT %PARENT COUNT
--------------------------------------------------------------------------------------------------
menu_select 160492937 100.00% 100.00% 16718
tick_nohz_get_sleep_length 100262940 62.47% 62.47% 16698
tick_nohz_next_event 67891299 42.30% 67.71% 16689
rcu_needs_cpu 2825649 1.76% 4.16% 16689
timekeeping_max_deferment 2380377 1.48% 3.51% 15296
hrtimer_next_event_without 17865162 11.13% 17.82% 15296
hrtimer_bases_next_event_without 2707631 1.69% 15.16% 15296
_raw_spin_lock_irqsave 2461132 1.53% 13.78% 15296
native_queued_spin_lock_slowpath 177 0.00% 0.01% 1
_raw_spin_unlock_irqrestore 2364647 1.47% 13.24% 15296
can_stop_idle_tick 2906072 1.81% 2.90% 16698
cpuidle_governor_latency_req 31988150 19.93% 19.93% 16718
get_cpu_device 4122502 2.57% 12.89% 16718
pm_qos_read_value 3427318 2.14% 10.71% 16718
cpu_latency_qos_limit 3005804 1.87% 9.40% 16718
cpu_wakeup_latency_qos_limit 3005475 1.87% 9.40% 16718
tick_nohz_tick_stopped 4551981 2.84% 2.84% 29496
%ROOT = share of menu_select; %PARENT = share of immediate caller (inclusive)
2) post-optimized
parent: menu_select
parent_total_ns: 55428604
parent_count: 7626
SYMBOL TIME_NS %ROOT %PARENT COUNT
--------------------------------------------------------------------------------------------------
menu_select 55428604 100.00% 100.00% 7626
tick_nohz_get_sleep_length 37464607 67.59% 67.59% 7544
tick_nohz_next_event 24076913 43.44% 64.27% 7522
get_next_timer_interrupt 16332381 29.47% 67.83% 5854
rcu_needs_cpu 1489633 2.69% 6.19% 7522
timekeeping_max_deferment 870851 1.57% 3.62% 5586
hrtimer_next_event_without 6140153 11.08% 16.39% 5586
hrtimer_bases_next_event_without 979000 1.77% 15.94% 5586
_raw_spin_lock_irqsave 808786 1.46% 13.17% 5586
_raw_spin_unlock_irqrestore 785518 1.42% 12.79% 5586
can_stop_idle_tick 1583137 2.86% 4.23% 7544
cpuidle_governor_latency_req 2321119 4.19% 4.19% 7626
tick_nohz_tick_stopped 1863015 3.36% 3.36% 12751
%ROOT = share of menu_select; %PARENT = share of immediate caller (inclusive)
A self-test case is introduced in patch 5 to validate that the existing
functionality remains intact. This can be handled as a standalone task.
This series:
- adds notifier APIs for the global CPU/wakeup latency QoS
- lets cpuidle subscribe and maintain a per-CPU generation
- invalidates only the affected CPU on per-CPU resume latency
changes
- caches the aggregated constraint in cpuidle_governor_latency_req()
- adds a kselftest covering the three QoS input paths
Yaxiong Tian (5):
pm: qos: add notifiers for CPU latency and wakeup latency QoS
cpuidle: subscribe to global latency QoS notifiers
cpuidle: invalidate latency gen on per-CPU resume QoS changes
cpuidle: cache aggregated governor latency QoS constraint
selftests/cpuidle: add latency_req QoS idle-state selection test
drivers/cpuidle/cpuidle.c | 15 +-
drivers/cpuidle/cpuidle.h | 2 +
drivers/cpuidle/governor.c | 141 +++++++-
include/linux/pm_qos.h | 23 ++
kernel/power/qos.c | 50 +++
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 +
10 files changed, 566 insertions(+), 8 deletions(-)
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
--
2.43.0
^ permalink raw reply [flat|nested] 8+ messages in thread* [PATCH 1/5] pm: qos: add notifiers for CPU latency and wakeup latency QoS 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian @ 2026-07-21 9:25 ` Yaxiong Tian 2026-07-21 9:25 ` [PATCH 2/5] cpuidle: subscribe to global latency QoS notifiers Yaxiong Tian ` (3 subsequent siblings) 4 siblings, 0 replies; 8+ messages in thread From: Yaxiong Tian @ 2026-07-21 9:25 UTC (permalink / raw) To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian Expose notifier registration for the system-wide CPU latency and wakeup latency QoS constraints so listeners can react when the aggregate target value changes. pm_qos_update_target() already invokes constraint notifiers when the effective value changes. Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> --- include/linux/pm_qos.h | 23 +++++++++++++++++++ kernel/power/qos.c | 50 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/include/linux/pm_qos.h b/include/linux/pm_qos.h index 6cea4455f867..3759b453c6d6 100644 --- a/include/linux/pm_qos.h +++ b/include/linux/pm_qos.h @@ -149,6 +149,8 @@ bool cpu_latency_qos_request_active(struct pm_qos_request *req); void cpu_latency_qos_add_request(struct pm_qos_request *req, s32 value); void cpu_latency_qos_update_request(struct pm_qos_request *req, s32 new_value); void cpu_latency_qos_remove_request(struct pm_qos_request *req); +int cpu_latency_qos_add_notifier(struct notifier_block *notifier); +int cpu_latency_qos_remove_notifier(struct notifier_block *notifier); #else static inline s32 cpu_latency_qos_limit(void) { return INT_MAX; } static inline bool cpu_latency_qos_request_active(struct pm_qos_request *req) @@ -160,15 +162,36 @@ static inline void cpu_latency_qos_add_request(struct pm_qos_request *req, static inline void cpu_latency_qos_update_request(struct pm_qos_request *req, s32 new_value) {} static inline void cpu_latency_qos_remove_request(struct pm_qos_request *req) {} +static inline int cpu_latency_qos_add_notifier(struct notifier_block *notifier) +{ + return 0; +} +static inline int +cpu_latency_qos_remove_notifier(struct notifier_block *notifier) +{ + return 0; +} #endif #ifdef CONFIG_PM_QOS_CPU_SYSTEM_WAKEUP s32 cpu_wakeup_latency_qos_limit(void); +int cpu_wakeup_latency_qos_add_notifier(struct notifier_block *notifier); +int cpu_wakeup_latency_qos_remove_notifier(struct notifier_block *notifier); #else static inline s32 cpu_wakeup_latency_qos_limit(void) { return PM_QOS_RESUME_LATENCY_NO_CONSTRAINT; } +static inline int +cpu_wakeup_latency_qos_add_notifier(struct notifier_block *notifier) +{ + return 0; +} +static inline int +cpu_wakeup_latency_qos_remove_notifier(struct notifier_block *notifier) +{ + return 0; +} #endif #ifdef CONFIG_PM diff --git a/kernel/power/qos.c b/kernel/power/qos.c index 1944dbeb0d4c..392ab3dbb923 100644 --- a/kernel/power/qos.c +++ b/kernel/power/qos.c @@ -212,12 +212,15 @@ bool pm_qos_update_flags(struct pm_qos_flags *pqf, #ifdef CONFIG_CPU_IDLE /* Definitions related to the CPU latency QoS. */ +static BLOCKING_NOTIFIER_HEAD(cpu_latency_qos_notifiers); + static struct pm_qos_constraints cpu_latency_constraints = { .list = PLIST_HEAD_INIT(cpu_latency_constraints.list), .target_value = PM_QOS_CPU_LATENCY_DEFAULT_VALUE, .default_value = PM_QOS_CPU_LATENCY_DEFAULT_VALUE, .no_constraint_value = PM_QOS_CPU_LATENCY_DEFAULT_VALUE, .type = PM_QOS_MIN, + .notifiers = &cpu_latency_qos_notifiers, }; static inline bool cpu_latency_qos_value_invalid(s32 value) @@ -335,6 +338,28 @@ void cpu_latency_qos_remove_request(struct pm_qos_request *req) } EXPORT_SYMBOL_GPL(cpu_latency_qos_remove_request); +/** + * cpu_latency_qos_add_notifier - Add CPU latency QoS change notifier. + * @notifier: Notifier block managed by the caller. + */ +int cpu_latency_qos_add_notifier(struct notifier_block *notifier) +{ + return blocking_notifier_chain_register(&cpu_latency_qos_notifiers, + notifier); +} +EXPORT_SYMBOL_GPL(cpu_latency_qos_add_notifier); + +/** + * cpu_latency_qos_remove_notifier - Remove CPU latency QoS change notifier. + * @notifier: Notifier block previously registered. + */ +int cpu_latency_qos_remove_notifier(struct notifier_block *notifier) +{ + return blocking_notifier_chain_unregister(&cpu_latency_qos_notifiers, + notifier); +} +EXPORT_SYMBOL_GPL(cpu_latency_qos_remove_notifier); + /* User space interface to the CPU latency QoS via misc device. */ static int cpu_latency_qos_open(struct inode *inode, struct file *filp) @@ -417,12 +442,15 @@ static struct miscdevice cpu_latency_qos_miscdev = { #ifdef CONFIG_PM_QOS_CPU_SYSTEM_WAKEUP /* The CPU system wakeup latency QoS. */ +static BLOCKING_NOTIFIER_HEAD(cpu_wakeup_latency_qos_notifiers); + static struct pm_qos_constraints cpu_wakeup_latency_constraints = { .list = PLIST_HEAD_INIT(cpu_wakeup_latency_constraints.list), .target_value = PM_QOS_RESUME_LATENCY_NO_CONSTRAINT, .default_value = PM_QOS_RESUME_LATENCY_NO_CONSTRAINT, .no_constraint_value = PM_QOS_RESUME_LATENCY_NO_CONSTRAINT, .type = PM_QOS_MIN, + .notifiers = &cpu_wakeup_latency_qos_notifiers, }; /** @@ -436,6 +464,28 @@ s32 cpu_wakeup_latency_qos_limit(void) return pm_qos_read_value(&cpu_wakeup_latency_constraints); } +/** + * cpu_wakeup_latency_qos_add_notifier - Add wakeup latency QoS notifier. + * @notifier: Notifier block managed by the caller. + */ +int cpu_wakeup_latency_qos_add_notifier(struct notifier_block *notifier) +{ + return blocking_notifier_chain_register(&cpu_wakeup_latency_qos_notifiers, + notifier); +} +EXPORT_SYMBOL_GPL(cpu_wakeup_latency_qos_add_notifier); + +/** + * cpu_wakeup_latency_qos_remove_notifier - Remove wakeup latency QoS notifier. + * @notifier: Notifier block previously registered. + */ +int cpu_wakeup_latency_qos_remove_notifier(struct notifier_block *notifier) +{ + return blocking_notifier_chain_unregister(&cpu_wakeup_latency_qos_notifiers, + notifier); +} +EXPORT_SYMBOL_GPL(cpu_wakeup_latency_qos_remove_notifier); + static int cpu_wakeup_latency_qos_open(struct inode *inode, struct file *filp) { struct pm_qos_request *req; -- 2.43.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 2/5] cpuidle: subscribe to global latency QoS notifiers 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian 2026-07-21 9:25 ` [PATCH 1/5] pm: qos: add notifiers for CPU latency and wakeup latency QoS Yaxiong Tian @ 2026-07-21 9:25 ` Yaxiong Tian 2026-07-21 9:25 ` [PATCH 3/5] cpuidle: invalidate latency gen on per-CPU resume QoS changes Yaxiong Tian ` (2 subsequent siblings) 4 siblings, 0 replies; 8+ messages in thread From: Yaxiong Tian @ 2026-07-21 9:25 UTC (permalink / raw) To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian Register for CPU latency and wakeup latency QoS notifications and bump a per-CPU generation counter when either aggregate constraint changes. This prepares for caching the governor latency requirement without introducing a direct qos-to-cpuidle call dependency. Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> --- drivers/cpuidle/governor.c | 58 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/drivers/cpuidle/governor.c b/drivers/cpuidle/governor.c index 5d0e7f78c6c5..bc4a70d30c34 100644 --- a/drivers/cpuidle/governor.c +++ b/drivers/cpuidle/governor.c @@ -8,10 +8,13 @@ * This code is licenced under the GPL. */ +#include <linux/atomic.h> #include <linux/cpu.h> #include <linux/cpuidle.h> +#include <linux/init.h> #include <linux/mutex.h> #include <linux/module.h> +#include <linux/notifier.h> #include <linux/pm_qos.h> #include "cpuidle.h" @@ -22,6 +25,61 @@ LIST_HEAD(cpuidle_governors); struct cpuidle_governor *cpuidle_curr_governor; struct cpuidle_governor *cpuidle_prev_governor; +/* + * Per-CPU generation bumped to invalidate that CPU's cached latency + * constraint. Consumers of the generation are added in later changes. + */ +static DEFINE_PER_CPU(atomic_t, latency_req_gen); + +static void cpuidle_latency_req_invalidate_cpu(unsigned int cpu) +{ + atomic_inc(per_cpu_ptr(&latency_req_gen, cpu)); +} + +static void cpuidle_latency_req_invalidate_all(void) +{ + unsigned int cpu; + + for_each_possible_cpu(cpu) + cpuidle_latency_req_invalidate_cpu(cpu); +} + +static int cpuidle_global_qos_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + cpuidle_latency_req_invalidate_all(); + return NOTIFY_OK; +} + +static struct notifier_block cpuidle_latency_qos_nb = { + .notifier_call = cpuidle_global_qos_notify, +}; + +#ifdef CONFIG_PM_QOS_CPU_SYSTEM_WAKEUP +static struct notifier_block cpuidle_wakeup_qos_nb = { + .notifier_call = cpuidle_global_qos_notify, +}; +#endif + +static int __init cpuidle_latency_req_init(void) +{ + int ret; + + ret = cpu_latency_qos_add_notifier(&cpuidle_latency_qos_nb); + if (ret) + return ret; + +#ifdef CONFIG_PM_QOS_CPU_SYSTEM_WAKEUP + ret = cpu_wakeup_latency_qos_add_notifier(&cpuidle_wakeup_qos_nb); + if (ret) { + cpu_latency_qos_remove_notifier(&cpuidle_latency_qos_nb); + return ret; + } +#endif + return 0; +} +core_initcall(cpuidle_latency_req_init); + /** * cpuidle_find_governor - finds a governor of the specified name * @str: the name -- 2.43.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 3/5] cpuidle: invalidate latency gen on per-CPU resume QoS changes 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian 2026-07-21 9:25 ` [PATCH 1/5] pm: qos: add notifiers for CPU latency and wakeup latency QoS Yaxiong Tian 2026-07-21 9:25 ` [PATCH 2/5] cpuidle: subscribe to global latency QoS notifiers Yaxiong Tian @ 2026-07-21 9:25 ` Yaxiong Tian 2026-07-21 9:25 ` [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint Yaxiong Tian 2026-07-21 9:26 ` [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test Yaxiong Tian 4 siblings, 0 replies; 8+ messages in thread From: Yaxiong Tian @ 2026-07-21 9:25 UTC (permalink / raw) To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian Register a DEV_PM_QOS_RESUME_LATENCY notifier for each CPU device and bump only that CPU's latency_req generation when its resume latency constraint changes. Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> --- drivers/cpuidle/cpuidle.c | 15 +++++++++--- drivers/cpuidle/cpuidle.h | 2 ++ drivers/cpuidle/governor.c | 48 +++++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/drivers/cpuidle/cpuidle.c b/drivers/cpuidle/cpuidle.c index 2d2f40a2cb81..9495f904fe85 100644 --- a/drivers/cpuidle/cpuidle.c +++ b/drivers/cpuidle/cpuidle.c @@ -613,6 +613,8 @@ static void __cpuidle_unregister_device(struct cpuidle_device *dev) { struct cpuidle_driver *drv = cpuidle_get_cpu_driver(dev); + cpuidle_latency_req_notifier_unregister(dev->cpu); + list_del(&dev->device_list); per_cpu(cpuidle_devices, dev->cpu) = NULL; module_put(drv->owner); @@ -661,10 +663,17 @@ static int __cpuidle_register_device(struct cpuidle_device *dev) ret = cpuidle_coupled_register_device(dev); if (ret) - __cpuidle_unregister_device(dev); - else - dev->registered = 1; + goto unreg; + + ret = cpuidle_latency_req_notifier_register(cpu); + if (ret) + goto unreg; + dev->registered = 1; + return 0; + +unreg: + __cpuidle_unregister_device(dev); return ret; } diff --git a/drivers/cpuidle/cpuidle.h b/drivers/cpuidle/cpuidle.h index 52701d9588f1..151fa9ebe483 100644 --- a/drivers/cpuidle/cpuidle.h +++ b/drivers/cpuidle/cpuidle.h @@ -25,6 +25,8 @@ extern void cpuidle_uninstall_idle_handler(void); /* governors */ extern struct cpuidle_governor *cpuidle_find_governor(const char *str); extern int cpuidle_switch_governor(struct cpuidle_governor *gov); +int cpuidle_latency_req_notifier_register(unsigned int cpu); +void cpuidle_latency_req_notifier_unregister(unsigned int cpu); /* sysfs */ diff --git a/drivers/cpuidle/governor.c b/drivers/cpuidle/governor.c index bc4a70d30c34..d286ccf19a69 100644 --- a/drivers/cpuidle/governor.c +++ b/drivers/cpuidle/governor.c @@ -27,10 +27,18 @@ struct cpuidle_governor *cpuidle_prev_governor; /* * Per-CPU generation bumped to invalidate that CPU's cached latency - * constraint. Consumers of the generation are added in later changes. + * constraint. Global QoS changes invalidate every CPU; per-CPU resume + * latency changes invalidate only the affected CPU. */ static DEFINE_PER_CPU(atomic_t, latency_req_gen); +struct cpuidle_cpu_qos_nb { + struct notifier_block nb; + unsigned int cpu; +}; + +static DEFINE_PER_CPU(struct cpuidle_cpu_qos_nb, cpuidle_cpu_qos_nb); + static void cpuidle_latency_req_invalidate_cpu(unsigned int cpu) { atomic_inc(per_cpu_ptr(&latency_req_gen, cpu)); @@ -61,6 +69,44 @@ static struct notifier_block cpuidle_wakeup_qos_nb = { }; #endif +static int cpuidle_cpu_qos_notify(struct notifier_block *nb, + unsigned long action, void *data) +{ + struct cpuidle_cpu_qos_nb *qos_nb = + container_of(nb, struct cpuidle_cpu_qos_nb, nb); + + cpuidle_latency_req_invalidate_cpu(qos_nb->cpu); + return NOTIFY_OK; +} + +int cpuidle_latency_req_notifier_register(unsigned int cpu) +{ + struct device *device = get_cpu_device(cpu); + struct cpuidle_cpu_qos_nb *qos_nb = + per_cpu_ptr(&cpuidle_cpu_qos_nb, cpu); + + if (!device) + return -ENODEV; + + qos_nb->cpu = cpu; + qos_nb->nb.notifier_call = cpuidle_cpu_qos_notify; + return dev_pm_qos_add_notifier(device, &qos_nb->nb, + DEV_PM_QOS_RESUME_LATENCY); +} + +void cpuidle_latency_req_notifier_unregister(unsigned int cpu) +{ + struct device *device = get_cpu_device(cpu); + struct cpuidle_cpu_qos_nb *qos_nb = + per_cpu_ptr(&cpuidle_cpu_qos_nb, cpu); + + if (!device) + return; + + dev_pm_qos_remove_notifier(device, &qos_nb->nb, + DEV_PM_QOS_RESUME_LATENCY); +} + static int __init cpuidle_latency_req_init(void) { int ret; -- 2.43.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian ` (2 preceding siblings ...) 2026-07-21 9:25 ` [PATCH 3/5] cpuidle: invalidate latency gen on per-CPU resume QoS changes Yaxiong Tian @ 2026-07-21 9:25 ` Yaxiong Tian 2026-07-23 12:44 ` Christian Loehle 2026-07-21 9:26 ` [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test Yaxiong Tian 4 siblings, 1 reply; 8+ messages in thread From: Yaxiong Tian @ 2026-07-21 9:25 UTC (permalink / raw) To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian cpuidle_governor_latency_req() runs on every idle-state selection and aggregates per-CPU resume latency with the global CPU latency and wakeup latency QoS limits. That path repeatedly walks get_cpu_device() and pm_qos_read_value(), which shows up hot under menu_select(). Cache the aggregated constraint per CPU and refresh it only when the corresponding per-CPU generation changes. The generation is already bumped by the global and per-CPU resume latency QoS notifiers added earlier in this series. On a menu governor profile, cpuidle_governor_latency_req() drops from about 19.9% of menu_select time to about 4.2%, roughly a 6x reduction in cost per call (~1.9 us down to ~0.3 us). Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> --- drivers/cpuidle/governor.c | 37 ++++++++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/drivers/cpuidle/governor.c b/drivers/cpuidle/governor.c index d286ccf19a69..67909470c14b 100644 --- a/drivers/cpuidle/governor.c +++ b/drivers/cpuidle/governor.c @@ -32,11 +32,17 @@ struct cpuidle_governor *cpuidle_prev_governor; */ static DEFINE_PER_CPU(atomic_t, latency_req_gen); +struct cpuidle_latency_req_cache { + unsigned int gen; + s64 latency_ns; +}; + struct cpuidle_cpu_qos_nb { struct notifier_block nb; unsigned int cpu; }; +static DEFINE_PER_CPU(struct cpuidle_latency_req_cache, latency_req_cache); static DEFINE_PER_CPU(struct cpuidle_cpu_qos_nb, cpuidle_cpu_qos_nb); static void cpuidle_latency_req_invalidate_cpu(unsigned int cpu) @@ -212,10 +218,22 @@ int cpuidle_register_governor(struct cpuidle_governor *gov) */ s64 cpuidle_governor_latency_req(unsigned int cpu) { - struct device *device = get_cpu_device(cpu); - int device_req = dev_pm_qos_raw_resume_latency(device); - int global_req = cpu_latency_qos_limit(); - int global_wake_req = cpu_wakeup_latency_qos_limit(); + struct cpuidle_latency_req_cache *cache; + unsigned int gen; + struct device *device; + int device_req, global_req, global_wake_req; + s64 latency_ns; + + cache = per_cpu_ptr(&latency_req_cache, cpu); + gen = atomic_read(per_cpu_ptr(&latency_req_gen, cpu)); + + if (likely(READ_ONCE(cache->gen) == gen)) + return READ_ONCE(cache->latency_ns); + + device = get_cpu_device(cpu); + device_req = dev_pm_qos_raw_resume_latency(device); + global_req = cpu_latency_qos_limit(); + global_wake_req = cpu_wakeup_latency_qos_limit(); if (global_req > global_wake_req) global_req = global_wake_req; @@ -223,5 +241,14 @@ s64 cpuidle_governor_latency_req(unsigned int cpu) if (device_req > global_req) device_req = global_req; - return (s64)device_req * NSEC_PER_USEC; + latency_ns = (s64)device_req * NSEC_PER_USEC; + + WRITE_ONCE(cache->latency_ns, latency_ns); + /* + * Store gen last so a concurrent invalidate cannot leave a stale + * latency_ns marked as current. + */ + WRITE_ONCE(cache->gen, gen); + + return latency_ns; } -- 2.43.0 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint 2026-07-21 9:25 ` [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint Yaxiong Tian @ 2026-07-23 12:44 ` Christian Loehle 0 siblings, 0 replies; 8+ messages in thread From: Christian Loehle @ 2026-07-23 12:44 UTC (permalink / raw) To: Yaxiong Tian, rafael, daniel.lezcano, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest On 7/21/26 10:25, Yaxiong Tian wrote: > cpuidle_governor_latency_req() runs on every idle-state selection and > aggregates per-CPU resume latency with the global CPU latency and > wakeup latency QoS limits. That path repeatedly walks get_cpu_device() > and pm_qos_read_value(), which shows up hot under menu_select(). > > Cache the aggregated constraint per CPU and refresh it only when the > corresponding per-CPU generation changes. The generation is already > bumped by the global and per-CPU resume latency QoS notifiers added > earlier in this series. > > On a menu governor profile, cpuidle_governor_latency_req() drops from > about 19.9% of menu_select time to about 4.2%, roughly a 6x reduction > in cost per call (~1.9 us down to ~0.3 us). > > Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> > --- > drivers/cpuidle/governor.c | 37 ++++++++++++++++++++++++++++++++----- > 1 file changed, 32 insertions(+), 5 deletions(-) > > diff --git a/drivers/cpuidle/governor.c b/drivers/cpuidle/governor.c > index d286ccf19a69..67909470c14b 100644 > --- a/drivers/cpuidle/governor.c > +++ b/drivers/cpuidle/governor.c > @@ -32,11 +32,17 @@ struct cpuidle_governor *cpuidle_prev_governor; > */ > static DEFINE_PER_CPU(atomic_t, latency_req_gen); > > +struct cpuidle_latency_req_cache { > + unsigned int gen; > + s64 latency_ns; > +}; > + > struct cpuidle_cpu_qos_nb { > struct notifier_block nb; > unsigned int cpu; > }; > > +static DEFINE_PER_CPU(struct cpuidle_latency_req_cache, latency_req_cache); So this will initialize with latency_req_cache.gen = 0 and latency.req_cache.latency_ns = 0... Come to think of it maybe a no-req run is also good as the first and last selftest case. > static DEFINE_PER_CPU(struct cpuidle_cpu_qos_nb, cpuidle_cpu_qos_nb); > > static void cpuidle_latency_req_invalidate_cpu(unsigned int cpu) > @@ -212,10 +218,22 @@ int cpuidle_register_governor(struct cpuidle_governor *gov) > */ > s64 cpuidle_governor_latency_req(unsigned int cpu) > { > - struct device *device = get_cpu_device(cpu); > - int device_req = dev_pm_qos_raw_resume_latency(device); > - int global_req = cpu_latency_qos_limit(); > - int global_wake_req = cpu_wakeup_latency_qos_limit(); > + struct cpuidle_latency_req_cache *cache; > + unsigned int gen; > + struct device *device; > + int device_req, global_req, global_wake_req; > + s64 latency_ns; > + > + cache = per_cpu_ptr(&latency_req_cache, cpu); > + gen = atomic_read(per_cpu_ptr(&latency_req_gen, cpu)); > + > + if (likely(READ_ONCE(cache->gen) == gen)) > + return READ_ONCE(cache->latency_ns); > + > + device = get_cpu_device(cpu); > + device_req = dev_pm_qos_raw_resume_latency(device); > + global_req = cpu_latency_qos_limit(); > + global_wake_req = cpu_wakeup_latency_qos_limit(); > > if (global_req > global_wake_req) > global_req = global_wake_req; > @@ -223,5 +241,14 @@ s64 cpuidle_governor_latency_req(unsigned int cpu) > if (device_req > global_req) > device_req = global_req; > > - return (s64)device_req * NSEC_PER_USEC; > + latency_ns = (s64)device_req * NSEC_PER_USEC; > + > + WRITE_ONCE(cache->latency_ns, latency_ns); > + /* > + * Store gen last so a concurrent invalidate cannot leave a stale > + * latency_ns marked as current. > + */ This doesn't guarantee the ordering. > + WRITE_ONCE(cache->gen, gen); > + > + return latency_ns; > } ^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian ` (3 preceding siblings ...) 2026-07-21 9:25 ` [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint Yaxiong Tian @ 2026-07-21 9:26 ` Yaxiong Tian 2026-07-23 12:51 ` Christian Loehle 4 siblings, 1 reply; 8+ messages in thread From: Yaxiong Tian @ 2026-07-21 9:26 UTC (permalink / raw) To: rafael, daniel.lezcano, christian.loehle, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest, Yaxiong Tian 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 <tianyaxiong@kylinos.cn> --- 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 ^ permalink raw reply related [flat|nested] 8+ messages in thread
* Re: [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test 2026-07-21 9:26 ` [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test Yaxiong Tian @ 2026-07-23 12:51 ` Christian Loehle 0 siblings, 0 replies; 8+ messages in thread From: Christian Loehle @ 2026-07-23 12:51 UTC (permalink / raw) To: Yaxiong Tian, rafael, daniel.lezcano, lenb, pavel, shuah Cc: linux-kernel, linux-pm, linux-kselftest On 7/21/26 10:26, Yaxiong Tian wrote: > 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 FWIW since you're mostly there anyway, can you extend this to cover cpuidle state disable too? Ideally also interaction between disable and latency_req. I think that would be quite useful. > > Signed-off-by: Yaxiong Tian <tianyaxiong@kylinos.cn> > --- > 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 See pm_qos_resume_latency_us_store(), n/a and 0 are distinct. > + 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 ^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-07-23 12:52 UTC | newest] Thread overview: 8+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-07-21 9:23 [PATCH 0/5] cpuidle: speed up do_idle() by caching the governor latency QoS constraint Yaxiong Tian 2026-07-21 9:25 ` [PATCH 1/5] pm: qos: add notifiers for CPU latency and wakeup latency QoS Yaxiong Tian 2026-07-21 9:25 ` [PATCH 2/5] cpuidle: subscribe to global latency QoS notifiers Yaxiong Tian 2026-07-21 9:25 ` [PATCH 3/5] cpuidle: invalidate latency gen on per-CPU resume QoS changes Yaxiong Tian 2026-07-21 9:25 ` [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint Yaxiong Tian 2026-07-23 12:44 ` Christian Loehle 2026-07-21 9:26 ` [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test Yaxiong Tian 2026-07-23 12:51 ` Christian Loehle
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox