2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -1,4 +1,9 @@
from sentry_sdk.profiler.continuous_profiler import start_profiler, stop_profiler
from sentry_sdk.profiler.continuous_profiler import (
start_profile_session,
start_profiler,
stop_profile_session,
stop_profiler,
)
from sentry_sdk.profiler.transaction_profiler import (
MAX_PROFILE_DURATION_NS,
PROFILE_MINIMUM_SAMPLES,
@@ -20,7 +25,9 @@ from sentry_sdk.profiler.utils import (
)
__all__ = [
"start_profile_session", # TODO: Deprecate this in favor of `start_profiler`
"start_profiler",
"stop_profile_session", # TODO: Deprecate this in favor of `stop_profiler`
"stop_profiler",
# DEPRECATED: The following was re-exported for backwards compatibility. It
# will be removed from sentry_sdk.profiler in a future release.
@@ -5,6 +5,7 @@ import sys
import threading
import time
import uuid
import warnings
from collections import deque
from datetime import datetime, timezone
@@ -74,9 +75,11 @@ def setup_continuous_profiler(options, sdk_info, capture_func):
# type: (Dict[str, Any], SDKInfo, Callable[[Envelope], None]) -> bool
global _scheduler
if _scheduler is not None:
already_initialized = _scheduler is not None
if already_initialized:
logger.debug("[Profiling] Continuous Profiler is already setup")
return False
teardown_continuous_profiler()
if is_gevent():
# If gevent has patched the threading modules then we cannot rely on
@@ -116,11 +119,19 @@ def setup_continuous_profiler(options, sdk_info, capture_func):
)
)
atexit.register(teardown_continuous_profiler)
if not already_initialized:
atexit.register(teardown_continuous_profiler)
return True
def is_profile_session_sampled():
# type: () -> bool
if _scheduler is None:
return False
return _scheduler.sampled
def try_autostart_continuous_profiler():
# type: () -> None
@@ -151,6 +162,17 @@ def start_profiler():
_scheduler.manual_start()
def start_profile_session():
# type: () -> None
warnings.warn(
"The `start_profile_session` function is deprecated. Please use `start_profile` instead.",
DeprecationWarning,
stacklevel=2,
)
start_profiler()
def stop_profiler():
# type: () -> None
if _scheduler is None:
@@ -159,6 +181,17 @@ def stop_profiler():
_scheduler.manual_stop()
def stop_profile_session():
# type: () -> None
warnings.warn(
"The `stop_profile_session` function is deprecated. Please use `stop_profile` instead.",
DeprecationWarning,
stacklevel=2,
)
stop_profiler()
def teardown_continuous_profiler():
# type: () -> None
stop_profiler()
@@ -213,6 +246,7 @@ class ContinuousScheduler:
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]
@@ -294,7 +328,7 @@ class ContinuousScheduler:
return self.buffer.profiler_id
def make_sampler(self):
# type: () -> Callable[..., None]
# type: () -> Callable[..., bool]
cwd = os.getcwd()
cache = LRUCache(max_size=256)
@@ -302,7 +336,7 @@ class ContinuousScheduler:
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.
@@ -310,8 +344,7 @@ class ContinuousScheduler:
# 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
@@ -334,7 +367,7 @@ class ContinuousScheduler:
# 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.
#
@@ -351,9 +384,7 @@ class ContinuousScheduler:
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
@@ -366,10 +397,12 @@ class ContinuousScheduler:
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.
@@ -386,11 +419,13 @@ class ContinuousScheduler:
# 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):
@@ -398,7 +433,7 @@ class ContinuousScheduler:
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
@@ -407,6 +442,15 @@ class ContinuousScheduler:
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()
@@ -435,6 +479,8 @@ class ThreadContinuousScheduler(ContinuousScheduler):
def ensure_running(self):
# type: () -> None
self.soft_shutdown = False
pid = os.getpid()
# is running on the right process
@@ -509,6 +555,9 @@ class GeventContinuousScheduler(ContinuousScheduler):
def ensure_running(self):
# type: () -> None
self.soft_shutdown = False
pid = os.getpid()
# is running on the right process
@@ -45,6 +45,7 @@ from sentry_sdk.profiler.utils import (
)
from sentry_sdk.utils import (
capture_internal_exception,
capture_internal_exceptions,
get_current_thread_meta,
is_gevent,
is_valid_sample_rate,
@@ -369,12 +370,13 @@ class Profile:
def __exit__(self, ty, value, tb):
# type: (Optional[Any], Optional[Any], Optional[Any]) -> None
self.stop()
with capture_internal_exceptions():
self.stop()
scope, old_profile = self._context_manager_state
del self._context_manager_state
scope, old_profile = self._context_manager_state
del self._context_manager_state
scope.profile = old_profile
scope.profile = old_profile
def write(self, ts, sample):
# type: (int, ExtractedSample) -> None
@@ -85,9 +85,7 @@ else:
if (
# the co_varnames start with the frame's positional arguments
# and we expect the first to be `self` if its an instance method
co_varnames
and co_varnames[0] == "self"
and "self" in frame.f_locals
co_varnames and co_varnames[0] == "self" and "self" in frame.f_locals
):
for cls in type(frame.f_locals["self"]).__mro__:
if name in cls.__dict__:
@@ -101,9 +99,7 @@ else:
if (
# the co_varnames start with the frame's positional arguments
# and we expect the first to be `cls` if its a class method
co_varnames
and co_varnames[0] == "cls"
and "cls" in frame.f_locals
co_varnames and co_varnames[0] == "cls" and "cls" in frame.f_locals
):
for cls in frame.f_locals["cls"].__mro__:
if name in cls.__dict__: