[PATCH v1 2/9] perf python sctop: Fix offline interval printing and test flakiness

From: Ian Rogers

Date: Mon Sep 28 2026 - 22:22:49 EST


In sctop.py, process_event() updated self.last_print_time on every
sample in offline mode even when skip was true for non-matching comm
events. When filtering by comm with a short interval (such as 'sleep 1'
in test_sctop_python.sh) on a system-wide recording spanning more than
1 second under heavy load, non-matching samples could trigger
print_current_totals() and clear self.syscalls, followed by finally:
printing a trailing empty table. In addition, analyzer.e_machine was
initialized after session.process_events() instead of before.

Fix sctop.py by initializing analyzer.e_machine before
session.process_events(), only advancing self.last_print_time when a
sample is not skipped, and only calling print_current_totals() in
finally: if no table has been printed yet or remaining syscalls are
pending.

In test_sctop_python.sh, use 'perf list tracepoint' instead of
unfiltered 'perf list', record the child workload directly without '-a'
and with '-B -N --no-bpf-event' to avoid system-wide ringbuffer overflow
and synthesis contention, and retry up to 5 times if needed.

Fixes: b83f0bacf5e4 ("perf python: Port sctop to perf module")
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@xxxxxxxxxx>
---
tools/perf/python/sctop.py | 9 ++--
tools/perf/tests/shell/test_sctop_python.sh | 48 +++++++++++----------
2 files changed, 31 insertions(+), 26 deletions(-)

diff --git a/tools/perf/python/sctop.py b/tools/perf/python/sctop.py
index 42e95ecfffad..c524e08e6440 100755
--- a/tools/perf/python/sctop.py
+++ b/tools/perf/python/sctop.py
@@ -37,6 +37,7 @@ class SCTopAnalyzer:
self.offline = offline
self.own_pid = os.getpid()
self.last_print_time: Optional[int] = None
+ self.printed = False
self.session: Optional[perf.session] = None
self.e_machine: Optional[int] = None

@@ -127,7 +128,7 @@ class SCTopAnalyzer:
if not skip and is_enter and 0 <= (syscall_id & ~0x40000000) <= 0xffff:
self.syscalls[syscall_id] += 1

- if self.offline and hasattr(sample, "sample_time"):
+ if not skip and self.offline and hasattr(sample, "sample_time"):
interval_ns = self.interval * (10 ** 9)
if self.last_print_time is None:
self.last_print_time = sample.sample_time
@@ -137,6 +138,7 @@ class SCTopAnalyzer:

def print_current_totals(self):
"""Print current syscall totals."""
+ self.printed = True
# Clear terminal
if not self.offline:
print("\x1b[2J\x1b[H", end="")
@@ -217,8 +219,8 @@ def main():
if args.input:
session = perf.session(perf.data(args.input), sample=analyzer.process_event)
analyzer.session = session
- session.process_events()
analyzer.e_machine = getattr(session, "e_machine", None)
+ session.process_events()
else:
try:
live_session = LiveSession(
@@ -237,7 +239,8 @@ def main():
sys.exit(1)
finally:
if args.input:
- analyzer.print_current_totals()
+ if not analyzer.printed or analyzer.syscalls:
+ analyzer.print_current_totals()
# Break the reference cycle between perf.session and analyzer.process_event
# because perf.session lacks cyclic GC support (tp_traverse).
analyzer.session = None
diff --git a/tools/perf/tests/shell/test_sctop_python.sh b/tools/perf/tests/shell/test_sctop_python.sh
index 007f2584cce6..230eeca96fb8 100755
--- a/tools/perf/tests/shell/test_sctop_python.sh
+++ b/tools/perf/tests/shell/test_sctop_python.sh
@@ -41,37 +41,39 @@ temp_out=$(mktemp /tmp/perf.out.XXXXXX)
echo "Testing sctop.py..."

# Create a perf.data file.
-if perf list | grep -q "raw_syscalls:sys_enter"; then
- perf record -e raw_syscalls:sys_enter -a -o "${temp_data}" \
- -- sleep 0.1 >/dev/null 2>&1 || \
- { echo "Skipping test, perf record failed"; exit 2; }
-else
+if ! perf list tracepoint | grep -q "raw_syscalls:sys_enter"; then
echo "Skipping test, no raw_syscalls:sys_enter event"
exit 2
fi

-if [ ! -s "${temp_data}" ]; then
- echo "Skipping test, perf record failed to create data"
- exit 2
-fi
+passed=0
+for _ in 1 2 3 4 5; do
+ rm -f "${temp_data}" "${temp_out}"
+ if ! perf record -B -N --no-bpf-event -e raw_syscalls:sys_enter -o "${temp_data}" \
+ -- sh -c "sleep 0.1; sleep 0.05" >/dev/null 2>&1; then
+ echo "Skipping test, perf record failed"
+ exit 2
+ fi
+
+ if [ ! -s "${temp_data}" ]; then
+ continue
+ fi
+
+ # Check that the script executes
+ if perf script sctop -i "${temp_data}" > "${temp_out}" && \
+ grep -E -q "[0-9]+$" "${temp_out}" && \
+ perf script sctop -i "${temp_data}" sleep 1 > "${temp_out}" && \
+ grep -E -q "[0-9]+$" "${temp_out}"; then
+ passed=1
+ break
+ fi
+done

-# Check that the script executes
-if ! perf script sctop -i "${temp_data}" > "${temp_out}"; then
+if [ "$passed" -eq 0 ]; then
echo "sctop.py test failed"
err=1
-elif ! grep -E -q "[0-9]+$" "${temp_out}"; then
- echo "Failed to find metric data rows in default run"
- err=1
-elif ! perf script sctop -i "${temp_data}" sleep 1 > "${temp_out}"; then
- echo "sctop.py comm+interval test failed"
- err=1
else
- if ! grep -E -q "[0-9]+$" "${temp_out}"; then
- echo "Failed to find metric data rows"
- err=1
- else
- echo "sctop test passed."
- fi
+ echo "sctop test passed."
fi
rm -f "${temp_out}"

--
2.56.0.rc1.315.gc6ed9934b7-goog