[PATCH v1 25/58] perf stat-cpi: Port stat-cpi to use python module
From: Ian Rogers
Date: Sun Apr 19 2026 - 20:06:01 EST
Port stat-cpi.py from the legacy framework to a standalone script.
Support both file processing mode (using perf.session) and live mode
(reading counters directly via perf.parse_events and evsel.read). Use
argparse for command line options handling. Calculate and display CPI
(Cycles Per Instruction) per interval per CPU/thread.
Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
tools/perf/python/stat-cpi.py | 139 ++++++++++++++++++++++++++++++++++
1 file changed, 139 insertions(+)
create mode 100755 tools/perf/python/stat-cpi.py
diff --git a/tools/perf/python/stat-cpi.py b/tools/perf/python/stat-cpi.py
new file mode 100755
index 000000000000..6f9c2343520e
--- /dev/null
+++ b/tools/perf/python/stat-cpi.py
@@ -0,0 +1,139 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: GPL-2.0
+"""Calculate CPI from perf stat data or live."""
+
+import argparse
+import sys
+import time
+from typing import Any, Optional
+import perf
+
+class StatCpiAnalyzer:
+ """Accumulates cycles and instructions and calculates CPI."""
+
+ def __init__(self, args: argparse.Namespace) -> None:
+ self.args = args
+ self.data: dict[str, tuple[int, int, int]] = {}
+ self.cpus: list[int] = []
+ self.threads: list[int] = []
+
+ def get_key(self, event: str, cpu: int, thread: int) -> str:
+ """Get key for data dictionary."""
+ return f"{event}-{cpu}-{thread}"
+
+ def store_key(self, cpu: int, thread: int) -> None:
+ """Store CPU and thread IDs."""
+ if cpu not in self.cpus:
+ self.cpus.append(cpu)
+ if thread not in self.threads:
+ self.threads.append(thread)
+
+ def store(self, event: str, cpu: int, thread: int, counts: tuple[int, int, int]) -> None:
+ """Store counter values."""
+ self.store_key(cpu, thread)
+ key = self.get_key(event, cpu, thread)
+ self.data[key] = counts
+
+ def get(self, event: str, cpu: int, thread: int) -> int:
+ """Get counter value."""
+ key = self.get_key(event, cpu, thread)
+ return self.data[key][0] if key in self.data else 0
+
+ def process_stat_event(self, event: Any, name: Optional[str] = None) -> None:
+ """Process PERF_RECORD_STAT and PERF_RECORD_STAT_ROUND events."""
+ if event.type == perf.RECORD_STAT:
+ if name:
+ if "cycles" in name:
+ event_name = "cycles"
+ elif "instructions" in name:
+ event_name = "instructions"
+ else:
+ return
+ self.store(event_name, event.cpu, event.thread, (event.val, event.ena, event.run))
+ elif event.type == perf.RECORD_STAT_ROUND:
+ timestamp = getattr(event, "time", 0)
+ self.print_interval(timestamp)
+ self.data.clear()
+ self.cpus.clear()
+ self.threads.clear()
+
+ def print_interval(self, timestamp: int) -> None:
+ """Print CPI for the current interval."""
+ for cpu in self.cpus:
+ for thread in self.threads:
+ cyc = self.get("cycles", cpu, thread)
+ ins = self.get("instructions", cpu, thread)
+ cpi = 0.0
+ if ins != 0:
+ cpi = cyc / float(ins)
+ t_sec = timestamp / 1000000000.0
+ print(f"{t_sec:15f}: cpu {cpu}, thread {thread} -> cpi {cpi:f} ({cyc}/{ins})")
+
+ def read_counters(self, evlist: Any) -> None:
+ """Read counters live."""
+ for evsel in evlist:
+ name = str(evsel)
+ if "cycles" in name:
+ event_name = "cycles"
+ elif "instructions" in name:
+ event_name = "instructions"
+ else:
+ continue
+
+ for cpu in evsel.cpus():
+ for thread in evsel.threads():
+ try:
+ counts = evsel.read(cpu, thread)
+ self.store(event_name, cpu, thread,
+ (counts.val, counts.ena, counts.run))
+ except OSError:
+ pass
+
+ def run_file(self) -> None:
+ """Process events from file."""
+ session = perf.session(perf.data(self.args.input), stat=self.process_stat_event)
+ session.process_events()
+
+ def run_live(self) -> None:
+ """Read counters live."""
+ evlist = perf.parse_events("cycles,instructions")
+ if not evlist:
+ print("Failed to parse events", file=sys.stderr)
+ return
+ try:
+ evlist.open()
+ except OSError as e:
+ print(f"Failed to open events: {e}", file=sys.stderr)
+ return
+
+ print("Live mode started. Press Ctrl+C to stop.")
+ try:
+ while True:
+ time.sleep(self.args.interval)
+ timestamp = time.time_ns()
+ self.read_counters(evlist)
+ self.print_interval(timestamp)
+ self.data.clear()
+ self.cpus.clear()
+ self.threads.clear()
+ except KeyboardInterrupt:
+ print("\nStopped.")
+ finally:
+ evlist.close()
+
+def main() -> None:
+ """Main function."""
+ ap = argparse.ArgumentParser(description="Calculate CPI from perf stat data or live")
+ ap.add_argument("-i", "--input", help="Input file name (enables file mode)")
+ ap.add_argument("-I", "--interval", type=float, default=1.0,
+ help="Interval in seconds for live mode")
+ args = ap.parse_args()
+
+ analyzer = StatCpiAnalyzer(args)
+ if args.input:
+ analyzer.run_file()
+ else:
+ analyzer.run_live()
+
+if __name__ == "__main__":
+ main()
--
2.54.0.rc1.513.gad8abe7a5a-goog