Skip to content

Commit 0233e27

Browse files
authored
ref(profiling): Do not send single sample profiles (#1879)
Single sample profiles are dropped in relay so there's no reason to send them to begin with. Save the extra bytes by just not sending it.
1 parent bac5bb1 commit 0233e27

File tree

7 files changed

+91
-29
lines changed

7 files changed

+91
-29
lines changed

sentry_sdk/profiler.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,14 +135,18 @@ def is_gevent():
135135

136136
_scheduler = None # type: Optional[Scheduler]
137137

138+
# The default sampling frequency to use. This is set at 101 in order to
139+
# mitigate the effects of lockstep sampling.
140+
DEFAULT_SAMPLING_FREQUENCY = 101
141+
142+
143+
# The minimum number of unique samples that must exist in a profile to be
144+
# considered valid.
145+
PROFILE_MINIMUM_SAMPLES = 2
146+
138147

139148
def setup_profiler(options):
140149
# type: (Dict[str, Any]) -> bool
141-
"""
142-
`buffer_secs` determines the max time a sample will be buffered for
143-
`frequency` determines the number of samples to take per second (Hz)
144-
"""
145-
146150
global _scheduler
147151

148152
if _scheduler is not None:
@@ -153,7 +157,7 @@ def setup_profiler(options):
153157
logger.warn("profiling is only supported on Python >= 3.3")
154158
return False
155159

156-
frequency = 101
160+
frequency = DEFAULT_SAMPLING_FREQUENCY
157161

158162
if is_gevent():
159163
# If gevent has patched the threading modules then we cannot rely on
@@ -429,6 +433,8 @@ def __init__(
429433
self.stacks = [] # type: List[ProcessedStack]
430434
self.samples = [] # type: List[ProcessedSample]
431435

436+
self.unique_samples = 0
437+
432438
transaction._profile = self
433439

434440
def update_active_thread_id(self):
@@ -540,6 +546,8 @@ def write(self, ts, sample):
540546
self.stop()
541547
return
542548

549+
self.unique_samples += 1
550+
543551
elapsed_since_start_ns = str(offset)
544552

545553
for tid, (stack_id, stack) in sample:
@@ -641,6 +649,14 @@ def to_json(self, event_opt, options):
641649
],
642650
}
643651

652+
def valid(self):
653+
# type: () -> bool
654+
return (
655+
self.sampled is not None
656+
and self.sampled
657+
and self.unique_samples >= PROFILE_MINIMUM_SAMPLES
658+
)
659+
644660

645661
class Scheduler(object):
646662
mode = "unknown"

sentry_sdk/tracing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ def finish(self, hub=None, end_timestamp=None):
716716
"spans": finished_spans,
717717
} # type: Event
718718

719-
if self._profile is not None and self._profile.sampled:
719+
if self._profile is not None and self._profile.valid():
720720
event["profile"] = self._profile
721721
contexts.update({"profile": self._profile.get_profile_context()})
722722
self._profile = None

tests/integrations/django/asgi/test_asgi.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77
from sentry_sdk.integrations.django import DjangoIntegration
88
from tests.integrations.django.myapp.asgi import channels_application
99

10+
try:
11+
from unittest import mock # python 3.3 and above
12+
except ImportError:
13+
import mock # python < 3.3
14+
1015
APPS = [channels_application]
1116
if django.VERSION >= (3, 0):
1217
from tests.integrations.django.myapp.asgi import asgi_application
@@ -81,32 +86,33 @@ async def test_async_views(sentry_init, capture_events, application):
8186
async def test_active_thread_id(
8287
sentry_init, capture_envelopes, teardown_profiling, endpoint, application
8388
):
84-
sentry_init(
85-
integrations=[DjangoIntegration()],
86-
traces_sample_rate=1.0,
87-
_experiments={"profiles_sample_rate": 1.0},
88-
)
89+
with mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0):
90+
sentry_init(
91+
integrations=[DjangoIntegration()],
92+
traces_sample_rate=1.0,
93+
_experiments={"profiles_sample_rate": 1.0},
94+
)
8995

90-
envelopes = capture_envelopes()
96+
envelopes = capture_envelopes()
9197

92-
comm = HttpCommunicator(application, "GET", endpoint)
93-
response = await comm.get_response()
94-
assert response["status"] == 200, response["body"]
98+
comm = HttpCommunicator(application, "GET", endpoint)
99+
response = await comm.get_response()
100+
assert response["status"] == 200, response["body"]
95101

96-
await comm.wait()
102+
await comm.wait()
97103

98-
data = json.loads(response["body"])
104+
data = json.loads(response["body"])
99105

100-
envelopes = [envelope for envelope in envelopes]
101-
assert len(envelopes) == 1
106+
envelopes = [envelope for envelope in envelopes]
107+
assert len(envelopes) == 1
102108

103-
profiles = [item for item in envelopes[0].items if item.type == "profile"]
104-
assert len(profiles) == 1
109+
profiles = [item for item in envelopes[0].items if item.type == "profile"]
110+
assert len(profiles) == 1
105111

106-
for profile in profiles:
107-
transactions = profile.payload.json["transactions"]
108-
assert len(transactions) == 1
109-
assert str(data["active"]) == transactions[0]["active_thread_id"]
112+
for profile in profiles:
113+
transactions = profile.payload.json["transactions"]
114+
assert len(transactions) == 1
115+
assert str(data["active"]) == transactions[0]["active_thread_id"]
110116

111117

112118
@pytest.mark.asyncio

tests/integrations/fastapi/test_fastapi.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@
1212
from sentry_sdk.integrations.starlette import StarletteIntegration
1313
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
1414

15+
try:
16+
from unittest import mock # python 3.3 and above
17+
except ImportError:
18+
import mock # python < 3.3
19+
1520

1621
def fastapi_app_factory():
1722
app = FastAPI()
@@ -155,6 +160,7 @@ def test_legacy_setup(
155160

156161

157162
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
163+
@mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0)
158164
def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint):
159165
sentry_init(
160166
traces_sample_rate=1.0,

tests/integrations/starlette/test_starlette.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,7 @@ def test_legacy_setup(
846846

847847

848848
@pytest.mark.parametrize("endpoint", ["/sync/thread_ids", "/async/thread_ids"])
849+
@mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0)
849850
def test_active_thread_id(sentry_init, capture_envelopes, teardown_profiling, endpoint):
850851
sentry_init(
851852
traces_sample_rate=1.0,

tests/integrations/wsgi/test_wsgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ def sample_app(environ, start_response):
287287
@pytest.mark.skipif(
288288
sys.version_info < (3, 3), reason="Profiling is only supported in Python >= 3.3"
289289
)
290+
@mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0)
290291
def test_profile_sent(
291292
sentry_init,
292293
capture_envelopes,

tests/test_profiler.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import inspect
2-
import mock
32
import os
43
import sys
54
import threading
@@ -21,6 +20,11 @@
2120
from sentry_sdk.tracing import Transaction
2221
from sentry_sdk._queue import Queue
2322

23+
try:
24+
from unittest import mock # python 3.3 and above
25+
except ImportError:
26+
import mock # python < 3.3
27+
2428
try:
2529
import gevent
2630
except ImportError:
@@ -88,6 +92,7 @@ def test_profiler_setup_twice(teardown_profiling):
8892
pytest.param(None, 0, id="profiler not enabled"),
8993
],
9094
)
95+
@mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0)
9196
def test_profiled_transaction(
9297
sentry_init,
9398
capture_envelopes,
@@ -115,6 +120,7 @@ def test_profiled_transaction(
115120
assert len(items["profile"]) == profile_count
116121

117122

123+
@mock.patch("sentry_sdk.profiler.PROFILE_MINIMUM_SAMPLES", 0)
118124
def test_profile_context(
119125
sentry_init,
120126
capture_envelopes,
@@ -145,6 +151,32 @@ def test_profile_context(
145151
}
146152

147153

154+
def test_minimum_unique_samples_required(
155+
sentry_init,
156+
capture_envelopes,
157+
teardown_profiling,
158+
):
159+
sentry_init(
160+
traces_sample_rate=1.0,
161+
_experiments={"profiles_sample_rate": 1.0},
162+
)
163+
164+
envelopes = capture_envelopes()
165+
166+
with start_transaction(name="profiling"):
167+
pass
168+
169+
items = defaultdict(list)
170+
for envelope in envelopes:
171+
for item in envelope.items:
172+
items[item.type].append(item)
173+
174+
assert len(items["transaction"]) == 1
175+
# because we dont leave any time for the profiler to
176+
# take any samples, it should be not be sent
177+
assert len(items["profile"]) == 0
178+
179+
148180
def get_frame(depth=1):
149181
"""
150182
This function is not exactly true to its name. Depending on
@@ -478,7 +510,7 @@ def test_thread_scheduler_single_background_thread(scheduler_class):
478510
pytest.param(GeventScheduler, marks=requires_gevent, id="gevent scheduler"),
479511
],
480512
)
481-
@mock.patch("sentry_sdk.profiler.MAX_PROFILE_DURATION_NS", int(1))
513+
@mock.patch("sentry_sdk.profiler.MAX_PROFILE_DURATION_NS", 1)
482514
def test_max_profile_duration_reached(scheduler_class):
483515
sample = [
484516
(
@@ -792,7 +824,7 @@ def test_max_profile_duration_reached(scheduler_class):
792824
pytest.param(GeventScheduler, marks=requires_gevent, id="gevent scheduler"),
793825
],
794826
)
795-
@mock.patch("sentry_sdk.profiler.MAX_PROFILE_DURATION_NS", int(5))
827+
@mock.patch("sentry_sdk.profiler.MAX_PROFILE_DURATION_NS", 5)
796828
def test_profile_processing(
797829
DictionaryContaining, # noqa: N803
798830
scheduler_class,

0 commit comments

Comments
 (0)