Conversation
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Problem Statement
The
MutationExecutionConcurrencyGateis called on every mutation execution. EveryEnterAsyncallocated an async state machine (ValueTask+ task), even when both the global and per state semaphores were immediately available. In the common case (low contention, distinct states), this overhead was pure waste.Additionally, stateless mutations (
stateIdis null/empty) still entered the fullasync ValueTaskpath, incurring the same overhead for a trivial no op state check.Solution Overview
1. Split Stateless and Stateful Paths
ConcurrentDictionarylookup and state gate entirely2. Sync TryEnter Fast Path
Wait(0)is non blocking interlocked operationValueTask<Lease>without async state machine allocationWaitAsynconly when contended3. Combined Global + State Gate Fast Path
Performance Impact
Benchmark Results
Optimized Path Analysis
Key Design Decisions
Why Wait(0) instead of TryEnter pattern?
SemaphoreSlim.Wait(0)is the built in try enter API. It performs anInterlocked.CompareExchangeon the semaphore count - returnstrueif available,falseimmediately if not. Zero blocking, zero allocation.Why split EnterAsync into three methods?
Avoids one size fits-all
async ValueTaskmethod. The JIT can inline the non async helpers and elide the state machine allocation entirely when the fast path is taken. The slow path keeps theasynckeyword only where actually needed.Why check dictionary existence before Wait(0) on global gate?
If the per state semaphore doesn't exist yet, the sync fast path can not succeed (no state gate to acquire). Checking
TryGetValuefirst avoids acquiring the global gate only to immediately release it - small optimization that prevents unnecessary global gate traffic.Why not AsyncLocal or callback based approach?
The gate is synchronous coordination primitive (semaphore based). AsyncLocal adds per await overhead. A callback based approach would change the public API surface. The current design preserves
ValueTask<Lease>return type with zero API change.Files Changed
MutationExecutionConcurrencyGate.cs
EnterAsyncinto stateless/stateful + fast/slow pathsWait(0)sync fast path for uncontended scenarioLeasestruct keeps directSemaphoreSlimreferencesConcurrentBagpool fieldMigration
Note
No breaking changes. The concurrency gate is internal. Public API (
IMutationEngine,ExecuteAsync,Lease) is unchanged. No migration steps required.closes #88