[PATCH 2/3] workqueue: expose per-workqueue stats in debugfs
From: Wilson Felipe Pereira
Date: Thu Oct 22 2026 - 11:57:45 EST
struct pool_workqueue tracks runtime counters in pwq->stats[],
pwq->total_in_flight, and pwq->nr_active, but reading them currently
requires tools/workqueue/wq_monitor.py with drgn and kernel DWARF
debuginfo. This makes it impractical to inspect workqueue stats on
systems without drgn or debug symbols.
Add /sys/kernel/debug/workqueue/stats to expose per-workqueue stats
directly from the kernel. Iterate over the workqueues list under RCU and
sum wq->retired_stats[] with each pwq's stats[], total_in_flight, and
nr_active to report queued, active, and running work items alongside the
cumulative pwq->stats[] counters.
Signed-off-by: Wilson Felipe Pereira <wfelipe@xxxxxxxxxx>
---
kernel/workqueue.c | 99 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 99 insertions(+)
diff --git a/kernel/workqueue.c b/kernel/workqueue.c
index 88c8cc2b38d8..ed0f0be6494a 100644
--- a/kernel/workqueue.c
+++ b/kernel/workqueue.c
@@ -57,6 +57,8 @@
#include <linux/delay.h>
#include <linux/irq_work.h>
#include <linux/math64.h>
+#include <linux/debugfs.h>
+#include <linux/seq_file.h>
#include "workqueue_internal.h"
@@ -7990,6 +7992,103 @@ static void workqueue_sysfs_unregister(struct workqueue_struct *wq)
static void workqueue_sysfs_unregister(struct workqueue_struct *wq) { }
#endif /* CONFIG_SYSFS */
+#ifdef CONFIG_DEBUG_FS
+
+static const char *wq_type_str(struct workqueue_struct *wq)
+{
+ if (wq->flags & WQ_BH)
+ return "bh";
+ if (wq->flags & WQ_UNBOUND) {
+ if (wq->flags & __WQ_ORDERED)
+ return "ordered";
+ return "unbound";
+ }
+ return "percpu";
+}
+
+static void *wq_stats_start(struct seq_file *m, loff_t *pos)
+ __acquires(rcu)
+{
+ rcu_read_lock();
+ return seq_list_start_head_rcu(&workqueues, *pos);
+}
+
+static void *wq_stats_next(struct seq_file *m, void *v, loff_t *pos)
+{
+ return seq_list_next_rcu(v, &workqueues, pos);
+}
+
+static void wq_stats_stop(struct seq_file *m, void *v)
+ __releases(rcu)
+{
+ rcu_read_unlock();
+}
+
+static int wq_stats_show(struct seq_file *m, void *v)
+{
+ struct workqueue_struct *wq;
+ struct pool_workqueue *pwq;
+ u64 stats[PWQ_NR_STATS];
+ long in_flight = 0, active = 0;
+ long running, queued;
+ int i;
+
+ if (v == &workqueues) {
+ seq_printf(m, "%-24s %-8s %6s %6s %7s %10s %10s %12s %8s %8s %8s %8s %8s\n",
+ "workqueue", "type", "queued", "active", "running",
+ "started", "completed", "cpu_time_us",
+ "cpu_itsv", "cm_wkup", "repat", "mayday", "rescued");
+ return 0;
+ }
+
+ wq = list_entry(v, struct workqueue_struct, list);
+
+ /* Advisory stats read without wq->mutex or pool->lock. */
+ for (i = 0; i < PWQ_NR_STATS; i++)
+ stats[i] = data_race(wq->retired_stats[i]);
+
+ for_each_pwq(pwq, wq) {
+ for (i = 0; i < PWQ_NR_STATS; i++)
+ stats[i] += data_race(pwq->stats[i]);
+ in_flight += data_race(pwq->total_in_flight);
+ active += data_race(pwq->nr_active);
+ }
+
+ running = max_t(long, stats[PWQ_STAT_STARTED] -
+ stats[PWQ_STAT_COMPLETED], 0);
+ queued = max_t(long, in_flight - running, 0);
+ active = max_t(long, active, 0);
+
+ seq_printf(m, "%-24s %-8s %6ld %6ld %7ld %10llu %10llu %12llu %8llu %8llu %8llu %8llu %8llu\n",
+ wq->name, wq_type_str(wq), queued, active, running,
+ stats[PWQ_STAT_STARTED], stats[PWQ_STAT_COMPLETED],
+ stats[PWQ_STAT_CPU_TIME], stats[PWQ_STAT_CPU_INTENSIVE],
+ stats[PWQ_STAT_CM_WAKEUP], stats[PWQ_STAT_REPATRIATED],
+ stats[PWQ_STAT_MAYDAY], stats[PWQ_STAT_RESCUED]);
+
+ return 0;
+}
+
+static const struct seq_operations wq_stats_sops = {
+ .start = wq_stats_start,
+ .next = wq_stats_next,
+ .stop = wq_stats_stop,
+ .show = wq_stats_show,
+};
+DEFINE_SEQ_ATTRIBUTE(wq_stats);
+
+static int __init wq_debugfs_init(void)
+{
+ struct dentry *dir;
+
+ dir = debugfs_create_dir("workqueue", NULL);
+ debugfs_create_file("stats", 0444, dir, NULL, &wq_stats_fops);
+ return 0;
+}
+late_initcall(wq_debugfs_init);
+
+#endif /* CONFIG_DEBUG_FS */
+
/*
* Workqueue watchdog.
*
--
2.56.0.218.g1ec904b173-goog