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, ");