Skip to content

perf(prof): speed up interrupt_count checks - #4074

Merged
realFlowControl merged 12 commits into
masterfrom
florian/prof-15506-guard-internal-interrupt
Jul 30, 2026
Merged

perf(prof): speed up interrupt_count checks#4074
realFlowControl merged 12 commits into
masterfrom
florian/prof-15506-guard-internal-interrupt

Conversation

@realFlowControl

@realFlowControl realFlowControl commented Jul 28, 2026

Copy link
Copy Markdown
Member

Description

Speed up interrupt_count checks by making it Relaxed and moving it to the ProfilerGlobals instead of using REQUEST_LOCALS. The biggest change comes from avoiding REQUEST_LOCALS, which in this case is totally safe because interrupt_count is atomic.

Notably, part of the interrupt handler checks can be a bit hot in certain cases where we don't expect there to be an interrupt very often:

  • PHP <=8.3: execute_internal wrapper
  • PHP 8.4+: frameless functions

So we want the "0 interrupt count" check to be as fast and as early as possible.

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.

These changes were inspired by and directly derived from iterating on Florian's original proposal (see Original description below). My (Levi) own benchmarks are similar to his (I hope), here is throughput:

  • Mean: 22,773,158/s → 24,067,515/s (+5.68%)
  • Median: 22,810,705/s → 23,913,084/s (+4.83%)
  • 95% Confidence interval: +1.09% to +10.44%
Original description

Profiler interrupt checks run after internal, traced, and frameless calls even though a profiler interrupt is usually not pending. Export an unlikely entry point that reads the profiler-owned per-thread interrupt_count with Ordering::Relaxed and returns immediately when it is zero; only a pending interrupt reaches the authoritative SeqCst swap and stack collection. The regular Zend VM interrupt callback remains unchanged.

Move interrupt_count from REQUEST_LOCALS to PHP module globals so the hot path avoids borrowing request locals. The PHP 8.3-and-older internal-function hook and tracer hook call sites use the new entry point; PHP 8.4+ frameless handling uses the same relaxed precheck. Tracer symbol lookup falls back to the old entry point for compatibility with older profiler versions.

Benchmark

Fresh release builds from master (24c420794) and this branch (32abe928a), using six balanced 60-second runs per binary on PHP 8.3 ZTS, macOS ARM64:

  • Mean throughput: 32,155,879/s to 34,645,984/s (+7.74%)
  • Median throughput: 32,308,450/s to 35,053,828/s (+8.50%)
  • Every paired run improved: +5.72% to +9.10%
  • Fresh 70-second native samples reduced interrupt entry-point top-of-stack samples from 4,335 to 1,096 (-74.7% relative)

Wall-time correctness on a four-second CPU/sleep workload remained stable:

  • Master: 4.00s total; sleep(): 2.01s (50.37%)
  • Candidate: 4.01s total; sleep(): 2.012s (50.16%)

Reviewer checklist

  • Test coverage seems ok.
  • Appropriate labels assigned.

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

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
@realFlowControl realFlowControl added the profiling Relates to the Continuous Profiler label Jul 28, 2026
@datadog-datadog-prod-us1

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

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

🚦 7 Pipeline jobs failed

DataDog/apm-reliability/dd-trace-php | pecl tests: [8.3]   View in Datadog   GitLab

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

DataDog/apm-reliability/dd-trace-php | ASAN test_c with multiple observers: [8.4]   View in Datadog   GitLab

View all 7 failed jobs.

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 59.64% (-1.03%)

Useful? React with 👍 / 👎

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

@realFlowControl realFlowControl added Overhead Relates to latency, CPU, or memory overhead AI Generated Largely based on code generated by an AI or LLM. This label is the same across all dd-trace-* repos labels Jul 28, 2026
@pr-commenter

pr-commenter Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmarks [ profiler ]

Benchmark execution time: 2026-07-30 11:44:59

Comparing candidate commit ac2e342 in PR branch florian/prof-15506-guard-internal-interrupt with baseline commit e3b075e in branch master.

