Skip to content

Commit 9d28979

Browse files
feat(profiling): profile asyncio.Semaphore primitives with Python Lock profiler
1 parent ca3c521 commit 9d28979

File tree

5 files changed

+125
-47
lines changed

5 files changed

+125
-47
lines changed
Lines changed: 14 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
1-
from asyncio.locks import Lock
2-
import typing
1+
import asyncio
32

4-
from .. import collector
53
from . import _lock
64

75

86
class _ProfiledAsyncioLock(_lock._ProfiledLock):
97
pass
108

119

10+
class _ProfiledAsyncioSemaphore(_lock._ProfiledLock):
11+
pass
12+
13+
1214
class AsyncioLockCollector(_lock.LockCollector):
1315
"""Record asyncio.Lock usage."""
1416

1517
PROFILED_LOCK_CLASS = _ProfiledAsyncioLock
18+
MODULE = asyncio
19+
PATCHED_LOCK_NAME = "Lock"
20+
21+
22+
class AsyncioSemaphoreCollector(_lock.LockCollector):
23+
"""Record asyncio.Semaphore usage."""
1624

17-
def _start_service(self) -> None:
18-
"""Start collecting lock usage."""
19-
try:
20-
import asyncio
21-
except ImportError as e:
22-
raise collector.CollectorUnavailable(e)
23-
self._asyncio_module = asyncio
24-
return super(AsyncioLockCollector, self)._start_service()
25-
26-
def _get_patch_target(self) -> typing.Type[Lock]:
27-
return self._asyncio_module.Lock
28-
29-
def _set_patch_target(
30-
self,
31-
value: typing.Any,
32-
) -> None:
33-
self._asyncio_module.Lock = value # type: ignore[misc]
25+
PROFILED_LOCK_CLASS = _ProfiledAsyncioSemaphore
26+
MODULE = asyncio
27+
PATCHED_LOCK_NAME = "Semaphore"

ddtrace/profiling/profiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ def start_collector(collector_class: Type[collector.Collector]) -> None:
218218
("threading", lambda _: start_collector(threading.ThreadingSemaphoreCollector)),
219219
("threading", lambda _: start_collector(threading.ThreadingBoundedSemaphoreCollector)),
220220
("asyncio", lambda _: start_collector(asyncio.AsyncioLockCollector)),
221+
("asyncio", lambda _: start_collector(asyncio.AsyncioSemaphoreCollector)),
221222
]
222223

223224
for module, hook in self._collectors_on_import:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
features:
3+
- |
4+
profiling: Add support for ``asyncio.Semaphore`` locking type profiling in Python.
5+
Also refactored asyncio collectors to use base class attributes (``MODULE`` and ``PATCHED_LOCK_NAME``)
6+
for consistency with the threading module collectors.
7+

tests/profiling/collector/test_asyncio.py

Lines changed: 102 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
import glob
44
import os
55
import sys
6+
from typing import Type
7+
from typing import Union
68
import uuid
79

810
import pytest
911

1012
from ddtrace import ext
1113
from ddtrace.internal.datadog.profiling import ddup
12-
from ddtrace.profiling.collector import asyncio as collector_asyncio
14+
from ddtrace.profiling.collector.asyncio import AsyncioLockCollector
15+
from ddtrace.profiling.collector.asyncio import AsyncioSemaphoreCollector
1316
from tests.profiling.collector import pprof_utils
1417
from tests.profiling.collector import test_collector
1518
from tests.profiling.collector.lock_utils import get_lock_linenos
@@ -20,16 +23,58 @@
2023

2124
PY_311_OR_ABOVE = sys.version_info[:2] >= (3, 11)
2225

