Linux Perf Users
 help / color / mirror / Atom feed
From: Tudor-Stefan Magirescu <tudor.magirescu@gmail.com>
To: peterz@infradead.org, mingo@redhat.com, acme@kernel.org,
	namhyung@kernel.org, mark.rutland@arm.com,
	alexander.shishkin@linux.intel.com, jolsa@kernel.org,
	irogers@google.com, adrian.hunter@intel.com
Cc: linux-perf-users@vger.kernel.org, linux-kernel@vger.kernel.org,
	tudor.magirescu@gmail.com
Subject: [RFC PATCH v2 1/1] perf scripts flamegraph: Add --asm option
Date: Thu, 27 Aug 2026 17:23:54 +0200	[thread overview]
Message-ID: <20260827152354.1513813-2-tudor.magirescu@gmail.com> (raw)
In-Reply-To: <20260827152354.1513813-1-tudor.magirescu@gmail.com>

The flamegraph script reports samples at function granularity, so a wide
frame shows which function is hot but not which part of it. Obtaining
that requires perf annotate, which reports per-instruction counts without
the calling context of a flame graph.

Add an --asm option to emit more fine-grained flame graphs by including
instruction-level information. By enabling this option, instructions
appear as the leaves in the flame graph call stacks:

main
  a(int)
    imul   $0x3b9aca07,%rax,%rdx [0x13ad]

Signed-off-by: Tudor-Stefan Magirescu <tudor.magirescu@gmail.com>
---
 tools/perf/scripts/python/flamegraph.py | 161 ++++++++++++++++++++++++
 1 file changed, 161 insertions(+)

diff --git a/tools/perf/scripts/python/flamegraph.py b/tools/perf/scripts/python/flamegraph.py
index ad735990c5be..60ace98e73ad 100755
--- a/tools/perf/scripts/python/flamegraph.py
+++ b/tools/perf/scripts/python/flamegraph.py
@@ -19,14 +19,18 @@
 # pylint: disable=missing-function-docstring
 
 import argparse
+from dataclasses import dataclass
 import hashlib
 import io
 import json
 import os
+import re
 import subprocess
 import sys
 from typing import Dict, Optional, Union
 import urllib.request
+from perf_trace_context import perf_config_get
+
 
 MINIMAL_HTML = """<head>
   <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/d3-flame-graph@4.1.3/dist/d3-flamegraph.css">
@@ -67,10 +71,146 @@ class Node:
         }
 
 
+@dataclass
+class Symbol:
+    start: int
+    size: int
+
+
+@dataclass
+class ResolvedDso:
+    symtab_file: Optional[str]
+    code_file:   Optional[str]
+
+
+class Instructions:
+    def __init__(self):
+        self.symbols:       dict[str, dict[str, Symbol]] = {}
+        self.instructions:  dict[tuple[str, str], dict[int, str]] = {}
+        self.resolved_dsos: dict[str, ResolvedDso] = {}
+        self.disasm_re:     re.Pattern = re.compile(r"^\s+([0-9a-f]+):\s+(.*)")
+        self.symtab_re:     re.Pattern = re.compile(
+            r"^([0-9a-f]+) (.{7})\s+\S+\s+([0-9a-f]+)\s+(.*)$")
+        self.objdump:       Optional[str] = self.default_objdump()
+
+    @staticmethod
+    def resolve_dso(dso_bid: str, filenames: tuple[str, str]) -> Optional[str]:
+        buildid_dir = os.environ.get('PERF_BUILDID_DIR',
+                                     os.path.expanduser('~/.debug'))
+        for filename in filenames:
+            path = (f"{buildid_dir}/.build-id/"
+                    f"{dso_bid[:2]}/{dso_bid[2:]}/{filename}")
+            if os.path.isfile(path):
+                return path
+        return None
+
+    @staticmethod
+    def default_objdump() -> Optional[str]:
+        config = perf_config_get("annotate.objdump")
+        cmd = config if config else "objdump"
+        try:
+            subprocess.run([cmd, "--version"], capture_output=True, check=False)
+        except FileNotFoundError as err:
+            print(f"Error running objdump: {err}; "
+                  "instruction annotations will be skipped", file=sys.stderr)
+            return None
+        return cmd
+
+    def load_symbols(self, resolved_dso: str) -> dict[str, Symbol]:
+        result = subprocess.run(
+            [self.objdump, "--demangle", "-t", resolved_dso],
+            capture_output=True, text=True, check=False
+        )
+
+        if result.returncode != 0:
+            return {}
+
+        symbols: dict[str, Symbol] = {}
+
+        for line in result.stdout.splitlines():
+            match = self.symtab_re.match(line)
+            if not match:
+                continue
+
+            value, flags, size_str, name = match.groups()
+            # the 7th flag column holds the symbol type, "F" for a function
+            if flags[6] != "F":
+                continue
+
+            size = int(size_str, 16)
+            if size == 0:
+                continue
+
+            # a name may be preceded by its visibility
+            parts = name.split(maxsplit=1)
+            if len(parts) == 2 and parts[0] in (".hidden", ".protected",
+                                                ".internal"):
+                name = parts[1]
+
+            symbols[name.strip()] = Symbol(start=int(value, 16), size=size)
+
+        return symbols
+
+    def load_instructions(self, resolved_dso: str, sym: Symbol) -> dict[int, str]:
+        result = subprocess.run(
+            [
+                self.objdump, "-d",
+                "--no-show-raw-insn",
+                f"--start-address=0x{sym.start:x}",
+                f"--stop-address=0x{sym.start + sym.size:x}",
+                resolved_dso,
+            ],
+            capture_output=True, text=True, check=False
+        )
+
+        instructions: dict[int, str] = {}
+
+        for line in result.stdout.splitlines():
+            match = self.disasm_re.match(line)
+            if not match:
+                continue
+            addr = int(match.group(1), 16)
+            instruction = match.group(2).strip()
+            instructions[addr] = instruction
+
+        return instructions
+
+    def lookup_instruction(self, dso: str, dso_bid: str, func: str,
+                           off: int) -> Optional[tuple[int, Optional[str]]]:
+        if self.objdump is None:
+            return None
+
+        if dso not in self.resolved_dsos:
+            self.resolved_dsos[dso] = ResolvedDso(
+                symtab_file=self.resolve_dso(dso_bid, ('debug', 'elf')),
+                code_file=self.resolve_dso(dso_bid, ('elf', 'debug')),
+            )
+
+        resolved_dso = self.resolved_dsos[dso]
+        if resolved_dso.symtab_file is None or resolved_dso.code_file is None:
+            return None
+
+        if (dso, func) in self.instructions:
+            addr = self.symbols[dso][func].start + off
+            return (addr, self.instructions[(dso, func)].get(addr))
+
+        if dso not in self.symbols:
+            self.symbols[dso] = self.load_symbols(resolved_dso.symtab_file)
+
+        sym = self.symbols[dso].get(func)
+        if sym is None:
+            return None
+
+        self.instructions[(dso, func)] = self.load_instructions(resolved_dso.code_file, sym)
+        addr = sym.start + off
+        return (addr, self.instructions[(dso, func)].get(addr))
+
+
 class FlameGraphCLI:
     def __init__(self, args):
         self.args = args
         self.stack = Node("all", "root")
+        self.instructions = Instructions() if args.asm else None
 
     @staticmethod
     def get_libtype_from_dso(dso: Optional[str]) -> str:
@@ -119,6 +259,23 @@ class FlameGraphCLI:
             name = event.get("symbol", "[unknown]")
             libtype = self.get_libtype_from_dso(event.get("dso"))
             node = self.find_or_create_node(node, name, libtype)
+
+        if self.args.asm:
+            # use the sample IP directly rather than callchain[0], since with
+            # precise event recording (i.e. :pp) the top callchain entry may
+            # point to the next instruction rather than the sampled IP
+            sym_name = event.get("symbol")
+            sym_off  = event.get("symoff")
+            dso      = event.get("dso")
+            dso_bid  = event.get("dso_bid")
+
+            if sym_name and sym_off is not None and dso and dso_bid:
+                found = self.instructions.lookup_instruction(dso, dso_bid, sym_name, sym_off)
+                if found is not None:
+                    addr, instruction = found
+                    name = f"{instruction} [0x{addr:x}]" if instruction else f"[0x{addr:x}]"
+                    libtype = self.get_libtype_from_dso(dso)
+                    node = self.find_or_create_node(node, name, libtype)
         node.value += 1
 
     def get_report_header(self) -> str:
@@ -259,6 +416,10 @@ if __name__ == "__main__":
                         dest="event_name",
                         type=str,
                         help="specify the event to generate flamegraph for")
+    parser.add_argument("--asm",
+                        default=False,
+                        action="store_true",
+                        help="annotate leaf frames with instruction-level nodes")
 
     cli_args = parser.parse_args()
     cli = FlameGraphCLI(cli_args)
-- 
2.43.0


  reply	other threads:[~2026-08-27 15:24 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-08-21  8:50 [RFC PATCH 0/1] perf scripts flamegraph: Add --asm option Tudor-Stefan Magirescu
2026-08-21  8:50 ` [RFC PATCH 1/1] " Tudor-Stefan Magirescu
2026-08-21  9:05   ` sashiko-bot
2026-08-27 15:23     ` [RFC PATCH v2 0/1] " Tudor-Stefan Magirescu
2026-08-27 15:23       ` Tudor-Stefan Magirescu [this message]
2026-08-27 15:37         ` [RFC PATCH v2 1/1] " sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260827152354.1513813-2-tudor.magirescu@gmail.com \
    --to=tudor.magirescu@gmail.com \
    --cc=acme@kernel.org \
    --cc=adrian.hunter@intel.com \
    --cc=alexander.shishkin@linux.intel.com \
    --cc=irogers@google.com \
    --cc=jolsa@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-perf-users@vger.kernel.org \
    --cc=mark.rutland@arm.com \
    --cc=mingo@redhat.com \
    --cc=namhyung@kernel.org \
    --cc=peterz@infradead.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox