55import threading
66import time
77import uuid
8+ from collections import deque
89from datetime import datetime , timezone
910
1011from sentry_sdk .consts import VERSION
2728if TYPE_CHECKING :
2829 from typing import Any
2930 from typing import Callable
31+ from typing import Deque
3032 from typing import Dict
3133 from typing import List
3234 from typing import Optional
@@ -120,6 +122,9 @@ def setup_continuous_profiler(options, sdk_info, capture_func):
120122
121123def try_autostart_continuous_profiler ():
122124 # type: () -> None
125+
126+ # TODO: deprecate this as it'll be replaced by the auto lifecycle option
127+
123128 if _scheduler is None :
124129 return
125130
@@ -129,6 +134,22 @@ def try_autostart_continuous_profiler():
129134 _scheduler .manual_start ()
130135
131136
137+ def try_profile_lifecycle_auto_start ():
138+ # type: () -> bool
139+ if _scheduler is None :
140+ return False
141+
142+ return _scheduler .auto_start ()
143+
144+
145+ def try_profile_lifecycle_auto_stop ():
146+ # type: () -> None
147+ if _scheduler is None :
148+ return
149+
150+ _scheduler .auto_stop ()
151+
152+
132153def start_profiler ():
133154 # type: () -> None
134155 if _scheduler is None :
@@ -179,16 +200,22 @@ def __init__(self, frequency, options, sdk_info, capture_func):
179200 self .options = options
180201 self .sdk_info = sdk_info
181202 self .capture_func = capture_func
203+
204+ self .lifecycle = self .options .get ("profile_lifecycle" )
205+ profile_session_sample_rate = self .options .get ("profile_session_sample_rate" )
206+ self .sampled = determine_profile_session_sampling_decision (
207+ profile_session_sample_rate
208+ )
209+
182210 self .sampler = self .make_sampler ()
183211 self .buffer = None # type: Optional[ProfileBuffer]
184212 self .pid = None # type: Optional[int]
185213
186214 self .running = False
187215
188- profile_session_sample_rate = self .options .get ("profile_session_sample_rate" )
189- self .sampled = determine_profile_session_sampling_decision (
190- profile_session_sample_rate
191- )
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]
192219
193220 def is_auto_start_enabled (self ):
194221 # type: () -> bool
@@ -207,15 +234,45 @@ def is_auto_start_enabled(self):
207234
208235 return experiments .get ("continuous_profiling_auto_start" )
209236
237+ def auto_start (self ):
238+ # type: () -> bool
239+ if not self .sampled :
240+ return False
241+
242+ if self .lifecycle != "auto" :
243+ return False
244+
245+ logger .debug ("[Profiling] Auto starting profiler" )
246+
247+ self .started_spans .append (None )
248+ self .ensure_running ()
249+
250+ return True
251+
252+ def auto_stop (self ):
253+ # type: () -> None
254+ if self .lifecycle != "auto" :
255+ return
256+
257+ logger .debug ("[Profiling] Auto stopping profiler" )
258+
259+ self .finished_spans .append (None )
260+
210261 def manual_start (self ):
211262 # type: () -> None
212263 if not self .sampled :
213264 return
214265
266+ if self .lifecycle != "manual" :
267+ return
268+
215269 self .ensure_running ()
216270
217271 def manual_stop (self ):
218272 # type: () -> None
273+ if self .lifecycle != "manual" :
274+ return
275+
219276 self .teardown ()
220277
221278 def ensure_running (self ):
@@ -249,28 +306,77 @@ def make_sampler(self):
249306
250307 cache = LRUCache (max_size = 256 )
251308
252- def _sample_stack (* args , ** kwargs ):
253- # type: (*Any, **Any) -> None
254- """
255- Take a sample of the stack on all the threads in the process.
256- This should be called at a regular interval to collect samples.
257- """
258-
259- ts = now ()
260-
261- try :
262- sample = [
263- (str (tid ), extract_stack (frame , cache , cwd ))
264- for tid , frame in sys ._current_frames ().items ()
265- ]
266- except AttributeError :
267- # For some reason, the frame we get doesn't have certain attributes.
268- # When this happens, we abandon the current sample as it's bad.
269- capture_internal_exception (sys .exc_info ())
270- return
271-
272- if self .buffer is not None :
273- self .buffer .write (ts , sample )
309+ if self .lifecycle == "auto" :
310+
311+ def _sample_stack (* args , ** kwargs ):
312+ # type: (*Any, **Any) -> None
313+ """
314+ Take a sample of the stack on all the threads in the process.
315+ This should be called at a regular interval to collect samples.
316+ """
317+
318+ if (
319+ not self .active_spans
320+ and not self .started_spans
321+ and not self .finished_spans
322+ ):
323+ self .running = False
324+ return
325+
326+ started_spans = len (self .started_spans )
327+ finished_spans = len (self .finished_spans )
328+
329+ ts = now ()
330+
331+ try :
332+ sample = [
333+ (str (tid ), extract_stack (frame , cache , cwd ))
334+ for tid , frame in sys ._current_frames ().items ()
335+ ]
336+ except AttributeError :
337+ # For some reason, the frame we get doesn't have certain attributes.
338+ # When this happens, we abandon the current sample as it's bad.
339+ capture_internal_exception (sys .exc_info ())
340+ return
341+
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
349+
350+ if self .buffer is None :
351+ self .reset_buffer ()
352+
353+ if self .buffer is not None :
354+ self .buffer .write (ts , sample )
355+
356+ else :
357+
358+ def _sample_stack (* args , ** kwargs ):
359+ # type: (*Any, **Any) -> None
360+ """
361+ Take a sample of the stack on all the threads in the process.
362+ This should be called at a regular interval to collect samples.
363+ """
364+
365+ ts = now ()
366+
367+ try :
368+ sample = [
369+ (str (tid ), extract_stack (frame , cache , cwd ))
370+ for tid , frame in sys ._current_frames ().items ()
371+ ]
372+ except AttributeError :
373+ # For some reason, the frame we get doesn't have certain attributes.
374+ # When this happens, we abandon the current sample as it's bad.
375+ capture_internal_exception (sys .exc_info ())
376+ return
377+
378+ if self .buffer is not None :
379+ self .buffer .write (ts , sample )
274380
275381 return _sample_stack
276382
@@ -294,6 +400,7 @@ def run(self):
294400
295401 if self .buffer is not None :
296402 self .buffer .flush ()
403+ self .buffer = None
297404
298405
299406class ThreadContinuousScheduler (ContinuousScheduler ):
0 commit comments