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