Skip to content

feat(profiling): add Linux CPU timer stack profiler - #18724

Draft
taegyunkim wants to merge 72 commits into
mainfrom
taegyunkim/prof-14213-timer-create
Draft

feat(profiling): add Linux CPU timer stack profiler#18724
taegyunkim wants to merge 72 commits into
mainfrom
taegyunkim/prof-14213-timer-create

Conversation

@taegyunkim

@taegyunkim taegyunkim commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Note

Thorough manual review is still in progress.

Description

Adds an opt-in Linux CPU timer path for the stack profiler on GIL-enabled CPython 3.12+. Enable it with _DD_PROFILING_STACK_CPU_TIMER_ENABLED=true; the default interval is 10 ms and the feature remains off by default.

Each Python thread receives a timer_create per-thread CPU timer that delivers SIGPROF through SIGEV_THREAD_ID. The signal handler validates that the signal came from this profiler, captures bounded raw frame and task identity data into a preallocated per-thread SPSC ring, and returns without allocating or locking. The sampler thread drains the rings, validates copied CPython data, and emits cpu-time samples with asyncio task or greenlet ancestry when the captured identity can be matched safely.

Wall sampling continues independently. When CPU timer mode is configured, wall samples stop reporting CPU time so the process does not mix accounting methods if the timer later disables itself.

Safety and lifecycle handling includes:

  • forwarding foreign SIGPROF signals to the previous handler
  • guarded CPython memory reads through the existing fault-recovery mechanism
  • permanent disable on incompatible signal masks, handler replacement, repeated capture failures, or unsupported platforms
  • thread discovery, thread churn, fork, shutdown, and one-shot restart handling
  • private diagnostics for dropped samples, timer overruns, capture failures, and disable reasons

PROF-14213

Testing

Coverage includes:

  • native tests for the SPSC sample ring and lazy lock-free TID table
  • integration tests for CPU sample emission, thread discovery/churn, fork/restart, signal ownership, handler replacement, and fault recovery
  • asyncio attribution and parent-task stitching across CPython 3.12, 3.13, and 3.14
  • greenlet attribution and logical parent stitching
  • syscall hazard reproducers for ppoll, read/readv, and nanosleep variants
  • the profiling suite with CPU timer sampling enabled under the default fast-copy configuration, plus focused fast-copy-disabled coverage
  • scripts/lint cformat, scripts/lint profiling-native-check, scripts/lint checks, and git diff --check
  • latest-main validation with CPython 3.10 and 3.12 native builds, linked-parent greenlet tests, CPU timer ring/TID-table tests, and CPU timer gevent attribution
  • post-feat(profiling): reservoir sampling for tasks #19428 validation with a complete CPython 3.10 native build and native sampling-cycle, ring, and TID-table tests
  • task-visitor validation with CPython 3.10 and 3.14 native builds and hundreds of correctly attributed, parent-stitched Python 3.14 asyncio CPU samples across repeated checks

On a CPython 3.12 workload with 501 asyncio tasks, selective task reconstruction reduced median CPU timer drain cost from about 2.82 seconds to 0.249 seconds, a 91% reduction, while preserving attribution and wall-sample recovery.

Risks

This path depends on asynchronous signals and version-specific CPython frame layouts. It is Linux-only, private, opt-in, and disabled by default. Unsupported or unsafe runtime conditions disable CPU timer sampling rather than falling back to mixed CPU accounting.

A pending SIGPROF can interrupt a native extension syscall that neither masks the signal nor retries EINTR. CPython's PEP 475 paths retry affected syscalls, and the test suite documents the remaining raw ppoll limitation with an expected-failure reproducer.

Additional Notes

#19428 now bounds the number of leaf task and greenlet samples emitted by each wall-sampling cycle. CPU timer samples remain outside that reservoir because each signal sample matches at most one captured task or greenlet and emits one CPU sample.

PRs spun out from this work

