Skip to content

Commit 5348834

Browse files
author
Pierre Massat
authored
feat(profiling): Convert profile output to the sample format (#1611)
1 parent f71a8f4 commit 5348834

File tree

6 files changed

+124
-68
lines changed

6 files changed

+124
-68
lines changed

sentry_sdk/_compat.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414

1515
PY2 = sys.version_info[0] == 2
16+
PY33 = sys.version_info[0] == 3 and sys.version_info[1] >= 3
17+
PY37 = sys.version_info[0] == 3 and sys.version_info[1] >= 7
1618

1719
if PY2:
1820
import urlparse

sentry_sdk/client.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -410,9 +410,12 @@ def capture_event(
410410

411411
if is_transaction:
412412
if "profile" in event_opt:
413-
event_opt["profile"]["transaction_id"] = event_opt["event_id"]
414413
event_opt["profile"]["environment"] = event_opt.get("environment")
415-
event_opt["profile"]["version_name"] = event_opt.get("release", "")
414+
event_opt["profile"]["release"] = event_opt.get("release", "")
415+
event_opt["profile"]["timestamp"] = event_opt.get("timestamp", "")
416+
event_opt["profile"]["transactions"][0]["id"] = event_opt[
417+
"event_id"
418+
]
416419
envelope.add_profile(event_opt.pop("profile"))
417420
envelope.add_transaction(event_opt)
418421
else:

sentry_sdk/profiler.py

Lines changed: 53 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@
2525
from contextlib import contextmanager
2626

2727
import sentry_sdk
28-
from sentry_sdk._compat import PY2
28+
from sentry_sdk._compat import PY33
29+
2930
from sentry_sdk._types import MYPY
31+
from sentry_sdk.utils import nanosecond_time
3032

3133
if MYPY:
3234
from typing import Any
@@ -43,22 +45,6 @@
4345
FrameData = Tuple[str, str, int]
4446

4547

46-
if PY2:
47-
48-
def nanosecond_time():
49-
# type: () -> int
50-
return int(time.clock() * 1e9)
51-
52-
else:
53-
54-
def nanosecond_time():
55-
# type: () -> int
56-
57-
# In python3.7+, there is a time.perf_counter_ns()
58-
# that we may want to switch to for more precision
59-
return int(time.perf_counter() * 1e9)
60-
61-
6248
_sample_buffer = None # type: Optional[_SampleBuffer]
6349
_scheduler = None # type: Optional[_Scheduler]
6450

@@ -73,6 +59,12 @@ def setup_profiler(options):
7359
buffer_secs = 60
7460
frequency = 101
7561

62+
if not PY33:
63+
from sentry_sdk.utils import logger
64+
65+
logger.warn("profiling is only supported on Python >= 3.3")
66+
return
67+
7668
global _sample_buffer
7769
global _scheduler
7870

@@ -194,19 +186,39 @@ def to_json(self):
194186
assert self._stop_ns is not None
195187

196188
return {
197-
"device_os_name": platform.system(),
198-
"device_os_version": platform.release(),
199-
"duration_ns": str(self._stop_ns - self._start_ns),
200189
"environment": None, # Gets added in client.py
190+
"event_id": uuid.uuid4().hex,
201191
"platform": "python",
202-
"platform_version": platform.python_version(),
203-
"profile_id": uuid.uuid4().hex,
204192
"profile": _sample_buffer.slice_profile(self._start_ns, self._stop_ns),
205-
"trace_id": self.transaction.trace_id,
206-
"transaction_id": None, # Gets added in client.py
207-
"transaction_name": self.transaction.name,
208-
"version_code": "", # TODO: Determine appropriate value. Currently set to empty string so profile will not get rejected.
209-
"version_name": None, # Gets added in client.py
193+
"release": None, # Gets added in client.py
194+
"timestamp": None, # Gets added in client.py
195+
"version": "1",
196+
"device": {
197+
"architecture": platform.machine(),
198+
},
199+
"os": {
200+
"name": platform.system(),
201+
"version": platform.release(),
202+
},
203+
"runtime": {
204+
"name": platform.python_implementation(),
205+
"version": platform.python_version(),
206+
},
207+
"transactions": [
208+
{
209+
"id": None, # Gets added in client.py
210+
"name": self.transaction.name,
211+
# we start the transaction before the profile and this is
212+
# the transaction start time relative to the profile, so we
213+
# hardcode it to 0 until we can start the profile before
214+
"relative_start_ns": "0",
215+
# use the duration of the profile instead of the transaction
216+
# because we end the transaction after the profile
217+
"relative_end_ns": str(self._stop_ns - self._start_ns),
218+
"trace_id": self.transaction.trace_id,
219+
"active_thread_id": str(self.transaction._active_thread_id),
220+
}
221+
],
210222
}
211223

212224

@@ -245,8 +257,10 @@ def write(self, sample):
245257
self.idx = (idx + 1) % self.capacity
246258

247259
def slice_profile(self, start_ns, stop_ns):
248-
# type: (int, int) -> Dict[str, List[Any]]
260+
# type: (int, int) -> Dict[str, Any]
249261
samples = [] # type: List[Any]
262+
stacks = dict() # type: Dict[Any, int]
263+
stacks_list = list() # type: List[Any]
250264
frames = dict() # type: Dict[FrameData, int]
251265
frames_list = list() # type: List[Any]
252266

@@ -265,10 +279,10 @@ def slice_profile(self, start_ns, stop_ns):
265279

266280
for tid, stack in raw_sample[1]:
267281
sample = {
268-
"frames": [],
269-
"relative_timestamp_ns": ts - start_ns,
270-
"thread_id": tid,
282+
"elapsed_since_start_ns": str(ts - start_ns),
283+
"thread_id": str(tid),
271284
}
285+
current_stack = []
272286

273287
for frame in stack:
274288
if frame not in frames:
@@ -280,11 +294,17 @@ def slice_profile(self, start_ns, stop_ns):
280294
"line": frame[2],
281295
}
282296
)
283-
sample["frames"].append(frames[frame])
297+
current_stack.append(frames[frame])
298+
299+
current_stack = tuple(current_stack)
300+
if current_stack not in stacks:
301+
stacks[current_stack] = len(stacks)
302+
stacks_list.append(current_stack)
284303

304+
sample["stack_id"] = stacks[current_stack]
285305
samples.append(sample)
286306

287-
return {"frames": frames_list, "samples": samples}
307+
return {"stacks": stacks_list, "frames": frames_list, "samples": samples}
288308

289309

290310
class _Scheduler(object):

sentry_sdk/tracing.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import uuid
22
import random
3+
import threading
34
import time
45

56
from datetime import datetime, timedelta
@@ -544,6 +545,7 @@ class Transaction(Span):
544545
"_measurements",
545546
"_profile",
546547
"_baggage",
548+
"_active_thread_id",
547549
)
548550

549551
def __init__(
@@ -579,6 +581,11 @@ def __init__(
579581
self._measurements = {} # type: Dict[str, Any]
580582
self._profile = None # type: Optional[Dict[str, Any]]
581583
self._baggage = baggage
584+
# for profiling, we want to know on which thread a transaction is started
585+
# to accurately show the active thread in the UI
586+
self._active_thread_id = (
587+
threading.current_thread().ident
588+
) # used by profiling.py
582589

583590
def __repr__(self):
584591
# type: () -> str

sentry_sdk/utils.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
import threading
88
import subprocess
99
import re
10+
import time
1011

1112
from datetime import datetime
1213

1314
import sentry_sdk
14-
from sentry_sdk._compat import urlparse, text_type, implements_str, PY2
15+
from sentry_sdk._compat import urlparse, text_type, implements_str, PY2, PY33, PY37
1516

1617
from sentry_sdk._types import MYPY
1718

@@ -1010,3 +1011,24 @@ def from_base64(base64_string):
10101011
)
10111012

10121013
return utf8_string
1014+
1015+
1016+
if PY37:
1017+
1018+
def nanosecond_time():
1019+
# type: () -> int
1020+
return time.perf_counter_ns()
1021+
1022+
elif PY33:
1023+
1024+
def nanosecond_time():
1025+
# type: () -> int
1026+
1027+
return int(time.perf_counter() * 1e9)
1028+
1029+
else:
1030+
1031+
def nanosecond_time():
1032+
# type: () -> int
1033+
1034+
raise AttributeError

tests/integrations/wsgi/test_wsgi.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
77
from sentry_sdk.profiler import teardown_profiler
88
from collections import Counter
9+
from sentry_sdk.utils import PY33
910

1011
try:
1112
from unittest import mock # python 3.3 and above
@@ -21,12 +22,6 @@ def app(environ, start_response):
2122
return app
2223

2324

24-
@pytest.fixture
25-
def profiling():
26-
yield
27-
teardown_profiler()
28-
29-
3025
class IterableApp(object):
3126
def __init__(self, iterable):
3227
self.iterable = iterable
@@ -289,31 +284,38 @@ def sample_app(environ, start_response):
289284
assert len(session_aggregates) == 1
290285

291286

292-
@pytest.mark.parametrize(
293-
"profiles_sample_rate,should_send",
294-
[(1.0, True), (0.75, True), (0.25, False), (None, False)],
295-
)
296-
def test_profile_sent_when_profiling_enabled(
297-
capture_envelopes, sentry_init, profiling, profiles_sample_rate, should_send
298-
):
299-
def test_app(environ, start_response):
300-
start_response("200 OK", [])
301-
return ["Go get the ball! Good dog!"]
302-
303-
sentry_init(
304-
traces_sample_rate=1.0,
305-
_experiments={"profiles_sample_rate": profiles_sample_rate},
306-
)
307-
app = SentryWsgiMiddleware(test_app)
308-
envelopes = capture_envelopes()
287+
if PY33:
309288

310-
with mock.patch("sentry_sdk.profiler.random.random", return_value=0.5):
311-
client = Client(app)
312-
client.get("/")
289+
@pytest.fixture
290+
def profiling():
291+
yield
292+
teardown_profiler()
313293

314-
profile_sent = False
315-
for item in envelopes[0].items:
316-
if item.headers["type"] == "profile":
317-
profile_sent = True
318-
break
319-
assert profile_sent == should_send
294+
@pytest.mark.parametrize(
295+
"profiles_sample_rate,should_send",
296+
[(1.0, True), (0.75, True), (0.25, False), (None, False)],
297+
)
298+
def test_profile_sent_when_profiling_enabled(
299+
capture_envelopes, sentry_init, profiling, profiles_sample_rate, should_send
300+
):
301+
def test_app(environ, start_response):
302+
start_response("200 OK", [])
303+
return ["Go get the ball! Good dog!"]
304+
305+
sentry_init(
306+
traces_sample_rate=1.0,
307+
_experiments={"profiles_sample_rate": profiles_sample_rate},
308+
)
309+
app = SentryWsgiMiddleware(test_app)
310+
envelopes = capture_envelopes()
311+
312+
with mock.patch("sentry_sdk.profiler.random.random", return_value=0.5):
313+
client = Client(app)
314+
client.get("/")
315+
316+
profile_sent = False
317+
for item in envelopes[0].items:
318+
if item.headers["type"] == "profile":
319+
profile_sent = True
320+
break
321+
assert profile_sent == should_send

0 commit comments

Comments
 (0)