Skip to content

fix(profiling): Ensure profiler thread exits when needed #4497

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 27 additions & 11 deletions sentry_sdk/profiler/continuous_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ def __init__(self, frequency, options, sdk_info, capture_func):
self.pid = None # type: Optional[int]

self.running = False
self.soft_shutdown = False

self.new_profiles = deque(maxlen=128) # type: Deque[ContinuousProfile]
self.active_profiles = set() # type: Set[ContinuousProfile]
Expand Down Expand Up @@ -317,24 +318,23 @@ def profiler_id(self):
return self.buffer.profiler_id

def make_sampler(self):
# type: () -> Callable[..., None]
# type: () -> Callable[..., bool]
cwd = os.getcwd()

cache = LRUCache(max_size=256)

if self.lifecycle == "trace":

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

# no profiles taking place, so we can stop early
if not self.new_profiles and not self.active_profiles:
self.running = False
return
return True

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

# Move the new profiles into the active_profiles set.
#
Expand All @@ -374,9 +374,7 @@ def _sample_stack(*args, **kwargs):
inactive_profiles = []

for profile in self.active_profiles:
if profile.active:
pass
else:
if not profile.active:
# If a profile is marked inactive, we buffer it
# to `inactive_profiles` so it can be removed.
# We cannot remove it here as it would result
Expand All @@ -389,10 +387,12 @@ def _sample_stack(*args, **kwargs):
if self.buffer is not None:
self.buffer.write(ts, sample)

return False

else:

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

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

return False

return _sample_stack

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

while self.running:
self.sampler()
self.soft_shutdown = self.sampler()

# some time may have elapsed since the last time
# we sampled, so we need to account for that and
Expand All @@ -430,6 +432,15 @@ def run(self):
if elapsed < self.interval:
thread_sleep(self.interval - elapsed)

# the soft shutdown happens here to give it a chance
# for the profiler to be reused
if self.soft_shutdown:
self.running = False

# make sure to explicitly exit the profiler here or there might
# be multiple profilers at once
break

# after sleeping, make sure to take the current
# timestamp so we can use it next iteration
last = time.perf_counter()
Expand Down Expand Up @@ -458,6 +469,8 @@ def __init__(self, frequency, options, sdk_info, capture_func):
def ensure_running(self):
# type: () -> None

self.soft_shutdown = False

pid = os.getpid()

# is running on the right process
Expand Down Expand Up @@ -532,6 +545,9 @@ def __init__(self, frequency, options, sdk_info, capture_func):

def ensure_running(self):
# type: () -> None

self.soft_shutdown = False

pid = os.getpid()

# is running on the right process
Expand Down
31 changes: 26 additions & 5 deletions tests/profiler/test_continuous_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,33 +459,54 @@ def test_continuous_profiler_auto_start_and_stop_sampled(

thread = threading.current_thread()

all_profiler_ids = set()

for _ in range(3):
envelopes.clear()

profiler_ids = set()

with sentry_sdk.start_transaction(name="profiling 1"):
assert get_profiler_id() is not None, "profiler should be running"
profiler_id = get_profiler_id()
assert profiler_id is not None, "profiler should be running"
profiler_ids.add(profiler_id)
with sentry_sdk.start_span(op="op"):
time.sleep(0.1)
assert get_profiler_id() is not None, "profiler should be running"
profiler_id = get_profiler_id()
assert profiler_id is not None, "profiler should be running"
profiler_ids.add(profiler_id)

time.sleep(0.03)

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

with sentry_sdk.start_transaction(name="profiling 2"):
assert get_profiler_id() is not None, "profiler should be running"
profiler_id = get_profiler_id()
assert profiler_id is not None, "profiler should be running"
profiler_ids.add(profiler_id)
with sentry_sdk.start_span(op="op"):
time.sleep(0.1)
assert get_profiler_id() is not None, "profiler should be running"
profiler_id = get_profiler_id()
assert profiler_id is not None, "profiler should be running"
profiler_ids.add(profiler_id)

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

assert len(profiler_ids) == 1
all_profiler_ids.add(profiler_ids.pop())

assert_single_transaction_with_profile_chunks(
envelopes, thread, max_chunks=1, transactions=2
)

assert len(all_profiler_ids) == 3


@pytest.mark.parametrize(
"mode",
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy