5
5
import threading
6
6
import time
7
7
import uuid
8
+ from collections import deque
8
9
from datetime import datetime , timezone
9
10
10
11
from sentry_sdk .consts import VERSION
27
28
if TYPE_CHECKING :
28
29
from typing import Any
29
30
from typing import Callable
31
+ from typing import Deque
30
32
from typing import Dict
31
33
from typing import List
32
34
from typing import Optional
35
+ from typing import Set
33
36
from typing import Type
34
37
from typing import Union
35
38
from typing_extensions import TypedDict
@@ -120,6 +123,9 @@ def setup_continuous_profiler(options, sdk_info, capture_func):
120
123
121
124
def try_autostart_continuous_profiler ():
122
125
# type: () -> None
126
+
127
+ # TODO: deprecate this as it'll be replaced by the auto lifecycle option
128
+
123
129
if _scheduler is None :
124
130
return
125
131
@@ -129,6 +135,14 @@ def try_autostart_continuous_profiler():
129
135
_scheduler .manual_start ()
130
136
131
137
138
+ def try_profile_lifecycle_trace_start ():
139
+ # type: () -> Union[ContinuousProfile, None]
140
+ if _scheduler is None :
141
+ return None
142
+
143
+ return _scheduler .auto_start ()
144
+
145
+
132
146
def start_profiler ():
133
147
# type: () -> None
134
148
if _scheduler is None :
@@ -170,6 +184,14 @@ def determine_profile_session_sampling_decision(sample_rate):
170
184
return random .random () < float (sample_rate )
171
185
172
186
187
+ class ContinuousProfile :
188
+ active : bool = True
189
+
190
+ def stop (self ):
191
+ # type: () -> None
192
+ self .active = False
193
+
194
+
173
195
class ContinuousScheduler :
174
196
mode = "unknown" # type: ContinuousProfilerMode
175
197
@@ -179,16 +201,21 @@ def __init__(self, frequency, options, sdk_info, capture_func):
179
201
self .options = options
180
202
self .sdk_info = sdk_info
181
203
self .capture_func = capture_func
204
+
205
+ self .lifecycle = self .options .get ("profile_lifecycle" )
206
+ profile_session_sample_rate = self .options .get ("profile_session_sample_rate" )
207
+ self .sampled = determine_profile_session_sampling_decision (
208
+ profile_session_sample_rate
209
+ )
210
+
182
211
self .sampler = self .make_sampler ()
183
212
self .buffer = None # type: Optional[ProfileBuffer]
184
213
self .pid = None # type: Optional[int]
185
214
186
215
self .running = False
187
216
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
- )
217
+ self .new_profiles = deque (maxlen = 128 ) # type: Deque[ContinuousProfile]
218
+ self .active_profiles = set () # type: Set[ContinuousProfile]
192
219
193
220
def is_auto_start_enabled (self ):
194
221
# type: () -> bool
@@ -207,15 +234,38 @@ def is_auto_start_enabled(self):
207
234
208
235
return experiments .get ("continuous_profiling_auto_start" )
209
236
237
+ def auto_start (self ):
238
+ # type: () -> Union[ContinuousProfile, None]
239
+ if not self .sampled :
240
+ return None
241
+
242
+ if self .lifecycle != "trace" :
243
+ return None
244
+
245
+ logger .debug ("[Profiling] Auto starting profiler" )
246
+
247
+ profile = ContinuousProfile ()
248
+
249
+ self .new_profiles .append (profile )
250
+ self .ensure_running ()
251
+
252
+ return profile
253
+
210
254
def manual_start (self ):
211
255
# type: () -> None
212
256
if not self .sampled :
213
257
return
214
258
259
+ if self .lifecycle != "manual" :
260
+ return
261
+
215
262
self .ensure_running ()
216
263
217
264
def manual_stop (self ):
218
265
# type: () -> None
266
+ if self .lifecycle != "manual" :
267
+ return
268
+
219
269
self .teardown ()
220
270
221
271
def ensure_running (self ):
@@ -249,28 +299,97 @@ def make_sampler(self):
249
299
250
300
cache = LRUCache (max_size = 256 )
251
301
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 )
302
+ if self .lifecycle == "trace" :
303
+
304
+ def _sample_stack (* args , ** kwargs ):
305
+ # type: (*Any, **Any) -> None
306
+ """
307
+ Take a sample of the stack on all the threads in the process.
308
+ This should be called at a regular interval to collect samples.
309
+ """
310
+
311
+ # no profiles taking place, so we can stop early
312
+ if not self .new_profiles and not self .active_profiles :
313
+ self .running = False
314
+ return
315
+
316
+ # This is the number of profiles we want to pop off.
317
+ # It's possible another thread adds a new profile to
318
+ # the list and we spend longer than we want inside
319
+ # the loop below.
320
+ #
321
+ # Also make sure to set this value before extracting
322
+ # frames so we do not write to any new profiles that
323
+ # were started after this point.
324
+ new_profiles = len (self .new_profiles )
325
+
326
+ ts = now ()
327
+
328
+ try :
329
+ sample = [
330
+ (str (tid ), extract_stack (frame , cache , cwd ))
331
+ for tid , frame in sys ._current_frames ().items ()
332
+ ]
333
+ except AttributeError :
334
+ # For some reason, the frame we get doesn't have certain attributes.
335
+ # When this happens, we abandon the current sample as it's bad.
336
+ capture_internal_exception (sys .exc_info ())
337
+ return
338
+
339
+ # Move the new profiles into the active_profiles set.
340
+ #
341
+ # We cannot directly add the to active_profiles set
342
+ # in `start_profiling` because it is called from other
343
+ # threads which can cause a RuntimeError when it the
344
+ # set sizes changes during iteration without a lock.
345
+ #
346
+ # We also want to avoid using a lock here so threads
347
+ # that are starting profiles are not blocked until it
348
+ # can acquire the lock.
349
+ for _ in range (new_profiles ):
350
+ self .active_profiles .add (self .new_profiles .popleft ())
351
+ inactive_profiles = []
352
+
353
+ for profile in self .active_profiles :
354
+ if profile .active :
355
+ pass
356
+ else :
357
+ # If a profile is marked inactive, we buffer it
358
+ # to `inactive_profiles` so it can be removed.
359
+ # We cannot remove it here as it would result
360
+ # in a RuntimeError.
361
+ inactive_profiles .append (profile )
362
+
363
+ for profile in inactive_profiles :
364
+ self .active_profiles .remove (profile )
365
+
366
+ if self .buffer is not None :
367
+ self .buffer .write (ts , sample )
368
+
369
+ else :
370
+
371
+ def _sample_stack (* args , ** kwargs ):
372
+ # type: (*Any, **Any) -> None
373
+ """
374
+ Take a sample of the stack on all the threads in the process.
375
+ This should be called at a regular interval to collect samples.
376
+ """
377
+
378
+ ts = now ()
379
+
380
+ try :
381
+ sample = [
382
+ (str (tid ), extract_stack (frame , cache , cwd ))
383
+ for tid , frame in sys ._current_frames ().items ()
384
+ ]
385
+ except AttributeError :
386
+ # For some reason, the frame we get doesn't have certain attributes.
387
+ # When this happens, we abandon the current sample as it's bad.
388
+ capture_internal_exception (sys .exc_info ())
389
+ return
390
+
391
+ if self .buffer is not None :
392
+ self .buffer .write (ts , sample )
274
393
275
394
return _sample_stack
276
395
@@ -294,6 +413,7 @@ def run(self):
294
413
295
414
if self .buffer is not None :
296
415
self .buffer .flush ()
416
+ self .buffer = None
297
417
298
418
299
419
class ThreadContinuousScheduler (ContinuousScheduler ):
0 commit comments