Skip to content

Commit 0373deb

Browse files
Julien DanjouKyle-Verhoogbrettlangdon
authored
feat(profiling/gevent): supports tracking tasks in gevent < 1.3 (#2929)
This implements a greenlet tracer similar to the one that gevent 1.3 ships. It is actually simpler than the one gevent provides as we don't need as many features, so it should be _slightly_ faster. This makes sure we can provide the same amazing experience whatever the gevent version being used might be. Co-authored-by: Kyle Verhoog <[email protected]> Co-authored-by: Brett Langdon <[email protected]>
1 parent 638d46b commit 0373deb

File tree

4 files changed

+82
-65
lines changed

4 files changed

+82
-65
lines changed

ddtrace/profiling/collector/_task.pyx

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,29 @@ from . import _threading
55

66

77
try:
8-
import gevent._tracer
8+
from greenlet import settrace, getcurrent
99
import gevent.thread
10+
import gevent.hub
1011
except ImportError:
1112
_gevent_tracer = None
1213
else:
13-
class DDGreenletTracer(gevent._tracer.GreenletTracer):
14-
def _trace(self, event, args):
15-
# Do not trace gevent Hub: the Hub is a greenlet but we want to know the latest active greenlet *before*
16-
# the application yielded back to the Hub. There's no point showing the Hub most of the time to the users as
17-
# that does not give any information about user code.
18-
if not isinstance(args[1], gevent.hub.Hub):
19-
gevent._tracer.GreenletTracer._trace(self, event, args)
14+
15+
class DDGreenletTracer(object):
16+
def __init__(self):
17+
# type: (...) -> None
18+
self.active_greenlet = getcurrent()
19+
self.previous_trace_function = settrace(self)
20+
21+
def __call__(self, event, args):
22+
if event in ('switch', 'throw'):
23+
# Do not trace gevent Hub: the Hub is a greenlet but we want to know the latest active greenlet *before*
24+
# the application yielded back to the Hub. There's no point showing the Hub most of the time to the
25+
# users as that does not give any information about user code.
26+
if not isinstance(args[1], gevent.hub.Hub):
27+
self.active_greenlet = args[1]
28+
29+
if self.previous_trace_function is not None:
30+
self.previous_trace_function(event, args)
2031

2132
# NOTE: bold assumption: this module is always imported by the MainThread.
2233
# A GreenletTracer is local to the thread instantiating it and we assume this is run by the MainThread.
@@ -28,16 +39,10 @@ cpdef get_task(thread_id):
2839
# gevent greenlet support:
2940
# we only support tracing tasks in the greenlets are run in the MainThread.
3041
if thread_id == nogevent.main_thread_id and _gevent_tracer is not None:
31-
if _gevent_tracer.active_greenlet is None:
32-
# That means gevent never switch to another greenlet, we're still in the main one
33-
task_id = compat.main_thread.ident
34-
frame = None
35-
else:
36-
task_id = gevent.thread.get_ident(_gevent_tracer.active_greenlet)
37-
frame = _gevent_tracer.active_greenlet.gr_frame
38-
42+
task_id = gevent.thread.get_ident(_gevent_tracer.active_greenlet)
3943
# Greenlets might be started as Thread in gevent
4044
task_name = _threading.get_thread_name(task_id)
45+
frame = _gevent_tracer.active_greenlet.gr_frame
4146
else:
4247
task_id = None
4348
task_name = None
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
features:
3+
- |
4+
Profiling now supports tracing greenlets with gevent version prior to 1.3.

tests/profiling/collector/test_stack.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from ddtrace.profiling import event as event_mod
1616
from ddtrace.profiling import profiler
1717
from ddtrace.profiling import recorder
18-
from ddtrace.profiling.collector import _task
1918
from ddtrace.profiling.collector import _threading
2019
from ddtrace.profiling.collector import stack
2120

@@ -70,7 +69,7 @@ def test_collect_once():
7069
stack_events = all_events[0]
7170
for e in stack_events:
7271
if e.thread_name == "MainThread":
73-
if TESTING_GEVENT and _task._gevent_tracer is not None:
72+
if TESTING_GEVENT:
7473
assert e.task_id > 0
7574
assert e.task_name == e.thread_name
7675
else:
@@ -95,7 +94,7 @@ def _fib(n):
9594
return _fib(n - 1) + _fib(n - 2)
9695

9796

98-
@pytest.mark.skipif(_task._gevent_tracer is None, reason="gevent tasks not supported")
97+
@pytest.mark.skipif(not TESTING_GEVENT, reason="Not testing gevent")
9998
def test_collect_gevent_thread_task():
10099
r = recorder.Recorder()
101100
s = stack.StackCollector(r)
@@ -163,7 +162,7 @@ def collect(self):
163162
return []
164163

165164

166-
@pytest.mark.skipif(_task._gevent_tracer is None, reason="gevent tasks not supported")
165+
@pytest.mark.skipif(not TESTING_GEVENT, reason="Not testing gevent")
167166
@pytest.mark.parametrize("ignore", (True, False))
168167
def test_ignore_profiler_gevent_task(monkeypatch, ignore):
169168
monkeypatch.setenv("DD_PROFILING_API_TIMEOUT", "0.1")
@@ -329,7 +328,7 @@ def test_exception_collection():
329328
assert e.sampling_period > 0
330329
assert e.thread_id == nogevent.thread_get_ident()
331330
assert e.thread_name == "MainThread"
332-
assert e.frames == [(__file__, 323, "test_exception_collection")]
331+
assert e.frames == [(__file__, 322, "test_exception_collection")]
333332
assert e.nframes == 1
334333
assert e.exc_type == ValueError
335334

@@ -352,7 +351,7 @@ def test_exception_collection_trace(tracer):
352351
assert e.sampling_period > 0
353352
assert e.thread_id == nogevent.thread_get_ident()
354353
assert e.thread_name == "MainThread"
355-
assert e.frames == [(__file__, 346, "test_exception_collection_trace")]
354+
assert e.frames == [(__file__, 345, "test_exception_collection_trace")]
356355
assert e.nframes == 1
357356
assert e.exc_type == ValueError
358357
assert e.span_id == span.span_id
@@ -597,7 +596,7 @@ def test_thread_time_cache():
597596
)
598597

