diff --git a/Benchmarks/Mockolate.Benchmarks/MockCreationBenchmarks.cs b/Benchmarks/Mockolate.Benchmarks/MockCreationBenchmarks.cs new file mode 100644 index 00000000..098ade80 --- /dev/null +++ b/Benchmarks/Mockolate.Benchmarks/MockCreationBenchmarks.cs @@ -0,0 +1,58 @@ +using BenchmarkDotNet.Attributes; +using FakeItEasy; +using Imposter.Abstractions; +using Mockolate.Benchmarks; +using NSubstitute; + +[assembly: GenerateImposter(typeof(MockCreationBenchmarks.ICalculatorService))] + +namespace Mockolate.Benchmarks; +#pragma warning disable CA1822 // Mark members as static + +/// +/// Measures the cost of creating an empty mock — no setup, no invocations, no verification. +/// +public class MockCreationBenchmarks : BenchmarksBase +{ + [Benchmark(Baseline = true, Description = "Mockolate")] + public object Mockolate_CreateMock() + => ICalculatorService.CreateMock(); + + [Benchmark(Description = "Imposter")] + public object Imposter_CreateMock() + { + ICalculatorServiceImposter imposter = ICalculatorService.Imposter(); + return imposter.Instance(); + } + + [Benchmark(Description = "TUnit.Mocks")] + public object TUnitMocks_CreateMock() + { + TUnit.Mocks.Mock mock = TUnit.Mocks.Mock.Of(); + return mock.Object; + } + + [Benchmark(Description = "Moq")] + public object Moq_CreateMock() + { + Moq.Mock mock = new(); + return mock.Object; + } + + [Benchmark(Description = "NSubstitute")] + public object NSubstitute_CreateMock() + => Substitute.For(); + + [Benchmark(Description = "FakeItEasy")] + public object FakeItEasy_CreateMock() + => A.Fake(); + + public interface ICalculatorService + { + int Zero { get; } + int Add(int a, int b); + double Divide(double numerator, double denominator); + string Format(int value); + } +} +#pragma warning restore CA1822 // Mark members as static diff --git a/Source/Mockolate/Interactions/ChunkedSlotStorage.cs b/Source/Mockolate/Interactions/ChunkedSlotStorage.cs index bcc2b7f7..4d6a20ea 100644 --- a/Source/Mockolate/Interactions/ChunkedSlotStorage.cs +++ b/Source/Mockolate/Interactions/ChunkedSlotStorage.cs @@ -13,9 +13,10 @@ namespace Mockolate.Interactions; /// Slot N is stored at chunks[N >> ][N & ]. /// A writer reserves a unique slot via , obtains its destination via /// , writes its fields, then calls . -/// The chunks array itself doubles when a new chunk index is past its end; that grow only copies -/// chunk references (which are stable once installed), so a concurrent writer to an existing -/// chunk cannot lose its data. +/// The chunks array itself is allocated lazily on the first and +/// doubles when a new chunk index is past its end; that grow only copies chunk references +/// (which are stable once installed), so a concurrent writer to an existing chunk cannot lose +/// its data. A buffer that is never written into stays allocation-free past its own header. /// #if !DEBUG [System.Diagnostics.DebuggerNonUserCode] @@ -27,8 +28,8 @@ internal sealed class ChunkedSlotStorage where TRecord : struct internal const int ChunkMask = ChunkSize - 1; internal readonly MockolateLock Lock = new(); - internal TRecord[]?[] Chunks = new TRecord[1][]; - internal bool[]?[] VerifiedChunks = new bool[1][]; + internal TRecord[]?[]? Chunks; + internal bool[]?[]? VerifiedChunks; private int _reserved; private int _published; @@ -52,8 +53,10 @@ public ref TRecord SlotForWrite(int slot) { int chunkIdx = slot >> ChunkShift; int offset = slot & ChunkMask; - TRecord[]?[] chunks = Volatile.Read(ref Chunks); - TRecord[]? chunk = chunkIdx < chunks.Length ? Volatile.Read(ref chunks[chunkIdx]) : null; + TRecord[]?[]? chunks = Volatile.Read(ref Chunks); + TRecord[]? chunk = chunks is not null && chunkIdx < chunks.Length + ? Volatile.Read(ref chunks[chunkIdx]) + : null; if (chunk is null) { chunk = EnsureChunk(chunkIdx); @@ -71,7 +74,7 @@ public ref TRecord SlotUnderLock(int slot) { int chunkIdx = slot >> ChunkShift; int offset = slot & ChunkMask; - return ref Chunks[chunkIdx]![offset]; + return ref Chunks![chunkIdx]![offset]; } /// @@ -81,7 +84,7 @@ public ref bool VerifiedUnderLock(int slot) { int chunkIdx = slot >> ChunkShift; int offset = slot & ChunkMask; - return ref VerifiedChunks[chunkIdx]![offset]; + return ref VerifiedChunks![chunkIdx]![offset]; } /// @@ -92,14 +95,17 @@ public void Clear() lock (Lock) { int n = _published; - TRecord[]?[] chunks = Chunks; - bool[]?[] verified = VerifiedChunks; - for (int slot = 0; slot < n; slot++) + if (n > 0) { - int chunkIdx = slot >> ChunkShift; - int offset = slot & ChunkMask; - chunks[chunkIdx]![offset] = default; - verified[chunkIdx]![offset] = false; + TRecord[]?[] chunks = Chunks!; + bool[]?[] verified = VerifiedChunks!; + for (int slot = 0; slot < n; slot++) + { + int chunkIdx = slot >> ChunkShift; + int offset = slot & ChunkMask; + chunks[chunkIdx]![offset] = default; + verified[chunkIdx]![offset] = false; + } } _reserved = 0; @@ -111,8 +117,20 @@ private TRecord[] EnsureChunk(int chunkIdx) { lock (Lock) { - TRecord[]?[] chunks = Chunks; - if (chunkIdx >= chunks.Length) + TRecord[]?[]? chunks = Chunks; + if (chunks is null) + { + int initialLen = 1; + while (chunkIdx >= initialLen) + { + initialLen *= 2; + } + + chunks = new TRecord[initialLen][]; + VerifiedChunks = new bool[initialLen][]; + Volatile.Write(ref Chunks, chunks); + } + else if (chunkIdx >= chunks.Length) { int newLen = chunks.Length; while (chunkIdx >= newLen) @@ -123,7 +141,7 @@ private TRecord[] EnsureChunk(int chunkIdx) TRecord[]?[] biggerChunks = new TRecord[newLen][]; Array.Copy(chunks, biggerChunks, chunks.Length); bool[]?[] biggerVerified = new bool[newLen][]; - Array.Copy(VerifiedChunks, biggerVerified, VerifiedChunks.Length); + Array.Copy(VerifiedChunks!, biggerVerified, VerifiedChunks!.Length); chunks = biggerChunks; VerifiedChunks = biggerVerified; Volatile.Write(ref Chunks, chunks); @@ -133,7 +151,7 @@ private TRecord[] EnsureChunk(int chunkIdx) if (chunk is null) { chunk = new TRecord[ChunkSize]; - VerifiedChunks[chunkIdx] = new bool[ChunkSize]; + VerifiedChunks![chunkIdx] = new bool[ChunkSize]; Volatile.Write(ref chunks[chunkIdx], chunk); } diff --git a/Source/Mockolate/Setup/MockSetups.cs b/Source/Mockolate/Setup/MockSetups.cs index 2448e26d..254cf151 100644 --- a/Source/Mockolate/Setup/MockSetups.cs +++ b/Source/Mockolate/Setup/MockSetups.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; +using System.Threading; namespace Mockolate.Setup; @@ -52,35 +53,87 @@ public bool TryGetScenario(string setupScenario, [NotNullWhen(true)] out MockSce internal class MockScenarioSetup { - internal MockSetups.EventSetups Events { get; } = new(); - internal MockSetups.IndexerSetups Indexers { get; } = new(); - internal MockSetups.MethodSetups Methods { get; } = new(); - internal MockSetups.PropertySetups Properties { get; } = new(); + private MockSetups.EventSetups? _events; + private MockSetups.IndexerSetups? _indexers; + private MockSetups.MethodSetups? _methods; + private MockSetups.PropertySetups? _properties; + + internal MockSetups.EventSetups Events + { + get + { + if (_events is null) + { + Interlocked.CompareExchange(ref _events, new MockSetups.EventSetups(), null); + } + + return _events!; + } + } + + internal MockSetups.IndexerSetups Indexers + { + get + { + if (_indexers is null) + { + Interlocked.CompareExchange(ref _indexers, new MockSetups.IndexerSetups(), null); + } + + return _indexers!; + } + } + + internal MockSetups.MethodSetups Methods + { + get + { + if (_methods is null) + { + Interlocked.CompareExchange(ref _methods, new MockSetups.MethodSetups(), null); + } + + return _methods!; + } + } + + internal MockSetups.PropertySetups Properties + { + get + { + if (_properties is null) + { + Interlocked.CompareExchange(ref _properties, new MockSetups.PropertySetups(), null); + } + + return _properties!; + } + } /// [EditorBrowsable(EditorBrowsableState.Never)] public override string ToString() { StringBuilder sb = new(); - int methodCount = Methods.Count; + int methodCount = _methods?.Count ?? 0; if (methodCount > 0) { sb.Append(methodCount).Append(methodCount == 1 ? " method, " : " methods, "); } - int propertyCount = Properties.Count; + int propertyCount = _properties?.Count ?? 0; if (propertyCount > 0) { sb.Append(propertyCount).Append(propertyCount == 1 ? " property, " : " properties, "); } - int indexerCount = Indexers.Count; + int indexerCount = _indexers?.Count ?? 0; if (indexerCount > 0) { sb.Append(indexerCount).Append(indexerCount == 1 ? " indexer, " : " indexers, "); } - int eventCount = Events.Count; + int eventCount = _events?.Count ?? 0; if (eventCount > 0) { sb.Append(eventCount).Append(eventCount == 1 ? " event, " : " events, ");