15287f926SAndreas Gerstmayr# flamegraph.py - create flame graphs from perf samples 25287f926SAndreas Gerstmayr# SPDX-License-Identifier: GPL-2.0 35287f926SAndreas Gerstmayr# 45287f926SAndreas Gerstmayr# Usage: 55287f926SAndreas Gerstmayr# 65287f926SAndreas Gerstmayr# perf record -a -g -F 99 sleep 60 75287f926SAndreas Gerstmayr# perf script report flamegraph 85287f926SAndreas Gerstmayr# 95287f926SAndreas Gerstmayr# Combined: 105287f926SAndreas Gerstmayr# 115287f926SAndreas Gerstmayr# perf script flamegraph -a -F 99 sleep 60 125287f926SAndreas Gerstmayr# 135287f926SAndreas Gerstmayr# Written by Andreas Gerstmayr <[email protected]> 145287f926SAndreas Gerstmayr# Flame Graphs invented by Brendan Gregg <[email protected]> 155287f926SAndreas Gerstmayr# Works in tandem with d3-flame-graph by Martin Spier <[email protected]> 16c611e4f2SAndreas Gerstmayr# 17c611e4f2SAndreas Gerstmayr# pylint: disable=missing-module-docstring 18c611e4f2SAndreas Gerstmayr# pylint: disable=missing-class-docstring 19c611e4f2SAndreas Gerstmayr# pylint: disable=missing-function-docstring 205287f926SAndreas Gerstmayr 215287f926SAndreas Gerstmayrfrom __future__ import print_function 225287f926SAndreas Gerstmayrimport argparse 23*b430d243SIan Rogersimport hashlib 24*b430d243SIan Rogersimport io 255287f926SAndreas Gerstmayrimport json 26*b430d243SIan Rogersimport os 27c611e4f2SAndreas Gerstmayrimport subprocess 28*b430d243SIan Rogersimport sys 29*b430d243SIan Rogersimport urllib.request 30*b430d243SIan Rogers 31*b430d243SIan Rogersminimal_html = """<head> 32*b430d243SIan Rogers <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/d3-flamegraph.css"> 33*b430d243SIan Rogers</head> 34*b430d243SIan Rogers<body> 35*b430d243SIan Rogers <div id="chart"></div> 36*b430d243SIan Rogers <script type="text/javascript" src="https://d3js.org/d3.v7.js"></script> 37*b430d243SIan Rogers <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/d3-flamegraph.min.js"></script> 38*b430d243SIan Rogers <script type="text/javascript"> 39*b430d243SIan Rogers const stacks = [/** @flamegraph_json **/]; 40*b430d243SIan Rogers // Note, options is unused. 41*b430d243SIan Rogers const options = [/** @options_json **/]; 42*b430d243SIan Rogers 43*b430d243SIan Rogers var chart = flamegraph(); 44*b430d243SIan Rogers d3.select("#chart") 45*b430d243SIan Rogers .datum(stacks[0]) 46*b430d243SIan Rogers .call(chart); 47*b430d243SIan Rogers </script> 48*b430d243SIan Rogers</body> 49*b430d243SIan Rogers""" 505287f926SAndreas Gerstmayr 51c611e4f2SAndreas Gerstmayr# pylint: disable=too-few-public-methods 525287f926SAndreas Gerstmayrclass Node: 53c611e4f2SAndreas Gerstmayr def __init__(self, name, libtype): 545287f926SAndreas Gerstmayr self.name = name 55c611e4f2SAndreas Gerstmayr # "root" | "kernel" | "" 56c611e4f2SAndreas Gerstmayr # "" indicates user space 575287f926SAndreas Gerstmayr self.libtype = libtype 585287f926SAndreas Gerstmayr self.value = 0 595287f926SAndreas Gerstmayr self.children = [] 605287f926SAndreas Gerstmayr 61c611e4f2SAndreas Gerstmayr def to_json(self): 625287f926SAndreas Gerstmayr return { 635287f926SAndreas Gerstmayr "n": self.name, 645287f926SAndreas Gerstmayr "l": self.libtype, 655287f926SAndreas Gerstmayr "v": self.value, 665287f926SAndreas Gerstmayr "c": self.children 675287f926SAndreas Gerstmayr } 685287f926SAndreas Gerstmayr 695287f926SAndreas Gerstmayr 705287f926SAndreas Gerstmayrclass FlameGraphCLI: 715287f926SAndreas Gerstmayr def __init__(self, args): 725287f926SAndreas Gerstmayr self.args = args 73c611e4f2SAndreas Gerstmayr self.stack = Node("all", "root") 745287f926SAndreas Gerstmayr 75c611e4f2SAndreas Gerstmayr @staticmethod 76c611e4f2SAndreas Gerstmayr def get_libtype_from_dso(dso): 77c611e4f2SAndreas Gerstmayr """ 78c611e4f2SAndreas Gerstmayr when kernel-debuginfo is installed, 79c611e4f2SAndreas Gerstmayr dso points to /usr/lib/debug/lib/modules/*/vmlinux 80c611e4f2SAndreas Gerstmayr """ 81c611e4f2SAndreas Gerstmayr if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")): 82c611e4f2SAndreas Gerstmayr return "kernel" 835287f926SAndreas Gerstmayr 84c611e4f2SAndreas Gerstmayr return "" 85c611e4f2SAndreas Gerstmayr 86c611e4f2SAndreas Gerstmayr @staticmethod 87c611e4f2SAndreas Gerstmayr def find_or_create_node(node, name, libtype): 885287f926SAndreas Gerstmayr for child in node.children: 89c611e4f2SAndreas Gerstmayr if child.name == name: 905287f926SAndreas Gerstmayr return child 915287f926SAndreas Gerstmayr 925287f926SAndreas Gerstmayr child = Node(name, libtype) 935287f926SAndreas Gerstmayr node.children.append(child) 945287f926SAndreas Gerstmayr return child 955287f926SAndreas Gerstmayr 965287f926SAndreas Gerstmayr def process_event(self, event): 97c611e4f2SAndreas Gerstmayr pid = event.get("sample", {}).get("pid", 0) 98c611e4f2SAndreas Gerstmayr # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux 99c611e4f2SAndreas Gerstmayr # for user-space processes; let's use pid for kernel or user-space distinction 100c611e4f2SAndreas Gerstmayr if pid == 0: 101c611e4f2SAndreas Gerstmayr comm = event["comm"] 102c611e4f2SAndreas Gerstmayr libtype = "kernel" 1035287f926SAndreas Gerstmayr else: 104c611e4f2SAndreas Gerstmayr comm = "{} ({})".format(event["comm"], pid) 105c611e4f2SAndreas Gerstmayr libtype = "" 106c611e4f2SAndreas Gerstmayr node = self.find_or_create_node(self.stack, comm, libtype) 107c611e4f2SAndreas Gerstmayr 108c611e4f2SAndreas Gerstmayr if "callchain" in event: 109c611e4f2SAndreas Gerstmayr for entry in reversed(event["callchain"]): 110c611e4f2SAndreas Gerstmayr name = entry.get("sym", {}).get("name", "[unknown]") 111c611e4f2SAndreas Gerstmayr libtype = self.get_libtype_from_dso(entry.get("dso")) 112c611e4f2SAndreas Gerstmayr node = self.find_or_create_node(node, name, libtype) 113c611e4f2SAndreas Gerstmayr else: 114c611e4f2SAndreas Gerstmayr name = event.get("symbol", "[unknown]") 115c611e4f2SAndreas Gerstmayr libtype = self.get_libtype_from_dso(event.get("dso")) 116c611e4f2SAndreas Gerstmayr node = self.find_or_create_node(node, name, libtype) 1175287f926SAndreas Gerstmayr node.value += 1 1185287f926SAndreas Gerstmayr 119c611e4f2SAndreas Gerstmayr def get_report_header(self): 120c611e4f2SAndreas Gerstmayr if self.args.input == "-": 121c611e4f2SAndreas Gerstmayr # when this script is invoked with "perf script flamegraph", 122c611e4f2SAndreas Gerstmayr # no perf.data is created and we cannot read the header of it 123c611e4f2SAndreas Gerstmayr return "" 124c611e4f2SAndreas Gerstmayr 125c611e4f2SAndreas Gerstmayr try: 126c611e4f2SAndreas Gerstmayr output = subprocess.check_output(["perf", "report", "--header-only"]) 127c611e4f2SAndreas Gerstmayr return output.decode("utf-8") 128c611e4f2SAndreas Gerstmayr except Exception as err: # pylint: disable=broad-except 129c611e4f2SAndreas Gerstmayr print("Error reading report header: {}".format(err), file=sys.stderr) 130c611e4f2SAndreas Gerstmayr return "" 131c611e4f2SAndreas Gerstmayr 1325287f926SAndreas Gerstmayr def trace_end(self): 133c611e4f2SAndreas Gerstmayr stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) 1345287f926SAndreas Gerstmayr 1355287f926SAndreas Gerstmayr if self.args.format == "html": 136c611e4f2SAndreas Gerstmayr report_header = self.get_report_header() 137c611e4f2SAndreas Gerstmayr options = { 138c611e4f2SAndreas Gerstmayr "colorscheme": self.args.colorscheme, 139c611e4f2SAndreas Gerstmayr "context": report_header 140c611e4f2SAndreas Gerstmayr } 141c611e4f2SAndreas Gerstmayr options_json = json.dumps(options) 142c611e4f2SAndreas Gerstmayr 143*b430d243SIan Rogers template_md5sum = None 144*b430d243SIan Rogers if self.args.format == "html": 145*b430d243SIan Rogers if os.path.isfile(self.args.template): 146*b430d243SIan Rogers template = f"file://{self.args.template}" 147*b430d243SIan Rogers else: 148*b430d243SIan Rogers if not self.args.allow_download: 149*b430d243SIan Rogers print(f"""Warning: Flame Graph template '{self.args.template}' 150*b430d243SIan Rogersdoes not exist. To avoid this please install a package such as the 151*b430d243SIan Rogersjs-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame 152*b430d243SIan Rogersgraph template (--template PATH) or use another output format (--format 153*b430d243SIan RogersFORMAT).""", 154*b430d243SIan Rogers file=sys.stderr) 155*b430d243SIan Rogers if self.args.input == "-": 156*b430d243SIan Rogers print("""Not attempting to download Flame Graph template as script command line 157*b430d243SIan Rogersinput is disabled due to using live mode. If you want to download the 158*b430d243SIan Rogerstemplate retry without live mode. For example, use 'perf record -a -g 159*b430d243SIan Rogers-F 99 sleep 60' and 'perf script report flamegraph'. Alternatively, 160*b430d243SIan Rogersdownload the template from: 161*b430d243SIan Rogershttps://cdn.jsdelivr.net/npm/[email protected]/dist/templates/d3-flamegraph-base.html 162*b430d243SIan Rogersand place it at: 163*b430d243SIan Rogers/usr/share/d3-flame-graph/d3-flamegraph-base.html""", 164*b430d243SIan Rogers file=sys.stderr) 165*b430d243SIan Rogers quit() 166*b430d243SIan Rogers s = None 167*b430d243SIan Rogers while s != "y" and s != "n": 168*b430d243SIan Rogers s = input("Do you wish to download a template from cdn.jsdelivr.net? (this warning can be suppressed with --allow-download) [yn] ").lower() 169*b430d243SIan Rogers if s == "n": 170*b430d243SIan Rogers quit() 171*b430d243SIan Rogers template = "https://cdn.jsdelivr.net/npm/[email protected]/dist/templates/d3-flamegraph-base.html" 172*b430d243SIan Rogers template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" 173*b430d243SIan Rogers 1745287f926SAndreas Gerstmayr try: 175*b430d243SIan Rogers with urllib.request.urlopen(template) as template: 176*b430d243SIan Rogers output_str = "".join([ 177*b430d243SIan Rogers l.decode("utf-8") for l in template.readlines() 178*b430d243SIan Rogers ]) 179*b430d243SIan Rogers except Exception as err: 180*b430d243SIan Rogers print(f"Error reading template {template}: {err}\n" 181*b430d243SIan Rogers "a minimal flame graph will be generated", file=sys.stderr) 182*b430d243SIan Rogers output_str = minimal_html 183*b430d243SIan Rogers template_md5sum = None 184*b430d243SIan Rogers 185*b430d243SIan Rogers if template_md5sum: 186*b430d243SIan Rogers download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest() 187*b430d243SIan Rogers if download_md5sum != template_md5sum: 188*b430d243SIan Rogers s = None 189*b430d243SIan Rogers while s != "y" and s != "n": 190*b430d243SIan Rogers s = input(f"""Unexpected template md5sum. 191*b430d243SIan Rogers{download_md5sum} != {template_md5sum}, for: 192*b430d243SIan Rogers{output_str} 193*b430d243SIan Rogerscontinue?[yn] """).lower() 194*b430d243SIan Rogers if s == "n": 195*b430d243SIan Rogers quit() 196*b430d243SIan Rogers 197*b430d243SIan Rogers output_str = output_str.replace("/** @options_json **/", options_json) 198*b430d243SIan Rogers output_str = output_str.replace("/** @flamegraph_json **/", stacks_json) 199*b430d243SIan Rogers 2005287f926SAndreas Gerstmayr output_fn = self.args.output or "flamegraph.html" 2015287f926SAndreas Gerstmayr else: 202c611e4f2SAndreas Gerstmayr output_str = stacks_json 2035287f926SAndreas Gerstmayr output_fn = self.args.output or "stacks.json" 2045287f926SAndreas Gerstmayr 2055287f926SAndreas Gerstmayr if output_fn == "-": 206c42ad5d4SAndreas Gerstmayr with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out: 207c42ad5d4SAndreas Gerstmayr out.write(output_str) 2085287f926SAndreas Gerstmayr else: 2095287f926SAndreas Gerstmayr print("dumping data to {}".format(output_fn)) 2105287f926SAndreas Gerstmayr try: 211c42ad5d4SAndreas Gerstmayr with io.open(output_fn, "w", encoding="utf-8") as out: 2125287f926SAndreas Gerstmayr out.write(output_str) 213c611e4f2SAndreas Gerstmayr except IOError as err: 214c611e4f2SAndreas Gerstmayr print("Error writing output file: {}".format(err), file=sys.stderr) 2155287f926SAndreas Gerstmayr sys.exit(1) 2165287f926SAndreas Gerstmayr 2175287f926SAndreas Gerstmayr 2185287f926SAndreas Gerstmayrif __name__ == "__main__": 2195287f926SAndreas Gerstmayr parser = argparse.ArgumentParser(description="Create flame graphs.") 2205287f926SAndreas Gerstmayr parser.add_argument("-f", "--format", 2215287f926SAndreas Gerstmayr default="html", choices=["json", "html"], 2225287f926SAndreas Gerstmayr help="output file format") 2235287f926SAndreas Gerstmayr parser.add_argument("-o", "--output", 2245287f926SAndreas Gerstmayr help="output file name") 2255287f926SAndreas Gerstmayr parser.add_argument("--template", 2265287f926SAndreas Gerstmayr default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", 2275287f926SAndreas Gerstmayr help="path to flame graph HTML template") 228c611e4f2SAndreas Gerstmayr parser.add_argument("--colorscheme", 229c611e4f2SAndreas Gerstmayr default="blue-green", 230c611e4f2SAndreas Gerstmayr help="flame graph color scheme", 231c611e4f2SAndreas Gerstmayr choices=["blue-green", "orange"]) 2325287f926SAndreas Gerstmayr parser.add_argument("-i", "--input", 2335287f926SAndreas Gerstmayr help=argparse.SUPPRESS) 234*b430d243SIan Rogers parser.add_argument("--allow-download", 235*b430d243SIan Rogers default=False, 236*b430d243SIan Rogers action="store_true", 237*b430d243SIan Rogers help="allow unprompted downloading of HTML template") 2385287f926SAndreas Gerstmayr 239c611e4f2SAndreas Gerstmayr cli_args = parser.parse_args() 240c611e4f2SAndreas Gerstmayr cli = FlameGraphCLI(cli_args) 2415287f926SAndreas Gerstmayr 2425287f926SAndreas Gerstmayr process_event = cli.process_event 2435287f926SAndreas Gerstmayr trace_end = cli.trace_end 244