244 lines · python
1# flamegraph.py - create flame graphs from perf samples2# SPDX-License-Identifier: GPL-2.03#4# Usage:5#6# perf record -a -g -F 99 sleep 607# perf script report flamegraph8#9# Combined:10#11# perf script flamegraph -a -F 99 sleep 6012#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-docstring18# pylint: disable=missing-class-docstring19# pylint: disable=missing-function-docstring20 21from __future__ import print_function22import argparse23import hashlib24import io25import json26import os27import subprocess28import sys29import urllib.request30 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-methods52class Node:53 def __init__(self, name, libtype):54 self.name = name55 # "root" | "kernel" | ""56 # "" indicates user space57 self.libtype = libtype58 self.value = 059 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.children67 }68 69 70class FlameGraphCLI:71 def __init__(self, args):72 self.args = args73 self.stack = Node("all", "root")74 75 @staticmethod76 def get_libtype_from_dso(dso):77 """78 when kernel-debuginfo is installed,79 dso points to /usr/lib/debug/lib/modules/*/vmlinux80 """81 if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")):82 return "kernel"83 84 return ""85 86 @staticmethod87 def find_or_create_node(node, name, libtype):88 for child in node.children:89 if child.name == name:90 return child91 92 child = Node(name, libtype)93 node.children.append(child)94 return child95 96 def process_event(self, event):97 pid = event.get("sample", {}).get("pid", 0)98 # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux99 # for user-space processes; let's use pid for kernel or user-space distinction100 if pid == 0:101 comm = event["comm"]102 libtype = "kernel"103 else:104 comm = "{} ({})".format(event["comm"], pid)105 libtype = ""106 node = self.find_or_create_node(self.stack, comm, libtype)107 108 if "callchain" in event:109 for entry in reversed(event["callchain"]):110 name = entry.get("sym", {}).get("name", "[unknown]")111 libtype = self.get_libtype_from_dso(entry.get("dso"))112 node = self.find_or_create_node(node, name, libtype)113 else:114 name = event.get("symbol", "[unknown]")115 libtype = self.get_libtype_from_dso(event.get("dso"))116 node = self.find_or_create_node(node, name, libtype)117 node.value += 1118 119 def get_report_header(self):120 if self.args.input == "-":121 # when this script is invoked with "perf script flamegraph",122 # no perf.data is created and we cannot read the header of it123 return ""124 125 try:126 output = subprocess.check_output(["perf", "report", "--header-only"])127 return output.decode("utf-8")128 except Exception as err: # pylint: disable=broad-except129 print("Error reading report header: {}".format(err), file=sys.stderr)130 return ""131 132 def trace_end(self):133 stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())134 135 if self.args.format == "html":136 report_header = self.get_report_header()137 options = {138 "colorscheme": self.args.colorscheme,139 "context": report_header140 }141 options_json = json.dumps(options)142 143 template_md5sum = None144 if self.args.format == "html":145 if os.path.isfile(self.args.template):146 template = f"file://{self.args.template}"147 else:148 if not self.args.allow_download:149 print(f"""Warning: Flame Graph template '{self.args.template}'150does not exist. To avoid this please install a package such as the151js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame152graph template (--template PATH) or use another output format (--format153FORMAT).""",154 file=sys.stderr)155 if self.args.input == "-":156 print("""Not attempting to download Flame Graph template as script command line157input is disabled due to using live mode. If you want to download the158template retry without live mode. For example, use 'perf record -a -g159-F 99 sleep 60' and 'perf script report flamegraph'. Alternatively,160download the template from:161https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html162and place it at:163/usr/share/d3-flame-graph/d3-flamegraph-base.html""",164 file=sys.stderr)165 quit()166 s = None167 while s != "y" and s != "n":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":170 quit()171 template = "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html"172 template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36"173 174 try:175 with urllib.request.urlopen(template) as template:176 output_str = "".join([177 l.decode("utf-8") for l in template.readlines()178 ])179 except Exception as err:180 print(f"Error reading template {template}: {err}\n"181 "a minimal flame graph will be generated", file=sys.stderr)182 output_str = minimal_html183 template_md5sum = None184 185 if template_md5sum:186 download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest()187 if download_md5sum != template_md5sum:188 s = None189 while s != "y" and s != "n":190 s = input(f"""Unexpected template md5sum.191{download_md5sum} != {template_md5sum}, for:192{output_str}193continue?[yn] """).lower()194 if s == "n":195 quit()196 197 output_str = output_str.replace("/** @options_json **/", options_json)198 output_str = output_str.replace("/** @flamegraph_json **/", stacks_json)199 200 output_fn = self.args.output or "flamegraph.html"201 else:202 output_str = stacks_json203 output_fn = self.args.output or "stacks.json"204 205 if output_fn == "-":206 with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:207 out.write(output_str)208 else:209 print("dumping data to {}".format(output_fn))210 try:211 with io.open(output_fn, "w", encoding="utf-8") as out:212 out.write(output_str)213 except IOError as err:214 print("Error writing output file: {}".format(err), file=sys.stderr)215 sys.exit(1)216 217 218if __name__ == "__main__":219 parser = argparse.ArgumentParser(description="Create flame graphs.")220 parser.add_argument("-f", "--format",221 default="html", choices=["json", "html"],222 help="output file format")223 parser.add_argument("-o", "--output",224 help="output file name")225 parser.add_argument("--template",226 default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",227 help="path to flame graph HTML template")228 parser.add_argument("--colorscheme",229 default="blue-green",230 help="flame graph color scheme",231 choices=["blue-green", "orange"])232 parser.add_argument("-i", "--input",233 help=argparse.SUPPRESS)234 parser.add_argument("--allow-download",235 default=False,236 action="store_true",237 help="allow unprompted downloading of HTML template")238 239 cli_args = parser.parse_args()240 cli = FlameGraphCLI(cli_args)241 242 process_event = cli.process_event243 trace_end = cli.trace_end244