Skip to content

Commit 6d622b2

Browse files
committed
Merge remote-tracking branch 'origin/master' into potel-base
2 parents 43348e9 + d39599f commit 6d622b2

File tree

4 files changed

+56
-15
lines changed

4 files changed

+56
-15
lines changed

scripts/populate_tox/tox.jinja

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,7 @@ deps =
297297
# Sanic
298298
sanic: websockets<11.0
299299
sanic: aiohttp
300+
{py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-sanic: tracerite<1.1.2
300301
sanic-v{24.6}: sanic_testing
301302
sanic-latest: sanic_testing
302303
sanic-v0.8: sanic~=0.8.0

sentry_sdk/profiler/continuous_profiler.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ def __init__(self, frequency, options, sdk_info, capture_func):
207207
self.pid = None # type: Optional[int]
208208

209209
self.running = False
210+
self.soft_shutdown = False
210211

211212
self.new_profiles = deque(maxlen=128) # type: Deque[ContinuousProfile]
212213
self.active_profiles = set() # type: Set[ContinuousProfile]
@@ -288,24 +289,23 @@ def profiler_id(self):
288289
return self.buffer.profiler_id
289290

290291
def make_sampler(self):
291-
# type: () -> Callable[..., None]
292+
# type: () -> Callable[..., bool]
292293
cwd = os.getcwd()
293294

294295
cache = LRUCache(max_size=256)
295296

296297
if self.lifecycle == "trace":
297298

298299
def _sample_stack(*args, **kwargs):
299-
# type: (*Any, **Any) -> None
300+
# type: (*Any, **Any) -> bool
300301
"""
301302
Take a sample of the stack on all the threads in the process.
302303
This should be called at a regular interval to collect samples.
303304
"""
304305

305306
# no profiles taking place, so we can stop early
306307
if not self.new_profiles and not self.active_profiles:
307-
self.running = False
308-
return
308+
return True
309309

310310
# This is the number of profiles we want to pop off.
311311
# It's possible another thread adds a new profile to
@@ -328,7 +328,7 @@ def _sample_stack(*args, **kwargs):
328328
# For some reason, the frame we get doesn't have certain attributes.
329329
# When this happens, we abandon the current sample as it's bad.
330330
capture_internal_exception(sys.exc_info())
331-
return
331+
return False
332332

333333
# Move the new profiles into the active_profiles set.
334334
#
@@ -345,9 +345,7 @@ def _sample_stack(*args, **kwargs):
345345
inactive_profiles = []
346346

347347
for profile in self.active_profiles:
348-
if profile.active:
349-
pass
350-
else:
348+
if not profile.active:
351349
# If a profile is marked inactive, we buffer it
352350
# to `inactive_profiles` so it can be removed.
353351
# We cannot remove it here as it would result
@@ -360,10 +358,12 @@ def _sample_stack(*args, **kwargs):
360358
if self.buffer is not None:
361359
self.buffer.write(ts, sample)
362360

361+
return False
362+
363363
else:
364364

365365
def _sample_stack(*args, **kwargs):
366-
# type: (*Any, **Any) -> None
366+
# type: (*Any, **Any) -> bool
367367
"""
368368
Take a sample of the stack on all the threads in the process.
369369
This should be called at a regular interval to collect samples.
@@ -380,19 +380,21 @@ def _sample_stack(*args, **kwargs):
380380
# For some reason, the frame we get doesn't have certain attributes.
381381
# When this happens, we abandon the current sample as it's bad.
382382
capture_internal_exception(sys.exc_info())
383-
return
383+
return False
384384

385385
if self.buffer is not None:
386386
self.buffer.write(ts, sample)
387387

388+
return False
389+
388390
return _sample_stack
389391

390392
def run(self):
391393
# type: () -> None
392394
last = time.perf_counter()
393395

394396
while self.running:
395-
self.sampler()
397+
self.soft_shutdown = self.sampler()
396398

397399
# some time may have elapsed since the last time
398400
# we sampled, so we need to account for that and
@@ -401,6 +403,15 @@ def run(self):
401403
if elapsed < self.interval:
402404
thread_sleep(self.interval - elapsed)
403405

406+
# the soft shutdown happens here to give it a chance
407+
# for the profiler to be reused
408+
if self.soft_shutdown:
409+
self.running = False
410+
411+
# make sure to explicitly exit the profiler here or there might
412+
# be multiple profilers at once
413+
break
414+
404415
# after sleeping, make sure to take the current
405416
# timestamp so we can use it next iteration
406417
last = time.perf_counter()
@@ -429,6 +440,8 @@ def __init__(self, frequency, options, sdk_info, capture_func):
429440
def ensure_running(self):
430441
# type: () -> None
431442

443+
self.soft_shutdown = False
444+
432445
pid = os.getpid()
433446

434447
# is running on the right process
@@ -503,6 +516,9 @@ def __init__(self, frequency, options, sdk_info, capture_func):
503516

504517
def ensure_running(self):
505518
# type: () -> None
519+
520+
self.soft_shutdown = False
521+
506522
pid = os.getpid()
507523

508524
# is running on the right process

tests/profiler/test_continuous_profiler.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -390,33 +390,56 @@ def test_continuous_profiler_auto_start_and_stop_sampled(
390390

391391
thread = threading.current_thread()
392392

393+
all_profiler_ids = set()
394+
393395
for _ in range(3):
394396
envelopes.clear()
395397

398+
profiler_ids = set()
399+
396400
with sentry_sdk.start_span(name="profiling 1"):
397401
assert get_profiler_id() is not None, "profiler should be running"
402+
profiler_id = get_profiler_id()
403+
assert profiler_id is not None, "profiler should be running"
404+
profiler_ids.add(profiler_id)
405+
398406
with sentry_sdk.start_span(op="op"):
399407
time.sleep(0.1)
400-
assert get_profiler_id() is not None, "profiler should be running"
408+
profiler_id = get_profiler_id()
409+
assert profiler_id is not None, "profiler should be running"
410+
profiler_ids.add(profiler_id)
411+
412+
time.sleep(0.03)
401413

402414
# the profiler takes a while to stop in auto mode so if we start
403415
# a transaction immediately, it'll be part of the same chunk
404-
assert get_profiler_id() is not None, "profiler should be running"
416+
profiler_id = get_profiler_id()
417+
assert profiler_id is not None, "profiler should be running"
418+
profiler_ids.add(profiler_id)
405419

406420
with sentry_sdk.start_span(name="profiling 2"):
407-
assert get_profiler_id() is not None, "profiler should be running"
421+
profiler_id = get_profiler_id()
422+
assert profiler_id is not None, "profiler should be running"
423+
profiler_ids.add(profiler_id)
408424
with sentry_sdk.start_span(op="op"):
409425
time.sleep(0.1)
410-
assert get_profiler_id() is not None, "profiler should be running"
426+
profiler_id = get_profiler_id()
427+
assert profiler_id is not None, "profiler should be running"
428+
profiler_ids.add(profiler_id)
411429

412430
# wait at least 1 cycle for the profiler to stop
413431
time.sleep(0.2)
414432
assert get_profiler_id() is None, "profiler should not be running"
415433

434+
assert len(profiler_ids) == 1
435+
all_profiler_ids.add(profiler_ids.pop())
436+
416437
assert_single_transaction_with_profile_chunks(
417438
envelopes, thread, max_chunks=1, transactions=2
418439
)
419440

441+
assert len(all_profiler_ids) == 3
442+
420443

421444
@pytest.mark.parametrize(
422445
"mode",

tox.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@ deps =
461461
# Sanic
462462
sanic: websockets<11.0
463463
sanic: aiohttp
464+
{py3.7,py3.8,py3.9,py3.10,py3.11,py3.12,py3.13}-sanic: tracerite<1.1.2
464465
sanic-v{24.6}: sanic_testing
465466
sanic-latest: sanic_testing
466467
sanic-v0.8: sanic~=0.8.0

0 commit comments

Comments
 (0)