diff --git a/src/Runtime/Interception/InterceptorPipeline.cs b/src/Runtime/Interception/InterceptorPipeline.cs
index 8051f57..f463d70 100644
--- a/src/Runtime/Interception/InterceptorPipeline.cs
+++ b/src/Runtime/Interception/InterceptorPipeline.cs
@@ -1,3 +1,5 @@
+using System.Buffers;
+using System.Runtime.CompilerServices;
using ModularityKit.Mutator.Abstractions.Changes;
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Intent;
@@ -16,22 +18,32 @@ namespace ModularityKit.Mutator.Runtime.Interception;
/// to ensure deterministic execution order.
///
///
-/// The pipeline also filters interceptors via .
+/// The pipeline also filters interceptors via .
/// Method calls are executed asynchronously to integrate with the ModularityKit mutation pipeline.
///
+///
+/// The execution path reads from a lock-free volatile snapshot updated atomically on register/unregister,
+/// avoiding any lock acquisition or array allocation on the hot path when no interceptors are registered.
+///
///
internal sealed class InterceptorPipeline : IInterceptorPipeline
{
private readonly List _interceptors = [];
private readonly Lock _lock = new();
+ // Cached immutable snapshot of the registered interceptors. Invalidated (set to null)
+ // on every Register/Unregister and lazily rebuilt on next read. `volatile` ensures
+ // readers on other threads observe fully published array or null, never a partial write.
+ private volatile IMutationInterceptor[]? _snapshotCache = [];
+
///
- /// Registers a new interceptor in the pipeline.
+ /// Registers new interceptor in the pipeline.
///
- /// The interceptor to register. Cannot be null.
+ /// The interceptor to register. Cannot be null.
/// Thrown if is null.
///
/// After adding, the interceptors list is sorted by Order to guarantee deterministic execution order.
+ /// The snapshot cache is invalidated so the next read lazily rebuilds it.
///
public void Register(IMutationInterceptor interceptor)
{
@@ -40,6 +52,7 @@ public void Register(IMutationInterceptor interceptor)
{
_interceptors.Add(interceptor);
_interceptors.Sort((a, b) => a.Order.CompareTo(b.Order));
+ _snapshotCache = null;
}
}
@@ -49,16 +62,43 @@ public void Register(IMutationInterceptor interceptor)
/// The name of the interceptor to remove.
public void Unregister(string name)
{
- lock (_lock) _interceptors.RemoveAll(i => i.Name == name);
+ lock (_lock)
+ {
+ _interceptors.RemoveAll(i => i.Name == name);
+ _snapshotCache = null;
+ }
}
///
- /// Gets a snapshot of the currently registered interceptors.
+ /// Gets snapshot of the currently registered interceptors.
///
- /// An array of interceptors.
+ /// An array of interceptors. Returns without taking
+ /// the lock when the cached snapshot is still valid and empty.
+ ///
+ /// volatile on guarantees an acquire fence on every read,
+ /// so the calling thread observe fully published array or null — never torn reference.
+ /// Because the array is immutable after publication, contents are implicitly safe.
+ ///
private IMutationInterceptor[] GetSnapshot()
{
- lock (_lock) return _interceptors.ToArray();
+ var cached = _snapshotCache;
+ if (cached is not null)
+ return cached;
+
+ lock (_lock)
+ {
+ // Re check under the lock in case another thread already rebuilt it.
+ cached = _snapshotCache;
+ if (cached is not null)
+ return cached;
+
+ cached = _interceptors.Count == 0
+ ? []
+ : [.. _interceptors];
+
+ _snapshotCache = cached;
+ return cached;
+ }
}
///
@@ -67,23 +107,51 @@ private IMutationInterceptor[] GetSnapshot()
/// The mutation intent.
/// The mutation context.
/// An array of interceptors that should be executed.
- private IMutationInterceptor[] GetApplicable(MutationIntent intent, MutationContext context)
+ ///
+ /// Uses the cached snapshot. When no interceptors are registered, returns the cached empty array
+ /// without any filtering overhead. When all interceptors are applicable, the snapshot is reused
+ /// directly without allocating a filtered copy.
+ /// A pooled buffer is used during filtering to avoid per-call List<T> allocation.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private IMutationInterceptor[] GetApplicable(
+ MutationIntent intent,
+ MutationContext context)
{
var snapshot = GetSnapshot();
- var result = new List(snapshot.Length);
+ if (snapshot.Length == 0)
+ return snapshot;
+
+ var pool = ArrayPool.Shared;
+ var buffer = pool.Rent(snapshot.Length);
+ var count = 0;
+ var excluded = false;
- foreach (var t in snapshot)
+ for (var i = 0; i < snapshot.Length; i++)
{
- if (t is MutationInterceptorBase baseInt)
+ var t = snapshot[i];
+ var shouldRun = t is not MutationInterceptorBase baseInt || baseInt.ShouldRun(intent, context);
+
+ if (shouldRun)
+ {
+ buffer[count++] = t;
+ }
+ else
{
- if (!baseInt.ShouldRun(intent, context))
- continue;
+ excluded = true;
}
+ }
- result.Add(t);
+ if (!excluded)
+ {
+ pool.Return(buffer);
+ return snapshot;
}
- return result.ToArray();
+ var result = new IMutationInterceptor[count];
+ Array.Copy(buffer, result, count);
+ pool.Return(buffer);
+ return result;
}
///
@@ -92,7 +160,10 @@ private IMutationInterceptor[] GetApplicable(MutationIntent intent, MutationCont
/// The action to execute for each interceptor.
/// The mutation intent.
/// The mutation context.
- private async Task ExecuteAsync(Func action, MutationIntent intent, MutationContext context)
+ private async Task ExecuteAsync(
+ Func action,
+ MutationIntent intent,
+ MutationContext context)
{
var interceptors = GetApplicable(intent, context);
foreach (var t in interceptors)
@@ -101,19 +172,115 @@ private async Task ExecuteAsync(Func action, Mutatio
}
}
- ///
- public Task OnBeforeMutationAsync(MutationIntent intent, MutationContext context, object state, string executionId, CancellationToken cancellationToken = default)
- => ExecuteAsync(i => i.OnBeforeMutationAsync(intent, context, state, executionId, cancellationToken), intent, context);
+ ///
+ /// Executes registered interceptors before a mutation is evaluated.
+ ///
+ /// The mutation intent.
+ /// The mutation execution context.
+ /// The current mutation state.
+ /// The unique execution identifier.
+ /// Token used to cancel the operation.
+ /// A task representing the asynchronous operation.
+ public Task OnBeforeMutationAsync(
+ MutationIntent intent,
+ MutationContext context,
+ object state,
+ string executionId,
+ CancellationToken cancellationToken = default) =>
+ ExecuteAsync(
+ interceptor => interceptor.OnBeforeMutationAsync(
+ intent,
+ context,
+ state,
+ executionId,
+ cancellationToken),
+ intent,
+ context);
- ///
- public Task OnAfterMutationAsync(MutationIntent intent, MutationContext context, object? oldState, object? newState, ChangeSet changes, string executionId, CancellationToken cancellationToken = default)
- => ExecuteAsync(i => i.OnAfterMutationAsync(intent, context, oldState, newState, changes, executionId, cancellationToken), intent, context);
+ ///
+ /// Executes registered interceptors after mutation has completed.
+ ///
+ /// The mutation intent.
+ /// The mutation execution context.
+ /// The state before the mutation.
+ /// The state after the mutation.
+ /// The changes produced by the mutation.
+ /// The unique execution identifier.
+ /// Token used to cancel the operation.
+ /// A task representing the asynchronous operation.
+ public Task OnAfterMutationAsync(
+ MutationIntent intent,
+ MutationContext context,
+ object? oldState,
+ object? newState,
+ ChangeSet changes,
+ string executionId,
+ CancellationToken cancellationToken = default) =>
+ ExecuteAsync(
+ interceptor => interceptor.OnAfterMutationAsync(
+ intent,
+ context,
+ oldState,
+ newState,
+ changes,
+ executionId,
+ cancellationToken),
+ intent,
+ context);
- ///
- public Task OnMutationFailedAsync(MutationIntent intent, MutationContext context, object state, Exception exception, string executionId, CancellationToken cancellationToken = default)
- => ExecuteAsync(i => i.OnMutationFailedAsync(intent, context, state, exception, executionId, cancellationToken), intent, context);
+ ///
+ /// Executes registered interceptors when mutation fails.
+ ///
+ /// The mutation intent.
+ /// The mutation execution context.
+ /// The current mutation state.
+ /// The exception that caused the mutation to fail.
+ /// The unique execution identifier.
+ /// Token used to cancel the operation.
+ /// A task representing the asynchronous operation.
+ public Task OnMutationFailedAsync(
+ MutationIntent intent,
+ MutationContext context,
+ object state,
+ Exception exception,
+ string executionId,
+ CancellationToken cancellationToken = default) =>
+ ExecuteAsync(
+ interceptor => interceptor.OnMutationFailedAsync(
+ intent,
+ context,
+ state,
+ exception,
+ executionId,
+ cancellationToken),
+ intent,
+ context);
- ///
- public Task OnPolicyBlockedAsync(MutationIntent intent, MutationContext context, object state, PolicyDecision decision, string executionId, CancellationToken cancellationToken = default)
- => ExecuteAsync(i => i.OnPolicyBlockedAsync(intent, context, state, decision, executionId, cancellationToken), intent, context);
-}
\ No newline at end of file
+ ///
+ /// Executes registered interceptors when mutation is blocked by policy.
+ ///
+ /// The mutation intent.
+ /// The mutation execution context.
+ /// The current mutation state.
+ /// The policy decision that blocked the mutation.
+ /// The unique execution identifier.
+ /// Token used to cancel the operation.
+ /// A task representing the asynchronous operation.
+ public Task OnPolicyBlockedAsync(
+ MutationIntent intent,
+ MutationContext context,
+ object state,
+ PolicyDecision decision,
+ string executionId,
+ CancellationToken cancellationToken = default) =>
+ ExecuteAsync(
+ interceptor => interceptor.OnPolicyBlockedAsync(
+ intent,
+ context,
+ state,
+ decision,
+ executionId,
+ cancellationToken),
+ intent,
+ context);
+}
diff --git a/src/Runtime/MutationEngine.cs b/src/Runtime/MutationEngine.cs
index 42131f0..9be496b 100644
--- a/src/Runtime/MutationEngine.cs
+++ b/src/Runtime/MutationEngine.cs
@@ -15,8 +15,12 @@
namespace ModularityKit.Mutator.Runtime;
///
-/// Coordinates mutation execution by handling admission, failure wrapping, and public runtime APIs.
+/// Coordinates mutation execution and runtime governance.
///
+///
+/// Handles mutation execution, policy evaluation, interception, auditing,
+/// history tracking, metrics, concurrency control, and failure processing.
+///
internal sealed class MutationEngine(
IMutationExecutor executor,
IPolicyRegistry policyRegistry,
@@ -33,6 +37,7 @@ internal sealed class MutationEngine(
private readonly IMetricsCollector _metricsCollector = metricsCollector ?? throw new ArgumentNullException(nameof(metricsCollector));
private readonly MutationEngineOptions _options = options ?? throw new ArgumentNullException(nameof(options));
private readonly MutationExecutionConcurrencyGate _concurrencyGate = CreateConcurrencyGate(options);
+ private static long _executionCounter;
private readonly MutationExecutionFailureHandler _failureHandler = new(interceptorPipeline, auditor);
private readonly MutationExecutionPipeline _executionPipeline =
new(
@@ -58,7 +63,7 @@ public async Task> ExecuteAsync(
TState state,
CancellationToken cancellationToken = default)
{
- var executionId = Guid.NewGuid().ToString();
+ var executionId = Interlocked.Increment(ref _executionCounter).ToString("x8");
var stopwatch = Stopwatch.StartNew();
IMetricsScope? metricsScope = null;