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 268