[PATCH v1 51/58] perf wakeup-latency: Port wakeup-latency to use python module

Ian Rogers
2026-04-20T00:01:19Z

Port the legacy Perl script wakeup-latency.pl to a python script using
the perf module in tools/perf/python.

The new script uses a class-based architecture and leverages the
perf.session API for event processing.

It measures wakeup latency by tracking timestamps of
sched:sched_wakeup and sched:sched_switch events.

Complications:
- Used min() and max() built-in functions instead of if blocks to
satisfy pylint recommendations.
- pylint warns about the module name not being snake_case, but it is
kept for consistency with the original script name.

Assisted-by: Gemini:gemini-3.1-pro-preview
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/python/wakeup-latency.py | 85 +++++++++++++++++++++++++++++
1 file changed, 85 insertions(+)
create mode 100755 tools/perf/python/wakeup-latency.py

diff --git a/tools/perf/python/wakeup-latency.py b/tools/perf/python/wakeup-latency.py
new file mode 100755
index 000000000000..109b751aefb3
--- /dev/null
+++ b/tools/perf/python/wakeup-latency.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0-only
+"""Display avg/min/max wakeup latency."""
+
+import argparse
+from collections import defaultdict
+import sys
+from typing import Optional, Dict
+import perf
+
+class WakeupLatency:
+ """Tracks and displays wakeup latency statistics."""
+ def __init__(self) -> None:
+ self.last_wakeup: Dict[int, int] = defaultdict(int)
+ self.max_wakeup_latency = 0
+ self.min_wakeup_latency = 1000000000
+ self.total_wakeup_latency = 0
+ self.total_wakeups = 0
+ self.unhandled: Dict[str, int] = defaultdict(int)
+ self.session: Optional[perf.session] = None
+
+ def process_event(self, sample: perf.sample_event) -> None:
+ """Process events."""
+ event_name = str(sample.evsel)
+ sample_time = sample.sample_time
+ cpu = sample.sample_cpu
+
+ if "sched:sched_wakeup" in event_name:
+ try:
+ target_cpu = sample.target_cpu
+ self.last_wakeup[target_cpu] = sample_time
+ except AttributeError:
+ pass
+ elif "sched:sched_switch" in event_name:
+ wakeup_ts = self.last_wakeup[cpu]
+ if wakeup_ts:
+ latency = sample_time - wakeup_ts
+ self.max_wakeup_latency = max(self.max_wakeup_latency, latency)
+ self.min_wakeup_latency = min(self.min_wakeup_latency, latency)
+ self.total_wakeup_latency += latency
+ self.total_wakeups += 1
+ self.last_wakeup[cpu] = 0
+ else:
+ self.unhandled[event_name] += 1
+
+ def print_totals(self) -> None:
+ """Print summary statistics."""
+ print("wakeup_latency stats:\n")
+ print(f"total_wakeups: {self.total_wakeups}")
+ if self.total_wakeups:
+ avg = self.total_wakeup_latency // self.total_wakeups
+ print(f"avg_wakeup_latency (ns): {avg}")
+ else:
+ print("avg_wakeup_latency (ns): N/A")
+ print(f"min_wakeup_latency (ns): {self.min_wakeup_latency}")
+ print(f"max_wakeup_latency (ns): {self.max_wakeup_latency}")
+
+ if self.unhandled:
+ print("\nunhandled events:\n")
+ print(f"{'event':<40s} {'count':>10s}")
+ print(f"{'-'*40} {'-'*10}")
+ for event_name, count in self.unhandled.items():
+ print(f"{event_name:<40s} {count:10d}")
+
+ def run(self, input_file: str) -> None:
+ """Run the session."""
+ self.session = perf.session(perf.data(input_file), sample=self.process_event)
+ self.session.process_events()
+ self.print_totals()
+
+def main() -> None:
+ """Main function."""
+ parser = argparse.ArgumentParser(description="Trace wakeup latency")
+ parser.add_argument("-i", "--input", default="perf.data", help="Input file")
+ args = parser.parse_args()
+
+ analyzer = WakeupLatency()
+ try:
+ analyzer.run(args.input)
+ except IOError as e:
+ print(e, file=sys.stderr)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
--
2.54.0.rc1.513.gad8abe7a5a-goog