History

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

Ian Rogers committed 3 years ago

Commit b430d241 parent 72879041 file · +85 −22Browse files

Showing the changes to tools/perf/scripts/python (1 of 1 file). Show all

Unified · Split

tools/perf/scripts/python/flamegraph.py+85−22
@@ -19,12 +19,34 @@ · 18 unmodified lines
1919 # pylint: disable=missing-function-docstring
2020
2121 from __future__ import print_function
22−import sys
23−import os
24−import io
2522 import argparse
23+import hashlib
24+import io
2625 import json
26+import os
2727 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+"""
2850
2951 # pylint: disable=too-few-public-methods
3052 class Node:
@@ -50,16 +72,6 @@ class FlameGraphCLI: · 19 unmodified lines
5072 self.args = args
5173 self.stack = Node("all", "root")
5274
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−
6375 @staticmethod
6476 def get_libtype_from_dso(dso):
6577 """
@@ -128,16 +140,63 @@ class FlameGraphCLI: · 62 unmodified lines
128140 }
129141 options_json = json.dumps(options)
130142
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+
131174 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+
141200 output_fn = self.args.output or "flamegraph.html"
142201 else:
143202 output_str = stacks_json
@@ -172,6 +231,10 @@ if __name__ == "__main__": · 28 unmodified lines
172231 choices=["blue-green", "orange"])
173232 parser.add_argument("-i", "--input",
174233 help=argparse.SUPPRESS)
234+ parser.add_argument("--allow-download",
235+ default=False,
236+ action="store_true",
237+ help="allow unprompted downloading of HTML template")
175238
176239 cli_args = parser.parse_args()
177240 cli = FlameGraphCLI(cli_args)