Span-to-profile correlation is covered by the separate span-attribution chain spun out from this work. It keeps correlation working for physical thread stacks, asyncio task stacks, and greenlet stacks:

Prerequisites already merged into main:

Open CI prerequisite:

Open task-discovery prerequisite:

Open max-frame prerequisite chain:

Independent test hardening already merged into main:

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jun 24, 2026

Copy link
Copy Markdown

Codeowners resolved as

Resolved from the full PR diff against main using the target branch CODEOWNERS file.
CODEOWNERS team requests not listed below are not required by the current file set.

ddtrace/internal/datadog/profiling/dd_wrapper/include/profiler_stats.hpp  @DataDog/profiling-python
ddtrace/internal/datadog/profiling/dd_wrapper/src/profiler_stats.cpp    @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/CMakeLists.txt                 @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/__init__.py                    @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/__init__.pyi                   @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/_stack.pyi                     @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/echion/echion/danger.h         @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/echion/echion/threads.h        @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/include/cpu_sample_ring.hpp    @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/include/cpu_timer.hpp          @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/include/cpu_timer_tid_table.hpp  @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/include/sampler.hpp            @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/include/stack_renderer.hpp     @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/cpu_timer.cpp              @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/echion/danger.cc           @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/echion/threads.cc          @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/sampler.cpp                @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/stack.cpp                  @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/src/stack_renderer.cpp         @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/test/CMakeLists.txt            @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/test/test_cpu_sample_ring.cpp  @DataDog/profiling-python
ddtrace/internal/datadog/profiling/stack/test/test_cpu_timer_tid_table.cpp  @DataDog/profiling-python
ddtrace/internal/settings/profiling.py                                  @DataDog/profiling-python
ddtrace/profiling/collector/stack.py                                    @DataDog/profiling-python
releasenotes/notes/prof-14213-cpu-timer-profiler.yaml                   @DataDog/apm-python
riotfile.py                                                             @DataDog/apm-python
tests/profiling/cpu_timer_native_syscall_hazard_app.py                  @DataDog/profiling-python
tests/profiling/native_cpu_timer_syscall_hazards.c                      @DataDog/profiling-python
tests/profiling/test_cpu_timer.py                                       @DataDog/profiling-python
tests/profiling/test_cpu_timer_native_syscalls.py                       @DataDog/profiling-python
tests/profiling/test_profiling_config.py                                @DataDog/profiling-python

@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 12 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-py | build linux serverless: [amd64, cp315-cp315, v126532274-233089d-musllinux_1_2_x86_64, 1]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-py | build linux serverless: [arm64, cp315-cp315, v113741357-d2b8243-manylinux2014_aarch64, 1]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-py | build linux: [amd64, cp315-cp315, v113741238-d2b8243-manylinux2014_x86_64]   View in Datadog   GitLab

View all 12 failed jobs.

❄️ 1 New flaky test detected

    test_cpu_timer_disables_when_fault_handler_is_replaced from test_cpu_timer.py   View in Datadog

View in Flaky Test Management

ℹ️ Info

No other issues found (see more)

🧪 All tests passed

🔄 Datadog auto-retried 3 jobs - 0 passed on retry View in Datadog

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 5c01fee | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Jun 24, 2026

Copy link
Copy Markdown

Benchmarks

Benchmark execution time: 2026-08-14 17:27:23

Comparing candidate commit 5c01fee in PR branch taegyunkim/prof-14213-timer-create with baseline commit 6f2816b in branch main.

📊 Benchmarking dashboard

Found 0 performance improvements and 5 performance regressions! Performance is the same for 616 metrics, 10 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:httppropagationinject-ids_only

  • 🟥 execution_time [+2.448µs; +2.668µs] or [+11.301%; +12.314%]

scenario:iastaspectsospath-ospathbasename_aspect

  • 🟥 execution_time [+114.703µs; +124.155µs] or [+27.074%; +29.305%]

