summaryrefslogtreecommitdiff
path: root/tools/perf/python/stackcollapse.py
blob: 0e8a65969db3217fd07ed6f27052364e855e7a7e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/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()