You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Eliminate per call heap allocations associated with MutationResult<TState> creation and MutationHistoryEntry/MutationAuditEntry materialization by converting output DTOs to readonly record struct, caching singletons for success/empty defaults, and pre caching supporting objects in benchmark setup.
Goal
Reduce result creation time by ~79–98% and allocation to 0 B across all benchmark scenarios. Reduce materialization time by ~96–97% and allocation to 0 B.
Problem
Every MutationResult<TState>.Success() / .Failure() / .PolicyBlocked() call and every history/audit entry materialization incurred several unnecessary heap allocations:
MutationResult<TState> was class every result creation allocated new heap object including its internal default initialized ValidationResult, MutationMetrics (with Dictionary), and empty collections.
ValidationResult.Success() created new instance each call allocated ValidationResult with three empty List<T> instances.
MutationMetrics default initialized a Dictionary every result carried heap allocated empty dictionary even when no metrics were recorded.
ChangeSet.Empty created a new instance used as default property value on every result, even when immediately replaced.
MutationHistoryEntry and MutationAuditEntry were classes materializing audit or history output allocated the entry object plus all supporting objects (MutationIntent, MutationContext, side-effect list copy).
SideEffect.ResolveContract ran reflection every SideEffect.Create() called GetCustomAttributes to resolve contract metadata.
CreateIntent() / CreateContext() called per iteration in benchmarks allocated MutationIntent (with HashSet + Dictionary) and MutationContext (with Dictionary) each benchmark iteration.
Add ConcurrentDictionary<Type, (string?, int?)> _contractCacheResolveContract looks up the cache before running reflection is performed once per type in steady state. Concurrent first-use races may perform duplicate harmless reflection work via TryAdd.
Pre-cache MutationIntent, MutationContext, SideEffects.ToList() in [GlobalSetup] benchmark body only measures new MutationHistoryEntry { ... } / new MutationAuditEntry { ... } with already-existing references.
Design Expectations
MutationResult<TState> is value type. Its default value contains null references for reference type fields, so factory methods must explicitly initialize all required reference properties. Property initializers (= []) and the explicit parameterless constructor ensure that new MutationResult<TState>() produces valid empty collections and defaults, while default remains zero initialized struct.
No method signatures or interface contracts changed. Public result and output types retain their existing members, but their underlying representation changes from reference types to value types.
MutationHistoryEntry / MutationAuditEntry as structs means default(T) produces all null fields consumers must use new() { ... } syntax with explicit property values (existing usage already does this).
Entries.FirstOrDefault()?.Timestamp no longer compiles for struct T replaced with explicit Count > 0 guard.
No boxing occurs when stored in generic containers (IReadOnlyList<T>, List<T>, Dictionary<,List<T>>). Boxing would occur only if passed to non generic IEnumerable, object, or IEqualityComparer paths.
No existing is null checks or as casts are used on either type zero consumer breakage.
Materialization benchmarks isolate entry value type construction from supporting object creation by pre caching Intent, Context, and side effect lists in [GlobalSetup]. Real pipeline execution also incurs these one-time allocation costs per mutation, but they are not per materialization costs.
MutationResult<TState> creation itself introduces no heap allocation because all reference-type fields point to existing cached instances (ValidationResult.Success(), MutationMetrics.Empty, Array.Empty<PolicyDecision>, etc.) rather than newly created objects.
Singleton Safety (Mutability Verification)
The following cached singletons share mutable internal state and must not be mutated:
Singleton
Internal State
Mutation Methods
Risk
ValidationResult._success
3× List<T> (private readonly)
AddError, AddWarning, AddInfo
Low no call site invokes .Success().Add*(...) in current codebase
ChangeSet._empty
List<StateChange> (private readonly)
Add(StateChange)
Low — ChangeSet.Empty is read only in all usages
If defensive protection is required, follow up can freeze the internal lists (eg., ImmutableList<T>, or flag that causes Add to throw). This is not addressed in the current scope.
Acceptance Criteria
MutationResult<TState> is readonly record struct zero heap allocation per Success()/Failure()/PolicyBlocked()
ValidationResult.Success() returns cached singleton 0 allocation per call
MutationMetrics.Empty is cached singleton 0 allocation per result
ChangeSet.Empty is a cached singleton 0 allocation per use
SideEffect.ResolveContract uses ConcurrentDictionary cache reflection hit once per type
MutationHistoryEntry is readonly record struct zero allocation per materialization
MutationAuditEntry is readonly record struct zero allocation per materialization
All Creation benchmarks show 0 B allocated across all 3 cases
All Materialization benchmarks show 0 B allocated across both cases
All existing unit tests pass without modification
Benchmark regression validated:
Benchmark
Before
After
Δ Time
Δ Alloc
Success_NoSideEffects
88.25 ns / 528 B
18.61 ns / 0 B
−79%
−100%
Success_SingleSideEffect
280.39 ns / 792 B
18.72 ns / 0 B
−93%
−100%
Success_MultipleSideEffects
899.22 ns / 1960 B
18.79 ns / 0 B
−98%
−100%
HistoryEntry_Materialization
688.7 ns / 896 B
23.40 ns / 0 B
−97%
−100%
AuditEntry_Materialization
666.0 ns / 848 B
24.47 ns / 0 B
−96%
−100%
Non Goals
This issue does not change IMutationHistoryStore or IMutationAuditor interfaces
This issue does not address StateSizeEstimator.Estimate call guards
This issue does not optimize PolicyModificationApplier.Apply heap allocation
This issue does not change MutationEngine, MutationExecutionPipeline, or any execution orchestration code
This issue does not introduce breaking changes to method signatures or interfaces (underlying type representation changes from reference to value types, but no API surface changes)
Note
Struct conversion of MutationResult<TState> required adding = [] property initializers on PolicyDecisions and SideEffects, plus an explicit public MutationResult() { } parameterless constructor.
MutationHistory.csFirstMutationAt/LastMutationAt used FirstOrDefault()?.Timestamp with struct MutationHistoryEntry, FirstOrDefault() returns default(T) (cannot be null), so ?. is invalid. Replaced with Entries.Count > 0 ? Entries[0].Timestamp : null.
ConcurrentDictionary.TryAdd means concurrent first use may run duplicate reflection results are idempotent and harmless.
Summary
Eliminate per call heap allocations associated with
MutationResult<TState>creation andMutationHistoryEntry/MutationAuditEntrymaterialization by converting output DTOs toreadonly record struct, caching singletons for success/empty defaults, and pre caching supporting objects in benchmark setup.Goal
Reduce result creation time by ~79–98% and allocation to 0 B across all benchmark scenarios. Reduce materialization time by ~96–97% and allocation to 0 B.
Problem
Every
MutationResult<TState>.Success()/.Failure()/.PolicyBlocked()call and every history/audit entry materialization incurred several unnecessary heap allocations:MutationResult<TState>was class every result creation allocated new heap object including its internal default initializedValidationResult,MutationMetrics(withDictionary), and empty collections.ValidationResult.Success()created new instance each call allocatedValidationResultwith three emptyList<T>instances.MutationMetricsdefault initialized aDictionaryevery result carried heap allocated empty dictionary even when no metrics were recorded.ChangeSet.Emptycreated a new instance used as default property value on every result, even when immediately replaced.MutationHistoryEntryandMutationAuditEntrywere classes materializing audit or history output allocated the entry object plus all supporting objects (MutationIntent,MutationContext, side-effect list copy).SideEffect.ResolveContractran reflection everySideEffect.Create()calledGetCustomAttributesto resolve contract metadata.CreateIntent()/CreateContext()called per iteration in benchmarks allocatedMutationIntent(withHashSet+Dictionary) andMutationContext(withDictionary) each benchmark iteration.Scope
MutationResult<TState>(src/Abstractions/Results/MutationResult.cs):sealed record(class) toreadonly record structremoves the per instance heap allocation associated with result object creation.= []property initializers onPolicyDecisionsandSideEffects, plus explicit parameterless.ValidationResult(src/Abstractions/Results/ValidationResult.cs):private static readonly ValidationResult _success = new()singletonSuccess()returns the cached instance instead ofnew ValidationResult().MutationMetrics(src/Abstractions/Metrics/MutationMetrics.cs):internal static readonly MutationMetrics Empty = new()used as default in place ofnew().private static readonly IReadOnlyDictionary<string, object> _emptyAdditionalMetricsshared empty dictionary default.ChangeSet(src/Abstractions/Changes/ChangeSet.cs):private static readonly ChangeSet _empty = new()singletonEmptyproperty returns cached instance.SideEffect(src/Abstractions/Effects/SideEffect.cs):ConcurrentDictionary<Type, (string?, int?)> _contractCacheResolveContractlooks up the cache before running reflection is performed once per type in steady state. Concurrent first-use races may perform duplicate harmless reflection work viaTryAdd.MutationHistoryEntry(src/Abstractions/History/MutationHistoryEntry.cs):sealed classtoreadonly record structmaterialized as value type, eliminating the entry object heap allocation.MutationAuditEntry(src/Abstractions/Audit/MutationAuditEntry.cs):sealed classtoreadonly record structmaterialized as value type, eliminating the entry object heap allocation.MutationHistory(src/Abstractions/History/MutationHistory.cs):FirstMutationAt/LastMutationAtreplacedFirstOrDefault()?.Timestamp/LastOrDefault()?.TimestampwithEntries.Count > 0 ? Entries[0].Timestamp : null/Entries[^1].Timestamppattern (structFirstOrDefault()returnsdefault(T)so?.is inapplicable).Benchmarks/Results/MutationResultCreationBenchmarks.cs:[GlobalSetup]benchmark body only measuresMutationResult<TState>.Success()with already existing references.Benchmarks/Results/MutationOutputMaterializationBenchmarks.cs:MutationIntent,MutationContext,SideEffects.ToList()in[GlobalSetup]benchmark body only measuresnew MutationHistoryEntry { ... }/new MutationAuditEntry { ... }with already-existing references.Design Expectations
MutationResult<TState>is value type. Itsdefaultvalue contains null references for reference type fields, so factory methods must explicitly initialize all required reference properties. Property initializers (= []) and the explicit parameterless constructor ensure thatnew MutationResult<TState>()produces valid empty collections and defaults, whiledefaultremains zero initialized struct.MutationHistoryEntry/MutationAuditEntryas structs meansdefault(T)produces all null fields consumers must usenew() { ... }syntax with explicit property values (existing usage already does this).Entries.FirstOrDefault()?.Timestampno longer compiles for structTreplaced with explicitCount > 0guard.IReadOnlyList<T>,List<T>,Dictionary<,List<T>>). Boxing would occur only if passed to non genericIEnumerable,object, orIEqualityComparerpaths.is nullchecks orascasts are used on either type zero consumer breakage.Intent,Context, and side effect lists in[GlobalSetup]. Real pipeline execution also incurs these one-time allocation costs per mutation, but they are not per materialization costs.MutationResult<TState>creation itself introduces no heap allocation because all reference-type fields point to existing cached instances (ValidationResult.Success(),MutationMetrics.Empty,Array.Empty<PolicyDecision>, etc.) rather than newly created objects.Singleton Safety (Mutability Verification)
The following cached singletons share mutable internal state and must not be mutated:
ValidationResult._successList<T>(private readonly)AddError,AddWarning,AddInfo.Success().Add*(...)in current codebaseChangeSet._emptyList<StateChange>(private readonly)Add(StateChange)ChangeSet.Emptyis read only in all usagesIf defensive protection is required, follow up can freeze the internal lists (eg.,
ImmutableList<T>, or flag that causesAddto throw). This is not addressed in the current scope.Acceptance Criteria
MutationResult<TState>isreadonly record structzero heap allocation perSuccess()/Failure()/PolicyBlocked()ValidationResult.Success()returns cached singleton 0 allocation per callMutationMetrics.Emptyis cached singleton 0 allocation per resultChangeSet.Emptyis a cached singleton 0 allocation per useSideEffect.ResolveContractusesConcurrentDictionarycache reflection hit once per typeMutationHistoryEntryisreadonly record structzero allocation per materializationMutationAuditEntryisreadonly record structzero allocation per materializationBenchmark regression validated:
Success_NoSideEffectsSuccess_SingleSideEffectSuccess_MultipleSideEffectsHistoryEntry_MaterializationAuditEntry_MaterializationNon Goals
IMutationHistoryStoreorIMutationAuditorinterfacesStateSizeEstimator.Estimatecall guardsPolicyModificationApplier.Applyheap allocationMutationEngine,MutationExecutionPipeline, or any execution orchestration codeNote
MutationResult<TState>required adding= []property initializers onPolicyDecisionsandSideEffects, plus an explicitpublic MutationResult() { }parameterless constructor.MutationHistory.csFirstMutationAt/LastMutationAtusedFirstOrDefault()?.Timestampwith structMutationHistoryEntry,FirstOrDefault()returnsdefault(T)(cannot be null), so?.is invalid. Replaced withEntries.Count > 0 ? Entries[0].Timestamp : null.ConcurrentDictionary.TryAddmeans concurrent first use may run duplicate reflection results are idempotent and harmless.