@@ -449,11 +449,12 @@ class DiffFlamegraphCollector(FlamegraphCollector):
449449
450450 def __init__ (self , sample_interval_usec , * , baseline_binary_path , skip_idle = False ):
451451 super ().__init__ (sample_interval_usec , skip_idle = skip_idle )
452+ if not os .path .exists (baseline_binary_path ):
453+ raise ValueError (f"Baseline file not found: { baseline_binary_path } " )
452454 self .baseline_binary_path = baseline_binary_path
453455 self ._baseline_collector = None
454456 self ._elided_paths = set ()
455457
456-
457458 def _load_baseline (self ):
458459 """Load baseline profile from binary file."""
459460 from .binary_reader import BinaryReader
@@ -478,7 +479,8 @@ def _aggregate_path_samples(self, root_node, path=None):
478479 stats = {}
479480
480481 for func , node in root_node ["children" ].items ():
481- func_key = (func [0 ], func [2 ])
482+ filename , _lineno , funcname = func
483+ func_key = (filename , funcname )
482484 path_key = path + (func_key ,)
483485
484486 total_samples = node .get ("samples" , 0 )
@@ -509,15 +511,22 @@ def _convert_to_flamegraph_format(self):
509511 self ._load_baseline ()
510512
511513 current_flamegraph = super ()._convert_to_flamegraph_format ()
512- if self ._total_samples == 0 :
513- return current_flamegraph
514514
515515 current_stats = self ._aggregate_path_samples (self ._root )
516516 baseline_stats = self ._aggregate_path_samples (self ._baseline_collector ._root )
517517
518- # Scale baseline values to make them comparable when sample counts differ
519- scale = (self ._total_samples / self ._baseline_collector ._total_samples
520- if self ._baseline_collector ._total_samples > 0 else 1.0 )
518+ # Scale baseline values to make them comparable, accounting for both
519+ # sample count differences and sample interval differences.
520+ baseline_total = self ._baseline_collector ._total_samples
521+ if baseline_total > 0 and self ._total_samples > 0 :
522+ current_time = self ._total_samples * self .sample_interval_usec
523+ baseline_time = baseline_total * self ._baseline_collector .sample_interval_usec
524+ scale = current_time / baseline_time
525+ elif baseline_total > 0 :
526+ # Current profile is empty - use interval-based scale for elided display
527+ scale = self .sample_interval_usec / self ._baseline_collector .sample_interval_usec
528+ else :
529+ scale = 1.0
521530
522531 self ._annotate_nodes_with_diff (current_flamegraph , current_stats , baseline_stats , scale )
523532 self ._add_elided_flamegraph (current_flamegraph , current_stats , baseline_stats , scale )
@@ -576,7 +585,7 @@ def _is_promoted_root(self, data):
576585
577586 def _add_elided_flamegraph (self , current_flamegraph , current_stats , baseline_stats , scale ):
578587 """Calculate elided paths and add elided flamegraph to stats."""
579- self ._elided_paths = set ( baseline_stats .keys ()) - set ( current_stats .keys () )
588+ self ._elided_paths = baseline_stats .keys () - current_stats .keys ()
580589
581590 current_flamegraph ["stats" ]["elided_count" ] = len (self ._elided_paths )
582591
@@ -586,23 +595,39 @@ def _add_elided_flamegraph(self, current_flamegraph, current_stats, baseline_sta
586595 current_flamegraph ["stats" ]["elided_flamegraph" ] = elided_flamegraph
587596
588597 def _build_elided_flamegraph (self , baseline_stats , scale ):
589- """Build flamegraph containing only elided paths from baseline."""
598+ """Build flamegraph containing only elided paths from baseline.
599+
600+ This re-runs the base conversion pipeline on the baseline collector
601+ to produce a complete formatted flamegraph, then prunes it to keep
602+ only elided paths.
603+ """
590604 if not self ._baseline_collector or not self ._elided_paths :
591605 return None
592606
593- baseline_data = self ._baseline_collector ._convert_to_flamegraph_format ()
607+ # Suppress source line collection for elided nodes - these functions
608+ # no longer exist in the current profile, so source lines from the
609+ # current machine's filesystem would be misleading or unavailable.
610+ orig_get_source = self ._baseline_collector ._get_source_lines
611+ self ._baseline_collector ._get_source_lines = lambda func : None
612+ try :
613+ baseline_data = self ._baseline_collector ._convert_to_flamegraph_format ()
614+ finally :
615+ self ._baseline_collector ._get_source_lines = orig_get_source
594616
595617 # Remove non-elided nodes and recalculate values
596618 if not self ._extract_elided_nodes (baseline_data , path = ()):
597619 return None
598620
599621 self ._add_elided_metadata (baseline_data , baseline_stats , scale , path = ())
600622
601- baseline_data ["stats" ].update (self .stats )
623+ # Merge only profiling metadata, not thread-level stats
624+ for key in ("sample_interval_usec" , "duration_sec" , "sample_rate" ,
625+ "error_rate" , "missed_samples" , "mode" ):
626+ if key in self .stats :
627+ baseline_data ["stats" ][key ] = self .stats [key ]
602628 baseline_data ["stats" ]["is_differential" ] = True
603629 baseline_data ["stats" ]["baseline_samples" ] = self ._baseline_collector ._total_samples
604630 baseline_data ["stats" ]["current_samples" ] = self ._total_samples
605- baseline_data ["baseline_strings" ] = self ._baseline_collector ._string_table .get_strings ()
606631
607632 return baseline_data
608633
@@ -626,8 +651,9 @@ def _extract_elided_nodes(self, node, path):
626651 total_value += child .get ("value" , 0 )
627652 node ["children" ] = elided_children
628653
629- # Recalculate value based on remaining children
630- if elided_children :
654+ # Recalculate value for structural (non-elided) ancestor nodes;
655+ # elided nodes keep their original value to preserve self-samples
656+ if elided_children and not is_elided :
631657 node ["value" ] = total_value
632658
633659 # Keep this node if it's elided or has elided descendants
@@ -655,7 +681,14 @@ def _add_elided_metadata(self, node, baseline_stats, scale, path):
655681 node ["diff" ] = 0
656682
657683 node ["self_time" ] = 0
658- node ["diff_pct" ] = - 100.0
684+ # Elided paths have zero current self-time, so the change is always
685+ # -100% when there was actual baseline self-time to lose.
686+ # For internal nodes with no baseline self-time, use 0% to avoid
687+ # misleading tooltips.
688+ if baseline_self > 0 :
689+ node ["diff_pct" ] = - 100.0
690+ else :
691+ node ["diff_pct" ] = 0.0
659692
660693 if "children" in node and node ["children" ]:
661694 for child in node ["children" ]:
0 commit comments