@@ -84,8 +84,8 @@ def __init__(self, *, skip_idle=False):
8484 self .native_code_start = {} # Thread running native code (on CPU without GIL)
8585 self .gil_wait_start = {} # Thread waiting for GIL
8686
87- # GC event tracking: track if we're currently in a GC
88- self .potential_gc_start = None
87+ # GC event tracking: track GC start time per thread
88+ self .gc_start_per_thread = {} # tid -> start_time
8989
9090 # Track which threads have been initialized for state tracking
9191 self .initialized_threads = set ()
@@ -136,17 +136,12 @@ def collect(self, stack_frames):
136136 ) / self .sample_count
137137 self .last_sample_time = current_time
138138
139- # GC Event Detection and process threads
140- gc_collecting = False
141-
139+ # Process threads and track GC per thread
142140 for interpreter_info in stack_frames :
143141 for thread_info in interpreter_info .threads :
144- # Track GC status
145- if thread_info .gc_collecting :
146- gc_collecting = True
147-
148142 frames = thread_info .frame_info
149143 tid = thread_info .thread_id
144+ gc_collecting = thread_info .gc_collecting
150145
151146 # Initialize thread if needed
152147 if tid not in self .threads :
@@ -202,6 +197,19 @@ def collect(self, stack_frames):
202197 self ._add_marker (tid , "Waiting for GIL" , self .gil_wait_start .pop (tid ),
203198 current_time , CATEGORY_GIL )
204199
200+ # Track GC events - attribute to all threads that hold the GIL during GC
201+ # (GC is interpreter-wide but runs on whichever thread(s) have the GIL)
202+ # If GIL switches during GC, multiple threads will get GC markers
203+ if gc_collecting and has_gil :
204+ # Start GC marker if not already started for this thread
205+ if tid not in self .gc_start_per_thread :
206+ self .gc_start_per_thread [tid ] = current_time
207+ elif tid in self .gc_start_per_thread :
208+ # End GC marker if it was running for this thread
209+ # (either GC finished or thread lost GIL)
210+ self ._add_marker (tid , "GC Collecting" , self .gc_start_per_thread .pop (tid ),
211+ current_time , CATEGORY_GC )
212+
205213 # Mark thread as initialized after processing all state transitions
206214 self .initialized_threads .add (tid )
207215
@@ -224,17 +232,6 @@ def collect(self, stack_frames):
224232 samples ["time" ].append (current_time )
225233 samples ["eventDelay" ].append (None )
226234
227- # Handle GC event markers after processing all threads
228- if gc_collecting :
229- if self .potential_gc_start is None :
230- # Start of GC
231- self .potential_gc_start = current_time
232- else :
233- # End of GC
234- if self .potential_gc_start is not None :
235- self ._add_gc_marker (self .potential_gc_start , current_time )
236- self .potential_gc_start = None
237-
238235 self .sample_count += 1
239236
240237 def _create_thread (self , tid ):
@@ -372,24 +369,6 @@ def _add_marker(self, tid, name, start_time, end_time, category):
372369 "tid" : tid
373370 })
374371
375- def _add_gc_marker (self , start_time , end_time ):
376- """Add a GC Collecting event marker to the main thread."""
377- if not self .threads :
378- return
379-
380- # Find the main thread by checking isMainThread flag
381- main_tid = None
382- for tid , thread_data in self .threads .items ():
383- if thread_data .get ("isMainThread" , False ):
384- main_tid = tid
385- break
386-
387- # If we can't find the main thread, use the first thread as fallback
388- if main_tid is None :
389- main_tid = next (iter (self .threads ))
390-
391- self ._add_marker (main_tid , "GC Collecting" , start_time , end_time , CATEGORY_GC )
392-
393372 def _process_stack (self , thread_data , frames ):
394373 """Process a stack and return the stack index."""
395374 if not frames :
@@ -571,18 +550,14 @@ def _finalize_markers(self):
571550 (self .python_code_start , "Python Code" , CATEGORY_CODE_TYPE ),
572551 (self .native_code_start , "Native Code" , CATEGORY_CODE_TYPE ),
573552 (self .gil_wait_start , "Waiting for GIL" , CATEGORY_GIL ),
553+ (self .gc_start_per_thread , "GC Collecting" , CATEGORY_GC ),
574554 ]
575555
576556 for state_dict , marker_name , category in marker_states :
577557 for tid in list (state_dict .keys ()):
578558 self ._add_marker (tid , marker_name , state_dict [tid ], end_time , category )
579559 del state_dict [tid ]
580560
581- # Close any open GC marker
582- if self .potential_gc_start is not None :
583- self ._add_gc_marker (self .potential_gc_start , end_time )
584- self .potential_gc_start = None
585-
586561 def export (self , filename ):
587562 """Export the profile to a Gecko JSON file."""
588563
0 commit comments