From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: X-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on aws-us-west-2-korg-lkml-1.web.codeaurora.org Received: from bombadil.infradead.org (bombadil.infradead.org [198.137.202.133]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.lore.kernel.org (Postfix) with ESMTPS id AD8E7FF885C for ; Sat, 25 Apr 2026 22:43:29 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=lists.infradead.org; s=bombadil.20210309; h=Sender:List-Subscribe:List-Help :List-Post:List-Archive:List-Unsubscribe:List-Id:Content-Type:Cc:To:From: Subject:Message-ID:References:Mime-Version:In-Reply-To:Date:Reply-To: Content-Transfer-Encoding:Content-ID:Content-Description:Resent-Date: Resent-From:Resent-Sender:Resent-To:Resent-Cc:Resent-Message-ID:List-Owner; bh=efNil+PNmmmvTcZKEec15sBNEnsydHEhEYd+ZnlvuZ8=; b=uZ1/OviAl/0O1fWoNTKeRU8WUo BG4Er3+C77R/8hYcszwuc1GLnG1yxXDk6mMJAtn4bizFDsMdgn1r9tGrJx0FNCMTeIePKFDqKxFlc 7lPBGXw8JOlQ5Ve99EOm5qUIgUP639I8RnNP89Vn1HqQ7C/s7KXjfVjkCQVr5TUhFTcMUl8SfD1/t HD6m4u+TN8D+JUp1C6aoiBFFHwMWXmbUZxVwVUMOeORJSt4M6m79Do0LMGCze4Zg2Hd9W/UPYmyoi KQgD5JiaarM/D5S37orf3UR/oWMiQkFLchqqi2xZoaMbeXNF0WWJbjE9sfhaa6k8yzbat2yBBkYBP BE+9YKUg==; Received: from localhost ([::1] helo=bombadil.infradead.org) by bombadil.infradead.org with esmtp (Exim 4.98.2 #2 (Red Hat Linux)) id 1wGliV-0000000EuRf-0mdZ; Sat, 25 Apr 2026 22:43:15 +0000 Received: from mail-pj1-x1049.google.com ([2607:f8b0:4864:20::1049]) by bombadil.infradead.org with esmtps (Exim 4.98.2 #2 (Red Hat Linux)) id 1wGlhp-0000000Etf0-1KGv for linux-arm-kernel@lists.infradead.org; Sat, 25 Apr 2026 22:42:39 +0000 Received: by mail-pj1-x1049.google.com with SMTP id 98e67ed59e1d1-35fbaada2f3so16614124a91.0 for ; Sat, 25 Apr 2026 15:42:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1777156952; x=1777761752; darn=lists.infradead.org; h=cc:to:from:subject:message-id:references:mime-version:in-reply-to :date:from:to:cc:subject:date:message-id:reply-to; bh=efNil+PNmmmvTcZKEec15sBNEnsydHEhEYd+ZnlvuZ8=; b=OUjFrDuC7OJNZlPRLMCDEvIOiFFE49RQ+VXM796/HW6eWXlZ6J8LSoJhJrEDQVdAYe 4vUyVK/kmbwRbSW96fnOpacQ94wqzL6qVqXHHac4b7VHyVdFXYyVOXMxMs+8HZMJYEso AthZfWB2PNSbPi0WOuc0P6EbHuZUN+SAd65DavoAXuDc2/K0oxvjN/O33FH7KFphvTIH VI5FSeEH837j3x6hqoxou9jVuBLUy1NJrjM0L9unbyUEIyAEVH8zpcCgAXlsJWV9/d7r WejQKOiat97TgPJ8PcLMi4fkArmtWcCpxDSW6aeAZgpoVR9zFWoJw2hfNxsrknVQZicH k3jA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1777156952; x=1777761752; h=cc:to:from:subject:message-id:references:mime-version:in-reply-to :date:x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; bh=efNil+PNmmmvTcZKEec15sBNEnsydHEhEYd+ZnlvuZ8=; b=H8kEb2ZA84RhlRU98+GgXHYBHACJIabFxkYdMutqkIycGmiay1V9fJhK/b/YymdazX rvdKq0Z0IR/QmqOd8qAkW2K/GB/rjVlyZMDA/CdfLRjb62kz0xJwICM0wSzL/Q2uOMQ8 7PaCD3+uyWNLIA2ju2mqC+PCc38G9yDf8nRzMUPsdo3oqpeh83AIBd/JEYbmtsrRiBoW EhHZ9Rpmw3dkSJ2Y6J0kJeDwbY9ElD5FGWCbgPG0z5PMbz79Oleo169v+jxK97tXTAgX r6ipQOjGqTDdW9rUrfemEWHCLtEV85My1rhdr8SrChOFx4yAhZJQ42HhEdSorJ2YMCoA w8Tg== X-Forwarded-Encrypted: i=1; AFNElJ8matZ2hP3mM9q/tElIL5ExNImZCFlCckuAZaoI7+0zxBnLWQW9x9AZI/5QBLianoeRMntmC7nXUWZOIQ/ySMkx@lists.infradead.org X-Gm-Message-State: AOJu0YwlA2YXVCgqwZ7Op38oxjZ7Y2cU5L+qNg/7ry/oSKT8yp4/G1qV 6nglYkrovxvR6tieVjoH22WM2IKLA76C/f89HA+uaNeifSZdto4tfYI7Ni7eeQ7SWBtjiAg+VIF iCcqerU2/1Q== X-Received: from pgar13.prod.google.com ([2002:a05:6a02:2e8d:b0:c76:98c9:d80a]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a17:90b:4b44:b0:361:3224:2f65 with SMTP id 98e67ed59e1d1-361403bdd07mr37724111a91.3.1777156952160; Sat, 25 Apr 2026 15:42:32 -0700 (PDT) Date: Sat, 25 Apr 2026 15:40:55 -0700 In-Reply-To: <20260425224125.160890-1-irogers@google.com> Mime-Version: 1.0 References: <20260425174858.3922152-1-irogers@google.com> <20260425224125.160890-1-irogers@google.com> X-Mailer: git-send-email 2.54.0.545.g6539524ca2-goog Message-ID: <20260425224125.160890-31-irogers@google.com> Subject: [PATCH v7 30/59] perf flamegraph: Port flamegraph to use python module From: Ian Rogers To: acme@kernel.org, adrian.hunter@intel.com, james.clark@linaro.org, leo.yan@linux.dev, namhyung@kernel.org, tmricht@linux.ibm.com Cc: alice.mei.rogers@gmail.com, dapeng1.mi@linux.intel.com, linux-arm-kernel@lists.infradead.org, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org, Ian Rogers Content-Type: text/plain; charset="UTF-8" X-CRM114-Version: 20100106-BlameMichelson ( TRE 0.8.0 (BSD) ) MR-646709E3 X-CRM114-CacheID: sfid-20260425_154233_390993_D981783F X-CRM114-Status: GOOD ( 19.44 ) X-BeenThere: linux-arm-kernel@lists.infradead.org X-Mailman-Version: 2.1.34 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Sender: "linux-arm-kernel" Errors-To: linux-arm-kernel-bounces+linux-arm-kernel=archiver.kernel.org@lists.infradead.org Add a port of the flamegraph script that uses the perf python module directly. This approach improves performance by avoiding intermediate dictionaries for event fields. Assisted-by: Gemini:gemini-3.1-pro-preview Signed-off-by: Ian Rogers --- v5: 1. Fix Event Filtering: Corrected event filtering check to search for a substring match within the parsed event string, preventing all events from being dropped due to the `evsel(...)` wrapper. v6: - Fixed terminal injection risk by not printing unverified content in prompt. --- tools/perf/python/flamegraph.py | 250 ++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100755 tools/perf/python/flamegraph.py diff --git a/tools/perf/python/flamegraph.py b/tools/perf/python/flamegraph.py new file mode 100755 index 000000000000..b0eb5844b772 --- /dev/null +++ b/tools/perf/python/flamegraph.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +""" +flamegraph.py - create flame graphs from perf samples using perf python module +""" + +import argparse +import hashlib +import json +import os +import subprocess +import sys +import urllib.request +from typing import Dict, Optional, Union +import perf + +MINIMAL_HTML = """ + + + +
+ + + + +""" + +class Node: + """A node in the flame graph tree.""" + def __init__(self, name: str, libtype: str): + self.name = name + self.libtype = libtype + self.value: int = 0 + self.children: dict[str, Node] = {} + + def to_json(self) -> Dict[str, Union[str, int, list[Dict]]]: + """Convert the node to a JSON-serializable dictionary.""" + return { + "n": self.name, + "l": self.libtype, + "v": self.value, + "c": [x.to_json() for x in self.children.values()] + } + + +class FlameGraphCLI: + """Command-line interface for generating flame graphs.""" + def __init__(self, args): + self.args = args + self.stack = Node("all", "root") + self.session = None + + @staticmethod + def get_libtype_from_dso(dso: Optional[str]) -> str: + """Determine the library type from the DSO name.""" + if dso and (dso == "[kernel.kallsyms]" or dso.endswith("/vmlinux") or dso == "[kernel]"): + return "kernel" + return "" + + @staticmethod + def find_or_create_node(node: Node, name: str, libtype: str) -> Node: + """Find a child node with the given name or create a new one.""" + if name in node.children: + return node.children[name] + child = Node(name, libtype) + node.children[name] = child + return child + + def process_event(self, sample) -> None: + """Process a single perf sample event.""" + if self.args.event_name and self.args.event_name not in str(sample.evsel): + return + + pid = sample.sample_pid + dso_type = "" + try: + thread = self.session.find_thread(sample.sample_tid) + comm = thread.comm() + except Exception: + comm = "[unknown]" + + if pid == 0: + comm = "swapper" + dso_type = "kernel" + else: + comm = f"{comm} ({pid})" + + node = self.find_or_create_node(self.stack, comm, dso_type) + + callchain = sample.callchain + if callchain: + # We want to traverse from root to leaf. + # perf callchain iterator gives leaf to root. + # We collect them and reverse. + frames = list(callchain) + for entry in reversed(frames): + name = entry.symbol or "[unknown]" + libtype = self.get_libtype_from_dso(entry.dso) + node = self.find_or_create_node(node, name, libtype) + else: + # Fallback if no callchain + name = getattr(sample, "symbol", "[unknown]") + libtype = self.get_libtype_from_dso(getattr(sample, "dso", "[unknown]")) + node = self.find_or_create_node(node, name, libtype) + + node.value += 1 + + def get_report_header(self) -> str: + """Get the header from the perf report.""" + try: + input_file = self.args.input or "perf.data" + output = subprocess.check_output(["perf", "report", "--header-only", "-i", input_file]) + result = output.decode("utf-8") + if self.args.event_name: + result += "\nFocused event: " + self.args.event_name + return result + except Exception: + return "" + + def run(self) -> None: + """Run the flame graph generation.""" + input_file = self.args.input or "perf.data" + if not os.path.exists(input_file): + print(f"Error: {input_file} not found. (try 'perf record' first)", file=sys.stderr) + sys.exit(1) + + try: + self.session = perf.session(perf.data(input_file), + sample=self.process_event) + except Exception as e: + print(f"Error opening session: {e}", file=sys.stderr) + sys.exit(1) + + self.session.process_events() + + stacks_json = json.dumps(self.stack, default=lambda x: x.to_json()) + # Escape HTML special characters to prevent XSS + stacks_json = stacks_json.replace("<", "\\u003c") \ + .replace(">", "\\u003e").replace("&", "\\u0026") + + if self.args.format == "html": + report_header = self.get_report_header() + options = { + "colorscheme": self.args.colorscheme, + "context": report_header + } + options_json = json.dumps(options) + options_json = options_json.replace("<", "\\u003c") \ + .replace(">", "\\u003e").replace("&", "\\u0026") + + template = self.args.template + template_md5sum = None + output_str = None + + if not os.path.isfile(template): + if template.startswith("http://") or template.startswith("https://"): + if not self.args.allow_download: + print("Warning: Downloading templates is disabled. " + "Use --allow-download.", file=sys.stderr) + template = None + else: + print(f"Warning: Template file '{template}' not found.", file=sys.stderr) + if self.args.allow_download: + print("Using default CDN template.", file=sys.stderr) + template = ( + "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/" + "d3-flamegraph-base.html" + ) + template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" + else: + template = None + + use_minimal = False + try: + if not template: + use_minimal = True + elif template.startswith("http"): + with urllib.request.urlopen(template) as url_template: + output_str = "".join([l.decode("utf-8") for l in url_template.readlines()]) + else: + with open(template, "r", encoding="utf-8") as f: + output_str = f.read() + except Exception as err: + print(f"Error reading template {template}: {err}\n", file=sys.stderr) + use_minimal = True + + if use_minimal: + print("Using internal minimal HTML that refers to d3's web site. JavaScript " + + "loaded this way from a local file may be blocked unless your " + + "browser has relaxed permissions. Run with '--allow-download' to fetch" + + "the full D3 HTML template.", file=sys.stderr) + output_str = MINIMAL_HTML + + elif template_md5sum: + assert output_str is not None + download_md5sum = hashlib.md5(output_str.encode("utf-8")).hexdigest() + if download_md5sum != template_md5sum: + s = None + while s not in ["y", "n"]: + s = input(f"""Unexpected template md5sum. +{download_md5sum} != {template_md5sum}, for: +{template} +continue?[yn] """).lower() + if s == "n": + sys.exit(1) + + assert output_str is not None + output_str = output_str.replace("/** @options_json **/", options_json) + output_str = output_str.replace("/** @flamegraph_json **/", stacks_json) + output_fn = self.args.output or "flamegraph.html" + else: + output_str = stacks_json + output_fn = self.args.output or "stacks.json" + + if output_fn == "-": + sys.stdout.write(output_str) + else: + print(f"dumping data to {output_fn}") + with open(output_fn, "w", encoding="utf-8") as out: + out.write(output_str) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Create flame graphs using perf python module.") + parser.add_argument("-f", "--format", default="html", choices=["json", "html"], + help="output file format") + parser.add_argument("-o", "--output", help="output file name") + parser.add_argument("--template", + default="/usr/share/d3-flame-graph/d3-flamegraph-base.html", + help="path to flame graph HTML template") + parser.add_argument("--colorscheme", default="blue-green", + help="flame graph color scheme", choices=["blue-green", "orange"]) + parser.add_argument("-i", "--input", help="input perf.data file") + parser.add_argument("--allow-download", default=False, action="store_true", + help="allow unprompted downloading of HTML template") + parser.add_argument("-e", "--event", default="", dest="event_name", type=str, + help="specify the event to generate flamegraph for") + + cli_args = parser.parse_args() + cli = FlameGraphCLI(cli_args) + cli.run() -- 2.54.0.545.g6539524ca2-goog