Skip to content

[Perf]: Optimize concurrency gate with sync TryEnter fast path #88

Description

@rian-be

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 EnterAsync into 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 struct Lease synchronously on success
    • Stateful, gate missing or contended - falls back to original async ValueTask path with GetOrAdd
  • Lease struct keeps direct SemaphoreSlim references (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 returns false and 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

  • EnterAsync returns synchronously when global gate is uncontended and no stateId is provided
  • EnterAsync returns synchronously when both global and state gates are uncontended
  • Contended paths fall back to WaitAsync with 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 SemaphoreSlim eviction or dictionary cleanup
  • This issue does not change ValueTask<T> return types in other engine components
  • This issue does not touch MutationEngine or InterceptorPipeline
  • 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.

Metadata

Metadata

Assignees

Labels

performancePerformance improvements or regressionsruntimeRuntime implementation and execution flow

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions