Linux Audio

Check our new training course

Loading...
v6.2
  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 sys
 23import os
 24import io
 25import argparse
 
 
 26import json
 
 27import subprocess
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 28
 29# pylint: disable=too-few-public-methods
 30class Node:
 31    def __init__(self, name, libtype):
 32        self.name = name
 33        # "root" | "kernel" | ""
 34        # "" indicates user space
 35        self.libtype = libtype
 36        self.value = 0
 37        self.children = []
 38
 39    def to_json(self):
 40        return {
 41            "n": self.name,
 42            "l": self.libtype,
 43            "v": self.value,
 44            "c": self.children
 45        }
 46
 47
 48class FlameGraphCLI:
 49    def __init__(self, args):
 50        self.args = args
 51        self.stack = Node("all", "root")
 52
 53        if self.args.format == "html" and \
 54                not os.path.isfile(self.args.template):
 55            print("Flame Graph template {} does not exist. Please install "
 56                  "the js-d3-flame-graph (RPM) or libjs-d3-flame-graph (deb) "
 57                  "package, specify an existing flame graph template "
 58                  "(--template PATH) or another output format "
 59                  "(--format FORMAT).".format(self.args.template),
 60                  file=sys.stderr)
 61            sys.exit(1)
 62
 63    @staticmethod
 64    def get_libtype_from_dso(dso):
 65        """
 66        when kernel-debuginfo is installed,
 67        dso points to /usr/lib/debug/lib/modules/*/vmlinux
 68        """
 69        if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")):
 70            return "kernel"
 71
 72        return ""
 73
 74    @staticmethod
 75    def find_or_create_node(node, name, libtype):
 76        for child in node.children:
 77            if child.name == name:
 78                return child
 79
 80        child = Node(name, libtype)
 81        node.children.append(child)
 82        return child
 83
 84    def process_event(self, event):
 85        pid = event.get("sample", {}).get("pid", 0)
 86        # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux
 87        # for user-space processes; let's use pid for kernel or user-space distinction
 88        if pid == 0:
 89            comm = event["comm"]
 90            libtype = "kernel"
 91        else:
 92            comm = "{} ({})".format(event["comm"], pid)
 93            libtype = ""
 94        node = self.find_or_create_node(self.stack, comm, libtype)
 95
 96        if "callchain" in event:
 97            for entry in reversed(event["callchain"]):
 98                name = entry.get("sym", {}).get("name", "[unknown]")
 99                libtype = self.get_libtype_from_dso(entry.get("dso"))
100                node = self.find_or_create_node(node, name, libtype)
101        else:
102            name = event.get("symbol", "[unknown]")
103            libtype = self.get_libtype_from_dso(event.get("dso"))
104            node = self.find_or_create_node(node, name, libtype)
105        node.value += 1
106
107    def get_report_header(self):
108        if self.args.input == "-":
109            # when this script is invoked with "perf script flamegraph",
110            # no perf.data is created and we cannot read the header of it
111            return ""
112
113        try:
114            output = subprocess.check_output(["perf", "report", "--header-only"])
115            return output.decode("utf-8")
116        except Exception as err:  # pylint: disable=broad-except
117            print("Error reading report header: {}".format(err), file=sys.stderr)
118            return ""
119
120    def trace_end(self):
121        stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())
122
123        if self.args.format == "html":
124            report_header = self.get_report_header()
125            options = {
126                "colorscheme": self.args.colorscheme,
127                "context": report_header
128            }
129            options_json = json.dumps(options)
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131            try:
132                with io.open(self.args.template, encoding="utf-8") as template:
133                    output_str = (
134                        template.read()
135                        .replace("/** @options_json **/", options_json)
136                        .replace("/** @flamegraph_json **/", stacks_json)
137                    )
138            except IOError as err:
139                print("Error reading template file: {}".format(err), file=sys.stderr)
140                sys.exit(1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141            output_fn = self.args.output or "flamegraph.html"
142        else:
143            output_str = stacks_json
144            output_fn = self.args.output or "stacks.json"
145
146        if output_fn == "-":
147            with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:
148                out.write(output_str)
149        else:
150            print("dumping data to {}".format(output_fn))
151            try:
152                with io.open(output_fn, "w", encoding="utf-8") as out:
153                    out.write(output_str)
154            except IOError as err:
155                print("Error writing output file: {}".format(err), file=sys.stderr)
156                sys.exit(1)
157
158
159if __name__ == "__main__":
160    parser = argparse.ArgumentParser(description="Create flame graphs.")
161    parser.add_argument("-f", "--format",
162                        default="html", choices=["json", "html"],
163                        help="output file format")
164    parser.add_argument("-o", "--output",
165                        help="output file name")
166    parser.add_argument("--template",
167                        default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",
168                        help="path to flame graph HTML template")
169    parser.add_argument("--colorscheme",
170                        default="blue-green",
171                        help="flame graph color scheme",
172                        choices=["blue-green", "orange"])
173    parser.add_argument("-i", "--input",
174                        help=argparse.SUPPRESS)
 
 
 
 
175
176    cli_args = parser.parse_args()
177    cli = FlameGraphCLI(cli_args)
178
179    process_event = cli.process_event
180    trace_end = cli.trace_end
v6.9.4
  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        pid = event.get("sample", {}).get("pid", 0)
 98        # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux
 99        # for user-space processes; let's use pid for kernel or user-space distinction
100        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 += 1
118
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 it
123            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-except
129            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_header
140            }
141            options_json = json.dumps(options)
142
143            template_md5sum = None
144            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 the
151js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame
152graph template (--template PATH) or use another output format (--format
153FORMAT).""",
154                              file=sys.stderr)
155                        if self.args.input == "-":
156                            print("""Not attempting to download Flame Graph template as script command line
157input is disabled due to using live mode. If you want to download the
158template retry without live mode. For example, use 'perf record -a -g
159-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.html
162and place it at:
163/usr/share/d3-flame-graph/d3-flamegraph-base.html""",
164                                  file=sys.stderr)
165                            quit()
166                        s = None
167                        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_html
183                template_md5sum = None
184
185            if template_md5sum:
186                download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest()
187                if download_md5sum != template_md5sum:
188                    s = None
189                    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_json
203            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_event
243    trace_end = cli.trace_end