Skip to content

Commit f21fb93

Browse files
committed
use old logic for tracking transactions
1 parent 5a99c97 commit f21fb93

File tree

6 files changed

+104
-64
lines changed

6 files changed

+104
-64
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -529,7 +529,7 @@ def __init__(
529529
profiles_sample_rate=None, # type: Optional[float]
530530
profiles_sampler=None, # type: Optional[TracesSampler]
531531
profiler_mode=None, # type: Optional[ProfilerMode]
532-
profile_lifecycle="manual", # type: Literal["manual", "auto"]
532+
profile_lifecycle="manual", # type: Literal["manual", "trace"]
533533
profile_session_sample_rate=None, # type: Optional[float]
534534
auto_enabling_integrations=True, # type: bool
535535
disabled_integrations=None, # type: Optional[Sequence[sentry_sdk.integrations.Integration]]

sentry_sdk/profiler/continuous_profiler.py

Lines changed: 61 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from typing import Dict
3333
from typing import List
3434
from typing import Optional
35+
from typing import Set
3536
from typing import Type
3637
from typing import Union
3738
from typing_extensions import TypedDict
@@ -134,15 +135,15 @@ def try_autostart_continuous_profiler():
134135
_scheduler.manual_start()
135136

136137

137-
def try_profile_lifecycle_auto_start():
138-
# type: () -> bool
138+
def try_profile_lifecycle_trace_start():
139+
# type: () -> Union[ContinuousProfile, None]
139140
if _scheduler is None:
140-
return False
141+
return None
141142

142143
return _scheduler.auto_start()
143144

144145

145-
def try_profile_lifecycle_auto_stop():
146+
def try_profile_lifecycle_trace_stop():
146147
# type: () -> None
147148
if _scheduler is None:
148149
return
@@ -191,6 +192,14 @@ def determine_profile_session_sampling_decision(sample_rate):
191192
return random.random() < float(sample_rate)
192193

193194

195+
class ContinuousProfile:
196+
active: bool = True
197+
198+
def stop(self):
199+
# type: () -> None
200+
self.active = False
201+
202+
194203
class ContinuousScheduler:
195204
mode = "unknown" # type: ContinuousProfilerMode
196205

@@ -213,9 +222,8 @@ def __init__(self, frequency, options, sdk_info, capture_func):
213222

214223
self.running = False
215224

216-
self.active_spans = 0
217-
self.started_spans = deque(maxlen=128) # type: Deque[None]
218-
self.finished_spans = deque(maxlen=128) # type: Deque[None]
225+
self.new_profiles = deque(maxlen=128) # type: Deque[ContinuousProfile]
226+
self.active_profiles = set() # type: Set[ContinuousProfile]
219227

220228
def is_auto_start_enabled(self):
221229
# type: () -> bool
@@ -235,28 +243,21 @@ def is_auto_start_enabled(self):
235243
return experiments.get("continuous_profiling_auto_start")
236244

237245
def auto_start(self):
238-
# type: () -> bool
246+
# type: () -> Union[ContinuousProfile, None]
239247
if not self.sampled:
240-
return False
248+
return None
241249

242-
if self.lifecycle != "auto":
243-
return False
250+
if self.lifecycle != "trace":
251+
return None
244252

245253
logger.debug("[Profiling] Auto starting profiler")
246254

247-
self.started_spans.append(None)
248-
self.ensure_running()
249-
250-
return True
255+
profile = ContinuousProfile()
251256

252-
def auto_stop(self):
253-
# type: () -> None
254-
if self.lifecycle != "auto":
255-
return
256-
257-
logger.debug("[Profiling] Auto stopping profiler")
257+
self.new_profiles.append(profile)
258+
self.ensure_running()
258259

259-
self.finished_spans.append(None)
260+
return profile
260261

261262
def manual_start(self):
262263
# type: () -> None
@@ -306,7 +307,7 @@ def make_sampler(self):
306307

307308
cache = LRUCache(max_size=256)
308309

309-
if self.lifecycle == "auto":
310+
if self.lifecycle == "trace":
310311

311312
def _sample_stack(*args, **kwargs):
312313
# type: (*Any, **Any) -> None
@@ -315,16 +316,20 @@ def _sample_stack(*args, **kwargs):
315316
This should be called at a regular interval to collect samples.
316317
"""
317318

318-
if (
319-
not self.active_spans
320-
and not self.started_spans
321-
and not self.finished_spans
322-
):
319+
# no profiles taking place, so we can stop early
320+
if not self.new_profiles and not self.active_profiles:
323321
self.running = False
324322
return
325323

326-
started_spans = len(self.started_spans)
327-
finished_spans = len(self.finished_spans)
324+
# This is the number of profiles we want to pop off.
325+
# It's possible another thread adds a new profile to
326+
# the list and we spend longer than we want inside
327+
# the loop below.
328+
#
329+
# Also make sure to set this value before extracting
330+
# frames so we do not write to any new profiles that
331+
# were started after this point.
332+
new_profiles = len(self.new_profiles)
328333

329334
ts = now()
330335

@@ -339,13 +344,32 @@ def _sample_stack(*args, **kwargs):
339344
capture_internal_exception(sys.exc_info())
340345
return
341346

342-
for _ in range(started_spans):
343-
self.started_spans.popleft()
344-
345-
for _ in range(finished_spans):
346-
self.finished_spans.popleft()
347-
348-
self.active_spans = self.active_spans + started_spans - finished_spans
347+
# Move the new profiles into the active_profiles set.
348+
#
349+
# We cannot directly add the to active_profiles set
350+
# in `start_profiling` because it is called from other
351+
# threads which can cause a RuntimeError when it the
352+
# set sizes changes during iteration without a lock.
353+
#
354+
# We also want to avoid using a lock here so threads
355+
# that are starting profiles are not blocked until it
356+
# can acquire the lock.
357+
for _ in range(new_profiles):
358+
self.active_profiles.add(self.new_profiles.popleft())
359+
inactive_profiles = []
360+
361+
for profile in self.active_profiles:
362+
if profile.active:
363+
pass
364+
else:
365+
# If a profile is marked inactive, we buffer it
366+
# to `inactive_profiles` so it can be removed.
367+
# We cannot remove it here as it would result
368+
# in a RuntimeError.
369+
inactive_profiles.append(profile)
370+
371+
for profile in inactive_profiles:
372+
self.active_profiles.remove(profile)
349373

350374
if self.buffer is None:
351375
self.reset_buffer()

sentry_sdk/profiler/transaction_profiler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -644,7 +644,7 @@ def _sample_stack(*args, **kwargs):
644644
if profile.active:
645645
profile.write(now, sample)
646646
else:
647-
# If a thread is marked inactive, we buffer it
647+
# If a profile is marked inactive, we buffer it
648648
# to `inactive_profiles` so it can be removed.
649649
# We cannot remove it here as it would result
650650
# in a RuntimeError.

sentry_sdk/scope.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from sentry_sdk.profiler.continuous_profiler import (
1616
get_profiler_id,
1717
try_autostart_continuous_profiler,
18-
try_profile_lifecycle_auto_start,
18+
try_profile_lifecycle_trace_start,
1919
)
2020
from sentry_sdk.profiler.transaction_profiler import Profile
2121
from sentry_sdk.session import Session
@@ -1055,12 +1055,12 @@ def start_transaction(
10551055

10561056
transaction._profile = profile
10571057

1058-
transaction._started_profile_lifecycle = try_profile_lifecycle_auto_start()
1058+
transaction._continuous_profile = try_profile_lifecycle_trace_start()
10591059

10601060
# Typically, the profiler is set when the transaction is created. But when
10611061
# using the auto lifecycle, the profiler isn't running when the first
10621062
# transaction is started. So make sure we update the profiler id on it.
1063-
if transaction._started_profile_lifecycle:
1063+
if transaction._continuous_profile is not None:
10641064
transaction.set_profiler_id(get_profiler_id())
10651065

10661066
# we don't bother to keep spans if we already know we're not going to

sentry_sdk/tracing.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55

66
import sentry_sdk
77
from sentry_sdk.consts import INSTRUMENTER, SPANSTATUS, SPANDATA
8-
from sentry_sdk.profiler.continuous_profiler import (
9-
get_profiler_id,
10-
try_profile_lifecycle_auto_stop,
11-
)
8+
from sentry_sdk.profiler.continuous_profiler import get_profiler_id
129
from sentry_sdk.utils import (
1310
get_current_thread_meta,
1411
is_valid_sample_rate,
@@ -37,7 +34,8 @@
3734
P = ParamSpec("P")
3835
R = TypeVar("R")
3936

40-
import sentry_sdk.profiler
37+
from sentry_sdk.profiler.continuous_profiler import ContinuousProfile
38+
from sentry_sdk.profiler.transaction_profiler import Profile
4139
from sentry_sdk._types import (
4240
Event,
4341
MeasurementUnit,
@@ -272,7 +270,6 @@ class Span:
272270
"scope",
273271
"origin",
274272
"name",
275-
"_started_profile_lifecycle",
276273
)
277274

278275
def __init__(
@@ -771,6 +768,7 @@ class Transaction(Span):
771768
"_measurements",
772769
"_contexts",
773770
"_profile",
771+
"_continuous_profile",
774772
"_baggage",
775773
)
776774

@@ -792,10 +790,8 @@ def __init__( # type: ignore[misc]
792790
self.parent_sampled = parent_sampled
793791
self._measurements = {} # type: Dict[str, MeasurementValue]
794792
self._contexts = {} # type: Dict[str, Any]
795-
self._profile = (
796-
None
797-
) # type: Optional[sentry_sdk.profiler.transaction_profiler.Profile]
798-
self._started_profile_lifecycle = False # type: bool
793+
self._profile = None # type: Optional[Profile]
794+
self._continuous_profile = None # type: Optional[ContinuousProfile]
799795
self._baggage = baggage
800796

801797
def __repr__(self):
@@ -848,8 +844,8 @@ def __exit__(self, ty, value, tb):
848844
if self._profile is not None:
849845
self._profile.__exit__(ty, value, tb)
850846

851-
if self._started_profile_lifecycle:
852-
try_profile_lifecycle_auto_stop()
847+
if self._continuous_profile is not None:
848+
self._continuous_profile.stop()
853849

854850
super().__exit__(ty, value, tb)
855851

tests/profiler/test_continuous_profiler.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,17 @@ def test_continuous_profiler_setup_twice(mode, make_options, teardown_profiling)
126126
)
127127

128128

129-
def assert_single_transaction_with_profile_chunks(envelopes, thread):
129+
def assert_single_transaction_with_profile_chunks(
130+
envelopes, thread, max_chunks, transactions=1
131+
):
130132
items = defaultdict(list)
131133
for envelope in envelopes:
132134
for item in envelope.items:
133135
items[item.type].append(item)
134136

135-
assert len(items["transaction"]) == 1
137+
assert len(items["transaction"]) == transactions
136138
assert len(items["profile_chunk"]) > 0
139+
assert len(items["profile_chunk"]) <= max_chunks
137140

138141
transaction = items["transaction"][0].payload.json
139142

@@ -231,7 +234,7 @@ def test_continuous_profiler_auto_start_and_manual_stop(
231234
with sentry_sdk.start_span(op="op"):
232235
time.sleep(0.05)
233236

234-
assert_single_transaction_with_profile_chunks(envelopes, thread)
237+
assert_single_transaction_with_profile_chunks(envelopes, thread, max_chunks=10)
235238

236239
for _ in range(3):
237240
stop_profiler()
@@ -252,7 +255,7 @@ def test_continuous_profiler_auto_start_and_manual_stop(
252255
with sentry_sdk.start_span(op="op"):
253256
time.sleep(0.05)
254257

255-
assert_single_transaction_with_profile_chunks(envelopes, thread)
258+
assert_single_transaction_with_profile_chunks(envelopes, thread, max_chunks=10)
256259

257260

258261
@pytest.mark.parametrize(
@@ -298,7 +301,7 @@ def test_continuous_profiler_manual_start_and_stop_sampled(
298301
with sentry_sdk.start_span(op="op"):
299302
time.sleep(0.05)
300303

301-
assert_single_transaction_with_profile_chunks(envelopes, thread)
304+
assert_single_transaction_with_profile_chunks(envelopes, thread, max_chunks=10)
302305

303306
stop_profiler()
304307

@@ -367,15 +370,17 @@ def test_continuous_profiler_manual_start_and_stop_unsampled(
367370
pytest.param(get_client_options(False), id="experiment"),
368371
],
369372
)
370-
@mock.patch("sentry_sdk.profiler.continuous_profiler.PROFILE_BUFFER_SECONDS", 0.01)
373+
@mock.patch("sentry_sdk.profiler.continuous_profiler.PROFILE_BUFFER_SECONDS", 0.1)
371374
def test_continuous_profiler_auto_start_and_stop_sampled(
372375
sentry_init,
373376
capture_envelopes,
374377
mode,
375378
make_options,
376379
teardown_profiling,
377380
):
378-
options = make_options(mode=mode, profile_session_sample_rate=1.0, lifecycle="auto")
381+
options = make_options(
382+
mode=mode, profile_session_sample_rate=1.0, lifecycle="trace"
383+
)
379384
sentry_init(
380385
traces_sample_rate=1.0,
381386
**options,
@@ -391,13 +396,26 @@ def test_continuous_profiler_auto_start_and_stop_sampled(
391396
with sentry_sdk.start_transaction(name="profiling"):
392397
assert get_profiler_id() is not None, "profiler should be running"
393398
with sentry_sdk.start_span(op="op"):
394-
time.sleep(0.05)
399+
time.sleep(0.03)
400+
assert get_profiler_id() is not None, "profiler should be running"
401+
402+
# the profiler takes a while to stop so if we start a transaction
403+
# immediately, it'll be part of the same chunk
404+
assert get_profiler_id() is not None, "profiler should be running"
405+
406+
with sentry_sdk.start_transaction(name="profiling"):
407+
assert get_profiler_id() is not None, "profiler should be running"
408+
with sentry_sdk.start_span(op="op"):
409+
time.sleep(0.03)
395410
assert get_profiler_id() is not None, "profiler should be running"
396411

397412
# wait at least 1 cycle for the profiler to stop
398413
time.sleep(0.05)
399414
assert get_profiler_id() is None, "profiler should not be running"
400-
assert_single_transaction_with_profile_chunks(envelopes, thread)
415+
416+
assert_single_transaction_with_profile_chunks(
417+
envelopes, thread, max_chunks=1, transactions=2
418+
)
401419

402420

403421
@pytest.mark.parametrize(
@@ -422,7 +440,9 @@ def test_continuous_profiler_auto_start_and_stop_unsampled(
422440
make_options,
423441
teardown_profiling,
424442
):
425-
options = make_options(mode=mode, profile_session_sample_rate=0.0, lifecycle="auto")
443+
options = make_options(
444+
mode=mode, profile_session_sample_rate=0.0, lifecycle="trace"
445+
)
426446
sentry_init(
427447
traces_sample_rate=1.0,
428448
**options,

0 commit comments

Comments
 (0)