Skip to content

Commit ce0bf92

Browse files
Swatinemloewenheim
andauthored
feat: Implement instruction_addr_adjustment (#42533)
Implements handling of `instruction_addr_adjustment` and attaching appropriate per-frame attributes (`adjust_instruction_addr`) when sending stack traces to symbolicator. It implements the Sentry and Profiling side of [RFC 43](getsentry/rfcs#43) The Symbolicator side is implemented in getsentry/symbolicator#948. --------- Co-authored-by: Sebastian Zivota <[email protected]>
1 parent b35c18c commit ce0bf92

File tree

4 files changed

+153
-10
lines changed

4 files changed

+153
-10
lines changed

src/sentry/lang/native/processing.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,15 +302,24 @@ def _handles_frame(data, frame):
302302
return is_native_platform(platform) and frame.get("instruction_addr") is not None
303303

304304

305-
def get_frames_for_symbolication(frames, data, modules):
305+
def get_frames_for_symbolication(
306+
frames,
307+
data,
308+
modules,
309+
adjustment=None,
310+
):
306311
modules_by_debug_id = None
307312
rv = []
313+
adjustment = adjustment or "auto"
308314

309315
for frame in reversed(frames):
310316
if not _handles_frame(data, frame):
311317
continue
312318
s_frame = dict(frame)
313319

320+
if adjustment == "none":
321+
s_frame["adjust_instruction_addr"] = False
322+
314323
# validate and expand addressing modes. If we can't validate and
315324
# expand it, we keep None which is absolute. That's not great but
316325
# at least won't do damage.
@@ -342,6 +351,13 @@ def get_frames_for_symbolication(frames, data, modules):
342351
s_frame["addr_mode"] = sanitized_addr_mode
343352
rv.append(s_frame)
344353

354+
if len(rv) > 0:
355+
first_frame = rv[0]
356+
if adjustment == "all":
357+
first_frame["adjust_instruction_addr"] = True
358+
elif adjustment == "all_but_first":
359+
first_frame["adjust_instruction_addr"] = False
360+
345361
return rv
346362

347363

@@ -362,7 +378,10 @@ def process_payload(data):
362378
{
363379
"registers": sinfo.stacktrace.get("registers") or {},
364380
"frames": get_frames_for_symbolication(
365-
sinfo.stacktrace.get("frames") or (), data, modules
381+
sinfo.stacktrace.get("frames") or (),
382+
data,
383+
modules,
384+
sinfo.stacktrace.get("instruction_addr_adjustment"),
366385
),
367386
}
368387
for sinfo in stacktrace_infos

src/sentry/profiles/task.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from copy import deepcopy
34
from datetime import datetime
45
from time import sleep, time
56
from typing import Any, List, Mapping, MutableMapping, Optional, Tuple
@@ -72,6 +73,8 @@ def process_profile_task(
7273
)
7374
return
7475

76+
# WARNING(loewenheim): This function call may mutate `profile`'s frame list!
77+
# See comments in the function for why this happens.
7578
raw_modules, raw_stacktraces = _prepare_frames_from_profile(profile)
7679
modules, stacktraces = _symbolicate(
7780
project=project,
@@ -221,18 +224,41 @@ def _normalize(profile: Profile, organization: Organization) -> None:
221224
def _prepare_frames_from_profile(profile: Profile) -> Tuple[List[Any], List[Any]]:
222225
modules = profile["debug_meta"]["images"]
223226

227+
# NOTE: the usage of `adjust_instruction_addr` assumes that all
228+
# the profilers on all the platforms are walking stacks right from a
229+
# suspended threads cpu context
230+
224231
# in the sample format, we have a frames key containing all the frames
225232
if "version" in profile:
226-
stacktraces = [{"registers": {}, "frames": profile["profile"]["frames"]}]
233+
frames = profile["profile"]["frames"]
234+
235+
for stack in profile["profile"]["stacks"]:
236+
if len(stack) > 0:
237+
# Make a deep copy of the leaf frame with adjust_instruction_addr = False
238+
# and append it to the list. This ensures correct behavior
239+
# if the leaf frame also shows up in the middle of another stack.
240+
first_frame_idx = stack[0]
241+
frame = deepcopy(frames[first_frame_idx])
242+
frame["adjust_instruction_addr"] = False
243+
frames.append(frame)
244+
stack[0] = len(frames) - 1
245+
246+
stacktraces = [{"registers": {}, "frames": frames}]
227247
# in the original format, we need to gather frames from all samples
228248
else:
229-
stacktraces = [
230-
{
231-
"registers": {},
232-
"frames": s["frames"],
233-
}
234-
for s in profile["sampled_profile"]["samples"]
235-
]
249+
stacktraces = []
250+
for s in profile["sampled_profile"]["samples"]:
251+
frames = s["frames"]
252+
253+
if len(frames) > 0:
254+
frames[0]["adjust_instruction_addr"] = False
255+
256+
stacktraces.append(
257+
{
258+
"registers": {},
259+
"frames": frames,
260+
}
261+
)
236262
return (modules, stacktraces)
237263

238264

tests/sentry/lang/native/test_processing.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,54 @@ def test_filter_frames():
178178
assert len(filtered_frames) == 0
179179

180180

181+
def test_instruction_addr_adjustment_auto():
182+
frames = [
183+
{"instruction_addr": "0xdeadbeef", "platform": "native"},
184+
{"instruction_addr": "0xbeefdead", "platform": "native"},
185+
]
186+
187+
processed_frames = get_frames_for_symbolication(frames, None, None, None)
188+
189+
assert "adjust_instruction_addr" not in processed_frames[0].keys()
190+
assert "adjust_instruction_addr" not in processed_frames[1].keys()
191+
192+
193+
def test_instruction_addr_adjustment_all():
194+
frames = [
195+
{"instruction_addr": "0xdeadbeef", "platform": "native"},
196+
{"instruction_addr": "0xbeefdead", "platform": "native"},
197+
]
198+
199+
processed_frames = get_frames_for_symbolication(frames, None, None, "all")
200+
201+
assert processed_frames[0]["adjust_instruction_addr"]
202+
assert "adjust_instruction_addr" not in processed_frames[1].keys()
203+
204+
205+
def test_instruction_addr_adjustment_all_but_first():
206+
frames = [
207+
{"instruction_addr": "0xdeadbeef", "platform": "native"},
208+
{"instruction_addr": "0xbeefdead", "platform": "native"},
209+
]
210+
211+
processed_frames = get_frames_for_symbolication(frames, None, None, "all_but_first")
212+
213+
assert not processed_frames[0]["adjust_instruction_addr"]
214+
assert "adjust_instruction_addr" not in processed_frames[1].keys()
215+
216+
217+
def test_instruction_addr_adjustment_none():
218+
frames = [
219+
{"instruction_addr": "0xdeadbeef", "platform": "native"},
220+
{"instruction_addr": "0xbeefdead", "platform": "native"},
221+
]
222+
223+
processed_frames = get_frames_for_symbolication(frames, None, None, "none")
224+
225+
assert not processed_frames[0]["adjust_instruction_addr"]
226+
assert not processed_frames[1]["adjust_instruction_addr"]
227+
228+
181229
@pytest.mark.django_db
182230
@mock.patch("sentry.lang.native.processing.Symbolicator")
183231
def test_il2cpp_symbolication(mock_symbolicator, default_project):

tests/sentry/profiles/consumers/test_process.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from copy import copy
12
from datetime import datetime
23
from unittest.mock import Mock, patch
34

@@ -6,6 +7,7 @@
67
from arroyo.types import BrokerValue, Message, Partition, Topic
78

89
from sentry.profiles.consumers.process.factory import ProcessProfileStrategyFactory
10+
from sentry.profiles.task import _prepare_frames_from_profile
911
from sentry.testutils.cases import TestCase
1012
from sentry.utils import json
1113

@@ -56,3 +58,51 @@ def test_basic_profile_to_celery(self, process_profile_task):
5658
)
5759

5860
process_profile_task.assert_called_with(profile=profile)
61+
62+
63+
def test_adjust_instruction_addr_sample_format():
64+
original_frames = [
65+
{"instruction_addr": "0xdeadbeef"},
66+
{"instruction_addr": "0xbeefdead"},
67+
{"instruction_addr": "0xfeedface"},
68+
]
69+
profile = {
70+
"version": "1",
71+
"profile": {
72+
"frames": copy(original_frames),
73+
"stacks": [[1, 0], [0, 1, 2]],
74+
},
75+
"debug_meta": {"images": []},
76+
}
77+
78+
_, stacktraces = _prepare_frames_from_profile(profile)
79+
assert profile["profile"]["stacks"] == [[3, 0], [4, 1, 2]]
80+
frames = stacktraces[0]["frames"]
81+
82+
for i in range(3):
83+
assert frames[i] == original_frames[i]
84+
85+
assert frames[3] == {"instruction_addr": "0xbeefdead", "adjust_instruction_addr": False}
86+
assert frames[4] == {"instruction_addr": "0xdeadbeef", "adjust_instruction_addr": False}
87+
88+
89+
def test_adjust_instruction_addr_original_format():
90+
profile = {
91+
"sampled_profile": {
92+
"samples": [
93+
{
94+
"frames": [
95+
{"instruction_addr": "0xdeadbeef", "platform": "native"},
96+
{"instruction_addr": "0xbeefdead", "platform": "native"},
97+
],
98+
}
99+
]
100+
},
101+
"debug_meta": {"images": []},
102+
}
103+
104+
_, stacktraces = _prepare_frames_from_profile(profile)
105+
frames = stacktraces[0]["frames"]
106+
107+
assert not frames[0]["adjust_instruction_addr"]
108+
assert "adjust_instruction_addr" not in frames[1]

0 commit comments

Comments
 (0)