flamegraph.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  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. from __future__ import print_function
  21. import argparse
  22. import hashlib
  23. import io
  24. import json
  25. import os
  26. import subprocess
  27. import sys
  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, libtype):
  50. self.name = name
  51. # "root" | "kernel" | ""
  52. # "" indicates user space
  53. self.libtype = libtype
  54. self.value = 0
  55. self.children = []
  56. def to_json(self):
  57. return {
  58. "n": self.name,
  59. "l": self.libtype,
  60. "v": self.value,
  61. "c": 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):
  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, name, libtype):
  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):
  85. pid = event.get("sample", {}).get("pid", 0)
  86. # event["dso"] sometimes contains /usr/lib/debug/lib/modules/*/vmlinux
  87. # for user-space processes; let's use pid for kernel or user-space distinction
  88. if pid == 0:
  89. comm = event["comm"]
  90. libtype = "kernel"
  91. else:
  92. comm = "{} ({})".format(event["comm"], pid)
  93. libtype = ""
  94. node = self.find_or_create_node(self.stack, comm, libtype)
  95. if "callchain" in event:
  96. for entry in reversed(event["callchain"]):
  97. name = entry.get("sym", {}).get("name", "[unknown]")
  98. libtype = self.get_libtype_from_dso(entry.get("dso"))
  99. node = self.find_or_create_node(node, name, libtype)
  100. else:
  101. name = event.get("symbol", "[unknown]")
  102. libtype = self.get_libtype_from_dso(event.get("dso"))
  103. node = self.find_or_create_node(node, name, libtype)
  104. node.value += 1
  105. def get_report_header(self):
  106. if self.args.input == "-":
  107. # when this script is invoked with "perf script flamegraph",
  108. # no perf.data is created and we cannot read the header of it
  109. return ""
  110. try:
  111. output = subprocess.check_output(["perf", "report", "--header-only"])
  112. return output.decode("utf-8")
  113. except Exception as err: # pylint: disable=broad-except
  114. print("Error reading report header: {}".format(err), file=sys.stderr)
  115. return ""
  116. def trace_end(self):
  117. stacks_json = json.dumps(self.stack, default=lambda x: x.to_json())
  118. if self.args.format == "html":
  119. report_header = self.get_report_header()
  120. options = {
  121. "colorscheme": self.args.colorscheme,
  122. "context": report_header
  123. }
  124. options_json = json.dumps(options)
  125. template_md5sum = None
  126. if self.args.format == "html":
  127. if os.path.isfile(self.args.template):
  128. template = f"file://{self.args.template}"
  129. else:
  130. if not self.args.allow_download:
  131. print(f"""Warning: Flame Graph template '{self.args.template}'
  132. does not exist. To avoid this please install a package such as the
  133. js-d3-flame-graph or libjs-d3-flame-graph, specify an existing flame
  134. graph template (--template PATH) or use another output format (--format
  135. FORMAT).""",
  136. file=sys.stderr)
  137. if self.args.input == "-":
  138. print("""Not attempting to download Flame Graph template as script command line
  139. input is disabled due to using live mode. If you want to download the
  140. template retry without live mode. For example, use 'perf record -a -g
  141. -F 99 sleep 60' and 'perf script report flamegraph'. Alternatively,
  142. download the template from:
  143. https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html
  144. and place it at:
  145. /usr/share/d3-flame-graph/d3-flamegraph-base.html""",
  146. file=sys.stderr)
  147. quit()
  148. s = None
  149. while s != "y" and s != "n":
  150. s = input("Do you wish to download a template from cdn.jsdelivr.net? (this warning can be suppressed with --allow-download) [yn] ").lower()
  151. if s == "n":
  152. quit()
  153. template = "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/d3-flamegraph-base.html"
  154. template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36"
  155. try:
  156. with urllib.request.urlopen(template) as template:
  157. output_str = "".join([
  158. l.decode("utf-8") for l in template.readlines()
  159. ])
  160. except Exception as err:
  161. print(f"Error reading template {template}: {err}\n"
  162. "a minimal flame graph will be generated", file=sys.stderr)
  163. output_str = minimal_html
  164. template_md5sum = None
  165. if template_md5sum:
  166. download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest()
  167. if download_md5sum != template_md5sum:
  168. s = None
  169. while s != "y" and s != "n":
  170. s = input(f"""Unexpected template md5sum.
  171. {download_md5sum} != {template_md5sum}, for:
  172. {output_str}
  173. continue?[yn] """).lower()
  174. if s == "n":
  175. quit()
  176. output_str = output_str.replace("/** @options_json **/", options_json)
  177. output_str = output_str.replace("/** @flamegraph_json **/", stacks_json)
  178. output_fn = self.args.output or "flamegraph.html"
  179. else:
  180. output_str = stacks_json
  181. output_fn = self.args.output or "stacks.json"
  182. if output_fn == "-":
  183. with io.open(sys.stdout.fileno(), "w", encoding="utf-8", closefd=False) as out:
  184. out.write(output_str)
  185. else:
  186. print("dumping data to {}".format(output_fn))
  187. try:
  188. with io.open(output_fn, "w", encoding="utf-8") as out:
  189. out.write(output_str)
  190. except IOError as err:
  191. print("Error writing output file: {}".format(err), file=sys.stderr)
  192. sys.exit(1)
  193. if __name__ == "__main__":
  194. parser = argparse.ArgumentParser(description="Create flame graphs.")
  195. parser.add_argument("-f", "--format",
  196. default="html", choices=["json", "html"],
  197. help="output file format")
  198. parser.add_argument("-o", "--output",
  199. help="output file name")
  200. parser.add_argument("--template",
  201. default="/usr/share/d3-flame-graph/d3-flamegraph-base.html",
  202. help="path to flame graph HTML template")
  203. parser.add_argument("--colorscheme",
  204. default="blue-green",
  205. help="flame graph color scheme",
  206. choices=["blue-green", "orange"])
  207. parser.add_argument("-i", "--input",
  208. help=argparse.SUPPRESS)
  209. parser.add_argument("--allow-download",
  210. default=False,
  211. action="store_true",
  212. help="allow unprompted downloading of HTML template")
  213. cli_args = parser.parse_args()
  214. cli = FlameGraphCLI(cli_args)
  215. process_event = cli.process_event
  216. trace_end = cli.trace_end