Skip to content

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

Draft
amarziali wants to merge 2 commits into
masterfrom
andrea.marziali/vthread-context-perf
Draft

Reduce virtual-thread context-propagation overhead on park/unpark#11893
amarziali wants to merge 2 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

The virtual-thread instrumentation swapped the whole scope stack on every VirtualThread.mount()/unmount() (i.e. on every park/unpark).

The trace scope stack lives in a virtual-thread-aware ThreadLocal, so it follows the VT across park/unpark and carrier migration on its own. It only needs seeding once (first mount), never swapping again. The ddprof profiler context is different: it's keyed by the carrier OS thread, so it's re-bound on mount and cleared on unmount, but only when carrier-bound profiling is active.

I added few JMH bench

Results (per park/unpark, JDK 21)

Benchmark Design · profiling Throughput (ops/µs) Alloc (B/op)
currentCycle_profilingOff current · off 463.8 ± 251.7 176
proposedSteady_profilingOff redesign · off 5010.4 ± 354.7 ~0
currentCycle_profilingOn current · on 272.3 ± 21.0 288
proposedRebindUnbind_profilingOn redesign · on 1755.8 ± 473.9 ~0

And the deltas:

Profiling Throughput Allocation
off 463.8 → 5010.4 ops/µs (~11×) 176 → ~0 B/op
on 272.3 → 1755.8 ops/µs (~6.5×) 288 → ~0 B/op

Motivation

Additional Notes

Contributor Checklist

  • Format the title according to the contribution guidelines
  • Assign the type: and (comp: or inst:) labels in addition to any other useful labels
  • Avoid using close, fix, or any linking keywords when referencing an issue
    Use solves instead, and assign the PR milestone to the issue
  • Update the CODEOWNERS file on source file addition, migration, or deletion
  • Update public documentation with any new configuration flags or behaviors
  • Add your completed PR to the merge queue by commenting /merge. You can also:
    • Customize the commit message associated with the merge with /merge --commit-message "..."
    • Remove your PR from the merge queue with /merge -c
    • Skip all merge queue checks with /merge -f --reason "reason"; please use this judiciously, as some checks do not run at the PR-level (note: the PR still needs to be mergeable, this will only skip the pre-merge build)
    • Get more information in this doc

Jira ticket: [PROJ-IDENT]

@datadog-datadog-prod-us1-2

This comment has been minimized.

@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 13.94 s 13.94 s [-0.7%; +0.7%] (no difference)
startup:insecure-bank:tracing:Agent 12.89 s 12.92 s [-1.0%; +0.5%] (no difference)
startup:petclinic:appsec:Agent 16.24 s 16.78 s [-7.7%; +1.3%] (no difference)
startup:petclinic:iast:Agent 16.81 s 16.94 s [-1.8%; +0.3%] (no difference)
startup:petclinic:profiling:Agent 16.56 s 16.70 s [-2.0%; +0.3%] (no difference)
startup:petclinic:sca:Agent 16.93 s 16.74 s [+0.3%; +2.0%] (maybe worse)
startup:petclinic:tracing:Agent 16.07 s 16.23 s [-1.9%; -0.1%] (maybe better)

Commit: d80872eb · 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.

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.

2 participants