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 mutation heap allocations from MutationAuditEntry and MutationHistoryEntry creation when the registered auditor or history store does not consume them ( NoOpAuditor/NoOpHistoryStore). Remove redundant defensive copy in CreateHistoryEntry.
Goal
Lower latency and GC pressure for the common case where diagnostics are configured but no real auditor/store is attached, without breaking existing public API contracts.
Problem
Every mutation execution unconditionally allocates MutationAuditEntry (via CreateSuccess) and MutationHistoryEntry (via CreateHistoryEntry) objects, even when the registered IMutationAuditor and IMutationHistoryStore implementations are no-ops that discard the data immediately. The factory runs first, allocates, then passes the result to a consumer that ignores it.
Additionally, CreateHistoryEntry called result.SideEffects.ToList() which allocated a new List<SideEffect> on every commit a redundant copy of an already read only collection.
Hot path with NoOpAuditor/NoOpHistoryStore must allocate zero bytes for audit/history entries per ExecuteAsync.
IsEnabled uses default interface method existing implementors are unaffected and do not require source changes.
Allocations for enabled auditors (e.g. InMemoryAuditor) remain unchanged entries are still created because they are consumed.
No breaking public API changes. Additive IsEnabled default interface properties preserve compatibility with existing implementations.
Acceptance Criteria
OutcomeProcessor skips CreateSuccess when _auditor.IsEnabled is false
OutcomeProcessor skips CreateFailure when _auditor.IsEnabled is false
OutcomeProcessor skips CreateHistoryEntry when _historyStore.IsEnabled is false
CreateHistoryEntry no longer calls .ToList() on side effects
NoOpAuditor and NoOpHistoryStore declare IsEnabled => false
All existing unit tests pass without modification
Benchmark regression validated:
Benchmark
Before
After
Δ
NoDiagnostics_Baseline
2.237 us / —
1.430 us / 2.55 KB
-36.1%
AuditHistory_Enabled
4.974 us / —
3.660 us / 2.97 KB
-26.4%
CombinedInterceptionAndDiagnostics_Enabled
5.653 us / 4.01 KB
3.966 us / 3.52 KB
-29.8% / -12.2%
Allocation measurements for the previous baseline were not captured, so delta focuses on latency. The 2.55 KB after for NoDiagnostics_Baseline includes other required allocations audit/history entry creation itself is zero.
Non Goals
This issue does not change Task<T> to ValueTask<T>.
This issue does not address the with expression allocation in FinalizeResultAsync.
This issue does not introduce any breaking public API changes.
Note
IsEnabled is default interface method with default return of true. No existing implementor needs to change. The NoDiagnostics_Baseline improvement is driven entirely by skipping factory calls NoOpAuditor + NoOpHistoryStore now avoid MutationAuditEntry, MutationHistoryEntry, and all intermediate allocations. Removing .ToList() from CreateHistoryEntry also benefits AuditHistory_Enabled and Combined*, though those still allocate entries because their auditor/store is InMemoryAuditor/InMemoryHistoryStore (both IsEnabled = true).
Summary
Eliminate per mutation heap allocations from
MutationAuditEntryandMutationHistoryEntrycreation when the registered auditor or history store does not consume them (NoOpAuditor/NoOpHistoryStore). Remove redundant defensive copy inCreateHistoryEntry.Goal
Lower latency and GC pressure for the common case where diagnostics are configured but no real auditor/store is attached, without breaking existing public API contracts.
Problem
Every mutation execution unconditionally allocates
MutationAuditEntry(viaCreateSuccess) andMutationHistoryEntry(viaCreateHistoryEntry) objects, even when the registeredIMutationAuditorandIMutationHistoryStoreimplementations are no-ops that discard the data immediately. The factory runs first, allocates, then passes the result to a consumer that ignores it.Additionally,
CreateHistoryEntrycalledresult.SideEffects.ToList()which allocated a newList<SideEffect>on every commit a redundant copy of an already read only collection.IMutationAuditor (
src/Abstractions/Audit/IMutationAuditor.cs):bool IsEnabled => true;default interface methodfalseIMutationHistoryStore (
src/Abstractions/History/IMutationHistoryStore.cs):bool IsEnabled => true;default interface methodfalseMutationExecutionOutcomeProcessor (
src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs):AuditSuccessAsync/AuditFailureAsyncwithif (!_auditor.IsEnabled) return;StoreInHistoryAsyncwithif (!_historyStore.IsEnabled) return;MutationAuditEntryFactory (
src/Runtime/Diagnostics/MutationAuditEntryFactory.cs):result.SideEffects.ToList()with existingIReadOnlyListinstance no defensive copyNoOpAuditor / NoOpHistoryStore (
Benchmarks/Diagnostics/Support/):bool IsEnabled => false;overrideImportant
Design Expectations
NoOpAuditor/NoOpHistoryStoremust allocate zero bytes for audit/history entries perExecuteAsync.IsEnableduses default interface method existing implementors are unaffected and do not require source changes.InMemoryAuditor) remain unchanged entries are still created because they are consumed.IsEnableddefault interface properties preserve compatibility with existing implementations.Acceptance Criteria
OutcomeProcessorskipsCreateSuccesswhen_auditor.IsEnabledisfalseOutcomeProcessorskipsCreateFailurewhen_auditor.IsEnabledisfalseOutcomeProcessorskipsCreateHistoryEntrywhen_historyStore.IsEnabledisfalseCreateHistoryEntryno longer calls.ToList()on side effectsNoOpAuditorandNoOpHistoryStoredeclareIsEnabled => falseNoDiagnostics_BaselineAuditHistory_EnabledCombinedInterceptionAndDiagnostics_EnabledNon Goals
Task<T>toValueTask<T>.withexpression allocation inFinalizeResultAsync.Note
IsEnabledis default interface method with default return oftrue. No existing implementor needs to change. TheNoDiagnostics_Baselineimprovement is driven entirely by skipping factory callsNoOpAuditor+NoOpHistoryStorenow avoidMutationAuditEntry,MutationHistoryEntry, and all intermediate allocations. Removing.ToList()fromCreateHistoryEntryalso benefitsAuditHistory_EnabledandCombined*, though those still allocate entries because their auditor/store isInMemoryAuditor/InMemoryHistoryStore(bothIsEnabled = true).