[PATCH v1 3/9] perf python stat-cpi: Fix live mode signal races and test flakiness

From: Ian Rogers

Date: Mon Sep 28 2026 - 22:23:18 EST


In test_stat_cpi_python.sh, 'perf test -w noploop &' defaults to a
1-second duration and can exit under heavy parallel load before
'perf stat -p' and 'perf script stat-cpi' finish starting up. In
addition, the fixed 'sleep 0.5' before sending SIGINT can fire before
Python finishes importing the perf module, opening the live evlist, and
flushing the first interval.

In stat-cpi.py, register SIGINT and SIGTERM handlers before calling
_open_live_evlist() and pass flush=True when printing live output so
redirected stdout is flushed immediately after each interval.

In test_stat_cpi_python.sh, run 'perf test -w noploop 60 &' so the
target workload stays alive until killed, and poll the output file for
'cpi' (up to 5 seconds) before sending SIGINT.

Fixes: 4425182d426b ("perf python: Port stat-cpi to perf module")
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
tools/perf/python/stat-cpi.py | 19 ++++++++++---------
.../perf/tests/shell/test_stat_cpi_python.sh | 12 +++++++++---
2 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/tools/perf/python/stat-cpi.py b/tools/perf/python/stat-cpi.py
index 0b7d76876a6c..da92cf560067 100755
--- a/tools/perf/python/stat-cpi.py
+++ b/tools/perf/python/stat-cpi.py
@@ -106,7 +106,8 @@ class StatCpiAnalyzer:
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})")
+ print(f"{t_sec:15f}: cpu {cpu}, thread {thread} -> cpi {cpi:f} ({cyc:.0f}/{ins:.0f})",
+ flush=True)

def read_counters(self, evlist: Any) -> None:
"""Read counters live."""
@@ -173,19 +174,19 @@ class StatCpiAnalyzer:

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:
+ evlist = self._open_live_evlist()
+ except OSError as e:
+ print(f"Failed to open events: {e}", file=sys.stderr)
+ sys.exit(1)
+
+ print("Live mode started. Press Ctrl+C to stop.", flush=True)
try:
while True:
time.sleep(self.args.interval)
@@ -195,7 +196,7 @@ class StatCpiAnalyzer:
self.data.clear()
self.recorded_pairs.clear()
except KeyboardInterrupt:
- print("\nStopped.")
+ print("\nStopped.", flush=True)
finally:
evlist.close()

diff --git a/tools/perf/tests/shell/test_stat_cpi_python.sh b/tools/perf/tests/shell/test_stat_cpi_python.sh
index fe7562307634..6cb376c92e2f 100755
--- a/tools/perf/tests/shell/test_stat_cpi_python.sh
+++ b/tools/perf/tests/shell/test_stat_cpi_python.sh
@@ -50,7 +50,7 @@ test_live_mode() {
echo "perf stat failed (permissions?), skipping live mode test."
return 0
fi
- perf test -w noploop &
+ perf test -w noploop 60 &
workload_pid=$!
if ! perf stat -e cycles,instructions -p "$workload_pid" -- sleep 0.05 2>/dev/null && \
! perf stat -e cycles:u,instructions:u -p "$workload_pid" -- sleep 0.05 2>/dev/null; then
@@ -61,10 +61,16 @@ test_live_mode() {
fi
ran=1

- # Run live mode for 1 interval in the background, give it a tiny sleep, then interrupt
+ # Run live mode in the background, wait until at least one interval is
+ # printed, then interrupt.
perf script stat-cpi -I 0.1 -p "$workload_pid" > "${temp_out}" &
pid=$!
- sleep 0.5
+ for _ in $(seq 1 50); do
+ if grep -q "cpi" "${temp_out}"; then
+ break
+ fi
+ sleep 0.1
+ done
kill -INT "$pid" 2>/dev/null || true
set +e
wait "$pid"
--
2.56.0.rc1.315.gc6ed9934b7-goog