From e18ef60ba0b1bfe54587c4cee1bdb67fc8310b47 Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Mon, 27 Jul 2026 19:54:04 +0200 Subject: [PATCH 1/2] perf: avoid unnecessary async state machine --- .../MutationExecutionOutcomeProcessor.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs b/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs index f2b6191..8de1e18 100644 --- a/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs +++ b/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs @@ -153,7 +153,7 @@ await StoreInHistoryAsync( /// The context containing execution timing and metrics information. /// The mutation result to finalize. /// The mutation result with finalized execution metrics. - public async Task> FinalizeResultAsync( + public Task> FinalizeResultAsync( MutationExecutionContext executionContext, MutationResult result) { @@ -162,12 +162,22 @@ public async Task> FinalizeResultAsync( if (executionContext.MetricsScope is not null) { - await _metricsCollector.RecordAsync( - executionContext.ExecutionId, - executionContext.MetricsScope.Build(), - executionContext.CancellationToken).ConfigureAwait(false); + return FinalizeWithMetricsAsync(executionContext, result, totalElapsed); } + return Task.FromResult(result); + } + + private async Task> FinalizeWithMetricsAsync( + MutationExecutionContext executionContext, + MutationResult result, + TimeSpan totalElapsed) + { + await _metricsCollector.RecordAsync( + executionContext.ExecutionId, + executionContext.MetricsScope!.Build(), + executionContext.CancellationToken).ConfigureAwait(false); + return result with { Metrics = result.Metrics with { ExecutionTime = totalElapsed } From 62d6776cbd452135c3bcbf008167ae30e4c28d7c Mon Sep 17 00:00:00 2001 From: "Rian.be" Date: Tue, 28 Jul 2026 00:44:18 +0200 Subject: [PATCH 2/2] perf: optimize policy evaluation and execution pipeline --- Benchmarks/Diagnostics/Support/NoOpAuditor.cs | 1 + .../Diagnostics/Support/NoOpHistoryStore.cs | 1 + Benchmarks/Policy/PolicyBenchmarkSupport.cs | 39 ++- src/Abstractions/Policies/PolicyDecision.cs | 18 +- .../Evaluation/MutationPolicyEvaluator.cs | 255 ++++++++++++++---- .../Execution/MutationExecutionContext.cs | 60 ++++- .../MutationExecutionOutcomeProcessor.cs | 9 +- .../Execution/MutationExecutionPipeline.cs | 112 ++++++-- src/Runtime/MutationEngine.cs | 12 +- 9 files changed, 408 insertions(+), 99 deletions(-) diff --git a/Benchmarks/Diagnostics/Support/NoOpAuditor.cs b/Benchmarks/Diagnostics/Support/NoOpAuditor.cs index 596034c..8ed5671 100644 --- a/Benchmarks/Diagnostics/Support/NoOpAuditor.cs +++ b/Benchmarks/Diagnostics/Support/NoOpAuditor.cs @@ -7,6 +7,7 @@ namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support; /// internal sealed class NoOpAuditor : IMutationAuditor { + public bool IsEnabled => false; /// /// Ignores the supplied audit entry. /// diff --git a/Benchmarks/Diagnostics/Support/NoOpHistoryStore.cs b/Benchmarks/Diagnostics/Support/NoOpHistoryStore.cs index 887fcb4..59a3e7e 100644 --- a/Benchmarks/Diagnostics/Support/NoOpHistoryStore.cs +++ b/Benchmarks/Diagnostics/Support/NoOpHistoryStore.cs @@ -7,6 +7,7 @@ namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support; /// internal sealed class NoOpHistoryStore : IMutationHistoryStore { + public bool IsEnabled => false; /// /// Ignores the supplied history entry. /// diff --git a/Benchmarks/Policy/PolicyBenchmarkSupport.cs b/Benchmarks/Policy/PolicyBenchmarkSupport.cs index 4269b0d..a44c669 100644 --- a/Benchmarks/Policy/PolicyBenchmarkSupport.cs +++ b/Benchmarks/Policy/PolicyBenchmarkSupport.cs @@ -1,11 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using ModularityKit.Mutator.Abstractions; +using ModularityKit.Mutator.Abstractions.Audit; using ModularityKit.Mutator.Abstractions.Changes; using ModularityKit.Mutator.Abstractions.Context; using ModularityKit.Mutator.Abstractions.Engine; +using ModularityKit.Mutator.Abstractions.History; using ModularityKit.Mutator.Abstractions.Intent; using ModularityKit.Mutator.Abstractions.Policies; using ModularityKit.Mutator.Abstractions.Results; +using ModularityKit.Mutator.Benchmarks.Diagnostics.Support; using ModularityKit.Mutator.Runtime; namespace ModularityKit.Mutator.Benchmarks.Policy; @@ -21,6 +24,8 @@ public static IMutationEngine BuildEngine(Action? configure = n { var services = new ServiceCollection(); services.AddMutators(MutationEngineOptions.Performance); + services.AddSingleton(new NoOpAuditor()); + services.AddSingleton(new NoOpHistoryStore()); var engine = services .BuildServiceProvider() @@ -85,7 +90,15 @@ public MutationResult Apply(PolicyBenchmarkState state) /// public sealed class SyncAllowBenchmarkPolicy : IMutationPolicy { - public SyncAllowBenchmarkPolicy(int priority) => Priority = priority; + private readonly PolicyDecision _decision; + private readonly Task _decisionTask; + + public SyncAllowBenchmarkPolicy(int priority) + { + Priority = priority; + _decision = PolicyDecision.Allow(Name, "Synchronous benchmark policy allowed the mutation."); + _decisionTask = Task.FromResult(_decision); + } public string Name => $"{nameof(SyncAllowBenchmarkPolicy)}_{Priority}"; @@ -94,7 +107,13 @@ public sealed class SyncAllowBenchmarkPolicy : IMutationPolicy "Synchronous allow policy for benchmark measurements."; public PolicyDecision Evaluate(IMutation mutation, PolicyBenchmarkState state) - => PolicyDecision.Allow(Name, "Synchronous benchmark policy allowed the mutation."); + => _decision; + + public Task EvaluateAsync( + IMutation mutation, + PolicyBenchmarkState state, + CancellationToken cancellationToken = default) + => _decisionTask; } /// @@ -102,7 +121,15 @@ public PolicyDecision Evaluate(IMutation mutation, PolicyB /// public sealed class AsyncAllowBenchmarkPolicy : IMutationPolicy { - public AsyncAllowBenchmarkPolicy(int priority) => Priority = priority; + private readonly PolicyDecision _decision; + private readonly Task _decisionTask; + + public AsyncAllowBenchmarkPolicy(int priority) + { + Priority = priority; + _decision = PolicyDecision.Allow(Name, "Asynchronous benchmark policy allowed the mutation."); + _decisionTask = Task.FromResult(_decision); + } public string Name => $"{nameof(AsyncAllowBenchmarkPolicy)}_{Priority}"; @@ -110,15 +137,13 @@ public sealed class AsyncAllowBenchmarkPolicy : IMutationPolicy "Asynchronous allow policy for benchmark measurements."; - public async Task EvaluateAsync( + public Task EvaluateAsync( IMutation mutation, PolicyBenchmarkState state, CancellationToken cancellationToken = default) { - await Task.CompletedTask; cancellationToken.ThrowIfCancellationRequested(); - - return PolicyDecision.Allow(Name, "Asynchronous benchmark policy allowed the mutation."); + return _decisionTask; } } } diff --git a/src/Abstractions/Policies/PolicyDecision.cs b/src/Abstractions/Policies/PolicyDecision.cs index a660db2..a6e23b8 100644 --- a/src/Abstractions/Policies/PolicyDecision.cs +++ b/src/Abstractions/Policies/PolicyDecision.cs @@ -1,7 +1,7 @@ namespace ModularityKit.Mutator.Abstractions.Policies; /// -/// Represents the decision of a policy regarding a mutation. +/// Represents the decision of policy regarding mutation. /// Contains approval, denial, modification instructions, and metadata. /// public sealed class PolicyDecision @@ -46,6 +46,8 @@ public sealed class PolicyDecision /// public DateTimeOffset Timestamp { get; init; } = DateTimeOffset.UtcNow; + private static readonly PolicyDecision _allow = new() { IsAllowed = true }; + /// /// Creates an allow decision. /// @@ -53,15 +55,19 @@ public sealed class PolicyDecision /// Optional human-readable reason. /// A policy decision that allows the mutation. public static PolicyDecision Allow(string? policyName = null, string? reason = null) - => new() + { + if (policyName is null && reason is null) + return _allow; + return new PolicyDecision { IsAllowed = true, PolicyName = policyName, Reason = reason }; + } /// - /// Creates a deny decision with standard error severity. + /// Creates deny decision with standard error severity. /// /// Reason for denial. /// Optional policy name. @@ -76,7 +82,7 @@ public static PolicyDecision Deny(string reason, string? policyName = null) }; /// - /// Creates a deny decision with critical severity. + /// Creates deny decision with critical severity. /// /// Reason for denial. /// Optional policy name. @@ -91,7 +97,7 @@ public static PolicyDecision DenyCritical(string reason, string? policyName = nu }; /// - /// Creates a modification decision that adjusts mutation values. + /// Creates modification decision that adjusts mutation values. /// /// Dictionary of modifications to apply. /// Optional policy name. @@ -107,7 +113,7 @@ public static PolicyDecision Modify( }; /// - /// Creates a decision that requires additional approval before proceeding. + /// Creates decision that requires additional approval before proceeding. /// /// The requirement that must be fulfilled. /// Optional policy name. diff --git a/src/Runtime/Internal/Evaluation/MutationPolicyEvaluator.cs b/src/Runtime/Internal/Evaluation/MutationPolicyEvaluator.cs index 925c145..9dba07e 100644 --- a/src/Runtime/Internal/Evaluation/MutationPolicyEvaluator.cs +++ b/src/Runtime/Internal/Evaluation/MutationPolicyEvaluator.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using ModularityKit.Mutator.Abstractions; using ModularityKit.Mutator.Abstractions.Engine; using ModularityKit.Mutator.Abstractions.Exceptions; @@ -12,6 +13,8 @@ namespace ModularityKit.Mutator.Runtime.Internal.Evaluation; /// The evaluator resolves policies from the registry, applies deterministic /// priority ordering, invokes each policy with optional timeout handling, and /// returns the first blocking or modifying decision. +/// Sorted policies are cached per state type to avoid repeated sorting and registry lookups. +/// Synchronous policies complete without allocating async state machines. /// internal sealed class MutationPolicyEvaluator( IPolicyRegistry policyRegistry, @@ -19,6 +22,7 @@ internal sealed class MutationPolicyEvaluator( { private readonly IPolicyRegistry _policyRegistry = policyRegistry ?? throw new ArgumentNullException(nameof(policyRegistry)); private readonly MutationEngineOptions _options = options ?? throw new ArgumentNullException(nameof(options)); + private readonly ConcurrentDictionary _sortedPolicies = new(); /// /// Evaluates all registered policies for the supplied mutation and state. @@ -30,107 +34,266 @@ internal sealed class MutationPolicyEvaluator( /// /// The first blocking or modifying , or an allow decision when all policies pass. /// - public async Task EvaluateAsync( + public ValueTask EvaluateAsync( IMutation mutation, TState state, CancellationToken cancellationToken) { - var policies = _policyRegistry.GetPolicies(); + var policies = GetSortedPolicies(); - foreach (var policy in policies - .OrderByDescending(p => p.Priority) - .ThenBy(p => p.Name, StringComparer.Ordinal) - .ThenBy(p => p.GetType().FullName ?? string.Empty, StringComparer.Ordinal)) + if (policies.Length == 0) + return new ValueTask(PolicyDecision.Allow()); + + if (!_options.PolicyEvaluationTimeout.HasValue) + return EvaluateNoTimeoutAsync(policies, mutation, state, cancellationToken); + + return new ValueTask( + EvaluateWithTimeoutAsync(policies, mutation, state, cancellationToken)); + } + + /// + /// Retrieves sorted policies for the given state type from the cache or builds and caches them. + /// + /// + /// The first access per state type queries the registry and applies OrderByDescending / + /// ThenBy sorting. Subsequent calls return the cached array without any registry or sort overhead. + /// Thread safety relies on ConcurrentDictionary — duplicate builds are harmless since the array is immutable. + /// + /// The state type whose policies are being retrieved. + /// Cached array of policies sorted by descending priority, then by name, then by full type name. + private IMutationPolicy[] GetSortedPolicies() + { + var type = typeof(TState); + if (_sortedPolicies.TryGetValue(type, out var cached)) + return (IMutationPolicy[])cached; + + var policies = _policyRegistry.GetPolicies() + .OrderByDescending(static p => p.Priority) + .ThenBy(static p => p.Name, StringComparer.Ordinal) + .ThenBy(static p => p.GetType().FullName ?? string.Empty, StringComparer.Ordinal) + .ToArray(); + + _sortedPolicies[type] = policies; + return policies; + } + + /// + /// Evaluates the policy set without per policy timeouts. + /// + /// + /// The first blocking or modifying wrapped in a , + /// or an allow decision when all policies pass. + /// When all policies in the array complete synchronously the method returns a synchronously + /// completed without allocating an async state machine. + /// + /// The state type handled by the policies. + /// The sorted policy array to evaluate. + /// The mutation being evaluated. + /// The current state snapshot. + /// Token used to cancel policy evaluation. + /// Thrown when policy call throws non cancellation exception synchronously. + private static ValueTask EvaluateNoTimeoutAsync( + IMutationPolicy[] policies, + IMutation mutation, + TState state, + CancellationToken cancellationToken) + { + for (var i = 0; i < policies.Length; i++) { - var decision = await EvaluatePolicyAsync( - policy, - mutation, - state, - cancellationToken).ConfigureAwait(false); + Task task; + try + { + task = policies[i].EvaluateAsync(mutation, state, cancellationToken); + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + throw new PolicyEvaluationException( + policies[i].Name, + $"Policy '{policies[i].Name}' evaluation failed: {ex.Message}", + ex); + } - if (!decision.IsAllowed || decision.Modifications != null) - return decision; + if (task.IsCompletedSuccessfully) + { + var decision = task.Result; + if (!decision.IsAllowed || decision.Modifications is not null) + return new ValueTask(decision); + } + else + { + return AwaitRemainingAsync(policies, i, task, mutation, state, cancellationToken); + } } - return PolicyDecision.Allow(); + return new ValueTask(PolicyDecision.Allow()); } /// - /// Evaluates a single policy with optional runtime timeout handling. + /// Continues evaluation asynchronously after the first non synchronously-completing policy is encountered. /// - /// The state type handled by the policy. - /// The policy to evaluate. + /// The state type handled by the policies. + /// The sorted policy array to evaluate. + /// The index of the policy whose task is being awaited. + /// The task of the current policy that did not complete synchronously. /// The mutation being evaluated. /// The current state snapshot. /// Token used to cancel policy evaluation. - /// The decision produced by the policy. - /// - /// Thrown when policy evaluation exceeds the configured timeout. - /// - /// - /// Thrown when the policy evaluation fails with a non-cancellation exception. - /// - private async Task EvaluatePolicyAsync( - IMutationPolicy policy, + /// + /// The first blocking or modifying , or an allow decision when all policies pass. + /// + /// Thrown when policy evaluation fails with non cancellation exception. + private static async ValueTask AwaitRemainingAsync( + IMutationPolicy[] policies, + int startIndex, + Task currentTask, IMutation mutation, TState state, CancellationToken cancellationToken) { - if (!_options.PolicyEvaluationTimeout.HasValue) - return await InvokePolicyAsync(policy, mutation, state, cancellationToken).ConfigureAwait(false); + var decision = await CatchPolicyErrorAsync(currentTask, policies[startIndex].Name) + .ConfigureAwait(false); - using var timeoutSource = new CancellationTokenSource(_options.PolicyEvaluationTimeout.Value); - using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource( - cancellationToken, - timeoutSource.Token); + if (!decision.IsAllowed || decision.Modifications is not null) + return decision; - try + for (var i = startIndex + 1; i < policies.Length; i++) { - return await policy.EvaluateAsync(mutation, state, linkedSource.Token).ConfigureAwait(false); + Task task; + try + { + task = policies[i].EvaluateAsync(mutation, state, cancellationToken); + } + catch (OperationCanceledException) { throw; } + catch (Exception ex) + { + throw new PolicyEvaluationException( + policies[i].Name, + $"Policy '{policies[i].Name}' evaluation failed: {ex.Message}", + ex); + } + + if (task.IsCompletedSuccessfully) + { + decision = task.Result; + } + else + { + decision = await CatchPolicyErrorAsync(task, policies[i].Name) + .ConfigureAwait(false); + } + + if (!decision.IsAllowed || decision.Modifications is not null) + return decision; } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + + return PolicyDecision.Allow(); + } + + /// + /// Awaits policy task and wraps non-cancellation failures into . + /// + /// The task returned by policy.EvaluateAsync. + /// The name of the policy, used as the exception policy name. + /// The produced by the policy. + /// Rethrown without wrapping. + /// + /// Thrown when the task faults with any exception other than . + /// The original exception is set as the inner exception. + /// + private static async Task CatchPolicyErrorAsync( + Task task, + string policyName) + { + try { - throw; + return await task.ConfigureAwait(false); } - catch (OperationCanceledException) when (timeoutSource.IsCancellationRequested) + catch (OperationCanceledException) { - throw new PolicyEvaluationTimeoutException(policy.Name, _options.PolicyEvaluationTimeout.Value); + throw; } catch (Exception ex) { throw new PolicyEvaluationException( - policy.Name, - $"Policy '{policy.Name}' evaluation failed: {ex.Message}", + policyName, + $"Policy '{policyName}' evaluation failed: {ex.Message}", ex); } } /// - /// Invokes a policy and normalizes unexpected failures into policy evaluation exceptions. + /// Evaluates all policies with per-policy timeout enforcement. + /// + /// The state type handled by the policies. + /// The sorted policy array to evaluate. + /// The mutation being evaluated. + /// The current state snapshot. + /// Token used to cancel policy evaluation. + /// + /// The first blocking or modifying , or an allow decision when all policies pass. + /// + /// + /// Thrown when any policy evaluation exceeds PolicyEvaluationTimeout. + /// + /// Thrown when policy evaluation fails with a non-cancellation exception. + private async Task EvaluateWithTimeoutAsync( + IMutationPolicy[] policies, + IMutation mutation, + TState state, + CancellationToken cancellationToken) + { + for (var i = 0; i < policies.Length; i++) + { + var decision = await EvaluateSingleWithTimeoutAsync( + policies[i], mutation, state, cancellationToken).ConfigureAwait(false); + + if (!decision.IsAllowed || decision.Modifications is not null) + return decision; + } + + return PolicyDecision.Allow(); + } + + /// + /// Evaluates single policy with dedicated cancellation timeout. /// /// The state type handled by the policy. - /// The policy to invoke. + /// The policy to evaluate. /// The mutation being evaluated. /// The current state snapshot. /// Token used to cancel policy evaluation. - /// The decision produced by the policy. + /// The produced by the policy. + /// + /// Thrown when the policy evaluation exceeds PolicyEvaluationTimeout. + /// + /// Rethrown when the caller's cancellation token triggers before the timeout. /// - /// Thrown when the policy evaluation fails with a non-cancellation exception. + /// Thrown when the policy evaluation fails with non cancellation exception. + /// The original exception is set as the inner exception. /// - private static async Task InvokePolicyAsync( + private async Task EvaluateSingleWithTimeoutAsync( IMutationPolicy policy, IMutation mutation, TState state, CancellationToken cancellationToken) { + using var timeoutSource = new CancellationTokenSource(_options.PolicyEvaluationTimeout!.Value); + using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource( + cancellationToken, timeoutSource.Token); + try { - return await policy.EvaluateAsync(mutation, state, cancellationToken).ConfigureAwait(false); + return await policy.EvaluateAsync(mutation, state, linkedSource.Token).ConfigureAwait(false); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } + catch (OperationCanceledException) when (timeoutSource.IsCancellationRequested) + { + throw new PolicyEvaluationTimeoutException(policy.Name, _options.PolicyEvaluationTimeout.Value); + } catch (Exception ex) { throw new PolicyEvaluationException( diff --git a/src/Runtime/Internal/Execution/MutationExecutionContext.cs b/src/Runtime/Internal/Execution/MutationExecutionContext.cs index fe23fe5..e087ff0 100644 --- a/src/Runtime/Internal/Execution/MutationExecutionContext.cs +++ b/src/Runtime/Internal/Execution/MutationExecutionContext.cs @@ -1,42 +1,76 @@ using System.Diagnostics; +using ModularityKit.Mutator.Abstractions; using ModularityKit.Mutator.Abstractions.Engine; using ModularityKit.Mutator.Abstractions.Metrics; namespace ModularityKit.Mutator.Runtime.Internal.Execution; /// -/// Carries shared execution state across the runtime mutation pipeline. +/// Carries mutation, state, and runtime metadata through the execution pipeline. /// -/// The state type handled by the mutation. -internal sealed record MutationExecutionContext +/// The state type the mutation operates on. +/// +/// This struct is created by before the pipeline +/// starts and flows through concurrency admission, policy evaluation, interception, +/// execution, and outcome processing. Using readonly record struct +/// avoids heap allocation per execution while preserving value semantics. +/// +internal readonly record struct MutationExecutionContext { /// - /// The mutation being executed. + /// The mutation instance being executed. /// - public IMutation Mutation { get; init; } = null!; + /// + /// Provides access to the mutation's intent, context, Apply, Validate, + /// and Simulate methods throughout the pipeline stages. + /// + public IMutation Mutation { get; init; } /// - /// The current state snapshot being mutated. + /// The current state snapshot before mutation execution. /// - public TState State { get; init; } = default!; + /// + /// Passed to policy evaluation, validation, interception, and finally to + /// Apply / Simulate. The executor produces the new state + /// which replaces this value in the result. + /// + public TState State { get; init; } /// - /// The unique identifier for this execution run. + /// Unique identifier for this execution, used in audit entries, history records, + /// metrics scopes, and interceptor callbacks. /// - public string ExecutionId { get; init; } = string.Empty; + /// + /// Generated by via Interlocked.Increment + /// formatted as an eight digit hexadecimal string. + /// + public string ExecutionId { get; init; } /// - /// The shared stopwatch tracking total execution time. + /// Stopwatch.GetTimestamp value captured at the start of execution, used + /// to compute elapsed time via Stopwatch.GetElapsedTime without allocating + /// instance. /// - public Stopwatch Stopwatch { get; init; } = null!; + public long StartTimestamp { get; init; } /// - /// The optional metrics scope for detailed runtime metrics. + /// Optional metrics scope for recording detailed execution diagnostics. /// + /// + /// Allocated by only when + /// is true. + /// When null, all metric-recording calls in the pipeline become no-ops + /// and the result is returned without the with expression. + /// public IMetricsScope? MetricsScope { get; init; } /// - /// The cancellation token for the current execution. + /// Token used to observe cancellation requests throughout the execution pipeline. /// + /// + /// Propagated to concurrency gate wait, policy evaluation, interceptor callbacks, + /// executor, and outcome processor. The same token instance flows through the + /// entire execution so that single cancellation source affects all stages. + /// public CancellationToken CancellationToken { get; init; } } diff --git a/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs b/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs index 8de1e18..6ccd079 100644 --- a/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs +++ b/src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using ModularityKit.Mutator.Abstractions.Audit; using ModularityKit.Mutator.Abstractions.Context; using ModularityKit.Mutator.Abstractions.Engine; @@ -64,7 +65,7 @@ await AuditFailureAsync( executionContext.Mutation, blockedResult, executionContext.ExecutionId, - executionContext.Stopwatch.Elapsed).ConfigureAwait(false); + Stopwatch.GetElapsedTime(executionContext.StartTimestamp)).ConfigureAwait(false); return await FinalizeResultAsync(executionContext, blockedResult).ConfigureAwait(false); } @@ -88,7 +89,7 @@ await AuditFailureAsync( executionContext.Mutation, validationFailureResult, executionContext.ExecutionId, - executionContext.Stopwatch.Elapsed).ConfigureAwait(false); + Stopwatch.GetElapsedTime(executionContext.StartTimestamp)).ConfigureAwait(false); return await FinalizeResultAsync(executionContext, validationFailureResult).ConfigureAwait(false); } @@ -113,7 +114,7 @@ public async Task> CompleteMutationAsync( MutationResult mutationResult, PolicyDecision policyDecision) { - var totalElapsed = executionContext.Stopwatch.Elapsed; + var totalElapsed = Stopwatch.GetElapsedTime(executionContext.StartTimestamp); var finalizedMutationResult = PolicyModificationApplier.Apply(mutationResult, policyDecision.Modifications); await _interceptorPipeline.OnAfterMutationAsync( @@ -157,7 +158,7 @@ public Task> FinalizeResultAsync( MutationExecutionContext executionContext, MutationResult result) { - var totalElapsed = executionContext.Stopwatch.Elapsed; + var totalElapsed = Stopwatch.GetElapsedTime(executionContext.StartTimestamp); executionContext.MetricsScope?.RecordStateSize(StateSizeEstimator.Estimate(executionContext.State)); if (executionContext.MetricsScope is not null) diff --git a/src/Runtime/Internal/Execution/MutationExecutionPipeline.cs b/src/Runtime/Internal/Execution/MutationExecutionPipeline.cs index 507aad5..dfb3194 100644 --- a/src/Runtime/Internal/Execution/MutationExecutionPipeline.cs +++ b/src/Runtime/Internal/Execution/MutationExecutionPipeline.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using ModularityKit.Mutator.Abstractions; using ModularityKit.Mutator.Abstractions.Context; using ModularityKit.Mutator.Abstractions.Interception; @@ -10,6 +11,19 @@ namespace ModularityKit.Mutator.Runtime.Internal.Execution; /// /// Orchestrates the high level mutation pipeline after concurrency admission succeeds. /// +/// +/// +/// The pipeline runs policy evaluation, validation, mode-specific execution, and +/// outcome processing in fixed order. Each stage can short circuit: +/// a blocking policy decision skips execution; a validation failure skips execution; +/// otherwise the mutation runs through the configured mode runner and the result +/// is finalized by the outcome processor. +/// +/// +/// Policy evaluation timing is recorded on the metrics scope when available. +/// Validation is skipped for Commit mode when AlwaysValidate is false. +/// +/// internal sealed class MutationExecutionPipeline( MutationPolicyEvaluator policyEvaluator, IInterceptorPipeline interceptorPipeline, @@ -24,8 +38,33 @@ internal sealed class MutationExecutionPipeline( private readonly MutationEngineOptions _options = options ?? throw new ArgumentNullException(nameof(options)); /// - /// Runs policy evaluation, validation, execution, and outcome processing for a mutation execution context. + /// Runs policy evaluation, validation, execution, and outcome processing for mutation execution context. /// + /// The state type handled by the mutation. + /// + /// The context carrying the mutation, current state, execution metadata, + /// cancellation token, and optional metrics scope. + /// + /// + /// A containing the new state, applied changes, + /// and final execution metrics. + /// + /// + /// + /// The execution order is: + /// + /// Before mutation interceptor notification + /// Policy evaluation + /// Validation (when required by mode and options) + /// Mode-specific execution (simulate / validate only / commit) + /// After mutation interceptor notification, auditing, history, and metrics finalization + /// + /// + /// + /// A blocking policy decision or validation failure stops the pipeline immediately + /// and returns the corresponding result without executing the mutation. + /// + /// public async Task> ExecuteAsync(MutationExecutionContext executionContext) { await _interceptorPipeline.OnBeforeMutationAsync( @@ -54,38 +93,81 @@ await _interceptorPipeline.OnBeforeMutationAsync( } /// - /// Evaluates policies and records the elapsed policy evaluation time in metrics. + /// Evaluates all registered policies and records the elapsed policy evaluation time in metrics. /// - private async Task EvaluatePolicyDecisionAsync( + /// + /// Policy evaluation timing is measured via Stopwatch.GetElapsedTime and recorded on + /// MetricsScope when available. The measurement excludes interceptor and validation time. + /// + /// The state type handled by the mutation. + /// The context carrying the mutation, state, execution metadata, and optional metrics scope. + /// + /// A containing the produced by policy evaluation. + /// When the evaluator completes synchronously the result is wrapped without allocating a Task. + /// + private ValueTask EvaluatePolicyDecisionAsync( MutationExecutionContext executionContext) { - var policyEvaluationStart = executionContext.Stopwatch.Elapsed; - var policyDecision = await _policyEvaluator - .EvaluateAsync( - executionContext.Mutation, - executionContext.State, - executionContext.CancellationToken) - .ConfigureAwait(false); + var policyEvaluationStart = Stopwatch.GetElapsedTime(executionContext.StartTimestamp); + var evaluateTask = _policyEvaluator.EvaluateAsync( + executionContext.Mutation, + executionContext.State, + executionContext.CancellationToken); - executionContext.MetricsScope?.RecordPolicyEvaluationTime( - executionContext.Stopwatch.Elapsed - policyEvaluationStart); + if (evaluateTask.IsCompletedSuccessfully) + { + var policyDecision = evaluateTask.Result; + executionContext.MetricsScope?.RecordPolicyEvaluationTime( + Stopwatch.GetElapsedTime(executionContext.StartTimestamp) - policyEvaluationStart); + return new ValueTask(policyDecision); + } + + return AwaitPolicyAndRecordAsync(evaluateTask, executionContext, policyEvaluationStart); + } + /// + /// Awaits the policy evaluation task and records timing once the result is available. + /// + /// The state type handled by the mutation. + /// The in flight policy evaluation task. + /// The context carrying the start timestamp and optional metrics scope. + /// The GetElapsedTime value captured before the evaluator was invoked. + /// The produced by policy evaluation. + private static async ValueTask AwaitPolicyAndRecordAsync( + ValueTask evaluateTask, + MutationExecutionContext executionContext, + TimeSpan policyEvaluationStart) + { + var policyDecision = await evaluateTask.ConfigureAwait(false); + executionContext.MetricsScope?.RecordPolicyEvaluationTime( + Stopwatch.GetElapsedTime(executionContext.StartTimestamp) - policyEvaluationStart); return policyDecision; } /// - /// Validates the mutation when the current mode and engine options require it. + /// Validates the mutation when the current execution mode and engine options require it. /// + /// + /// Validation is skipped entirely in Commit mode when AlwaysValidate is + /// false (the performance-optimized configuration). Validation time is recorded + /// on the metrics scope when available. + /// + /// The state type handled by the mutation. + /// The context carrying the mutation and current state. + /// + /// A with validation errors when validation fails; + /// otherwise null. + /// private MutationResult? ValidateIfRequired( MutationExecutionContext executionContext) { if (executionContext.Mutation.Context.Mode == MutationMode.Commit && !_options.AlwaysValidate) return null; - var validationStart = executionContext.Stopwatch.Elapsed; + var validationStart = Stopwatch.GetElapsedTime(executionContext.StartTimestamp); var validation = executionContext.Mutation.Validate(executionContext.State); executionContext.MetricsScope?.RecordValidationTime( - executionContext.Stopwatch.Elapsed - validationStart); + Stopwatch.GetElapsedTime(executionContext.StartTimestamp) - validationStart); return validation.IsValid ? null diff --git a/src/Runtime/MutationEngine.cs b/src/Runtime/MutationEngine.cs index 9be496b..2bd5aa6 100644 --- a/src/Runtime/MutationEngine.cs +++ b/src/Runtime/MutationEngine.cs @@ -64,7 +64,7 @@ public async Task> ExecuteAsync( CancellationToken cancellationToken = default) { var executionId = Interlocked.Increment(ref _executionCounter).ToString("x8"); - var stopwatch = Stopwatch.StartNew(); + var startTimestamp = Stopwatch.GetTimestamp(); IMetricsScope? metricsScope = null; await using var executionLease = await _concurrencyGate @@ -79,7 +79,7 @@ public async Task> ExecuteAsync( Mutation = mutation, State = state, ExecutionId = executionId, - Stopwatch = stopwatch, + StartTimestamp = startTimestamp, MetricsScope = metricsScope, CancellationToken = cancellationToken }; @@ -94,23 +94,19 @@ public async Task> ExecuteAsync( } catch (MutationException ex) { - stopwatch.Stop(); - await _failureHandler.HandleKnownExceptionAsync( executionContext, ex, - stopwatch.Elapsed).ConfigureAwait(false); + Stopwatch.GetElapsedTime(startTimestamp)).ConfigureAwait(false); throw; } catch (Exception ex) { - stopwatch.Stop(); - throw await _failureHandler.HandleUnexpectedExceptionAsync( executionContext, ex, - stopwatch.Elapsed).ConfigureAwait(false); + Stopwatch.GetElapsedTime(startTimestamp)).ConfigureAwait(false); } finally {