mirror of
https://github.com/torvalds/linux.git
synced 2026-11-10 17:11:02 +01:00
Port stat-cpi.py from the legacy embedded scripting framework to a standalone Python script in tools/perf/python/ to calculate Cycles Per Instruction (CPI) per interval per CPU or thread. Improvements compared to the legacy script: - Support both perf.data file mode (via perf.session stat callbacks) and live counter collection mode (using perf.parse_events, evlist.open, and evsel.read across intervals), with automatic fallback to user-space (:u) and self-process monitoring when perf_event_paranoid restricts system-wide events (EACCES). - Compute per-interval counter deltas (val, ena, run) keyed by raw event name so cumulative PERF_RECORD_STAT snapshots and hybrid PMU events (e.g. cpu_core/cycles/, cpu_atom/cycles/) are accumulated accurately, and scale counts by time_enabled / time_running when multiplexed. - Replace hard-coded CPU ([0, 1]) and thread ([0]) arrays with dynamic CPU and thread discovery so arbitrary system topologies work automatically. - Add CLI option handling (-i, -I, -p) via argparse and type annotations passing mypy and pylint. Add a shell test (test_stat_cpi_python.sh) to verify the standalone script. Assisted-by: Antigravity:gemini-3.1-pro Signed-off-by: Ian Rogers <irogers@google.com> Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
220 lines
8.0 KiB
Python
Executable File
220 lines
8.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
"""Calculate CPI from perf stat data or live."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import signal
|
|
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, float] = {}
|
|
self.prev_data: dict[str, tuple[int, int, int]] = {}
|
|
self.recorded_pairs: set[tuple[int, int]] = set()
|
|
|
|
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."""
|
|
self.recorded_pairs.add((cpu, thread))
|
|
|
|
def store(self, event: str, cpu: int, thread: int,
|
|
counts: tuple[int, int, int], is_delta: bool = False,
|
|
raw_name: Optional[str] = None) -> None:
|
|
"""Store counter values, computing difference from previous
|
|
absolute values if not already deltas."""
|
|
self.store_key(cpu, thread)
|
|
key = self.get_key(event, cpu, thread)
|
|
prev_key = self.get_key(raw_name or event, cpu, thread)
|
|
|
|
val, ena, run = counts
|
|
if is_delta:
|
|
# counts are already deltas
|
|
cur_val = val
|
|
cur_ena = ena
|
|
cur_run = run
|
|
else:
|
|
if prev_key in self.prev_data:
|
|
prev_val, prev_ena, prev_run = self.prev_data[prev_key]
|
|
cur_val = val - prev_val
|
|
cur_ena = ena - prev_ena
|
|
cur_run = run - prev_run
|
|
else:
|
|
cur_val = val
|
|
cur_ena = ena
|
|
cur_run = run
|
|
self.prev_data[prev_key] = counts # Store absolute value for next time
|
|
|
|
# Scale each raw event's delta by its own multiplexing ratio before
|
|
# summing across PMUs (e.g. cpu_core and cpu_atom on hybrid systems) so
|
|
# enabled time from an idle PMU does not inflate the multiplier of an
|
|
# active PMU.
|
|
scaled_val = cur_val * (cur_ena / float(cur_run)) if cur_run > 0 else float(cur_val)
|
|
self.data[key] = self.data.get(key, 0.0) + scaled_val
|
|
|
|
def get(self, event: str, cpu: int, thread: int) -> float:
|
|
"""Get scaled counter value."""
|
|
key = self.get_key(event, cpu, thread)
|
|
return self.data.get(key, 0.0)
|
|
|
|
@staticmethod
|
|
def _classify_event(name: str) -> Optional[str]:
|
|
"""Classify an event name as 'cycles' or 'instructions'."""
|
|
ev = name[6:-1] if name.startswith("evsel(") and name.endswith(")") else name
|
|
ev = ev.split(":", 1)[0]
|
|
if "/" in ev:
|
|
parts = [p for p in ev.split("/") if p]
|
|
if len(parts) >= 2:
|
|
ev = parts[1]
|
|
if ev in ("cycles", "cpu-cycles"):
|
|
return "cycles"
|
|
if ev == "instructions":
|
|
return "instructions"
|
|
return None
|
|
|
|
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:
|
|
event_name = self._classify_event(name)
|
|
if not event_name:
|
|
return
|
|
self.store(event_name, event.cpu, event.thread,
|
|
(event.val, event.ena, event.run), raw_name=name)
|
|
elif event.type == perf.RECORD_STAT_ROUND:
|
|
timestamp = getattr(event, "time", 0)
|
|
self.print_interval(timestamp)
|
|
self.data.clear()
|
|
self.recorded_pairs.clear()
|
|
|
|
def print_interval(self, timestamp: int) -> None:
|
|
"""Print CPI for the current interval."""
|
|
for cpu, thread in sorted(self.recorded_pairs):
|
|
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:.0f}/{ins:.0f})")
|
|
|
|
def read_counters(self, evlist: Any) -> None:
|
|
"""Read counters live."""
|
|
for evsel in evlist:
|
|
name = str(evsel)
|
|
event_name = self._classify_event(name)
|
|
if not event_name:
|
|
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),
|
|
is_delta=True, raw_name=name)
|
|
except OSError:
|
|
pass
|
|
|
|
def run_file(self) -> None:
|
|
"""Process events from file."""
|
|
session: Optional[perf.session] = perf.session(
|
|
perf.data(self.args.input), stat=self.process_stat_event
|
|
)
|
|
try:
|
|
assert session is not None
|
|
session.process_events()
|
|
finally:
|
|
session = None
|
|
|
|
def _open_live_evlist(self) -> Any:
|
|
"""Open evlist for live mode, falling back to user-space or process scope on EACCES."""
|
|
threads = perf.thread_map(self.args.pid) if self.args.pid else None
|
|
candidates = [
|
|
("cycles,instructions", threads),
|
|
("cycles:u,instructions:u", threads),
|
|
]
|
|
if threads is None:
|
|
self_threads = perf.thread_map(os.getpid())
|
|
candidates.append(("cycles,instructions", self_threads))
|
|
candidates.append(("cycles:u,instructions:u", self_threads))
|
|
|
|
last_err: Optional[OSError] = None
|
|
for events, tmap in candidates:
|
|
try:
|
|
evlist = perf.parse_events(events, None, tmap)
|
|
for evsel in evlist:
|
|
evsel.read_format |= (
|
|
perf.FORMAT_TOTAL_TIME_ENABLED | perf.FORMAT_TOTAL_TIME_RUNNING
|
|
)
|
|
evlist.open()
|
|
evlist.enable()
|
|
return evlist
|
|
except PermissionError as e:
|
|
last_err = e
|
|
except OSError as e:
|
|
if e.errno == 13:
|
|
last_err = e
|
|
else:
|
|
raise
|
|
if last_err is not None:
|
|
raise last_err
|
|
raise RuntimeError("Failed to open events")
|
|
|
|
def run_live(self) -> None:
|
|
"""Read counters live."""
|
|
try:
|
|
evlist = self._open_live_evlist()
|
|
except OSError as e:
|
|
print(f"Failed to open events: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
def handle_signal(_signum: int, _frame: Any) -> None:
|
|
raise KeyboardInterrupt
|
|
|
|
signal.signal(signal.SIGINT, signal.default_int_handler)
|
|
signal.signal(signal.SIGTERM, handle_signal)
|
|
|
|
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.recorded_pairs.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")
|
|
ap.add_argument("-p", "--pid", type=int,
|
|
help="Monitor specific process ID in live mode")
|
|
args = ap.parse_args()
|
|
|
|
analyzer = StatCpiAnalyzer(args)
|
|
if args.input:
|
|
analyzer.run_file()
|
|
else:
|
|
analyzer.run_live()
|
|
|
|
if __name__ == "__main__":
|
|
main()
|