Skip to content

Reduce virtual-thread context-propagation overhead on park/unpark - #11893

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 21 commits into
masterfrom
andrea.marziali/vthread-context-perf
Sep 23, 2026
Merged

gh-worker-dd-mergequeue-cf854d[bot] merged 21 commits into
masterfrom
andrea.marziali/vthread-context-perf

Conversation

@amarziali

@amarziali amarziali commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

What Does This Do

Reduces virtual-thread context-propagation overhead on JDK 22+ by avoiding full trace-context swaps on every mount and unmount.

The trace scope stack is stored in a virtual-thread-aware ThreadLocal, so it follows the virtual thread across parking and carrier migration. It is seeded once when the virtual thread starts.

ddprof context remains carrier-thread-local. When carrier-bound profiling is active, it is rebound on mount and cleared on unmount.

JDK 21 retains the existing per-mount context-swap path because its virtual-thread lifecycle differs across update releases.

Motivation

The existing path restores the saved trace context on every mount and unmount. Although it reuses the saved scope stack, each cycle still performs two full context swaps and allocates context wrappers.

Benchmark

JMH on JDK 21. The current-path benchmark retains the context returned by each reverse swap, matching repeated production mount/unmount cycles.

Throughput uses 8 threads. Allocation uses a single-thread control run to avoid cross-thread effects.

Design · profiling Throughput, 8 threads (ops/µs) Allocation, 1 thread (B/op)
Current · off 732.232 ± 34.589 32.004
Proposed · off 4,969.171 ± 130.247 ~0
Current · Java profiling stub 496.360 ± 30.376 32.007
Proposed · Java profiling stub 1,118.481 ± 35.419 ~0

In this Java-side microbenchmark, the proposed path provides approximately:

  • 6.8× higher throughput with profiling disabled
  • 2.3× higher throughput with the profiling stub
  • 32 B less allocation per mount/unmount cycle

The profiling stub matches ddprof's shared integration lifetime, per-thread isolation, and set/clear pattern. It does not execute native ddprof calls, so profiling-on throughput is not an estimate of production ddprof performance.

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
• Patch Coverage: 0.00%
• Overall Coverage: 59.06% (-0.04%)

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

@dd-octo-sts

dd-octo-sts Bot commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

Suite Status
Startup 🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 14.77 s 14.72 s [-0.7%; +1.4%] (no difference)
startup:insecure-bank:tracing:Agent 13.67 s 13.73 s [-1.1%; +0.3%] (no difference)
startup:petclinic:appsec:Agent 17.53 s 17.32 s [+0.4%; +2.1%] (maybe worse)
startup:petclinic:iast:Agent 17.39 s 17.02 s [-2.2%; +6.5%] (no difference)
startup:petclinic:profiling:Agent 17.30 s 17.39 s [-1.9%; +0.8%] (no difference)
startup:petclinic:sca:Agent 17.43 s 17.42 s [-1.1%; +1.2%] (no difference)
startup:petclinic:tracing:Agent 16.49 s 16.67 s [-2.0%; -0.2%] (maybe better)

Commit: e917d529 · CI Pipeline · Benchmarking Platform UI


Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion.

@mcculls
mcculls self-requested a review July 9, 2026 09:08
@mcculls

mcculls commented Jul 9, 2026 •

Copy link
Copy Markdown
Contributor

As discussed the inefficiency in ContinuableScopeManager.swap is an artefact of having to wrap the current ScopeStack in the returned response.

