@@ -87,6 +87,44 @@ def __init__(self, *, skip_idle=False):
8787 # GC event tracking: track if we're currently in a GC
8888 self .potential_gc_start = None
8989
90+ # Track which threads have been initialized for state tracking
91+ self .initialized_threads = set ()
92+
93+ def _track_state_transition (self , tid , condition , active_dict , inactive_dict ,
94+ active_name , inactive_name , category , current_time ):
95+ """Track binary state transitions and emit markers.
96+
97+ Args:
98+ tid: Thread ID
99+ condition: Whether the active state is true
100+ active_dict: Dict tracking start time of active state
101+ inactive_dict: Dict tracking start time of inactive state
102+ active_name: Name for active state marker
103+ inactive_name: Name for inactive state marker
104+ category: Gecko category for the markers
105+ current_time: Current timestamp
106+ """
107+ # On first observation of a thread, just record the current state
108+ # without creating a marker (we don't know what the previous state was)
109+ if tid not in self .initialized_threads :
110+ if condition :
111+ active_dict [tid ] = current_time
112+ else :
113+ inactive_dict [tid ] = current_time
114+ return
115+
116+ # For already-initialized threads, track transitions
117+ if condition :
118+ active_dict .setdefault (tid , current_time )
119+ if tid in inactive_dict :
120+ self ._add_marker (tid , inactive_name , inactive_dict .pop (tid ),
121+ current_time , category )
122+ else :
123+ inactive_dict .setdefault (tid , current_time )
124+ if tid in active_dict :
125+ self ._add_marker (tid , active_name , active_dict .pop (tid ),
126+ current_time , category )
127+
90128 def collect (self , stack_frames ):
91129 """Collect a sample from stack frames."""
92130 current_time = (time .time () * 1000 ) - self .start_time
@@ -120,83 +158,52 @@ def collect(self, stack_frames):
120158 status_flags = thread_info .status
121159 has_gil = bool (status_flags & THREAD_STATUS_HAS_GIL )
122160 on_cpu = bool (status_flags & THREAD_STATUS_ON_CPU )
123- unknown = bool (status_flags & THREAD_STATUS_UNKNOWN )
124161 gil_requested = bool (status_flags & THREAD_STATUS_GIL_REQUESTED )
125162
126- # Track GIL possession (interval marker: "Has GIL" / "No GIL")
127- if has_gil :
128- if tid not in self .has_gil_start :
129- self .has_gil_start [tid ] = current_time
130- # End "No GIL" if it was running
131- if tid in self .no_gil_start :
132- start_time = self .no_gil_start [tid ]
133- self ._add_marker (tid , "No GIL" , start_time , current_time , CATEGORY_GIL )
134- del self .no_gil_start [tid ]
135- else :
136- if tid not in self .no_gil_start :
137- self .no_gil_start [tid ] = current_time
138- # End "Has GIL" if it was running
139- if tid in self .has_gil_start :
140- start_time = self .has_gil_start [tid ]
141- self ._add_marker (tid , "Has GIL" , start_time , current_time , CATEGORY_GIL )
142- del self .has_gil_start [tid ]
143-
144- # Track "On CPU" / "Off CPU" state
145- if on_cpu :
146- if tid not in self .on_cpu_start :
147- self .on_cpu_start [tid ] = current_time
148- # End "Off CPU" if it was running
149- if tid in self .off_cpu_start :
150- start_time = self .off_cpu_start [tid ]
151- self ._add_marker (tid , "Off CPU" , start_time , current_time , CATEGORY_CPU )
152- del self .off_cpu_start [tid ]
153- else :
154- if tid not in self .off_cpu_start :
155- self .off_cpu_start [tid ] = current_time
156- # End "On CPU" if it was running
157- if tid in self .on_cpu_start :
158- start_time = self .on_cpu_start [tid ]
159- self ._add_marker (tid , "On CPU" , start_time , current_time , CATEGORY_CPU )
160- del self .on_cpu_start [tid ]
161-
162- # Track "Python Code" (has GIL) / "Native Code" (on CPU without GIL)
163+ # Track GIL possession (Has GIL / No GIL)
164+ self ._track_state_transition (
165+ tid , has_gil , self .has_gil_start , self .no_gil_start ,
166+ "Has GIL" , "No GIL" , CATEGORY_GIL , current_time
167+ )
168+
169+ # Track CPU state (On CPU / Off CPU)
170+ self ._track_state_transition (
171+ tid , on_cpu , self .on_cpu_start , self .off_cpu_start ,
172+ "On CPU" , "Off CPU" , CATEGORY_CPU , current_time
173+ )
174+
175+ # Track code type (Python Code / Native Code)
176+ # This is tri-state: Python (has_gil), Native (on_cpu without gil), or Neither
163177 if has_gil :
164- if tid not in self .python_code_start :
165- self .python_code_start [tid ] = current_time
166- # End "Native Code" if it was running
167- if tid in self .native_code_start :
168- start_time = self .native_code_start [tid ]
169- self ._add_marker (tid , "Native Code" , start_time , current_time , CATEGORY_CODE_TYPE )
170- del self .native_code_start [tid ]
178+ self ._track_state_transition (
179+ tid , True , self .python_code_start , self .native_code_start ,
180+ "Python Code" , "Native Code" , CATEGORY_CODE_TYPE , current_time
181+ )
171182 elif on_cpu :
172- # Native code: on CPU without GIL
173- if tid not in self .native_code_start :
174- self .native_code_start [tid ] = current_time
175- # End "Python Code" if it was running
176- if tid in self .python_code_start :
177- start_time = self .python_code_start [tid ]
178- self ._add_marker (tid , "Python Code" , start_time , current_time , CATEGORY_CODE_TYPE )
179- del self .python_code_start [tid ]
183+ self ._track_state_transition (
184+ tid , True , self .native_code_start , self .python_code_start ,
185+ "Native Code" , "Python Code" , CATEGORY_CODE_TYPE , current_time
186+ )
180187 else :
181- # Neither has GIL nor on CPU - end both if running
182- if tid in self . python_code_start :
183- start_time = self .python_code_start [ tid ]
184- self . _add_marker ( tid , "Python Code" , start_time , current_time , CATEGORY_CODE_TYPE )
185- del self .python_code_start [ tid ]
186- if tid in self . native_code_start :
187- start_time = self .native_code_start [ tid ]
188- self ._add_marker (tid , "Native Code" , start_time , current_time , CATEGORY_CODE_TYPE )
189- del self . native_code_start [ tid ]
190-
191- # Track "Waiting for GIL" intervals
188+ # Thread is idle (neither has GIL nor on CPU) - close any open code markers
189+ # This handles the third state that _track_state_transition doesn't cover
190+ if tid in self .initialized_threads :
191+ if tid in self . python_code_start :
192+ self ._add_marker ( tid , "Python Code" , self . python_code_start . pop ( tid ),
193+ current_time , CATEGORY_CODE_TYPE )
194+ if tid in self .native_code_start :
195+ self ._add_marker (tid , "Native Code" , self . native_code_start . pop ( tid ),
196+ current_time , CATEGORY_CODE_TYPE )
197+
198+ # Track "Waiting for GIL" intervals (one-sided tracking)
192199 if gil_requested :
193- if tid not in self .gil_wait_start :
194- self .gil_wait_start [ tid ] = current_time
195- else :
196- if tid in self . gil_wait_start :
197- start_time = self . gil_wait_start [ tid ]
198- self . _add_marker ( tid , "Waiting for GIL" , start_time , current_time , CATEGORY_GIL )
199- del self .gil_wait_start [ tid ]
200+ self .gil_wait_start . setdefault ( tid , current_time )
201+ elif tid in self .gil_wait_start :
202+ self . _add_marker ( tid , "Waiting for GIL" , self . gil_wait_start . pop ( tid ),
203+ current_time , CATEGORY_GIL )
204+
205+ # Mark thread as initialized after processing all state transitions
206+ self .initialized_threads . add ( tid )
200207
201208 # Categorize: idle if neither has GIL nor on CPU
202209 is_idle = not has_gil and not on_cpu
@@ -366,13 +373,22 @@ def _add_marker(self, tid, name, start_time, end_time, category):
366373 })
367374
368375 def _add_gc_marker (self , start_time , end_time ):
369- """Add a GC Collecting event marker to the main thread (or first thread we see) ."""
376+ """Add a GC Collecting event marker to the main thread."""
370377 if not self .threads :
371378 return
372379
373- # Add GC marker to the first thread (typically the main thread)
374- first_tid = next (iter (self .threads ))
375- self ._add_marker (first_tid , "GC Collecting" , start_time , end_time , CATEGORY_GC )
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 )
376392
377393 def _process_stack (self , thread_data , frames ):
378394 """Process a stack and return the stack index."""
@@ -546,41 +562,21 @@ def _finalize_markers(self):
546562 """Close any open markers at the end of profiling."""
547563 end_time = self .last_sample_time
548564
549- # Close all open markers for each thread
550- for tid in list (self .has_gil_start .keys ()):
551- start_time = self .has_gil_start [tid ]
552- self ._add_marker (tid , "Has GIL" , start_time , end_time , CATEGORY_GIL )
553- del self .has_gil_start [tid ]
554-
555- for tid in list (self .no_gil_start .keys ()):
556- start_time = self .no_gil_start [tid ]
557- self ._add_marker (tid , "No GIL" , start_time , end_time , CATEGORY_GIL )
558- del self .no_gil_start [tid ]
559-
560- for tid in list (self .on_cpu_start .keys ()):
561- start_time = self .on_cpu_start [tid ]
562- self ._add_marker (tid , "On CPU" , start_time , end_time , CATEGORY_CPU )
563- del self .on_cpu_start [tid ]
564-
565- for tid in list (self .off_cpu_start .keys ()):
566- start_time = self .off_cpu_start [tid ]
567- self ._add_marker (tid , "Off CPU" , start_time , end_time , CATEGORY_CPU )
568- del self .off_cpu_start [tid ]
569-
570- for tid in list (self .python_code_start .keys ()):
571- start_time = self .python_code_start [tid ]
572- self ._add_marker (tid , "Python Code" , start_time , end_time , CATEGORY_CODE_TYPE )
573- del self .python_code_start [tid ]
574-
575- for tid in list (self .native_code_start .keys ()):
576- start_time = self .native_code_start [tid ]
577- self ._add_marker (tid , "Native Code" , start_time , end_time , CATEGORY_CODE_TYPE )
578- del self .native_code_start [tid ]
579-
580- for tid in list (self .gil_wait_start .keys ()):
581- start_time = self .gil_wait_start [tid ]
582- self ._add_marker (tid , "Waiting for GIL" , start_time , end_time , CATEGORY_GIL )
583- del self .gil_wait_start [tid ]
565+ # Close all open markers for each thread using a generic approach
566+ marker_states = [
567+ (self .has_gil_start , "Has GIL" , CATEGORY_GIL ),
568+ (self .no_gil_start , "No GIL" , CATEGORY_GIL ),
569+ (self .on_cpu_start , "On CPU" , CATEGORY_CPU ),
570+ (self .off_cpu_start , "Off CPU" , CATEGORY_CPU ),
571+ (self .python_code_start , "Python Code" , CATEGORY_CODE_TYPE ),
572+ (self .native_code_start , "Native Code" , CATEGORY_CODE_TYPE ),
573+ (self .gil_wait_start , "Waiting for GIL" , CATEGORY_GIL ),
574+ ]
575+
576+ for state_dict , marker_name , category in marker_states :
577+ for tid in list (state_dict .keys ()):
578+ self ._add_marker (tid , marker_name , state_dict [tid ], end_time , category )
579+ del state_dict [tid ]
584580
585581 # Close any open GC marker
586582 if self .potential_gc_start is not None :
0 commit comments