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 | 19 | # pylint: disable=missing-function-docstring |
| 20 | 20 | |
| 21 | 21 | from __future__ import print_function |
| 22 | −import sys | |
| 23 | −import os | |
| 24 | −import io | |
| 25 | 22 | import argparse |
| 23 | +import hashlib | |
| 24 | +import io | |
| 26 | 25 | import json |
| 26 | +import os | |
| 27 | 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 | 51 | # pylint: disable=too-few-public-methods |
| 30 | 52 | class Node: |
| @@ -50,16 +72,6 @@ class FlameGraphCLI: · 19 unmodified lines | ||
| 50 | 72 | self.args = args |
| 51 | 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 | 75 | @staticmethod |
| 64 | 76 | def get_libtype_from_dso(dso): |
| 65 | 77 | """ |
| @@ -128,16 +140,63 @@ class FlameGraphCLI: · 62 unmodified lines | ||
| 128 | 140 | } |
| 129 | 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 | 174 | 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) | |
| 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} | |
| 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 | 200 | output_fn = self.args.output or "flamegraph.html" |
| 142 | 201 | else: |
| 143 | 202 | output_str = stacks_json |
| @@ -172,6 +231,10 @@ if __name__ == "__main__": · 28 unmodified lines | ||
| 172 | 231 | choices=["blue-green", "orange"]) |
| 173 | 232 | parser.add_argument("-i", "--input", |
| 174 | 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 | 239 | cli_args = parser.parse_args() |
| 177 | 240 | cli = FlameGraphCLI(cli_args) |