Skip to content

Commit 2391f79

Browse files
test(profiling): add tests for asyncio.wait and asyncio.gather
1 parent 9592cd4 commit 2391f79

File tree

4 files changed

+321
-262
lines changed

4 files changed

+321
-262
lines changed

tests/profiling/collector/pprof_utils.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ def __init__(self, locations: Optional[Any] = None, exception_type=None, *args,
109109
self.exception_type = exception_type
110110
super().__init__(*args, **kwargs)
111111

112+
def __str__(self):
113+
return f"StackEvent(locations={self.locations}, exception_type={self.exception_type})"
114+
112115

113116
# A simple class to hold the expected attributes of a lock sample.
114117
class LockEvent(EventBaseClass):
@@ -297,15 +300,17 @@ def assert_lock_event(profile: pprof_pb2.Profile, sample: pprof_pb2.Sample, expe
297300
assert_base_event(profile.string_table, sample, expected_event)
298301

299302

300-
def assert_sample_has_locations(profile, sample, expected_locations: Optional[List[StackLocation]]):
303+
def assert_sample_has_locations(
304+
profile: pprof_pb2.Profile, sample: pprof_pb2.Sample, expected_locations: Optional[Sequence[StackLocation]]
305+
) -> None:
301306
if not expected_locations:
302307
return
303308

304309
expected_locations_idx = 0
305310
checked = False
306311

307312
# For debug printing
308-
sample_loc_strs = []
313+
sample_loc_strs: list[str] = []
309314
# in a sample there can be multiple locations, we need to check
310315
# whether there's a consecutive subsequence of locations that match
311316
# the expected locations
@@ -354,38 +359,6 @@ def assert_profile_has_sample(
354359
samples: List[pprof_pb2.Sample],
355360
expected_sample: StackEvent,
356361
):
357-
# Print all samples with line number + function name + labels
358-
print(f"\n=== Printing all {len(samples)} samples ===")
359-
for i, sample in enumerate(samples):
360-
print(f"\nSample {i}:")
361-
362-
# Print locations (stack trace)
363-
print(" Stack trace:")
364-
for j, location_id in enumerate(sample.location_id):
365-
location = get_location_with_id(profile, location_id)
366-
if location.line:
367-
line = location.line[0]
368-
function = get_function_with_id(profile, line.function_id)
369-
function_name = profile.string_table[function.name]
370-
filename = profile.string_table[function.filename]
371-
print(f" [{j}] {filename}:{line.line} in {function_name}()")
372-
373-
# Print labels
374-
print(" Labels:")
375-
for label in sample.label:
376-
key_str = profile.string_table[label.key]
377-
if label.str:
378-
value_str = profile.string_table[label.str]
379-
print(f" {key_str}: {value_str}")
380-
elif label.num:
381-
print(f" {key_str}: {label.num}")
382-
383-
# Print values
384-
if sample.value:
385-
print(f" Values: {sample.value}")
386-
387-
print("=== End of samples ===\n")
388-
389362
found = False
390363
for sample in samples:
391364
try:
@@ -397,4 +370,4 @@ def assert_profile_has_sample(
397370
if DEBUG_TEST:
398371
print(e)
399372

400-
assert found, "Expected samples not found in profile"
373+
assert found, f"Expected samples not found in profile, {str(expected_sample)}"
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import pytest
2+
3+
4+
@pytest.mark.subprocess(
5+
env=dict(
6+
DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_utils_gather",
7+
),
8+
err=None,
9+
)
10+
# For macOS: err=None ignores expected stderr from tracer failing to connect to agent (not relevant to this test)
11+
def test_asyncio_gather() -> None:
12+
import asyncio
13+
import os
14+
import time
15+
import uuid
16+
17+
from ddtrace import ext
18+
from ddtrace.internal.datadog.profiling import stack_v2
19+
from ddtrace.profiling import profiler
20+
from ddtrace.trace import tracer
21+
from tests.profiling.collector import pprof_utils
22+
23+
assert stack_v2.is_available, stack_v2.failure_msg
24+
25+
sleep_time = 0.2
26+
loop_run_time = 3
27+
28+
async def inner1() -> None:
29+
start_time = time.time()
30+
while time.time() < start_time + loop_run_time:
31+
await asyncio.sleep(sleep_time)
32+
33+
async def inner2() -> None:
34+
start_time = time.time()
35+
while time.time() < start_time + loop_run_time:
36+
await asyncio.sleep(sleep_time)
37+
38+
async def outer() -> None:
39+
t1 = asyncio.create_task(inner1(), name="inner 1")
40+
t2 = asyncio.create_task(inner2(), name="inner 2")
41+
await asyncio.gather(t1, t2)
42+
43+
resource = str(uuid.uuid4())
44+
span_type = ext.SpanTypes.WEB
45+
46+
p = profiler.Profiler(tracer=tracer)
47+
p.start()
48+
with tracer.trace("test_asyncio", resource=resource, span_type=span_type) as span:
49+
span_id = span.span_id
50+
local_root_span_id = span._local_root.span_id
51+
52+
loop = asyncio.new_event_loop()
53+
asyncio.set_event_loop(loop)
54+
main_task = loop.create_task(outer(), name="outer")
55+
loop.run_until_complete(main_task)
56+
57+
p.stop()
58+
59+
output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid())
60+
61+
profile = pprof_utils.parse_newest_profile(output_filename)
62+
63+
samples_with_span_id = pprof_utils.get_samples_with_label_key(profile, "span id")
64+
assert len(samples_with_span_id) > 0
65+
66+
# get samples with task_name
67+
samples = pprof_utils.get_samples_with_label_key(profile, "task name")
68+
# The next fails if stack_v2 is not properly configured with asyncio task
69+
# tracking via ddtrace.profiling._asyncio
70+
assert len(samples) > 0
71+
72+
pprof_utils.assert_profile_has_sample(
73+
profile,
74+
samples,
75+
expected_sample=pprof_utils.StackEvent(
76+
thread_name="MainThread",
77+
task_name="outer",
78+
span_id=span_id,
79+
local_root_span_id=local_root_span_id,
80+
locations=[
81+
pprof_utils.StackLocation(
82+
function_name="outer", filename="test_asyncio_gather.py", line_no=outer.__code__.co_firstlineno + 3
83+
),
84+
# TODO: We should add the locations of the gathered Tasks here as they should be in the same Stack
85+
],
86+
),
87+
)
88+
89+
pprof_utils.assert_profile_has_sample(
90+
profile,
91+
samples,
92+
expected_sample=pprof_utils.StackEvent(
93+
thread_name="MainThread",
94+
task_name="outer", # TODO: This is a bug and we need to fix it, it should be "inner 1"
95+
span_id=span_id,
96+
local_root_span_id=local_root_span_id,
97+
locations=[
98+
pprof_utils.StackLocation(
99+
function_name="inner2",
100+
filename="test_asyncio_gather.py",
101+
line_no=inner2.__code__.co_firstlineno + 3,
102+
),
103+
pprof_utils.StackLocation(
104+
function_name="outer", filename="test_asyncio_gather.py", line_no=outer.__code__.co_firstlineno + 3
105+
),
106+
],
107+
),
108+
)
109+
110+
pprof_utils.assert_profile_has_sample(
111+
profile,
112+
samples,
113+
expected_sample=pprof_utils.StackEvent(
114+
thread_name="MainThread",
115+
task_name="inner 1",
116+
span_id=span_id,
117+
local_root_span_id=local_root_span_id,
118+
locations=[
119+
pprof_utils.StackLocation(
120+
function_name="inner1",
121+
filename="test_asyncio_gather.py",
122+
line_no=inner1.__code__.co_firstlineno + 3,
123+
),
124+
pprof_utils.StackLocation(
125+
function_name="outer", filename="test_asyncio_gather.py", line_no=outer.__code__.co_firstlineno + 3
126+
),
127+
],
128+
),
129+
)

0 commit comments

Comments
 (0)