Skip to content

[Perf]: Eliminate audit and history entry allocations when auditor/store is disabled #90

Description

@rian-be

Summary

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.

IMutationAuditor (src/Abstractions/Audit/IMutationAuditor.cs):

  • Add bool IsEnabled => true; default interface method
  • Implementations that do not consume entries override to return false

IMutationHistoryStore (src/Abstractions/History/IMutationHistoryStore.cs):

  • Add bool IsEnabled => true; default interface method
  • Implementations that do not consume entries override to return false

MutationExecutionOutcomeProcessor (src/Runtime/Internal/Execution/MutationExecutionOutcomeProcessor.cs):

  • Guard AuditSuccessAsync/AuditFailureAsync with if (!_auditor.IsEnabled) return;
  • Guard StoreInHistoryAsync with if (!_historyStore.IsEnabled) return;

MutationAuditEntryFactory (src/Runtime/Diagnostics/MutationAuditEntryFactory.cs):

  • Replace result.SideEffects.ToList() with existing IReadOnlyList instance no defensive copy

NoOpAuditor / NoOpHistoryStore (Benchmarks/Diagnostics/Support/):

  • Add bool IsEnabled => false; override

Important

Design Expectations

  • 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).

Metadata

Metadata

Assignees

Labels

performancePerformance improvements or regressions

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions