From 29cb67d0727fc1c94d451e9017bee36feffd6a3e Mon Sep 17 00:00:00 2001 From: Tony Xiao Date: Fri, 7 Jun 2024 10:49:47 -0400 Subject: [PATCH] re-export profiler for back compat --- sentry_sdk/profiler/__init__.py | 40 ++++- sentry_sdk/profiler/transaction_profiler.py | 154 +------------------- tests/profiler/test_transaction_profiler.py | 4 +- 3 files changed, 46 insertions(+), 152 deletions(-) diff --git a/sentry_sdk/profiler/__init__.py b/sentry_sdk/profiler/__init__.py index e1ac1b4896..e813bea4e0 100644 --- a/sentry_sdk/profiler/__init__.py +++ b/sentry_sdk/profiler/__init__.py @@ -1,3 +1,41 @@ from sentry_sdk.profiler.continuous_profiler import start_profiler, stop_profiler +from sentry_sdk.profiler.transaction_profiler import ( + MAX_PROFILE_DURATION_NS, + PROFILE_MINIMUM_SAMPLES, + Profile, + Scheduler, + ThreadScheduler, + GeventScheduler, + has_profiling_enabled, + setup_profiler, + teardown_profiler, +) +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + MAX_STACK_DEPTH, + get_frame_name, + extract_frame, + extract_stack, + frame_id, +) -__all__ = ["start_profiler", "stop_profiler"] +__all__ = [ + "start_profiler", + "stop_profiler", + # Re-exported for backwards compatibility + "MAX_PROFILE_DURATION_NS", + "PROFILE_MINIMUM_SAMPLES", + "Profile", + "Scheduler", + "ThreadScheduler", + "GeventScheduler", + "has_profiling_enabled", + "setup_profiler", + "teardown_profiler", + "DEFAULT_SAMPLING_FREQUENCY", + "MAX_STACK_DEPTH", + "get_frame_name", + "extract_frame", + "extract_stack", + "frame_id", +] diff --git a/sentry_sdk/profiler/transaction_profiler.py b/sentry_sdk/profiler/transaction_profiler.py index 5fc2d14050..a4f32dba90 100644 --- a/sentry_sdk/profiler/transaction_profiler.py +++ b/sentry_sdk/profiler/transaction_profiler.py @@ -37,12 +37,14 @@ from collections import deque import sentry_sdk -from sentry_sdk._compat import PY311 from sentry_sdk._lru_cache import LRUCache from sentry_sdk._types import TYPE_CHECKING +from sentry_sdk.profiler.utils import ( + DEFAULT_SAMPLING_FREQUENCY, + extract_stack, +) from sentry_sdk.utils import ( capture_internal_exception, - filename_for_module, get_current_thread_meta, is_gevent, is_valid_sample_rate, @@ -52,7 +54,6 @@ ) if TYPE_CHECKING: - from types import FrameType from typing import Any from typing import Callable from typing import Deque @@ -69,7 +70,6 @@ FrameId, StackId, ThreadId, - ExtractedStack, ExtractedSample, ) from sentry_sdk._types import Event, SamplingContext, ProfilerMode @@ -107,10 +107,6 @@ _scheduler = None # type: Optional[Scheduler] -# The default sampling frequency to use. This is set at 101 in order to -# mitigate the effects of lockstep sampling. -DEFAULT_SAMPLING_FREQUENCY = 101 - # The minimum number of unique samples that must exist in a profile to be # considered valid. @@ -202,148 +198,6 @@ def teardown_profiler(): _scheduler = None -# We want to impose a stack depth limit so that samples aren't too large. -MAX_STACK_DEPTH = 128 - - -def extract_stack( - raw_frame, # type: Optional[FrameType] - cache, # type: LRUCache - cwd, # type: str - max_stack_depth=MAX_STACK_DEPTH, # type: int -): - # type: (...) -> ExtractedStack - """ - Extracts the stack starting the specified frame. The extracted stack - assumes the specified frame is the top of the stack, and works back - to the bottom of the stack. - - In the event that the stack is more than `MAX_STACK_DEPTH` frames deep, - only the first `MAX_STACK_DEPTH` frames will be returned. - """ - - raw_frames = deque(maxlen=max_stack_depth) # type: Deque[FrameType] - - while raw_frame is not None: - f_back = raw_frame.f_back - raw_frames.append(raw_frame) - raw_frame = f_back - - frame_ids = tuple(frame_id(raw_frame) for raw_frame in raw_frames) - frames = [] - for i, fid in enumerate(frame_ids): - frame = cache.get(fid) - if frame is None: - frame = extract_frame(fid, raw_frames[i], cwd) - cache.set(fid, frame) - frames.append(frame) - - # Instead of mapping the stack into frame ids and hashing - # that as a tuple, we can directly hash the stack. - # This saves us from having to generate yet another list. - # Additionally, using the stack as the key directly is - # costly because the stack can be large, so we pre-hash - # the stack, and use the hash as the key as this will be - # needed a few times to improve performance. - # - # To Reduce the likelihood of hash collisions, we include - # the stack depth. This means that only stacks of the same - # depth can suffer from hash collisions. - stack_id = len(raw_frames), hash(frame_ids) - - return stack_id, frame_ids, frames - - -def frame_id(raw_frame): - # type: (FrameType) -> FrameId - return (raw_frame.f_code.co_filename, raw_frame.f_lineno, get_frame_name(raw_frame)) - - -def extract_frame(fid, raw_frame, cwd): - # type: (FrameId, FrameType, str) -> ProcessedFrame - abs_path = raw_frame.f_code.co_filename - - try: - module = raw_frame.f_globals["__name__"] - except Exception: - module = None - - # namedtuples can be many times slower when initialing - # and accessing attribute so we opt to use a tuple here instead - return { - # This originally was `os.path.abspath(abs_path)` but that had - # a large performance overhead. - # - # According to docs, this is equivalent to - # `os.path.normpath(os.path.join(os.getcwd(), path))`. - # The `os.getcwd()` call is slow here, so we precompute it. - # - # Additionally, since we are using normalized path already, - # we skip calling `os.path.normpath` entirely. - "abs_path": os.path.join(cwd, abs_path), - "module": module, - "filename": filename_for_module(module, abs_path) or None, - "function": fid[2], - "lineno": raw_frame.f_lineno, - } - - -if PY311: - - def get_frame_name(frame): - # type: (FrameType) -> str - return frame.f_code.co_qualname - -else: - - def get_frame_name(frame): - # type: (FrameType) -> str - - f_code = frame.f_code - co_varnames = f_code.co_varnames - - # co_name only contains the frame name. If the frame was a method, - # the class name will NOT be included. - name = f_code.co_name - - # if it was a method, we can get the class name by inspecting - # the f_locals for the `self` argument - try: - 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 - ): - for cls in frame.f_locals["self"].__class__.__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # if it was a class method, (decorated with `@classmethod`) - # we can get the class name by inspecting the f_locals for the `cls` argument - try: - 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 - ): - for cls in frame.f_locals["cls"].__mro__: - if name in cls.__dict__: - return "{}.{}".format(cls.__name__, name) - except (AttributeError, ValueError): - pass - - # nothing we can do if it is a staticmethod (decorated with @staticmethod) - - # we've done all we can, time to give up and return what we have - return name - - MAX_PROFILE_DURATION_NS = int(3e10) # 30 seconds diff --git a/tests/profiler/test_transaction_profiler.py b/tests/profiler/test_transaction_profiler.py index ede4a141fe..0f1cc12931 100644 --- a/tests/profiler/test_transaction_profiler.py +++ b/tests/profiler/test_transaction_profiler.py @@ -14,11 +14,13 @@ Profile, Scheduler, ThreadScheduler, + setup_profiler, +) +from sentry_sdk.profiler.utils import ( extract_frame, extract_stack, frame_id, get_frame_name, - setup_profiler, ) from sentry_sdk._lru_cache import LRUCache