mirror of
https://github.com/python/cpython.git
synced 2025-12-08 06:10:17 +00:00
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:
parent
70f2ae025f
commit
aedc000a15
2 changed files with 117 additions and 10 deletions
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
@ -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>`;
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -994,6 +1070,9 @@ function initFlamegraph() {
|
|||
processedData = resolveStringIndices(EMBEDDED_DATA);
|
||||
}
|
||||
|
||||
// Initialize opcode mapping from embedded data
|
||||
initOpcodeMapping(EMBEDDED_DATA);
|
||||
|
||||
originalData = processedData;
|
||||
initThreadFilter(processedData);
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue