Skip to content

Commit

Permalink
re-export profiler for back compat
Browse files Browse the repository at this point in the history
  • Loading branch information
Zylphrex committed Jun 7, 2024
1 parent fdb17e5 commit 29cb67d
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 152 deletions.
40 changes: 39 additions & 1 deletion sentry_sdk/profiler/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
154 changes: 4 additions & 150 deletions sentry_sdk/profiler/transaction_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -52,7 +54,6 @@
)

if TYPE_CHECKING:
from types import FrameType
from typing import Any
from typing import Callable
from typing import Deque
Expand All @@ -69,7 +70,6 @@
FrameId,
StackId,
ThreadId,
ExtractedStack,
ExtractedSample,
)
from sentry_sdk._types import Event, SamplingContext, ProfilerMode
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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


Expand Down
4 changes: 3 additions & 1 deletion tests/profiler/test_transaction_profiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 29cb67d

Please sign in to comment.