Skip to content

Commit bfad86f

Browse files
authored
perf: Optimize concurrency gate with sync TryEnter fast path (#89)
2 parents a8c361f + 31676d0 commit bfad86f

1 file changed

Lines changed: 125 additions & 26 deletions

File tree

Lines changed: 125 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,130 @@
11
using System.Collections.Concurrent;
2+
using System.Runtime.CompilerServices;
23

34
namespace ModularityKit.Mutator.Runtime.Internal.Execution;
45

56
/// <summary>
6-
/// Coordinates core mutation execution concurrency across the engine.
7+
/// Coordinates concurrent mutation execution using global concurrency limit
8+
/// and per state serialization.
79
/// </summary>
8-
internal sealed class MutationExecutionConcurrencyGate(int maxConcurrentMutations)
10+
/// <remarks>
11+
/// When state identifier is provided, an additional per state gate ensures that
12+
/// mutations targeting the same state are executed sequentially, while mutations
13+
/// targeting different states may execute concurrently up to the global limit.
14+
/// </remarks>
15+
/// <param name="maxConcurrentMutations">The maximum number of mutations that may execute concurrently across all states. </param>
16+
internal sealed class MutationExecutionConcurrencyGate(
17+
int maxConcurrentMutations)
918
{
10-
private readonly SemaphoreSlim _globalGate = new(maxConcurrentMutations, maxConcurrentMutations);
11-
private readonly ConcurrentDictionary<string, SemaphoreSlim> _stateGates = new(StringComparer.Ordinal);
19+
private readonly SemaphoreSlim _globalGate =
20+
new(maxConcurrentMutations, maxConcurrentMutations);
1221

13-
public async ValueTask<Lease> EnterAsync(string? stateId, CancellationToken cancellationToken)
22+
private readonly ConcurrentDictionary<string, SemaphoreSlim> _stateGates =
23+
new(StringComparer.Ordinal);
24+
25+
/// <summary>
26+
/// Enters the concurrency gate for mutation execution.
27+
/// </summary>
28+
/// <param name="stateId">The identifier of the state being mutated.</param>
29+
/// <param name="cancellationToken">Token that can be used to cancel waiting for the required concurrency gates. </param>
30+
/// <returns>An asynchronous lease that releases the acquired concurrency gates when disposed. </returns>
31+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
32+
public ValueTask<Lease> EnterAsync(
33+
string? stateId,
34+
CancellationToken cancellationToken)
1435
{
15-
await _globalGate.WaitAsync(cancellationToken).ConfigureAwait(false);
36+
if (stateId is null)
37+
return EnterGlobalOnlyAsync(cancellationToken);
1638

17-
var stateGate = default(SemaphoreSlim);
39+
return EnterWithStateAsync(stateId, cancellationToken);
40+
}
1841

19-
try
42+
/// <summary>
43+
/// Attempts to acquire the global concurrency gate without asynchronous waiting
44+
/// when capacity is immediately available.
45+
/// </summary>
46+
private ValueTask<Lease> EnterGlobalOnlyAsync(
47+
CancellationToken cancellationToken)
48+
{
49+
if (_globalGate.Wait(0, cancellationToken))
50+
{
51+
return new ValueTask<Lease>(
52+
new Lease(_globalGate, null));
53+
}
54+
55+
return SlowEnterGlobalOnlyAsync(cancellationToken);
56+
}
57+
58+
/// <summary>
59+
/// Asynchronously waits for the global concurrency gate when it could not be
60+
/// acquired immediately.
61+
/// </summary>
62+
private async ValueTask<Lease> SlowEnterGlobalOnlyAsync(
63+
CancellationToken cancellationToken)
64+
{
65+
await _globalGate
66+
.WaitAsync(cancellationToken)
67+
.ConfigureAwait(false);
68+
69+
return new Lease(_globalGate, null);
70+
}
71+
72+
/// <summary>
73+
/// Attempts to acquire both the global concurrency gate and the gate associated
74+
/// with the specified state.
75+
/// </summary>
76+
/// <remarks>
77+
/// The global gate is always acquired before the per state gate to maintain
78+
/// consistent acquisition order and avoid lock order inversion.
79+
/// </remarks>
80+
private ValueTask<Lease> EnterWithStateAsync(
81+
string stateId,
82+
CancellationToken cancellationToken)
83+
{
84+
if (_stateGates.TryGetValue(stateId, out var existing))
2085
{
21-
if (!string.IsNullOrWhiteSpace(stateId))
86+
if (_globalGate.Wait(0, cancellationToken))
2287
{
23-
stateGate = _stateGates.GetOrAdd(stateId, static _ => new SemaphoreSlim(1, 1));
24-
await stateGate.WaitAsync(cancellationToken).ConfigureAwait(false);
88+
if (existing.Wait(0, cancellationToken))
89+
{
90+
return new ValueTask<Lease>(
91+
new Lease(_globalGate, existing));
92+
}
93+
94+
_globalGate.Release();
2595
}
96+
}
97+
98+
return SlowEnterWithStateAsync(
99+
stateId,
100+
cancellationToken);
101+
}
102+
103+
/// <summary>
104+
/// Asynchronously acquires the global and per state gates when the fast path
105+
/// could not acquire them immediately.
106+
/// </summary>
107+
private async ValueTask<Lease> SlowEnterWithStateAsync(
108+
string stateId,
109+
CancellationToken cancellationToken)
110+
{
111+
await _globalGate
112+
.WaitAsync(cancellationToken)
113+
.ConfigureAwait(false);
26114

27-
return new Lease(_globalGate, stateGate);
115+
try
116+
{
117+
var stateGate = _stateGates.GetOrAdd(
118+
stateId,
119+
static _ => new SemaphoreSlim(1, 1));
120+
121+
await stateGate
122+
.WaitAsync(cancellationToken)
123+
.ConfigureAwait(false);
124+
125+
return new Lease(
126+
_globalGate,
127+
stateGate);
28128
}
29129
catch
30130
{
@@ -34,24 +134,23 @@ public async ValueTask<Lease> EnterAsync(string? stateId, CancellationToken canc
34134
}
35135

36136
/// <summary>
37-
/// Represents an acquired execution slot.
137+
/// Represents an acquired concurrency lease that releases the associated gates
138+
/// when disposed.
38139
/// </summary>
39-
internal readonly struct Lease : IAsyncDisposable
140+
internal readonly struct Lease(
141+
SemaphoreSlim globalGate,
142+
SemaphoreSlim? stateGate) : IAsyncDisposable
40143
{
41-
private readonly SemaphoreSlim _globalGate;
42-
private readonly SemaphoreSlim? _stateGate;
43-
44-
public Lease(SemaphoreSlim globalGate, SemaphoreSlim? stateGate)
45-
{
46-
_globalGate = globalGate;
47-
_stateGate = stateGate;
48-
}
49-
144+
/// <summary>
145+
/// Releases the per state gate, when present, followed by the global gate.
146+
/// </summary>
147+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
50148
public ValueTask DisposeAsync()
51149
{
52-
_stateGate?.Release();
53-
_globalGate.Release();
150+
stateGate?.Release();
151+
globalGate.Release();
152+
54153
return ValueTask.CompletedTask;
55154
}
56155
}
57-
}
156+
}

0 commit comments

Comments
 (0)