diff --git a/Mediator.sln b/Mediator.sln index c5e18c3..d7c774c 100644 --- a/Mediator.sln +++ b/Mediator.sln @@ -64,13 +64,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleStreaming", "samples\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ASPNET", "samples\ASPNET\ASPNET.csproj", "{35D4B136-E164-48A3-ADC3-E22237CCDC9C}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SourceGenerator.Roslyn38", "src\Mediator.SourceGenerator.Roslyn38\Mediator.SourceGenerator.Roslyn38.csproj", "{FCB31060-4C23-4BB7-9A95-4C636B43F6B5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Roslyn38", "src\Mediator.SourceGenerator.Roslyn38\Mediator.SourceGenerator.Roslyn38.csproj", "{FCB31060-4C23-4BB7-9A95-4C636B43F6B5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SourceGenerator.Roslyn40", "src\Mediator.SourceGenerator.Roslyn40\Mediator.SourceGenerator.Roslyn40.csproj", "{C5AD8C4D-D731-4DDD-96BD-A6776A1FF20E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Roslyn40", "src\Mediator.SourceGenerator.Roslyn40\Mediator.SourceGenerator.Roslyn40.csproj", "{C5AD8C4D-D731-4DDD-96BD-A6776A1FF20E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SourceGenerator.Implementation", "src\Mediator.SourceGenerator.Implementation\Mediator.SourceGenerator.Implementation.csproj", "{22984D49-8DDF-4263-8375-D018643E44B2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Implementation", "src\Mediator.SourceGenerator.Implementation\Mediator.SourceGenerator.Implementation.csproj", "{22984D49-8DDF-4263-8375-D018643E44B2}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SourceGenerator.Roslyn40.Tests", "test\Mediator.SourceGenerator.Roslyn40.Tests\Mediator.SourceGenerator.Roslyn40.Tests.csproj", "{E4EE80C5-179C-4483-9E91-3107B3E1CD5A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mediator.SourceGenerator.Roslyn40.Tests", "test\Mediator.SourceGenerator.Roslyn40.Tests\Mediator.SourceGenerator.Roslyn40.Tests.csproj", "{E4EE80C5-179C-4483-9E91-3107B3E1CD5A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mediator.SmokeTestConsole", "test\Mediator.SmokeTestConsole\Mediator.SmokeTestConsole.csproj", "{65D8C0F8-E4CF-452D-86FB-482FE63C7C89}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -322,6 +324,18 @@ Global {E4EE80C5-179C-4483-9E91-3107B3E1CD5A}.Release|x64.Build.0 = Release|Any CPU {E4EE80C5-179C-4483-9E91-3107B3E1CD5A}.Release|x86.ActiveCfg = Release|Any CPU {E4EE80C5-179C-4483-9E91-3107B3E1CD5A}.Release|x86.Build.0 = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|Any CPU.Build.0 = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|x64.ActiveCfg = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|x64.Build.0 = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|x86.ActiveCfg = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Debug|x86.Build.0 = Debug|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|Any CPU.ActiveCfg = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|Any CPU.Build.0 = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x64.ActiveCfg = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x64.Build.0 = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x86.ActiveCfg = Release|Any CPU + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -348,6 +362,7 @@ Global {C5AD8C4D-D731-4DDD-96BD-A6776A1FF20E} = {438C928E-FBB5-45B2-9CD2-7ED3341AC75E} {22984D49-8DDF-4263-8375-D018643E44B2} = {438C928E-FBB5-45B2-9CD2-7ED3341AC75E} {E4EE80C5-179C-4483-9E91-3107B3E1CD5A} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84} + {65D8C0F8-E4CF-452D-86FB-482FE63C7C89} = {ED1809FC-733B-4D9E-8FEE-70D3BB0BBD84} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D45B5457-4190-49B6-BF89-7FA5F4C8ABE2} diff --git a/src/Mediator.SourceGenerator.Implementation/Analysis/CompilationAnalyzer.cs b/src/Mediator.SourceGenerator.Implementation/Analysis/CompilationAnalyzer.cs index 210b8f0..0b99beb 100644 --- a/src/Mediator.SourceGenerator.Implementation/Analysis/CompilationAnalyzer.cs +++ b/src/Mediator.SourceGenerator.Implementation/Analysis/CompilationAnalyzer.cs @@ -99,6 +99,9 @@ internal sealed class CompilationAnalyzer public bool ServiceLifetimeIsTransient => ServiceLifetimeSymbol.Name == "Transient"; + public bool IsTestRun => (_context.Compilation.AssemblyName?.StartsWith("Mediator.Tests") ?? false) || + (_context.Compilation.AssemblyName?.StartsWith("Mediator.SmokeTest") ?? false); + public CompilationAnalyzer(in CompilationAnalyzerContext context) { _context = context; diff --git a/src/Mediator.SourceGenerator.Implementation/resources/Mediator.sbn-cs b/src/Mediator.SourceGenerator.Implementation/resources/Mediator.sbn-cs index fd1cee9..60da7f1 100644 --- a/src/Mediator.SourceGenerator.Implementation/resources/Mediator.sbn-cs +++ b/src/Mediator.SourceGenerator.Implementation/resources/Mediator.sbn-cs @@ -150,9 +150,9 @@ namespace {{ MediatorNamespace }} { private readonly global::System.IServiceProvider _sp; {{~ if ServiceLifetimeIsSingleton ~}} - private FastLazyValue _diCacheLazy; + {{ if IsTestRun }}internal{{ else }}private{{ end }} FastLazyValue _diCacheLazy; {{~ else ~}} - private DICache _diCache; + {{ if IsTestRun }}internal{{ else }}private{{ end }} DICache _diCache; {{~ end ~}} /// @@ -178,16 +178,18 @@ namespace {{ MediatorNamespace }} ? (s => ((object[])s).Length) : (s => s.Count()); } - private struct FastLazyValue + {{ if IsTestRun }}internal{{ else }}private{{ end }} struct FastLazyValue where T : struct { - private const long UNINIT = 0; - private const long INITING = 1; - private const long INITD = 2; + {{ if IsTestRun }}internal{{ else }}private{{ end }} const long UNINIT = 0; + {{ if IsTestRun }}internal{{ else }}private{{ end }} const long INITING = 1; + {{ if IsTestRun }}internal{{ else }}private{{ end }} const long INITD = 2; + {{ if IsTestRun }}internal const long INVALID = -1;{{ end }} + {{ if IsTestRun }}internal const long CACHED = 3;{{ end }} - private global::System.Func _generator; - private long _state; - private T _value; + {{ if IsTestRun }}internal{{ else }}private{{ end }} global::System.Func _generator; + {{ if IsTestRun }}internal{{ else }}private{{ end }} long _state; + {{ if IsTestRun }}internal{{ else }}private{{ end }} T _value; public T Value { @@ -201,7 +203,7 @@ namespace {{ MediatorNamespace }} } } - private T ValueSlow + {{ if IsTestRun }}internal{{ else }}private{{ end }} T ValueSlow { [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] get @@ -228,6 +230,47 @@ namespace {{ MediatorNamespace }} } } + {{~ if IsTestRun ~}} + internal (T, long) ValueInstrumented + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + get + { + if (_state != INITD) + return ValueSlowInstrumented; + + return (_value, CACHED); + } + } + + internal (T, long) ValueSlowInstrumented + { + [global::System.Runtime.CompilerServices.MethodImpl(global::System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] + get + { + var prevState = global::System.Threading.Interlocked.CompareExchange(ref _state, INITING, UNINIT); + switch (prevState) + { + case INITD: + // Someone has already completed init + return (_value, INITD); + case INITING: + // Wait for someone else to complete + var spinWait = default(global::System.Threading.SpinWait); + while (global::System.Threading.Interlocked.Read(ref _state) < INITD) + spinWait.SpinOnce(); + return (_value, INITING); + case UNINIT: + _value = _generator(); + global::System.Threading.Interlocked.Exchange(ref _state, INITD); + return (_value, UNINIT); + } + + return (_value, INVALID); + } + } + {{~ end ~}} + public FastLazyValue(global::System.Func generator) { _generator = generator; @@ -236,7 +279,7 @@ namespace {{ MediatorNamespace }} } } - private readonly struct DICache + {{ if IsTestRun }}internal{{ else }}private{{ end }} readonly struct DICache { private readonly global::System.IServiceProvider _sp; diff --git a/test/Mediator.SmokeTestConsole/Mediator.SmokeTestConsole.csproj b/test/Mediator.SmokeTestConsole/Mediator.SmokeTestConsole.csproj new file mode 100644 index 0000000..b48e876 --- /dev/null +++ b/test/Mediator.SmokeTestConsole/Mediator.SmokeTestConsole.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0 + + true + true + + + + + + + + + + + + + + + + diff --git a/test/Mediator.SmokeTestConsole/Program.cs b/test/Mediator.SmokeTestConsole/Program.cs new file mode 100644 index 0000000..638b826 --- /dev/null +++ b/test/Mediator.SmokeTestConsole/Program.cs @@ -0,0 +1,131 @@ +using Mediator; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using DICache = Mediator.Mediator.DICache; +using LazyDICache = Mediator.Mediator.FastLazyValue; + +await Host.CreateDefaultBuilder() + .ConfigureLogging((context, logging) => + { + logging.ClearProviders(); + logging.AddSimpleConsole(options => + { + options.SingleLine = true; + }); + }) + .ConfigureServices(services => + { + services.AddHostedService(); + }) + .Build() + .RunAsync(); + +public sealed class Work : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var concurrency = Environment.ProcessorCount; + var threads = new Task<(DICache Cache, long State)>[concurrency]; + + var services = new ServiceCollection(); + services.AddMediator(); + + var iteration = 0; + const int maxIterations = 1_000_000; + + Console.WriteLine( + $"Starting smoketests - " + + $"{nameof(concurrency)}={concurrency}" + + $", {nameof(maxIterations)}={maxIterations}" + ); + + while (!stoppingToken.IsCancellationRequested && iteration < maxIterations) + { + await using var sp = services.BuildServiceProvider(validateScopes: true); + + var mediator = sp.GetRequiredService(); + + var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + for (int i = 0; i < concurrency; i++) + threads[i] = Task.Run(Thread); + + start.SetResult(); + var values = await Task.WhenAll(threads).ConfigureAwait(false); + var states = values.Select(v => v.State).ToArray(); + var firstHandlers = values.Select(v => v.Cache.Wrapper_For_Request).ToArray(); + var firstHandler = firstHandlers[0]; + var lastHandlers = values.Select(v => v.Cache.Wrapper_For_Request5).ToArray(); + var lastHandler = lastHandlers[0]; + + var hasInvalidHandler = firstHandlers.Any(h => !ReferenceEquals(h, firstHandler)) || + lastHandlers.Any(h => !ReferenceEquals(h, lastHandler)); + var hasInvalid = states.Any(s => s == LazyDICache.INVALID); + var wasUninitCount = states.Count(s => s == LazyDICache.UNINIT); + var wasInitingCount = states.Count(s => s == LazyDICache.INITING); + var wasInitedCount = states.Count(s => s == LazyDICache.INITD); + var wasCachedCount = states.Count(s => s == LazyDICache.CACHED); + + + Console.WriteLine( + $"Ran smoketest iteration {++iteration} - " + + $"{nameof(hasInvalidHandler)}={hasInvalidHandler}" + + $", {nameof(hasInvalid)}={hasInvalid}" + + $", {nameof(wasUninitCount)}={wasUninitCount}" + + $", {nameof(wasInitingCount)}={wasInitingCount}" + + $", {nameof(wasInitedCount)}={wasInitedCount}" + + $", {nameof(wasCachedCount)}={wasCachedCount}" + ); + + if (hasInvalidHandler || hasInvalid || wasUninitCount != 1) + { + Console.WriteLine("Error condition, exiting..."); + break; + } + + async Task<(DICache Cache, long State)> Thread() + { + await start.Task.ConfigureAwait(false); + + return mediator._diCacheLazy.ValueInstrumented; + } + } + + Console.WriteLine("------------------"); + Console.WriteLine( + $"Done smoketesting! - " + + $"{nameof(concurrency)}={concurrency}, {nameof(maxIterations)}={maxIterations}" + ); + } +} + +public sealed record Request() : IRequest; +public sealed class RequestHandler : IRequestHandler +{ + public ValueTask Handle(Request request, CancellationToken cancellationToken) => default; +} + +public sealed record Request2() : IRequest; +public sealed class Request2Handler : IRequestHandler +{ + public ValueTask Handle(Request2 request, CancellationToken cancellationToken) => default; +} + +public sealed record Request3() : IRequest; +public sealed class Request3Handler : IRequestHandler +{ + public ValueTask Handle(Request3 request, CancellationToken cancellationToken) => default; +} + +public sealed record Request4() : IRequest; +public sealed class Request4Handler : IRequestHandler +{ + public ValueTask Handle(Request4 request, CancellationToken cancellationToken) => default; +} + +public sealed record Request5() : IRequest; +public sealed class Request5Handler : IRequestHandler +{ + public ValueTask Handle(Request5 request, CancellationToken cancellationToken) => default; +} diff --git a/test/Mediator.Tests.ScopedLifetime/Mediator.Tests.ScopedLifetime.csproj b/test/Mediator.Tests.ScopedLifetime/Mediator.Tests.ScopedLifetime.csproj index d2840d8..05823c7 100644 --- a/test/Mediator.Tests.ScopedLifetime/Mediator.Tests.ScopedLifetime.csproj +++ b/test/Mediator.Tests.ScopedLifetime/Mediator.Tests.ScopedLifetime.csproj @@ -5,6 +5,7 @@ false true + true @@ -33,7 +34,7 @@ - + %(RecursiveDir)%(Filename)%(Extension) diff --git a/test/Mediator.Tests.TransientLifetime/Mediator.Tests.TransientLifetime.csproj b/test/Mediator.Tests.TransientLifetime/Mediator.Tests.TransientLifetime.csproj index 34a78d6..792a4ab 100644 --- a/test/Mediator.Tests.TransientLifetime/Mediator.Tests.TransientLifetime.csproj +++ b/test/Mediator.Tests.TransientLifetime/Mediator.Tests.TransientLifetime.csproj @@ -5,6 +5,7 @@ false true + true @@ -33,7 +34,7 @@ - + %(RecursiveDir)%(Filename)%(Extension) diff --git a/test/Mediator.Tests/Mediator.Tests.csproj b/test/Mediator.Tests/Mediator.Tests.csproj index 9ebba85..f32fde3 100644 --- a/test/Mediator.Tests/Mediator.Tests.csproj +++ b/test/Mediator.Tests/Mediator.Tests.csproj @@ -5,6 +5,7 @@ false true + true diff --git a/test/Mediator.Tests/SenderTests.cs b/test/Mediator.Tests/SenderTests.cs index 4f8bac6..a7b2189 100644 --- a/test/Mediator.Tests/SenderTests.cs +++ b/test/Mediator.Tests/SenderTests.cs @@ -3,8 +3,6 @@ namespace Mediator.Tests; - - public sealed class SenderTests { [Fact] diff --git a/test/Mediator.Tests/SmokeTests.FastLazy.cs b/test/Mediator.Tests/SmokeTests.FastLazy.cs new file mode 100644 index 0000000..f49de9d --- /dev/null +++ b/test/Mediator.Tests/SmokeTests.FastLazy.cs @@ -0,0 +1,50 @@ +using DICache = Mediator.Mediator.DICache; +using LazyDICache = Mediator.Mediator.FastLazyValue; + +namespace Mediator.Tests; + +public partial class SmokeTests +{ + [Theory] + [InlineData(1L << 2)] + [InlineData(1L << 3)] + [InlineData(1L << 4)] + [InlineData(1L << 5)] + [InlineData(1L << 6)] + [InlineData(1L << 7)] + [InlineData(1L << 8)] + [InlineData(1L << 9)] + [InlineData(1L << 10)] + public async Task Test_FastLazy(long concurrency) + { + var (_, mediator) = Fixture.GetMediator(); + var concrete = (Mediator)mediator; + + var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var threads = new Task<(DICache Cache, long State)>[concurrency]; + for (int i = 0; i < concurrency; i++) + { + threads[i] = Task.Run(Thread); + } + + start.SetResult(); + var values = await Task.WhenAll(threads).ConfigureAwait(false); + var states = values.Select(v => v.State).ToArray(); + + Assert.DoesNotContain(LazyDICache.INVALID, states); + Assert.Single(states.Where(s => s == LazyDICache.UNINIT)); + + var handlers = values.Select(v => v.Cache.Wrapper_For_Mediator_Tests_TestTypes_SomeRequest).ToArray(); + var handler = handlers[0]; + + Assert.All(handlers, h => Assert.Same(handler, h)); + + async Task<(DICache Cache, long State)> Thread() + { + await start.Task.ConfigureAwait(false); + + return concrete._diCacheLazy.ValueInstrumented; + } + } +} diff --git a/test/Mediator.Tests/SmokeTests.cs b/test/Mediator.Tests/SmokeTests.cs new file mode 100644 index 0000000..da53689 --- /dev/null +++ b/test/Mediator.Tests/SmokeTests.cs @@ -0,0 +1,48 @@ +using Mediator.Tests.TestTypes; + +namespace Mediator.Tests; + +public partial class SmokeTests +{ + [Theory] + [InlineData(1L << 2)] + [InlineData(1L << 3)] + [InlineData(1L << 4)] + [InlineData(1L << 5)] + [InlineData(1L << 6)] + [InlineData(1L << 7)] + [InlineData(1L << 8)] + [InlineData(1L << 9)] + [InlineData(1L << 10)] + public async Task Test_Concurrent_Messages(long concurrency) + { + var (_, mediator) = Fixture.GetMediator(); + var concrete = (Mediator)mediator; + + var id = Guid.NewGuid(); + var message = new SomeRequest(id); + + var start = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var threads = new Task[concurrency]; + for (int i = 0; i < concurrency; i++) + { + threads[i] = Task.Run(Thread); + } + + start.SetResult(); + await Task.WhenAll(threads).ConfigureAwait(false); + + async Task Thread() + { + await start.Task.ConfigureAwait(false); + + const int count = 1000; + for (int i = 0; i < count; i++) + { + var response = await concrete.Send(message).ConfigureAwait(false); + Assert.Equal(id, response.Id); + } + } + } +}