23-
24-
def test_repr():
25-
test_collector._test_repr(
26-
collector_asyncio.AsyncioLockCollector,
27-
"AsyncioLockCollector(status=<ServiceStatus.STOPPED: 'stopped'>, capture_pct=1.0, nframes=64, tracer=None)", # noqa: E501
28-
)
26+
# Type aliases for collector and lock types
27+
CollectorType = Union[
28+
Type[AsyncioLockCollector],
29+
Type[AsyncioSemaphoreCollector],
30+
]
31+
LockType = Union[Type[asyncio.Lock], Type[asyncio.Semaphore]]
32+
33+
34+
@pytest.mark.parametrize(
35+
"collector_class,expected_repr",
36+
[
37+
(
38+
AsyncioLockCollector,
39+
"AsyncioLockCollector(status=<ServiceStatus.STOPPED: 'stopped'>, capture_pct=1.0, nframes=64, tracer=None)",
40+
),
41+
(
42+
AsyncioSemaphoreCollector,
43+
"AsyncioSemaphoreCollector(status=<ServiceStatus.STOPPED: 'stopped'>, capture_pct=1.0, nframes=64, tracer=None)", # noqa: E501
44+
),
45+
],
46+
)
47+
def test_collector_repr(collector_class: CollectorType, expected_repr: str) -> None:
48+
test_collector._test_repr(collector_class, expected_repr)
2949

3050

3151
@pytest.mark.asyncio
32-
class TestAsyncioLockCollector:
52+
class BaseAsyncioLockCollectorTest:
53+
"""Base test class for asyncio lock collectors.
54+
55+
Child classes must implement:
56+
- collector_class: The collector class to test
57+
- lock_class: The asyncio lock class to test
58+
- lock_init_args: Arguments to pass to lock constructor (default: ())
59+
"""
60+
61+
@property
62+
def collector_class(self) -> CollectorType:
63+
raise NotImplementedError("Child classes must implement collector_class")
64+
65+
@property
66+
def lock_class(self) -> LockType:
67+
raise NotImplementedError("Child classes must implement lock_class")
68+
69+
@property
70+
def lock_init_args(self) -> tuple:
71+
"""Arguments to pass to lock constructor. Override for Semaphore-like locks."""
72+
return ()
73+
74+
def create_lock(self) -> Union[asyncio.Lock, asyncio.Semaphore]:
75+
"""Create a lock instance with the appropriate arguments."""
76+
return self.lock_class(*self.lock_init_args)
77+
3378
def setup_method(self, method):
3479
self.test_name = method.__qualname__ if PY_311_OR_ABOVE else method.__name__
3580
self.output_prefix = "/tmp" + os.sep + self.test_name
@@ -51,16 +96,17 @@ def teardown_method(self):
5196
except Exception as e:
5297
print("Error while deleting file: ", e)
5398

54-
async def test_asyncio_lock_events(self):
55-
with collector_asyncio.AsyncioLockCollector(capture_pct=100):
56-
lock = asyncio.Lock() # !CREATE! test_asyncio_lock_events
57-
await lock.acquire() # !ACQUIRE! test_asyncio_lock_events
99+
async def test_lock_events(self):
100+
"""Test basic acquire/release event profiling."""
101+
with self.collector_class(capture_pct=100):
102+
lock = self.create_lock() # !CREATE! test_lock_events
103+
await lock.acquire() # !ACQUIRE! test_lock_events
58104
assert lock.locked()
59-
lock.release() # !RELEASE! test_asyncio_lock_events
105+
lock.release() # !RELEASE! test_lock_events
60106

61107
ddup.upload()
62108

63-
linenos = get_lock_linenos("test_asyncio_lock_events")
109+
linenos = get_lock_linenos("test_lock_events")
64110
profile = pprof_utils.parse_newest_profile(self.output_filename)
65111
expected_thread_id = _thread.get_ident()
66112
pprof_utils.assert_lock_events(
@@ -85,29 +131,30 @@ async def test_asyncio_lock_events(self):
85131
],
86132
)
87133

88-
async def test_asyncio_lock_events_tracer(self, tracer):
134+
async def test_lock_events_tracer(self, tracer):
135+
"""Test event profiling with tracer integration."""
89136
tracer._endpoint_call_counter_span_processor.enable()
90137
resource = str(uuid.uuid4())
91138
span_type = ext.SpanTypes.WEB
92139

93-
with collector_asyncio.AsyncioLockCollector(capture_pct=100, tracer=tracer):
94-
lock = asyncio.Lock() # !CREATE! test_asyncio_lock_events_tracer_1
95-
await lock.acquire() # !ACQUIRE! test_asyncio_lock_events_tracer_1
140+
with self.collector_class(capture_pct=100, tracer=tracer):
141+
lock = self.create_lock() # !CREATE! test_lock_events_tracer_1
142+
await lock.acquire() # !ACQUIRE! test_lock_events_tracer_1
96143
with tracer.trace("test", resource=resource, span_type=span_type) as t:
97-
lock2 = asyncio.Lock() # !CREATE! test_asyncio_lock_events_tracer_2
98-
await lock2.acquire() # !ACQUIRE! test_asyncio_lock_events_tracer_2
99-
lock.release() # !RELEASE! test_asyncio_lock_events_tracer_1
144+
lock2 = self.create_lock() # !CREATE! test_lock_events_tracer_2
145+
await lock2.acquire() # !ACQUIRE! test_lock_events_tracer_2
146+
lock.release() # !RELEASE! test_lock_events_tracer_1
100147
span_id = t.span_id
101-
lock2.release() # !RELEASE! test_asyncio_lock_events_tracer_2
148+
lock2.release() # !RELEASE! test_lock_events_tracer_2
102149

103-
lock_ctx = asyncio.Lock() # !CREATE! test_asyncio_lock_events_tracer_3
104-
async with lock_ctx: # !ACQUIRE! !RELEASE! test_asyncio_lock_events_tracer_3
150+
lock_ctx = self.create_lock() # !CREATE! test_lock_events_tracer_3
151+
async with lock_ctx: # !ACQUIRE! !RELEASE! test_lock_events_tracer_3
105152
pass
106153
ddup.upload(tracer=tracer)
107154

108-
linenos_1 = get_lock_linenos("test_asyncio_lock_events_tracer_1")
109-
linenos_2 = get_lock_linenos("test_asyncio_lock_events_tracer_2")
110-
linenos_3 = get_lock_linenos("test_asyncio_lock_events_tracer_3", with_stmt=True)
155+
linenos_1 = get_lock_linenos("test_lock_events_tracer_1")
156+
linenos_2 = get_lock_linenos("test_lock_events_tracer_2")
157+
linenos_3 = get_lock_linenos("test_lock_events_tracer_3", with_stmt=True)
111158

112159
profile = pprof_utils.parse_newest_profile(self.output_filename)
113160
expected_thread_id = _thread.get_ident()
@@ -167,3 +214,31 @@ async def test_asyncio_lock_events_tracer(self, tracer):
167214
),
168215
],
169216
)
217+
218+
219+
class TestAsyncioLockCollector(BaseAsyncioLockCollectorTest):
220+
"""Test asyncio.Lock profiling."""
221+
222+
@property
223+
def collector_class(self):
224+
return AsyncioLockCollector
225+
226+
@property
227+
def lock_class(self):
228+
return asyncio.Lock
229+
230+
231+
class TestAsyncioSemaphoreCollector(BaseAsyncioLockCollectorTest):
232+
"""Test asyncio.Semaphore profiling."""
233+
234+
@property
235+
def collector_class(self):
236+
return AsyncioSemaphoreCollector
237+
238+
@property
239+
def lock_class(self):
240+
return asyncio.Semaphore
241+
242+
@property
243+
def lock_init_args(self):
244+
return (2,) # Initial semaphore value

tests/profiling/test_profiler.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ def test_default_collectors():
138138
pass
139139
else:
140140
assert any(isinstance(c, asyncio.AsyncioLockCollector) for c in p._profiler._collectors)
141+
assert any(isinstance(c, asyncio.AsyncioSemaphoreCollector) for c in p._profiler._collectors)
141142
p.stop(flush=False)
142143

143144

0 commit comments

Comments
 (0)