-
Notifications
You must be signed in to change notification settings - Fork 344
Reduce virtual-thread context-propagation overhead on park/unpark #11893
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
amarziali
wants to merge
3
commits into
master
Choose a base branch
from
andrea.marziali/vthread-context-perf
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,26 @@ | ||
| package datadog.trace.bootstrap.instrumentation.java.lang; | ||
|
|
||
| import datadog.context.Context; | ||
| import datadog.trace.api.InstrumenterConfig; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentScope.Continuation; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentTracer; | ||
|
|
||
| /** | ||
| * This class holds the saved context and scope continuation for a virtual thread. | ||
| * Holds the context and continuation for a virtual thread. | ||
| * | ||
| * <p>Used by java-lang-21.0 {@code VirtualThreadInstrumentation} to swap the entire scope stack on | ||
| * mount/unmount. | ||
| * <p>With the legacy context manager, {@code swap()} rebuilds the whole scope stack on every | ||
| * mount/unmount, which is costly on the virtual-thread park/unpark hot path. There the context is | ||
| * seeded once on the first mount — it then follows the thread across park/unpark and carrier | ||
| * migration via its virtual-thread-aware {@code ThreadLocal} scope stack — and the profiler context | ||
| * (which is keyed by carrier thread) is re-applied on each subsequent mount and cleared on unmount. | ||
| * | ||
| * <p>With the new context manager {@code swap()} is cheap and drives the profiler through its | ||
| * context listener, so we simply swap in on mount and out on unmount. | ||
| */ | ||
| public final class VirtualThreadState { | ||
| private static final boolean LEGACY_CONTEXT_MANAGER = | ||
| InstrumenterConfig.get().isLegacyContextManagerEnabled(); | ||
|
|
||
| /** The virtual thread's saved context (scope stack snapshot). */ | ||
| private Context context; | ||
|
|
||
|
|
@@ -19,21 +30,35 @@ public final class VirtualThreadState { | |
| /** The carrier thread's saved context, set between mount and unmount. */ | ||
| private Context previousContext; | ||
|
|
||
| /** Determines whenever the context has been already set for the first mount */ | ||
| private boolean seeded; | ||
|
|
||
| public VirtualThreadState(Context context, Continuation continuation) { | ||
| this.context = context; | ||
| this.continuation = continuation; | ||
| } | ||
|
|
||
| /** Called on mount: swaps the virtual thread's context into the carrier thread. */ | ||
| public void onMount() { | ||
| this.previousContext = this.context.swap(); | ||
| if (LEGACY_CONTEXT_MANAGER) { | ||
| if (!seeded) { | ||
| // First mount also applies the profiler context to the carrier. | ||
| context.swap(); | ||
| seeded = true; | ||
| context = null; | ||
| } else { | ||
| AgentTracer.get().getProfilingContext().setContext(Context.current()); | ||
| } | ||
| } else { | ||
| previousContext = context.swap(); | ||
| } | ||
| } | ||
|
|
||
| /** Called on unmount: restores the carrier thread's original context. */ | ||
| public void onUnmount() { | ||
| if (this.previousContext != null) { | ||
| this.context = this.previousContext.swap(); | ||
| this.previousContext = null; | ||
| if (LEGACY_CONTEXT_MANAGER) { | ||
| AgentTracer.get().getProfilingContext().clearContext(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume it's ok to just clear the profiling context on unmount - but just want to be sure :) |
||
| } else if (previousContext != null) { | ||
| context = previousContext.swap(); | ||
| previousContext = null; | ||
| } | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
...tdog/trace/instrumentation/java/lang/jdk21/VirtualThreadApiInstrumentationForkedTest.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package testdog.trace.instrumentation.java.lang.jdk21; | ||
|
|
||
| import datadog.trace.junit.utils.config.WithConfig; | ||
|
|
||
| /** | ||
| * Runs the {@link VirtualThreadApiInstrumentationTest} cases with the legacy context manager | ||
| * disabled, so {@code VirtualThreadState} takes the swap path instead of seed-once. Forked because | ||
| * the legacy-context-manager choice is captured once per JVM. | ||
| */ | ||
| @WithConfig(key = "legacy.context-manager.enabled", value = "false") | ||
| class VirtualThreadApiInstrumentationForkedTest extends VirtualThreadApiInstrumentationTest {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
185 changes: 185 additions & 0 deletions
185
dd-trace-core/src/jmh/java/datadog/trace/core/VirtualThreadContextBenchmark.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| package datadog.trace.core; | ||
|
|
||
| import static java.util.concurrent.TimeUnit.MICROSECONDS; | ||
|
|
||
| import datadog.context.Context; | ||
| import datadog.trace.api.EndpointTracker; | ||
| import datadog.trace.api.Stateful; | ||
| import datadog.trace.api.profiling.ProfilingContextAttribute; | ||
| import datadog.trace.api.profiling.ProfilingScope; | ||
| import datadog.trace.api.profiling.Timer.TimerType; | ||
| import datadog.trace.api.profiling.Timing; | ||
| import datadog.trace.bootstrap.instrumentation.api.AgentSpan; | ||
| import datadog.trace.bootstrap.instrumentation.api.ProfilerContext; | ||
| import datadog.trace.bootstrap.instrumentation.api.ProfilingContextIntegration; | ||
| import datadog.trace.core.monitor.HealthMetrics; | ||
| import datadog.trace.core.scopemanager.ContinuableScopeManager; | ||
| import org.openjdk.jmh.annotations.Benchmark; | ||
| import org.openjdk.jmh.annotations.BenchmarkMode; | ||
| import org.openjdk.jmh.annotations.Fork; | ||
| import org.openjdk.jmh.annotations.Level; | ||
| import org.openjdk.jmh.annotations.Measurement; | ||
| import org.openjdk.jmh.annotations.Mode; | ||
| import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
| import org.openjdk.jmh.annotations.Scope; | ||
| import org.openjdk.jmh.annotations.Setup; | ||
| import org.openjdk.jmh.annotations.State; | ||
| import org.openjdk.jmh.annotations.TearDown; | ||
| import org.openjdk.jmh.annotations.Threads; | ||
| import org.openjdk.jmh.annotations.Warmup; | ||
|
|
||
| /** | ||
| * Per park/unpark cost of virtual-thread context propagation: the current full {@link | ||
| * ContinuableScopeManager#swap(Context)} on every mount/unmount versus the proposed seed-once path | ||
| * (nothing when profiling is off, a profiler rebind/unbind when it is on). ddprof's native {@code | ||
| * setContext} is modelled by a stub {@link ProfilingContextIntegration} whose {@link Stateful} | ||
| * writes four volatile longs and allocates nothing. | ||
| * | ||
| * <pre> | ||
| * {@code ./gradlew :dd-trace-core:jmh -Pjmh.includes=VirtualThreadContextBenchmark -PtestJvm=21 -Pjmh.profilers=gc} | ||
| * </pre> | ||
| * | ||
| * <p>Sample run (JDK 21.0.9, {@code @Threads(8)}; run-to-run variance is high, the alloc/op figures | ||
| * are stable): | ||
| * | ||
| * <pre>{@code | ||
| * Benchmark Mode Cnt Score Error Units | ||
| * currentCycle_profilingOff thrpt 5 463.841 ± 251.726 ops/us | ||
| * currentCycle_profilingOff:gc.alloc.rate.norm thrpt 5 176.002 ± 0.016 B/op | ||
| * currentCycle_profilingOn thrpt 5 272.253 ± 21.041 ops/us | ||
| * currentCycle_profilingOn:gc.alloc.rate.norm thrpt 5 288.003 ± 0.022 B/op | ||
| * proposedSteady_profilingOff thrpt 5 5010.376 ± 354.716 ops/us | ||
| * proposedSteady_profilingOff:gc.alloc.rate.norm thrpt 5 ~0 B/op | ||
| * proposedRebindUnbind_profilingOn thrpt 5 1755.815 ± 473.954 ops/us | ||
| * proposedRebindUnbind_profilingOn:gc.alloc.rate.norm thrpt 5 ~0 B/op | ||
| * }</pre> | ||
| */ | ||
| @State(Scope.Benchmark) | ||
| @Warmup(iterations = 3, time = 1) | ||
| @Measurement(iterations = 5, time = 1) | ||
| @BenchmarkMode(Mode.Throughput) | ||
| @Threads(8) | ||
| @OutputTimeUnit(MICROSECONDS) | ||
| @Fork(value = 1) | ||
| public class VirtualThreadContextBenchmark { | ||
|
|
||
| static final CoreTracer TRACER = CoreTracer.builder().build(); | ||
|
|
||
| ContinuableScopeManager plainManager; // profiling off | ||
| ContinuableScopeManager profiledManager; // profiling on | ||
|
|
||
| @Setup | ||
| public void setup() { | ||
| plainManager = new ContinuableScopeManager(0, false); | ||
| profiledManager = | ||
| new ContinuableScopeManager(0, false, new StubProfiling(), HealthMetrics.NO_OP); | ||
| } | ||
|
|
||
| @State(Scope.Thread) | ||
| public static class ThreadState { | ||
| AgentSpan span; | ||
| Context spanContext; | ||
| Stateful profilerState; | ||
|
|
||
| @Setup(Level.Trial) | ||
| public void setup(VirtualThreadContextBenchmark bench) { | ||
| span = TRACER.startSpan("benchmark", "vt"); | ||
| spanContext = span; | ||
| profilerState = new StubProfiling().newScopeState((ProfilerContext) span.spanContext()); | ||
| } | ||
|
|
||
| @TearDown(Level.Trial) | ||
| public void tearDown() { | ||
| span.finish(); | ||
| } | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void currentCycle_profilingOff(ThreadState t) { | ||
| Context previous = plainManager.swap(t.spanContext); | ||
| plainManager.swap(previous); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public void currentCycle_profilingOn(ThreadState t) { | ||
| Context previous = profiledManager.swap(t.spanContext); | ||
| profiledManager.swap(previous); | ||
| } | ||
|
|
||
| // current() is the faithful upper bound for the seed-once steady state (a scope-stack read). | ||
| @Benchmark | ||
| public Context proposedSteady_profilingOff(ThreadState t) { | ||
| return plainManager.current(); | ||
| } | ||
|
|
||
| @Benchmark | ||
| public Context proposedRebindUnbind_profilingOn(ThreadState t) { | ||
| Context active = profiledManager.current(); | ||
| t.profilerState.activate(t.span.spanContext()); | ||
| t.profilerState.close(); | ||
| return active; | ||
| } | ||
|
|
||
| static final class StubProfiling implements ProfilingContextIntegration { | ||
| @Override | ||
| public Stateful newScopeState(ProfilerContext profilerContext) { | ||
| // Per-scope storage mirrors ddprof's per-OS-thread native slots, avoiding the cross-thread | ||
| // cache contention a single shared instance would introduce. | ||
| return new StubState(); | ||
| } | ||
|
|
||
| @Override | ||
| public String name() { | ||
| return "stub"; | ||
| } | ||
|
|
||
| @Override | ||
| public ProfilingContextAttribute createContextAttribute(String attribute) { | ||
| return ProfilingContextAttribute.NoOp.INSTANCE; | ||
| } | ||
|
|
||
| @Override | ||
| public ProfilingScope newScope() { | ||
| return ProfilingScope.NO_OP; | ||
| } | ||
|
|
||
| @Override | ||
| public void onRootSpanFinished(AgentSpan rootSpan, EndpointTracker tracker) {} | ||
|
|
||
| @Override | ||
| public EndpointTracker onRootSpanStarted(AgentSpan rootSpan) { | ||
| return EndpointTracker.NO_OP; | ||
| } | ||
|
|
||
| @Override | ||
| public Timing start(TimerType type) { | ||
| return Timing.NoOp.INSTANCE; | ||
| } | ||
| } | ||
|
|
||
| static final class StubState implements Stateful { | ||
| volatile long rootSpanId; | ||
| volatile long spanId; | ||
| volatile long traceHigh; | ||
| volatile long traceLow; | ||
|
|
||
| @Override | ||
| public void activate(Object context) { | ||
| if (context instanceof ProfilerContext) { | ||
| ProfilerContext c = (ProfilerContext) context; | ||
| rootSpanId = c.getRootSpanId(); | ||
| spanId = c.getSpanId(); | ||
| traceHigh = c.getTraceIdHigh(); | ||
| traceLow = c.getTraceIdLow(); | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void close() { | ||
| rootSpanId = 0; | ||
| spanId = 0; | ||
| traceHigh = 0; | ||
| traceLow = 0; | ||
| } | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: the legacy context manager doesn't rebuild the whole scope stack, but it does wrap the stack and context together so the original stack can be restored when the context is swapped back.