Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
1# flamegraph.py - create flame graphs from perf samples
2# SPDX-License-Identifier: GPL-2.0
3#
4# Usage:
5#
6# perf record -a -g -F 99 sleep 60
7# perf script report flamegraph
8#
9# Combined:
10#
11# perf script flamegraph -a -F 99 sleep 60
12#
13# Written by Andreas Gerstmayr <agerstmayr@redhat.com>
14# Flame Graphs invented by Brendan Gregg <bgregg@netflix.com>
15# Works in tandem with d3-flame-graph by Martin Spier <mspier@netflix.com>
16#
17# pylint: disable=missing-module-docstring
18# pylint: disable=missing-class-docstring
19# pylint: disable=missing-function-docstring
20
21import argparse
22import hashlib
23import io
24import json
25import os
26import subprocess
27import sys
28from typing import Dict, Optional, Union
29import urllib.request
30
31MINIMAL_HTML = """<head>
32 <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css">
33</head>
34<body>
35 <div id="chart"></div>
36 <script type="text/javascript" src="https://d3js.org/d3.v7.js"></script>
37 <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.min.js"></script>
38 <script type="text/javascript">
39 const stacks = [/** @flamegraph_json **/];
40 // Note, options is unused.
41 const options = [/** @options_json **/];
42
43 var chart = flamegraph();
44 d3.select("#chart")
45 .datum(stacks[0])
46 .call(chart);
47 </script>
48</body>
49"""
50
51# pylint: disable=too-few-public-methods
52class Node:
53 def __init__(self, name: str, libtype: str):
54 self.name = name
55 # "root" | "kernel" | ""
56 # "" indicates user space
57 self.libtype = libtype
58 self.value: int = 0
59 self.children: list[Node] = []
60
61 def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]:
62 return {
63 "n": self.name,
64 "l": self.libtype,
65 "v": self.value,
66 "c": [x.to_json() for x in self.children]
67 }
68
69
70class FlameGraphCLI:
71 def __init__(self, args):
72 self.args = args
73 self.stack = Node("all", "root")
74
75 @staticmethod
76 def get_libtype_from_dso(dso: Optional[str]) -> str:
77 """
78 when kernel-debuginfo is installed,
79 dso points to /usr/lib/debug/lib/modules/*/vmlinux
80 """
81 if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")):
82 return "kernel"
83
84 return ""
85
86 @staticmethod
87 def find_or_create_node(node: Node, name: str, libtype: str) -> Node:
88 for child in node.children:
89 if child.name == name:
90 return child
91
92 child = Node(name, libtype)
93 node.children.append(child)
94 return child
95
96 def process_event(self, event) -> None:
97 # ignore events where the event name does not match
98 # the one specified by the user
99 if self.args.event_name and event.get("ev_name") != self.args.event_name:
100 return
101
102 pid = event.get("sample", {}).get("pid", 0)
103 # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux
104 # for user-space processes; let's use pid for kernel or user-space distinction
105 if pid == 0:
106 comm = event["comm"]
107 libtype = "kernel"
108 else:
109 comm = f"{event['comm']} ({pid})"
110 libtype = ""
111 node = self.find_or_create_node(self.stack, comm, libtype)
112
113 if "callchain" in event:
114 for entry in reversed(event["callchain"]):
115 name = entry.get("sym", {}).get("name", "[unknown]")
116 libtype = self.get_libtype_from_dso(entry.get("dso"))
117 node = self.find_or_create_node(node, name, libtype)
118 else:
119 name = event.get("symbol", "[unknown]")
120 libtype = self.get_libtype_from_dso(event.get("dso"))
121 node = self.find_or_create_node(node, name, libtype)
122 node.value += 1
123
124 def get_report_header(self) -> str:
125 if self.args.input == "-":
126 # when this script is invoked with "perf script flamegraph",
127 # no perf.data is created and we cannot read the header of it
128 return ""
129
130 try:
131 # if the file name other than perf.data is given,
132 # we read the header of that file
133 if self.args.input:
134 output = subprocess.check_output(["perf", "report", "--header-only",
135 "-i", self.args.input])
136 else:
137 output = subprocess.check_output(["perf", "report", "--header-only"])
138
139 result = output.decode("utf-8")
140 if self.args.event_name:
141 result += "\nFocused event: " + self.args.event_name
142 return result
143 except Exception as err: # pylint: disable=broad-except
144 print(f"Error reading report header: {err}", file=sys.stderr)
145 return ""
146
147 def trace_end(self) -> None:
148 stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())
149
150 if self.args.format == "html":
151 report_header = self.get_report_header()
152 options = {
153 "colorscheme": self.args.colorscheme,
154 "context": report_header
155 }
156 options_json = json.dumps(options)
157
158 template_md5sum = None
159 if self.args.format == "html":
160 if os.path.isfile(self.args.template):
161 template = f"file://{self.args.template}"
162 else:
163 if not self.args.allow_download:
164 print(f"""Warning: Flame Graph template '{self.args.template}'
165does not exist. To avoid this please install a package such as the
166js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame
167graph template (--template PATH) or use another output format (--format
168FORMAT).""",
169 file=sys.stderr)
170 if self.args.input == "-":
171 print(
172"""Not attempting to download Flame Graph template as script command line
173input is disabled due to using live mode. If you want to download the
174template retry without live mode. For example, use 'perf record -a -g
175-F 99 sleep 60' and 'perf script report flamegraph'. Alternatively,
176download the template from:
177https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html
178and place it at:
179/usr/share/d3-flame-graph/d3-flamegraph-base.html""",
180 file=sys.stderr)
181 sys.exit(1)
182 s = None
183 while s not in ["y", "n"]:
184 s = input("Do you wish to download a template from cdn.jsdelivr.net?" +
185 "(this warning can be suppressed with --allow-download) [yn] "
186 ).lower()
187 if s == "n":
188 sys.exit(1)
189 template = ("https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/"
190 "d3-flamegraph-base.html")
191 template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36"
192
193 try:
194 with urllib.request.urlopen(template) as url_template:
195 output_str = "".join([
196 l.decode("utf-8") for l in url_template.readlines()
197 ])
198 except Exception as err:
199 print(f"Error reading template {template}: {err}\n"
200 "a minimal flame graph will be generated", file=sys.stderr)
201 output_str = MINIMAL_HTML
202 template_md5sum = None
203
204 if template_md5sum:
205 download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest()
206 if download_md5sum != template_md5sum:
207 s = None
208 while s not in ["y", "n"]:
209 s = input(f"""Unexpected template md5sum.
210{download_md5sum} != {template_md5sum}, for:
211{output_str}
212continue?[yn] """).lower()
213 if s == "n":
214 sys.exit(1)
215
216 output_str = output_str.replace("/** @options_json **/", options_json)
217 output_str = output_str.replace("/** @flamegraph_json **/", stacks_json)
218
219 output_fn = self.args.output or "flamegraph.html"
220 else:
221 output_str = stacks_json
222 output_fn = self.args.output or "stacks.json"
223
224 if output_fn == "-":
225 with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:
226 out.write(output_str)
227 else:
228 print(f"dumping data to {output_fn}")
229 try:
230 with io.open(output_fn, "w", encoding="utf-8") as out:
231 out.write(output_str)
232 except IOError as err:
233 print(f"Error writing output file: {err}", file=sys.stderr)
234 sys.exit(1)
235
236
237if __name__ == "__main__":
238 parser = argparse.ArgumentParser(description="Create flame graphs.")
239 parser.add_argument("-f", "--format",
240 default="html", choices=["json", "html"],
241 help="output file format")
242 parser.add_argument("-o", "--output",
243 help="output file name")
244 parser.add_argument("--template",
245 default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",
246 help="path to flame graph HTML template")
247 parser.add_argument("--colorscheme",
248 default="blue-green",
249 help="flame graph color scheme",
250 choices=["blue-green", "orange"])
251 parser.add_argument("-i", "--input",
252 help=argparse.SUPPRESS)
253 parser.add_argument("--allow-download",
254 default=False,
255 action="store_true",
256 help="allow unprompted downloading of HTML template")
257 parser.add_argument("-e", "--event",
258 default="",
259 dest="event_name",
260 type=str,
261 help="specify the event to generate flamegraph for")
262
263 cli_args = parser.parse_args()
264 cli = FlameGraphCLI(cli_args)
265
266 process_event = cli.process_event
267 trace_end = cli.trace_end