Found 0 performance improvements and 0 performance regressions! Performance is the same for 27 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 ----------------------------------'

@pr-commenter

pr-commenter Bot commented Jul 28, 2026

Copy link
Copy Markdown

Benchmarks [ tracer ]

Benchmark execution time: 2026-07-28 19:42:54

Comparing candidate commit be25644 in PR branch florian/prof-15506-guard-internal-interrupt with baseline commit 659848d in branch master.

Found 2 performance improvements and 0 performance regressions! Performance is the same for 192 metrics, 0 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:MessagePackSerializationBench/benchMessagePackSerialization

  • 🟩 execution_time [-5.720µs; -3.940µs] or [-5.004%; -3.446%]

scenario:MessagePackSerializationBench/benchMessagePackSerialization-opcache

  • 🟩 execution_time [-7.977µs; -5.583µs] or [-6.889%; -4.822%]

@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.

Early on in ddog_php_prof_interrupt_function we check the interrupt count being greater-than 0. I am surprised this helped so much. I'm going to play with it a bit locally.

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).
@morrisonlevi
morrisonlevi force-pushed the florian/prof-15506-guard-internal-interrupt branch from 84d0bcd to 8cb3cde Compare July 29, 2026 00:13
@morrisonlevi

morrisonlevi commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

I could not reproduce such a significant gain! Florian, will you pull my changes and see what those give on your benchmarks? My changes performed more consistently for me locally, and keep the same spirit (fast return if there isn't a pending interrupt).

The reason ddog_php_prof_interrupt_function_unlikely is faster for its callers than ddog_php_prof_interrupt_function is because it uses a load(Relaxed) of the interrupt_count. This avoids a write synchronization that the store(0, SeqCst) version incurs even if there wasn't an interrupt. The EG(vm_interrupt) check you did would do the same, but we don't actually care about EG(vm_interrupt), only interrupt_count.

I also move the interrupt_count into ProfilerGlobals and access SystemSettings through the global so we don't have to use REQUEST_LOCALS at all on the hot path here.

Nine pinned 60-second runs each, PHP 8.3 ZTS on my benchmark:

  • Mean throughput: 23,174,141/s → 24,435,029/s (+5.44%)
  • Median throughput: 23,200,257/s → 24,290,987/s (+4.70%)
  • Paired improvement: +5.51%
  • 95% CI: +2.08% to +8.95%

And use system settings from the global, rather than through the
REQUEST_LOCALS.
@realFlowControl
realFlowControl marked this pull request as ready for review July 29, 2026 06:44
@realFlowControl
realFlowControl requested review from a team as code owners July 29, 2026 06:44

@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: 32abe928af

ℹ️ 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/wall_time.rs Outdated
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 changed the title perf(profiling): skip idle internal interrupt handling perf(prof): use TSRM globals for interrupt_count, Ordering::Relaxed Jul 29, 2026
@morrisonlevi morrisonlevi changed the title perf(prof): use TSRM globals for interrupt_count, Ordering::Relaxed perf(prof): speed up interrupt_count checks Jul 29, 2026

@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 approve of this PR--I basically rewrote it! Florian, please review. I made sure to preserve your commit history. Without your work here this wouldn't haven't happened. Also, some of the TSRM stuff was pulled from another one of your PRs, so you do have code in here still despite the reverted commits!

@realFlowControl realFlowControl left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I can't formally approve this PR as the author 😉
But here we are: ✅

About the two nitpicks: I am pretty sure the compiler will just inline them, so feel free to ignore. One upside (that I am not sure about) could be that in a debug build we might actually not inline and have the symbol available.

Comment thread profiling/src/wall_time.rs
Comment thread profiling/src/module_globals.rs
Comment thread profiling/src/module_globals.rs
@realFlowControl
realFlowControl merged commit 47bcde2 into master Jul 30, 2026
2151 of 2159 checks passed
@realFlowControl
realFlowControl deleted the florian/prof-15506-guard-internal-interrupt branch July 30, 2026 14:55
@github-actions github-actions Bot added this to the 1.24.0 milestone Jul 30, 2026
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 tracing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants