mirror of
https://github.com/torvalds/linux.git
synced 2026-11-10 17:11:02 +01:00
Port sctop.py from tools/perf/scripts/python/ to a standalone script in
tools/perf/python/ using an SCTopAnalyzer class structure.
Improvements compared to the legacy script:
- Support both offline perf.data analysis (via perf.session, advancing
display intervals deterministically using event timestamps) and live
monitoring (via LiveSession with automatic tracepoint fallback from
raw_syscalls:sys_enter to syscalls:sys_enter_*).
- Resolve architecture-aware syscall names via
perf.syscall_name(id, session.e_machine) without requiring
python-audit.
- Replace unsafe signal.SIGALRM dictionary mutation and os.popen("clear")
subshell spawning with a synchronized threading.Lock / threading.Event
timer and direct ANSI terminal escape sequences ('\x1b[2J\x1b[H').
Add a shell test (test_sctop_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>
251 lines
9.5 KiB
Python
Executable File
251 lines
9.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
"""
|
|
System call top
|
|
|
|
Periodically displays system-wide system call totals, broken down by
|
|
syscall. If a [comm] arg is specified, only syscalls called by
|
|
[comm] are displayed. If an [interval] arg is specified, the display
|
|
will be refreshed every [interval] seconds. The default interval is
|
|
3 seconds.
|
|
|
|
Ported from tools/perf/scripts/python/sctop.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import defaultdict
|
|
import os
|
|
import sys
|
|
import threading
|
|
from typing import Optional
|
|
import perf
|
|
from perf_live import LiveSession
|
|
|
|
|
|
class SCTopAnalyzer:
|
|
"""Periodically displays system-wide system call totals."""
|
|
|
|
def __init__(self, for_comm: Optional[str], interval: int, offline: bool = False):
|
|
self.for_comm = for_comm
|
|
self.interval = interval
|
|
self.syscalls: dict[int, int] = defaultdict(int)
|
|
self.comm_cache: dict[int, str] = {}
|
|
self.lock = threading.Lock()
|
|
self.stop_event = threading.Event()
|
|
self.thread = threading.Thread(target=self.print_syscall_totals)
|
|
self.offline = offline
|
|
self.own_pid = os.getpid()
|
|
self.last_print_time: Optional[int] = None
|
|
self.session: Optional[perf.session] = None
|
|
self.e_machine: Optional[int] = None
|
|
|
|
def syscall_name(self, syscall_id: int) -> str:
|
|
"""Lookup syscall name by ID."""
|
|
# Mask out the x86_64 x32 ABI bit (__X32_SYSCALL_BIT = 0x40000000) before
|
|
# resolving the syscall number in the architecture's syscall table.
|
|
raw_sc_id = syscall_id & ~0x40000000
|
|
try:
|
|
e_machine = getattr(self.session, "e_machine", self.e_machine)
|
|
if e_machine is not None:
|
|
name = perf.syscall_name(raw_sc_id, e_machine)
|
|
else:
|
|
name = perf.syscall_name(raw_sc_id)
|
|
if name is not None:
|
|
return name
|
|
except (TypeError, OverflowError):
|
|
pass
|
|
return str(syscall_id)
|
|
|
|
def process_event(self, sample: perf.sample_event) -> None:
|
|
"""Collect syscall events."""
|
|
if not self.offline and sample.sample_pid == self.own_pid:
|
|
return
|
|
|
|
name = str(sample.evsel)
|
|
# Per-syscall syscalls:sys_enter_* tracepoints expose '__syscall_nr' (or 'nr')
|
|
# and may have an unrelated syscall argument named 'id', whereas
|
|
# raw_syscalls:sys_enter (and legacy pre-2.6.35 syscalls:sys_enter) expose 'id'.
|
|
if name.startswith("evsel(syscalls:sys_enter_"):
|
|
syscall_id = getattr(sample, "__syscall_nr", -1)
|
|
if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
|
|
syscall_id = getattr(sample, "nr", -1)
|
|
elif name.startswith(("evsel(raw_syscalls:sys_enter", "evsel(syscalls:sys_enter")):
|
|
syscall_id = getattr(sample, "id", -1)
|
|
if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
|
|
syscall_id = getattr(sample, "__syscall_nr", -1)
|
|
if not (0 <= (syscall_id & ~0x40000000) <= 0xffff):
|
|
syscall_id = getattr(sample, "nr", -1)
|
|
else:
|
|
syscall_id = -1
|
|
|
|
skip = False
|
|
with self.lock:
|
|
if self.for_comm is not None:
|
|
is_execve = (0 <= (syscall_id & ~0x40000000) <= 0xffff and
|
|
self.syscall_name(syscall_id) in ("execve", "execveat"))
|
|
if is_execve:
|
|
self.comm_cache.pop(sample.sample_pid, None)
|
|
|
|
comm = "Unknown"
|
|
if hasattr(self, 'session') and self.session:
|
|
# In offline perf.data mode, query session.find_thread() directly
|
|
# so PERF_RECORD_COMM updates after execve (e.g. perf -> sleep)
|
|
# are reflected immediately rather than returning a stale cached comm.
|
|
try:
|
|
proc = self.session.find_thread(sample.sample_pid, sample.sample_tid)
|
|
if proc:
|
|
comm = proc.comm() or "Unknown"
|
|
except TypeError:
|
|
pass
|
|
if comm != "Unknown" and not is_execve:
|
|
self.comm_cache[sample.sample_pid] = comm
|
|
elif sample.sample_pid in self.comm_cache:
|
|
comm = self.comm_cache[sample.sample_pid]
|
|
elif sample.sample_pid in self.comm_cache:
|
|
comm = self.comm_cache[sample.sample_pid]
|
|
else:
|
|
try:
|
|
with open(f"/proc/{sample.sample_pid}/comm", "r",
|
|
encoding="utf-8", errors="replace") as f:
|
|
comm = f.read().strip()
|
|
except OSError:
|
|
comm = "Unknown"
|
|
# Cache both matching and non-matching comms (including "Unknown"
|
|
# when a PID has exited or is inaccessible) so live system-wide
|
|
# tracing does not re-open /proc/<pid>/comm on every syscall.
|
|
# Do not cache during sys_enter(execve/execveat) since /proc/<pid>/comm
|
|
# still holds the pre-exec command name until the syscall completes.
|
|
if not is_execve:
|
|
self.comm_cache[sample.sample_pid] = comm
|
|
|
|
if comm != self.for_comm:
|
|
skip = True
|
|
|
|
is_enter = (name.startswith("evsel(raw_syscalls:sys_enter") or
|
|
name.startswith("evsel(syscalls:sys_enter"))
|
|
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"):
|
|
interval_ns = self.interval * (10 ** 9)
|
|
if self.last_print_time is None:
|
|
self.last_print_time = sample.sample_time
|
|
elif sample.sample_time - self.last_print_time >= interval_ns:
|
|
self.print_current_totals()
|
|
self.last_print_time = sample.sample_time
|
|
|
|
def print_current_totals(self):
|
|
"""Print current syscall totals."""
|
|
# Clear terminal
|
|
if not self.offline:
|
|
print("\x1b[2J\x1b[H", end="")
|
|
else:
|
|
print()
|
|
|
|
with self.lock:
|
|
for_comm = self.for_comm
|
|
if for_comm is not None:
|
|
print(f"\nsyscall events for {for_comm}:\n")
|
|
else:
|
|
print("\nsyscall events:\n")
|
|
|
|
print(f"{'event':40s} {'count':10s}")
|
|
print(f"{'-' * 40:40s} {'-' * 10:10s}")
|
|
|
|
with self.lock:
|
|
current_syscalls = list(self.syscalls.items())
|
|
self.syscalls.clear()
|
|
self.comm_cache.clear()
|
|
|
|
current_syscalls.sort(key=lambda kv: (-kv[1], kv[0]))
|
|
|
|
for syscall_id, val in current_syscalls:
|
|
print(f"{self.syscall_name(syscall_id):<40s} {val:10d}")
|
|
|
|
def print_syscall_totals(self):
|
|
"""Periodically print syscall totals."""
|
|
while not self.stop_event.is_set():
|
|
self.print_current_totals()
|
|
self.stop_event.wait(self.interval)
|
|
# Print final batch
|
|
self.print_current_totals()
|
|
|
|
def start(self):
|
|
"""Start the background thread."""
|
|
self.thread.start()
|
|
|
|
def stop(self):
|
|
"""Stop the background thread."""
|
|
self.stop_event.set()
|
|
self.thread.join()
|
|
|
|
|
|
def main():
|
|
"""Main function."""
|
|
ap = argparse.ArgumentParser(description="System call top")
|
|
ap.add_argument("args", nargs="*", help="[comm] [interval] or [interval]")
|
|
ap.add_argument("-i", "--input", help="Input file name")
|
|
args = ap.parse_args()
|
|
|
|
for_comm = None
|
|
default_interval = 3
|
|
interval = default_interval
|
|
|
|
if len(args.args) > 2:
|
|
print("Usage: python sctop.py [comm] [interval]")
|
|
sys.exit(1)
|
|
|
|
if len(args.args) > 1:
|
|
for_comm = args.args[0]
|
|
try:
|
|
interval = int(args.args[1])
|
|
except ValueError:
|
|
print(f"Invalid interval: {args.args[1]}")
|
|
sys.exit(1)
|
|
elif len(args.args) > 0:
|
|
try:
|
|
interval = int(args.args[0])
|
|
except ValueError:
|
|
for_comm = args.args[0]
|
|
interval = default_interval
|
|
|
|
analyzer = SCTopAnalyzer(for_comm, interval, offline=bool(args.input))
|
|
session = None
|
|
|
|
try:
|
|
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)
|
|
else:
|
|
try:
|
|
live_session = LiveSession(
|
|
"raw_syscalls:sys_enter", sample_callback=analyzer.process_event
|
|
)
|
|
except OSError:
|
|
live_session = LiveSession(
|
|
"syscalls:sys_enter_*", sample_callback=analyzer.process_event
|
|
)
|
|
analyzer.start()
|
|
live_session.run()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
except (OSError, IOError) as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
finally:
|
|
if args.input:
|
|
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
|
|
session = None
|
|
elif analyzer.thread.is_alive():
|
|
analyzer.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|