@@ -68,6 +68,10 @@ def export(self, filename):
6868 return True
6969
7070
71+ # Bounded by the unwinder's maximum captured stack depth (MAX_FRAMES).
72+ _FLAMEGRAPH_RECURSION_MARGIN = 2000
73+
74+
7175class FlamegraphCollector (StackTraceCollector ):
7276 def __init__ (self , * args , ** kwargs ):
7377 super ().__init__ (* args , ** kwargs )
@@ -166,34 +170,41 @@ def set_mode(self, mode):
166170 self .stats ["mode" ] = mode
167171
168172 def export (self , filename ):
169- flamegraph_data = self ._convert_to_flamegraph_format ()
170-
171- # Debug output with string table statistics
172- num_functions = len (flamegraph_data .get ("children" , []))
173- total_time = flamegraph_data .get ("value" , 0 )
174- string_count = len (self ._string_table )
175- s1 = "" if num_functions == 1 else "s"
176- s2 = "" if total_time == 1 else "s"
177- s3 = "" if string_count == 1 else "s"
178- print (
179- f"Flamegraph data: { num_functions } root function{ s1 } , "
180- f"{ total_time } total sample{ s2 } , "
181- f"{ string_count } unique string{ s3 } "
182- )
183-
184- if num_functions == 0 :
173+ # Converting the call tree recurses to the sampled stack depth.
174+ old_limit = sys .getrecursionlimit ()
175+ sys .setrecursionlimit (old_limit + _FLAMEGRAPH_RECURSION_MARGIN )
176+ try :
177+ flamegraph_data = self ._convert_to_flamegraph_format ()
178+
179+ # Debug output with string table statistics
180+ num_functions = len (flamegraph_data .get ("children" , []))
181+ total_time = flamegraph_data .get ("value" , 0 )
182+ string_count = len (self ._string_table )
183+ s1 = "" if num_functions == 1 else "s"
184+ s2 = "" if total_time == 1 else "s"
185+ s3 = "" if string_count == 1 else "s"
185186 print (
186- "Warning: No functions found in profiling data. Check if sampling captured any data."
187+ f"Flamegraph data: { num_functions } root function{ s1 } , "
188+ f"{ total_time } total sample{ s2 } , "
189+ f"{ string_count } unique string{ s3 } "
187190 )
188- return False
189191
190- html_content = self ._create_flamegraph_html (flamegraph_data )
192+ if num_functions == 0 :
193+ print (
194+ "Warning: No functions found in profiling data. "
195+ "Check if sampling captured any data."
196+ )
197+ return False
191198
192- with open (filename , "w" , encoding = "utf-8" ) as f :
193- f .write (html_content )
199+ html_content = self ._create_flamegraph_html (flamegraph_data )
194200
195- print (f"Flamegraph saved to: { filename } " )
196- return True
201+ with open (filename , "w" , encoding = "utf-8" ) as f :
202+ f .write (html_content )
203+
204+ print (f"Flamegraph saved to: { filename } " )
205+ return True
206+ finally :
207+ sys .setrecursionlimit (old_limit )
197208
198209 @staticmethod
199210 @functools .lru_cache (maxsize = None )
@@ -665,16 +676,16 @@ def _convert_to_flamegraph_format(self):
665676 current_stats = self ._aggregate_path_samples (self ._root )
666677 baseline_stats = self ._aggregate_path_samples (self ._baseline_collector ._root )
667678
668- # Scale baseline values to make them comparable, accounting for both
669- # sample count differences and sample interval differences.
679+ # Express baseline samples in units of the current sample interval.
680+ # Do not normalize by total profile duration: doing so makes unchanged
681+ # functions appear different when another function becomes faster or
682+ # slower.
670683 baseline_total = self ._baseline_collector ._total_samples
671- if baseline_total > 0 and self ._total_samples > 0 :
672- current_time = self ._total_samples * self .sample_interval_usec
673- baseline_time = baseline_total * self ._baseline_collector .sample_interval_usec
674- scale = current_time / baseline_time
675- elif baseline_total > 0 :
676- # Current profile is empty - use interval-based scale for elided display
677- scale = self .sample_interval_usec / self ._baseline_collector .sample_interval_usec
684+ if baseline_total > 0 :
685+ scale = (
686+ self ._baseline_collector .sample_interval_usec
687+ / self .sample_interval_usec
688+ )
678689 else :
679690 scale = 1.0
680691
@@ -891,6 +902,10 @@ def _add_elided_metadata(self, node, baseline_stats, scale, path):
891902 else :
892903 node ["diff_pct" ] = 0.0
893904
905+ # Scale geometry after computing metadata from raw baseline counts.
906+ node ["value" ] = node .get ("value" , 0 ) * scale
907+ node ["self" ] = node .get ("self" , 0 ) * scale
908+
894909 if "children" in node and node ["children" ]:
895910 for child in node ["children" ]:
896911 self ._add_elided_metadata (child , baseline_stats , scale , current_path )
0 commit comments