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()
|