Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

perf script flamegraph: Avoid d3-flame-graph package dependency

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

Reviewed-by: Andreas Gerstmayr <agerstmayr@redhat.com>
Signed-off-by: Ian Rogers <irogers@google.com>
Cc: 996839@bugs.debian.org
Cc: Alexander Shishkin <alexander.shishkin@linux.intel.com>
Cc: Brendan Gregg <brendan@intel.com>
Cc: Ingo Molnar <mingo@redhat.com>
Cc: Jiri Olsa <jolsa@kernel.org>
Cc: Mark Rutland <mark.rutland@arm.com>
Cc: Martin Spier <spiermar@gmail.com>
Cc: Namhyung Kim <namhyung@kernel.org>
Cc: Peter Zijlstra <peterz@infradead.org>
Link: https://lore.kernel.org/r/20230118072409.147786-1-irogers@google.com # v3 discussion
Link: https://lore.kernel.org/r/20230112220024.32709-1-irogers@google.com # v2 discussion
Link: https://lore.kernel.org/r/CAP-5=fXi_9zdhTAoYApiFQoLURAvpEatFzU3uL23o3zs=z25ZQ@mail.gmail.com # v1 discussion
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>

authored by

Ian Rogers and committed by
Arnaldo Carvalho de Melo
b430d243 7287904c

+85 -22
+85 -22
tools/perf/scripts/python/flamegraph.py
··· 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: ··· 71 49 def __init__(self, args): 72 50 self.args = args 73 51 self.stack = Node("all", "root") 74 - 75 - if self.args.format == "html" and \ 76 - not os.path.isfile(self.args.template): 77 - print("Flame Graph template {} does not exist. Please install " 78 - "the js-d3-flame-graph (RPM) or libjs-d3-flame-graph (deb) " 79 - "package, specify an existing flame graph template " 80 - "(--template PATH) or another output format " 81 - "(--format FORMAT).".format(self.args.template), 82 - file=sys.stderr) 83 - sys.exit(1) 84 52 85 53 @staticmethod 86 54 def get_libtype_from_dso(dso): ··· 140 128 } 141 129 options_json = json.dumps(options) 142 130 131 + template_md5sum = None 132 + if self.args.format == "html": 133 + if os.path.isfile(self.args.template): 134 + template = f"file://{self.args.template}" 135 + else: 136 + if not self.args.allow_download: 137 + print(f"""Warning: Flame Graph template '{self.args.template}' 138 + does not exist. To avoid this please install a package such as the 139 + js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame 140 + graph template (--template PATH) or use another output format (--format 141 + FORMAT).""", 142 + file=sys.stderr) 143 + if self.args.input == "-": 144 + print("""Not attempting to download Flame Graph template as script command line 145 + input is disabled due to using live mode. If you want to download the 146 + template retry without live mode. For example, use 'perf record -a -g 147 + -F 99 sleep 60' and 'perf script report flamegraph'. Alternatively, 148 + download the template from: 149 + https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html 150 + and place it at: 151 + /usr/share/d3-flame-graph/d3-flamegraph-base.html""", 152 + file=sys.stderr) 153 + quit() 154 + s = None 155 + while s != "y" and s != "n": 156 + s = input("Do you wish to download a template from cdn.jsdelivr.net? (this warning can be suppressed with --allow-download) [yn] ").lower() 157 + if s == "n": 158 + quit() 159 + template = "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html" 160 + template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" 161 + 143 162 try: 144 - with io.open(self.args.template, encoding="utf-8") as template: 145 - output_str = ( 146 - template.read() 147 - .replace("/** @options_json **/", options_json) 148 - .replace("/** @flamegraph_json **/", stacks_json) 149 - ) 150 - except IOError as err: 151 - print("Error reading template file: {}".format(err), file=sys.stderr) 152 - sys.exit(1) 163 + with urllib.request.urlopen(template) as template: 164 + output_str = "".join([ 165 + l.decode("utf-8") for l in template.readlines() 166 + ]) 167 + except Exception as err: 168 + print(f"Error reading template {template}: {err}\n" 169 + "a minimal flame graph will be generated", file=sys.stderr) 170 + output_str = minimal_html 171 + template_md5sum = None 172 + 173 + if template_md5sum: 174 + download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest() 175 + if download_md5sum != template_md5sum: 176 + s = None 177 + while s != "y" and s != "n": 178 + s = input(f"""Unexpected template md5sum. 179 + {download_md5sum} != {template_md5sum}, for: 180 + {output_str} 181 + continue?[yn] """).lower() 182 + if s == "n": 183 + quit() 184 + 185 + output_str = output_str.replace("/** @options_json **/", options_json) 186 + output_str = output_str.replace("/** @flamegraph_json **/", stacks_json) 187 + 153 188 output_fn = self.args.output or "flamegraph.html" 154 189 else: 155 190 output_str = stacks_json ··· 231 172 choices=["blue-green", "orange"]) 232 173 parser.add_argument("-i", "--input", 233 174 help=argparse.SUPPRESS) 175 + parser.add_argument("--allow-download", 176 + default=False, 177 + action="store_true", 178 + help="allow unprompted downloading of HTML template") 234 179 235 180 cli_args = parser.parse_args() 236 181 cli = FlameGraphCLI(cli_args)