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/flamegraph.py (1 of 1 file). Show all

Unified · Split

tools/perf/scripts/python/flamegraph.py+85−22
@@ -19,12 +19,34 @@ · 18 unmodified lines
19# pylint: disable=missing-function-docstring19# pylint: disable=missing-function-docstring
20 20
21from __future__ import print_function21from __future__ import print_function
22import sys
23import os
24import io
25import argparse22import argparse
23import hashlib
24import io
26import json25import json
26import os
27import subprocess27import 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"""
28 50
29# pylint: disable=too-few-public-methods51# pylint: disable=too-few-public-methods
30class Node:52class Node:
@@ -50,16 +72,6 @@ class FlameGraphCLI: · 19 unmodified lines
50 self.args = args72 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 @staticmethod75 @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}'
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
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}
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
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_json202 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)