77import os
88
99from ._css_utils import get_combined_css
10- from .collector import Collector
10+ from .collector import Collector , extract_lineno
11+ from .opcode_utils import get_opcode_mapping
1112from .string_table import StringTable
1213
1314
@@ -32,7 +33,11 @@ def __init__(self, *args, **kwargs):
3233 self .stack_counter = collections .Counter ()
3334
3435 def process_frames (self , frames , thread_id ):
35- call_tree = tuple (reversed (frames ))
36+ # Extract only (filename, lineno, funcname) - opcode not needed for collapsed stacks
37+ # frame is (filename, location, funcname, opcode)
38+ call_tree = tuple (
39+ (f [0 ], extract_lineno (f [1 ]), f [2 ]) for f in reversed (frames )
40+ )
3641 self .stack_counter [(call_tree , thread_id )] += 1
3742
3843 def export (self , filename ):
@@ -205,6 +210,11 @@ def convert_children(children, min_samples):
205210 source_indices = [self ._string_table .intern (line ) for line in source ]
206211 child_entry ["source" ] = source_indices
207212
213+ # Include opcode data if available
214+ opcodes = node .get ("opcodes" , {})
215+ if opcodes :
216+ child_entry ["opcodes" ] = dict (opcodes )
217+
208218 # Recurse
209219 child_entry ["children" ] = convert_children (
210220 node ["children" ], min_samples
@@ -251,6 +261,9 @@ def convert_children(children, min_samples):
251261 ** stats
252262 }
253263
264+ # Build opcode mapping for JS
265+ opcode_mapping = get_opcode_mapping ()
266+
254267 # If we only have one root child, make it the root to avoid redundant level
255268 if len (root_children ) == 1 :
256269 main_child = root_children [0 ]
@@ -265,6 +278,7 @@ def convert_children(children, min_samples):
265278 }
266279 main_child ["threads" ] = sorted (list (self ._all_threads ))
267280 main_child ["strings" ] = self ._string_table .get_strings ()
281+ main_child ["opcode_mapping" ] = opcode_mapping
268282 return main_child
269283
270284 return {
@@ -277,27 +291,41 @@ def convert_children(children, min_samples):
277291 "per_thread_stats" : per_thread_stats_with_pct
278292 },
279293 "threads" : sorted (list (self ._all_threads )),
280- "strings" : self ._string_table .get_strings ()
294+ "strings" : self ._string_table .get_strings (),
295+ "opcode_mapping" : opcode_mapping
281296 }
282297
283298 def process_frames (self , frames , thread_id ):
284- # Reverse to root->leaf
285- call_tree = reversed (frames )
299+ """Process stack frames into flamegraph tree structure.
300+
301+ Args:
302+ frames: List of (filename, location, funcname, opcode) tuples in
303+ leaf-to-root order. location is (lineno, end_lineno, col_offset, end_col_offset).
304+ opcode is None if not gathered.
305+ thread_id: Thread ID for this stack trace
306+ """
307+ # Reverse to root->leaf order for tree building
286308 self ._root ["samples" ] += 1
287309 self ._total_samples += 1
288310 self ._root ["threads" ].add (thread_id )
289311 self ._all_threads .add (thread_id )
290312
291313 current = self ._root
292- for func in call_tree :
314+ for filename , location , funcname , opcode in reversed (frames ):
315+ lineno = extract_lineno (location )
316+ func = (filename , lineno , funcname )
293317 func = self ._func_intern .setdefault (func , func )
294- children = current [ "children" ]
295- node = children .get (func )
318+
319+ node = current [ " children" ] .get (func )
296320 if node is None :
297- node = {"samples" : 0 , "children" : {}, "threads" : set ()}
298- children [func ] = node
321+ node = {"samples" : 0 , "children" : {}, "threads" : set (), "opcodes" : collections . Counter () }
322+ current [ " children" ] [func ] = node
299323 node ["samples" ] += 1
300324 node ["threads" ].add (thread_id )
325+
326+ if opcode is not None :
327+ node ["opcodes" ][opcode ] += 1
328+
301329 current = node
302330
303331 def _get_source_lines (self , func ):
0 commit comments