mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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; 10+ 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] 10+ 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; 10+ 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	[flat|nested] 10+ 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; 10+ 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	[flat|nested] 10+ 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; 10+ 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	[flat|nested] 10+ 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; 10+ 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	[flat|nested] 10+ 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; 10+ 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	[flat|nested] 10+ 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
  2026-07-24  3:25     ` Yaxiong Tian
  0 siblings, 1 reply; 10+ 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] 10+ 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
  2026-07-24  3:31     ` Yaxiong Tian
  0 siblings, 1 reply; 10+ 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] 10+ messages in thread

* Re: [PATCH 4/5] cpuidle: cache aggregated governor latency QoS constraint
  2026-07-23 12:44   ` Christian Loehle
@ 2026-07-24  3:25     ` Yaxiong Tian
  0 siblings, 0 replies; 10+ messages in thread
From: Yaxiong Tian @ 2026-07-24  3:25 UTC (permalink / raw)
  To: Christian Loehle, rafael, daniel.lezcano, lenb, pavel, shuah
  Cc: linux-kernel, linux-pm, linux-kselftest


在 2026/7/23 20:44, Christian Loehle 写道:
> 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.

Yes, there is a  bug here that will cause the system to stay in the 
shallowest idle state when there is no QoS requirement. My system does 
have QoS requirements, which is why I didn't discover it. I will fix it 
in the next release.

Yes, adding this test case is necessary. However, a 'no-req run' does 
not mean that the system must enter the deepest idle state all the time. 
there are other factors that may restrict it. Let me explain in detail 
how to proceed."
>
>>   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.

Yes, the order is not guaranteed here. My comment was wrong.

However, in the current code, the call path of 
cpuidle_governor_latency_req() is per-CPU, so there is no cross-CPU 
cache access. If we consider multi-core access to this function, there 
would be a penalty on the fast path. I'll optimize the comment.

>
>> +	WRITE_ONCE(cache->gen, gen);
>> +
>> +	return latency_ns;
>>   }

^ permalink raw reply	[flat|nested] 10+ messages in thread

* Re: [PATCH 5/5] selftests/cpuidle: add latency_req QoS idle-state selection test
  2026-07-23 12:51   ` Christian Loehle
@ 2026-07-24  3:31     ` Yaxiong Tian
  0 siblings, 0 replies; 10+ messages in thread
From: Yaxiong Tian @ 2026-07-24  3:31 UTC (permalink / raw)
  To: Christian Loehle, rafael, daniel.lezcano, lenb, pavel, shuah
  Cc: linux-kernel, linux-pm, linux-kselftest


在 2026/7/23 20:51, Christian Loehle 写道:
> 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.
Of course.I'll add it in the next version.
>
>> 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.
Yes, normalizing n/a to 0 is wrong. I will remove this logic.
>
>> +		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] 10+ messages in thread

end of thread, other threads:[~2026-07-24  3:31 UTC | newest]

Thread overview: 10+ 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-24  3:25     ` Yaxiong Tian
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
2026-07-24  3:31     ` Yaxiong Tian

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®