| #!/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() |