Skip to content

Commit 8e6f388

Browse files
committed
perf: reduce result creation and materialization overhead
1 parent 2127863 commit 8e6f388

10 files changed

Lines changed: 148 additions & 107 deletions

File tree

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.

src/Abstractions/History/MutationHistoryEntry.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,39 @@ namespace ModularityKit.Mutator.Abstractions.History;
1111
/// <remarks>
1212
/// Each <see cref="MutationHistoryEntry"/> captures the details of a single mutation,
1313
/// including its intent, context, state changes, side effects, execution timing, and integrity hashes.
14-
/// This class is used by <see cref="MutationHistory"/> to store a chronological sequence of mutations.
14+
/// This type is used by <see cref="MutationHistory"/> to store a chronological sequence of mutations.
1515
/// </remarks>
16-
public sealed class MutationHistoryEntry
16+
public readonly record struct MutationHistoryEntry
1717
{
1818
/// <summary>
1919
/// Unique identifier for the execution of this mutation.
2020
/// </summary>
21-
public string ExecutionId { get; init; } = string.Empty;
21+
public string ExecutionId { get; init; }
2222

2323
/// <summary>
2424
/// Identifier of the state this mutation was applied to.
2525
/// </summary>
26-
public string StateId { get; init; } = string.Empty;
26+
public string StateId { get; init; }
2727

2828
/// <summary>
2929
/// The intent behind the mutation.
3030
/// </summary>
31-
public MutationIntent Intent { get; init; } = null!;
31+
public MutationIntent Intent { get; init; }
3232

3333
/// <summary>
3434
/// Contextual information about the mutation execution.
3535
/// </summary>
36-
public MutationContext Context { get; init; } = null!;
36+
public MutationContext Context { get; init; }
3737

3838
/// <summary>
3939
/// Set of changes applied by this mutation.
4040
/// </summary>
41-
public ChangeSet Changes { get; init; } = ChangeSet.Empty;
41+
public ChangeSet Changes { get; init; }
4242

4343
/// <summary>
4444
/// Side effects produced by this mutation.
4545
/// </summary>
46-
public IReadOnlyList<SideEffect> SideEffects { get; init; } = [];
46+
public IReadOnlyList<SideEffect> SideEffects { get; init; }
4747

4848
/// <summary>
4949
/// Timestamp indicating when the mutation occurred.

src/Abstractions/Metrics/MutationMetrics.cs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace ModularityKit.Mutator.Abstractions.Metrics;
1212
/// <para>
1313
/// Key considerations:
1414
/// <list type="bullet">
15+
/// <item><see cref="RecordedAt"/> timestamp of the mutation recording.</item>
1516
/// <item><see cref="ExecutionTime"/> measures the total duration of the mutation.</item>
1617
/// <item><see cref="ValidationTime"/> measures time spent validating the mutation.</item>
1718
/// <item><see cref="PolicyEvaluationTime"/> measures time spent evaluating policies.</item>
@@ -25,6 +26,11 @@ namespace ModularityKit.Mutator.Abstractions.Metrics;
2526
/// </remarks>
2627
public sealed record MutationMetrics
2728
{
29+
private static readonly IReadOnlyDictionary<string, object> _emptyAdditionalMetrics
30+
= new Dictionary<string, object>();
31+
32+
internal static readonly MutationMetrics Empty = new();
33+
2834
/// <summary>
2935
/// Timestamp when the mutation was recorded.
3036
/// </summary>
@@ -46,12 +52,12 @@ public sealed record MutationMetrics
4652
public TimeSpan PolicyEvaluationTime { get; init; }
4753

4854
/// <summary>
49-
/// Number of rules validated during the mutation.
55+
/// Number of validated rules.
5056
/// </summary>
5157
public int ValidatedRules { get; init; }
5258

5359
/// <summary>
54-
/// Number of policies evaluated during the mutation.
60+
/// Number of evaluated policies.
5561
/// </summary>
5662
public int EvaluatedPolicies { get; init; }
5763

@@ -61,23 +67,23 @@ public sealed record MutationMetrics
6167
public int ChangesCount { get; init; }
6268

6369
/// <summary>
64-
/// Size of the state before the mutation (in bytes, if applicable).
70+
/// Size of the state object in bytes, if available.
6571
/// </summary>
6672
public long? StateSize { get; init; }
6773

6874
/// <summary>
69-
/// Memory used during mutation execution (in bytes).
75+
/// Memory used during mutation execution in bytes, if available.
7076
/// </summary>
7177
public long? MemoryUsed { get; init; }
7278

7379
/// <summary>
74-
/// Indicates whether a cache was used.
80+
/// Indicates whether caching was used during the mutation.
7581
/// </summary>
7682
public bool UsedCache { get; init; }
7783

7884
/// <summary>
79-
/// Additional custom metrics.
85+
/// Additional metrics for extensibility.
8086
/// </summary>
8187
public IReadOnlyDictionary<string, object> AdditionalMetrics { get; init; }
82-
= new Dictionary<string, object>();
88+
= _emptyAdditionalMetrics;
8389
}

0 commit comments

Comments
 (0)