Skip to content

Commit db5d2d3

Browse files
test(profiling): port new tests over from echion (#15581)
## Description This ports new echion tests to dd-trace-py.
1 parent 3632c6c commit db5d2d3

File tree

5 files changed

+534
-0
lines changed

5 files changed

+534
-0
lines changed
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import pytest
2+
3+
4+
@pytest.mark.subprocess(
5+
env=dict(
6+
DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_executor",
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_executor_wall_time() -> None:
12+
import asyncio
13+
import os
14+
15+
from ddtrace.internal.datadog.profiling import stack_v2
16+
from ddtrace.profiling import profiler
17+
from tests.profiling.collector import pprof_utils
18+
19+
assert stack_v2.is_available, stack_v2.failure_msg
20+
from typing import AsyncGenerator
21+
22+
async def deep_dependency() -> None:
23+
# This is a regular (non-generator) coroutine called
24+
# by an async generator.
25+
await asyncio.sleep(0.15)
26+
27+
async def async_generator_dep(i: int) -> AsyncGenerator[int, None]:
28+
# This is an async generator called by an async generator.
29+
# We want to make sure that recursive async generators are correctly sampled.
30+
for j in range(i):
31+
await deep_dependency()
32+
yield j
33+
34+
async def async_generator() -> AsyncGenerator[int, None]:
35+
# This is an async generator called by a coroutine.
36+
# We want to make sure we unwind async generators correctly.
37+
for i in range(5):
38+
async for j in async_generator_dep(i):
39+
yield j
40+
41+
async def asynchronous_function() -> None:
42+
# This is a normal (non-generator) coroutine that calls into an async generator.
43+
# Stack samples should not stopped at this function, they should continue unwinding
44+
# into the async generator.
45+
async for _ in async_generator():
46+
pass
47+
48+
async def main() -> None:
49+
await asynchronous_function()
50+
51+
p = profiler.Profiler()
52+
p.start()
53+
54+
loop = asyncio.new_event_loop()
55+
asyncio.set_event_loop(loop)
56+
loop.run_until_complete(main())
57+
58+
p.stop()
59+
60+
output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid())
61+
62+
profile = pprof_utils.parse_newest_profile(output_filename)
63+
64+
task_samples = pprof_utils.get_samples_with_label_key(profile, "task name")
65+
assert len(task_samples) > 0
66+
67+
def loc(f_name: str) -> pprof_utils.StackLocation:
68+
return pprof_utils.StackLocation(function_name=f_name, filename="", line_no=-1)
69+
70+
# Thread Pool Executor
71+
pprof_utils.assert_profile_has_sample(
72+
profile,
73+
task_samples,
74+
expected_sample=pprof_utils.StackEvent(
75+
thread_name="MainThread",
76+
task_name="Task-1",
77+
locations=list(
78+
reversed(
79+
[
80+
# loc("Task-1"),
81+
loc("main"),
82+
loc("asynchronous_function"),
83+
loc("async_generator"),
84+
loc("async_generator_dep"),
85+
loc("deep_dependency"),
86+
loc("sleep"),
87+
]
88+
)
89+
),
90+
),
91+
print_samples_on_failure=True,
92+
)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import pytest
2+
3+
4+
@pytest.mark.subprocess(
5+
env=dict(
6+
DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_gather_deep_coroutines",
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_deep_coroutines() -> None:
12+
import asyncio
13+
import os
14+
15+
from ddtrace.internal.datadog.profiling import stack_v2
16+
from ddtrace.profiling import profiler
17+
from tests.profiling.collector import pprof_utils
18+
19+
assert stack_v2.is_available, stack_v2.failure_msg
20+
21+
async def deeper() -> None:
22+
await asyncio.sleep(1.0)
23+
24+
async def deep() -> None:
25+
await deeper()
26+
27+
async def inner() -> None:
28+
await deep()
29+
30+
async def main() -> None:
31+
await asyncio.gather(inner(), inner())
32+
33+
p = profiler.Profiler()
34+
p.start()
35+
36+
asyncio.run(main())
37+
38+
p.stop()
39+
40+
output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid())
41+
42+
profile = pprof_utils.parse_newest_profile(output_filename)
43+
44+
samples = pprof_utils.get_samples_with_label_key(profile, "task name")
45+
assert len(samples) > 0
46+
47+
def loc(f_name: str) -> pprof_utils.StackLocation:
48+
return pprof_utils.StackLocation(function_name=f_name, filename="", line_no=-1)
49+
50+
# Test that we see stacks for inner_1 and inner_2
51+
pprof_utils.assert_profile_has_sample(
52+
profile,
53+
samples,
54+
expected_sample=pprof_utils.StackEvent(
55+
thread_name="MainThread",
56+
locations=list(
57+
reversed(
58+
[
59+
# loc("Task-1"),
60+
loc("main"),
61+
# loc("Task-2"),
62+
loc("inner"),
63+
loc("deep"),
64+
loc("deeper"),
65+
loc("sleep"),
66+
]
67+
),
68+
),
69+
),
70+
print_samples_on_failure=True,
71+
)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import pytest
2+
3+
4+
@pytest.mark.subprocess(
5+
env=dict(
6+
DD_PROFILING_OUTPUT_PPROF="/tmp/test_asyncio_recursive_on_cpu_coros",
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_recursive_on_cpu_coros():
12+
import asyncio
13+
import os
14+
from sys import version_info as PYVERSION
15+
import time
16+
import uuid
17+
18+
from ddtrace import ext
19+
from ddtrace.internal.datadog.profiling import stack_v2
20+
from ddtrace.profiling import profiler
21+
from ddtrace.trace import tracer
22+
from tests.profiling.collector import pprof_utils
23+
from tests.profiling.collector.pprof_utils import StackLocation
24+
25+
assert stack_v2.is_available, stack_v2.failure_msg
26+
27+
def sync_code() -> int:
28+
target = time.time() + 1
29+
result = 0
30+
while time.time() < target:
31+
result += 1
32+
33+
return result
34+
35+
def sync_code_outer() -> int:
36+
return sync_code()
37+
38+
async def inner3() -> int:
39+
return sync_code_outer()
40+
41+
async def inner2() -> int:
42+
return await inner3()
43+
44+
async def inner1() -> int:
45+
return await inner2()
46+
47+
async def outer():
48+
return await inner1()
49+
50+
async def async_main():
51+
return await outer()
52+
53+
def main_sync():
54+
asyncio.run(async_main())
55+
56+
resource = str(uuid.uuid4())
57+
span_type = ext.SpanTypes.WEB
58+
59+
p = profiler.Profiler(tracer=tracer)
60+
p.start()
61+
with tracer.trace("test_asyncio", resource=resource, span_type=span_type) as span:
62+
span_id = span.span_id
63+
local_root_span_id = span._local_root.span_id
64+
65+
main_sync()
66+
67+
p.stop()
68+
69+
output_filename = os.environ["DD_PROFILING_OUTPUT_PPROF"] + "." + str(os.getpid())
70+
profile = pprof_utils.parse_newest_profile(output_filename)
71+
72+
def loc(f_name: str) -> StackLocation:
73+
return pprof_utils.StackLocation(function_name=f_name, filename="", line_no=-1)
74+
75+
runner_prefix = "Runner." if PYVERSION >= (3, 11) else ""
76+
base_event_loop_prefix = "BaseEventLoop." if PYVERSION >= (3, 11) else ""
77+
handle_prefix = "Handle." if PYVERSION >= (3, 11) else ""
78+
79+
pprof_utils.assert_profile_has_sample(
80+
profile,
81+
list(profile.sample),
82+
pprof_utils.StackEvent(
83+
thread_name="MainThread",
84+
span_id=span_id,
85+
local_root_span_id=local_root_span_id,
86+
locations=list(
87+
reversed(
88+
[
89+
loc("<module>"),
90+
loc("main_sync"),
91+
loc("run"),
92+
]
93+
+ ([loc(f"{runner_prefix}run")] if PYVERSION >= (3, 11) else [])
94+
+ [
95+
loc(f"{base_event_loop_prefix}run_until_complete"),
96+
loc(f"{base_event_loop_prefix}run_forever"),
97+
loc(f"{base_event_loop_prefix}_run_once"),
98+
loc(f"{handle_prefix}_run"),
99+
loc("async_main"),
100+
loc("outer"),
101+
loc("inner1"),
102+
loc("inner2"),
103+
loc("inner3"),
104+
loc("sync_code_outer"),
105+
loc("sync_code"),
106+
]
107+
)
108+
),
109+
),
110+
print_samples_on_failure=True,
111+
)

0 commit comments

Comments
 (0)