Skip to content

Commit ff338a1

Browse files
[python_concurrent_cpu_wall] Add new check (#134)
1 parent 3555677 commit ff338a1

5 files changed

Lines changed: 121 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
ARG BASE_IMAGE="prof-python-3.11"
2+
FROM $BASE_IMAGE
3+
4+
COPY ./scenarios/python_concurrent_cpu_wall_3.11/main.py \
5+
./scenarios/python_concurrent_cpu_wall_3.11/requirements.txt \
6+
/app/
7+
RUN chmod 644 /app/*
8+
9+
WORKDIR /app
10+
11+
RUN pip install -r requirements.txt
12+
13+
ENV EXECUTION_TIME_SEC="30"
14+
15+
ENV DD_PROFILING_ENABLED="true"
16+
17+
CMD python main.py
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# python_concurrent_cpu_wall_3.11
2+
3+
Verifies per-thread CPU-vs-wall attribution for threads that are on-CPU and
4+
off-CPU *at the same time*. Unlike `python_spiky_3.11` (single thread that
5+
alternates between bursts and sleeps sequentially), this runs two threads
6+
concurrently for the full duration:
7+
8+
- **busy**: burns CPU in `busy_loop`
9+
- **idle**: loops over short `time.sleep` calls in `idle_wait`
10+
11+
This exercises the sampler reading each thread's own CPU clock
12+
(`clock_gettime(cpu_clock_id)`) independently, and the "wall is guaranteed, CPU
13+
is optional" semantics: a sleeping thread still produces wall-time samples but
14+
~0 CPU.
15+
16+
## Expected behavior
17+
18+
- **cpu-time**: almost all CPU is in `busy_loop` (label `thread name: busy`);
19+
the idle thread contributes ~0.
20+
- **wall-time**: `busy_loop` and `idle_wait` contribute the *same* amount
21+
(~33% each, the remaining third being the MainThread blocked in `join`),
22+
confirming that wall time is attributed equally to the on-CPU and the
23+
sleeping thread.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"test_name": "python_concurrent_cpu_wall",
3+
"stacks": [
4+
{
5+
"profile-type": "cpu-time",
6+
"stack-content": [
7+
{
8+
"regular_expression": ".*busy_loop",
9+
"percent": 95,
10+
"error_margin": 5,
11+
"labels": [
12+
{
13+
"key": "thread name",
14+
"values": ["busy"]
15+
}
16+
]
17+
},
18+
{
19+
"regular_expression": ".*busy_loop",
20+
"percent": 0,
21+
"error_margin": 5,
22+
"labels": [
23+
{
24+
"key": "thread name",
25+
"values": ["idle"]
26+
}
27+
]
28+
}
29+
]
30+
},
31+
{
32+
"profile-type": "wall-time",
33+
"stack-content": [
34+
{
35+
"regular_expression": ".*busy_loop",
36+
"percent": 33,
37+
"error_margin": 12
38+
},
39+
{
40+
"regular_expression": ".*idle_wait",
41+
"percent": 33,
42+
"error_margin": 12
43+
}
44+
]
45+
}
46+
],
47+
"scale_by_duration": false
48+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import os
2+
import threading
3+
from time import sleep, time
4+
5+
6+
def busy_loop(end: float) -> None:
7+
x = 0
8+
while time() < end:
9+
for i in range(10000):
10+
x += i
11+
12+
13+
def idle_wait(end: float) -> None:
14+
while time() < end:
15+
sleep(0.05)
16+
17+
18+
if __name__ == "__main__":
19+
from ddtrace.profiling import Profiler
20+
21+
prof = Profiler()
22+
prof.start()
23+
24+
execution_time = float(os.environ.get("EXECUTION_TIME_SEC", "30"))
25+
end = time() + execution_time
26+
27+
busy = threading.Thread(target=busy_loop, args=(end,), name="busy")
28+
idle = threading.Thread(target=idle_wait, args=(end,), name="idle")
29+
busy.start()
30+
idle.start()
31+
busy.join()
32+
idle.join()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ddtrace

0 commit comments

Comments
 (0)