* display perf.data header * display PIDs of user stacks * added option to change color scheme * default to blue/green color scheme to improve accessibility * correctly identify kernel stacks when kernel-debuginfo is installed
Showing the changes to tools/perf/scripts/python (1 of 1 file). Show all
Unified · Split
| @@ -13,6 +13,10 @@ · 12 unmodified lines | ||
| 13 | 13 | # Written by Andreas Gerstmayr <agerstmayr@redhat.com> |
| 14 | 14 | # Flame Graphs invented by Brendan Gregg <bgregg@netflix.com> |
| 15 | 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 | |
| 16 | 20 | |
| 17 | 21 | from __future__ import print_function |
| 18 | 22 | import sys |
| @@ -20,16 +24,19 @@ import os · 1 unmodified line | ||
| 20 | 24 | import io |
| 21 | 25 | import argparse |
| 22 | 26 | import json |
| 27 | +import subprocess | |
| 23 | 28 | |
| 24 | − | |
| 29 | +# pylint: disable=too-few-public-methods | |
| 25 | 30 | class Node: |
| 26 | − def __init__(self, name, libtype=""): | |
| 31 | + def __init__(self, name, libtype): | |
| 27 | 32 | self.name = name |
| 33 | + # "root" | "kernel" | "" | |
| 34 | + # "" indicates user space | |
| 28 | 35 | self.libtype = libtype |
| 29 | 36 | self.value = 0 |
| 30 | 37 | self.children = [] |
| 31 | 38 | |
| 32 | − def toJSON(self): | |
| 39 | + def to_json(self): | |
| 33 | 40 | return { |
| 34 | 41 | "n": self.name, |
| 35 | 42 | "l": self.libtype, |
| @@ -41,7 +48,7 @@ class Node: · 5 unmodified lines | ||
| 41 | 48 | class FlameGraphCLI: |
| 42 | 49 | def __init__(self, args): |
| 43 | 50 | self.args = args |
| 44 | − self.stack = Node("root") | |
| 51 | + self.stack = Node("all", "root") | |
| 45 | 52 | |
| 46 | 53 | if self.args.format == "html" and \ |
| 47 | 54 | not os.path.isfile(self.args.template): |
| @@ -53,13 +60,21 @@ class FlameGraphCLI: · 5 unmodified lines | ||
| 53 | 60 | file=sys.stderr) |
| 54 | 61 | sys.exit(1) |
| 55 | 62 | |
| 56 | − def find_or_create_node(self, node, name, dso): | |
| 57 | − libtype = "kernel" if dso == "[kernel.kallsyms]" else "" | |
| 58 | − if name is None: | |
| 59 | − name = "[unknown]" | |
| 63 | + @staticmethod | |
| 64 | + def get_libtype_from_dso(dso): | |
| 65 | + """ | |
| 66 | + when kernel-debuginfo is installed, | |
| 67 | + dso points to /usr/lib/debug/lib/modules/*/vmlinux | |
| 68 | + """ | |
| 69 | + if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux")): | |
| 70 | + return "kernel" | |
| 60 | 71 | |
| 72 | + return "" | |
| 73 | + | |
| 74 | + @staticmethod | |
| 75 | + def find_or_create_node(node, name, libtype): | |
| 61 | 76 | for child in node.children: |
| 62 | − if child.name == name and child.libtype == libtype: | |
| 77 | + if child.name == name: | |
| 63 | 78 | return child |
| 64 | 79 | |
| 65 | 80 | child = Node(name, libtype) |
| @@ -67,30 +82,65 @@ class FlameGraphCLI: · 1 unmodified line | ||
| 67 | 82 | return child |
| 68 | 83 | |
| 69 | 84 | def process_event(self, event): |
| 70 | − node = self.find_or_create_node(self.stack, event["comm"], None) | |
| 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 | + | |
| 71 | 96 | if "callchain" in event: |
| 72 | − for entry in reversed(event['callchain']): | |
| 73 | − node = self.find_or_create_node( | |
| 74 | − node, entry.get("sym", {}).get("name"), event.get("dso")) | |
| 97 | + for entry in reversed(event["callchain"]): | |
| 98 | + name = entry.get("sym", {}).get("name", "[unknown]") | |
| 99 | + libtype = self.get_libtype_from_dso(entry.get("dso")) | |
| 100 | + node = self.find_or_create_node(node, name, libtype) | |
| 75 | 101 | else: |
| 76 | − node = self.find_or_create_node( | |
| 77 | − node, entry.get("symbol"), event.get("dso")) | |
| 102 | + name = event.get("symbol", "[unknown]") | |
| 103 | + libtype = self.get_libtype_from_dso(event.get("dso")) | |
| 104 | + node = self.find_or_create_node(node, name, libtype) | |
| 78 | 105 | node.value += 1 |
| 79 | 106 | |
| 107 | + def get_report_header(self): | |
| 108 | + if self.args.input == "-": | |
| 109 | + # when this script is invoked with "perf script flamegraph", | |
| 110 | + # no perf.data is created and we cannot read the header of it | |
| 111 | + return "" | |
| 112 | + | |
| 113 | + try: | |
| 114 | + output = subprocess.check_output(["perf", "report", "--header-only"]) | |
| 115 | + return output.decode("utf-8") | |
| 116 | + except Exception as err: # pylint: disable=broad-except | |
| 117 | + print("Error reading report header: {}".format(err), file=sys.stderr) | |
| 118 | + return "" | |
| 119 | + | |
| 80 | 120 | def trace_end(self): |
| 81 | − json_str = json.dumps(self.stack, default=lambda x: x.toJSON()) | |
| 121 | + stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) | |
| 82 | 122 | |
| 83 | 123 | if self.args.format == "html": |
| 124 | + report_header = self.get_report_header() | |
| 125 | + options = { | |
| 126 | + "colorscheme": self.args.colorscheme, | |
| 127 | + "context": report_header | |
| 128 | + } | |
| 129 | + options_json = json.dumps(options) | |
| 130 | + | |
| 84 | 131 | try: |
| 85 | − with io.open(self.args.template, encoding="utf-8") as f: | |
| 86 | − output_str = f.read().replace("/** @flamegraph_json **/", | |
| 87 | − json_str) | |
| 88 | − except IOError as e: | |
| 89 | − print("Error reading template file: {}".format(e), file=sys.stderr) | |
| 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) | |
| 90 | 140 | sys.exit(1) |
| 91 | 141 | output_fn = self.args.output or "flamegraph.html" |
| 92 | 142 | else: |
| 93 | − output_str = json_str | |
| 143 | + output_str = stacks_json | |
| 94 | 144 | output_fn = self.args.output or "stacks.json" |
| 95 | 145 | |
| 96 | 146 | if output_fn == "-": |
| @@ -101,8 +151,8 @@ class FlameGraphCLI: · 4 unmodified lines | ||
| 101 | 151 | try: |
| 102 | 152 | with io.open(output_fn, "w", encoding="utf-8") as out: |
| 103 | 153 | out.write(output_str) |
| 104 | − except IOError as e: | |
| 105 | − print("Error writing output file: {}".format(e), file=sys.stderr) | |
| 154 | + except IOError as err: | |
| 155 | + print("Error writing output file: {}".format(err), file=sys.stderr) | |
| 106 | 156 | sys.exit(1) |
| 107 | 157 | |
| 108 | 158 | |
| @@ -115,12 +165,16 @@ if __name__ == "__main__": · 6 unmodified lines | ||
| 115 | 165 | help="output file name") |
| 116 | 166 | parser.add_argument("--template", |
| 117 | 167 | default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", |
| 118 | − help="path to flamegraph HTML template") | |
| 168 | + help="path to flame graph HTML template") | |
| 169 | + parser.add_argument("--colorscheme", | |
| 170 | + default="blue-green", | |
| 171 | + help="flame graph color scheme", | |
| 172 | + choices=["blue-green", "orange"]) | |
| 119 | 173 | parser.add_argument("-i", "--input", |
| 120 | 174 | help=argparse.SUPPRESS) |
| 121 | 175 | |
| 122 | − args = parser.parse_args() | |
| 123 | − cli = FlameGraphCLI(args) | |
| 176 | + cli_args = parser.parse_args() | |
| 177 | + cli = FlameGraphCLI(cli_args) | |
| 124 | 178 | |
| 125 | 179 | process_event = cli.process_event |
| 126 | 180 | trace_end = cli.trace_end |