scenario:span-start

  • 🟥 execution_time [+1.220ms; +1.385ms] or [+8.024%; +9.113%]

scenario:telemetryaddmetric-1-count-metric-1-times

  • 🟥 execution_time [+480.436ns; +528.839ns] or [+18.095%; +19.918%]

scenario:tracer-small

  • 🟥 execution_time [+32.460µs; +34.553µs] or [+9.956%; +10.598%]

Unstable benchmarks

These benchmarks have a confidence interval too wide to call a change; treat them as noise rather than signal.

scenario:coreapiscenario-context_with_data_listeners

  • unstable execution_time [-767.439ns; +710.737ns] or [-7.019%; +6.500%]

scenario:coreapiscenario-core_dispatch_1_listener

  • unstable execution_time [-34.657ns; +31.018ns] or [-5.691%; +5.093%]

scenario:coreapiscenario-core_dispatch_50_listeners

  • unstable execution_time [-1617.100ns; +1661.450ns] or [-9.537%; +9.799%]

scenario:coreapiscenario-core_dispatch_exception_listeners

  • unstable execution_time [-1312.228ns; +1165.895ns] or [-10.106%; +8.979%]

scenario:coreapiscenario-core_dispatch_listeners

  • unstable execution_time [-322.291ns; +326.060ns] or [-8.779%; +8.882%]

scenario:coreapiscenario-core_dispatch_no_args_listeners

  • unstable execution_time [-255.693ns; +254.726ns] or [-8.738%; +8.705%]

scenario:coreapiscenario-core_dispatch_with_results_1_listener

  • unstable execution_time [-75.048ns; +72.103ns] or [-6.486%; +6.231%]

scenario:coreapiscenario-core_dispatch_with_results_50_listeners

  • unstable execution_time [-4004.318ns; +4075.020ns] or [-9.781%; +9.953%]

scenario:coreapiscenario-core_dispatch_with_results_listeners

  • unstable execution_time [-855.364ns; +692.193ns] or [-10.487%; +8.487%]

scenario:packagesupdateimporteddependencies-import_many_stdlib_cached

  • unstable execution_time [-42952.781ns; +41830.580ns] or [-6.729%; +6.553%]

@r1viollet

r1viollet commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

@taegyunkim do you need help reviewing on this PR ?

@taegyunkim taegyunkim left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@r1viollet I'd want to make more changes to this PR. Will let you know when review is needed!

@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Jul 9, 2026

Copy link
Copy Markdown

Circular import analysis

⚠️ Existing circular imports

There are 5 circular imports that already exist on the base branch and have not been changed by this PR.

ddtrace.contrib.internal.django.patch -> ddtrace.contrib.internal.django.response -> ddtrace.contrib.internal.django.patch
ddtrace.contrib.internal.pytorch._distributed -> ddtrace.contrib.internal.pytorch._rank_root -> ddtrace.contrib.internal.pytorch._distributed
ddtrace.llmobs -> ddtrace.llmobs._evaluators -> ddtrace.llmobs._evaluators.format -> ddtrace.llmobs._experiment -> ddtrace.llmobs
ddtrace.errortracking._handled_exceptions.bytecode_injector -> ddtrace.errortracking._handled_exceptions.callbacks -> ddtrace.errortracking._handled_exceptions.collector -> ddtrace.errortracking._handled_exceptions.bytecode_reporting -> ddtrace.errortracking._handled_exceptions.bytecode_injector
ddtrace.appsec._asm_request_context -> ddtrace.appsec._iast._iast_request_context_base -> ddtrace.appsec._iast._iast_env -> ddtrace.appsec._iast.reporter -> ddtrace.appsec._exploit_prevention.stack_traces -> ddtrace.appsec._asm_request_context

Comment thread ddtrace/internal/datadog/profiling/docs/timer_create.md Outdated
@taegyunkim
taegyunkim force-pushed the taegyunkim/prof-14213-timer-create branch from 351ca0e to 3ebcfb0 Compare July 13, 2026 17:19
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 13, 2026
## Description

Pure refactor of existing stack sampler internals, with no intended behavior or internal contract changes, in preparation for timer_create based cpu time profiling in #18724 

Specifically:

- Extracts the existing task/greenlet/thread-stack rendering branch in `ThreadInfo::sample()` into a local `render_unwound_stacks()` helper.
- Extracts duplicate one-time thread registration failure logging in `Sampler::register_thread()` into a local helper.

This does not introduce CPU timer profiling, new configuration, new native bindings, or changes to thread registration semantics.

## Testing

- `scripts/lint cformat`

Attempted a targeted profiling test run from the worktree, but the test runner failed during riot venv setup before executing tests due the worktree using `/home/bits/project/.riot`, which was not writable/available in that context.

## Risks

Low. This is intended to be behavior-preserving refactoring only.

## Additional Notes

No changelog needed, internal refactor only.


Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
taegyunkim and others added 5 commits July 13, 2026 20:15
Explain why ThreadAltStack adopts a pre-existing alternate signal stack
instead of replacing it: the application, CPython's faulthandler, or
libdatadog's crashtracker (which by default creates and owns a larger alt
stack) may have installed it, and the destructor must never disable or free
an alt stack this object did not allocate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a native gtest for ThreadAltStack: an adopted (pre-existing) alternate
signal stack must not be disabled on thread-local cleanup, while a stack this
helper allocated is disabled and freed. The adopted-stack case fails without
the owns_mapping guard, so it locks in this PR's behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d it

ThreadAltStack's destructor disabled whatever alternate signal stack was
installed on the thread whenever it owned a mapping, without checking that the
installed stack was still the one it installed. If another component (for
example libdatadog crashtracker) replaced the thread's alt stack after we
installed ours, teardown would strip that replacement, degrading the other
owner's fault handling. Only disable the alt stack when it is still ours
(cur.ss_sp == mem); free our own mapping regardless.

Adds a regression test (DestructorDoesNotDisableReplacedAltStack) that fails
without this guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 14, 2026
## Description

The profiling safe-copy helper uses a per-thread alternate signal stack for its `SIGSEGV` and `SIGBUS` handlers. When an alternate stack was already installed by the application, CPython's faulthandler, or crashtracker, `ThreadAltStack` reused it but still disabled it during thread-local teardown. That could leave the original owner without its alternate stack and weaken its fault handling.

Track whether `ThreadAltStack` allocated the mapping itself. Teardown now disables and unmaps only profiler-owned stacks, while adopted stacks remain installed for their original owner. The installation failure path also unmaps a profiler allocation when `sigaltstack()` fails.

A release note documents the fault-handling fix.

## Testing

- Passed locally: `ddtrace/internal/datadog/profiling/build_standalone.sh -- RelWithDebInfo stack_test`
  - 4/4 native CTests passed
  - verifies that an adopted alternate stack remains installed after `ThreadAltStack` teardown
  - verifies that a profiler-owned alternate stack is disabled during teardown
- `scripts/lint cformat`

## Risks

Low. The change narrows teardown to resources owned by the profiler and does not alter public APIs or configuration. Profiler-owned alternate stacks retain their existing cleanup behavior.

## Additional Notes

Stacked out from the CPU timer profiler work, #18724, to keep this native safety fix independently reviewable.

Follow-up PR #19026 handles the complementary case where another component replaces a profiler-owned alternate stack after the profiler installs it.

[PROF-14213](https://datadoghq.atlassian.net/browse/PROF-14213)


[PROF-14213]: https://datadoghq.atlassian.net/browse/PROF-14213?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ

Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Jul 30, 2026
## Description

Strengthens the existing uWSGI `--lazy-apps` test without `--master` by checking the PID returned from `waitpid()` and rejecting signal-based termination.

This closes a test blind spot where the process could crash during shutdown while the test continued to validate generated profile samples. The sibling worker is cleaned up before the captured exit status is asserted, including on the failure path. The change is extracted from #18724 because it is independent of the CPU timer profiler.

## Testing

- `scripts/run-tests --venv 1ef9287 -- -s -- -k test_uwsgi_threads_processes_no_primary_lazy_apps`
  - `1 passed, 12 deselected` on Python 3.13.13 with `uwsgi<2.0.30`
- `scripts/lint fmt -- tests/profiling/test_uwsgi.py`
- `scripts/lint checks`
- `git diff --check`

## Risks

None. This only makes an existing test detect process crashes that it previously overlooked.

## Additional Notes

No release note is needed because this is a test-only change.



Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
brettlangdon pushed a commit that referenced this pull request Aug 3, 2026
## Description

Strengthens the existing uWSGI `--lazy-apps` test without `--master` by checking the PID returned from `waitpid()` and rejecting signal-based termination.

This closes a test blind spot where the process could crash during shutdown while the test continued to validate generated profile samples. The sibling worker is cleaned up before the captured exit status is asserted, including on the failure path. The change is extracted from #18724 because it is independent of the CPU timer profiler.

## Testing

- `scripts/run-tests --venv 1ef9287 -- -s -- -k test_uwsgi_threads_processes_no_primary_lazy_apps`
  - `1 passed, 12 deselected` on Python 3.13.13 with `uwsgi<2.0.30`
- `scripts/lint fmt -- tests/profiling/test_uwsgi.py`
- `scripts/lint checks`
- `git diff --check`

## Risks

None. This only makes an existing test detect process crashes that it previously overlooked.

## Additional Notes

No release note is needed because this is a test-only change.



Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 10, 2026

Copy link
Copy Markdown

Dependency direction analysis

⚠️ Existing dependency direction violations

There are 255 dependency direction violations that already exist on the base branch and have not been changed by this PR.

Show existing violations (showing 5 of 255 highest severity)
ddtrace.internal.tracemethods -×-> ddtrace.trace  (internal-core -> product:tracing, score=134)
ddtrace.llmobs._integrations.claude_agent_sdk -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=132)
ddtrace.llmobs._evaluators.runner -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=132)
ddtrace.llmobs._integrations.google_adk -×-> ddtrace.trace  (product:llmobs -> product:tracing, score=132)
ddtrace.internal.openfeature._span_enrichment -×-> ddtrace.trace  (product:openfeature -> product:tracing, score=132)

To see all violations, download the layers-base.json and layers-pr.json artifacts from this CI job and run:

uv run --script scripts/import-analysis/layers.py compare layers-base.json layers-pr.json

@taegyunkim
taegyunkim force-pushed the taegyunkim/prof-14213-timer-create branch from ca1a6da to e276144 Compare August 10, 2026 17:39
@taegyunkim
taegyunkim force-pushed the taegyunkim/prof-14213-timer-create branch from e276144 to 1847103 Compare August 10, 2026 17:40
gh-worker-dd-mergequeue-cf854d Bot pushed a commit that referenced this pull request Aug 13, 2026
## Description

Consolidates the two native frame updates performed for each greenlet switch into one private `record_greenlet_switch()` call and one `greenlet_info_map` mutex acquisition.

The existing behavior is preserved: the origin frame is always updated, while the running target frame is left unchanged when it is needed for parent-greenlet stack unwinding.

This is an independently mergeable prerequisite extracted from #18724.

## Testing

- `scripts/lint fmt -- ddtrace/profiling/_gevent.py tests/profiling/test_gevent.py tests/profiling/collector/test_stack.py`
- `scripts/lint cformat`
- `scripts/lint profiling-native-check`
- `scripts/lint checks`
- `git diff --check`
- Fresh CPython 3.12 native extension build
- Native linked-parent switch regression covering preserved and updated target frames
- Existing high-cardinality greenlet switch contention regression

