Skip to content

Commit 29cf86e

Browse files
committed
benchmarks: add diagnostics suite for #58
1 parent 44567df commit 29cf86e

10 files changed

Lines changed: 523 additions & 2 deletions
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModularityKit.Mutator.Abstractions.Audit;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.History;
6+
using ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
7+
8+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics;
9+
10+
/// <summary>
11+
/// Benchmarks audit, history, and logging-style overhead in the core mutation pipeline.
12+
/// </summary>
13+
[BenchmarkCategory("Diagnostics")]
14+
[MemoryDiagnoser]
15+
[InProcess]
16+
public class DiagnosticsOverheadBenchmarks
17+
{
18+
private IMutationEngine _noDiagnosticsEngine = null!;
19+
private IMutationEngine _auditHistoryEngine = null!;
20+
private IMutationEngine _combinedDiagnosticsEngine = null!;
21+
private DiagnosticsState _state = null!;
22+
private DiagnosticsMutation _mutation = null!;
23+
24+
/// <summary>
25+
/// Prepares baseline, audit-history, and combined observability benchmark engines.
26+
/// </summary>
27+
[GlobalSetup]
28+
public void Setup()
29+
{
30+
_noDiagnosticsEngine = DiagnosticsBenchmarkScenario.BuildEngine(
31+
services =>
32+
{
33+
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
34+
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
35+
});
36+
37+
_auditHistoryEngine = DiagnosticsBenchmarkScenario.BuildEngine();
38+
39+
_combinedDiagnosticsEngine = DiagnosticsBenchmarkScenario.BuildEngine(
40+
configureEngine: engine =>
41+
{
42+
engine.RegisterInterceptor(new PassiveBenchmarkInterceptor());
43+
engine.RegisterInterceptor(new FormattingLoggingInterceptor());
44+
});
45+
46+
_state = new DiagnosticsState(42, "baseline");
47+
_mutation = DiagnosticsBenchmarkScenario.CreateCommitMutation("diagnostics");
48+
}
49+
50+
/// <summary>
51+
/// Measures commit execution with observability paths disabled via no-op audit and history services.
52+
/// </summary>
53+
[Benchmark(Baseline = true)]
54+
public async Task NoDiagnostics_Baseline()
55+
{
56+
var result = await _noDiagnosticsEngine.ExecuteAsync(_mutation, _state);
57+
GC.KeepAlive(result);
58+
}
59+
60+
/// <summary>
61+
/// Measures commit execution with the default audit and history capture path enabled.
62+
/// </summary>
63+
[Benchmark]
64+
public async Task AuditHistory_Enabled()
65+
{
66+
var result = await _auditHistoryEngine.ExecuteAsync(_mutation, _state);
67+
GC.KeepAlive(result);
68+
}
69+
70+
/// <summary>
71+
/// Measures commit execution with audit/history capture plus interceptor and logging-style formatting enabled.
72+
/// </summary>
73+
[Benchmark]
74+
public async Task CombinedInterceptionAndDiagnostics_Enabled()
75+
{
76+
var result = await _combinedDiagnosticsEngine.ExecuteAsync(_mutation, _state);
77+
GC.KeepAlive(result);
78+
}
79+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using BenchmarkDotNet.Attributes;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using ModularityKit.Mutator.Abstractions.Audit;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.History;
6+
using ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
7+
8+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics;
9+
10+
/// <summary>
11+
/// Benchmarks interceptor overhead in the core mutation pipeline independently from audit and history storage.
12+
/// </summary>
13+
[BenchmarkCategory("Diagnostics")]
14+
[MemoryDiagnoser]
15+
[InProcess]
16+
public class InterceptorOverheadBenchmarks
17+
{
18+
private IMutationEngine _baselineEngine = null!;
19+
private IMutationEngine _interceptorEngine = null!;
20+
private DiagnosticsState _state = null!;
21+
private DiagnosticsMutation _mutation = null!;
22+
23+
/// <summary>
24+
/// Prepares engines with and without a passive interceptor while disabling audit and history storage noise.
25+
/// </summary>
26+
[GlobalSetup]
27+
public void Setup()
28+
{
29+
_baselineEngine = DiagnosticsBenchmarkScenario.BuildEngine(
30+
services =>
31+
{
32+
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
33+
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
34+
});
35+
36+
_interceptorEngine = DiagnosticsBenchmarkScenario.BuildEngine(
37+
services =>
38+
{
39+
services.AddSingleton<IMutationAuditor, NoOpAuditor>();
40+
services.AddSingleton<IMutationHistoryStore, NoOpHistoryStore>();
41+
},
42+
engine => engine.RegisterInterceptor(new PassiveBenchmarkInterceptor()));
43+
44+
_state = new DiagnosticsState(42, "baseline");
45+
_mutation = DiagnosticsBenchmarkScenario.CreateCommitMutation("interceptor");
46+
}
47+
48+
/// <summary>
49+
/// Measures the commit pipeline without interceptors, audit persistence, or history persistence.
50+
/// </summary>
51+
[Benchmark(Baseline = true)]
52+
public async Task NoInterceptor_Baseline()
53+
{
54+
var result = await _baselineEngine.ExecuteAsync(_mutation, _state);
55+
GC.KeepAlive(result);
56+
}
57+
58+
/// <summary>
59+
/// Measures the same commit pipeline with one passive interceptor enabled.
60+
/// </summary>
61+
[Benchmark]
62+
public async Task PassiveInterceptor_Enabled()
63+
{
64+
var result = await _interceptorEngine.ExecuteAsync(_mutation, _state);
65+
GC.KeepAlive(result);
66+
}
67+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
using ModularityKit.Mutator.Abstractions;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Runtime;
6+
7+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
8+
9+
/// <summary>
10+
/// Creates repeatable diagnostics benchmark scenarios and engine instances.
11+
/// </summary>
12+
internal static class DiagnosticsBenchmarkScenario
13+
{
14+
/// <summary>
15+
/// Gets the shared state identifier used by diagnostics benchmark cases.
16+
/// </summary>
17+
public const string StateId = "diagnostics-benchmark-state";
18+
19+
/// <summary>
20+
/// Builds performance oriented mutation engine for diagnostics benchmark scenarios.
21+
/// </summary>
22+
/// <param name="configureServices">Optional service registrations overriding default runtime services.</param>
23+
/// <param name="configureEngine">Optional engine configuration executed after engine resolution.</param>
24+
/// <returns>A configured mutation engine instance.</returns>
25+
public static IMutationEngine BuildEngine(
26+
Action<IServiceCollection>? configureServices = null,
27+
Action<IMutationEngine>? configureEngine = null)
28+
{
29+
var services = new ServiceCollection();
30+
services.AddMutators(MutationEngineOptions.Performance);
31+
32+
configureServices?.Invoke(services);
33+
34+
var engine = services
35+
.BuildServiceProvider()
36+
.GetRequiredService<IMutationEngine>();
37+
38+
configureEngine?.Invoke(engine);
39+
return engine;
40+
}
41+
42+
/// <summary>
43+
/// Creates a minimal commit mutation instance bound to the shared diagnostics benchmark state.
44+
/// </summary>
45+
/// <param name="operationSuffix">The operation suffix used to build a stable correlation identifier.</param>
46+
/// <returns>A diagnostics mutation configured for commit execution.</returns>
47+
public static DiagnosticsMutation CreateCommitMutation(string operationSuffix)
48+
{
49+
var context = MutationContext.System("diagnostics-benchmark") with
50+
{
51+
StateId = StateId,
52+
Mode = MutationMode.Commit,
53+
CorrelationId = $"{StateId}:{operationSuffix}"
54+
};
55+
56+
return new DiagnosticsMutation(context);
57+
}
58+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using ModularityKit.Mutator.Abstractions;
2+
using ModularityKit.Mutator.Abstractions.Changes;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Engine;
5+
using ModularityKit.Mutator.Abstractions.Intent;
6+
using ModularityKit.Mutator.Abstractions.Results;
7+
8+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
9+
10+
/// <summary>
11+
/// Minimal commit mutation used to measure diagnostics and interception overhead.
12+
/// </summary>
13+
internal sealed class DiagnosticsMutation(MutationContext context) : IMutation<DiagnosticsState>
14+
{
15+
/// <summary>
16+
/// Gets the benchmark mutation intent metadata.
17+
/// </summary>
18+
public MutationIntent Intent { get; } = new()
19+
{
20+
OperationName = "DiagnosticsMutation",
21+
Category = "Benchmark",
22+
Description = "Minimal commit mutation used to measure interception and diagnostics overhead.",
23+
RiskLevel = MutationRiskLevel.Low,
24+
IsReversible = true
25+
};
26+
27+
/// <summary>
28+
/// Gets the execution context bound to the benchmark mutation instance.
29+
/// </summary>
30+
public MutationContext Context { get; } = context;
31+
32+
/// <summary>
33+
/// Applies the benchmark mutation to the provided state.
34+
/// </summary>
35+
/// <param name="state">The input state.</param>
36+
/// <returns>The successful mutation result containing the updated state and changes.</returns>
37+
public MutationResult<DiagnosticsState> Apply(DiagnosticsState state)
38+
{
39+
var nextState = state with
40+
{
41+
Counter = state.Counter + 1,
42+
LastOperation = Context.CorrelationId ?? string.Empty
43+
};
44+
45+
return MutationResult<DiagnosticsState>.Success(
46+
nextState,
47+
ChangeSet.FromChanges(
48+
StateChange.Modified(nameof(DiagnosticsState.Counter), state.Counter, nextState.Counter),
49+
StateChange.Modified(nameof(DiagnosticsState.LastOperation), state.LastOperation, nextState.LastOperation)
50+
));
51+
}
52+
53+
/// <summary>
54+
/// Validates the provided state before mutation execution.
55+
/// </summary>
56+
/// <param name="state">The input state.</param>
57+
/// <returns>A successful validation result.</returns>
58+
public ValidationResult Validate(DiagnosticsState state) => ValidationResult.Success();
59+
60+
/// <summary>
61+
/// Simulates the benchmark mutation using the same state transition as commit execution.
62+
/// </summary>
63+
/// <param name="state">The input state.</param>
64+
/// <returns>The simulated mutation result.</returns>
65+
public MutationResult<DiagnosticsState> Simulate(DiagnosticsState state) => Apply(state);
66+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
2+
3+
/// <summary>
4+
/// Minimal state used by diagnostics benchmark scenarios.
5+
/// </summary>
6+
/// <param name="Counter">The mutable numeric field exercised by the benchmark mutation.</param>
7+
/// <param name="LastOperation">The last logical operation label written by the mutation.</param>
8+
public sealed record DiagnosticsState(int Counter, string LastOperation);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using ModularityKit.Mutator.Abstractions.Changes;
2+
using ModularityKit.Mutator.Abstractions.Context;
3+
using ModularityKit.Mutator.Abstractions.Intent;
4+
using ModularityKit.Mutator.Runtime.Interception;
5+
6+
namespace ModularityKit.Mutator.Benchmarks.Diagnostics.Support;
7+
8+
/// <summary>
9+
/// Logging style interceptor that formats lifecycle messages and writes them to a sink.
10+
/// </summary>
11+
internal sealed class FormattingLoggingInterceptor : MutationInterceptorBase
12+
{
13+
private readonly TextWriter _sink = TextWriter.Null;
14+
15+
/// <summary>
16+
/// Gets the interceptor name used in benchmark scenarios.
17+
/// </summary>
18+
public override string Name => nameof(FormattingLoggingInterceptor);
19+
20+
/// <summary>
21+
/// Formats and writes pre mutation log line to the configured sink.
22+
/// </summary>
23+
/// <param name="intent">The mutation intent.</param>
24+
/// <param name="context">The mutation context.</param>
25+
/// <param name="state">The current state.</param>
26+
/// <param name="executionId">The execution identifier.</param>
27+
/// <param name="cancellationToken">The cancellation token.</param>
28+
/// <returns>A completed task.</returns>
29+
public override Task OnBeforeMutationAsync(
30+
MutationIntent intent,
31+
MutationContext context,
32+
object state,
33+
string executionId,
34+
CancellationToken cancellationToken = default)
35+
{
36+
_sink.WriteLine($"[Before] {intent.OperationName} by {context.ActorId} (ExecutionId: {executionId})");
37+
return Task.CompletedTask;
38+
}
39+
40+
/// <summary>
41+
/// Formats and writes post mutation log lines, including the generated change set, to the configured sink.
42+
/// </summary>
43+
/// <param name="intent">The mutation intent.</param>
44+
/// <param name="context">The mutation context.</param>
45+
/// <param name="oldState">The state before mutation execution.</param>
46+
/// <param name="newState">The state after mutation execution.</param>
47+
/// <param name="changes">The generated change set.</param>
48+
/// <param name="executionId">The execution identifier.</param>
49+
/// <param name="cancellationToken">The cancellation token.</param>
50+
/// <returns>A completed task.</returns>
51+
public override Task OnAfterMutationAsync(
52+
MutationIntent intent,
53+
MutationContext context,
54+
object? oldState,
55+
object? newState,
56+
ChangeSet changes,
57+
string executionId,
58+
CancellationToken cancellationToken = default)
59+
{
60+
_sink.WriteLine($"[After] {intent.OperationName}, changes: {changes.Changes.Count} (ExecutionId: {executionId})");
61+
62+
foreach (var change in changes.Changes)
63+
_sink.WriteLine($" - {change.Path}: {change.OldValue} -> {change.NewValue}");
64+
65+
return Task.CompletedTask;
66+
}
67+
}

0 commit comments

Comments
 (0)