@@ -71,7 +71,13 @@ class FlamegraphCollector(StackTraceCollector):
7171 def __init__ (self , * args , ** kwargs ):
7272 super ().__init__ (* args , ** kwargs )
7373 self .stats = {}
74- self ._root = {"samples" : 0 , "children" : {}, "threads" : set ()}
74+ self ._root = {
75+ "samples" : 0 ,
76+ "children" : {},
77+ "threads" : set (),
78+ "thread_samples" : collections .Counter (),
79+ "thread_self" : collections .Counter (),
80+ }
7581 self ._total_samples = 0
7682 self ._sample_count = 0 # Track actual number of samples (not thread traces)
7783 self ._func_intern = {}
@@ -220,7 +226,18 @@ def convert_children(children, min_samples, path_info):
220226 out = []
221227 for func , node in children .items ():
222228 samples = node ["samples" ]
223- if samples < min_samples :
229+ significant_for_thread = any (
230+ thread_samples >= max (
231+ 1 ,
232+ int (
233+ self ._root ["thread_samples" ][thread_id ]
234+ * 0.001
235+ ),
236+ )
237+ for thread_id , thread_samples
238+ in node ["thread_samples" ].items ()
239+ )
240+ if samples < min_samples and not significant_for_thread :
224241 continue
225242
226243 # Intern all string components for maximum efficiency
@@ -243,6 +260,15 @@ def convert_children(children, min_samples, path_info):
243260 "lineno" : func [1 ],
244261 "funcname" : funcname_idx ,
245262 "threads" : sorted (list (node .get ("threads" , set ()))),
263+ "thread_values" : {
264+ thread_id : [
265+ samples ,
266+ node ["thread_self" ].get (thread_id , 0 ),
267+ ]
268+ for thread_id , samples in sorted (
269+ node ["thread_samples" ].items ()
270+ )
271+ },
246272 }
247273
248274 source = self ._get_source_lines (func )
@@ -255,6 +281,14 @@ def convert_children(children, min_samples, path_info):
255281 opcodes = node .get ("opcodes" , {})
256282 if opcodes :
257283 child_entry ["opcodes" ] = dict (opcodes )
284+ thread_opcodes = node .get ("thread_opcodes" )
285+ if thread_opcodes :
286+ child_entry ["thread_opcodes" ] = {
287+ thread_id : dict (counts )
288+ for thread_id , counts in sorted (
289+ thread_opcodes .items ()
290+ )
291+ }
258292
259293 # Recurse
260294 child_entry ["children" ] = convert_children (
@@ -311,7 +345,25 @@ def convert_children(children, min_samples, path_info):
311345 opcode_mapping = get_opcode_mapping ()
312346
313347 # If we only have one root child, make it the root to avoid redundant level
314- if len (root_children ) == 1 :
348+ root_thread_values = {
349+ thread_id : [samples , 0 ]
350+ for thread_id , samples in sorted (
351+ self ._root ["thread_samples" ].items ()
352+ )
353+ }
354+ sole_root_covers_profile = (
355+ len (root_children ) == 1
356+ and root_children [0 ]["value" ] == total_samples
357+ and {
358+ thread_id : values [0 ]
359+ for thread_id , values
360+ in root_children [0 ]["thread_values" ].items ()
361+ } == {
362+ thread_id : values [0 ]
363+ for thread_id , values in root_thread_values .items ()
364+ }
365+ )
366+ if sole_root_covers_profile :
315367 main_child = root_children [0 ]
316368 # Update name and label to indicate it's the program root
317369 old_name = self ._string_table .get_string (main_child ["name" ])
@@ -340,6 +392,7 @@ def convert_children(children, min_samples, path_info):
340392 "per_thread_stats" : per_thread_stats_with_pct
341393 },
342394 "threads" : sorted (list (self ._all_threads )),
395+ "thread_values" : root_thread_values ,
343396 "strings" : self ._string_table .get_strings (),
344397 "opcode_mapping" : opcode_mapping
345398 }
@@ -356,6 +409,7 @@ def process_frames(self, frames, thread_id, weight=1):
356409 """
357410 # Reverse to root->leaf order for tree building
358411 self ._root ["samples" ] += weight
412+ self ._root ["thread_samples" ][thread_id ] += weight
359413 self ._total_samples += weight
360414 self ._root ["threads" ].add (thread_id )
361415 self ._all_threads .add (thread_id )
@@ -368,18 +422,32 @@ def process_frames(self, frames, thread_id, weight=1):
368422
369423 node = current ["children" ].get (func )
370424 if node is None :
371- node = {"samples" : 0 , "children" : {}, "threads" : set (), "opcodes" : collections .Counter (), "self" : 0 }
425+ node = {
426+ "samples" : 0 ,
427+ "children" : {},
428+ "threads" : set (),
429+ "thread_samples" : collections .Counter (),
430+ "thread_self" : collections .Counter (),
431+ "opcodes" : collections .Counter (),
432+ "self" : 0 ,
433+ }
372434 current ["children" ][func ] = node
373435 node ["samples" ] += weight
436+ node ["thread_samples" ][thread_id ] += weight
374437 node ["threads" ].add (thread_id )
375438
376439 if opcode is not None :
377440 node ["opcodes" ][opcode ] += weight
441+ thread_opcodes = node .setdefault ("thread_opcodes" , {})
442+ thread_opcodes .setdefault (
443+ thread_id , collections .Counter ()
444+ )[opcode ] += weight
378445
379446 current = node
380447
381448 if current is not self ._root :
382449 current ["self" ] += weight
450+ current ["thread_self" ][thread_id ] += weight
383451
384452 def _get_source_lines (self , func ):
385453 filename , lineno , _ = func
0 commit comments