Track opcode sample counts in flamegraph collector

Stores per-node opcode counts in the tree structure. Exports opcode
mapping (names and deopt relationships) in JSON so the JS renderer can
show instruction names and distinguish specialized variants.
This commit is contained in:
Pablo Galindo Salgado 2025-12-03 03:43:18 +00:00
parent 70f2ae025f
commit aedc000a15
2 changed files with 117 additions and 10 deletions

View file

@ -7,7 +7,8 @@
import os
from ._css_utils import get_combined_css
from .collector import Collector
from .collector import Collector, extract_lineno
from .opcode_utils import get_opcode_mapping
from .string_table import StringTable
@ -32,7 +33,11 @@ def __init__(self, *args, **kwargs):
self.stack_counter = collections.Counter()
def process_frames(self, frames, thread_id):
call_tree = tuple(reversed(frames))
# Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks
# frame is (filename, location, funcname, opcode)
call_tree = tuple(
(f[0], extract_lineno(f[1]), f[2]) for f in reversed(frames)
)
self.stack_counter[(call_tree, thread_id)] += 1
def export(self, filename):
@ -205,6 +210,11 @@ def convert_children(children, min_samples):
source_indices = [self._string_table.intern(line) for line in source]
child_entry["source"] = source_indices
# Include opcode data if available
opcodes = node.get("opcodes", {})
if opcodes:
child_entry["opcodes"] = dict(opcodes)
# Recurse
child_entry["children"] = convert_children(
node["children"], min_samples
@ -251,6 +261,9 @@ def convert_children(children, min_samples):
**stats
}
# Build opcode mapping for JS
opcode_mapping = get_opcode_mapping()
# If we only have one root child, make it the root to avoid redundant level
if len(root_children) == 1:
main_child = root_children[0]
@ -265,6 +278,7 @@ def convert_children(children, min_samples):
}
main_child["threads"] = sorted(list(self._all_threads))
main_child["strings"] = self._string_table.get_strings()
main_child["opcode_mapping"] = opcode_mapping
return main_child
return {
@ -277,27 +291,41 @@ def convert_children(children, min_samples):
"per_thread_stats": per_thread_stats_with_pct
},
"threads": sorted(list(self._all_threads)),
"strings": self._string_table.get_strings()
"strings": self._string_table.get_strings(),
"opcode_mapping": opcode_mapping
}
def process_frames(self, frames, thread_id):
# Reverse to root->leaf
call_tree = reversed(frames)
"""Process stack frames into flamegraph tree structure.
Args:
frames: List of (filename, location, funcname, opcode) tuples in
leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
opcode is None if not gathered.
thread_id: Thread ID for this stack trace
"""
# Reverse to root->leaf order for tree building
self._root["samples"] += 1
self._total_samples += 1
self._root["threads"].add(thread_id)
self._all_threads.add(thread_id)
current = self._root
for func in call_tree:
for filename, location, funcname, opcode in reversed(frames):
lineno = extract_lineno(location)
func = (filename, lineno, funcname)
func = self._func_intern.setdefault(func, func)
children = current["children"]
node = children.get(func)
node = current["children"].get(func)
if node is None:
node = {"samples": 0, "children": {}, "threads": set()}
children[func] = node
node = {"samples": 0, "children": {}, "threads": set(), "opcodes": collections.Counter()}
current["children"][func] = node
node["samples"] += 1
node["threads"].add(thread_id)
if opcode is not None:
node["opcodes"][opcode] += 1
current = node
def _get_source_lines(self, func):