@@ -141,27 +141,35 @@ def _track_state_transition(self, tid, condition, active_dict, inactive_dict,
141141 self ._add_marker (tid , active_name , active_dict .pop (tid ),
142142 current_time , category )
143143
144- def collect (self , stack_frames , timestamp_us = None ):
145- """Collect a sample from stack frames."""
146- if timestamp_us is not None :
147- # Use provided timestamp (from binary replay)
148- # Track first timestamp as base for relative time calculation
149- if self ._replay_base_timestamp_us is None :
150- self ._replay_base_timestamp_us = timestamp_us
151- # Convert to milliseconds relative to first sample
152- current_time = (timestamp_us - self ._replay_base_timestamp_us ) / 1000
153- else :
154- # Live sampling - use monotonic clock
144+ def collect (self , stack_frames , timestamps_us = None ):
145+ """Collect samples from stack frames.
146+
147+ Args:
148+ stack_frames: List of interpreter/thread frame info
149+ timestamps_us: List of timestamps in microseconds (None for live sampling)
150+ """
151+ # Handle live sampling (no timestamps provided)
152+ if timestamps_us is None :
155153 current_time = (time .monotonic () * 1000 ) - self .start_time
154+ times = [current_time ]
155+ else :
156+ if not timestamps_us :
157+ return
158+ # Initialize base timestamp if needed
159+ if self ._replay_base_timestamp_us is None :
160+ self ._replay_base_timestamp_us = timestamps_us [0 ]
161+ # Convert all timestamps to times (ms relative to first sample)
162+ base = self ._replay_base_timestamp_us
163+ times = [(ts - base ) / 1000 for ts in timestamps_us ]
164+
165+ first_time = times [0 ]
156166
157167 # Update interval calculation
158168 if self .sample_count > 0 and self .last_sample_time > 0 :
159- self .interval = (
160- current_time - self .last_sample_time
161- ) / self .sample_count
162- self .last_sample_time = current_time
169+ self .interval = (times [- 1 ] - self .last_sample_time ) / self .sample_count
170+ self .last_sample_time = times [- 1 ]
163171
164- # Process threads and track GC per thread
172+ # Process threads
165173 for interpreter_info in stack_frames :
166174 for thread_info in interpreter_info .threads :
167175 frames = thread_info .frame_info
@@ -179,92 +187,86 @@ def collect(self, stack_frames, timestamp_us=None):
179187 on_cpu = bool (status_flags & THREAD_STATUS_ON_CPU )
180188 gil_requested = bool (status_flags & THREAD_STATUS_GIL_REQUESTED )
181189
182- # Track GIL possession (Has GIL / No GIL)
190+ # Track state transitions using first timestamp
183191 self ._track_state_transition (
184192 tid , has_gil , self .has_gil_start , self .no_gil_start ,
185- "Has GIL" , "No GIL" , CATEGORY_GIL , current_time
193+ "Has GIL" , "No GIL" , CATEGORY_GIL , first_time
186194 )
187-
188- # Track CPU state (On CPU / Off CPU)
189195 self ._track_state_transition (
190196 tid , on_cpu , self .on_cpu_start , self .off_cpu_start ,
191- "On CPU" , "Off CPU" , CATEGORY_CPU , current_time
197+ "On CPU" , "Off CPU" , CATEGORY_CPU , first_time
192198 )
193199
194- # Track code type (Python Code / Native Code)
195- # This is tri-state: Python (has_gil), Native (on_cpu without gil), or Neither
200+ # Track code type
196201 if has_gil :
197202 self ._track_state_transition (
198203 tid , True , self .python_code_start , self .native_code_start ,
199- "Python Code" , "Native Code" , CATEGORY_CODE_TYPE , current_time
204+ "Python Code" , "Native Code" , CATEGORY_CODE_TYPE , first_time
200205 )
201206 elif on_cpu :
202207 self ._track_state_transition (
203208 tid , True , self .native_code_start , self .python_code_start ,
204- "Native Code" , "Python Code" , CATEGORY_CODE_TYPE , current_time
209+ "Native Code" , "Python Code" , CATEGORY_CODE_TYPE , first_time
205210 )
206211 else :
207- # Thread is idle (neither has GIL nor on CPU) - close any open code markers
208- # This handles the third state that _track_state_transition doesn't cover
209212 if tid in self .initialized_threads :
210213 if tid in self .python_code_start :
211214 self ._add_marker (tid , "Python Code" , self .python_code_start .pop (tid ),
212- current_time , CATEGORY_CODE_TYPE )
215+ first_time , CATEGORY_CODE_TYPE )
213216 if tid in self .native_code_start :
214217 self ._add_marker (tid , "Native Code" , self .native_code_start .pop (tid ),
215- current_time , CATEGORY_CODE_TYPE )
218+ first_time , CATEGORY_CODE_TYPE )
216219
217- # Track "Waiting for GIL" intervals (one-sided tracking)
220+ # Track GIL wait
218221 if gil_requested :
219- self .gil_wait_start .setdefault (tid , current_time )
222+ self .gil_wait_start .setdefault (tid , first_time )
220223 elif tid in self .gil_wait_start :
221224 self ._add_marker (tid , "Waiting for GIL" , self .gil_wait_start .pop (tid ),
222- current_time , CATEGORY_GIL )
225+ first_time , CATEGORY_GIL )
223226
224- # Track exception state (Has Exception / No Exception)
227+ # Track exception state
225228 has_exception = bool (status_flags & THREAD_STATUS_HAS_EXCEPTION )
226229 self ._track_state_transition (
227230 tid , has_exception , self .exception_start , self .no_exception_start ,
228- "Has Exception" , "No Exception" , CATEGORY_EXCEPTION , current_time
231+ "Has Exception" , "No Exception" , CATEGORY_EXCEPTION , first_time
229232 )
230233
231- # Track GC events by detecting <GC> frames in the stack trace
232- # This leverages the improved GC frame tracking from commit 336366fd7ca
233- # which precisely identifies the thread that initiated GC collection
234+ # Track GC events
234235 has_gc_frame = any (frame [2 ] == "<GC>" for frame in frames )
235236 if has_gc_frame :
236- # This thread initiated GC collection
237237 if tid not in self .gc_start_per_thread :
238- self .gc_start_per_thread [tid ] = current_time
238+ self .gc_start_per_thread [tid ] = first_time
239239 elif tid in self .gc_start_per_thread :
240- # End GC marker when no more GC frames are detected
241240 self ._add_marker (tid , "GC Collecting" , self .gc_start_per_thread .pop (tid ),
242- current_time , CATEGORY_GC )
241+ first_time , CATEGORY_GC )
243242
244- # Mark thread as initialized after processing all state transitions
243+ # Mark thread as initialized
245244 self .initialized_threads .add (tid )
246245
247- # Categorize: idle if neither has GIL nor on CPU
246+ # Skip idle threads if requested
248247 is_idle = not has_gil and not on_cpu
249-
250- # Skip idle threads if skip_idle is enabled
251248 if self .skip_idle and is_idle :
252249 continue
253250
254251 if not frames :
255252 continue
256253
257- # Process the stack
254+ # Process stack once to get stack_index
258255 stack_index = self ._process_stack (thread_data , frames )
259256
260- # Add sample - cache references to avoid dictionary lookups
257+ # Add samples with timestamps
261258 samples = thread_data ["samples" ]
262- samples ["stack" ].append (stack_index )
263- samples ["time" ].append (current_time )
264- samples ["eventDelay" ].append (None )
259+ samples_stack = samples ["stack" ]
260+ samples_time = samples ["time" ]
261+ samples_delay = samples ["eventDelay" ]
262+
263+ for t in times :
264+ samples_stack .append (stack_index )
265+ samples_time .append (t )
266+ samples_delay .append (None )
265267
266- # Track opcode state changes for interval markers (leaf frame only)
267- if self .opcodes_enabled :
268+ # Handle opcodes
269+ if self .opcodes_enabled and frames :
268270 leaf_frame = frames [0 ]
269271 filename , location , funcname , opcode = leaf_frame
270272 if isinstance (location , tuple ):
@@ -276,18 +278,15 @@ def collect(self, stack_frames, timestamp_us=None):
276278 current_state = (opcode , lineno , col_offset , funcname , filename )
277279
278280 if tid not in self .opcode_state :
279- # First observation - start tracking
280- self .opcode_state [tid ] = (* current_state , current_time )
281+ self .opcode_state [tid ] = (* current_state , first_time )
281282 elif self .opcode_state [tid ][:5 ] != current_state :
282- # State changed - emit marker for previous state
283283 prev_opcode , prev_lineno , prev_col , prev_funcname , prev_filename , prev_start = self .opcode_state [tid ]
284284 self ._add_opcode_interval_marker (
285- tid , prev_opcode , prev_lineno , prev_col , prev_funcname , prev_start , current_time
285+ tid , prev_opcode , prev_lineno , prev_col , prev_funcname , prev_start , first_time
286286 )
287- # Start tracking new state
288- self .opcode_state [tid ] = (* current_state , current_time )
287+ self .opcode_state [tid ] = (* current_state , first_time )
289288
290- self .sample_count += 1
289+ self .sample_count += len ( times )
291290
292291 def _create_thread (self , tid ):
293292 """Create a new thread structure with processed profile format."""
0 commit comments