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 21from __future__ import print_function 22import argparse 23import hashlib 24import io 25import json 26import os 27import subprocess 28import sys 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, libtype): 54 self.name = name 55 # "root" | "kernel" | "" 56 # "" indicates user space 57 self.libtype = libtype 58 self.value = 0 59 self.children = [] 60 61 def to_json(self): 62 return { 63 "n": self.name, 64 "l": self.libtype, 65 "v": self.value, 66 "c": 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): 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, name, libtype): 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): 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 = "{} ({})".format(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): 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", "-i", self.args.input]) 135 else: 136 output = subprocess.check_output(["perf", "report", "--header-only"]) 137 138 result = output.decode("utf-8") 139 if self.args.event_name: 140 result += "\nFocused event: " + self.args.event_name 141 return result 142 except Exception as err: # pylint: disable=broad-except 143 print("Error reading report header: {}".format(err), file=sys.stderr) 144 return "" 145 146 def trace_end(self): 147 stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) 148 149 if self.args.format == "html": 150 report_header = self.get_report_header() 151 options = { 152 "colorscheme": self.args.colorscheme, 153 "context": report_header 154 } 155 options_json = json.dumps(options) 156 157 template_md5sum = None 158 if self.args.format == "html": 159 if os.path.isfile(self.args.template): 160 template = f"file://{self.args.template}" 161 else: 162 if not self.args.allow_download: 163 print(f"""Warning: Flame Graph template '{self.args.template}' 164does not exist. To avoid this please install a package such as the 165js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame 166graph template (--template PATH) or use another output format (--format 167FORMAT).""", 168 file=sys.stderr) 169 if self.args.input == "-": 170 print("""Not attempting to download Flame Graph template as script command line 171input is disabled due to using live mode. If you want to download the 172template retry without live mode. For example, use 'perf record -a -g 173-F 99 sleep 60' and 'perf script report flamegraph'. Alternatively, 174download the template from: 175https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html 176and place it at: 177/usr/share/d3-flame-graph/d3-flamegraph-base.html""", 178 file=sys.stderr) 179 quit() 180 s = None 181 while s != "y" and s != "n": 182 s = input("Do you wish to download a template from cdn.jsdelivr.net? (this warning can be suppressed with --allow-download) [yn] ").lower() 183 if s == "n": 184 quit() 185 template = "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html" 186 template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" 187 188 try: 189 with urllib.request.urlopen(template) as template: 190 output_str = "".join([ 191 l.decode("utf-8") for l in template.readlines() 192 ]) 193 except Exception as err: 194 print(f"Error reading template {template}: {err}\n" 195 "a minimal flame graph will be generated", file=sys.stderr) 196 output_str = minimal_html 197 template_md5sum = None 198 199 if template_md5sum: 200 download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest() 201 if download_md5sum != template_md5sum: 202 s = None 203 while s != "y" and s != "n": 204 s = input(f"""Unexpected template md5sum. 205{download_md5sum} != {template_md5sum}, for: 206{output_str} 207continue?[yn] """).lower() 208 if s == "n": 209 quit() 210 211 output_str = output_str.replace("/** @options_json **/", options_json) 212 output_str = output_str.replace("/** @flamegraph_json **/", stacks_json) 213 214 output_fn = self.args.output or "flamegraph.html" 215 else: 216 output_str = stacks_json 217 output_fn = self.args.output or "stacks.json" 218 219 if output_fn == "-": 220 with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out: 221 out.write(output_str) 222 else: 223 print("dumping data to {}".format(output_fn)) 224 try: 225 with io.open(output_fn, "w", encoding="utf-8") as out: 226 out.write(output_str) 227 except IOError as err: 228 print("Error writing output file: {}".format(err), file=sys.stderr) 229 sys.exit(1) 230 231 232if __name__ == "__main__": 233 parser = argparse.ArgumentParser(description="Create flame graphs.") 234 parser.add_argument("-f", "--format", 235 default="html", choices=["json", "html"], 236 help="output file format") 237 parser.add_argument("-o", "--output", 238 help="output file name") 239 parser.add_argument("--template", 240 default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", 241 help="path to flame graph HTML template") 242 parser.add_argument("--colorscheme", 243 default="blue-green", 244 help="flame graph color scheme", 245 choices=["blue-green", "orange"]) 246 parser.add_argument("-i", "--input", 247 help=argparse.SUPPRESS) 248 parser.add_argument("--allow-download", 249 default=False, 250 action="store_true", 251 help="allow unprompted downloading of HTML template") 252 parser.add_argument("-e", "--event", 253 default="", 254 dest="event_name", 255 type=str, 256 help="specify the event to generate flamegraph for") 257 258 cli_args = parser.parse_args() 259 cli = FlameGraphCLI(cli_args) 260 261 process_event = cli.process_event 262 trace_end = cli.trace_end 263