### Benchmark

The benchmark isolates the native operation changed by this PR. Each measured run performs 1,000,000 logical greenlet switch updates after 10,000 warm-up updates:

- baseline `e11d640110`: two `update_greenlet_frame()` native calls and two mutex acquisitions per update
- candidate `93702898f8`: one `record_greenlet_switch()` native call and one mutex acquisition per update

Both `_stack` extensions were built in Release mode from the exact commits, then run in the same container pinned to CPU 0 with `--cpuset-cpus=0`.

Results over five runs:

- before: 0.531681 seconds median, runs `[0.528339, 0.531681, 0.532128, 0.534566, 0.529234]`
- after: 0.366206 seconds median, runs `[0.380415, 0.368066, 0.360803, 0.362898, 0.366206]`
- change: approximately 31% faster for the isolated native update

Environment:

- AWS KVM VM, Intel Xeon Platinum 8175M at 2.50 GHz
- 16 vCPUs, 8 cores with 2 threads per core, 61 GiB RAM
- Linux 6.8.0-1055-aws, x86-64
- Docker server 29.5.2, container pinned to one CPU
- CPython 3.12.13, GCC 14.2.0, glibc 2.41
- container image `sha256:ce7c46dbb7f07d352aecd756e19fd7f39550a72d4a13e68d30f02f5448eebd69`

<details>
<summary>Benchmark script</summary>

```python
import statistics
import time

from ddtrace.internal.datadog.profiling import stack


ITERATIONS = 1_000_000
RUNS = 5
ORIGIN_ID = 101
TARGET_ID = 102

stack.track_greenlet(ORIGIN_ID, "origin", False)
stack.track_greenlet(TARGET_ID, "target", False)

if hasattr(stack, "record_greenlet_switch"):

    def update():
        stack.record_greenlet_switch(ORIGIN_ID, False, TARGET_ID, None, True)

else:

    def update():
        stack.update_greenlet_frame(ORIGIN_ID, False)
        stack.update_greenlet_frame(TARGET_ID, None)


def run_once():
    start = time.perf_counter()
    for _ in range(ITERATIONS):
        update()
    return time.perf_counter() - start


for _ in range(10_000):
    update()
runs = [run_once() for _ in range(RUNS)]

stack.untrack_greenlet(ORIGIN_ID)
stack.untrack_greenlet(TARGET_ID)

print(f"median={statistics.median(runs):.6f}s runs={runs}")
```

</details>

This is an isolated native-call benchmark, not an end-to-end application throughput claim. The earlier 67% figure was discarded after reproducing with exact baseline and candidate builds because the candidate run had not activated the profiling hook.

## Risks

Low. The changed Python and native APIs are private and ship together. Existing parent-greenlet frame retention remains unchanged.

## Additional Notes

The GIL release around native state mutation is retained intentionally. It was introduced by #14852 in commit `54a3a0ea35` to prevent potential thread-pool deadlocks while waiting for profiler mutexes. This change reduces two such release windows per greenlet switch to one.

No release note is needed because this is an internal performance optimization with no user-facing API or behavior change.






Co-authored-by: taegyun.kim <taegyun.kim@datadoghq.com>
…-timer-create

# Conflicts:
#	ddtrace/internal/datadog/profiling/stack/src/sampler.cpp
#	ddtrace/profiling/_gevent.py
…-timer-create

# Conflicts:
#	ddtrace/internal/datadog/profiling/stack/__init__.pyi
#	ddtrace/internal/datadog/profiling/stack/_stack.pyi
#	ddtrace/internal/datadog/profiling/stack/echion/echion/threads.h
#	ddtrace/internal/datadog/profiling/stack/include/stack_renderer.hpp
#	ddtrace/internal/datadog/profiling/stack/src/stack.cpp
#	ddtrace/internal/datadog/profiling/stack/src/stack_renderer.cpp
#	ddtrace/profiling/collector/stack.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants