Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Benchmarks/Mockolate.Benchmarks/MockCreationBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Measures the cost of creating an empty mock — no setup, no invocations, no verification.
/// </summary>
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<ICalculatorService> mock = TUnit.Mocks.Mock.Of<ICalculatorService>();
return mock.Object;
}

[Benchmark(Description = "Moq")]
public object Moq_CreateMock()
{
Moq.Mock<ICalculatorService> mock = new();
return mock.Object;
}

[Benchmark(Description = "NSubstitute")]
public object NSubstitute_CreateMock()
=> Substitute.For<ICalculatorService>();

[Benchmark(Description = "FakeItEasy")]
public object FakeItEasy_CreateMock()
=> A.Fake<ICalculatorService>();

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
58 changes: 38 additions & 20 deletions Source/Mockolate/Interactions/ChunkedSlotStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ namespace Mockolate.Interactions;
/// Slot N is stored at <c>chunks[N &gt;&gt; <see cref="ChunkShift" />][N &amp; <see cref="ChunkMask" />]</c>.
/// A writer reserves a unique slot via <see cref="Reserve" />, obtains its destination via
/// <see cref="SlotForWrite" />, writes its fields, then calls <see cref="Publish" />.
/// 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 <see cref="SlotForWrite" /> 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.
/// </remarks>
#if !DEBUG
[System.Diagnostics.DebuggerNonUserCode]
Expand All @@ -27,8 +28,8 @@ internal sealed class ChunkedSlotStorage<TRecord> 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;

Expand All @@ -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);
Expand All @@ -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];
}

/// <summary>
Expand All @@ -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];
}

/// <summary>
Expand All @@ -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;
Expand All @@ -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)
Expand All @@ -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);
Expand All @@ -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);
}

Expand Down
69 changes: 61 additions & 8 deletions Source/Mockolate/Setup/MockSetups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Threading;

namespace Mockolate.Setup;

Expand Down Expand Up @@ -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!;
}
}

/// <inheritdoc cref="object.ToString()" />
[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, ");
Expand Down