599598

600-
@pytest.mark.skipif(_task._gevent_tracer is None, reason="gevent tasks not supported")
599+
@pytest.mark.skipif(not TESTING_GEVENT, reason="Not testing gevent")
601600
def test_collect_gevent_thread_hub():
602601
# type: (...) -> None
603602
r = recorder.Recorder()

tests/profiling/collector/test_threading.py

Lines changed: 51 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
1+
import os
12
import threading
23
import uuid
34

45
import pytest
56

67
from ddtrace.internal import nogevent
78
from ddtrace.profiling import recorder
8-
from ddtrace.profiling.collector import _task
99
from ddtrace.profiling.collector import threading as collector_threading
1010

1111
from . import test_collector
1212

1313

14+
TESTING_GEVENT = os.getenv("DD_PROFILE_TEST_GEVENT", False)
15+
16+
1417
def test_repr():
1518
test_collector._test_repr(
1619
collector_threading.LockCollector,
@@ -63,13 +66,13 @@ def test_lock_acquire_events():
6366
assert len(r.events[collector_threading.LockAcquireEvent]) == 1
6467
assert len(r.events[collector_threading.LockReleaseEvent]) == 0
6568
event = r.events[collector_threading.LockAcquireEvent][0]
66-
assert event.lock_name == "test_threading.py:61"
69+
assert event.lock_name == "test_threading.py:64"
6770
assert event.thread_id == nogevent.thread_get_ident()
6871
assert event.wait_time_ns > 0
6972
# It's called through pytest so I'm sure it's gonna be that long, right?
7073
assert len(event.frames) > 3
7174
assert event.nframes > 3
72-
assert event.frames[0] == (__file__, 62, "test_lock_acquire_events")
75+
assert event.frames[0] == (__file__, 65, "test_lock_acquire_events")
7376
assert event.sampling_pct == 100
7477

7578

@@ -90,14 +93,14 @@ def test_lock_events_tracer(tracer):
9093
events = r.reset()
9194
# The tracer might use locks, so we need to look into every event to assert we got ours
9295
for event_type in (collector_threading.LockAcquireEvent, collector_threading.LockReleaseEvent):
93-
assert {"test_threading.py:81", "test_threading.py:84"}.issubset({e.lock_name for e in events[event_type]})
96+
assert {"test_threading.py:84", "test_threading.py:87"}.issubset({e.lock_name for e in events[event_type]})
9497
for event in events[event_type]:
95-
if event.name == "test_threading.py:81":
98+
if event.name == "test_threading.py:84":
9699
assert event.trace_id is None
97100
assert event.span_id is None
98101
assert event.trace_resource_container is None
99102
assert event.trace_type is None
100-
elif event.name == "test_threading.py:84":
103+
elif event.name == "test_threading.py:87":
101104
assert event.trace_id == trace_id
102105
assert event.span_id == span_id
103106
assert event.trace_resource_container[0] == t.resource
@@ -123,14 +126,14 @@ def test_lock_events_tracer_late_finish(tracer):
123126
events = r.reset()
124127
# The tracer might use locks, so we need to look into every event to assert we got ours
125128
for event_type in (collector_threading.LockAcquireEvent, collector_threading.LockReleaseEvent):
126-
assert {"test_threading.py:112", "test_threading.py:115"}.issubset({e.lock_name for e in events[event_type]})
129+
assert {"test_threading.py:115", "test_threading.py:118"}.issubset({e.lock_name for e in events[event_type]})
127130
for event in events[event_type]:
128-
if event.name == "test_threading.py:112":
131+
if event.name == "test_threading.py:115":
129132
assert event.trace_id is None
130133
assert event.span_id is None
131134
assert event.trace_resource_container is None
132135
assert event.trace_type is None
133-
elif event.name == "test_threading.py:115":
136+
elif event.name == "test_threading.py:118":
134137
assert event.trace_id == trace_id
135138
assert event.span_id == span_id
136139
assert event.trace_resource_container[0] == span.resource
@@ -155,14 +158,14 @@ def test_resource_not_collected(monkeypatch, tracer):
155158
events = r.reset()
156159
# The tracer might use locks, so we need to look into every event to assert we got ours
157160
for event_type in (collector_threading.LockAcquireEvent, collector_threading.LockReleaseEvent):
158-
assert {"test_threading.py:146", "test_threading.py:149"}.issubset({e.lock_name for e in events[event_type]})
161+
assert {"test_threading.py:149", "test_threading.py:152"}.issubset({e.lock_name for e in events[event_type]})
159162
for event in events[event_type]:
160-
if event.name == "test_threading.py:146":
163+
if event.name == "test_threading.py:149":
161164
assert event.trace_id is None
162165
assert event.span_id is None
163166
assert event.trace_resource_container is None
164167
assert event.trace_type is None
165-
elif event.name == "test_threading.py:149":
168+
elif event.name == "test_threading.py:152":
166169
assert event.trace_id == trace_id
167170
assert event.span_id == span_id
168171
assert event.trace_resource_container is None
@@ -178,17 +181,17 @@ def test_lock_release_events():
178181
assert len(r.events[collector_threading.LockAcquireEvent]) == 1
179182
assert len(r.events[collector_threading.LockReleaseEvent]) == 1
180183
event = r.events[collector_threading.LockReleaseEvent][0]
181-
assert event.lock_name == "test_threading.py:175"
184+
assert event.lock_name == "test_threading.py:178"
182185
assert event.thread_id == nogevent.thread_get_ident()
183186
assert event.locked_for_ns >= 0.1
184187
# It's called through pytest so I'm sure it's gonna be that long, right?
185188
assert len(event.frames) > 3
186189
assert event.nframes > 3
187-
assert event.frames[0] == (__file__, 177, "test_lock_release_events")
190+
assert event.frames[0] == (__file__, 180, "test_lock_release_events")
188191
assert event.sampling_pct == 100
189192

190193

191-
@pytest.mark.skipif(_task._gevent_tracer is None, reason="gevent tasks not supported")
194+
@pytest.mark.skipif(not TESTING_GEVENT, reason="Not testing gevent")
192195
def test_lock_gevent_tasks():
193196
r = recorder.Recorder()
194197

@@ -205,33 +208,39 @@ def play_with_lock():
205208
assert len(r.events[collector_threading.LockAcquireEvent]) >= 1
206209
assert len(r.events[collector_threading.LockReleaseEvent]) >= 1
207210

208-
event = r.events[collector_threading.LockAcquireEvent][0]
209-
assert event.lock_name == "test_threading.py:196"
210-
assert event.thread_id == nogevent.main_thread_id
211-
assert event.wait_time_ns >= 0
212-
assert event.task_id == t.ident
213-
assert event.task_name == "foobar"
214-
# It's called through pytest so I'm sure it's gonna be that long, right?
215-
assert len(event.frames) > 3
216-
assert event.nframes > 3
217-
assert event.frames[0] == (__file__, 197, "play_with_lock")
218-
assert event.sampling_pct == 100
219-
assert event.task_id == t.ident
220-
assert event.task_name == "foobar"
221-
222-
event = r.events[collector_threading.LockReleaseEvent][0]
223-
assert event.lock_name == "test_threading.py:196"
224-
assert event.thread_id == nogevent.main_thread_id
225-
assert event.locked_for_ns >= 0.1
226-
assert event.task_id == t.ident
227-
assert event.task_name == "foobar"
228-
# It's called through pytest so I'm sure it's gonna be that long, right?
229-
assert len(event.frames) > 3
230-
assert event.nframes > 3
231-
assert event.frames[0] == (__file__, 198, "play_with_lock")
232-
assert event.sampling_pct == 100
233-
assert event.task_id == t.ident
234-
assert event.task_name == "foobar"
211+
for event in r.events[collector_threading.LockAcquireEvent]:
212+
if event.lock_name == "test_threading.py:199":
213+
assert event.thread_id == nogevent.main_thread_id
214+
assert event.wait_time_ns >= 0
215+
assert event.task_id == t.ident
216+
assert event.task_name == "foobar"
217+
# It's called through pytest so I'm sure it's gonna be that long, right?
218+
assert len(event.frames) > 3
219+
assert event.nframes > 3
220+
assert event.frames[0] == (__file__, 200, "play_with_lock")
221+
assert event.sampling_pct == 100
222+
assert event.task_id == t.ident
223+
assert event.task_name == "foobar"
224+
break
225+
else:
226+
pytest.fail("Lock event not found")
227+
228+
for event in r.events[collector_threading.LockReleaseEvent]:
229+
if event.lock_name == "test_threading.py:199":
230+
assert event.thread_id == nogevent.main_thread_id
231+
assert event.locked_for_ns >= 0.1
232+
assert event.task_id == t.ident
233+
assert event.task_name == "foobar"
234+
# It's called through pytest so I'm sure it's gonna be that long, right?
235+
assert len(event.frames) > 3
236+
assert event.nframes > 3
237+
assert event.frames[0] == (__file__, 201, "play_with_lock")
238+
assert event.sampling_pct == 100
239+
assert event.task_id == t.ident
240+
assert event.task_name == "foobar"
241+
break
242+
else:
243+
pytest.fail("Lock event not found")
235244

236245

237246
@pytest.mark.benchmark(

0 commit comments

Comments
 (0)