Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
dd27e5e
Extend RemoteUnwinder to capture precise bytecode locations
pablogsal Dec 3, 2025
70f2ae0
Add opcode utilities and --opcodes CLI flag
pablogsal Dec 3, 2025
aedc000
Track opcode sample counts in flamegraph collector
pablogsal Dec 3, 2025
19ff11b
Emit opcode interval markers in Gecko collector
pablogsal Dec 3, 2025
af27d23
Add bytecode panel to heatmap visualization
pablogsal Dec 3, 2025
7ffe4cb
Add opcode panel to live profiler TUI
pablogsal Dec 3, 2025
8b423df
Update tests for location tuple and opcode field
pablogsal Dec 3, 2025
8129e3d
Merge remote-tracking branch 'upstream/main' into tachyon-opcodes
pablogsal Dec 7, 2025
965f521
Better test
pablogsal Dec 7, 2025
dac78a5
Merge remote-tracking branch 'upstream/main' into tachyon-opcodes
pablogsal Dec 7, 2025
12c02f6
Add news entry
pablogsal Dec 7, 2025
c10628a
CI fixes
pablogsal Dec 7, 2025
04563f0
CI fixes
pablogsal Dec 8, 2025
f368890
Fix C-API calls
pablogsal Dec 8, 2025
93f7abd
CSS fixes for classes and dark mode
savannahostrowski Dec 9, 2025
dc127bb
Merge pull request #110 from savannahostrowski/tachyon-opcodes-savannah
pablogsal Dec 9, 2025
43a298b
address review
pablogsal Dec 9, 2025
a4685fd
Merge branch 'main' into tachyon-opcodes
pablogsal Dec 9, 2025
b13b6f0
Docs
pablogsal Dec 9, 2025
50f63d0
Make bytecode spacer
pablogsal Dec 9, 2025
6eed927
Update Lib/profiling/sampling/_heatmap_assets/heatmap.js
pablogsal Dec 9, 2025
1c630ce
Update heatmap.css
pablogsal Dec 9, 2025
56e68c1
Tachyon Heatmap responsive styles
savannahostrowski Dec 10, 2025
e010870
Merge pull request #113 from savannahostrowski/heatmap-responsive
pablogsal Dec 10, 2025
ede0f79
Update Doc/library/profiling.sampling.rst
pablogsal Dec 10, 2025
f838c5d
Fix shift when selecting
StanFromIreland Dec 10, 2025
13ecd61
Merge pull request #114 from StanFromIreland/tachyon-opcodes-jump
pablogsal Dec 10, 2025
66610ff
Use any for stdlib line numbers
pablogsal Dec 10, 2025
aab1f3c
Update profiling.sampling.rst
pablogsal Dec 11, 2025
947f555
Docs
pablogsal Dec 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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.
  • Loading branch information
pablogsal committed Dec 3, 2025
commit aedc000a1573b77df73e473cd6d93a2089085d98
79 changes: 79 additions & 0 deletions Lib/profiling/sampling/_flamegraph_assets/flamegraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ let currentThreadFilter = 'all';
// Heat colors are now defined in CSS variables (--heat-1 through --heat-8)
// and automatically switch with theme changes - no JS color arrays needed!

// Opcode mappings - loaded from embedded data (generated by Python)
let OPCODE_NAMES = {};
let DEOPT_MAP = {};

// Initialize opcode mappings from embedded data
function initOpcodeMapping(data) {
if (data && data.opcode_mapping) {
OPCODE_NAMES = data.opcode_mapping.names || {};
DEOPT_MAP = data.opcode_mapping.deopt || {};
}
}

// Get opcode info from opcode number
function getOpcodeInfo(opcode) {
const opname = OPCODE_NAMES[opcode] || `<${opcode}>`;
const baseOpcode = DEOPT_MAP[opcode];
const isSpecialized = baseOpcode !== undefined;
const baseOpname = isSpecialized ? (OPCODE_NAMES[baseOpcode] || `<${baseOpcode}>`) : opname;

return {
opname: opname,
baseOpname: baseOpname,
isSpecialized: isSpecialized
};
}

// ============================================================================
// String Resolution
// ============================================================================
Expand Down Expand Up @@ -249,6 +275,55 @@ function createPythonTooltip(data) {
</div>`;
}

// Create bytecode/opcode section if available
let opcodeSection = "";
const opcodes = d.data.opcodes;
if (opcodes && typeof opcodes === 'object' && Object.keys(opcodes).length > 0) {
// Sort opcodes by sample count (descending)
const sortedOpcodes = Object.entries(opcodes)
.sort((a, b) => b[1] - a[1])
.slice(0, 8); // Limit to top 8

const totalOpcodeSamples = sortedOpcodes.reduce((sum, [, count]) => sum + count, 0);
const maxCount = sortedOpcodes[0][1] || 1;

const opcodeLines = sortedOpcodes.map(([opcode, count]) => {
const opcodeInfo = getOpcodeInfo(parseInt(opcode, 10));
const pct = ((count / totalOpcodeSamples) * 100).toFixed(1);
const barWidth = (count / maxCount) * 100;
const specializedBadge = opcodeInfo.isSpecialized
? '<span style="background: #2e7d32; color: white; font-size: 9px; padding: 1px 4px; border-radius: 3px; margin-left: 4px;">SPECIALIZED</span>'
: '';
const baseOpHint = opcodeInfo.isSpecialized
? `<span style="color: #888; font-size: 11px; margin-left: 4px;">(${opcodeInfo.baseOpname})</span>`
: '';

return `
<div style="display: grid; grid-template-columns: 1fr 60px 60px; gap: 8px; align-items: center; padding: 3px 0;">
<div style="font-family: monospace; font-size: 11px; color: ${opcodeInfo.isSpecialized ? '#2e7d32' : '#333'}; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
${opcodeInfo.opname}${baseOpHint}${specializedBadge}
</div>
<div style="text-align: right; font-size: 11px; color: #666;">${count.toLocaleString()}</div>
<div style="background: #e9ecef; border-radius: 2px; height: 8px; overflow: hidden;">
<div style="background: linear-gradient(90deg, #3776ab, #5a9bd5); height: 100%; width: ${barWidth}%;"></div>
</div>
</div>`;
}).join('');

opcodeSection = `
<div style="margin-top: 16px; padding-top: 12px;
border-top: 1px solid #e9ecef;">
<div style="color: #3776ab; font-size: 13px;
margin-bottom: 8px; font-weight: 600;">
Bytecode Instructions:
</div>
<div style="background: #f8f9fa; border: 1px solid #e9ecef;
border-radius: 6px; padding: 10px;">
${opcodeLines}
</div>
</div>`;
}

const fileLocationHTML = isSpecialFrame ? "" : `
<div class="tooltip-location">${filename}${d.data.lineno ? ":" + d.data.lineno : ""}</div>`;

Expand All @@ -275,6 +350,7 @@ function createPythonTooltip(data) {
` : ''}
</div>
${sourceSection}
${opcodeSection}
<div class="tooltip-hint">
${childCount > 0 ? "Click to zoom into this function" : "Leaf function - no children"}
</div>
Expand Down Expand Up @@ -994,6 +1070,9 @@ function initFlamegraph() {
processedData = resolveStringIndices(EMBEDDED_DATA);
}

// Initialize opcode mapping from embedded data
initOpcodeMapping(EMBEDDED_DATA);

originalData = processedData;
initThreadFilter(processedData);

Expand Down
48 changes: 38 additions & 10 deletions Lib/profiling/sampling/stack_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand All @@ -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 {
Expand All @@ -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):
Expand Down