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 f2b6191..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(
@@ -153,21 +154,31 @@ 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)
{
- var totalElapsed = executionContext.Stopwatch.Elapsed;
+ var totalElapsed = Stopwatch.GetElapsedTime(executionContext.StartTimestamp);
executionContext.MetricsScope?.RecordStateSize(StateSizeEstimator.Estimate(executionContext.State));
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 }
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
{