Skip to content

Commit f475421

Browse files
authored
Perf: Eliminate all heap allocations in mutation result creation and materialization (#96)
2 parents f9e60bf + 8e6f388 commit f475421

15 files changed

Lines changed: 189 additions & 150 deletions

File tree

Benchmarks/Governance/Materialization/GovernanceMaterializationOutputBenchmarks.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public MutationHistoryEntry HistoryEntry_FromGovernedExecution()
3737
StateId = _fixture.Result.Request.StateId,
3838
Intent = _fixture.Mutation.Intent,
3939
Context = _fixture.Mutation.Context,
40-
Changes = _fixture.Result.MutationResult!.Changes,
40+
Changes = _fixture.Result.MutationResult!.Value.Changes,
4141
SideEffects = _fixture.Result.Request.SideEffects.ToList(),
4242
Timestamp = _fixture.Result.Request.Versioning.ExecutedAt ?? DateTimeOffset.UtcNow,
4343
ExecutionTime = TimeSpan.FromMilliseconds(2)
@@ -50,17 +50,18 @@ public MutationHistoryEntry HistoryEntry_FromGovernedExecution()
5050
[Benchmark]
5151
public MutationAuditEntry AuditEntry_FromGovernedExecution()
5252
{
53+
var mr = _fixture.Result.MutationResult!.Value;
5354
return new MutationAuditEntry
5455
{
5556
ExecutionId = _fixture.Result.Request.RequestId,
5657
StateId = _fixture.Result.Request.StateId,
5758
StateType = _fixture.Result.Request.StateType,
5859
MutationIntent = _fixture.Mutation.Intent,
5960
Context = _fixture.Mutation.Context,
60-
Changes = _fixture.Result.MutationResult!.Changes,
61-
IsSuccess = _fixture.Result.MutationResult.IsSuccess,
61+
Changes = mr.Changes,
62+
IsSuccess = mr.IsSuccess,
6263
ErrorMessage = null,
63-
PolicyDecisions = _fixture.Result.MutationResult.PolicyDecisions,
64+
PolicyDecisions = mr.PolicyDecisions,
6465
SideEffects = _fixture.Result.Request.SideEffects.ToList(),
6566
Timestamp = _fixture.Result.Request.Versioning.ExecutedAt ?? DateTimeOffset.UtcNow,
6667
Duration = TimeSpan.FromMilliseconds(2),

Benchmarks/Results/MutationOutputMaterializationBenchmarks.cs

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,71 @@
11
using BenchmarkDotNet.Attributes;
22
using ModularityKit.Mutator.Abstractions.Audit;
3+
using ModularityKit.Mutator.Abstractions.Context;
4+
using ModularityKit.Mutator.Abstractions.Effects;
35
using ModularityKit.Mutator.Abstractions.History;
6+
using ModularityKit.Mutator.Abstractions.Intent;
47
using ModularityKit.Mutator.Abstractions.Results;
58
using ModularityKit.Mutator.Benchmarks.Results.Support;
69

710
namespace ModularityKit.Mutator.Benchmarks.Results;
811

9-
/// <summary>
10-
/// Benchmarks materialization of history and audit output from an executed mutation result.
11-
/// </summary>
1212
[BenchmarkCategory("Results")]
1313
[MemoryDiagnoser]
1414
[InProcess]
1515
public class MutationOutputMaterializationBenchmarks
1616
{
17-
private MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> _result = null!;
17+
private MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> _result = default!;
1818
private string _executionId = string.Empty;
1919
private TimeSpan _duration;
20+
private IReadOnlyList<SideEffect> _sideEffectList = null!;
21+
private MutationIntent _historyIntent = null!;
22+
private MutationIntent _auditIntent = null!;
23+
private MutationContext _historyContext = null!;
24+
private MutationContext _auditContext = null!;
2025

21-
/// <summary>
22-
/// Prepares a representative executed mutation result for output materialization benchmarks.
23-
/// </summary>
2426
[GlobalSetup]
2527
public void Setup()
2628
{
2729
_result = ResultsBenchmarkSupport.CreateExecutedResult(sideEffectCount: 3, changeCount: 4);
2830
_executionId = "results-benchmark-execution";
2931
_duration = TimeSpan.FromMilliseconds(2);
32+
_sideEffectList = _result.SideEffects.ToList();
33+
_historyIntent = ResultsBenchmarkSupport.CreateIntent(
34+
"ResultHistoryMaterialization",
35+
"Materialize history output for benchmark results.");
36+
_auditIntent = ResultsBenchmarkSupport.CreateIntent(
37+
"ResultAuditMaterialization",
38+
"Materialize audit output for benchmark results.");
39+
_historyContext = ResultsBenchmarkSupport.CreateContext("history");
40+
_auditContext = ResultsBenchmarkSupport.CreateContext("audit");
3041
}
3142

32-
/// <summary>
33-
/// Measures materialization of the mutation history entry, including change and side effect copying.
34-
/// </summary>
3543
[Benchmark(Baseline = true)]
3644
public MutationHistoryEntry HistoryEntry_Materialization()
3745
{
3846
return new MutationHistoryEntry
3947
{
4048
ExecutionId = _executionId,
4149
StateId = ResultsBenchmarkSupport.StateId,
42-
Intent = ResultsBenchmarkSupport.CreateIntent(
43-
"ResultHistoryMaterialization",
44-
"Materialize history output for benchmark results."),
45-
Context = ResultsBenchmarkSupport.CreateContext("history"),
50+
Intent = _historyIntent,
51+
Context = _historyContext,
4652
Changes = _result.Changes,
47-
SideEffects = _result.SideEffects.ToList(),
53+
SideEffects = _sideEffectList,
4854
Timestamp = DateTimeOffset.UtcNow,
4955
ExecutionTime = _duration
5056
};
5157
}
5258

53-
/// <summary>
54-
/// Measures materialization of the audit entry produced from the same executed mutation result.
55-
/// </summary>
5659
[Benchmark]
5760
public MutationAuditEntry AuditEntry_Materialization()
5861
{
5962
return new MutationAuditEntry
6063
{
6164
ExecutionId = _executionId,
6265
StateId = ResultsBenchmarkSupport.StateId,
63-
StateType = nameof(ResultsBenchmarkSupport.ResultBenchmarkState),
64-
MutationIntent = ResultsBenchmarkSupport.CreateIntent(
65-
"ResultAuditMaterialization",
66-
"Materialize audit output for benchmark results."),
67-
Context = ResultsBenchmarkSupport.CreateContext("audit"),
66+
StateType = "ResultBenchmarkState",
67+
MutationIntent = _auditIntent,
68+
Context = _auditContext,
6869
Changes = _result.Changes,
6970
IsSuccess = _result.IsSuccess,
7071
ErrorMessage = null,

Benchmarks/Results/MutationResultCreationBenchmarks.cs

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,21 @@ namespace ModularityKit.Mutator.Benchmarks.Results;
1414
[InProcess]
1515
public class MutationResultCreationBenchmarks
1616
{
17-
private ResultsBenchmarkSupport.ResultBenchmarkState _state = null!;
17+
private ResultsBenchmarkSupport.ResultBenchmarkState _state = default!;
1818
private ChangeSet _changes = null!;
19+
private IReadOnlyList<SideEffect> _singleSideEffect = null!;
20+
private IReadOnlyList<SideEffect> _multipleSideEffects = null!;
1921

2022
/// <summary>
21-
/// Prepares the shared state and change set used by the result creation cases.
23+
/// Prepares the shared state, change set, and side effect lists used by the result creation cases.
2224
/// </summary>
2325
[GlobalSetup]
2426
public void Setup()
2527
{
2628
_state = new ResultsBenchmarkSupport.ResultBenchmarkState(0, 42);
2729
_changes = ResultsBenchmarkSupport.CreateChangeSet(_state.Revision, 2);
30+
_singleSideEffect = ResultsBenchmarkSupport.CreateSideEffects(1);
31+
_multipleSideEffects = ResultsBenchmarkSupport.CreateSideEffects(4);
2832
}
2933

3034
/// <summary>
@@ -45,22 +49,14 @@ _state with
4549
/// </summary>
4650
[Benchmark]
4751
public MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState> Success_SingleSideEffect()
48-
{
49-
var sideEffect = SideEffect.Create(
50-
"ResultMaterialization",
51-
"Single side effect",
52-
new ResultsBenchmarkSupport.SideEffectPayload(1, "single"),
53-
SideEffectSeverity.Info);
54-
55-
return MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
52+
=> MutationResult<ResultsBenchmarkSupport.ResultBenchmarkState>.Success(
5653
_state with
5754
{
5855
Revision = _state.Revision + 1,
5956
Value = _state.Value + 1
6057
},
6158
_changes,
62-
[sideEffect]);
63-
}
59+
_singleSideEffect);
6460

6561
/// <summary>
6662
/// Measures creation of a successful mutation result with several side effects.
@@ -74,5 +70,5 @@ _state with
7470
Value = _state.Value + 1
7571
},
7672
_changes,
77-
ResultsBenchmarkSupport.CreateSideEffects(4));
73+
_multipleSideEffects);
7874
}

Benchmarks/Results/Support/ResultsBenchmarkSupport.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,12 @@ public static MutationResult<ResultBenchmarkState> CreateExecutedResult(
119119
/// </summary>
120120
/// <param name="Revision">The revision counter advanced on each benchmark mutation.</param>
121121
/// <param name="Value">The mutable numeric value exercised by the benchmark mutation.</param>
122-
public sealed record ResultBenchmarkState(int Revision, int Value);
122+
public readonly record struct ResultBenchmarkState(int Revision, int Value);
123123

124124
/// <summary>
125125
/// Typed payload used to give side effects realistic materialization shape.
126126
/// </summary>
127127
/// <param name="Index">The ordinal of the side effect.</param>
128128
/// <param name="Token">A stable payload token.</param>
129-
public sealed record SideEffectPayload(int Index, string Token);
129+
public readonly record struct SideEffectPayload(int Index, string Token);
130130
}

Tests/ModularityKit.Mutator.Governance.Tests/Execution/GovernanceExecutionManagerCompensationTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public async Task ExecuteApproved_executes_operator_rollback_compensation_and_li
7373
var compensationResult = await executionManager.ExecuteApproved(
7474
compensationRequest.RequestId,
7575
compensationMutation,
76-
originalResult.MutationResult!.NewState!,
76+
originalResult.MutationResult!.Value.NewState!,
7777
governanceContext: MutationContext.Service("governance-runtime", "Execute operator rollback"),
7878
strategy: VersionedRequestResolutionStrategy.RejectStale);
7979

src/Abstractions/Audit/MutationAuditEntry.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@
77
namespace ModularityKit.Mutator.Abstractions.Audit;
88

99
/// <summary>
10-
/// Represents a single audit record for a mutation operation.
10+
/// Represents single audit record for a mutation operation.
1111
/// Captures the intent, context, changes, policy decisions, and metadata of the mutation.
1212
/// </summary>
1313
/// <remarks>
1414
/// Mutation audit entries are used for compliance, traceability, debugging, and monitoring.
1515
/// Each entry is immutable once created. Typical consumers include auditors, logging systems,
1616
/// or analytics pipelines.
1717
/// </remarks>
18-
public sealed class MutationAuditEntry
18+
public readonly record struct MutationAuditEntry
1919
{
2020
/// <summary>
2121
/// Unique execution identifier for this mutation.
2222
/// </summary>
23-
public string ExecutionId { get; init; } = string.Empty;
23+
public string ExecutionId { get; init; }
2424

2525
/// <summary>
2626
/// Identifier of the state object that was mutated.
@@ -35,17 +35,17 @@ public sealed class MutationAuditEntry
3535
/// <summary>
3636
/// The intent describing what the mutation is trying to achieve.
3737
/// </summary>
38-
public MutationIntent MutationIntent { get; init; } = null!;
38+
public MutationIntent MutationIntent { get; init; }
3939

4040
/// <summary>
4141
/// Context of the mutation (e.g., correlation data, user context).
4242
/// </summary>
43-
public MutationContext Context { get; init; } = null!;
43+
public MutationContext Context { get; init; }
4444

4545
/// <summary>
4646
/// Changes applied by the mutation.
4747
/// </summary>
48-
public ChangeSet Changes { get; init; } = ChangeSet.Empty;
48+
public ChangeSet Changes { get; init; }
4949

5050
/// <summary>
5151
/// Indicates whether the mutation was successful.
@@ -60,12 +60,12 @@ public sealed class MutationAuditEntry
6060
/// <summary>
6161
/// Decisions made by policies during the mutation evaluation.
6262
/// </summary>
63-
public IReadOnlyList<PolicyDecision> PolicyDecisions { get; init; } = [];
63+
public IReadOnlyList<PolicyDecision> PolicyDecisions { get; init; }
6464

6565
/// <summary>
6666
/// Side effects produced during the mutation.
6767
/// </summary>
68-
public IReadOnlyList<SideEffect> SideEffects { get; init; } = [];
68+
public IReadOnlyList<SideEffect> SideEffects { get; init; }
6969

7070
/// <summary>
7171
/// Timestamp when the mutation started.

src/Abstractions/Changes/ChangeSet.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
namespace ModularityKit.Mutator.Abstractions.Changes;
22

33
/// <summary>
4-
/// Represents a collection of state changes introduced by a mutation.
5-
/// This is a **primary feature** of a mutation and not an optional addition.
4+
/// Represents collection of state changes introduced by a mutation.
5+
/// This is primary feature of mutation and not an optional addition.
66
/// </summary>
77
public sealed class ChangeSet
88
{
9+
private static readonly ChangeSet _empty = new();
910
private readonly List<StateChange> _changes = [];
1011

1112
/// <summary>
@@ -34,7 +35,7 @@ public sealed class ChangeSet
3435
public string? Checksum { get; init; }
3536

3637
/// <summary>
37-
/// Adds a new state change to the changeset.
38+
/// Adds new state change to the changeset.
3839
/// </summary>
3940
/// <param name="change">The state change to add.</param>
4041
/// <exception cref="ArgumentNullException">Thrown if <paramref name="change"/> is null.</exception>
@@ -45,15 +46,15 @@ public void Add(StateChange change)
4546
}
4647

4748
/// <summary>
48-
/// Retrieves all changes corresponding to a specific path.
49+
/// Retrieves all changes corresponding to specific path.
4950
/// </summary>
5051
/// <param name="path">The path to filter changes.</param>
5152
/// <returns>Enumerable of <see cref="StateChange"/> matching the path.</returns>
5253
public IEnumerable<StateChange> GetChanges(string path)
5354
=> _changes.Where(c => c.Path == path);
5455

5556
/// <summary>
56-
/// Determines whether a specific path has been changed.
57+
/// Determines whether specific path has been changed.
5758
/// </summary>
5859
/// <param name="path">The path to check.</param>
5960
/// <returns>True if the path has been changed; otherwise, false.</returns>
@@ -70,7 +71,7 @@ public IEnumerable<string> GetChangedPaths()
7071
/// <summary>
7172
/// Returns an empty changeset.
7273
/// </summary>
73-
public static ChangeSet Empty => new();
74+
public static ChangeSet Empty => _empty;
7475

7576
/// <summary>
7677
/// Creates changeset containing single state change.

src/Abstractions/Effects/SideEffect.cs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Collections.Concurrent;
12
using System.Text.Json.Serialization;
23

34
namespace ModularityKit.Mutator.Abstractions.Effects;
@@ -189,20 +190,33 @@ private static SideEffect CreateCore(
189190
};
190191
}
191192

193+
private static readonly ConcurrentDictionary<Type, (string? ContractType, int? ContractVersion)> _contractCache = new();
194+
192195
private static (string? ContractType, int? ContractVersion) ResolveContract(object? data)
193196
{
194197
if (data is null)
195198
return (null, null);
196199

197200
var dataType = data.GetType();
201+
if (_contractCache.TryGetValue(dataType, out var cached))
202+
return cached;
203+
198204
var contract = dataType.GetCustomAttributes(typeof(SideEffectDataContractAttribute), inherit: false)
199205
.OfType<SideEffectDataContractAttribute>()
200206
.SingleOrDefault();
201207

208+
(string?, int?) result;
202209
if (contract is null)
203-
return (null, null);
210+
{
211+
result = (null, null);
212+
}
213+
else
214+
{
215+
SideEffectDataContractRegistry.Register(dataType);
216+
result = (contract.ContractType, contract.ContractVersion);
217+
}
204218

205-
SideEffectDataContractRegistry.Register(dataType);
206-
return (contract.ContractType, contract.ContractVersion);
219+
_contractCache.TryAdd(dataType, result);
220+
return result;
207221
}
208222
}

src/Abstractions/History/MutationHistory.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
namespace ModularityKit.Mutator.Abstractions.History;
44

55
/// <summary>
6-
/// Represents the full mutation history of a specific state object.
6+
/// Represents the full mutation history of specific state object.
77
/// </summary>
88
/// <remarks>
9-
/// MutationHistory stores a chronological sequence of <see cref="MutationHistoryEntry"/> entries.
9+
/// MutationHistory stores chronological sequence of <see cref="MutationHistoryEntry"/> entries.
1010
/// It allows replaying state changes, querying timelines for specific paths, and computing statistics.
1111
/// This is typically used in combination with <see cref="IMutationHistoryStore"/> to persist and retrieve histories.
1212
/// </remarks>
@@ -32,13 +32,13 @@ public sealed class MutationHistory
3232
/// Timestamp of the first mutation in the history.
3333
/// </summary>
3434
public DateTimeOffset? FirstMutationAt
35-
=> Entries.FirstOrDefault()?.Timestamp;
35+
=> Entries.Count > 0 ? Entries[0].Timestamp : null;
3636

3737
/// <summary>
3838
/// Timestamp of the last mutation in the history.
3939
/// </summary>
4040
public DateTimeOffset? LastMutationAt
41-
=> Entries.LastOrDefault()?.Timestamp;
41+
=> Entries.Count > 0 ? Entries[^1].Timestamp : null;
4242

4343
/// <summary>
4444
/// Total number of mutations recorded.

0 commit comments

Comments
 (0)