flamegraph.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. # flamegraph.py - create flame graphs from perf samples
  2. # SPDX-License-Identifier: GPL-2.0
  3. #
  4. # Usage:
  5. #
  6. # perf record -a -g -F 99 sleep 60
  7. # perf script report flamegraph
  8. #
  9. # Combined:
  10. #
  11. # perf script flamegraph -a -F 99 sleep 60
  12. #
  13. # Written by Andreas Gerstmayr <agerstmayr@redhat.com>
  14. # Flame Graphs invented by Brendan Gregg <bgregg@netflix.com>
  15. # Works in tandem with d3-flame-graph by Martin Spier <mspier@netflix.com>
  16. #
  17. # pylint: disable=missing-module-docstring
  18. # pylint: disable=missing-class-docstring
  19. # pylint: disable=missing-function-docstring
  20. import argparse
  21. import hashlib
  22. import io
  23. import json
  24. import os
  25. import subprocess
  26. import sys
  27. from typing import Dict, Optional, Union
  28. import urllib.request
  29. MINIMAL_HTML = """<head>
  30. <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css">
  31. </head>
  32. <body>
  33. <div id="chart"></div>
  34. <script type="text/javascript" src="https://d3js.org/d3.v7.js"></script>
  35. <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.min.js"></script>
  36. <script type="text/javascript">
  37. const stacks = [/** @flamegraph_json **/];
  38. // Note, options is unused.
  39. const options = [/** @options_json **/];
  40. var chart = flamegraph();
  41. d3.select("#chart")
  42. .datum(stacks[0])
  43. .call(chart);
  44. </script>
  45. </body>
  46. """
  47. # pylint: disable=too-few-public-methods
  48. class Node:
  49. def __init__(self, name: str, libtype: str):
  50. self.name = name
  51. # "root" | "kernel" | ""
  52. # "" indicates user space
  53. self.libtype = libtype
  54. self.value: int = 0
  55. self.children: list[Node] = []
  56. def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]:
  57. return {
  58. "n": self.name,
  59. "l": self.libtype,
  60. "v": self.value,
  61. "c": [x.to_json() for x in self.children]
  62. }
  63. class FlameGraphCLI:
  64. def __init__(self, args):
  65. self.args = args
  66. self.stack = Node("all", "root")
  67. @staticmethod
  68. def get_libtype_from_dso(dso: Optional[str]) -> str:
  69. """
  70. when kernel-debuginfo is installed,
  71. dso points to /usr/lib/debug/lib/modules/*/vmlinux
  72. """
  73. if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")):
  74. return "kernel"
  75. return ""
  76. @staticmethod
  77. def find_or_create_node(node: Node, name: str, libtype: str) -> Node:
  78. for child in node.children:
  79. if child.name == name:
  80. return child
  81. child = Node(name, libtype)
  82. node.children.append(child)
  83. return child
  84. def process_event(self, event) -> None:
  85. # ignore events where the event name does not match
  86. # the one specified by the user
  87. if self.args.event_name and event.get("ev_name") != self.args.event_name:
  88. return
  89. pid = event.get("sample", {}).get("pid", 0)
  90. # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux
  91. # for user-space processes; let's use pid for kernel or user-space distinction
  92. if pid == 0:
  93. comm = event["comm"]
  94. libtype = "kernel"
  95. else:
  96. comm = f"{event['comm']} ({pid})"
  97. libtype = ""
  98. node = self.find_or_create_node(self.stack, comm, libtype)
  99. if "callchain" in event:
  100. for entry in reversed(event["callchain"]):
  101. name = entry.get("sym", {}).get("name", "[unknown]")
  102. libtype = self.get_libtype_from_dso(entry.get("dso"))
  103. node = self.find_or_create_node(node, name, libtype)
  104. else:
  105. name = event.get("symbol", "[unknown]")
  106. libtype = self.get_libtype_from_dso(event.get("dso"))
  107. node = self.find_or_create_node(node, name, libtype)
  108. node.value += 1
  109. def get_report_header(self) -> str:
  110. if self.args.input == "-":
  111. # when this script is invoked with "perf script flamegraph",
  112. # no perf.data is created and we cannot read the header of it
  113. return ""
  114. try:
  115. # if the file name other than perf.data is given,
  116. # we read the header of that file
  117. if self.args.input:
  118. output = subprocess.check_output(["perf", "report", "--header-only",
  119. "-i", self.args.input])
  120. else:
  121. output = subprocess.check_output(["perf", "report", "--header-only"])
  122. result = output.decode("utf-8")
  123. if self.args.event_name:
  124. result += "\nFocused event: " + self.args.event_name
  125. return result
  126. except Exception as err: # pylint: disable=broad-except
  127. print(f"Error reading report header: {err}", file=sys.stderr)
  128. return ""
  129. def trace_end(self) -> None:
  130. stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())
  131. if self.args.format == "html":
  132. report_header = self.get_report_header()
  133. options = {
  134. "colorscheme": self.args.colorscheme,
  135. "context": report_header
  136. }
  137. options_json = json.dumps(options)
  138. template_md5sum = None
  139. if self.args.format == "html":
  140. if os.path.isfile(self.args.template):
  141. template = f"file://{self.args.template}"
  142. else:
  143. if not self.args.allow_download:
  144. print(f"""Warning: Flame Graph template '{self.args.template}'
  145. does not exist. To avoid this please install a package such as the
  146. js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame
  147. graph template (--template PATH) or use another output format (--format
  148. FORMAT).""",
  149. file=sys.stderr)
  150. if self.args.input == "-":
  151. print(
  152. """Not attempting to download Flame Graph template as script command line
  153. input is disabled due to using live mode. If you want to download the
  154. template retry without live mode. For example, use 'perf record -a -g
  155. -F 99 sleep 60' and 'perf script report flamegraph'. Alternatively,
  156. download the template from:
  157. https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html
  158. and place it at:
  159. /usr/share/d3-flame-graph/d3-flamegraph-base.html""",
  160. file=sys.stderr)
  161. sys.exit(1)
  162. s = None
  163. while s not in ["y", "n"]:
  164. s = input("Do you wish to download a template from cdn.jsdelivr.net?" +
  165. "(this warning can be suppressed with --allow-download) [yn] "
  166. ).lower()
  167. if s == "n":
  168. sys.exit(1)
  169. template = ("https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/"
  170. "d3-flamegraph-base.html")
  171. template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36"
  172. try:
  173. with urllib.request.urlopen(template) as url_template:
  174. output_str = "".join([
  175. l.decode("utf-8") for l in url_template.readlines()
  176. ])
  177. except Exception as err:
  178. print(f"Error reading template {template}: {err}\n"
  179. "a minimal flame graph will be generated", file=sys.stderr)
  180. output_str = MINIMAL_HTML
  181. template_md5sum = None
  182. if template_md5sum:
  183. download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest()
  184. if download_md5sum != template_md5sum:
  185. s = None
  186. while s not in ["y", "n"]:
  187. s = input(f"""Unexpected template md5sum.
  188. {download_md5sum} != {template_md5sum}, for:
  189. {output_str}
  190. continue?[yn] """).lower()
  191. if s == "n":
  192. sys.exit(1)
  193. output_str = output_str.replace("/** @options_json **/", options_json)
  194. output_str = output_str.replace("/** @flamegraph_json **/", stacks_json)
  195. output_fn = self.args.output or "flamegraph.html"
  196. else:
  197. output_str = stacks_json
  198. output_fn = self.args.output or "stacks.json"
  199. if output_fn == "-":
  200. with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:
  201. out.write(output_str)
  202. else:
  203. print(f"dumping data to {output_fn}")
  204. try:
  205. with io.open(output_fn, "w", encoding="utf-8") as out:
  206. out.write(output_str)
  207. except IOError as err:
  208. print(f"Error writing output file: {err}", file=sys.stderr)
  209. sys.exit(1)
  210. if __name__ == "__main__":
  211. parser = argparse.ArgumentParser(description="Create flame graphs.")
  212. parser.add_argument("-f", "--format",
  213. default="html", choices=["json", "html"],
  214. help="output file format")
  215. parser.add_argument("-o", "--output",
  216. help="output file name")
  217. parser.add_argument("--template",
  218. default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",
  219. help="path to flame graph HTML template")
  220. parser.add_argument("--colorscheme",
  221. default="blue-green",
  222. help="flame graph color scheme",
  223. choices=["blue-green", "orange"])
  224. parser.add_argument("-i", "--input",
  225. help=argparse.SUPPRESS)
  226. parser.add_argument("--allow-download",
  227. default=False,
  228. action="store_true",
  229. help="allow unprompted downloading of HTML template")
  230. parser.add_argument("-e", "--event",
  231. default="",
  232. dest="event_name",
  233. type=str,
  234. help="specify the event to generate flamegraph for")
  235. cli_args = parser.parse_args()
  236. cli = FlameGraphCLI(cli_args)
  237. process_event = cli.process_event
  238. trace_end = cli.trace_end