From 5383a060248052e220970868c8178859c473ada5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 20 Jun 2026 17:12:45 +0200 Subject: [PATCH 1/3] perf: defer eager allocations in the mock-creation path Empty-mock creation eagerly allocated several infrastructure objects that a mock which is only created (never set up, invoked, or verified) never needs. Defer them to first use: - MockSetups and the scenario state are now lazy in MockRegistry; the Scenario getter no longer allocates on the invocation hot path, and the method-lookup read paths short-circuit to Array.Empty when no setups have been registered. - FastMockInteractions._verifiedLock is allocated only on the verification path. - The whole FastMockInteractions store is now built lazily by MockRegistry from a stored member count (new public MockRegistry(MockBehavior, int, object?[]?) ctor); the generator passes MemberCount instead of constructing the store up front. HttpClient's shared-store factory path stays eager. CreateMock for an empty interface mock drops from ~440 B / ~51 ns to ~184 B / ~18 ns (3 heap objects instead of 7). Updates the public-API and generator-output snapshots accordingly. --- .../Sources/Sources.MockClass.cs | 4 +- .../Interactions/FastMockInteractions.cs | 21 ++++-- Source/Mockolate/MockRegistry.Interactions.cs | 35 ++++++--- Source/Mockolate/MockRegistry.Setup.cs | 18 ++++- Source/Mockolate/MockRegistry.cs | 72 +++++++++++++++---- .../Expected/Mockolate_net10.0.txt | 3 +- .../Expected/Mockolate_net8.0.txt | 3 +- .../Expected/Mockolate_netstandard2.0.txt | 3 +- .../Mock.ComprehensiveAbstractClass.g.cs | 4 +- .../Mock.ICombinationMockA.g.cs | 4 +- .../Mock.ICombinationMockB.g.cs | 4 +- .../Mock.ComprehensiveAbstractClass.g.cs | 4 +- .../Mock.IComprehensiveInterface.g.cs | 4 +- .../Mock.HttpClient.g.cs | 2 +- .../Mock.HttpMessageHandler.g.cs | 4 +- .../Mock.IKeywordEdgeCases.g.cs | 4 +- .../Mock.IRefStructConsumer.g.cs | 4 +- .../Mock.IStaticAbstractMembers.g.cs | 4 +- 18 files changed, 144 insertions(+), 53 deletions(-) diff --git a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs index 9230cda4..9be05474 100644 --- a/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs +++ b/Source/Mockolate.SourceGenerators/Sources/Sources.MockClass.cs @@ -934,7 +934,7 @@ public static string MockClass( else { sb.Append("\t\t\tmockBehavior ??= global::Mockolate.MockBehavior.Default;").AppendLine(); - sb.Append("\t\t\tglobal::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.").Append(name).Append(".CreateFastInteractions(mockBehavior), constructorParameters);").AppendLine(); + sb.Append("\t\t\tglobal::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.").Append(name).Append(".MemberCount, constructorParameters);").AppendLine(); } sb.Append("\t\t\treturn CreateMockInstance(mockRegistry, constructorParameters, setup);").AppendLine(); @@ -1383,7 +1383,7 @@ private static void AppendCreateRegistryFromBehavior(StringBuilder sb, string in sb.Append(indent).Append("/// ").AppendLine(); sb.Append(indent).Append("private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior)").AppendLine(); sb.Append(indent).Append("{").AppendLine(); - sb.Append(indent).Append("\tglobal::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior));").AppendLine(); + sb.Append(indent).Append("\tglobal::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount);").AppendLine(); if (setsMockRegistryProvider) { sb.Append(indent).Append("\tMockRegistryProvider.Value = registry;").AppendLine(); diff --git a/Source/Mockolate/Interactions/FastMockInteractions.cs b/Source/Mockolate/Interactions/FastMockInteractions.cs index f17498aa..e1bbb008 100644 --- a/Source/Mockolate/Interactions/FastMockInteractions.cs +++ b/Source/Mockolate/Interactions/FastMockInteractions.cs @@ -22,7 +22,20 @@ public class FastMockInteractions : IMockInteractions private long _globalSequence; [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly MockolateLock _verifiedLock = new(); + [field: DebuggerBrowsable(DebuggerBrowsableState.Never)] + private MockolateLock VerifiedLock + { + get + { + if (field is { } existing) + { + return existing; + } + + Interlocked.CompareExchange(ref field, new MockolateLock(), null); + return field!; + } + } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private HashSet? _verified; @@ -191,7 +204,7 @@ public IReadOnlyCollection GetUnverifiedInteractions() unverified.Sort(static (left, right) => left.Seq.CompareTo(right.Seq)); } - lock (_verifiedLock) + lock (VerifiedLock) { if (_verified is null || _verified.Count == 0) { @@ -224,7 +237,7 @@ public IReadOnlyCollection GetUnverifiedInteractions() void IMockInteractions.Verified(IEnumerable interactions) { - lock (_verifiedLock) + lock (VerifiedLock) { _verified ??= []; foreach (IInteraction interaction in interactions) @@ -245,7 +258,7 @@ public void Clear() Volatile.Read(ref _fallback)?.Clear(); - lock (_verifiedLock) + lock (VerifiedLock) { _verified = null; } diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index bc379d0a..b22164b2 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -11,10 +11,25 @@ namespace Mockolate; public partial class MockRegistry { + private IMockInteractions? _interactions; + private readonly int _interactionMemberCount; + /// /// Gets the collection of interactions recorded by the mock object. /// - public IMockInteractions Interactions { get; } + /// + /// When the registry was created from a member count (the generator-emitted CreateMock path), the + /// backing is allocated lazily on first access — a mock that is only + /// created, never invoked or verified, allocates no interaction store at all. + /// + public IMockInteractions Interactions => _interactions ?? EnsureInteractions(); + + private IMockInteractions EnsureInteractions() + { + Interlocked.CompareExchange(ref _interactions, + new FastMockInteractions(_interactionMemberCount, Behavior.SkipInteractionRecording), null); + return _interactions!; + } /// /// Clears all interactions recorded by the mock object. @@ -131,8 +146,9 @@ public void ClearAllInteractions() /// A lazy stream of matching setups, scenario-scoped first. public IEnumerable GetMethodSetups(string methodName) where T : MethodSetup { - if (!string.IsNullOrEmpty(Scenario) && - Setup.TryGetScenario(Scenario, out MockScenarioSetup? scopedBucket)) + MockSetups? setups = _setups; + if (!string.IsNullOrEmpty(Scenario) && setups is not null && + setups.TryGetScenario(Scenario, out MockScenarioSetup? scopedBucket)) { return EnumerateScopedAndGlobalMethodSetups(methodName, scopedBucket); } @@ -140,7 +156,7 @@ public IEnumerable GetMethodSetups(string methodName) where T : MethodSetu MethodSetup[]?[]? snapshot = Volatile.Read(ref _setupsByMemberId); if (snapshot is null) { - return Setup.Methods.EnumerateByName(methodName); + return setups is null ? Array.Empty() : setups.Methods.EnumerateByName(methodName); } return EnumerateGlobalMethodSetups(methodName, snapshot); @@ -178,11 +194,14 @@ private IEnumerable EnumerateGlobalMethodSetups(string methodName, } // Hand-written SetupMethod(MethodSetup) entries (e.g. the HttpClientExtensions pipeline) live - // only in the root dict; the empty-storage fast path returns Array.Empty so the loop - // allocates nothing further when no such entry exists. - foreach (T setup in Setup.Methods.EnumerateByName(methodName)) + // only in the root dict; when no MockSetups has been allocated there can be none, so the loop + // is skipped entirely and never forces the lazy allocation. + if (_setups is { } setups) { - yield return setup; + foreach (T setup in setups.Methods.EnumerateByName(methodName)) + { + yield return setup; + } } } diff --git a/Source/Mockolate/MockRegistry.Setup.cs b/Source/Mockolate/MockRegistry.Setup.cs index 52c1d732..26f1e3b2 100644 --- a/Source/Mockolate/MockRegistry.Setup.cs +++ b/Source/Mockolate/MockRegistry.Setup.cs @@ -17,7 +17,7 @@ public partial class MockRegistry /// instead of growing one slot at a time as setups for higher-numbered members come in. /// private int GetMemberCountHint() - => Interactions is FastMockInteractions fast ? fast.Buffers.Length : 0; + => _interactions is FastMockInteractions fast ? fast.Buffers.Length : _interactionMemberCount; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private EventSetup[]?[]? _eventSetupsByMemberId; @@ -31,10 +31,24 @@ private int GetMemberCountHint() [DebuggerBrowsable(DebuggerBrowsableState.Never)] private MethodSetup[]?[]? _setupsByMemberId; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private MockSetups? _setups; + /// /// The registered setups for the mock, including methods, properties, indexers and events. /// - internal MockSetups Setup { get; } + /// + /// Allocated lazily on first access (setup registration or a read that has to consult the + /// string-keyed/scenario buckets). A mock that is only created — and only ever has its members + /// invoked through the member-id snapshot path — never allocates a . + /// + internal MockSetups Setup => _setups ?? EnsureSetups(); + + private MockSetups EnsureSetups() + { + Interlocked.CompareExchange(ref _setups, new MockSetups(), null); + return _setups!; + } /// /// Registers for the default scenario. diff --git a/Source/Mockolate/MockRegistry.cs b/Source/Mockolate/MockRegistry.cs index 991dca8a..4e451d85 100644 --- a/Source/Mockolate/MockRegistry.cs +++ b/Source/Mockolate/MockRegistry.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using System.Threading; using Mockolate.Interactions; using Mockolate.Setup; @@ -18,7 +19,7 @@ namespace Mockolate; public partial class MockRegistry { [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly ScenarioState _scenarioState; + private ScenarioState? _scenarioState; /// /// Creates a new with the given , a caller-provided @@ -46,9 +47,32 @@ public MockRegistry(MockBehavior behavior, IMockInteractions interactions, Behavior = behavior; ConstructorParameters = constructorParameters; - Interactions = interactions; - Setup = new MockSetups(); - _scenarioState = new ScenarioState(); + _interactions = interactions; + // Setup (MockSetups) and _scenarioState are allocated lazily on first use so a mock that is + // only created — and never has a setup registered or a scenario transition — pays for neither. + Wraps = null; + } + + /// + /// Creates a new with the given whose interaction + /// store is a sized to , allocated lazily + /// on first access. + /// + /// + /// The generator-emitted CreateMock paths use this overload so that a mock which is only created — and + /// never has a member invoked or verified — never allocates an interaction store at all. + /// + /// The that governs how the mock responds without a matching setup. + /// The number of distinct mockable members the lazily-created interaction store should hold. + /// + /// Values forwarded to the base-class constructor, or if no base constructor call is needed. + /// + public MockRegistry(MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) + { + Behavior = behavior; + ConstructorParameters = constructorParameters; + _interactionMemberCount = memberCount; + // Interactions, Setup and _scenarioState are all allocated lazily on first use. Wraps = null; } @@ -62,9 +86,11 @@ public MockRegistry(MockRegistry registry, object wraps) { Behavior = registry.Behavior; ConstructorParameters = registry.ConstructorParameters; - Interactions = new FastMockInteractions(0, registry.Behavior.SkipInteractionRecording); - Setup = registry.Setup; - _scenarioState = registry._scenarioState; + _interactions = new FastMockInteractions(0, registry.Behavior.SkipInteractionRecording); + // Materialize the parent's setups and scenario state so they stay shared by reference + // (derived registries for wrap/monitor/constructor-params share state with their source). + _setups = registry.Setup; + _scenarioState = registry.GetOrCreateScenarioState(); Wraps = wraps; } @@ -78,9 +104,12 @@ public MockRegistry(MockRegistry registry, object?[] constructorParameters) { Behavior = registry.Behavior; ConstructorParameters = constructorParameters; - Interactions = registry.Interactions; - Setup = registry.Setup; - _scenarioState = registry._scenarioState; + // Materialize the parent's interactions so this derived registry shares the same store. + _interactions = registry.Interactions; + // Materialize the parent's setups and scenario state so they stay shared by reference + // (derived registries for wrap/monitor/constructor-params share state with their source). + _setups = registry.Setup; + _scenarioState = registry.GetOrCreateScenarioState(); Wraps = registry.Wraps; } @@ -104,9 +133,11 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions) Behavior = registry.Behavior; ConstructorParameters = registry.ConstructorParameters; - Interactions = interactions; - Setup = registry.Setup; - _scenarioState = registry._scenarioState; + _interactions = interactions; + // Materialize the parent's setups and scenario state so they stay shared by reference + // (derived registries for wrap/monitor/constructor-params share state with their source). + _setups = registry.Setup; + _scenarioState = registry.GetOrCreateScenarioState(); Wraps = registry.Wraps; } @@ -131,7 +162,7 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions) /// Scenario setups add to, rather than replace, the default bucket - register catch-alls at the default scope /// and override specific members per scenario. /// - public string Scenario => _scenarioState.Current; + public string Scenario => _scenarioState?.Current ?? ""; /// /// Gets the behavior settings used by this mock instance. @@ -166,7 +197,18 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions) /// for the full resolution order. /// public void TransitionTo(string scenario) - => _scenarioState.Current = scenario; + => GetOrCreateScenarioState().Current = scenario; + + private ScenarioState GetOrCreateScenarioState() + { + if (_scenarioState is { } existing) + { + return existing; + } + + Interlocked.CompareExchange(ref _scenarioState, new ScenarioState(), null); + return _scenarioState!; + } private sealed class ScenarioState { diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index e6e1a560..2509f009 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -215,6 +215,7 @@ namespace Mockolate public MockRegistry(Mockolate.MockRegistry registry, object wraps) { } public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { } public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { } + public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { } public Mockolate.MockBehavior Behavior { get; } public object?[]? ConstructorParameters { get; } public Mockolate.Interactions.IMockInteractions Interactions { get; } @@ -3041,4 +3042,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} \ No newline at end of file +} diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 8cc498c1..1106455e 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -202,6 +202,7 @@ namespace Mockolate public MockRegistry(Mockolate.MockRegistry registry, object wraps) { } public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { } public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { } + public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { } public Mockolate.MockBehavior Behavior { get; } public object?[]? ConstructorParameters { get; } public Mockolate.Interactions.IMockInteractions Interactions { get; } @@ -2595,4 +2596,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} \ No newline at end of file +} diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index b17771d3..a063b392 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -177,6 +177,7 @@ namespace Mockolate public MockRegistry(Mockolate.MockRegistry registry, object wraps) { } public MockRegistry(Mockolate.MockRegistry registry, object?[] constructorParameters) { } public MockRegistry(Mockolate.MockBehavior behavior, Mockolate.Interactions.IMockInteractions interactions, object?[]? constructorParameters = null) { } + public MockRegistry(Mockolate.MockBehavior behavior, int memberCount, object?[]? constructorParameters = null) { } public Mockolate.MockBehavior Behavior { get; } public object?[]? ConstructorParameters { get; } public Mockolate.Interactions.IMockInteractions Interactions { get; } @@ -2524,4 +2525,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} \ No newline at end of file +} diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs index 731eb0c4..f48b324e 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs @@ -40,7 +40,7 @@ internal class ComprehensiveAbstractClass : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } @@ -943,7 +943,7 @@ internal static partial class MockExtensionsForComprehensiveAbstractClass } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockA.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockA.g.cs index 07c4f063..0022e9bf 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockA.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockA.g.cs @@ -39,7 +39,7 @@ internal class ICombinationMockA : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); return registry; } @@ -544,7 +544,7 @@ internal static partial class MockExtensionsForICombinationMockA } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockA.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockA.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockB.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockB.g.cs index a3701df5..935f23c5 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockB.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/BaseClass_WithMultipleAdditionalInterfaces_CanBeCreated/Mock.ICombinationMockB.g.cs @@ -39,7 +39,7 @@ internal class ICombinationMockB : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); return registry; } @@ -544,7 +544,7 @@ internal static partial class MockExtensionsForICombinationMockB } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockB.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ICombinationMockB.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveAbstractClass_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveAbstractClass_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs index 731eb0c4..f48b324e 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveAbstractClass_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveAbstractClass_CanBeCreated/Mock.ComprehensiveAbstractClass.g.cs @@ -40,7 +40,7 @@ internal class ComprehensiveAbstractClass : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } @@ -943,7 +943,7 @@ internal static partial class MockExtensionsForComprehensiveAbstractClass } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.ComprehensiveAbstractClass.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveInterface_CanBeCreated/Mock.IComprehensiveInterface.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveInterface_CanBeCreated/Mock.IComprehensiveInterface.g.cs index 0bc6de8c..c073699c 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveInterface_CanBeCreated/Mock.IComprehensiveInterface.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/ComprehensiveInterface_CanBeCreated/Mock.IComprehensiveInterface.g.cs @@ -92,7 +92,7 @@ internal class IComprehensiveInterface : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } @@ -5985,7 +5985,7 @@ internal static partial class MockExtensionsForIComprehensiveInterface } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IComprehensiveInterface.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IComprehensiveInterface.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpClient.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpClient.g.cs index 43f25a21..2f55e729 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpClient.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpClient.g.cs @@ -38,7 +38,7 @@ internal class HttpClient : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpMessageHandler.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpMessageHandler.g.cs index 76722f9f..be36ce5b 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpMessageHandler.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/HttpClient_CanBeCreated/Mock.HttpMessageHandler.g.cs @@ -38,7 +38,7 @@ internal class HttpMessageHandler : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } @@ -1146,7 +1146,7 @@ internal static partial class MockExtensionsForHttpMessageHandler } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.HttpMessageHandler.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.HttpMessageHandler.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/KeywordEdgeCases_CanBeCreated/Mock.IKeywordEdgeCases.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/KeywordEdgeCases_CanBeCreated/Mock.IKeywordEdgeCases.g.cs index ee7020eb..2d6faf64 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/KeywordEdgeCases_CanBeCreated/Mock.IKeywordEdgeCases.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/KeywordEdgeCases_CanBeCreated/Mock.IKeywordEdgeCases.g.cs @@ -45,7 +45,7 @@ internal class IKeywordEdgeCases : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); return registry; } @@ -1430,7 +1430,7 @@ internal static partial class MockExtensionsForIKeywordEdgeCases } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IKeywordEdgeCases.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IKeywordEdgeCases.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/Mock.IRefStructConsumer.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/Mock.IRefStructConsumer.g.cs index 1c809374..40a77913 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/Mock.IRefStructConsumer.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/RefStructConsumer_CanBeCreated/Mock.IRefStructConsumer.g.cs @@ -46,7 +46,7 @@ internal class IRefStructConsumer : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); return registry; } @@ -1425,7 +1425,7 @@ internal static partial class MockExtensionsForIRefStructConsumer } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IRefStructConsumer.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IRefStructConsumer.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } diff --git a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/StaticAbstractMembers_CanBeCreated/Mock.IStaticAbstractMembers.g.cs b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/StaticAbstractMembers_CanBeCreated/Mock.IStaticAbstractMembers.g.cs index 56747f5e..c442eac0 100644 --- a/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/StaticAbstractMembers_CanBeCreated/Mock.IStaticAbstractMembers.g.cs +++ b/Tests/Mockolate.SourceGenerators.Tests/Snapshot/Expected/StaticAbstractMembers_CanBeCreated/Mock.IStaticAbstractMembers.g.cs @@ -45,7 +45,7 @@ internal class IStaticAbstractMembers : /// private static global::Mockolate.MockRegistry MockolateCreateRegistryFromBehavior(global::Mockolate.MockBehavior behavior) { - global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, CreateFastInteractions(behavior)); + global::Mockolate.MockRegistry registry = new global::Mockolate.MockRegistry(behavior, MemberCount); MockRegistryProvider.Value = registry; return registry; } @@ -789,7 +789,7 @@ internal static partial class MockExtensionsForIStaticAbstractMembers } mockBehavior ??= global::Mockolate.MockBehavior.Default; - global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IStaticAbstractMembers.CreateFastInteractions(mockBehavior), constructorParameters); + global::Mockolate.MockRegistry mockRegistry = new global::Mockolate.MockRegistry(mockBehavior, global::Mockolate.Mock.IStaticAbstractMembers.MemberCount, constructorParameters); return CreateMockInstance(mockRegistry, constructorParameters, setup); } From 2e5fbdc86c5b4adb39f6f5c53bafead9befda1f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 20 Jun 2026 17:15:56 +0200 Subject: [PATCH 2/3] perf: defer the setup-registration lock in MockRegistry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _setupsByMemberIdLock is only taken by the setup-registration paths (AppendTo*/PublishProperty*), so a mock that is only created — and never has a setup registered — never needs it. Allocate it lazily like the other deferred collaborators. This removes the last eager allocation from empty-mock creation, which now allocates just two heap objects (the mock and its MockRegistry): ~160 B / ~14 ns. --- Source/Mockolate/MockRegistry.Setup.cs | 27 +++++++++++++++---- .../Expected/Mockolate_net10.0.txt | 2 +- .../Expected/Mockolate_net8.0.txt | 2 +- .../Expected/Mockolate_netstandard2.0.txt | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Source/Mockolate/MockRegistry.Setup.cs b/Source/Mockolate/MockRegistry.Setup.cs index 26f1e3b2..06063cf1 100644 --- a/Source/Mockolate/MockRegistry.Setup.cs +++ b/Source/Mockolate/MockRegistry.Setup.cs @@ -9,7 +9,24 @@ namespace Mockolate; public partial class MockRegistry { [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private readonly object _setupsByMemberIdLock = new(); + private object? _setupsByMemberIdLock; + + // Lazily allocated: only setup registration (the AppendTo*/PublishProperty* paths) takes this lock, + // so a mock that is only created — and never has a setup registered — never allocates it. + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private object SetupsByMemberIdLock + { + get + { + if (_setupsByMemberIdLock is { } existing) + { + return existing; + } + + Interlocked.CompareExchange(ref _setupsByMemberIdLock, new object(), null); + return _setupsByMemberIdLock!; + } + } /// /// Returns the generator-known member count hint when is a @@ -102,7 +119,7 @@ public void SetupIndexer(int memberId, string scenario, IndexerSetup indexerSetu private void AppendToIndexerMemberIdBucket(int memberId, IndexerSetup indexerSetup) { - lock (_setupsByMemberIdLock) + lock (SetupsByMemberIdLock) { IndexerSetup[]?[]? oldTable = _indexerSetupsByMemberId; int requiredLen = memberId + 1; @@ -198,7 +215,7 @@ public void SetupMethod(int memberId, string scenario, MethodSetup methodSetup) private void AppendToMemberIdBucket(int memberId, MethodSetup methodSetup) { - lock (_setupsByMemberIdLock) + lock (SetupsByMemberIdLock) { MethodSetup[]?[]? oldTable = _setupsByMemberId; int requiredLen = memberId + 1; @@ -293,7 +310,7 @@ public void SetupProperty(int memberId, string scenario, PropertySetup propertyS private void PublishPropertyToMemberIdBucket(int memberId, PropertySetup propertySetup) { - lock (_setupsByMemberIdLock) + lock (SetupsByMemberIdLock) { PropertySetup?[]? oldTable = _propertySetupsByMemberId; int requiredLen = memberId + 1; @@ -383,7 +400,7 @@ public void SetupEvent(int memberId, string scenario, EventSetup eventSetup) private void AppendToEventMemberIdBucket(int memberId, EventSetup eventSetup) { - lock (_setupsByMemberIdLock) + lock (SetupsByMemberIdLock) { EventSetup[]?[]? oldTable = _eventSetupsByMemberId; int requiredLen = memberId + 1; diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt index 2509f009..f82bbcb1 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net10.0.txt @@ -3042,4 +3042,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} +} \ No newline at end of file diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt index 1106455e..dac64b74 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_net8.0.txt @@ -2596,4 +2596,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} +} \ No newline at end of file diff --git a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt index a063b392..945ebdd9 100644 --- a/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt +++ b/Tests/Mockolate.Api.Tests/Expected/Mockolate_netstandard2.0.txt @@ -2525,4 +2525,4 @@ namespace Mockolate.Web TParameter WithHeaders(string headers); } } -} +} \ No newline at end of file From 517f20ebe3a7f56052d45925bac0a84feabf7ba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sun, 21 Jun 2026 07:15:49 +0200 Subject: [PATCH 3/3] perf: remove redundant comments and clarify lazy allocation in MockRegistry --- Source/Mockolate/MockRegistry.Interactions.cs | 5 ----- Source/Mockolate/MockRegistry.Setup.cs | 17 ++++------------- Source/Mockolate/MockRegistry.cs | 10 ---------- 3 files changed, 4 insertions(+), 28 deletions(-) diff --git a/Source/Mockolate/MockRegistry.Interactions.cs b/Source/Mockolate/MockRegistry.Interactions.cs index b22164b2..45871c04 100644 --- a/Source/Mockolate/MockRegistry.Interactions.cs +++ b/Source/Mockolate/MockRegistry.Interactions.cs @@ -17,11 +17,6 @@ public partial class MockRegistry /// /// Gets the collection of interactions recorded by the mock object. /// - /// - /// When the registry was created from a member count (the generator-emitted CreateMock path), the - /// backing is allocated lazily on first access — a mock that is only - /// created, never invoked or verified, allocates no interaction store at all. - /// public IMockInteractions Interactions => _interactions ?? EnsureInteractions(); private IMockInteractions EnsureInteractions() diff --git a/Source/Mockolate/MockRegistry.Setup.cs b/Source/Mockolate/MockRegistry.Setup.cs index 06063cf1..e65e08b6 100644 --- a/Source/Mockolate/MockRegistry.Setup.cs +++ b/Source/Mockolate/MockRegistry.Setup.cs @@ -9,22 +9,18 @@ namespace Mockolate; public partial class MockRegistry { [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private object? _setupsByMemberIdLock; - - // Lazily allocated: only setup registration (the AppendTo*/PublishProperty* paths) takes this lock, - // so a mock that is only created — and never has a setup registered — never allocates it. - [DebuggerBrowsable(DebuggerBrowsableState.Never)] + [field: DebuggerBrowsable(DebuggerBrowsableState.Never)] private object SetupsByMemberIdLock { get { - if (_setupsByMemberIdLock is { } existing) + if (field is { } existing) { return existing; } - Interlocked.CompareExchange(ref _setupsByMemberIdLock, new object(), null); - return _setupsByMemberIdLock!; + Interlocked.CompareExchange(ref field, new object(), null); + return field!; } } @@ -54,11 +50,6 @@ private int GetMemberCountHint() /// /// The registered setups for the mock, including methods, properties, indexers and events. /// - /// - /// Allocated lazily on first access (setup registration or a read that has to consult the - /// string-keyed/scenario buckets). A mock that is only created — and only ever has its members - /// invoked through the member-id snapshot path — never allocates a . - /// internal MockSetups Setup => _setups ?? EnsureSetups(); private MockSetups EnsureSetups() diff --git a/Source/Mockolate/MockRegistry.cs b/Source/Mockolate/MockRegistry.cs index 4e451d85..b522dd9b 100644 --- a/Source/Mockolate/MockRegistry.cs +++ b/Source/Mockolate/MockRegistry.cs @@ -48,8 +48,6 @@ public MockRegistry(MockBehavior behavior, IMockInteractions interactions, Behavior = behavior; ConstructorParameters = constructorParameters; _interactions = interactions; - // Setup (MockSetups) and _scenarioState are allocated lazily on first use so a mock that is - // only created — and never has a setup registered or a scenario transition — pays for neither. Wraps = null; } @@ -72,7 +70,6 @@ public MockRegistry(MockBehavior behavior, int memberCount, object?[]? construct Behavior = behavior; ConstructorParameters = constructorParameters; _interactionMemberCount = memberCount; - // Interactions, Setup and _scenarioState are all allocated lazily on first use. Wraps = null; } @@ -87,8 +84,6 @@ public MockRegistry(MockRegistry registry, object wraps) Behavior = registry.Behavior; ConstructorParameters = registry.ConstructorParameters; _interactions = new FastMockInteractions(0, registry.Behavior.SkipInteractionRecording); - // Materialize the parent's setups and scenario state so they stay shared by reference - // (derived registries for wrap/monitor/constructor-params share state with their source). _setups = registry.Setup; _scenarioState = registry.GetOrCreateScenarioState(); Wraps = wraps; @@ -104,10 +99,7 @@ public MockRegistry(MockRegistry registry, object?[] constructorParameters) { Behavior = registry.Behavior; ConstructorParameters = constructorParameters; - // Materialize the parent's interactions so this derived registry shares the same store. _interactions = registry.Interactions; - // Materialize the parent's setups and scenario state so they stay shared by reference - // (derived registries for wrap/monitor/constructor-params share state with their source). _setups = registry.Setup; _scenarioState = registry.GetOrCreateScenarioState(); Wraps = registry.Wraps; @@ -134,8 +126,6 @@ public MockRegistry(MockRegistry registry, IMockInteractions interactions) Behavior = registry.Behavior; ConstructorParameters = registry.ConstructorParameters; _interactions = interactions; - // Materialize the parent's setups and scenario state so they stay shared by reference - // (derived registries for wrap/monitor/constructor-params share state with their source). _setups = registry.Setup; _scenarioState = registry.GetOrCreateScenarioState(); Wraps = registry.Wraps;