Repository navigation
Description
Summary
Reduce async state machine allocations and kernel transitions on the concurrency gate hot path by adding synchronous TryEnter fast path when the global and per state semaphores are uncontended.
Goal
Lower latency and GC pressure for concurrent mutation execution without changing public API contracts or concurrency semantics.
Problem
Every EnterAsync on MutationExecutionConcurrencyGate always goes through the async state machine (allocating ValueTask box + task), even when both the global gate and the per state gate are immediately available. In common case (low contention, distinct states) this overhead is pure waste.
Additionally, for stateless mutations (stateId is null/empty) the gate still entered the full async ValueTask path, incurring the same overhead for trivial no op state check.
Scope
MutationExecutionConcurrencyGate (src/Runtime/Internal/Execution/MutationExecutionConcurrencyGate.cs):
- Split
EnterAsyncinto three paths:- Stateless - skips state gate entirely, uses sync
Wait(0)on global gate before falling back to async - Stateful, gate exists - tries sync
Wait(0)on global + state gate, returns structLeasesynchronously on success - Stateful, gate missing or contended - falls back to original
async ValueTaskpath withGetOrAdd
- Stateless - skips state gate entirely, uses sync
Leasestruct keeps directSemaphoreSlimreferences (no indirection via owner)
Important
Design Expectations
- Hot path (uncontended gate) must return completed
ValueTask<Lease>without allocating an async state machine. - Sync
Wait(0)is non blocking interlocked operation - safe for use from async code paths when the semaphore is available. When contended, it immediately returnsfalseand falls back to the async path. - No changes to concurrency semantics: global gate still limits total concurrent mutations, state gate still serializes per state access.
- No public API or interface changes.
Acceptance Criteria
-
EnterAsyncreturns synchronously when global gate is uncontended and nostateIdis provided -
EnterAsyncreturns synchronously when both global and state gates are uncontended - Contended paths fall back to
WaitAsyncwith equivalent behaviour - All existing unit tests pass without modification
- Benchmark regression validated:
| Benchmark | Before | After | Δ |
|---|---|---|---|
BatchScheduling (2 batches × 4 items) |
18.12 us / 27.09 KB | 13.54 us / 26.16 KB | -25.3% |
BatchScheduling (2 × 16) |
71.95 us / 105.09 KB | 52.21 us / 101.34 KB | -27.4% |
BatchScheduling (4 × 4) |
36.71 us / 54.09 KB | 27.13 us / 52.22 KB | -26.1% |
BatchScheduling (4 × 16) |
140.05 us / 210.10 KB | 104.11 us / 202.59 KB | -25.7% |
ParallelExecution (2) |
4.324 us / 6.42 KB | 3.237 us / 6.19 KB | -25.1% |
ParallelExecution (8) |
17.557 us / 25.41 KB | 12.537 us / 24.47 KB | -28.6% |
Non Goals
- This issue does not add per-state
SemaphoreSlimeviction or dictionary cleanup - This issue does not change
ValueTask<T>return types in other engine components - This issue does not touch
MutationEngineorInterceptorPipeline - This issue does not change any public API or interface
Note
The sync fast path uses SemaphoreSlim.Wait(0, CancellationToken) which is equivalent to Wait(TimeSpan.Zero) with cancellation check. It returns immediately without blocking the calling thread.
Activity