We have to do this because all we know is that the caller will eventually pass that same context back into swap  (that's the essential contract of this call.)

It's also why we need to create a new ScopeStack  for the incoming context - basically the caller is saying preserve the current state of the stack and restore it when I call swap again. This is essential for things like Kotlin Coroutines where the thread-binding is done above the level of JDK ThreadLocal's

The upcoming ThreadLocalContextManager does not have this overhead - it's basically a ThreadLocal holding the current context, so no allocations are required in swap. The benchmark when using ThreadLocalContextManager for the existing code is effectively the same as the proposed approach.

Given this I think it's valid to optimize the VirtualThread instrumentation in the short-term to avoid using swap when we know it's not needed and we can trust the ThreadLocal binding, with the understanding that this won't be an issue with ThreadLocalContextManager.

@amarziali
amarziali force-pushed the andrea.marziali/vthread-context-perf branch from d80872e to b05f030 Compare July 9, 2026 13:22
@PerfectSlayer
PerfectSlayer self-requested a review July 9, 2026 14:50
@PerfectSlayer

Copy link
Copy Markdown
Collaborator

Sphinx Review

(as we were discussing during the sync meeting, using this generated PR as exemple to demo the output)

MEDIUM (9)

  1. Tests only exercise the no-op default (ProfilingContextIntegrationTest.java:35) — the tests would pass identically if the real carrier-bound setContext/clearContext implementation were deleted entirely.

  2. DatadogProfilingIntegration.setContext untested (DatadogProfilingIntegration.java:83) — the actual ddprof rebind logic (AgentSpan.fromContext + contextManager.activate + null-span guard) never runs under test since instrumentation tests run with profiling off.

  3. Unconditional Context.current() + dispatch on every mount (VirtualThreadState.java:49) — even with profiling disabled, every mount pays a ThreadLocal read + virtual dispatch, undercutting the PR's own hot-path goal. The plan's capability-gate would have skipped this.

  4. Profiler-carrier-binding concern leaked into VT lifecycle helper (same line) — no explicit isCarrierThreadBound() gate; correctness relies on unrelated integrations inheriting no-op defaults.

  5. Java-lang-21 forked test ordering is unenforced (VirtualThreadApiInstrumentationForkedTest.java:11) — relies on VirtualThreadState's static flag being captured after @WithConfig sets the system property, with nothing asserting that actually happened; a mis-capture would silently test the same branch twice.

  6. Discarded context.swap() return value, no restore-on-unmount (VirtualThreadState.java:45) — legacy path never restores the carrier's prior context on unmount (pre-diff behavior always did). Needs an explicit justification comment or a restore.

  7. Asymmetric clearContext() vs. guarded setContext() (VirtualThreadState.java:58) — onUnmount() clears unconditionally even when onMount() set nothing, doing extra native ddprof clears on the hot path.

  8. Missing Javadoc on onMount()/onUnmount() (VirtualThreadState.java:41) — the new branch-dependent semantics (seed-once, no restore in legacy branch) aren't documented, unlike the prior version.

LOW (5)

  1. Stale/broken field Javadocs (VirtualThreadState.java:30) — previousContext/context/seeded docs no longer match the reworked two-branch design; one is grammatically broken.
  2. No Javadoc on new setContext(Context) override (DatadogProfilingIntegration.java:83).
  3. clearContext() duplicates contextManager.close() body instead of delegating (DatadogProfilingIntegration.java:91) — asymmetric with setContext() → activate(); risks drift.
  4. Carrier-migration test likely doesn't force migration (VirtualThreadLifeCycleTest.java:219) — sets jdk.virtualThreadScheduler.parallelism=2 at test time, but the JVM reads it only at scheduler init, which has likely already happened; test gives false confidence.
  5. Two mutually-exclusive strategies conflated in one class (VirtualThreadState.java:31) — legacy vs. swap paths each leave one field permanently unused; worth documenting or splitting.

Bottom line: the core idea (seed-once instead of swap-every-park/unpark) is sound and the benchmarks back up the perf claim, but #7 (silently dropping restore-on-unmount) and #4/#8 (unconditional work even when profiling is off) are worth a maintainer's explicit sign-off before merge, and the test coverage gaps (#2, #3, #6, #13) mean the new carrier-rebind logic isn't actually exercised by CI yet.

@mcculls mcculls left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!

@PerfectSlayer PerfectSlayer 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 left a bunch of suggestions 🙏

Comment thread docs/superpowers/plans/2026-07-08-virtual-thread-instrumentation-performance.md Outdated
@amarziali
amarziali marked this pull request as ready for review July 10, 2026 10:06
@amarziali
amarziali requested review from a team as code owners July 10, 2026 10:06
@amarziali
amarziali requested review from bric3 and mhlidd and removed request for a team July 10, 2026 10:06
@dd-octo-sts

dd-octo-sts Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Hi! 👋 Thanks for your pull request! 🎉

To help us review it, please make sure to:

  • Add at least one type, and one component or instrumentation label to the pull request

If you need help, please check our contributing guidelines.

@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: 498ed03df5

ℹ️ 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".

@amarziali amarziali added type: feature Enhancements and improvements inst: java Core Java language instrumentation labels Jul 10, 2026
@amarziali
amarziali marked this pull request as draft July 16, 2026 10:23
@amarziali amarziali added the tag: do not merge Do not merge changes label Jul 16, 2026
@PerfectSlayer
PerfectSlayer requested review from a team and removed request for a team August 20, 2026 07:16

@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: 73b38147c8

ℹ️ 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".

@amarziali

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 73b38147c8

ℹ️ 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".

@amarziali
amarziali force-pushed the andrea.marziali/vthread-context-perf branch from 47c3097 to 76c8e26 Compare September 8, 2026 12:34
@amarziali

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 76c8e267d6

ℹ️ 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".

@amarziali

Copy link
Copy Markdown
Contributor Author

@DataDog review

@datadog-datadog-prod-us1-2 datadog-datadog-prod-us1-2 Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Datadog Autotest: PASS

More details

The PR reduces virtual-thread park and unpark work.
JDK 22 and later seed trace context once and rebind ddprof on each mount.
JDK 21 now keeps the safe per-mount path.
The prior update fixes the benchmark model and states its native-call limit.
No open finding remains.

Was this helpful? React 👍 or 👎

Open Bits AI session

🤖 Datadog Autotest · Commit 76c8e26 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@amarziali amarziali removed the tag: do not merge Do not merge changes label Sep 8, 2026

@jbachorik jbachorik left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks reasonable. LGTM!

@amarziali

Copy link
Copy Markdown
Contributor Author

/code blockers

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-09-21 09:44:44 UTC ℹ️ Start processing command /code blockers


2026-09-21 09:44:44 UTC ℹ️ Devflow:

Checking merge blockers for #11893...


2026-09-21 09:44:57 UTC ℹ️ Devflow: /code blockers

Detected 1 merge blocker(s) to address:

🟠 Pending

  • Merge gate rule reviewers-approval is running: 2 approvals missing
    All required reviewers must approve this pull request before it can be merged. Learn more in our FAQ.

    Hint: Questions about this check? Reach out in #dx-source-code-management on Slack.

@bric3 bric3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM, I suggest a few comment tweaks.

FYI here's the schema I used to understand the code:

flowchart TD
    E[Virtual-thread lifecycle event] --> P{Per-mount context required?}
    P -->|JDK 21, CWS enabled,<br/>or legacy manager disabled| OLD
    OLD[Swap saved Context<br/>on every mount and unmount]
    P -->|JDK 22+, legacy manager,<br/>CWS disabled| NEW
    NEW[Initialize inherited trace context once<br/>around VirtualThread.run]
    NEW --> Q{Profiler binding required?}
    Q -->|Yes| PROF[Bind current context on mount<br/>and root on unmount]
    Q -->|No| NONE[No context operation<br/>during park/unpark]
Loading
sequenceDiagram
    participant VT as java.lang.VirtualThread
    participant Store as ContextStore
    participant State as VirtualThreadState
    participant Ctx as Context
    participant Prof as ProfilingContextIntegration

    Note over VT,Prof: Calls from VirtualThread include the injected instrumentation advice

    alt JDK 21 OR legacy context manager disabled OR CWS enabled
        Note over VT,Ctx: Existing behavior retained: swap context on every mount and unmount

        loop Each execution segment, from mount to unmount
            VT->>VT: mount()
            VT->>Store: get(virtualThread)
            Store-->>VT: state
            VT->>State: onMount()
            State->>Ctx: context.swap()
            Ctx-->>State: previousContext
            Note over State,Prof: Normal context/scope activation callbacks run

            VT->>VT: Execute or resume application code

            Note over VT,State: Before unmount(), while the virtual thread is still current
            VT->>Store: get(virtualThread)
            Store-->>VT: state
            VT->>State: onUnmount()
            State->>Ctx: previousContext.swap()
            Ctx-->>State: Save returned context for the next mount
            VT->>VT: unmount()
        end

    else JDK 22+ AND legacy context manager enabled AND CWS disabled
        Note over VT,Prof: New behavior: Initialize inherited trace context once, Update the carrier’s profiler binding on every mount.

        VT->>VT: First mount()
        VT->>State: onMountWithoutStore()
        opt Carrier binding required
            State->>Prof: setContext(Context.current())
            Note right of Prof: Initially root:<br/>captured context is not installed yet
        end

        Note over VT,State: Enter private run(Runnable), after the first mount
        VT->>Store: get(virtualThread)
        Store-->>VT: state
        Note over VT,State: Keep state in an advice local across suspension
        VT->>State: onRun()
        State->>Ctx: captured context.swap()
        Ctx-->>State: previousContext
        opt Profiling enabled
            Ctx->>Prof: Scope activation binds the captured span
        end
        VT->>VT: Start application code

        loop Each unmount/remount cycle (can happen repeatitively in the VT lifetime)
            Note over VT,State: For example:<br/>doSomeWork()<br/>Thread.sleep(100)<br/>doSomeWork()

            VT->>State: onUnmountWithoutStore()
            opt Carrier binding required
                State->>Prof: setContext(Context.root())
            end
            VT->>VT: unmount()
            Note over VT,Ctx: Trace scope stack remains in the virtual thread's ThreadLocal
            VT->>VT: mount(), possibly on another carrier
            VT->>State: onMountWithoutStore()
            opt Carrier binding required
                State->>Ctx: Context.current()
                Ctx-->>State: Current context, including any active child span
                State->>Prof: setContext(current context)
            end
            VT->>VT: Resume application code
        end

        Note over VT,State: Exit run(Runnable), including exceptional exit
        VT->>State: afterRun()
        State->>Ctx: previousContext.swap()
        VT->>State: onUnmountWithoutStore()
        opt Carrier binding required
            State->>Prof: setContext(Context.root())
        end
        VT->>VT: Final unmount()
    end

    Note over VT,State: Termination advice: afterDone / afterTerminate
    VT->>Store: remove(virtualThread)
    Store-->>VT: state
    VT->>State: onTerminate()
    State->>Ctx: continuation.release()
Loading

Comment thread dd-trace-core/src/jmh/java/datadog/trace/core/VirtualThreadContextBenchmark.java Outdated
@amarziali

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-09-23 06:21:03 UTC ℹ️ Start processing command /merge


2026-09-23 06:21:08 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-09-23 07:35:57 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit 7c8d904 into master Sep 23, 2026
604 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the andrea.marziali/vthread-context-perf branch September 23, 2026 07:35
@github-actions github-actions Bot added this to the 1.67.0 milestone Sep 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

inst: java Core Java language instrumentation type: feature Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants