diff --git a/README.md b/README.md index edb2e00..c48908b 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Both use [dd-octo-sts](https://github.com/DataDog/dd-octo-sts-action) (`dd-trace **Inputs:** - `dd_trace_py_commit_sha` — commit to test (required) -- `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.*`) +- `test_scenarios` — regexp passed to `TEST_SCENARIOS` (dd-trace-py passes the [16-scenario 3.14/3.15 migration gate](scenarios/python_downstream_gate/README.md); the downstream workflow default alone is `python.*`) ## Creating new tests diff --git a/scenarios/python_asyncio_3.14/Dockerfile b/scenarios/python_asyncio_3.14/Dockerfile new file mode 100644 index 0000000..6238def --- /dev/null +++ b/scenarios/python_asyncio_3.14/Dockerfile @@ -0,0 +1,15 @@ +ARG BASE_IMAGE="prof-python-3.14" +FROM $BASE_IMAGE + +COPY ./scenarios/python_asyncio_3.14/requirements.txt /app/requirements.txt +RUN pip install -r /app/requirements.txt + +COPY ./scenarios/python_asyncio_3.14/main.py /app/main.py +WORKDIR /app + +ENV EXECUTION_TIME_SEC=5 +ENV ECHION_USE_FAST_COPY_MEMORY=1 +ENV _DD_PROFILING_STACK_ADAPTIVE_SAMPLING_ENABLED=0 +ENV DD_PROFILING_ENABLED=true + +CMD ddtrace-run python main.py diff --git a/scenarios/python_asyncio_3.14/README.md b/scenarios/python_asyncio_3.14/README.md new file mode 100644 index 0000000..48cc362 --- /dev/null +++ b/scenarios/python_asyncio_3.14/README.md @@ -0,0 +1,6 @@ +## Asyncio task-name labels (3.14 baseline) + +Validates **wall-time** stack samples carry asyncio `task name` labels (named and +unnamed tasks). Migration pair baseline; see `python_asyncio_3.15`. + +Reuses `scenarios/python_asyncio_3.11` workload. diff --git a/scenarios/python_asyncio_3.14/expected_profile.json b/scenarios/python_asyncio_3.14/expected_profile.json new file mode 100644 index 0000000..39a662e --- /dev/null +++ b/scenarios/python_asyncio_3.14/expected_profile.json @@ -0,0 +1,52 @@ +{ + "test_name": "python_asyncio_3.14", + "stacks": [ + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*my_coroutine.*", + "percent": 99, + "error_margin": 0, + "labels": [ + { + "key": "thread name", + "values": [ + "MainThread" + ] + }, + { + "key": "task name", + "values": [ + "short_task" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*my_coroutine.*", + "percent": 99, + "error_margin": 0, + "labels": [ + { + "key": "thread name", + "values": [ + "MainThread" + ] + }, + { + "key": "task name", + "values_regex": "Task-[0-9]+" + } + ] + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_asyncio_3.14/main.py b/scenarios/python_asyncio_3.14/main.py new file mode 100644 index 0000000..55a6967 --- /dev/null +++ b/scenarios/python_asyncio_3.14/main.py @@ -0,0 +1,27 @@ +import asyncio +import os + + +async def my_coroutine(n: float) -> None: + await asyncio.sleep(n) + + +async def main() -> None: + # Simple application that creates two Tasks with different durations: + # - "unnamed Task" runs my_coroutine() for 2 second + # - short_task runs my_coroutine() for 1 second + # The profiler should capture both Tasks with their respective durations. + + # Give the Profiler some time to start up + await asyncio.sleep(0.5) + + execution_time_sec = float(os.environ.get("EXECUTION_TIME_SEC", "5")) + + short_task = asyncio.create_task(my_coroutine(execution_time_sec / 2.0), name="short_task") + + # asyncio.gather will automatically wrap my_coroutine into a Task + await asyncio.gather(short_task, my_coroutine(execution_time_sec)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scenarios/python_asyncio_3.14/requirements.txt b/scenarios/python_asyncio_3.14/requirements.txt new file mode 100644 index 0000000..749bf29 --- /dev/null +++ b/scenarios/python_asyncio_3.14/requirements.txt @@ -0,0 +1 @@ +ddtrace diff --git a/scenarios/python_asyncio_3.15/Dockerfile b/scenarios/python_asyncio_3.15/Dockerfile new file mode 100644 index 0000000..66c2c12 --- /dev/null +++ b/scenarios/python_asyncio_3.15/Dockerfile @@ -0,0 +1,12 @@ +ARG BASE_IMAGE="prof-python-3.15" +FROM $BASE_IMAGE + +COPY ./scenarios/python_asyncio_3.15/main.py /app/main.py +WORKDIR /app + +ENV EXECUTION_TIME_SEC=5 +ENV ECHION_USE_FAST_COPY_MEMORY=1 +ENV _DD_PROFILING_STACK_ADAPTIVE_SAMPLING_ENABLED=0 +ENV DD_PROFILING_ENABLED=true + +CMD ddtrace-run python main.py diff --git a/scenarios/python_asyncio_3.15/README.md b/scenarios/python_asyncio_3.15/README.md new file mode 100644 index 0000000..f97723a --- /dev/null +++ b/scenarios/python_asyncio_3.15/README.md @@ -0,0 +1,4 @@ +## Asyncio task-name labels (3.15 candidate) + +Wheel-only 3.15 candidate half of the asyncio task-name gate pair. See +`python_asyncio_3.14`. diff --git a/scenarios/python_asyncio_3.15/expected_profile.json b/scenarios/python_asyncio_3.15/expected_profile.json new file mode 100644 index 0000000..11b78c6 --- /dev/null +++ b/scenarios/python_asyncio_3.15/expected_profile.json @@ -0,0 +1,52 @@ +{ + "test_name": "python_asyncio_3.15", + "stacks": [ + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*my_coroutine.*", + "percent": 50, + "error_margin": 100, + "labels": [ + { + "key": "thread name", + "values": [ + "MainThread" + ] + }, + { + "key": "task name", + "values": [ + "short_task" + ] + } + ] + } + ] + }, + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*my_coroutine.*", + "percent": 50, + "error_margin": 100, + "labels": [ + { + "key": "thread name", + "values": [ + "MainThread" + ] + }, + { + "key": "task name", + "values_regex": "Task-[0-9]+" + } + ] + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_asyncio_3.15/main.py b/scenarios/python_asyncio_3.15/main.py new file mode 100644 index 0000000..55a6967 --- /dev/null +++ b/scenarios/python_asyncio_3.15/main.py @@ -0,0 +1,27 @@ +import asyncio +import os + + +async def my_coroutine(n: float) -> None: + await asyncio.sleep(n) + + +async def main() -> None: + # Simple application that creates two Tasks with different durations: + # - "unnamed Task" runs my_coroutine() for 2 second + # - short_task runs my_coroutine() for 1 second + # The profiler should capture both Tasks with their respective durations. + + # Give the Profiler some time to start up + await asyncio.sleep(0.5) + + execution_time_sec = float(os.environ.get("EXECUTION_TIME_SEC", "5")) + + short_task = asyncio.create_task(my_coroutine(execution_time_sec / 2.0), name="short_task") + + # asyncio.gather will automatically wrap my_coroutine into a Task + await asyncio.gather(short_task, my_coroutine(execution_time_sec)) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scenarios/python_deep_stack_3.14/Dockerfile b/scenarios/python_deep_stack_3.14/Dockerfile new file mode 100644 index 0000000..31bdf1c --- /dev/null +++ b/scenarios/python_deep_stack_3.14/Dockerfile @@ -0,0 +1,14 @@ +ARG BASE_IMAGE="prof-python-3.14" +FROM $BASE_IMAGE + +COPY ./scenarios/python_deep_stack_3.14/main.py \ + ./scenarios/python_deep_stack_3.14/requirements.txt \ + /app/ + +WORKDIR /app +RUN pip install -r requirements.txt + +ENV EXECUTION_TIME_SEC=10 +ENV DD_PROFILING_ENABLED=true + +CMD python main.py diff --git a/scenarios/python_deep_stack_3.14/README.md b/scenarios/python_deep_stack_3.14/README.md new file mode 100644 index 0000000..0652364 --- /dev/null +++ b/scenarios/python_deep_stack_3.14/README.md @@ -0,0 +1,4 @@ +## Deep stack unwinding (3.14 baseline) + +Validates **cpu-time** samples through a 400-frame recursive stack. See +`python_deep_stack_3.15`. diff --git a/scenarios/python_deep_stack_3.14/expected_profile.json b/scenarios/python_deep_stack_3.14/expected_profile.json new file mode 100644 index 0000000..3d1c4ca --- /dev/null +++ b/scenarios/python_deep_stack_3.14/expected_profile.json @@ -0,0 +1,16 @@ +{ + "test_name": "python_deep_stack_3.14", + "stacks": [ + { + "profile-type": "cpu-time", + "stack-content": [ + { + "regular_expression": "[0-9]+ frames omitted\u003e;(recurse;){50,}burn", + "percent": 100, + "error_margin": 10 + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_deep_stack_3.14/main.py b/scenarios/python_deep_stack_3.14/main.py new file mode 100644 index 0000000..d73ef1e --- /dev/null +++ b/scenarios/python_deep_stack_3.14/main.py @@ -0,0 +1,32 @@ +import os +import sys +from time import time + +from ddtrace.profiling import Profiler + +DEPTH = 400 + + +def burn(end: float) -> None: + x = 0 + while time() < end: + for i in range(10000): + x += i + + +def recurse(depth: int, end: float) -> None: + if depth <= 0: + burn(end) + return + recurse(depth - 1, end) + + +if __name__ == "__main__": + sys.setrecursionlimit(10000) + + prof = Profiler() + prof.start() + + execution_time = float(os.environ.get("EXECUTION_TIME_SEC", "30")) + end = time() + execution_time + recurse(DEPTH, end) diff --git a/scenarios/python_deep_stack_3.14/requirements.txt b/scenarios/python_deep_stack_3.14/requirements.txt new file mode 100644 index 0000000..749bf29 --- /dev/null +++ b/scenarios/python_deep_stack_3.14/requirements.txt @@ -0,0 +1 @@ +ddtrace diff --git a/scenarios/python_deep_stack_3.15/Dockerfile b/scenarios/python_deep_stack_3.15/Dockerfile new file mode 100644 index 0000000..37d3ee7 --- /dev/null +++ b/scenarios/python_deep_stack_3.15/Dockerfile @@ -0,0 +1,10 @@ +ARG BASE_IMAGE="prof-python-3.15" +FROM $BASE_IMAGE + +COPY ./scenarios/python_deep_stack_3.15/main.py /app/main.py +WORKDIR /app + +ENV EXECUTION_TIME_SEC=10 +ENV DD_PROFILING_ENABLED=true + +CMD python main.py diff --git a/scenarios/python_deep_stack_3.15/README.md b/scenarios/python_deep_stack_3.15/README.md new file mode 100644 index 0000000..915aba5 --- /dev/null +++ b/scenarios/python_deep_stack_3.15/README.md @@ -0,0 +1,3 @@ +## Deep stack unwinding (3.15 candidate) + +Wheel-only candidate half of the deep-stack gate pair. See `python_deep_stack_3.14`. diff --git a/scenarios/python_deep_stack_3.15/expected_profile.json b/scenarios/python_deep_stack_3.15/expected_profile.json new file mode 100644 index 0000000..14e2a82 --- /dev/null +++ b/scenarios/python_deep_stack_3.15/expected_profile.json @@ -0,0 +1,16 @@ +{ + "test_name": "python_deep_stack_3.15", + "stacks": [ + { + "profile-type": "cpu-time", + "stack-content": [ + { + "regular_expression": "[0-9]+ frames omitted\u003e;(recurse;){50,}burn", + "percent": 100, + "error_margin": 10 + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_deep_stack_3.15/main.py b/scenarios/python_deep_stack_3.15/main.py new file mode 100644 index 0000000..d73ef1e --- /dev/null +++ b/scenarios/python_deep_stack_3.15/main.py @@ -0,0 +1,32 @@ +import os +import sys +from time import time + +from ddtrace.profiling import Profiler + +DEPTH = 400 + + +def burn(end: float) -> None: + x = 0 + while time() < end: + for i in range(10000): + x += i + + +def recurse(depth: int, end: float) -> None: + if depth <= 0: + burn(end) + return + recurse(depth - 1, end) + + +if __name__ == "__main__": + sys.setrecursionlimit(10000) + + prof = Profiler() + prof.start() + + execution_time = float(os.environ.get("EXECUTION_TIME_SEC", "30")) + end = time() + execution_time + recurse(DEPTH, end) diff --git a/scenarios/python_downstream_gate/README.md b/scenarios/python_downstream_gate/README.md index 0bd87ba..7740df9 100644 --- a/scenarios/python_downstream_gate/README.md +++ b/scenarios/python_downstream_gate/README.md @@ -1,6 +1,6 @@ # Python downstream gate (dd-trace-py) -Ten prof-correctness scenarios exercise the profiling stack on **3.14 +Sixteen prof-correctness scenarios exercise the profiling stack on **3.14 (baseline)** and **3.15 (candidate)** for the same workloads. They are the default set when dd-trace-py triggers downstream CI on profiling changes. @@ -10,45 +10,42 @@ default set when dd-trace-py triggers downstream CI on profiling changes. |--------|-------------------|------------------| | cpu (stack) | `python_cpu_3.14` | `python_cpu_3.15` | | alloc (memory) | `python_alloc_3.14` | `python_alloc_3.15` | +| asyncio (task labels) | `python_asyncio_3.14` | `python_asyncio_3.15` | +| native-cpu | `python_native_cpu_3.14` | `python_native_cpu_3.15` | +| deep-stack | `python_deep_stack_3.14` | `python_deep_stack_3.15` | | exceptions | `python_exceptions_3.14` | `python_exceptions_3.15` | | async-gen | `python_async_gen_3.14` | `python_async_gen_3.15` | | lock | `python_lock_3.14` | `python_lock_3.15` | ## Profiler coverage -| Family | Profile type asserted | Collectors / setup | -|--------|----------------------|--------------------| -| cpu (stack) | `cpu-time` + `thread name` | Stack via `ddtrace-run`; memory off; CPU-bound loops | -| alloc (memory) | `alloc-space` + `alloc-samples` | Memory profiler; stack/lock off | -| exceptions | `exception-samples` + `exception type` | Exception profiler | -| async-gen | `wall-time` | Full profiler via `ddtrace-run`; asyncio async-generator workload | -| lock | `lock-acquire` + `lock-release` + `lock name` | Lock profiler; threaded lock churn | +| Family | Profile type asserted | +|--------|----------------------| +| cpu (stack) | `cpu-time` + `thread name` | +| alloc (memory) | `alloc-space` + `alloc-samples` | +| asyncio | `wall-time` + `task name` | +| native-cpu | `cpu-time` + `wall-time` (C-extension frames) | +| deep-stack | `cpu-time` (400-frame unwinding) | +| exceptions | `exception-samples` + `exception type` | +| async-gen | `wall-time` | +| lock | `lock-acquire` + `lock-release` + `lock name` | -Feature-specific pairs (mem_domain, live_heap) and extended coverage land in -follow-up PRs. +Feature-specific pairs (mem_domain, live_heap) and cross-cutting integration +scenarios land in follow-up PRs. ## Default downstream regexp ``` -python_(cpu|alloc|exceptions|async_gen|lock)_3\.(14|15) +python_(cpu|alloc|asyncio|native_cpu|deep_stack|exceptions|async_gen|lock)_3\.(14|15) ``` -Override via `workflow_dispatch` → `test_scenarios`, or when triggering -[`downstream-python.yml`](../../.github/workflows/downstream-python.yml) manually. - -## Wheel install - -- **All `*_3.15` folders** use `DDTRACE_INSTALL_URL` (excluded from `main` CI). -- **3.14 folders** run on prof-correctness `main` CI against PyPI ddtrace. - ## Local run ```sh export DDTRACE_INSTALL_URL="https://dd-trace-py-builds.s3.amazonaws.com//install.sh" -TEST_SCENARIOS='python_(cpu|alloc|exceptions|async_gen|lock)_3\.(14|15)' go test -v -run TestScenarios +TEST_SCENARIOS='python_(cpu|alloc|asyncio|native_cpu|deep_stack|exceptions|async_gen|lock)_3\.(14|15)' go test -v -run TestScenarios ``` ## Further reading -- Gate infra: PR stacking from `vlad/gate-infra` - prof-correctness downstream wiring: [README](../../README.md#downstream-from-dd-trace-py) diff --git a/scenarios/python_native_cpu_3.14/Dockerfile b/scenarios/python_native_cpu_3.14/Dockerfile new file mode 100644 index 0000000..b552e44 --- /dev/null +++ b/scenarios/python_native_cpu_3.14/Dockerfile @@ -0,0 +1,14 @@ +ARG BASE_IMAGE="prof-python-3.14" +FROM $BASE_IMAGE + +COPY ./scenarios/python_native_cpu_3.14/main.py \ + ./scenarios/python_native_cpu_3.14/requirements.txt \ + /app/ + +WORKDIR /app +RUN pip install -r requirements.txt + +ENV EXECUTION_TIME_SEC=10 +ENV _DD_PROFILING_STACK_ADAPTIVE_SAMPLING_ENABLED=0 + +CMD python main.py diff --git a/scenarios/python_native_cpu_3.14/README.md b/scenarios/python_native_cpu_3.14/README.md new file mode 100644 index 0000000..dbfc7df --- /dev/null +++ b/scenarios/python_native_cpu_3.14/README.md @@ -0,0 +1,4 @@ +## Native C-extension frames (3.14 baseline) + +Validates **cpu-time** and **wall-time** stacks include C-extension frames from +stdlib modules (hashlib, zlib, etc.). See `python_native_cpu_3.15`. diff --git a/scenarios/python_native_cpu_3.14/expected_profile.json b/scenarios/python_native_cpu_3.14/expected_profile.json new file mode 100644 index 0000000..0255062 --- /dev/null +++ b/scenarios/python_native_cpu_3.14/expected_profile.json @@ -0,0 +1,66 @@ +{ + "test_name": "python_native_cpu_3.14", + "stacks": [ + { + "profile-type": "cpu-time", + "stack-content": [ + { + "regular_expression": ".*hash_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*compress_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*factorial_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*regex_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*crc_work", + "percent": 20, + "error_margin": 10 + } + ] + }, + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*hash_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*compress_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*factorial_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*regex_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*crc_work", + "percent": 20, + "error_margin": 10 + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_native_cpu_3.14/main.py b/scenarios/python_native_cpu_3.14/main.py new file mode 100644 index 0000000..a74ade4 --- /dev/null +++ b/scenarios/python_native_cpu_3.14/main.py @@ -0,0 +1,65 @@ +import binascii +import hashlib +import math +import os +import re +import zlib +from time import time + +from ddtrace.profiling import Profiler + +# 1 MB of compressible data shared across workers to avoid allocation overhead. +_DATA: bytes = b"abcdefgh" * (128 * 1024) +_TEXT: str = "the quick brown fox jumps over the lazy dog " * 5000 +_PATTERN: re.Pattern[str] = re.compile(r"\b\w+\b") + + +def hash_work(duration: float) -> None: + """Burn CPU in hashlib.sha256 (C extension).""" + end = time() + duration + while time() < end: + hashlib.sha256(_DATA).digest() + + +def compress_work(duration: float) -> None: + """Burn CPU in zlib.compress (C extension).""" + end = time() + duration + while time() < end: + zlib.compress(_DATA, 6) + + +def factorial_work(duration: float) -> None: + """Burn CPU in math.factorial (C extension).""" + end = time() + duration + while time() < end: + math.factorial(100_000) + + +def regex_work(duration: float) -> None: + """Burn CPU in re.findall (C extension regex engine).""" + end = time() + duration + while time() < end: + _PATTERN.findall(_TEXT) + + +def crc_work(duration: float) -> None: + """Burn CPU in binascii.crc32 (C extension).""" + end = time() + duration + while time() < end: + binascii.crc32(_DATA) + + +if __name__ == "__main__": + prof = Profiler() + prof.start() + + execution_time = float(os.environ.get("EXECUTION_TIME_SEC", "25")) + all_functions = [hash_work, compress_work, factorial_work, regex_work, crc_work] + + runs = 3 + time_per_function = execution_time / len(all_functions) + time_per_run = time_per_function / runs + + for _ in range(runs): + for func in all_functions: + func(time_per_run) diff --git a/scenarios/python_native_cpu_3.14/requirements.txt b/scenarios/python_native_cpu_3.14/requirements.txt new file mode 100644 index 0000000..749bf29 --- /dev/null +++ b/scenarios/python_native_cpu_3.14/requirements.txt @@ -0,0 +1 @@ +ddtrace diff --git a/scenarios/python_native_cpu_3.15/Dockerfile b/scenarios/python_native_cpu_3.15/Dockerfile new file mode 100644 index 0000000..aeb9c1d --- /dev/null +++ b/scenarios/python_native_cpu_3.15/Dockerfile @@ -0,0 +1,10 @@ +ARG BASE_IMAGE="prof-python-3.15" +FROM $BASE_IMAGE + +COPY ./scenarios/python_native_cpu_3.15/main.py /app/main.py +WORKDIR /app + +ENV EXECUTION_TIME_SEC=10 +ENV _DD_PROFILING_STACK_ADAPTIVE_SAMPLING_ENABLED=0 + +CMD python main.py diff --git a/scenarios/python_native_cpu_3.15/README.md b/scenarios/python_native_cpu_3.15/README.md new file mode 100644 index 0000000..04f3f1a --- /dev/null +++ b/scenarios/python_native_cpu_3.15/README.md @@ -0,0 +1,3 @@ +## Native C-extension frames (3.15 candidate) + +Wheel-only candidate half of the native CPU gate pair. See `python_native_cpu_3.14`. diff --git a/scenarios/python_native_cpu_3.15/expected_profile.json b/scenarios/python_native_cpu_3.15/expected_profile.json new file mode 100644 index 0000000..7141875 --- /dev/null +++ b/scenarios/python_native_cpu_3.15/expected_profile.json @@ -0,0 +1,66 @@ +{ + "test_name": "python_native_cpu_3.15", + "stacks": [ + { + "profile-type": "cpu-time", + "stack-content": [ + { + "regular_expression": ".*hash_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*compress_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*factorial_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*regex_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*crc_work", + "percent": 20, + "error_margin": 10 + } + ] + }, + { + "profile-type": "wall-time", + "stack-content": [ + { + "regular_expression": ".*hash_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*compress_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*factorial_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*regex_work", + "percent": 20, + "error_margin": 10 + }, + { + "regular_expression": ".*crc_work", + "percent": 20, + "error_margin": 10 + } + ] + } + ], + "scale_by_duration": false +} diff --git a/scenarios/python_native_cpu_3.15/main.py b/scenarios/python_native_cpu_3.15/main.py new file mode 100644 index 0000000..a74ade4 --- /dev/null +++ b/scenarios/python_native_cpu_3.15/main.py @@ -0,0 +1,65 @@ +import binascii +import hashlib +import math +import os +import re +import zlib +from time import time + +from ddtrace.profiling import Profiler + +# 1 MB of compressible data shared across workers to avoid allocation overhead. +_DATA: bytes = b"abcdefgh" * (128 * 1024) +_TEXT: str = "the quick brown fox jumps over the lazy dog " * 5000 +_PATTERN: re.Pattern[str] = re.compile(r"\b\w+\b") + + +def hash_work(duration: float) -> None: + """Burn CPU in hashlib.sha256 (C extension).""" + end = time() + duration + while time() < end: + hashlib.sha256(_DATA).digest() + + +def compress_work(duration: float) -> None: + """Burn CPU in zlib.compress (C extension).""" + end = time() + duration + while time() < end: + zlib.compress(_DATA, 6) + + +def factorial_work(duration: float) -> None: + """Burn CPU in math.factorial (C extension).""" + end = time() + duration + while time() < end: + math.factorial(100_000) + + +def regex_work(duration: float) -> None: + """Burn CPU in re.findall (C extension regex engine).""" + end = time() + duration + while time() < end: + _PATTERN.findall(_TEXT) + + +def crc_work(duration: float) -> None: + """Burn CPU in binascii.crc32 (C extension).""" + end = time() + duration + while time() < end: + binascii.crc32(_DATA) + + +if __name__ == "__main__": + prof = Profiler() + prof.start() + + execution_time = float(os.environ.get("EXECUTION_TIME_SEC", "25")) + all_functions = [hash_work, compress_work, factorial_work, regex_work, crc_work] + + runs = 3 + time_per_function = execution_time / len(all_functions) + time_per_run = time_per_function / runs + + for _ in range(runs): + for func in all_functions: + func(time_per_run)