Skip to content

Commit ceb3bc1

Browse files
refactor(python): pair live-heap scenario to 3.14/3.15 migration gate
Replace the standalone python_live_heap_3.13 scenario with a paired python_live_heap_3.14 (baseline) + python_live_heap_3.15 (candidate) so live-heap actually tests the 3.14->3.15 migration delta like the other four families. Both dirs are wheel-only (persistent live-heap is unreleased), so they build against DDTRACE_INSTALL_URL and are excluded from main CI until the feature ships. Update the downstream gate to 10 scenarios: add the live-heap row + coverage entry, extend the gate regexp to include live_heap, clarify wheel-install rules, and note floor-version (oldest-supported) coverage as future work. Drop the now-unused base_images/Dockerfile.python-3.13 and gitignore .vscode.
1 parent c4b7683 commit ceb3bc1

13 files changed

Lines changed: 231 additions & 39 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ data/*
66
__pycache__/
77

88
.idea
9+
.vscode
910
gems.locked
1011

1112
.DS_Store

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ Both use [dd-octo-sts](https://github.com/DataDog/dd-octo-sts-action) (`dd-trace
5555
**Inputs:**
5656

5757
- `dd_trace_py_commit_sha` — commit to test (required)
58-
- `test_scenarios` — regexp passed to `TEST_SCENARIOS` (dd-trace-py passes the [8-scenario 3.14/3.15 migration gate](scenarios/python_downstream_gate/README.md); the downstream workflow default alone is `python.*`)
58+
- `test_scenarios` — regexp passed to `TEST_SCENARIOS` (dd-trace-py passes the [10-scenario 3.14/3.15 migration gate](scenarios/python_downstream_gate/README.md); the downstream workflow default alone is `python.*`)
5959

6060
## Creating new tests
6161

scenarios/python_downstream_gate/README.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Python downstream gate (dd-trace-py)
22

3-
Eight prof-correctness scenarios exercise the profiling stack on **3.14
3+
Ten prof-correctness scenarios exercise the profiling stack on **3.14
44
(baseline)** and **3.15 (candidate)** for the same workloads. They are the
55
default set when dd-trace-py triggers downstream CI on profiling changes.
66

@@ -12,11 +12,12 @@ default set when dd-trace-py triggers downstream CI on profiling changes.
1212
| exceptions | `python_exceptions_3.14` | `python_exceptions_3.15` |
1313
| async-gen | `python_async_gen_3.14` | `python_async_gen_3.15` |
1414
| lock | `python_lock_3.14` | `python_lock_3.15` |
15+
| live-heap | `python_live_heap_3.14` | `python_live_heap_3.15` |
1516

1617
## Profiler coverage
1718

1819
Each family asserts one **profile type** end-to-end (collector + export + pprof
19-
shape). Together they exercise four collectors on the 3.14 → 3.15 path — **not**
20+
shape). Together they exercise five collectors on the 3.14 → 3.15 path — **not**
2021
the full Python profiling matrix.
2122

2223
| Family | Profile type asserted | Collectors / setup | Not exercised in this scenario |
@@ -25,44 +26,58 @@ the full Python profiling matrix.
2526
| exceptions | `exception-samples` | **Exception** profiler (`DD_PROFILING_EXCEPTION_ENABLED`) | Other collectors at default/disabled |
2627
| async-gen | `wall-time` | Full profiler via `ddtrace-run` (`DD_PROFILING_ENABLED`); asyncio async-generator workload | Explicit CPU-only assertion; lock/heap/exception not targeted |
2728
| lock | `lock-acquire` | **Lock** profiler (`DD_PROFILING_LOCK_ENABLED`); threaded lock churn | Stack, heap, exceptions |
29+
| live-heap | `heap-space` + `heap-live-samples` | **Persistent live-heap** snapshot (`DD_PROFILING_HEAP_ENABLED`); OBJ-domain `bytes`; stack/lock off | CPU stack, wall-time, lock, exceptions, alloc-space |
2830

2931
**Not covered by this gate** (other prof-correctness scenarios or dd-trace-py riot
30-
tests): CPU/stack as the primary signal, heap-live samples, PyTorch, gevent/uwsgi
31-
integration shapes, Flask/FastAPI HTTP workloads, native alloc, deep stack, GIL
32-
contention, etc. Expand the downstream regexp to `python.*` when ready for
33-
broader E2E coverage.
32+
tests): CPU/stack as the primary signal, PyTorch, gevent/uwsgi integration
33+
shapes, Flask/FastAPI HTTP workloads, native alloc, deep stack, GIL contention,
34+
etc. Expand the downstream regexp to `python.*` when ready for broader E2E
35+
coverage.
3436

3537
## Default downstream regexp
3638

3739
GitLab and GitHub triggers in dd-trace-py both pass the same regexp (not the
3840
downstream workflow default of `python.*`):
3941

4042
```
41-
python_(mem_domain|exceptions|async_gen|lock)_3\.(14|15)
43+
python_(mem_domain|exceptions|async_gen|lock|live_heap)_3\.(14|15)
4244
```
4345

4446
Override via `workflow_dispatch``test_scenarios`, or when triggering
4547
`downstream-python.yml` manually.
4648

47-
## Wheel install (3.15)
49+
## Wheel install
4850

49-
3.15 scenarios require a **dd-trace-py wheel** at image build time:
51+
Every scenario builds against a **dd-trace-py wheel** via `DDTRACE_INSTALL_URL`
52+
(as `downstream-python.yml` does:
53+
`https://dd-trace-py-builds.s3.amazonaws.com/<sha>/install.sh`), pre-installed in
54+
the base image. Two cases need it unconditionally:
5055

51-
- Set `DDTRACE_INSTALL_URL` when running tests (as `downstream-python.yml` does:
52-
`https://dd-trace-py-builds.s3.amazonaws.com/<sha>/install.sh`).
53-
- Do **not** `pip install ddtrace` from PyPI in the scenario Dockerfile — 3.15
54-
wheels may not be published yet.
56+
- **All 3.15 folders** — PyPI wheels may not be published for 3.15 yet.
57+
- **Both live-heap folders (3.14 and 3.15)** — the persistent live-heap profile
58+
is recent and not yet in a published release.
5559

56-
3.15 folders are excluded from prof-correctness `main` CI until 3.15 wheels are
57-
generally available; they run via the dd-trace-py downstream gate.
60+
These folders are excluded from prof-correctness `main` CI (which uses PyPI
61+
ddtrace) until the relevant wheels are generally available; they run via the
62+
dd-trace-py downstream gate. The other 3.14 folders also run on `main` CI.
5863

5964
## Local run
6065

6166
```sh
6267
export DDTRACE_INSTALL_URL="https://dd-trace-py-builds.s3.amazonaws.com/<commit-sha>/install.sh"
63-
TEST_SCENARIOS='python_(mem_domain|exceptions|async_gen|lock)_3\.(14|15)' go test -v -run TestScenarios
68+
TEST_SCENARIOS='python_(mem_domain|exceptions|async_gen|lock|live_heap)_3\.(14|15)' go test -v -run TestScenarios
6469
```
6570

71+
## Future work
72+
73+
This gate tests the **migration delta** (3.14 → 3.15). A separate axis worth
74+
adding is **floor coverage**: run the same workloads on the oldest supported
75+
interpreter (currently 3.9; 3.10 once 3.15 drops 3.9) to catch regressions for
76+
users who jump directly from an old version to the newest — e.g. 3.9 → 3.15.
77+
Note prof-correctness only ships base images for 3.10–3.15 today, so a true 3.9
78+
floor scenario would need a new base image. This is intentionally out of scope
79+
for the 3.14/3.15 delta gate.
80+
6681
## Further reading
6782

6883
- Full migration playbook (unit / E2E / staging A/B):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
ARG BASE_IMAGE="prof-python-3.14"
2+
FROM $BASE_IMAGE
3+
4+
# ddtrace is pre-installed in the base image when DDTRACE_INSTALL_URL is set
5+
# (dd-trace-py downstream CI). Do not pip-install from PyPI here: the persistent
6+
# live-heap profile is recent and may not be in a published release yet, so this
7+
# scenario runs against the just-built wheel until it ships.
8+
COPY ./scenarios/python_live_heap_3.14/main.py /app/main.py
9+
WORKDIR /app
10+
11+
ENV EXECUTION_TIME_SEC="15"
12+
# Enable the live-heap profiler and make the snapshot the deliverable.
13+
ENV DD_PROFILING_MEMORY_ENABLED=true
14+
ENV DD_PROFILING_HEAP_ENABLED=true
15+
# Sample interval == object size -> ~one sample per object -> stable
16+
# per-stack proportions for both heap-space and heap-live-samples.
17+
ENV DD_PROFILING_HEAP_SAMPLE_SIZE=16384
18+
# Export several snapshots during the run so the persistent live set is
19+
# exercised across upload intervals (not just at shutdown).
20+
ENV DD_PROFILING_UPLOAD_INTERVAL=5
21+
# Isolate the heap profiler so other collectors don't add noise.
22+
ENV DD_PROFILING_STACK_ENABLED=false
23+
ENV DD_PROFILING_LOCK_ENABLED=false
24+
25+
CMD python main.py
Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
## Live-Heap Profiling
1+
## Live-Heap Profiling (3.14 baseline)
22

33
Validates that the Datadog Python profiler's **persistent live-heap profile**
4-
correctly reports the set of live (still-allocated) sampled objects.
4+
correctly reports the set of live (still-allocated) sampled objects. This is the
5+
**3.14 baseline** half of the `3.14 -> 3.15` migration pair (see
6+
`python_live_heap_3.15`).
57

68
Unlike `alloc-space`/`alloc-samples` (which count every allocation over the
79
interval), the live-heap profile is a running snapshot of what is *currently
@@ -21,11 +23,11 @@ differ only in count:
2123
Equal sizes mean the 80/20 split holds for both metrics the live-heap profile
2224
reports: `heap-space` (live bytes = count x size) and `heap-live-samples` (live
2325
object count). It uses `bytes` (`PyObject_Malloc`, the OBJ allocator domain) so
24-
the heap profiler tracks the objects identically on Python 3.12 and 3.13,
25-
independent of the `DD_PROFILING_MEMORY_MEM_DOMAIN_ENABLED` toggle (`bytearray`
26-
would be OBJ on 3.12 but MEM on 3.13). The objects are held alive while the
27-
process idles through several upload intervals (`DD_PROFILING_UPLOAD_INTERVAL=5`),
28-
so each exported heap snapshot contains the full live set.
26+
the heap profiler tracks the objects identically across versions, independent of
27+
the `DD_PROFILING_MEMORY_MEM_DOMAIN_ENABLED` toggle (`bytearray` moved OBJ -> MEM
28+
in 3.13). The objects are held alive while the process idles through several
29+
upload intervals (`DD_PROFILING_UPLOAD_INTERVAL=5`), so each exported heap
30+
snapshot contains the full live set.
2931

3032
## Expected Profile
3133

@@ -43,6 +45,7 @@ before the live set is fully built.
4345

4446
## Notes
4547

46-
The persistent live-heap profile is recent; run against a ddtrace build that
47-
includes it (e.g. via `DDTRACE_INSTALL_URL` pointing at a dd-trace-py S3 wheel)
48-
until it ships in a release.
48+
The persistent live-heap profile is recent, so this scenario runs against a
49+
ddtrace build that includes it (via `DDTRACE_INSTALL_URL` pointing at a
50+
dd-trace-py S3 wheel) rather than a PyPI release. It is excluded from
51+
prof-correctness `main` CI until the feature ships in a release.
File renamed without changes.
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
# - heap-space (live bytes) -> count * size -> 80/20
1515
# - heap-live-samples (live objects) -> count -> 80/20
1616
# We use ``bytes`` (PyObject_Malloc / OBJ domain), which the heap profiler
17-
# tracks identically on 3.12 and 3.13, independent of the MEM-domain toggle
18-
# (``bytearray`` would be OBJ on 3.12 but MEM on 3.13).
17+
# tracks identically across Python versions and independent of the MEM-domain
18+
# toggle (``bytearray`` moved OBJ -> MEM in 3.13). This keeps the scenario a
19+
# clean live-heap check across the 3.14 -> 3.15 migration.
1920
OBJ_SIZE = 16384 # 16 KiB, well above the pymalloc small-object threshold
2021
N_MAJOR = 1600 # ~80% of the live set (1600 * 16 KiB ~= 25 MiB)
2122
N_MINOR = 400 # ~20% of the live set ( 400 * 16 KiB ~= 6 MiB)
File renamed without changes.
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
ARG BASE_IMAGE="prof-python-3.13"
1+
ARG BASE_IMAGE="prof-python-3.15"
22
FROM $BASE_IMAGE
33

4-
# Copy the Python target into the container
5-
COPY ./scenarios/python_live_heap_3.13/main.py \
6-
./scenarios/python_live_heap_3.13/requirements.txt \
7-
/app/
8-
RUN chmod 644 /app/*
9-
4+
# ddtrace is pre-installed in the base image when DDTRACE_INSTALL_URL is set
5+
# (dd-trace-py downstream CI). Do not pip-install here — PyPI wheels may not
6+
# exist for 3.15 yet, and the persistent live-heap profile is recent.
7+
COPY ./scenarios/python_live_heap_3.15/main.py /app/main.py
108
WORKDIR /app
119

12-
RUN pip install --no-cache-dir -r requirements.txt
13-
1410
ENV EXECUTION_TIME_SEC="15"
1511
# Enable the live-heap profiler and make the snapshot the deliverable.
1612
ENV DD_PROFILING_MEMORY_ENABLED=true
@@ -25,5 +21,4 @@ ENV DD_PROFILING_UPLOAD_INTERVAL=5
2521
ENV DD_PROFILING_STACK_ENABLED=false
2622
ENV DD_PROFILING_LOCK_ENABLED=false
2723

28-
# Run the program when the container starts
2924
CMD python main.py
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
## Live-Heap Profiling (3.15 candidate)
2+
3+
Validates that the Datadog Python profiler's **persistent live-heap profile**
4+
correctly reports the set of live (still-allocated) sampled objects. This is the
5+
**3.15 candidate** half of the `3.14 -> 3.15` migration pair (see
6+
`python_live_heap_3.14`).
7+
8+
Unlike `alloc-space`/`alloc-samples` (which count every allocation over the
9+
interval), the live-heap profile is a running snapshot of what is *currently
10+
live*: allocations are added when sampled and subtracted when freed, and the
11+
snapshot is exported non-destructively on every upload. It is attached as a
12+
second pprof (`<prefix>.<pid>.<seq>.heap.pprof`) alongside the primary profile.
13+
14+
## Test Application
15+
16+
`main.py` retains a known live set for the whole run, split into two
17+
distinctly-named call sites that allocate **equal-size** objects (16 KiB) and
18+
differ only in count:
19+
20+
- `retain_major` - 1,600 objects (~25 MiB, ~80% of the live set)
21+
- `retain_minor` - 400 objects (~6 MiB, ~20% of the live set)
22+
23+
Equal sizes mean the 80/20 split holds for both metrics the live-heap profile
24+
reports: `heap-space` (live bytes = count x size) and `heap-live-samples` (live
25+
object count). It uses `bytes` (`PyObject_Malloc`, the OBJ allocator domain) so
26+
the heap profiler tracks the objects identically across versions, independent of
27+
the `DD_PROFILING_MEMORY_MEM_DOMAIN_ENABLED` toggle (`bytearray` moved OBJ -> MEM
28+
in 3.13). The objects are held alive while the process idles through several
29+
upload intervals (`DD_PROFILING_UPLOAD_INTERVAL=5`), so each exported heap
30+
snapshot contains the full live set.
31+
32+
## Expected Profile
33+
34+
Assertions run against the heap snapshot pprof only (selected with
35+
`pprof-regex`). `DD_PROFILING_HEAP_SAMPLE_SIZE=16384` (== object size) yields
36+
roughly one sample per object, keeping the per-stack proportions stable.
37+
38+
- `heap-space` (live bytes):
39+
- `^<module>;Target.run;Target.retain_major$` ~= 80%
40+
- `^<module>;Target.run;Target.retain_minor$` ~= 20%
41+
- `heap-live-samples` (live object count): same ~80/20 split.
42+
43+
`allow_first_profile_failure` tolerates the first snapshot, which may be taken
44+
before the live set is fully built.
45+
46+
## Notes
47+
48+
Requires a **dd-trace-py wheel** at image build time (set `DDTRACE_INSTALL_URL`,
49+
as `downstream-python.yml` does). Do **not** `pip install ddtrace` from PyPI —
50+
3.15 wheels may not be published yet. Excluded from prof-correctness `main` CI
51+
until 3.15 wheels are generally available; runs via the dd-trace-py downstream
52+
gate.

0 commit comments

Comments
 (0)