Skip to content

perf(profiling): reuse TSRM cache in allocation hook - #4072

Open
realFlowControl wants to merge 38 commits into
masterfrom
florian/prof-15506-reuse-tsrm-cache
Open

perf(profiling): reuse TSRM cache in allocation hook#4072
realFlowControl wants to merge 38 commits into
masterfrom
florian/prof-15506-reuse-tsrm-cache

Conversation

@realFlowControl

@realFlowControl realFlowControl commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

Reuse one ZTS local-storage cache pointer to access both profiler globals and executor globals in the PHP 8.3-and-older allocation callback. This removes one tsrm_get_ls_cache() call and the C current_execute_data wrapper per allocation while preserving the post-allocation executor-global read.

PHP 7.1 through 7.3 access executor globals through executor_globals_id; PHP 7.4 and newer use the fast executor_globals_offset. Both paths reuse the same cached TSRM storage pointer in either RINIT-selected normal or neighboring custom malloc callback.

This PR is stacked on #4070 and contains only the third allocation-profiler optimization.

Across six balanced 60-second runs per binary on PHP 8.3 ZTS:

  • Mean throughput: 32,677,620/s to 34,432,538/s (+5.37%)
  • Median throughput: 32,651,548/s to 34,461,652/s (+5.54%)
  • Selected TLS and wrapper sampled self time: 17.74% to 12.95% (-27.0% relative)

Validation after syncing #4070's malloc/free/realloc callback selection:

  • PHP 7.3 ZTS release build, allocation smoke test, and cargo test: 22 passed
  • PHP 8.3 ZTS release build and cargo test: 22 passed
  • PHP 8.5 NTS release build and cargo test: 22 passed; cargo test --all-features on Linux/macOS: 24 passed
  • macOS legacy executor-globals linker check and PHP 8.5 NTS release build: passed
  • PHP 8.3/8.5 allocation PHPTs on the parent: 5 passed each

Reviewer checklist

  • Test coverage seems ok.
  • Appropriate labels assigned.

https://datadoghq.atlassian.net/browse/PROF-15506

Read executor_globals.current_execute_data directly on NTS builds instead of calling through the C FFI wrapper for every allocation and reallocation. Keep the existing wrapper on ZTS builds.

A 60-second macOS sample reduced get_current_execute_data self time from 0.412% (207/50,210 main-thread samples) to 0.002% (1/49,737). Known allocation-hook self time fell from 4.184% to 3.601%, a 13.9% relative reduction. Repeated end-to-end throughput remained within system scheduling noise.

Validation: cargo test (22 passed); allocation sampling-distance, memory-peak, and GC PHPTs passed.
Forward PHP 8.4+ allocations and frees from a single ZendMMState read instead of loading a function pointer, making an indirect Rust call, and reloading the state. Preserve neighboring custom allocator support with a predictable previous-handler check.

Use unchecked heap extraction in the callbacks: rinit stores the heap before they can be invoked, and rshutdown removes them before clearing it. This avoids an Option discriminant check on every allocation and free.

Across six 60-second runs per binary, mean allocation throughput increased from 9,366,444/s to 10,018,262/s (+6.96%) and median throughput increased by 7.56%. A 60-second native sample reduced known forwarding-path self time from 3.599% to 2.438% (-32.3% relative).

Validation: cargo test (22 passed); allocation sampling-distance, memory-peak, and GC PHPTs passed.

https://datadoghq.atlassian.net/browse/PROF-15506
Apply the single-state allocation and free forwarding path to PHP 8.3 and older. Preserve the required ZendMM prepare/restore calls and neighboring custom allocator support while removing the intermediate function pointers and wrappers. On ZTS this also avoids repeated TSRM lookups.

Across six balanced 60-second runs per binary on PHP 8.3 ZTS, mean allocation throughput increased from 28,079,883/s to 32,661,234/s (+16.32%) and median throughput increased by 16.65%. A full 60-second native sample reduced known forwarding-path self time from 16.31% to 12.30% (-24.6% relative).

Validation: PHP 8.3 ZTS cargo test (22 passed); allocation sampling-distance, memory-peak, and GC PHPTs passed; PHP 8.5 release build and cargo test (22 passed).

https://datadoghq.atlassian.net/browse/PROF-15506
Reuse one ZTS local-storage cache pointer to access both profiler globals and executor globals in the PHP 8.3-and-older allocation callback. This removes one TSRM lookup and the C current_execute_data wrapper per allocation while preserving the post-allocation executor-global read.

Across six balanced 60-second runs per binary on PHP 8.3 ZTS, mean allocation throughput increased from 32,677,620/s to 34,432,538/s (+5.37%) and median throughput increased by 5.54%. Full native samples reduced selected TLS and wrapper self time from 17.74% to 12.95% (-27.0% relative).

Validation: PHP 8.3 ZTS cargo test (22 passed); allocation sampling-distance, memory-peak, and GC PHPTs passed; PHP 8.5 release build and cargo test (22 passed).

https://datadoghq.atlassian.net/browse/PROF-15506
@realFlowControl realFlowControl added the profiling Relates to the Continuous Profiler label Jul 27, 2026
The free-handler selection test retains allocation callbacks that now call _zend_mm_free directly, but Rust unit-test binaries are not loaded by PHP. Provide a test-only stub with release and debug PHP signatures so both ZendMM API implementations link.

Verified with cargo test on PHP 8.5 NTS and PHP 8.3 ZTS; both pass 22 tests.
The free-handler selection test retains allocation callbacks that now call _zend_mm_free directly, but Rust unit-test binaries are not loaded by PHP. Provide a test-only stub with release and debug PHP signatures so both ZendMM API implementations link.

Verified with cargo test on PHP 8.5 NTS and PHP 8.3 ZTS; both pass 22 tests.
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Jul 27, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 6 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | test_extension_ci: [8.1]   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-php | merge-gate   View in Datadog   GitLab

DataDog/apm-reliability/dd-trace-php | publish docker image for system tests   View in Datadog   GitLab

View all 6 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🔄 Datadog auto-retried 1 job - 1 passed on retry View in Datadog

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 60.64% (-0.01%)

Useful? React with 👍 / 👎

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

@pr-commenter

pr-commenter Bot commented Jul 27, 2026

Copy link
Copy Markdown

Benchmarks [ profiler ]

Benchmark execution time: 2026-07-31 17:52:16

Comparing candidate commit 616427c in PR branch florian/prof-15506-reuse-tsrm-cache with baseline commit af463f7 in branch master.

Found 0 performance improvements and 2 performance regressions! Performance is the same for 25 metrics, 9 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:walk_stack/1

  • 🟥 wall_time [+874.938ns; +878.740ns] or [+7.107%; +7.138%]

scenario:walk_stack/99

  • 🟥 wall_time [+383.110ns; +391.571ns] or [+2.484%; +2.539%]

@pr-commenter

pr-commenter Bot commented Jul 27, 2026

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-07-28 19:26:33

Comparing candidate commit 85850cc in PR branch florian/prof-15506-reuse-tsrm-cache with baseline commit 010c2f7 in branch florian/prof-15506-faster-allocation-forwarding.

Found 2 performance improvements and 4 performance regressions! Performance is the same for 187 metrics, 1 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:ContextPropagationBench/benchInject64Bit-opcache

  • 🟥 execution_time [+1.960µs; +2.324µs] or [+15.008%; +17.790%]

scenario:MessagePackSerializationBench/benchMessagePackSerialization

  • 🟩 execution_time [-7.474µs; -6.526µs] or [-6.807%; -5.944%]

scenario:MessagePackSerializationBench/benchMessagePackSerialization-opcache

  • 🟩 execution_time [-9.230µs; -7.010µs] or [-8.071%; -6.129%]

scenario:PDOBench/benchPDOOverhead

  • 🟥 execution_time [+10.412µs; +13.581µs] or [+4.053%; +5.286%]

scenario:PDOBench/benchPDOOverheadWithDBM

  • 🟥 execution_time [+10.358µs; +13.228µs] or [+4.044%; +5.164%]

scenario:PHPRedisBench/benchRedisOverhead

  • 🟥 execution_time [+47.254µs; +63.494µs] or [+4.668%; +6.272%]

Forward reallocations from one ZendMMState read instead of loading a function pointer, making an indirect Rust call, and reloading state. Preserve neighboring custom allocator support and the PHP 8.3-and-older heap prepare/restore calls.

Remove the realloc forwarding field, intermediate wrappers, and initialization panic callback from both ZendMM implementations.

Validation: PHP 8.5 NTS and PHP 8.3 ZTS cargo test (22 passed each); five allocation PHPTs passed on both builds.

https://datadoghq.atlassian.net/browse/PROF-15506
Forward reallocations from one ZendMMState read instead of loading a function pointer, making an indirect Rust call, and reloading state. Preserve neighboring custom allocator support and the PHP 8.3-and-older heap prepare/restore calls.

Remove the realloc forwarding field, intermediate wrappers, and initialization panic callback from both ZendMM implementations.

Validation: PHP 8.5 NTS and PHP 8.3 ZTS cargo test (22 passed each); five allocation PHPTs passed on both builds.

https://datadoghq.atlassian.net/browse/PROF-15506
PHP 7.1 through 7.3 expose executor globals through executor_globals_id instead of the fast offset introduced in PHP 7.4. Reuse the cached TSRM storage through the resource-ID path on those versions and keep the offset path for PHP 7.4 and newer.

Provide a test-only tsrm_get_ls_cache stub because Rust unit-test binaries are not loaded by PHP.

Validation: PHP 7.3 ZTS cargo test (22 passed), release build, and allocation smoke test; PHP 8.3 ZTS and PHP 8.5 NTS cargo test (22 passed each).

https://datadoghq.atlassian.net/browse/PROF-15506
@realFlowControl realFlowControl added AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos Overhead Relates to latency, CPU, or memory overhead labels Jul 28, 2026
On PHP 8.3 and older, the execute_internal hook called the full profiler interrupt handler after every internal function. Check EG(vm_interrupt) first so the TLS, request-state, and atomic work only runs when PHP has a pending VM interrupt.

Across six balanced 60-second runs per binary on PHP 8.3 ZTS, mean allocation-loop throughput increased from 34,160,664/s to 36,061,991/s (+5.57%) and median throughput increased by 5.92%. Native samples reduced the manual interrupt path from 14.04% to 8.62% of main-thread samples.

Wall-time correctness remained unchanged: a four-second CPU/sleep workload attributed 50.23% to sleep before and 50.31% after. Validation: PHP 7.3 ZTS cargo check; PHP 8.3 ZTS and PHP 8.5 NTS cargo test (22 passed each).

https://datadoghq.atlassian.net/browse/PROF-15506
On PHP 8.3 and older, select the direct ZendMM or neighboring custom allocator callback once during rinit. The normal allocation callback no longer loads or branches on prev_custom_mm_alloc for every allocation; the custom allocator callback remains as a documented cold compatibility path.

Across six balanced 60-second PHP 8.3 ZTS runs per binary, mean throughput increased from 31,167,496/s to 32,646,366/s (+4.74%) and median throughput increased by 4.56%. Native samples reduced alloc_prof_malloc self time from 6.13% to 3.54% (-42.3% relative).

The equivalent PHP 8.5 NTS experiment measured within system noise (+0.79% mean / +0.39% median) and was not retained.

Validation: PHP 8.5 NTS cargo test (22 passed), PHP 8.3 ZTS and PHP 7.3 ZTS cargo check, and five PHP 8.3 allocation PHPTs.

https://datadoghq.atlassian.net/browse/PROF-15506
…lorian/prof-15506-reuse-tsrm-cache

# Conflicts:
#	profiling/src/allocation/allocation_le83.rs
Apply the same RINIT-selected allocation callback used for PHP 8.3 and older to PHP 8.4 and newer. The normal callback has no previous-allocator load or branch, while a separate cold callback preserves neighboring custom allocator support.

The PHP 8.5 NTS benchmark was inconclusive but non-negative: a clean six-run retry measured +0.40% mean and +0.69% median with about 1.8% run variance. Keep the implementation for symmetry across ZendMM APIs rather than as a claimed performance win.

Validation: PHP 8.5 NTS cargo test (22 passed) and profiler-release build.

https://datadoghq.atlassian.net/browse/PROF-15506
Base automatically changed from florian/prof-15506-faster-allocation-forwarding to master July 28, 2026 19:56
…reuse-tsrm-cache

# Conflicts:
#	profiling/src/allocation/allocation_le83.rs
@morrisonlevi
morrisonlevi marked this pull request as ready for review July 28, 2026 20:04
@morrisonlevi
morrisonlevi requested a review from a team as a code owner July 28, 2026 20:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce7adb22ad

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread profiling/src/allocation/mod.rs
morrisonlevi and others added 15 commits July 28, 2026 18:05
This is very similar to the approach taken by Florian, which did a
check for EG(vm_interrupt). Although I was able to reproduce a small
speedup there, I realized that it's more nuanced than that:

 1. This doesn't apply to actual VM interrupts, only to things which
    need to check for a pending interrupt.
 2. EG(vm_interrupt) technically isn't related to the thing we care
    about, which is the interrupt count.

So this adds ddog_php_prof_interrupt_function_unlikely which is the
same as ddog_php_prof_interrupt_function at a high level, but it is
optimized to assume that there isn't a pending interrupt (opposite of
ddog_php_prof_interrupt_function).
And use system settings from the global, rather than through the
REQUEST_LOCALS.
These changes specifically speed up the hot paths of the interrupt
function. Notably, this can be a bit hot on 8.3 and below because of
execute_internal, and it's used on frameless functions on 8.4+. The
biggest change comes from avoiding REQUEST_LOCALS. In this case, this
is totally safe because interrupt_count is atomic.

This also removes the profiling_enabled check of the interrupt
function. This is unnecessary because:

 1. We do not set interrupts when disabled to begin with, generally.
    trigger_time_sample can still trigger but this is test only.
 2. For edge cases having a pending interrupt going into a fork, the
    child will call `Profiler::kill()` which will cause
    `Profiler::get()` to return None.

@morrisonlevi morrisonlevi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I ported the changes to 8.4+ plus as well. I did a tiny bit of refactoring but not too much, will leave the rest of the refactoring another PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos Overhead Relates to latency, CPU, or memory overhead profiling Relates to the Continuous Profiler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants