1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
import os
from collections import deque
from sentry_sdk._compat import PY311
from sentry_sdk.utils import filename_for_module
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sentry_sdk._lru_cache import LRUCache
from types import FrameType
from typing import Deque
from typing import List
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing_extensions import TypedDict
ThreadId = str
ProcessedStack = List[int]
ProcessedFrame = TypedDict(
"ProcessedFrame",
{
"abs_path": str,
"filename": Optional[str],
"function": str,
"lineno": int,
"module": Optional[str],
},
)
ProcessedThreadMetadata = TypedDict(
"ProcessedThreadMetadata",
{"name": str},
)
FrameId = Tuple[
str, # abs_path
int, # lineno
str, # function
]
FrameIds = Tuple[FrameId, ...]
# The exact value of this id is not very meaningful. The purpose
# of this id is to give us a compact and unique identifier for a
# raw stack that can be used as a key to a dictionary so that it
# can be used during the sampled format generation.
StackId = Tuple[int, int]
ExtractedStack = Tuple[StackId, FrameIds, List[ProcessedFrame]]
ExtractedSample = Sequence[Tuple[ThreadId, ExtractedStack]]
# The default sampling frequency to use. This is set at 101 in order to
# mitigate the effects of lockstep sampling.
DEFAULT_SAMPLING_FREQUENCY = 101
# We want to impose a stack depth limit so that samples aren't too large.
MAX_STACK_DEPTH = 128
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 type(frame.f_locals["self"]).__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
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,
}
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
|