3232 from typing import Dict
3333 from typing import List
3434 from typing import Optional
35+ from typing import Set
3536 from typing import Type
3637 from typing import Union
3738 from typing_extensions import TypedDict
@@ -134,15 +135,15 @@ def try_autostart_continuous_profiler():
134135 _scheduler .manual_start ()
135136
136137
137- def try_profile_lifecycle_auto_start ():
138- # type: () -> bool
138+ def try_profile_lifecycle_trace_start ():
139+ # type: () -> Union[ContinuousProfile, None]
139140 if _scheduler is None :
140- return False
141+ return None
141142
142143 return _scheduler .auto_start ()
143144
144145
145- def try_profile_lifecycle_auto_stop ():
146+ def try_profile_lifecycle_trace_stop ():
146147 # type: () -> None
147148 if _scheduler is None :
148149 return
@@ -191,6 +192,14 @@ def determine_profile_session_sampling_decision(sample_rate):
191192 return random .random () < float (sample_rate )
192193
193194
195+ class ContinuousProfile :
196+ active : bool = True
197+
198+ def stop (self ):
199+ # type: () -> None
200+ self .active = False
201+
202+
194203class ContinuousScheduler :
195204 mode = "unknown" # type: ContinuousProfilerMode
196205
@@ -213,9 +222,8 @@ def __init__(self, frequency, options, sdk_info, capture_func):
213222
214223 self .running = False
215224
216- self .active_spans = 0
217- self .started_spans = deque (maxlen = 128 ) # type: Deque[None]
218- self .finished_spans = deque (maxlen = 128 ) # type: Deque[None]
225+ self .new_profiles = deque (maxlen = 128 ) # type: Deque[ContinuousProfile]
226+ self .active_profiles = set () # type: Set[ContinuousProfile]
219227
220228 def is_auto_start_enabled (self ):
221229 # type: () -> bool
@@ -235,28 +243,21 @@ def is_auto_start_enabled(self):
235243 return experiments .get ("continuous_profiling_auto_start" )
236244
237245 def auto_start (self ):
238- # type: () -> bool
246+ # type: () -> Union[ContinuousProfile, None]
239247 if not self .sampled :
240- return False
248+ return None
241249
242- if self .lifecycle != "auto " :
243- return False
250+ if self .lifecycle != "trace " :
251+ return None
244252
245253 logger .debug ("[Profiling] Auto starting profiler" )
246254
247- self .started_spans .append (None )
248- self .ensure_running ()
249-
250- return True
255+ profile = ContinuousProfile ()
251256
252- def auto_stop (self ):
253- # type: () -> None
254- if self .lifecycle != "auto" :
255- return
256-
257- logger .debug ("[Profiling] Auto stopping profiler" )
257+ self .new_profiles .append (profile )
258+ self .ensure_running ()
258259
259- self . finished_spans . append ( None )
260+ return profile
260261
261262 def manual_start (self ):
262263 # type: () -> None
@@ -306,7 +307,7 @@ def make_sampler(self):
306307
307308 cache = LRUCache (max_size = 256 )
308309
309- if self .lifecycle == "auto " :
310+ if self .lifecycle == "trace " :
310311
311312 def _sample_stack (* args , ** kwargs ):
312313 # type: (*Any, **Any) -> None
@@ -315,16 +316,20 @@ def _sample_stack(*args, **kwargs):
315316 This should be called at a regular interval to collect samples.
316317 """
317318
318- if (
319- not self .active_spans
320- and not self .started_spans
321- and not self .finished_spans
322- ):
319+ # no profiles taking place, so we can stop early
320+ if not self .new_profiles and not self .active_profiles :
323321 self .running = False
324322 return
325323
326- started_spans = len (self .started_spans )
327- finished_spans = len (self .finished_spans )
324+ # This is the number of profiles we want to pop off.
325+ # It's possible another thread adds a new profile to
326+ # the list and we spend longer than we want inside
327+ # the loop below.
328+ #
329+ # Also make sure to set this value before extracting
330+ # frames so we do not write to any new profiles that
331+ # were started after this point.
332+ new_profiles = len (self .new_profiles )
328333
329334 ts = now ()
330335
@@ -339,13 +344,32 @@ def _sample_stack(*args, **kwargs):
339344 capture_internal_exception (sys .exc_info ())
340345 return
341346
342- for _ in range (started_spans ):
343- self .started_spans .popleft ()
344-
345- for _ in range (finished_spans ):
346- self .finished_spans .popleft ()
347-
348- self .active_spans = self .active_spans + started_spans - finished_spans
347+ # Move the new profiles into the active_profiles set.
348+ #
349+ # We cannot directly add the to active_profiles set
350+ # in `start_profiling` because it is called from other
351+ # threads which can cause a RuntimeError when it the
352+ # set sizes changes during iteration without a lock.
353+ #
354+ # We also want to avoid using a lock here so threads
355+ # that are starting profiles are not blocked until it
356+ # can acquire the lock.
357+ for _ in range (new_profiles ):
358+ self .active_profiles .add (self .new_profiles .popleft ())
359+ inactive_profiles = []
360+
361+ for profile in self .active_profiles :
362+ if profile .active :
363+ pass
364+ else :
365+ # If a profile is marked inactive, we buffer it
366+ # to `inactive_profiles` so it can be removed.
367+ # We cannot remove it here as it would result
368+ # in a RuntimeError.
369+ inactive_profiles .append (profile )
370+
371+ for profile in inactive_profiles :
372+ self .active_profiles .remove (profile )
349373
350374 if self .buffer is None :
351375 self .reset_buffer ()
0 commit comments