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