From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-pj1-f73.google.com (mail-pj1-f73.google.com [209.85.216.73]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1D2E239A04D for ; Sat, 25 Apr 2026 22:42:33 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.216.73 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1777156954; cv=none; b=IxfO8CkLmblelh5PSVTC+mliQa+Zs5KdMMtQ4Wyp2lVTQ1TvquEAgcZp5+6SOA0tViUhboLViMuWMHrRzyCpkEDYNv14iWOAL2lD5emfUIcZ5rjjPMGmuEzafX3VD4qtTryvComDiO4Hlj2IAjy0m6yRd3JBGRa2BwR0drJzPzs= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1777156954; c=relaxed/simple; bh=4/G4WOpWLeIY4Q1/giU7GTp/xIECo8qdANOUyVELEPU=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=mSyRArUtj3c+1BxRljVhtucYNYfJl7xrwgisWO97yBDl6XR9ucy9W2KO6FsHHv19fxh6CxV3e0mIQ1/zZFwtkN3CvjZApZOpUYfn9MNU5BorcvLAf/PDyXV8eNar915UpHZzwgTkbD2FZALpr4Cx6EPRHSP91D1VWkqts/5cbN0= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b=febC+f6k; arc=none smtp.client-ip=209.85.216.73 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b="febC+f6k" Received: by mail-pj1-f73.google.com with SMTP id 98e67ed59e1d1-3568090851aso21777721a91.1 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=vger.kernel.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=febC+f6kE/Rt/hpGLiY9Pqn2Q63EwoASA0vy1crpjxgQzJqcRfhPgiml5GSMwnkLI5 JqV5nsMB3OrNmdLTR9/DT2PaO47JTqFa9k6FdGW88gsMOmbHUwT+yRRcHi9ryWHvb7B3 sc4cJ0bgbK4o9sUYeiMBaCKdsmh/2Bual81kwJPpteIOE6atYDHWqmDD28m78nN7K790 cb6UkL3HAaunQpD9Fi3/HHBhoFFkJXpK8Ucm5Y3CEhY8hYUlz0Fk/6fdPD8LWuapE2Zi rm/mKGJGA/HyuS4jS9cQa0d09dt+JUVlp85EZS4rxFMTQyJfLydFaaLfFCiWKeyo7kre wFtg== 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=H5uiWexwvMixUHbFJ5hh1woshNKxB39u4tGA4Kn79SOiUgoaXeu0DugQa6k/rpiHQJ NFR2m0V0axgxaX5OAUauBGrtlhs5t51Af9+nMl9UR4Rol10Z01WjvQr60XJP2ONPKzHC cp2Q3R2ZbzfxpS464wnXoYp5N1A1p9wS1tTbfakcehsI4P0IVeYQRDpLkCZJ8O73idjO oXER+EO66FHbXG18q9VfxQX3y0dIFDRvTwn5YzG3dT7IKSeG3PB226VIGvusc3t6xerf iIClk85GzBvMuvnsrJr895qUuMra4+INpEI80oGp5eJ8V+ZLfIvNZjgkEoGABIAr3XXo irRw== X-Forwarded-Encrypted: i=1; AFNElJ99piahmB522g2iMMtwcs7M8ZJRbvic6xTiGZ3v3xswAZQUDe7v2vJo01a1DlL3R7i7Q2JKdyWTFLEzvGMahKDP@vger.kernel.org X-Gm-Message-State: AOJu0YyufLKl95DHOK8RCkt4y1zJfwSk+jjWo5rBUTEoXdfxLoV8KXRF +leE0miTJNJF5GYyx6CiPnQD+r1GvkQ63mXEMPrlKSMUkY4mnU/Z1G8T0QK1L7+5Efhzj6lqy5k jjGgSgCLILQ== 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> Precedence: bulk X-Mailing-List: linux-perf-users@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: 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" 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