1+ using System . Buffers ;
2+ using System . Runtime . CompilerServices ;
13using ModularityKit . Mutator . Abstractions . Changes ;
24using ModularityKit . Mutator . Abstractions . Context ;
35using ModularityKit . Mutator . Abstractions . Intent ;
@@ -16,22 +18,32 @@ namespace ModularityKit.Mutator.Runtime.Interception;
1618/// to ensure deterministic execution order.
1719/// </para>
1820/// <para>
19- /// The pipeline also filters interceptors via <see cref="MutationInterceptorBase.ShouldRun"/>.
21+ /// The pipeline also filters interceptors via <see cref="MutationInterceptorBase.ShouldRun"/>.
2022/// Method calls are executed asynchronously to integrate with the ModularityKit mutation pipeline.
2123/// </para>
24+ /// <para>
25+ /// The execution path reads from a lock-free volatile snapshot updated atomically on register/unregister,
26+ /// avoiding any lock acquisition or array allocation on the hot path when no interceptors are registered.
27+ /// </para>
2228/// </remarks>
2329internal sealed class InterceptorPipeline : IInterceptorPipeline
2430{
2531 private readonly List < IMutationInterceptor > _interceptors = [ ] ;
2632 private readonly Lock _lock = new ( ) ;
2733
34+ // Cached immutable snapshot of the registered interceptors. Invalidated (set to null)
35+ // on every Register/Unregister and lazily rebuilt on next read. `volatile` ensures
36+ // readers on other threads observe fully published array or null, never a partial write.
37+ private volatile IMutationInterceptor [ ] ? _snapshotCache = [ ] ;
38+
2839 /// <summary>
29- /// Registers a new interceptor in the pipeline.
40+ /// Registers new interceptor in the pipeline.
3041 /// </summary>
31- /// <param name="interceptor">The interceptor to register. Cannot be <c> null</c> .</param>
42+ /// <param name="interceptor">The interceptor to register. Cannot be null.</param>
3243 /// <exception cref="ArgumentNullException">Thrown if <paramref name="interceptor"/> is <c>null</c>.</exception>
3344 /// <remarks>
3445 /// After adding, the interceptors list is sorted by <c>Order</c> to guarantee deterministic execution order.
46+ /// The snapshot cache is invalidated so the next read lazily rebuilds it.
3547 /// </remarks>
3648 public void Register ( IMutationInterceptor interceptor )
3749 {
@@ -40,6 +52,7 @@ public void Register(IMutationInterceptor interceptor)
4052 {
4153 _interceptors . Add ( interceptor ) ;
4254 _interceptors . Sort ( ( a , b ) => a . Order . CompareTo ( b . Order ) ) ;
55+ _snapshotCache = null ;
4356 }
4457 }
4558
@@ -49,16 +62,43 @@ public void Register(IMutationInterceptor interceptor)
4962 /// <param name="name">The name of the interceptor to remove.</param>
5063 public void Unregister ( string name )
5164 {
52- lock ( _lock ) _interceptors . RemoveAll ( i => i . Name == name ) ;
65+ lock ( _lock )
66+ {
67+ _interceptors . RemoveAll ( i => i . Name == name ) ;
68+ _snapshotCache = null ;
69+ }
5370 }
5471
5572 /// <summary>
56- /// Gets a snapshot of the currently registered interceptors.
73+ /// Gets snapshot of the currently registered interceptors.
5774 /// </summary>
58- /// <returns>An array of interceptors.</returns>
75+ /// <returns>An array of interceptors. Returns <see cref="Array.Empty{T}"/> without taking
76+ /// the lock when the cached snapshot is still valid and empty.</returns>
77+ /// <remarks>
78+ /// <c>volatile</c> on <see cref="_snapshotCache"/> guarantees an acquire fence on every read,
79+ /// so the calling thread observe fully published array or null — never torn reference.
80+ /// Because the array is immutable after publication, contents are implicitly safe.
81+ /// </remarks>
5982 private IMutationInterceptor [ ] GetSnapshot ( )
6083 {
61- lock ( _lock ) return _interceptors . ToArray ( ) ;
84+ var cached = _snapshotCache ;
85+ if ( cached is not null )
86+ return cached ;
87+
88+ lock ( _lock )
89+ {
90+ // Re check under the lock in case another thread already rebuilt it.
91+ cached = _snapshotCache ;
92+ if ( cached is not null )
93+ return cached ;
94+
95+ cached = _interceptors . Count == 0
96+ ? [ ]
97+ : [ .. _interceptors ] ;
98+
99+ _snapshotCache = cached ;
100+ return cached ;
101+ }
62102 }
63103
64104 /// <summary>
@@ -67,23 +107,51 @@ private IMutationInterceptor[] GetSnapshot()
67107 /// <param name="intent">The mutation intent.</param>
68108 /// <param name="context">The mutation context.</param>
69109 /// <returns>An array of interceptors that should be executed.</returns>
70- private IMutationInterceptor [ ] GetApplicable ( MutationIntent intent , MutationContext context )
110+ /// <remarks>
111+ /// Uses the cached snapshot. When no interceptors are registered, returns the cached empty array
112+ /// without any filtering overhead. When all interceptors are applicable, the snapshot is reused
113+ /// directly without allocating a filtered copy.
114+ /// A pooled buffer is used during filtering to avoid per-call <c>List<T></c> allocation.
115+ /// </remarks>
116+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
117+ private IMutationInterceptor [ ] GetApplicable (
118+ MutationIntent intent ,
119+ MutationContext context )
71120 {
72121 var snapshot = GetSnapshot ( ) ;
73- var result = new List < IMutationInterceptor > ( snapshot . Length ) ;
122+ if ( snapshot . Length == 0 )
123+ return snapshot ;
124+
125+ var pool = ArrayPool < IMutationInterceptor > . Shared ;
126+ var buffer = pool . Rent ( snapshot . Length ) ;
127+ var count = 0 ;
128+ var excluded = false ;
74129
75- foreach ( var t in snapshot )
130+ for ( var i = 0 ; i < snapshot . Length ; i ++ )
76131 {
77- if ( t is MutationInterceptorBase baseInt )
132+ var t = snapshot [ i ] ;
133+ var shouldRun = t is not MutationInterceptorBase baseInt || baseInt . ShouldRun ( intent , context ) ;
134+
135+ if ( shouldRun )
136+ {
137+ buffer [ count ++ ] = t ;
138+ }
139+ else
78140 {
79- if ( ! baseInt . ShouldRun ( intent , context ) )
80- continue ;
141+ excluded = true ;
81142 }
143+ }
82144
83- result . Add ( t ) ;
145+ if ( ! excluded )
146+ {
147+ pool . Return ( buffer ) ;
148+ return snapshot ;
84149 }
85150
86- return result . ToArray ( ) ;
151+ var result = new IMutationInterceptor [ count ] ;
152+ Array . Copy ( buffer , result , count ) ;
153+ pool . Return ( buffer ) ;
154+ return result ;
87155 }
88156
89157 /// <summary>
@@ -92,7 +160,10 @@ private IMutationInterceptor[] GetApplicable(MutationIntent intent, MutationCont
92160 /// <param name="action">The action to execute for each interceptor.</param>
93161 /// <param name="intent">The mutation intent.</param>
94162 /// <param name="context">The mutation context.</param>
95- private async Task ExecuteAsync ( Func < IMutationInterceptor , Task > action , MutationIntent intent , MutationContext context )
163+ private async Task ExecuteAsync (
164+ Func < IMutationInterceptor , Task > action ,
165+ MutationIntent intent ,
166+ MutationContext context )
96167 {
97168 var interceptors = GetApplicable ( intent , context ) ;
98169 foreach ( var t in interceptors )
@@ -101,19 +172,115 @@ private async Task ExecuteAsync(Func<IMutationInterceptor, Task> action, Mutatio
101172 }
102173 }
103174
104- /// <inheritdoc/>
105- public Task OnBeforeMutationAsync ( MutationIntent intent , MutationContext context , object state , string executionId , CancellationToken cancellationToken = default )
106- => ExecuteAsync ( i => i . OnBeforeMutationAsync ( intent , context , state , executionId , cancellationToken ) , intent , context ) ;
175+ /// <summary>
176+ /// Executes registered interceptors before a mutation is evaluated.
177+ /// </summary>
178+ /// <param name="intent">The mutation intent.</param>
179+ /// <param name="context">The mutation execution context.</param>
180+ /// <param name="state">The current mutation state.</param>
181+ /// <param name="executionId">The unique execution identifier.</param>
182+ /// <param name="cancellationToken">Token used to cancel the operation.</param>
183+ /// <returns>A task representing the asynchronous operation.</returns>
184+ public Task OnBeforeMutationAsync (
185+ MutationIntent intent ,
186+ MutationContext context ,
187+ object state ,
188+ string executionId ,
189+ CancellationToken cancellationToken = default ) =>
190+ ExecuteAsync (
191+ interceptor => interceptor . OnBeforeMutationAsync (
192+ intent ,
193+ context ,
194+ state ,
195+ executionId ,
196+ cancellationToken ) ,
197+ intent ,
198+ context ) ;
107199
108- /// <inheritdoc/>
109- public Task OnAfterMutationAsync ( MutationIntent intent , MutationContext context , object ? oldState , object ? newState , ChangeSet changes , string executionId , CancellationToken cancellationToken = default )
110- => ExecuteAsync ( i => i . OnAfterMutationAsync ( intent , context , oldState , newState , changes , executionId , cancellationToken ) , intent , context ) ;
200+ /// <summary>
201+ /// Executes registered interceptors after mutation has completed.
202+ /// </summary>
203+ /// <param name="intent">The mutation intent.</param>
204+ /// <param name="context">The mutation execution context.</param>
205+ /// <param name="oldState">The state before the mutation.</param>
206+ /// <param name="newState">The state after the mutation.</param>
207+ /// <param name="changes">The changes produced by the mutation.</param>
208+ /// <param name="executionId">The unique execution identifier.</param>
209+ /// <param name="cancellationToken">Token used to cancel the operation.</param>
210+ /// <returns>A task representing the asynchronous operation.</returns>
211+ public Task OnAfterMutationAsync (
212+ MutationIntent intent ,
213+ MutationContext context ,
214+ object ? oldState ,
215+ object ? newState ,
216+ ChangeSet changes ,
217+ string executionId ,
218+ CancellationToken cancellationToken = default ) =>
219+ ExecuteAsync (
220+ interceptor => interceptor . OnAfterMutationAsync (
221+ intent ,
222+ context ,
223+ oldState ,
224+ newState ,
225+ changes ,
226+ executionId ,
227+ cancellationToken ) ,
228+ intent ,
229+ context ) ;
111230
112- /// <inheritdoc/>
113- public Task OnMutationFailedAsync ( MutationIntent intent , MutationContext context , object state , Exception exception , string executionId , CancellationToken cancellationToken = default )
114- => ExecuteAsync ( i => i . OnMutationFailedAsync ( intent , context , state , exception , executionId , cancellationToken ) , intent , context ) ;
231+ /// <summary>
232+ /// Executes registered interceptors when mutation fails.
233+ /// </summary>
234+ /// <param name="intent">The mutation intent.</param>
235+ /// <param name="context">The mutation execution context.</param>
236+ /// <param name="state">The current mutation state.</param>
237+ /// <param name="exception">The exception that caused the mutation to fail.</param>
238+ /// <param name="executionId">The unique execution identifier.</param>
239+ /// <param name="cancellationToken">Token used to cancel the operation.</param>
240+ /// <returns>A task representing the asynchronous operation.</returns>
241+ public Task OnMutationFailedAsync (
242+ MutationIntent intent ,
243+ MutationContext context ,
244+ object state ,
245+ Exception exception ,
246+ string executionId ,
247+ CancellationToken cancellationToken = default ) =>
248+ ExecuteAsync (
249+ interceptor => interceptor . OnMutationFailedAsync (
250+ intent ,
251+ context ,
252+ state ,
253+ exception ,
254+ executionId ,
255+ cancellationToken ) ,
256+ intent ,
257+ context ) ;
115258
116- /// <inheritdoc/>
117- public Task OnPolicyBlockedAsync ( MutationIntent intent , MutationContext context , object state , PolicyDecision decision , string executionId , CancellationToken cancellationToken = default )
118- => ExecuteAsync ( i => i . OnPolicyBlockedAsync ( intent , context , state , decision , executionId , cancellationToken ) , intent , context ) ;
119- }
259+ /// <summary>
260+ /// Executes registered interceptors when mutation is blocked by policy.
261+ /// </summary>
262+ /// <param name="intent">The mutation intent.</param>
263+ /// <param name="context">The mutation execution context.</param>
264+ /// <param name="state">The current mutation state.</param>
265+ /// <param name="decision">The policy decision that blocked the mutation.</param>
266+ /// <param name="executionId">The unique execution identifier.</param>
267+ /// <param name="cancellationToken">Token used to cancel the operation.</param>
268+ /// <returns>A task representing the asynchronous operation.</returns>
269+ public Task OnPolicyBlockedAsync (
270+ MutationIntent intent ,
271+ MutationContext context ,
272+ object state ,
273+ PolicyDecision decision ,
274+ string executionId ,
275+ CancellationToken cancellationToken = default ) =>
276+ ExecuteAsync (
277+ interceptor => interceptor . OnPolicyBlockedAsync (
278+ intent ,
279+ context ,
280+ state ,
281+ decision ,
282+ executionId ,
283+ cancellationToken ) ,
284+ intent ,
285+ context ) ;
286+ }
0 commit comments