Currently flame graph generation requires a d3-flame-graph template to be installed. Unfortunately this is hard to come by for things like Debian [1]. If the template isn't installed then ask if it should be downloaded from jsdelivr CDN. The downloaded HTML file is validated against an md5sum. If the download fails, generate a minimal flame graph with the javascript coming from links to jsdelivr CDN. v3. Adds a warning message and quits before download in live mode. v2. Change the warning to a prompt about downloading and add the --allow-download command line flag. Add an md5sum check for the downloaded HTML. [1] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=996839
Showing the changes to tools/perf/scripts/python/flamegraph.py (1 of 1 file). Show all
Unified · Split
| @@ -19,12 +19,34 @@ · 18 unmodified lines | |||
| 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 sys | ||
| 23 | import os | ||
| 24 | import io | ||
| 25 | import argparse | 22 | import argparse |
| 23 | import hashlib | ||
| 24 | import io | ||
| 26 | import json | 25 | import json |
| 26 | import os | ||
| 27 | import subprocess | 27 | import subprocess |
| 28 | import sys | ||
| 29 | import urllib.request | ||
| 30 | |||
| 31 | minimal_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 | """ | ||
| 28 | 50 | ||
| 29 | # pylint: disable=too-few-public-methods | 51 | # pylint: disable=too-few-public-methods |
| 30 | class Node: | 52 | class Node: |
| @@ -50,16 +72,6 @@ class FlameGraphCLI: · 19 unmodified lines | |||
| 50 | self.args = args | 72 | self.args = args |
| 51 | self.stack = Node("all", "root") | 73 | self.stack = Node("all", "root") |
| 52 | 74 | ||
| 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 | 75 | @staticmethod |
| 64 | def get_libtype_from_dso(dso): | 76 | def get_libtype_from_dso(dso): |
| 65 | """ | 77 | """ |
| @@ -128,16 +140,63 @@ class FlameGraphCLI: · 62 unmodified lines | |||
| 128 | } | 140 | } |
| 129 | options_json = json.dumps(options) | 141 | options_json = json.dumps(options) |
| 130 | 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}' | ||
| 150 | does not exist. To avoid this please install a package such as the | ||
| 151 | js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame | ||
| 152 | graph template (--template PATH) or use another output format (--format | ||
| 153 | FORMAT).""", | ||
| 154 | file=sys.stderr) | ||
| 155 | if self.args.input == "-": | ||
| 156 | print("""Not attempting to download Flame Graph template as script command line | ||
| 157 | input is disabled due to using live mode. If you want to download the | ||
| 158 | template retry without live mode. For example, use 'perf record -a -g | ||
| 159 | -F 99 sleep 60' and 'perf script report flamegraph'. Alternatively, | ||
| 160 | download the template from: | ||
| 161 | https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html | ||
| 162 | and 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 | |||
| 131 | try: | 174 | try: |
| 132 | with io.open(self.args.template, encoding="utf-8") as template: | 175 | with urllib.request.urlopen(template) as template: |
| 133 | output_str = ( | 176 | output_str = "".join([ |
| 134 | template.read() | 177 | l.decode("utf-8") for l in template.readlines() |
| 135 | .replace("/** @options_json **/", options_json) | 178 | ]) |
| 136 | .replace("/** @flamegraph_json **/", stacks_json) | 179 | except Exception as err: |
| 137 | ) | 180 | print(f"Error reading template {template}: {err}\n" |
| 138 | except IOError as err: | 181 | "a minimal flame graph will be generated", file=sys.stderr) |
| 139 | print("Error reading template file: {}".format(err), file=sys.stderr) | 182 | output_str = minimal_html |
| 140 | sys.exit(1) | 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} | ||
| 193 | continue?[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 | |||
| 141 | output_fn = self.args.output or "flamegraph.html" | 200 | output_fn = self.args.output or "flamegraph.html" |
| 142 | else: | 201 | else: |
| 143 | output_str = stacks_json | 202 | output_str = stacks_json |
| @@ -172,6 +231,10 @@ if __name__ == "__main__": · 28 unmodified lines | |||
| 172 | choices=["blue-green", "orange"]) | 231 | choices=["blue-green", "orange"]) |
| 173 | parser.add_argument("-i", "--input", | 232 | parser.add_argument("-i", "--input", |
| 174 | help=argparse.SUPPRESS) | 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") | ||
| 175 | 238 | ||
| 176 | cli_args = parser.parse_args() | 239 | cli_args = parser.parse_args() |
| 177 | cli = FlameGraphCLI(cli_args) | 240 | cli = FlameGraphCLI(cli_args) |