mirror of
https://github.com/torvalds/linux.git
synced 2026-11-10 17:11:02 +01:00
Port stackcollapse.py from tools/perf/scripts/python/ to a standalone script in tools/perf/python/ refactored into a StackCollapseAnalyzer class. Improvements compared to the legacy script: - Traverse sample.callchain directly from perf.session without allocating per-event dictionaries, and fall back to sample.symbol when a sample has no callchain. - Replace deprecated optparse with argparse, adding -i/--input alongside --include-tid, --include-pid, --no-comm, --tidy-java, and --kernel. - Handle BrokenPipeError cleanly when output is piped into downstream tools (such as head or flamegraph.pl). Add a shell test (test_stackcollapse_python.sh) using a CPU workload (perf test -w noploop) 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>
146 lines
4.9 KiB
Python
Executable File
146 lines
4.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: GPL-2.0
|
|
"""
|
|
stackcollapse.py - format perf samples with one line per distinct call stack
|
|
|
|
This script's output has two space-separated fields. The first is a semicolon
|
|
separated stack including the program name (from the "comm" field) and the
|
|
function names from the call stack. The second is a count:
|
|
|
|
swapper;start_kernel;rest_init;cpu_idle;default_idle;native_safe_halt 2
|
|
|
|
The file is sorted according to the first field.
|
|
|
|
Ported from tools/perf/scripts/python/stackcollapse.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
from collections import defaultdict
|
|
import os
|
|
import sys
|
|
import perf
|
|
|
|
|
|
class StackCollapseAnalyzer:
|
|
"""Accumulates call stacks and prints them collapsed."""
|
|
|
|
def __init__(self, args: argparse.Namespace) -> None:
|
|
self.args = args
|
|
self.lines: dict[str, int] = defaultdict(int)
|
|
self.session: perf.session | None = None
|
|
|
|
def tidy_function_name(self, sym: str, dso: str) -> str:
|
|
"""Beautify function names based on options."""
|
|
if sym is None:
|
|
sym = "[unknown]"
|
|
|
|
sym = sym.replace(";", ":")
|
|
if self.args.tidy_java:
|
|
# Beautify Java signatures
|
|
sym = sym.replace("<", "")
|
|
sym = sym.replace(">", "")
|
|
if sym.startswith("L") and "/" in sym:
|
|
sym = sym[1:]
|
|
try:
|
|
sym = sym[:sym.index("(")]
|
|
except ValueError:
|
|
pass
|
|
|
|
if self.args.annotate_kernel and dso == "[kernel.kallsyms]":
|
|
return sym + "_[k]"
|
|
return sym
|
|
|
|
def process_event(self, sample: perf.sample_event) -> None:
|
|
"""Collect call stack for each sample."""
|
|
stack = []
|
|
callchain = sample.callchain
|
|
if callchain is not None:
|
|
for node in callchain:
|
|
stack.append(self.tidy_function_name(node.symbol, node.dso))
|
|
else:
|
|
# Fallback if no callchain
|
|
sym = (sample.symbol or '[unknown]')
|
|
dso = (sample.dso or '[unknown]')
|
|
stack.append(self.tidy_function_name(sym, dso))
|
|
|
|
if self.args.include_comm:
|
|
comm = "Unknown"
|
|
if self.session is not None:
|
|
try:
|
|
proc = self.session.find_thread(
|
|
sample.sample_pid, sample.sample_tid
|
|
)
|
|
if proc:
|
|
proc_comm = proc.comm()
|
|
if proc_comm is not None:
|
|
comm = proc_comm
|
|
except TypeError:
|
|
pass
|
|
comm = str(comm).replace(" ", "_")
|
|
sep = "-"
|
|
if self.args.include_pid:
|
|
comm = f"{comm}{sep}{(sample.sample_pid or 0)}"
|
|
sep = "/"
|
|
if self.args.include_tid:
|
|
comm = f"{comm}{sep}{(sample.sample_tid or 0)}"
|
|
stack.append(comm)
|
|
|
|
stack_string = ";".join(reversed(stack))
|
|
self.lines[stack_string] += 1
|
|
|
|
def print_totals(self) -> None:
|
|
"""Print sorted collapsed stacks."""
|
|
try:
|
|
for stack in sorted(self.lines):
|
|
print(f"{stack} {self.lines[stack]}")
|
|
sys.stdout.flush()
|
|
except BrokenPipeError:
|
|
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
os.dup2(devnull, sys.stdout.fileno())
|
|
os.close(devnull)
|
|
|
|
|
|
def main():
|
|
"""Main function."""
|
|
ap = argparse.ArgumentParser(
|
|
description="Format perf samples with one line per distinct call stack"
|
|
)
|
|
ap.add_argument("-i", "--input", default="perf.data", help="Input file name")
|
|
ap.add_argument("--include-tid", action="store_true", help="include thread id in stack")
|
|
ap.add_argument("--include-pid", action="store_true", help="include process id in stack")
|
|
ap.add_argument("--no-comm", dest="include_comm", action="store_false", default=True,
|
|
help="do not separate stacks according to comm")
|
|
ap.add_argument("--tidy-java", action="store_true", help="beautify Java signatures")
|
|
ap.add_argument("--kernel", dest="annotate_kernel", action="store_true",
|
|
help="annotate kernel functions with _[k]")
|
|
|
|
args = ap.parse_args()
|
|
|
|
if args.include_tid and not args.include_comm:
|
|
print("requesting tid but not comm is invalid", file=sys.stderr)
|
|
sys.exit(1)
|
|
if args.include_pid and not args.include_comm:
|
|
print("requesting pid but not comm is invalid", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
analyzer = StackCollapseAnalyzer(args)
|
|
|
|
try:
|
|
session = perf.session(perf.data(args.input), sample=analyzer.process_event)
|
|
analyzer.session = session
|
|
session.process_events()
|
|
except IOError as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
sys.exit(1)
|
|
except KeyboardInterrupt:
|
|
pass
|
|
finally:
|
|
analyzer.session = None
|
|
|
|
analyzer.print_totals()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|