From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from mail-dy1-f202.google.com (mail-dy1-f202.google.com [74.125.82.202]) (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 A993E320CD3 for ; Mon, 20 Apr 2026 00:00:34 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.202 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776643236; cv=none; b=spJRO7kCageXxiIgwimnN9AdwG2wvxeFmRzFNoriHQVzD9lU2WGG729YQdb+8FA7Gleb7mSY0BYzUnalCJmobBeSJ7BRYzzR5rUbfbFhFb2AwRmQUNl039ank02WwezPw/Vu2kP646BOK/DwmmQkBnqIcgXMN2rbdRyAdwpKazc= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1776643236; c=relaxed/simple; bh=etuLy1V1F0RX5awGDhdA4GPSQBYNAwEBuodb0W6q4j0=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=cNgeQrmZSkJLuknQVeIWy4wIjioyyXUjiaV1y559XME/q/G7M6G4cyp5Lq9WuqbDmDjEd5r0qfzeWDoaCL3/PoQOuw530JERl4RMD5bIPSeKHUwAUDbKM810RnhFHsw24K83CPHzQTaVaXGdi7kryi7H6OyBd57PK20WBo6lgs8= 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=VcGDqSYx; arc=none smtp.client-ip=74.125.82.202 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="VcGDqSYx" Received: by mail-dy1-f202.google.com with SMTP id 5a478bee46e88-2c0f6593ef5so2555787eec.1 for ; Sun, 19 Apr 2026 17:00:34 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1776643234; x=1777248034; 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=vugcSbemdk82js8t3EG1fRcF5eIw7MxVSdfYxHmqMQc=; b=VcGDqSYxqGYTrIWOqhsabiIxveUck4uITBodjlF2FHHuqQtZVQ++l5fdHPx/l3LrSR NsH9zeNuB2nMJGnsS/68jEfhaXRcmqJLBnUcbA3ep1BiByGCt3WYgtAE9sn7yBENyU6C uDqe2nZIW+czcrPXy6x2rdE7sHAk+XcdVUfHNsHqZtQkfZdaTJA3ExqkWlqqTbPPBSJO v2S1l8kvoPzB/095TK8xSQPP1di8Ud0BVd4FOOdKCtcV+1qelZMM6T2vNAmol9Md3iEZ ZYPi5UfKbKx82zX8BDG6bCENNze9qjUtxOggFivt5WWIjR63C/Clx8P8XlfNm2wT3SKH uR+A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1776643234; x=1777248034; 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=vugcSbemdk82js8t3EG1fRcF5eIw7MxVSdfYxHmqMQc=; b=WW6I3xdBnJpWGRB2HaZBdEY7hq0ZrdkV5KkhKL/QG/TY7ROKCPTXBLmxxNf2g8NKoP yWJ8iCJADwHiaxCyGIO0hm5Y0GVnlxO4y6vk8M++MtZCkp1Y50fv1ixVRReVnIqPh71N co5CxJ1oiKvDQ8ZDoJhWC+VMgqUEfa3JNI0+4PaOn+hAKBupyChm4XzN7oQ7xnva3d1j GVojrCnQI83W8mjdBye591kJk/8etXCOmz2W9koKy/dSaPxggRuBaWsJ+pTcIhPdanIr FpzVGZNfHuWCRfUI7NvoKMwlo14789Ch09/TG5D5Mv8yY1yCuu7SumcSKY6E9r2r8Njp yQ3Q== X-Forwarded-Encrypted: i=1; AFNElJ8P6TBtOuemspBYa0jL4flZMZfpJmcmUm23eWL4waPIgBqIYm/coWPgRYcE5+NvKel51bXL2wdS4c5hZhWhDgZu@vger.kernel.org X-Gm-Message-State: AOJu0YyT9KbvBg+dhHG6PPwOH8He67Q56z096+ZAftVcFAHq3AYDzewt 6j8nbDB7ki2FNg1KfokAuEEgnMVWVjZmtqyrhW96gLQYMef+VRqPJ4Kg/UJQ/KKa/4DI8V01+i7 TqWDtaRbulw== X-Received: from dycns6.prod.google.com ([2002:a05:7300:f786:b0:2df:c53c:24a5]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:7301:168a:b0:2d1:815f:19c1 with SMTP id 5a478bee46e88-2e479c0427dmr4988093eec.15.1776643230492; Sun, 19 Apr 2026 17:00:30 -0700 (PDT) Date: Sun, 19 Apr 2026 16:58:42 -0700 In-Reply-To: <20260419235911.2186050-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: <20260419235911.2186050-1-irogers@google.com> X-Mailer: git-send-email 2.54.0.rc1.513.gad8abe7a5a-goog Message-ID: <20260419235911.2186050-31-irogers@google.com> Subject: [PATCH v1 30/58] perf flamegraph: Port flamegraph to use python module From: Ian Rogers To: Peter Zijlstra , Ingo Molnar , Arnaldo Carvalho de Melo , Namhyung Kim , Jiri Olsa , Adrian Hunter , James Clark , Alice Rogers , Suzuki K Poulose , Mike Leach , John Garry , Leo Yan , Yicong Yang , Jonathan Cameron , Nick Terrell , David Sterba , Nathan Chancellor , Nick Desaulniers , Bill Wendling , Justin Stitt , Alexandre Chartre , Dmitrii Dolgov <9erthalion6@gmail.com>, Yuzhuo Jing , Blake Jones , Changbin Du , Gautam Menghani , Wangyang Guo , Pan Deng , Zhiguo Zhou , Tianyou Li , Thomas Falcon , Athira Rajeev , Collin Funk , Dapeng Mi , Ravi Bangoria , Zecheng Li , tanze , Thomas Richter , Ankur Arora , "Tycho Andersen (AMD)" , Howard Chu , Sun Jian , Derek Foreman , Swapnil Sapkal , Anubhav Shelat , Ricky Ringler , Qinxin Xia , Aditya Bodkhe , Chun-Tse Shao , Stephen Brennan , Yang Li , Chuck Lever , Chen Ni , linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, coresight@lists.linaro.org, linux-arm-kernel@lists.infradead.org Cc: 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 is significantly faster than using perf script callbacks as it avoids creating intermediate dictionaries for all event fields. Assisted-by: Gemini:gemini-3.1-pro-preview Signed-off-by: Ian Rogers --- tools/perf/python/flamegraph.py | 242 ++++++++++++++++++++++++++++++++ 1 file changed, 242 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..090e07a0992d --- /dev/null +++ b/tools/perf/python/flamegraph.py @@ -0,0 +1,242 @@ +#!/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: list[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] + } + + +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.""" + for child in node.children: + if child.name == name: + return child + child = Node(name, libtype) + node.children.append(child) + return child + + def process_event(self, sample) -> None: + """Process a single perf sample event.""" + if self.args.event_name and str(sample.evsel) != self.args.event_name: + return + + pid = sample.sample_pid + dso_type = "" + try: + thread = self.session.process(sample.sample_tid) + comm = thread.comm() + # Try to get libtype from thread's main executable + # This is a bit of a hack as pyrf_thread doesn't expose maps easily + 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) + 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()) + + if self.args.format == "html": + report_header = self.get_report_header() + options = { + "colorscheme": self.args.colorscheme, + "context": report_header + } + options_json = json.dumps(options) + + template = self.args.template + template_md5sum = None + output_str = None + if not os.path.isfile(template): + if not self.args.allow_download: + print(f"Warning: Flame Graph template '{template}' does not exist.", + file=sys.stderr) + template = ( + "https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/templates/" + "d3-flamegraph-base.html" + ) + template_md5sum = "143e0d06ba69b8370b9848dcd6ae3f36" + if not sys.stdin.isatty(): + print("Non-interactive environment detected, skipping download.") + template = None + else: + s = None + while s not in ["y", "n"]: + s = input("Do you wish to download a template from cdn.jsdelivr.net? " + + "(this warning can be suppressed with --allow-download) [yn] " + ).lower() + if s == "n": + template = None + template_md5sum = 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 is typically blocked unless your " + + "browser has relaxed permissions.") + output_str = MINIMAL_HTML + + elif template_md5sum: + 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: +{output_str} +continue?[yn] """).lower() + if s == "n": + sys.exit(1) + + 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.rc1.513.gad8abe7a5a-goog