Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
name: Publish Docs

env:
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true

on:
push:
branches: [ main ]
Expand Down Expand Up @@ -49,9 +52,15 @@ jobs:
needs: build
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
url: ${{ steps.deployment_retry.outputs.page_url || steps.deployment.outputs.page_url }}

steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
continue-on-error: true

- name: Retry GitHub Pages deployment
id: deployment_retry
if: steps.deployment.outcome == 'failure'
uses: actions/deploy-pages@v4
7 changes: 7 additions & 0 deletions Benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ This folder contains BenchmarkDotNet measurements for `ModularityKit.Mutator`.
- policy evaluation overhead for no policy, synchronous policy, asynchronous policy, and mixed multi policy runs
- interception, audit/history, and logging diagnostics overhead in the core runtime
- parallel execution, state gate contention, and concurrent batch scheduling pressure in the core runtime
- mutation result creation and history/audit output materialization in the core runtime

The throughput benchmarks use cloned array backed state so state size effects remain visible in the
actual mutation path rather than being hidden behind an artificial inner loop.
Expand Down Expand Up @@ -56,6 +57,12 @@ Run the concurrency suite:
dotnet Benchmarks/bin/Release/net10.0/ModularityKit.Mutator.Benchmarks.dll --anyCategories Concurrency
```

Run the results suite:

```bash
dotnet Benchmarks/bin/Release/net10.0/ModularityKit.Mutator.Benchmarks.dll --anyCategories Results
```

Key parameters reported by BenchmarkDotNet:

- `StateSize` controls the size of the cloned mutation state
Expand Down
77 changes: 77 additions & 0 deletions Benchmarks/Results/MutationOutputMaterializationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using BenchmarkDotNet.Attributes;
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.History;
using ModularityKit.Mutator.Abstractions.Results;
using ModularityKit.Mutator.Benchmarks.Results.Support;

namespace ModularityKit.Mutator.Benchmarks.Results;

/// <summary>
/// Benchmarks materialization of history and audit output from an executed mutation result.
/// </summary>
[BenchmarkCategory("Results")]
[MemoryDiagnoser]
[InProcess]
public class MutationOutputMaterializationBenchmarks
{
private MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> _result = null!;
private string _executionId = string.Empty;
private TimeSpan _duration;

/// <summary>
/// Prepares a representative executed mutation result for output materialization benchmarks.
/// </summary>
[GlobalSetup]
public void Setup()
{
_result = ResultsBenchmarkSupport.CreateExecutedResult(sideEffectCount: 3, changeCount: 4);
_executionId = "results-benchmark-execution";
_duration = TimeSpan.FromMilliseconds(2);
}

/// <summary>
/// Measures materialization of the mutation history entry, including change and side effect copying.
/// </summary>
[Benchmark(Baseline = true)]
public MutationHistoryEntry HistoryEntry_Materialization()
{
return new MutationHistoryEntry
{
ExecutionId = _executionId,
StateId = ResultsBenchmarkSupport.StateId,
Intent = ResultsBenchmarkSupport.CreateIntent(
"ResultHistoryMaterialization",
"Materialize history output for benchmark results."),
Context = ResultsBenchmarkSupport.CreateContext("history"),
Changes = _result.Changes,
SideEffects = _result.SideEffects.ToList(),
Timestamp = DateTimeOffset.UtcNow,
ExecutionTime = _duration
};
}

/// <summary>
/// Measures materialization of the audit entry produced from the same executed mutation result.
/// </summary>
[Benchmark]
public MutationAuditEntry AuditEntry_Materialization()
{
return new MutationAuditEntry
{
ExecutionId = _executionId,
StateId = ResultsBenchmarkSupport.StateId,
StateType = nameof(ResultsBenchmarkSupport.ResultBenchmarkState),
MutationIntent = ResultsBenchmarkSupport.CreateIntent(
"ResultAuditMaterialization",
"Materialize audit output for benchmark results."),
Context = ResultsBenchmarkSupport.CreateContext("audit"),
Changes = _result.Changes,
IsSuccess = _result.IsSuccess,
ErrorMessage = null,
PolicyDecisions = _result.PolicyDecisions,
SideEffects = _result.SideEffects,
Timestamp = DateTimeOffset.UtcNow,
Duration = _duration
};
}
}
78 changes: 78 additions & 0 deletions Benchmarks/Results/MutationResultCreationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using BenchmarkDotNet.Attributes;
using ModularityKit.Mutator.Abstractions.Changes;
using ModularityKit.Mutator.Abstractions.Effects;
using ModularityKit.Mutator.Abstractions.Results;
using ModularityKit.Mutator.Benchmarks.Results.Support;

namespace ModularityKit.Mutator.Benchmarks.Results;

/// <summary>
/// Benchmarks the cost of creating mutation results with and without side effects.
/// </summary>
[BenchmarkCategory("Results")]
[MemoryDiagnoser]
[InProcess]
public class MutationResultCreationBenchmarks
{
private ResultsBenchmarkSupport.ResultBenchmarkState _state = null!;
private ChangeSet _changes = null!;

/// <summary>
/// Prepares the shared state and change set used by the result creation cases.
/// </summary>
[GlobalSetup]
public void Setup()
{
_state = new ResultsBenchmarkSupport.ResultBenchmarkState(0, 42);
_changes = ResultsBenchmarkSupport.CreateChangeSet(_state.Revision, 2);
}

/// <summary>
/// Measures creation of a successful mutation result with no side effects.
/// </summary>
[Benchmark(Baseline = true)]
public MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> Success_NoSideEffects()
=> MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
_state with
{
Revision = _state.Revision + 1,
Value = _state.Value + 1
},
_changes);

/// <summary>
/// Measures creation of a successful mutation result with one side effect.
/// </summary>
[Benchmark]
public MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> Success_SingleSideEffect()
{
var sideEffect = SideEffect.Create(
"ResultMaterialization",
"Single side effect",
new ResultsBenchmarkSupport.SideEffectPayload(1, "single"),
SideEffectSeverity.Info);

return MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
_state with
{
Revision = _state.Revision + 1,
Value = _state.Value + 1
},
_changes,
[sideEffect]);
}

/// <summary>
/// Measures creation of a successful mutation result with several side effects.
/// </summary>
[Benchmark]
public MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> Success_MultipleSideEffects()
=> MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
_state with
{
Revision = _state.Revision + 1,
Value = _state.Value + 1
},
_changes,
ResultsBenchmarkSupport.CreateSideEffects(4));
}
130 changes: 130 additions & 0 deletions Benchmarks/Results/Support/ResultsBenchmarkSupport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using ModularityKit.Mutator.Abstractions.Audit;
using ModularityKit.Mutator.Abstractions.Changes;
using ModularityKit.Mutator.Abstractions.Context;
using ModularityKit.Mutator.Abstractions.Effects;
using ModularityKit.Mutator.Abstractions.Engine;
using ModularityKit.Mutator.Abstractions.Intent;
using ModularityKit.Mutator.Abstractions.Results;

namespace ModularityKit.Mutator.Benchmarks.Results.Support;

/// <summary>
/// Shared support types for result materialization benchmark scenarios.
/// </summary>
public static class ResultsBenchmarkSupport
{
/// <summary>
/// Gets the shared state identifier used by result benchmark cases.
/// </summary>
public const string StateId = "results-benchmark-state";

/// <summary>
/// Creates a reusable mutation context for result benchmarks.
/// </summary>
/// <param name="operationSuffix">The suffix used to distinguish benchmark cases.</param>
/// <returns>A system mutation context bound to the shared benchmark state.</returns>
public static MutationContext CreateContext(string operationSuffix)
{
return MutationContext.System("results-benchmark")
with
{
StateId = StateId,
Mode = MutationMode.Commit,
CorrelationId = $"{StateId}:{operationSuffix}"
};
}

/// <summary>
/// Creates a reusable mutation intent for result benchmarks.
/// </summary>
/// <param name="operationName">The operation name reported by the mutation.</param>
/// <param name="description">The human-readable description.</param>
/// <returns>A benchmark mutation intent.</returns>
public static MutationIntent CreateIntent(string operationName, string description)
=> new()
{
OperationName = operationName,
Category = "Benchmark",
Description = description,
RiskLevel = MutationRiskLevel.Low,
IsReversible = true
};

/// <summary>
/// Creates a change set with a stable revision marker and a configurable number of appended slot updates.
/// </summary>
/// <param name="revision">The revision number before mutation.</param>
/// <param name="updates">The number of slot updates to include.</param>
/// <returns>A populated change set.</returns>
public static ChangeSet CreateChangeSet(int revision, int updates)
{
var changes = new List<StateChange>(updates + 1)
{
StateChange.Modified(nameof(ResultBenchmarkState.Revision), revision, revision + 1)
};

for (var index = 0; index < updates; index++)
{
changes.Add(StateChange.Modified(
$"Slots[{index}]",
index,
index + 1));
}

return ChangeSet.FromChanges([.. changes]);
}

/// <summary>
/// Creates a fixed list of side effects with predictable payload shape.
/// </summary>
/// <param name="count">The number of side effects to create.</param>
/// <returns>A read-only side effect list.</returns>
public static IReadOnlyList<SideEffect> CreateSideEffects(int count)
{
var sideEffects = new List<SideEffect>(count);

for (var index = 0; index < count; index++)
{
sideEffects.Add(SideEffect.Create(
"ResultMaterialization",
$"Side effect #{index}",
new SideEffectPayload(index, $"payload-{index}"),
SideEffectSeverity.Info));
}

return sideEffects;
}

/// <summary>
/// Creates a reusable mutation result used as the input for output materialization benchmarks.
/// </summary>
/// <param name="sideEffectCount">The number of side effects to attach to the result.</param>
/// <param name="changeCount">The number of slot changes to include in the result.</param>
/// <returns>A mutation result prepopulated with changes and side effects.</returns>
public static MutationResult<ResultBenchmarkState> CreateExecutedResult(
int sideEffectCount,
int changeCount)
{
var state = new ResultBenchmarkState(42, 0);
var nextState = state with { Revision = state.Revision + 1 };

return MutationResult<ResultBenchmarkState>.Success(
nextState,
CreateChangeSet(state.Revision, changeCount),
CreateSideEffects(sideEffectCount));
}

/// <summary>
/// Minimal state used by result benchmark scenarios.
/// </summary>
/// <param name="Revision">The revision counter advanced on each benchmark mutation.</param>
/// <param name="Value">The mutable numeric value exercised by the benchmark mutation.</param>
public sealed record ResultBenchmarkState(int Revision, int Value);

/// <summary>
/// Typed payload used to give side effects realistic materialization shape.
/// </summary>
/// <param name="Index">The ordinal of the side effect.</param>
/// <param name="Token">A stable payload token.</param>
public sealed record SideEffectPayload(int Index, string Token);
}
Loading