xref: /linux/tools/perf/scripts/python/flamegraph.py (revision be59dba332e1e8edd3e88d991ba0e4795ae2bcb2)
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