From ef0da2013b31f7bc80315b343186e14c58d44d01 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Mon, 27 Jul 2026 02:14:23 +0200 Subject: [PATCH] perf(mutator): optimize interceptor pipeline and execution IDs --- .../Interception/InterceptorPipeline.cs | 225 +++++++++++++++--- src/Runtime/MutationEngine.cs | 9 +- 2 files changed, 203 insertions(+), 31 deletions(-) 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;