From a8e44f8a3908b49254979a3fefc4d728f0ac2184 Mon Sep 17 00:00:00 2001 From: Illia Filippov Date: Sun, 25 Oct 2026 00:45:01 +0200 Subject: [PATCH] feat(benchmarks): add DispatchR 2.3.1 comparison suite - 260 cases in 18 classes: Task, void, ValueTask and ValueTask void requests, streams, sequential events, catalog size, mixed traffic, short circuit, and direct-handler references. - Both dispatchers run scoped, since DispatchR registers IMediator as scoped only. - The probe now fails a class whose message fields do not carry its Completion parameter. --- RequestFlow.slnx | 1 + benchmarks/README.md | 25 +- .../Verification/BenchmarkVerification.cs | 21 ++ .../DispatchRCatalogSizeBenchmarks.cs | 50 +++ ...REventSequentialRetainedScopeBenchmarks.cs | 49 +++ ...ntSequentialScopePerOperationBenchmarks.cs | 66 ++++ .../DispatchRMixedTrafficBenchmarks.cs | 91 ++++++ .../DispatchRStreamRetainedScopeBenchmarks.cs | 52 +++ ...patchRStreamScopePerOperationBenchmarks.cs | 71 +++++ .../DispatchRTaskDirectHandlerBenchmarks.cs | 52 +++ .../DispatchRTaskRetainedScopeBenchmarks.cs | 49 +++ ...ispatchRTaskScopePerOperationBenchmarks.cs | 66 ++++ .../DispatchRValueDirectHandlerBenchmarks.cs | 52 +++ .../DispatchRValueRetainedScopeBenchmarks.cs | 49 +++ ...spatchRValueScopePerOperationBenchmarks.cs | 66 ++++ ...ValueVoidRequestRetainedScopeBenchmarks.cs | 49 +++ ...eVoidRequestScopePerOperationBenchmarks.cs | 66 ++++ ...atchRVoidRequestRetainedScopeBenchmarks.cs | 49 +++ ...RVoidRequestScopePerOperationBenchmarks.cs | 66 ++++ .../Program.cs | 10 + .../README.md | 125 ++++++++ .../RequestFlow.Benchmarks.DispatchR.csproj | 16 + .../Setup/DispatchRSetup.cs | 194 ++++++++++++ ...tchRShortCircuitRetainedScopeBenchmarks.cs | 46 +++ ...ShortCircuitScopePerOperationBenchmarks.cs | 60 ++++ .../DispatchRShortCircuitSetup.cs | 34 ++ .../ShortCircuit/ShortCircuitWorkloads.cs | 58 ++++ .../Verification/SuiteProbe.cs | 296 ++++++++++++++++++ .../Workloads/CatalogWorkloads.cs | 17 + .../Workloads/EventWorkloads.cs | 24 ++ .../Workloads/Messages.cs | 26 ++ .../Workloads/StreamWorkloads.cs | 41 +++ .../Workloads/TaskWorkloads.cs | 43 +++ .../Workloads/ValueVoidRequestWorkloads.cs | 43 +++ .../Workloads/ValueWorkloads.cs | 43 +++ .../Workloads/VoidRequestWorkloads.cs | 43 +++ .../packages.lock.json | 218 +++++++++++++ 37 files changed, 2322 insertions(+), 5 deletions(-) create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRCatalogSizeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRMixedTrafficBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskDirectHandlerBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueDirectHandlerBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Program.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/README.md create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/RequestFlow.Benchmarks.DispatchR.csproj create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Setup/DispatchRSetup.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitRetainedScopeBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitScopePerOperationBenchmarks.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitSetup.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/ShortCircuitWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Verification/SuiteProbe.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/CatalogWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/EventWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/Messages.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/StreamWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/TaskWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueVoidRequestWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/VoidRequestWorkloads.cs create mode 100644 benchmarks/RequestFlow.Benchmarks.DispatchR/packages.lock.json diff --git a/RequestFlow.slnx b/RequestFlow.slnx index e8fb28d..05948e5 100644 --- a/RequestFlow.slnx +++ b/RequestFlow.slnx @@ -3,6 +3,7 @@ + diff --git a/benchmarks/README.md b/benchmarks/README.md index f5efcdb..d40655f 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -1,17 +1,18 @@ # Benchmarks -Two BenchmarkDotNet suites, both on .NET 10: +Three BenchmarkDotNet suites, all on .NET 10: | Suite | Cases | Measures | | --- | --- | --- | | `RequestFlow.Benchmarks.MediatR` | 224 | RequestFlow against MediatR 12.5.0 in one executable, with matching workloads and service lifetimes. See the [MediatR comparison](RequestFlow.Benchmarks.MediatR/README.md). | +| `RequestFlow.Benchmarks.DispatchR` | 260 | RequestFlow against DispatchR.Mediator 2.3.1 in one executable, with matching workloads and service lifetimes. See the [DispatchR comparison](RequestFlow.Benchmarks.DispatchR/README.md). | | `RequestFlow.Benchmarks` | 34 | RequestFlow alone: Task vs ValueTask paths and stage chains. | - A case is one benchmark method with one parameter set. - Published results: [Benchmarks vs MediatR](../docs/benchmarks/mediatr/README.md), from the 2026-10-15 full run. - RequestFlow is built from the checked-out source, so record the commit when sharing results. - BenchmarkDotNet 0.15.8 and Microsoft DI 10.0.9. Package lock files pin the resolved dependencies. -- CI builds both suites but does not run them. +- CI builds all three suites but does not run them. ## Quick start @@ -63,7 +64,7 @@ dotnet run --project benchmarks/RequestFlow.Benchmarks -c Release -f net10.0 --n | `--filter` without `--smoke` or `--quick` | Runs `probe`, then the full profile: two launches, 15 to 50 warm-up and 60 to 100 measured iterations, 5% relative error target. | - Without arguments, `dotnet run` applies the MediatR suite's first launch profile, **Verify comparison**, so it only runs `probe`. -- The RequestFlow suite has no launch profile. Without arguments it runs `probe`, then shows BenchmarkDotNet's selection prompt. +- The DispatchR and RequestFlow suites have no launch profile. Without arguments they run `probe`, then show BenchmarkDotNet's selection prompt. ## Filters @@ -83,9 +84,22 @@ dotnet run --project benchmarks/RequestFlow.Benchmarks -c Release -f net10.0 --n | Short circuit | `*MediatRShortCircuit*` | 18 | | Direct-handler reference | `*MediatRTaskDirectHandler*` | 6 | +| DispatchR scenario | Filter | Cases | +| --- | --- | --- | +| Task requests | `*DispatchRTask*Scope*` | 36 | +| Void requests | `*DispatchRVoidRequest*` | 24 | +| ValueTask requests | `*DispatchRValueRetainedScope*` `*DispatchRValueScopePerOperation*` | 24 | +| ValueTask void requests | `*DispatchRValueVoidRequest*` | 24 | +| Streams | `*DispatchRStream*` | 72 | +| Sequential events | `*DispatchREventSequential*` | 16 | +| Catalog size | `*DispatchRCatalogSize*` | 18 | +| Mixed traffic | `*DispatchRMixedTraffic*` | 18 | +| Short circuit | `*DispatchRShortCircuit*` | 18 | +| Direct-handler references | `*DispatchR*DirectHandler*` | 10 | + ## Reports -- `--artifacts ` sets the output directory. Without it, reports go to `BenchmarkDotNet.Artifacts////` under the current directory, where `` is `mediatr` or `requestflow`. +- `--artifacts ` sets the output directory. Without it, reports go to `BenchmarkDotNet.Artifacts////` under the current directory, where `` is `mediatr`, `dispatchr` or `requestflow`. - Readable tables are the `results/*-report-github.md` files. `run-manifest.json` records the run status and host details. - Git ignores `BenchmarkDotNet.Artifacts/` and `benchmarks/artifacts/`. @@ -126,12 +140,13 @@ Use Visual Studio for exploration only. Publish from the command line. ## Projects -All five are in the root solution. +All six are in the root solution. | Project | Contents | | --- | --- | | `RequestFlow.Benchmarks` | Stage-chain measurements, Task vs ValueTask dispatch, and probes | | `RequestFlow.Benchmarks.MediatR` | MediatR fixtures, paired benchmarks, and probes. Does not depend on `RequestFlow.Benchmarks`. | +| `RequestFlow.Benchmarks.DispatchR` | DispatchR fixtures, paired benchmarks, and probes. Does not depend on `RequestFlow.Benchmarks`. | | `RequestFlow.Benchmarks.Harness` | BenchmarkDotNet configuration, runner, run manifest, and provenance records. Knows nothing about workloads. | | `RequestFlow.Benchmarks.Core` | Probe engine, trace names, provider hosts, shared workloads, and the short-circuit dependency graph | | `RequestFlow.Benchmarks.RequestFlowFixtures` | RequestFlow handlers, stages, registration, and message catalogs | diff --git a/benchmarks/RequestFlow.Benchmarks.Core/Verification/BenchmarkVerification.cs b/benchmarks/RequestFlow.Benchmarks.Core/Verification/BenchmarkVerification.cs index bd7ba07..1f6aa42 100644 --- a/benchmarks/RequestFlow.Benchmarks.Core/Verification/BenchmarkVerification.cs +++ b/benchmarks/RequestFlow.Benchmarks.Core/Verification/BenchmarkVerification.cs @@ -78,6 +78,7 @@ public static T[] Params(Type benchmarkType, string member) /// A class must retain one scope across calls. /// A class is exempt from scope checks. /// A Completion parameter fixes whether the returned awaitable is complete when the call returns. + /// Every message field of such a class must carry that Completion. /// public static BenchmarkProbeResult Run( Assembly assembly, @@ -230,6 +231,8 @@ private static BenchmarkObservation ObserveMethod( .Select(message => (Message: message, OriginalTrace: message.Trace)) .ToArray(); + VerifyMessageCompletion(type, method, parameters, [.. messages.Select(message => message.Message)]); + try { foreach (var message in messages) @@ -274,6 +277,24 @@ private static BenchmarkObservation ObserveMethod( } } + private static void VerifyMessageCompletion( + Type type, MethodInfo method, IReadOnlyDictionary parameters, WorkloadMessage[] messages) + { + if (!parameters.TryGetValue("Completion", out object? value) || value is not CompletionKind completion) + return; + + if (messages.Length == 0) + throw new InvalidOperationException( + $"'{type.Name}.{method.Name}' has a Completion parameter but no message field to check it against."); + + foreach (WorkloadMessage message in messages) + { + if (message.Completion != completion) + throw new InvalidOperationException( + $"'{type.Name}.{method.Name}' sends {message.GetType().Name} with Completion={message.Completion}, not {completion}."); + } + } + private static void VerifyCompletionShape(BenchmarkObservation observation, bool? completedOnReturn) { bool? synchronous = observation.Parameters.TryGetValue("Completion", out object? completion) && completion is CompletionKind kind diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRCatalogSizeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRCatalogSizeBenchmarks.cs new file mode 100644 index 0000000..15de28b --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRCatalogSizeBenchmarks.cs @@ -0,0 +1,50 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; +using RequestFlow.Benchmarks.RequestFlowFixtures.Catalog; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRCatalogSizeBenchmarks +{ + [Params(4, 16, 64)] + public int CatalogSize { get; set; } + + [Params(CompletionKind.Cached, CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private RuntimeTaskRequest _requestFlow1 = null!; + private DispatchRCatalogRequest _dispatchR1 = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.CatalogServices(CatalogSize))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.CatalogServices(CatalogSize))); + + _requestFlow1 = new RuntimeTaskRequest(Completion); + _dispatchR1 = new DispatchRCatalogRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlow1, CancellationToken.None); + + [Benchmark] + public Task DispatchR() + => _dispatchR.Service.Send(_dispatchR1, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..44ded0a --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialRetainedScopeBenchmarks.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchREventSequentialRetainedScopeBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(1, 5)] + public int HandlerCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private EventMessage _requestFlowRequest = null!; + private DispatchREvent _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.EventServices(HandlerCount, parallel: false))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.EventServices(HandlerCount))); + + _requestFlowRequest = new EventMessage(Completion); + _dispatchRRequest = new DispatchREvent(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => _requestFlow.Service.PublishAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public ValueTask DispatchR() + => _dispatchR.Service.Publish(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..06687c7 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchREventSequentialScopePerOperationBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchREventSequentialScopePerOperationBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(1, 5)] + public int HandlerCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private EventMessage _requestFlowRequest = null!; + private DispatchREvent _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.EventServices(HandlerCount, parallel: false))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.EventServices(HandlerCount))); + + _requestFlowRequest = new EventMessage(Completion); + _dispatchRRequest = new DispatchREvent(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async Task RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var publisher = scope.ServiceProvider + .GetRequiredService(); + + await publisher + .PublishAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async ValueTask DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var publisher = scope.ServiceProvider + .GetRequiredService(); + + await publisher + .Publish(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRMixedTrafficBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRMixedTrafficBenchmarks.cs new file mode 100644 index 0000000..7f6972e --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRMixedTrafficBenchmarks.cs @@ -0,0 +1,91 @@ +using System.Reflection; +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; +using RequestFlow.Benchmarks.RequestFlowFixtures.Catalog; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRMixedTrafficBenchmarks +{ + private static readonly MethodInfo BindDispatchRSendDefinition = + typeof(DispatchRMixedTrafficBenchmarks).GetMethod(nameof(BindDispatchRSend), BindingFlags.NonPublic | BindingFlags.Static)!; + + [Params(4, 16, 64)] + public int CatalogSize { get; set; } + + [Params(CompletionKind.Cached, CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private global::RequestFlow.IRequest[] _requestFlowRequests = null!; + private WorkloadMessage[] _dispatchRRequests = null!; + + private Func>[] _requestFlowSends = null!; + private Func>[] _dispatchRSends = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.CatalogServices(CatalogSize))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.CatalogServices(CatalogSize))); + + IReadOnlyList order = RuntimeCatalog.MixedTrafficOrder(CatalogSize); + + _requestFlowRequests = CreateRequests>(typeof(RuntimeTaskRequest<>), order); + _dispatchRRequests = CreateRequests(typeof(DispatchRCatalogRequest<>), order); + + _requestFlowSends = [.. _requestFlowRequests.Select(RequestFlowSend)]; + _dispatchRSends = [.. _dispatchRRequests.Select(DispatchRSend)]; + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true, OperationsPerInvoke = RuntimeCatalog.MixedTrafficBatch)] + public async Task RequestFlow() + { + int sum = 0; + + foreach (Func> send in _requestFlowSends) + sum += await send(_requestFlow.Service).ConfigureAwait(false); + + return sum; + } + + [Benchmark(OperationsPerInvoke = RuntimeCatalog.MixedTrafficBatch)] + public async Task DispatchR() + { + int sum = 0; + + foreach (Func> send in _dispatchRSends) + sum += await send(_dispatchR.Service).ConfigureAwait(false); + + return sum; + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } + + private TRequest[] CreateRequests(Type requestDefinition, IReadOnlyList order) + => [.. order.Select(tag => (TRequest)Activator.CreateInstance(requestDefinition.MakeGenericType(tag), Completion)!)]; + + private static Func> RequestFlowSend(global::RequestFlow.IRequest request) + => dispatcher => dispatcher.SendAsync(request, CancellationToken.None); + + // DispatchR resolves the handler from the compile-time request type, so each send is bound to its tag here. + private static Func> DispatchRSend(WorkloadMessage request) + => (Func>)BindDispatchRSendDefinition + .MakeGenericMethod(request.GetType().GetGenericArguments()[0]) + .Invoke(null, [request])!; + + private static Func> BindDispatchRSend(DispatchRCatalogRequest request) + => mediator => mediator.Send(request, CancellationToken.None); +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..cb82f90 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamRetainedScopeBenchmarks.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRStreamRetainedScopeBenchmarks +{ + [Params(1, 10, 100)] + public int ItemCount { get; set; } + + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private StreamRequest _requestFlowRequest = null!; + private DispatchRStreamRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.StreamServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.StreamServices(StageCount))); + + _requestFlowRequest = new StreamRequest(ItemCount, Completion); + _dispatchRRequest = new DispatchRStreamRequest(ItemCount, Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => BenchmarkWorkload.Consume(_requestFlow.Service.Stream(_requestFlowRequest, CancellationToken.None)); + + [Benchmark] + public Task DispatchR() + => BenchmarkWorkload.Consume(_dispatchR.Service.CreateStream(_dispatchRRequest, CancellationToken.None)); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..1ade200 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRStreamScopePerOperationBenchmarks.cs @@ -0,0 +1,71 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRStreamScopePerOperationBenchmarks +{ + [Params(1, 10, 100)] + public int ItemCount { get; set; } + + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private StreamRequest _requestFlowRequest = null!; + private DispatchRStreamRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.StreamServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.StreamServices(StageCount))); + + _requestFlowRequest = new StreamRequest(ItemCount, Completion); + _dispatchRRequest = new DispatchRStreamRequest(ItemCount, Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async Task RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var streamDispatcher = scope.ServiceProvider + .GetRequiredService(); + + var stream = streamDispatcher.Stream(_requestFlowRequest, CancellationToken.None); + + return await BenchmarkWorkload.Consume(stream).ConfigureAwait(false); + } + + [Benchmark] + public async Task DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + var stream = dispatcher.CreateStream(_dispatchRRequest, CancellationToken.None); + + return await BenchmarkWorkload.Consume(stream).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskDirectHandlerBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskDirectHandlerBenchmarks.cs new file mode 100644 index 0000000..4c12fe4 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskDirectHandlerBenchmarks.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.DirectHandler)] +public class DispatchRTaskDirectHandlerBenchmarks +{ + [Params(CompletionKind.Cached, CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + private readonly ScopeWitness _requestFlowWitness = new(); + private readonly ScopeWitness _dispatchRWitness = new(); + + private TaskRequestHandler _requestFlowHandler = null!; + private DispatchRTaskHandler _dispatchRHandler = null!; + + private TaskRequest _requestFlowRequest = null!; + private DispatchRTaskRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlowHandler = new TaskRequestHandler(_requestFlowWitness); + _dispatchRHandler = new DispatchRTaskHandler(_dispatchRWitness); + + _requestFlowRequest = new TaskRequest(Completion); + _dispatchRRequest = new DispatchRTaskRequest(Completion); + + RequestFlowDirectHandler().GetAwaiter().GetResult(); + DispatchRDirectHandler().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlowDirectHandler() + => _requestFlowHandler.HandleAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public Task DispatchRDirectHandler() + => _dispatchRHandler.Handle(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlowHandler.Dispose(); + _dispatchRHandler.Dispose(); + + _requestFlowWitness.Dispose(); + _dispatchRWitness.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..b8ebf26 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskRetainedScopeBenchmarks.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRTaskRetainedScopeBenchmarks +{ + [Params(CompletionKind.Cached, CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private TaskRequest _requestFlowRequest = null!; + private DispatchRTaskRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.TaskServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.TaskServices(StageCount))); + + _requestFlowRequest = new TaskRequest(Completion); + _dispatchRRequest = new DispatchRTaskRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public Task DispatchR() + => _dispatchR.Service.Send(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..23eb32b --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRTaskScopePerOperationBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRTaskScopePerOperationBenchmarks +{ + [Params(CompletionKind.Cached, CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private TaskRequest _requestFlowRequest = null!; + private DispatchRTaskRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.TaskServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.TaskServices(StageCount))); + + _requestFlowRequest = new TaskRequest(Completion); + _dispatchRRequest = new DispatchRTaskRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async Task RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .SendAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async Task DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .Send(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueDirectHandlerBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueDirectHandlerBenchmarks.cs new file mode 100644 index 0000000..56e40fd --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueDirectHandlerBenchmarks.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.DirectHandler)] +public class DispatchRValueDirectHandlerBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + private readonly ScopeWitness _requestFlowWitness = new(); + private readonly ScopeWitness _dispatchRWitness = new(); + + private ValueRequestHandler _requestFlowHandler = null!; + private DispatchRValueHandler _dispatchRHandler = null!; + + private ValueRequest _requestFlowRequest = null!; + private DispatchRValueRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlowHandler = new ValueRequestHandler(_requestFlowWitness); + _dispatchRHandler = new DispatchRValueHandler(_dispatchRWitness); + + _requestFlowRequest = new ValueRequest(Completion); + _dispatchRRequest = new DispatchRValueRequest(Completion); + + RequestFlowDirectHandler().GetAwaiter().GetResult(); + DispatchRDirectHandler().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public ValueTask RequestFlowDirectHandler() + => _requestFlowHandler.HandleAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public ValueTask DispatchRDirectHandler() + => _dispatchRHandler.Handle(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlowHandler.Dispose(); + _dispatchRHandler.Dispose(); + + _requestFlowWitness.Dispose(); + _dispatchRWitness.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..18db56a --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueRetainedScopeBenchmarks.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRValueRetainedScopeBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private ValueRequest _requestFlowRequest = null!; + private DispatchRValueRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.ValueServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.ValueServices(StageCount))); + + _requestFlowRequest = new ValueRequest(Completion); + _dispatchRRequest = new DispatchRValueRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public ValueTask RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public ValueTask DispatchR() + => _dispatchR.Service.Send(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..530eb17 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueScopePerOperationBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRValueScopePerOperationBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private ValueRequest _requestFlowRequest = null!; + private DispatchRValueRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.ValueServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.ValueServices(StageCount))); + + _requestFlowRequest = new ValueRequest(Completion); + _dispatchRRequest = new DispatchRValueRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async ValueTask RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .SendAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async ValueTask DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .Send(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..7a58eb0 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestRetainedScopeBenchmarks.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRValueVoidRequestRetainedScopeBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private ValueVoidRequest _requestFlowRequest = null!; + private DispatchRValueVoidRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.ValueVoidRequestServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.ValueVoidRequestServices(StageCount))); + + _requestFlowRequest = new ValueVoidRequest(Completion); + _dispatchRRequest = new DispatchRValueVoidRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public ValueTask RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public ValueTask DispatchR() + => _dispatchR.Service.Send(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..be7a030 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRValueVoidRequestScopePerOperationBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRValueVoidRequestScopePerOperationBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private ValueVoidRequest _requestFlowRequest = null!; + private DispatchRValueVoidRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.ValueVoidRequestServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.ValueVoidRequestServices(StageCount))); + + _requestFlowRequest = new ValueVoidRequest(Completion); + _dispatchRRequest = new DispatchRValueVoidRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async ValueTask RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + await dispatcher + .SendAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async ValueTask DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .Send(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..7468117 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestRetainedScopeBenchmarks.cs @@ -0,0 +1,49 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRVoidRequestRetainedScopeBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private TaskVoidRequest _requestFlowRequest = null!; + private DispatchRVoidRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.TaskVoidRequestServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.VoidRequestServices(StageCount))); + + _requestFlowRequest = new TaskVoidRequest(Completion); + _dispatchRRequest = new DispatchRVoidRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public Task DispatchR() + => _dispatchR.Service.Send(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..513dfc3 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Benchmarks/DispatchRVoidRequestScopePerOperationBenchmarks.cs @@ -0,0 +1,66 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRVoidRequestScopePerOperationBenchmarks +{ + [Params(CompletionKind.Synchronous, CompletionKind.Suspending)] + public CompletionKind Completion { get; set; } + + [Params(0, 1, 5)] + public int StageCount { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private TaskVoidRequest _requestFlowRequest = null!; + private DispatchRVoidRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(RequestFlowSetup.Build(RequestFlowSetup.TaskVoidRequestServices(StageCount))); + _dispatchR = new(DispatchRSetup.Build(DispatchRSetup.VoidRequestServices(StageCount))); + + _requestFlowRequest = new TaskVoidRequest(Completion); + _dispatchRRequest = new DispatchRVoidRequest(Completion); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async Task RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + await dispatcher + .SendAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async Task DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + + var dispatcher = scope.ServiceProvider + .GetRequiredService(); + + return await dispatcher + .Send(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Program.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Program.cs new file mode 100644 index 0000000..2da352f --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Program.cs @@ -0,0 +1,10 @@ +using RequestFlow.Benchmarks.Harness; + +namespace RequestFlow.Benchmarks.DispatchR; + +public static class Program +{ + public static int Main(string[] args) + => SuiteRunner.Run(typeof(Program).Assembly, "dispatchr", args, + SuiteProbe.Run, SuiteProbe.ReportLifetimes); +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/README.md b/benchmarks/RequestFlow.Benchmarks.DispatchR/README.md new file mode 100644 index 0000000..fe739a8 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/README.md @@ -0,0 +1,125 @@ +# RequestFlow vs DispatchR 2.3.1 + +- Compares RequestFlow with `DispatchR.Mediator 2.3.1` on .NET 10. +- Behavior notes on this page describe DispatchR.Mediator 2.3.1 only, not other releases. +- RequestFlow is not affiliated with or endorsed by DispatchR or its maintainers. +- Each class pairs a RequestFlow baseline with the matching native DispatchR operation. +- No timing results exist yet. Probes check behavior, not performance. + +## Matrix + +| Group | Completion | Work | Lifetime | +| --- | --- | --- | --- | +| Task response | Cached, newly completed, suspending | Returns 79; 0/1/5 stages | Retained scope and new scope per send | +| Task void request | Synchronous, suspending | 0/1/5 stages | Retained scope and new scope per send | +| ValueTask response | Synchronous, suspending | Returns 79; 0/1/5 stages | Retained scope and new scope per send | +| ValueTask void request | Synchronous, suspending | 0/1/5 stages | Retained scope and new scope per send | +| Stream | Synchronous, suspending | Fully consumes 1/10/100 items; 0/1/5 forwarding stages | Retained scope and new scope per enumeration | +| Sequential events | Synchronous, suspending | Awaits 1/5 handlers | Retained scope and new scope per publication | +| Direct Task handler reference | Cached, newly completed, suspending | One concrete handler call per row with a reused request | No dispatch or DI resolution in the method | +| Direct ValueTask handler reference | Synchronous, suspending | One concrete handler call per row with a reused request | No dispatch or DI resolution in the method | +| Catalog size | Cached, newly completed, suspending | One typed send within equal 4/16/64 handler catalogs | Retained scope | +| Mixed traffic | Cached, newly completed, suspending | 64 sends covering every request type of equal 4/16/64 handler catalogs; checksum 5,056 | Retained scope | +| Short circuit | Authorization reject, cache hit, full execution | Authorization, cache, transaction, handler | Retained scope and new scope per send | + +- Synchronous stages forward their native awaitable. Suspending request stages await it. Stream stages forward the sequence without adding their own iterator. +- Catalog-size rows always send `RuntimeTag1`, so the lookup key and call target stay fixed while the catalog grows. +- Mixed traffic varies both. It sends every request type equally often, in one fixed shuffled order from `RuntimeCatalog.MixedTrafficOrder` that both libraries share. +- DispatchR resolves the handler from the compile-time request type, so setup binds each mixed-traffic send to a typed delegate, and both libraries call through one delegate per send. RequestFlow's delegate calls its `IRequest` overload, which dispatches on the runtime type. +- This suite's RequestFlow mixed row is therefore not comparable with the MediatR suite's, which sends without a delegate. +- Direct-handler rows compare matching concrete handler calls. Their ratios describe workload cost and do not rank dispatchers. + +## Reading a result + +- `Mean` is the time of the whole operation the row describes. `Allocated` is all managed memory that operation allocates, including handler allocations such as a new Task or a stream iterator. +- `RetainedScope` setup builds providers, creates scopes, resolves dispatchers, constructs requests, and runs both methods once. Timed calls reuse those dispatchers, requests, and activated components, DispatchR's linked chain included. +- `ScopePerOperation` setup builds providers, constructs reusable requests and warms both methods. Each timed call creates a scope, resolves its dispatcher, awaits completion, and disposes the scope. `Allocated` includes the activated handlers, stages, shared disposable test dependency, and DI bookkeeping. +- Retained-scope request, void request, event, catalog-size and short-circuit methods return the dispatcher's awaitable directly, with no wrapper. +- New-scope Task response and short-circuit methods are `async Task` wrappers. Their results fall outside the runtime's cached tasks, so each operation allocates one extra `Task` on both sides. +- New-scope Task void request methods are `async Task` for RequestFlow and `async Task` for DispatchR. Neither wrapper allocates unless the operation suspends, because on .NET 10 `Task.FromResult` returns a cached task for the default value of a 1-byte struct such as `DispatchRVoidRequestResult`. +- New-scope ValueTask methods are `async ValueTask` or `async ValueTask` on both sides. New-scope event methods are `async Task` for RequestFlow and `async ValueTask` for DispatchR. These wrappers allocate only when the operation suspends. +- Mixed traffic sets `OperationsPerInvoke = 64`, so rows report cost per send. Each invocation also allocates one `Task` for the uncached checksum, so a cached mixed row never shows 0 B. +- Timed code keeps the null checks for optional tracing. Only probes create trace records. + +## Native API differences + +- Both dispatchers run scoped: DispatchR 2.3.1 registers `IMediator`, request handlers and pipeline behaviors as scoped, and RequestFlow's dispatchers are scoped by default. The probe checks both. The MediatR suite runs RequestFlow's dispatcher transient instead, to match MediatR. +- `IMediator.Send` resolves `IRequestHandler` from the scope on every send. The native registration is a scoped factory that resolves the handler and its pipeline behaviors as keyed services and links them, so each scope builds its whole chain on its first send and reuses it afterwards. +- The outermost pipeline behavior is registered as a keyed `IRequestHandler` and also returned by the factory, so disposing the scope disposes it twice. The short-circuit probe expects two authorization disposals. +- `ExcludeHandlers` applies to handlers only, and the assembly scan adds every pipeline behavior declared for a request type to that request's chain. Setup removes the keyed descriptors of pipeline behaviors a row did not select. +- Catalog rows exclude `DispatchRCatalogHandler<>` from the scan and register each closed `IRequestHandler, Task>` with plain `AddScoped`, so `Send` resolves the handler directly instead of through the keyed factory. +- DispatchR's package README says to use a generic `Task` or `ValueTask` result, because a bare `Task` or `ValueTask` may stop the handler from running when pipeline behaviors are present. DispatchR void request rows therefore return the empty benchmark-owned `DispatchRVoidRequestResult` struct. RequestFlow void handlers return plain `Task` or `ValueTask`. +- DispatchR void request handlers run the shared `BenchmarkWorkload.TaskUnitVoidRequest` or `ValueUnitVoidRequest`, the same work as RequestFlow's `TaskVoidRequest` and `ValueVoidRequest`. +- `Publish` resolves `IEnumerable>` on every call and runs the handlers in order, awaiting only those that did not complete synchronously. DispatchR has no parallel publisher, so this suite has no parallel event rows. +- The suite registers event handlers as scoped, in handler order, after native registration. DispatchR events return `ValueTask`; RequestFlow events return `Task`. +- Event rows cover successful publication of an exact event type. Failure, cancellation, and inheritance semantics are not compared. +- Timed stream rows always consume the full sequence. Probes also check early disposal. + +## Short-circuit requests + +The pipeline is authorization, cache, transaction, then handler. Authorization and cache can return without calling the next component. The transaction has a unit of work and change buffer; the handler has a repository, data session, and separate command buffer. + +The constructor probe checks this on the first send in a fresh scope: + +| Outcome | Components executed by both libraries | RequestFlow constructs | DispatchR 2.3.1 constructs | +| --- | --- | --- | --- | +| Authorization rejects | Authorization | Authorization and its dependencies | All pipeline behaviors, handler and their dependencies | +| Cache returns a result | Authorization and cache | Authorization, cache and their dependencies | All pipeline behaviors, handler and their dependencies | +| Request reaches the handler | All three stages and handler | All stages, handler and their dependencies | All pipeline behaviors, handler and their dependencies | + +- `DispatchRShortCircuitRetainedScopeBenchmarks` has 6 cases: three outcomes and two libraries, with 0-byte buffers. Setup sends the selected outcome once, and timed rows reuse the instances it created. +- `DispatchRShortCircuitScopePerOperationBenchmarks` has 12 cases: three outcomes, 0 or 4,096 bytes per dependency buffer, and two libraries. Rows include scope creation, dispatcher resolution, activation, send, and disposal. Provider construction stays outside timing. +- Read the two classes as separate scenarios. +- All components and dependencies are scoped. The 0-byte case keeps the dependency graph without allocating the buffers. +- With 4,096-byte buffers, RequestFlow constructs neither the transaction buffer nor the handler's buffer on either early return. DispatchR 2.3.1 constructs both, even though neither the transaction nor the handler runs. +- `probe` checks every case for constructor reachability, result, execution order, token propagation, scoped reuse, dependency sizes, and disposal. + +## Commands + +Run from the repository root. + +```powershell +dotnet restore benchmarks/RequestFlow.Benchmarks.DispatchR -p:RestoreLockedMode=true +dotnet build benchmarks/RequestFlow.Benchmarks.DispatchR -c Release --no-restore + +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- probe +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- lifetimes +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- --list flat +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- --smoke --filter '*' +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- --quick --filter '*DispatchRTaskRetainedScopeBenchmarks*' + +# Full run of all 260 cases into an unused directory. +dotnet run --project benchmarks/RequestFlow.Benchmarks.DispatchR -c Release -f net10.0 --no-build --no-restore -- --runtimes net10.0 --filter '*' --artifacts benchmarks/artifacts/YYYY-MM-DD-dispatchr-full +``` + +- `probe` runs both methods of every parameter set twice. It validates results and checks stage order, tokens, completion shape, component and dispatcher identities within and across scopes, scope reuse and disposal, stream checksums and early disposal, awaited event completion, and short-circuit construction. +- A passing probe ends with `Probe passed: checked 18 benchmark classes, 130 parameter sets and 260 cases.` +- `lifetimes` prints the DI descriptors for workload components, dispatchers, `IEventPublisher`, DispatchR's `IRequestHandler<,>`, `IStreamRequestHandler<,>` and `INotificationHandler<>` registrations, and RequestFlow stages, short-circuit graphs included. For each workload pair it also runs the identity and disposal checks, because descriptors alone do not prove lifetime parity. +- DispatchR keys each handler chain with a new `Guid`, so the `lifetimes` report numbers chains in registration order instead. +- `--list flat` lists all 36 benchmark methods without timing. +- `--smoke` runs each case once and `--quick` gives rough timings. Neither is publishable. +- To review one group on its own, select it with `--filter`, for example `'*Event*'` or `'*ShortCircuit*'`. + +## Publishing a run + +- Follow [Publishing a run](../README.md#publishing-a-run) in the benchmarks README before and after a full run. + +## Where each row lives + +Benchmark classes are in `Benchmarks/` and `ShortCircuit/`, one file per class. Class names below omit the `Benchmarks` suffix. RequestFlow fixtures live in `RequestFlow.Benchmarks.RequestFlowFixtures`. + +| Benchmark classes | DispatchR setup | DispatchR workload | RequestFlow setup and workload | +| --- | --- | --- | --- | +| `DispatchRTaskRetainedScope`, `DispatchRTaskScopePerOperation` | `DispatchRSetup.TaskServices` | `Workloads/TaskWorkloads.cs` | `RequestFlowSetup.TaskServices`, `Requests/TaskRequests.cs` | +| `DispatchRTaskDirectHandler` | None. Constructs `DispatchRTaskHandler` with `new`. | `Workloads/TaskWorkloads.cs` | None. Constructs `TaskRequestHandler` from `Requests/TaskRequests.cs` with `new`. | +| `DispatchRVoidRequestRetainedScope`, `DispatchRVoidRequestScopePerOperation` | `DispatchRSetup.VoidRequestServices` | `Workloads/VoidRequestWorkloads.cs` | `RequestFlowSetup.TaskVoidRequestServices`, `Requests/TaskVoidRequests.cs` | +| `DispatchRValueRetainedScope`, `DispatchRValueScopePerOperation` | `DispatchRSetup.ValueServices` | `Workloads/ValueWorkloads.cs` | `RequestFlowSetup.ValueServices`, `Requests/ValueRequests.cs` | +| `DispatchRValueDirectHandler` | None. Constructs `DispatchRValueHandler` with `new`. | `Workloads/ValueWorkloads.cs` | None. Constructs `ValueRequestHandler` from `Requests/ValueRequests.cs` with `new`. | +| `DispatchRValueVoidRequestRetainedScope`, `DispatchRValueVoidRequestScopePerOperation` | `DispatchRSetup.ValueVoidRequestServices` | `Workloads/ValueVoidRequestWorkloads.cs` | `RequestFlowSetup.ValueVoidRequestServices`, `Requests/ValueVoidRequests.cs` | +| `DispatchRStreamRetainedScope`, `DispatchRStreamScopePerOperation` | `DispatchRSetup.StreamServices` | `Workloads/StreamWorkloads.cs` | `RequestFlowSetup.StreamServices`, `Streams/Streams.cs` | +| `DispatchREventSequentialRetainedScope`, `DispatchREventSequentialScopePerOperation` | `DispatchRSetup.EventServices` | `Workloads/EventWorkloads.cs` | `RequestFlowSetup.EventServices`, `Events/Events.cs` | +| `DispatchRCatalogSize`, `DispatchRMixedTraffic` | `DispatchRSetup.CatalogServices` | `Workloads/CatalogWorkloads.cs` | `RequestFlowSetup.CatalogServices`, `Catalog/RuntimeTaskRequests.cs` | +| `DispatchRShortCircuitRetainedScope`, `DispatchRShortCircuitScopePerOperation` | `DispatchRShortCircuitSetup.Build` | `ShortCircuit/ShortCircuitWorkloads.cs` | `ShortCircuitServices.Build`, `ShortCircuit/ShortCircuitTaskWorkloads.cs` | + +- DispatchR requests, the event and `DispatchRVoidRequestResult` are in `Workloads/Messages.cs`, except the catalog request in `Workloads/CatalogWorkloads.cs` and the short-circuit request in `ShortCircuit/ShortCircuitWorkloads.cs`. +- `Verification/SuiteProbe.cs` holds the `probe` and `lifetimes` checks. diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/RequestFlow.Benchmarks.DispatchR.csproj b/benchmarks/RequestFlow.Benchmarks.DispatchR/RequestFlow.Benchmarks.DispatchR.csproj new file mode 100644 index 0000000..39ffc50 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/RequestFlow.Benchmarks.DispatchR.csproj @@ -0,0 +1,16 @@ + + + Exe + net10.0 + enable + + + + + + + + + + + diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Setup/DispatchRSetup.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Setup/DispatchRSetup.cs new file mode 100644 index 0000000..d75627b --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Setup/DispatchRSetup.cs @@ -0,0 +1,194 @@ +using DispatchR.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +internal static class DispatchRSetup +{ + private static readonly Type[] TaskStages = + [ + typeof(DispatchRTaskStage1), + typeof(DispatchRTaskStage2), + typeof(DispatchRTaskStage3), + typeof(DispatchRTaskStage4), + typeof(DispatchRTaskStage5) + ]; + + private static readonly Type[] ValueStages = + [ + typeof(DispatchRValueStage1), + typeof(DispatchRValueStage2), + typeof(DispatchRValueStage3), + typeof(DispatchRValueStage4), + typeof(DispatchRValueStage5) + ]; + + private static readonly Type[] VoidRequestStages = + [ + typeof(DispatchRVoidRequestStage1), + typeof(DispatchRVoidRequestStage2), + typeof(DispatchRVoidRequestStage3), + typeof(DispatchRVoidRequestStage4), + typeof(DispatchRVoidRequestStage5) + ]; + + private static readonly Type[] ValueVoidRequestStages = + [ + typeof(DispatchRValueVoidRequestStage1), + typeof(DispatchRValueVoidRequestStage2), + typeof(DispatchRValueVoidRequestStage3), + typeof(DispatchRValueVoidRequestStage4), + typeof(DispatchRValueVoidRequestStage5) + ]; + + private static readonly Type[] StreamStages = + [ + typeof(DispatchRStreamStage1), + typeof(DispatchRStreamStage2), + typeof(DispatchRStreamStage3), + typeof(DispatchRStreamStage4), + typeof(DispatchRStreamStage5) + ]; + + private static readonly Type[] PipelineContracts = + [ + typeof(global::DispatchR.Abstractions.Send.IPipelineBehavior<,>), + typeof(global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior<,>) + ]; + + private static readonly Type[] EventHandlers = + [ + typeof(DispatchREventHandler1), + typeof(DispatchREventHandler2), + typeof(DispatchREventHandler3), + typeof(DispatchREventHandler4), + typeof(DispatchREventHandler5) + ]; + + public static ServiceCollection TaskServices(int stageCount) + => Services(stageCount, TaskStages, typeof(DispatchRTaskHandler)); + + public static ServiceCollection ValueServices(int stageCount) + => Services(stageCount, ValueStages, typeof(DispatchRValueHandler)); + + public static ServiceCollection VoidRequestServices(int stageCount) + => Services(stageCount, VoidRequestStages, typeof(DispatchRVoidRequestHandler)); + + public static ServiceCollection ValueVoidRequestServices(int stageCount) + => Services(stageCount, ValueVoidRequestStages, typeof(DispatchRValueVoidRequestHandler)); + + public static ServiceCollection StreamServices(int stageCount) + { + CheckStageCount(stageCount); + + return BaseServices(StreamStages.Take(stageCount).ToArray(), notifications: false, typeof(DispatchRStreamHandler)); + } + + public static ServiceCollection EventServices(int handlerCount) + { + if (handlerCount is not (1 or 5)) + throw new ArgumentOutOfRangeException(nameof(handlerCount)); + + ServiceCollection services = BaseServices([], notifications: true); + + services.RemoveAll>(); + + foreach (Type handler in EventHandlers.Take(handlerCount)) + services.AddScoped( + typeof(global::DispatchR.Abstractions.Notification.INotificationHandler), + handler); + + return services; + } + + public static ServiceCollection CatalogServices(int catalogSize) + { + Type[] tags = RuntimeCatalog.Tags(catalogSize); + ServiceCollection services = BaseServices([], notifications: false); + + foreach (Type tag in tags) + { + Type request = typeof(DispatchRCatalogRequest<>).MakeGenericType(tag); + Type handler = typeof(DispatchRCatalogHandler<>).MakeGenericType(tag); + + services.AddScoped( + typeof(global::DispatchR.Abstractions.Send.IRequestHandler<,>).MakeGenericType( + request, + typeof(Task)), + handler); + } + + return services; + } + + public static void AddShortCircuit(IServiceCollection services) + => RegisterDispatchR( + services, + [ + typeof(DispatchRShortCircuitAuthorization), + typeof(DispatchRShortCircuitCache), + typeof(DispatchRShortCircuitTransaction) + ], + notifications: false, + typeof(DispatchRShortCircuitHandler)); + + public static ServiceProvider Build(IServiceCollection services) + => services.BuildServiceProvider(new ServiceProviderOptions + { + ValidateOnBuild = true, + ValidateScopes = true, + }); + + private static ServiceCollection Services(int stageCount, Type[] stages, Type handler) + { + CheckStageCount(stageCount); + + return BaseServices(stages.Take(stageCount).ToArray(), notifications: false, handler); + } + + private static ServiceCollection BaseServices(Type[] pipelineOrder, bool notifications, params Type[] handlers) + { + var services = new ServiceCollection(); + + services.AddScoped(); + RegisterDispatchR(services, pipelineOrder, notifications, handlers); + + return services; + } + + private static void RegisterDispatchR(IServiceCollection services, Type[] pipelineOrder, bool notifications, params Type[] handlers) + { + services.AddDispatchR(options => + { + options.Assemblies.Add(typeof(DispatchRTaskHandler).Assembly); + options.RegisterPipelines = pipelineOrder.Length != 0; + options.RegisterNotifications = notifications; + options.PipelineOrder = [.. pipelineOrder]; + options.ExcludeHandlers = [.. HandlerTypes().Where(type => !handlers.Contains(type))]; + }); + + // ExcludeHandlers does not apply to pipelines, so DispatchR chains every pipeline of the request type in the assembly. + ServiceDescriptor[] unselected = services.Where(descriptor => + descriptor.IsKeyedService && + descriptor.ServiceType == typeof(global::DispatchR.Abstractions.Send.IRequestHandler) && + !pipelineOrder.Contains(descriptor.KeyedImplementationType) && + !handlers.Contains(descriptor.KeyedImplementationType)) + .ToArray(); + + foreach (ServiceDescriptor descriptor in unselected) + services.Remove(descriptor); + } + + private static IEnumerable HandlerTypes() + => typeof(DispatchRTaskHandler).Assembly.GetTypes().Where(type => + type.IsClass && !type.IsAbstract && typeof(WorkloadComponent).IsAssignableFrom(type) && + !type.GetInterfaces().Any(contract => contract.IsGenericType && PipelineContracts.Contains(contract.GetGenericTypeDefinition()))); + + private static void CheckStageCount(int count) + { + if (count is not (0 or 1 or 5)) + throw new ArgumentOutOfRangeException(nameof(count)); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitRetainedScopeBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitRetainedScopeBenchmarks.cs new file mode 100644 index 0000000..6217c42 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitRetainedScopeBenchmarks.cs @@ -0,0 +1,46 @@ +using BenchmarkDotNet.Attributes; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures.ShortCircuit; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.RetainedScope)] +public class DispatchRShortCircuitRetainedScopeBenchmarks +{ + [Params(ShortCircuitOutcome.AuthorizationReject, ShortCircuitOutcome.CacheHit, ShortCircuitOutcome.FullExecution)] + public ShortCircuitOutcome Outcome { get; set; } + + private RetainedScopeHost _requestFlow = null!; + private RetainedScopeHost _dispatchR = null!; + + private ShortCircuitTaskRequest _requestFlowRequest = null!; + private DispatchRShortCircuitRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(ShortCircuitServices.Build(bufferBytes: 0, valueTask: false)); + _dispatchR = new(DispatchRShortCircuitSetup.Build(bufferBytes: 0)); + + _requestFlowRequest = new(Outcome); + _dispatchRRequest = new(Outcome); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public Task RequestFlow() + => _requestFlow.Service.SendAsync(_requestFlowRequest, CancellationToken.None); + + [Benchmark] + public Task DispatchR() + => _dispatchR.Service.Send(_dispatchRRequest, CancellationToken.None); + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitScopePerOperationBenchmarks.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitScopePerOperationBenchmarks.cs new file mode 100644 index 0000000..13f8b0d --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitScopePerOperationBenchmarks.cs @@ -0,0 +1,60 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures.ShortCircuit; + +namespace RequestFlow.Benchmarks.DispatchR; + +[BenchmarkScope(ScopeLifetime.ScopePerOperation)] +public class DispatchRShortCircuitScopePerOperationBenchmarks +{ + [Params(ShortCircuitOutcome.AuthorizationReject, ShortCircuitOutcome.CacheHit, ShortCircuitOutcome.FullExecution)] + public ShortCircuitOutcome Outcome { get; set; } + + [Params(0, 4096)] + public int BufferBytes { get; set; } + + private ScopePerOperationHost _requestFlow = null!; + private ScopePerOperationHost _dispatchR = null!; + + private ShortCircuitTaskRequest _requestFlowRequest = null!; + private DispatchRShortCircuitRequest _dispatchRRequest = null!; + + [GlobalSetup] + public void Setup() + { + _requestFlow = new(ShortCircuitServices.Build(BufferBytes, valueTask: false)); + _dispatchR = new(DispatchRShortCircuitSetup.Build(BufferBytes)); + + _requestFlowRequest = new(Outcome); + _dispatchRRequest = new(Outcome); + + RequestFlow().GetAwaiter().GetResult(); + DispatchR().GetAwaiter().GetResult(); + } + + [Benchmark(Baseline = true)] + public async Task RequestFlow() + { + using IServiceScope scope = _requestFlow.Provider.CreateScope(); + var dispatcher = scope.ServiceProvider.GetRequiredService(); + + return await dispatcher.SendAsync(_requestFlowRequest, CancellationToken.None).ConfigureAwait(false); + } + + [Benchmark] + public async Task DispatchR() + { + using IServiceScope scope = _dispatchR.Provider.CreateScope(); + var dispatcher = scope.ServiceProvider.GetRequiredService(); + + return await dispatcher.Send(_dispatchRRequest, CancellationToken.None).ConfigureAwait(false); + } + + [GlobalCleanup] + public void Cleanup() + { + _requestFlow.Dispose(); + _dispatchR.Dispose(); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitSetup.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitSetup.cs new file mode 100644 index 0000000..8dc5e5f --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/DispatchRShortCircuitSetup.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.RequestFlowFixtures.ShortCircuit; + +namespace RequestFlow.Benchmarks.DispatchR; + +public static class DispatchRShortCircuitSetup +{ + public static ServiceProvider Build(int bufferBytes, ConstructionTrace? trace = null) + => DispatchRSetup.Build(Services(bufferBytes, trace)); + + public static ServiceCollection Services(int bufferBytes, ConstructionTrace? trace = null) + { + var services = new ServiceCollection(); + + ShortCircuitWorkload.AddDependencies(services, bufferBytes, trace); + DispatchRSetup.AddShortCircuit(services); + + return services; + } + + public static void Verify() + { + ShortCircuitServices.VerifyRequestFlow(valueTask: false); + + ShortCircuitVerification.Verify( + "DispatchR", + (bytes, trace) => Build(bytes, trace), + outcome => new(outcome), + (dispatcher, message, token) => new(dispatcher.Send(message, token)), + DependencyResolution.EntireChain, + authorizationDisposals: 2); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/ShortCircuitWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/ShortCircuitWorkloads.cs new file mode 100644 index 0000000..e199759 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/ShortCircuit/ShortCircuitWorkloads.cs @@ -0,0 +1,58 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRShortCircuitRequest(ShortCircuitOutcome outcome) + : ShortCircuitMessage(outcome), global::DispatchR.Abstractions.Send.IRequest>; + +public sealed class DispatchRShortCircuitAuthorization(DependencyState state, AccessPolicy dependency) + : DependencyComponent(state, "authorization"), global::DispatchR.Abstractions.Send.IPipelineBehavior> +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public Task Handle(DispatchRShortCircuitRequest request, CancellationToken token) + { + Enter(request, token); + + return dependency.Allows(request) ? NextPipeline.Handle(request, token) : ShortCircuitWorkload.Rejected; + } +} + +public sealed class DispatchRShortCircuitCache(DependencyState state, CachePolicy dependency) + : DependencyComponent(state, "cache"), global::DispatchR.Abstractions.Send.IPipelineBehavior> +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public Task Handle(DispatchRShortCircuitRequest request, CancellationToken token) + { + Enter(request, token); + + return dependency.Contains(request) ? ShortCircuitWorkload.Cached : NextPipeline.Handle(request, token); + } +} + +public sealed class DispatchRShortCircuitTransaction(DependencyState state, UnitOfWork dependency) + : DependencyComponent(state, "transaction"), global::DispatchR.Abstractions.Send.IPipelineBehavior> +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public Task Handle(DispatchRShortCircuitRequest request, CancellationToken token) + { + Enter(request, token); + + return dependency.Capacity >= 0 ? NextPipeline.Handle(request, token) : throw new InvalidOperationException(); + } +} + +public sealed class DispatchRShortCircuitHandler(DependencyState state, Repository dependency) + : DependencyComponent(state, TraceNames.Handler), global::DispatchR.Abstractions.Send.IRequestHandler> +{ + public Task Handle(DispatchRShortCircuitRequest request, CancellationToken token) + { + Enter(request, token); + + Task result = dependency.Read(); + + return request.Trace is { } trace ? trace.Track(result) : result; + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Verification/SuiteProbe.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Verification/SuiteProbe.cs new file mode 100644 index 0000000..df11559 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Verification/SuiteProbe.cs @@ -0,0 +1,296 @@ +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using RequestFlow.Benchmarks.Core; +using RequestFlow.Benchmarks.Harness; +using RequestFlow.Benchmarks.RequestFlowFixtures; +using RequestFlow.Benchmarks.RequestFlowFixtures.Catalog; +using RequestFlow.Benchmarks.RequestFlowFixtures.ShortCircuit; +using static RequestFlow.Benchmarks.Core.BenchmarkVerification; +using static RequestFlow.Benchmarks.Core.RegistrationReport; +using static RequestFlow.Benchmarks.Core.ScopeLifetimeVerification; +using static RequestFlow.Benchmarks.Core.TraceAssertions; +using IMediator = global::DispatchR.IMediator; + +namespace RequestFlow.Benchmarks.DispatchR; + +public static class SuiteProbe +{ + private static readonly Type[] DispatchRServices = + [ + typeof(global::DispatchR.Abstractions.Send.IRequestHandler<,>), + typeof(global::DispatchR.Abstractions.Stream.IStreamRequestHandler<,>), + typeof(global::DispatchR.Abstractions.Notification.INotificationHandler<>), + ]; + + private static readonly ComparedLibrary DispatchRLibrary = new("DispatchR", DispatchRSetup.Build, DispatchRServices); + + private static readonly MethodInfo VerifyCatalogRequestDefinition = + typeof(SuiteProbe).GetMethod(nameof(VerifyCatalogRequest), BindingFlags.NonPublic | BindingFlags.Static)!; + + public static BenchmarkProbeResult Run() + { + VerifyLifetimes(report: false); + DispatchRShortCircuitSetup.Verify(); + + return VerifyBenchmarkMethods(); + } + + public static void ReportLifetimes() + { + VerifyLifetimes(report: true); + + ReportRegistrations( + "RequestFlow short circuit", ShortCircuitServices.Services(bufferBytes: 0), + typeof(global::RequestFlow.IRequestDispatcher), RequestFlowSetup.ReportedServices, RequestFlowSetup.StageContracts); + + ReportRegistrations( + "DispatchR short circuit", WithStableKeys(DispatchRShortCircuitSetup.Services(bufferBytes: 0)), + typeof(IMediator), DispatchRServices); + } + + private static void VerifyLifetimes(bool report) + { + VerifyRequestLifetimes(report); + VerifyStreamLifetimes(report); + VerifyEventLifetimes(report); + VerifyCatalogs(report); + } + + private static void VerifyRequestLifetimes(bool report) + { + foreach (int stages in Params(typeof(DispatchRTaskRetainedScopeBenchmarks), nameof(DispatchRTaskRetainedScopeBenchmarks.StageCount))) + foreach (CompletionKind completion in Params( + typeof(DispatchRTaskRetainedScopeBenchmarks), nameof(DispatchRTaskRetainedScopeBenchmarks.Completion))) + { + VerifyDispatchRPair( + $"Task, completion={completion}, stages={stages}", RequestNames(stages), report, + RequestFlowSetup.TaskServices(stages), + (dispatcher, trace, token) => ExpectResult(dispatcher.SendAsync(new TaskRequest(completion) { Trace = trace }, token)), + DispatchRSetup.TaskServices(stages), + (mediator, trace, token) => ExpectResult(mediator.Send(new DispatchRTaskRequest(completion) { Trace = trace }, token))); + } + + foreach (int stages in Params(typeof(DispatchRVoidRequestRetainedScopeBenchmarks), nameof(DispatchRVoidRequestRetainedScopeBenchmarks.StageCount))) + foreach (CompletionKind completion in Params( + typeof(DispatchRVoidRequestRetainedScopeBenchmarks), nameof(DispatchRVoidRequestRetainedScopeBenchmarks.Completion))) + { + VerifyDispatchRPair( + $"Void request, completion={completion}, stages={stages}", RequestNames(stages), report, + RequestFlowSetup.TaskVoidRequestServices(stages), + (dispatcher, trace, token) => dispatcher.SendAsync(new TaskVoidRequest(completion) { Trace = trace }, token), + DispatchRSetup.VoidRequestServices(stages), + (mediator, trace, token) => mediator.Send(new DispatchRVoidRequest(completion) { Trace = trace }, token)); + } + + foreach (int stages in Params(typeof(DispatchRValueRetainedScopeBenchmarks), nameof(DispatchRValueRetainedScopeBenchmarks.StageCount))) + foreach (CompletionKind completion in Params( + typeof(DispatchRValueRetainedScopeBenchmarks), nameof(DispatchRValueRetainedScopeBenchmarks.Completion))) + { + VerifyDispatchRPair( + $"Value, completion={completion}, stages={stages}", RequestNames(stages), report, + RequestFlowSetup.ValueServices(stages), + (dispatcher, trace, token) => ExpectResult(dispatcher.SendAsync(new ValueRequest(completion) { Trace = trace }, token)), + DispatchRSetup.ValueServices(stages), + (mediator, trace, token) => ExpectResult(mediator.Send(new DispatchRValueRequest(completion) { Trace = trace }, token))); + } + + foreach (int stages in Params(typeof(DispatchRValueVoidRequestRetainedScopeBenchmarks), nameof(DispatchRValueVoidRequestRetainedScopeBenchmarks.StageCount))) + foreach (CompletionKind completion in Params( + typeof(DispatchRValueVoidRequestRetainedScopeBenchmarks), nameof(DispatchRValueVoidRequestRetainedScopeBenchmarks.Completion))) + { + VerifyDispatchRPair( + $"Value void request, completion={completion}, stages={stages}", RequestNames(stages), report, + RequestFlowSetup.ValueVoidRequestServices(stages), + (dispatcher, trace, token) => dispatcher.SendAsync(new ValueVoidRequest(completion) { Trace = trace }, token).AsTask(), + DispatchRSetup.ValueVoidRequestServices(stages), + (mediator, trace, token) => mediator.Send(new DispatchRValueVoidRequest(completion) { Trace = trace }, token).AsTask()); + } + } + + private static void VerifyStreamLifetimes(bool report) + { + foreach (int stages in Params(typeof(DispatchRStreamRetainedScopeBenchmarks), nameof(DispatchRStreamRetainedScopeBenchmarks.StageCount))) + foreach (int count in Params(typeof(DispatchRStreamRetainedScopeBenchmarks), nameof(DispatchRStreamRetainedScopeBenchmarks.ItemCount))) + foreach (CompletionKind completion in Params(typeof(DispatchRStreamRetainedScopeBenchmarks), nameof(DispatchRStreamRetainedScopeBenchmarks.Completion))) + foreach (bool early in new[] { false, true }) + { + VerifyDispatchRPair( + $"Stream, items={count}, completion={completion}, stages={stages}, early={early}", + StreamNames(stages, count, early: early), report, + RequestFlowSetup.StreamServices(stages), + (dispatcher, trace, token) => ReadStream( + dispatcher.Stream(new StreamRequest(count, completion) { Trace = trace }, token), count, early, token), + DispatchRSetup.StreamServices(stages), + (mediator, trace, token) => ReadStream( + mediator.CreateStream(new DispatchRStreamRequest(count, completion) { Trace = trace }, token), count, early, token)); + } + } + + private static void VerifyEventLifetimes(bool report) + { + foreach (CompletionKind completion in Params( + typeof(DispatchREventSequentialRetainedScopeBenchmarks), nameof(DispatchREventSequentialRetainedScopeBenchmarks.Completion))) + foreach (int handlers in Params( + typeof(DispatchREventSequentialRetainedScopeBenchmarks), nameof(DispatchREventSequentialRetainedScopeBenchmarks.HandlerCount))) + { + VerifyDispatchRPair( + $"Event, completion={completion}, handlers={handlers}, parallel=False", EventNames(handlers), report, + RequestFlowSetup.EventServices(handlers, parallel: false), + (publisher, trace, token) => publisher.PublishAsync(new EventMessage(completion) { Trace = trace }, token), + DispatchRSetup.EventServices(handlers), + (mediator, trace, token) => mediator.Publish(new DispatchREvent(completion) { Trace = trace }, token).AsTask()); + } + } + + private static void VerifyCatalogs(bool report) + { + foreach (int count in Params(typeof(DispatchRCatalogSizeBenchmarks), nameof(DispatchRCatalogSizeBenchmarks.CatalogSize))) + { + ServiceCollection requestFlowServices = RequestFlowSetup.CatalogServices(count); + ServiceCollection dispatchRServices = DispatchRSetup.CatalogServices(count); + + Require( + GetComponentDescriptors(requestFlowServices).Count() == count, + $"RequestFlow catalog={count} registered the wrong number of handlers."); + + Require( + GetComponentDescriptors(dispatchRServices).Count() == count, + $"DispatchR catalog={count} registered the wrong number of handlers."); + + foreach (Type tag in RuntimeCatalog.Tags(count)) + { + VerifyCatalogRequestDefinition.MakeGenericMethod(tag).Invoke( + null, BindingFlags.DoNotWrapExceptions, binder: null, + [count, report, requestFlowServices, dispatchRServices], culture: null); + } + } + } + + // DispatchR resolves the handler from the compile-time request type, so each tag is bound here. + private static void VerifyCatalogRequest( + int count, bool report, ServiceCollection requestFlowServices, ServiceCollection dispatchRServices) + => VerifyDispatchRPair( + $"catalog request {typeof(TTag).Name}, catalog={count}", [TraceNames.Handler], report, + requestFlowServices, + (dispatcher, trace, token) => ExpectResult(dispatcher.SendAsync(new RuntimeTaskRequest { Trace = trace }, token)), + dispatchRServices, + (mediator, trace, token) => ExpectResult(mediator.Send(new DispatchRCatalogRequest { Trace = trace }, token))); + + // Runs the shared pair check; the report prints DispatchR's random chain keys as stable labels. + private static void VerifyDispatchRPair( + string label, + string[] expectedNames, + bool report, + ServiceCollection requestFlowServices, + Func requestFlow, + ServiceCollection dispatchRServices, + Func dispatchR) + where TRequestFlow : notnull + { + if (report) + { + ReportRegistrations( + "RequestFlow " + label, requestFlowServices, typeof(TRequestFlow), + RequestFlowSetup.ReportedServices, RequestFlowSetup.StageContracts); + ReportRegistrations("DispatchR " + label, WithStableKeys(dispatchRServices), typeof(IMediator), DispatchRServices); + } + + VerifyPair( + label, expectedNames, report: false, + RequestFlowSetup.Library, requestFlowServices, requestFlow, + DispatchRLibrary, dispatchRServices, dispatchR); + + if (report) + Console.WriteLine(" Verified dispatcher, component and dependency identities, tokens, traversal and disposal."); + } + + // DispatchR keys each handler chain with a new Guid, so the report numbers chains in registration order instead. + private static IServiceCollection WithStableKeys(IServiceCollection services) + { + var keys = new Dictionary(); + IServiceCollection report = new ServiceCollection(); + + foreach (ServiceDescriptor descriptor in services) + { + if (descriptor.IsKeyedService && descriptor.ServiceKey is Guid key && descriptor.KeyedImplementationType is Type implementation) + { + if (!keys.TryGetValue(key, out string? label)) + { + label = "chain " + (keys.Count + 1); + keys.Add(key, label); + } + + report.Add(new ServiceDescriptor(descriptor.ServiceType, label, implementation, descriptor.Lifetime)); + } + else + { + report.Add(descriptor); + } + } + + return report; + } + + private static BenchmarkProbeResult VerifyBenchmarkMethods() + { + var expectations = new Dictionary, BenchmarkExpectation>> + { + [typeof(DispatchRShortCircuitRetainedScopeBenchmarks)] = ShortCircuitWorkload.Expectation, + [typeof(DispatchRShortCircuitScopePerOperationBenchmarks)] = ShortCircuitWorkload.Expectation, + [typeof(DispatchRTaskRetainedScopeBenchmarks)] = RequestWithStages, + [typeof(DispatchRTaskScopePerOperationBenchmarks)] = RequestWithStages, + [typeof(DispatchRValueRetainedScopeBenchmarks)] = ValueRequestWithStages, + [typeof(DispatchRValueScopePerOperationBenchmarks)] = ValueRequestWithStages, + [typeof(DispatchRVoidRequestRetainedScopeBenchmarks)] = VoidRequestWithStages, + [typeof(DispatchRVoidRequestScopePerOperationBenchmarks)] = VoidRequestWithStages, + [typeof(DispatchRValueVoidRequestRetainedScopeBenchmarks)] = ValueVoidRequestWithStages, + [typeof(DispatchRValueVoidRequestScopePerOperationBenchmarks)] = ValueVoidRequestWithStages, + [typeof(DispatchRStreamRetainedScopeBenchmarks)] = StreamWithStages, + [typeof(DispatchRStreamScopePerOperationBenchmarks)] = StreamWithStages, + [typeof(DispatchREventSequentialRetainedScopeBenchmarks)] = SequentialEvent, + [typeof(DispatchREventSequentialScopePerOperationBenchmarks)] = SequentialEvent, + [typeof(DispatchRTaskDirectHandlerBenchmarks)] = _ => BenchmarkExpectation.Request(), + [typeof(DispatchRValueDirectHandlerBenchmarks)] = _ => BenchmarkExpectation.Request(), + [typeof(DispatchRCatalogSizeBenchmarks)] = _ => BenchmarkExpectation.Request(), + [typeof(DispatchRMixedTrafficBenchmarks)] = MixedTrafficExpectation, + }; + + return BenchmarkVerification.Run(typeof(Program).Assembly, expectations); + + static BenchmarkExpectation RequestWithStages(IReadOnlyDictionary parameters) + => BenchmarkExpectation.Request(stages: (int)parameters[nameof(DispatchRTaskRetainedScopeBenchmarks.StageCount)]!); + + static BenchmarkExpectation ValueRequestWithStages(IReadOnlyDictionary parameters) + => BenchmarkExpectation.Request(stages: (int)parameters[nameof(DispatchRValueRetainedScopeBenchmarks.StageCount)]!); + + static BenchmarkExpectation VoidRequestWithStages(IReadOnlyDictionary parameters) + => BenchmarkExpectation.VoidRequest(stages: (int)parameters[nameof(DispatchRVoidRequestRetainedScopeBenchmarks.StageCount)]!); + + static BenchmarkExpectation ValueVoidRequestWithStages(IReadOnlyDictionary parameters) + => BenchmarkExpectation.VoidRequest(stages: (int)parameters[nameof(DispatchRValueVoidRequestRetainedScopeBenchmarks.StageCount)]!); + + static BenchmarkExpectation StreamWithStages(IReadOnlyDictionary parameters) + => BenchmarkExpectation.Stream( + stages: (int)parameters[nameof(DispatchRStreamRetainedScopeBenchmarks.StageCount)]!, + count: (int)parameters[nameof(DispatchRStreamRetainedScopeBenchmarks.ItemCount)]!); + + static BenchmarkExpectation SequentialEvent(IReadOnlyDictionary parameters) + => BenchmarkExpectation.Event(handlers: (int)parameters[nameof(DispatchREventSequentialRetainedScopeBenchmarks.HandlerCount)]!); + } + + private static BenchmarkExpectation MixedTrafficExpectation(IReadOnlyDictionary parameters) + { + IReadOnlyList order = RuntimeCatalog.MixedTrafficOrder((int)parameters[nameof(DispatchRMixedTrafficBenchmarks.CatalogSize)]!); + + return new( + Names: [.. Enumerable.Repeat(TraceNames.Handler, RuntimeCatalog.MixedTrafficBatch)], + Result: RuntimeCatalog.MixedTrafficBatch * BenchmarkWorkload.Expected, + HandlerTypes: new Dictionary + { + [nameof(DispatchRMixedTrafficBenchmarks.RequestFlow)] = + [.. order.Select(tag => typeof(RuntimeTaskHandler<>).MakeGenericType(tag))], + [nameof(DispatchRMixedTrafficBenchmarks.DispatchR)] = + [.. order.Select(tag => typeof(DispatchRCatalogHandler<>).MakeGenericType(tag))], + }); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/CatalogWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/CatalogWorkloads.cs new file mode 100644 index 0000000..f9c94d4 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/CatalogWorkloads.cs @@ -0,0 +1,17 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRCatalogRequest(CompletionKind completion = CompletionKind.Cached) + : WorkloadMessage(completion), global::DispatchR.Abstractions.Send.IRequest, Task>; + +public sealed class DispatchRCatalogHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Send.IRequestHandler, Task> +{ + public Task Handle(DispatchRCatalogRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return BenchmarkWorkload.TaskResponse(request, cancellationToken); + } +} diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/EventWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/EventWorkloads.cs new file mode 100644 index 0000000..a983c44 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/EventWorkloads.cs @@ -0,0 +1,24 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public abstract class DispatchREventHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Notification.INotificationHandler where TSlot : IStageSlot +{ + public ValueTask Handle(DispatchREvent request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.HandlerName, cancellationToken); + + return RuntimeTracing.EventValue(request, this, StageSlot.HandlerName, cancellationToken); + } +} + +public sealed class DispatchREventHandler1(ScopeWitness scope) : DispatchREventHandler(scope); + +public sealed class DispatchREventHandler2(ScopeWitness scope) : DispatchREventHandler(scope); + +public sealed class DispatchREventHandler3(ScopeWitness scope) : DispatchREventHandler(scope); + +public sealed class DispatchREventHandler4(ScopeWitness scope) : DispatchREventHandler(scope); + +public sealed class DispatchREventHandler5(ScopeWitness scope) : DispatchREventHandler(scope); diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/Messages.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/Messages.cs new file mode 100644 index 0000000..092a0f4 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/Messages.cs @@ -0,0 +1,26 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRTaskRequest(CompletionKind completion = CompletionKind.Cached, int value = 42) + : WorkloadMessage(completion, value), global::DispatchR.Abstractions.Send.IRequest>; + +public sealed class DispatchRValueRequest(CompletionKind completion = CompletionKind.Cached, int value = 42) + : WorkloadMessage(completion, value), global::DispatchR.Abstractions.Send.IRequest>; + +public sealed class DispatchRVoidRequest(CompletionKind completion = CompletionKind.Cached) + : WorkloadMessage(completion), global::DispatchR.Abstractions.Send.IRequest>; + +public sealed class DispatchRValueVoidRequest(CompletionKind completion = CompletionKind.Cached) + : WorkloadMessage(completion), global::DispatchR.Abstractions.Send.IRequest>; + +public readonly record struct DispatchRVoidRequestResult; + +public sealed class DispatchRStreamRequest(int count = 10, CompletionKind completion = CompletionKind.Synchronous) + : WorkloadMessage(completion), global::DispatchR.Abstractions.Stream.IStreamRequest +{ + public int Count { get; } = count; +} + +public sealed class DispatchREvent(CompletionKind completion = CompletionKind.Cached) + : WorkloadMessage(completion), global::DispatchR.Abstractions.Notification.INotification; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/StreamWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/StreamWorkloads.cs new file mode 100644 index 0000000..e3749b1 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/StreamWorkloads.cs @@ -0,0 +1,41 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRStreamHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Stream.IStreamRequestHandler +{ + public IAsyncEnumerable Handle(DispatchRStreamRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return RuntimeTracing.Stream(request, request.Count, this, cancellationToken); + } +} + +public abstract class DispatchRStreamStage(ScopeWitness scope) : WorkloadComponent(scope) where TSlot : IStageSlot +{ + public required global::DispatchR.Abstractions.Stream.IStreamRequestHandler NextPipeline { get; set; } + + public IAsyncEnumerable Handle(DispatchRStreamRequest request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.StageName, cancellationToken); + + return NextPipeline.Handle(request, cancellationToken); + } +} + +public sealed class DispatchRStreamStage1(ScopeWitness scope) + : DispatchRStreamStage(scope), global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior; + +public sealed class DispatchRStreamStage2(ScopeWitness scope) + : DispatchRStreamStage(scope), global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior; + +public sealed class DispatchRStreamStage3(ScopeWitness scope) + : DispatchRStreamStage(scope), global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior; + +public sealed class DispatchRStreamStage4(ScopeWitness scope) + : DispatchRStreamStage(scope), global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior; + +public sealed class DispatchRStreamStage5(ScopeWitness scope) + : DispatchRStreamStage(scope), global::DispatchR.Abstractions.Stream.IStreamPipelineBehavior; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/TaskWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/TaskWorkloads.cs new file mode 100644 index 0000000..4a876f7 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/TaskWorkloads.cs @@ -0,0 +1,43 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRTaskHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Send.IRequestHandler> +{ + public Task Handle(DispatchRTaskRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return BenchmarkWorkload.TaskResponse(request, cancellationToken); + } +} + +public abstract class DispatchRTaskStage(ScopeWitness scope) : WorkloadComponent(scope) where TSlot : IStageSlot +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public Task Handle(DispatchRTaskRequest request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.StageName, cancellationToken); + + Task result = NextPipeline.Handle(request, cancellationToken); + + return request.Completion == CompletionKind.Suspending ? BenchmarkWorkload.AwaitTask(result) : result; + } +} + +public sealed class DispatchRTaskStage1(ScopeWitness scope) + : DispatchRTaskStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRTaskStage2(ScopeWitness scope) + : DispatchRTaskStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRTaskStage3(ScopeWitness scope) + : DispatchRTaskStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRTaskStage4(ScopeWitness scope) + : DispatchRTaskStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRTaskStage5(ScopeWitness scope) + : DispatchRTaskStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueVoidRequestWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueVoidRequestWorkloads.cs new file mode 100644 index 0000000..9048e47 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueVoidRequestWorkloads.cs @@ -0,0 +1,43 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRValueVoidRequestHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Send.IRequestHandler> +{ + public ValueTask Handle(DispatchRValueVoidRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return BenchmarkWorkload.ValueUnitVoidRequest(request, cancellationToken); + } +} + +public abstract class DispatchRValueVoidRequestStage(ScopeWitness scope) : WorkloadComponent(scope) where TSlot : IStageSlot +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public ValueTask Handle(DispatchRValueVoidRequest request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.StageName, cancellationToken); + + ValueTask result = NextPipeline.Handle(request, cancellationToken); + + return request.Completion == CompletionKind.Suspending ? BenchmarkWorkload.AwaitValue(result) : result; + } +} + +public sealed class DispatchRValueVoidRequestStage1(ScopeWitness scope) + : DispatchRValueVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueVoidRequestStage2(ScopeWitness scope) + : DispatchRValueVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueVoidRequestStage3(ScopeWitness scope) + : DispatchRValueVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueVoidRequestStage4(ScopeWitness scope) + : DispatchRValueVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueVoidRequestStage5(ScopeWitness scope) + : DispatchRValueVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueWorkloads.cs new file mode 100644 index 0000000..091055b --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/ValueWorkloads.cs @@ -0,0 +1,43 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRValueHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Send.IRequestHandler> +{ + public ValueTask Handle(DispatchRValueRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return BenchmarkWorkload.ValueResponse(request, cancellationToken); + } +} + +public abstract class DispatchRValueStage(ScopeWitness scope) : WorkloadComponent(scope) where TSlot : IStageSlot +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public ValueTask Handle(DispatchRValueRequest request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.StageName, cancellationToken); + + ValueTask result = NextPipeline.Handle(request, cancellationToken); + + return request.Completion == CompletionKind.Suspending ? BenchmarkWorkload.AwaitValue(result) : result; + } +} + +public sealed class DispatchRValueStage1(ScopeWitness scope) + : DispatchRValueStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueStage2(ScopeWitness scope) + : DispatchRValueStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueStage3(ScopeWitness scope) + : DispatchRValueStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueStage4(ScopeWitness scope) + : DispatchRValueStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRValueStage5(ScopeWitness scope) + : DispatchRValueStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/VoidRequestWorkloads.cs b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/VoidRequestWorkloads.cs new file mode 100644 index 0000000..5dad2fd --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/Workloads/VoidRequestWorkloads.cs @@ -0,0 +1,43 @@ +using RequestFlow.Benchmarks.Core; + +namespace RequestFlow.Benchmarks.DispatchR; + +public sealed class DispatchRVoidRequestHandler(ScopeWitness scope) + : WorkloadComponent(scope), global::DispatchR.Abstractions.Send.IRequestHandler> +{ + public Task Handle(DispatchRVoidRequest request, CancellationToken cancellationToken) + { + Observe(request, TraceNames.Handler, cancellationToken); + + return BenchmarkWorkload.TaskUnitVoidRequest(request, cancellationToken); + } +} + +public abstract class DispatchRVoidRequestStage(ScopeWitness scope) : WorkloadComponent(scope) where TSlot : IStageSlot +{ + public required global::DispatchR.Abstractions.Send.IRequestHandler> NextPipeline { get; set; } + + public Task Handle(DispatchRVoidRequest request, CancellationToken cancellationToken) + { + Observe(request, StageSlot.StageName, cancellationToken); + + Task result = NextPipeline.Handle(request, cancellationToken); + + return request.Completion == CompletionKind.Suspending ? BenchmarkWorkload.AwaitTask(result) : result; + } +} + +public sealed class DispatchRVoidRequestStage1(ScopeWitness scope) + : DispatchRVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRVoidRequestStage2(ScopeWitness scope) + : DispatchRVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRVoidRequestStage3(ScopeWitness scope) + : DispatchRVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRVoidRequestStage4(ScopeWitness scope) + : DispatchRVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; + +public sealed class DispatchRVoidRequestStage5(ScopeWitness scope) + : DispatchRVoidRequestStage(scope), global::DispatchR.Abstractions.Send.IPipelineBehavior>; diff --git a/benchmarks/RequestFlow.Benchmarks.DispatchR/packages.lock.json b/benchmarks/RequestFlow.Benchmarks.DispatchR/packages.lock.json new file mode 100644 index 0000000..f8f5cc4 --- /dev/null +++ b/benchmarks/RequestFlow.Benchmarks.DispatchR/packages.lock.json @@ -0,0 +1,218 @@ +{ + "version": 1, + "dependencies": { + "net10.0": { + "DispatchR.Mediator": { + "type": "Direct", + "requested": "[2.3.1, )", + "resolved": "2.3.1", + "contentHash": "Fmn8red8UZY2NeYnfgOkyDaVqWJGgC0tCO9t237xQoATKmKGUxrLI+5ky1j2V/8rKZys+eVOB2+RNqz0xW/Iug==", + "dependencies": { + "DispatchR.Mediator.Abstractions": "2.3.1", + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Direct", + "requested": "[10.0.9, )", + "resolved": "10.0.9", + "contentHash": "NijozhERJDIaJ4k5TSMy1jOi0cSC2HfkvRD/Sl+kGSSKgVbFnF4GxgtMN/MrzHB8D1JxIrD4xSer9Blh9v3axQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.9" + } + }, + "BenchmarkDotNet": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "paCfrWxSeHqn3rUZc0spYXVFnHCF0nzRhG0nOLnyTjZYs8spsimBaaNmb3vwqvALKIplbYq/TF393vYiYSnh/Q==", + "dependencies": { + "BenchmarkDotNet.Annotations": "0.15.8", + "CommandLineParser": "2.9.1", + "Gee.External.Capstone": "2.3.0", + "Iced": "1.21.0", + "Microsoft.CodeAnalysis.CSharp": "4.14.0", + "Microsoft.Diagnostics.Runtime": "3.1.512801", + "Microsoft.Diagnostics.Tracing.TraceEvent": "3.1.21", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Perfolizer": "[0.6.1]", + "System.Management": "9.0.5" + } + }, + "BenchmarkDotNet.Annotations": { + "type": "Transitive", + "resolved": "0.15.8", + "contentHash": "hfucY0ycAsB0SsoaZcaAp9oq5wlWBJcylvEJb9pmvdYUx6PD6S4mDiYnZWjdjAlLhIpe/xtGCwzORfzAzPqvzA==" + }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "DispatchR.Mediator.Abstractions": { + "type": "Transitive", + "resolved": "2.3.1", + "contentHash": "If570gWOhtDXhDsWoYn+sLCZ447pEzsiunaC2B9GfRbjne+G6NnwsDUYU+oXpA11n1ufnNYwg18wx48NZGrfMw==" + }, + "Gee.External.Capstone": { + "type": "Transitive", + "resolved": "2.3.0", + "contentHash": "2ap/rYmjtzCOT8hxrnEW/QeiOt+paD8iRrIcdKX0cxVwWLFa1e+JDBNeECakmccXrSFeBQuu5AV8SNkipFMMMw==" + }, + "Iced": { + "type": "Transitive", + "resolved": "1.21.0", + "contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.11.0", + "contentHash": "v/EW3UE8/lbEYHoC2Qq7AR/DnmvpgdtAMndfQNmpuIMx/Mto8L5JnuCfdBYtgvalQOtfNCnxFejxuRrryvUTsg==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "PC3tuwZYnC+idaPuoC/AZpEdwrtX7qFpmnrfQkgobGIWiYmGi5MCRtl5mx6QrfMGQpK78X2lfIEoZDLg/qnuHg==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.14.0", + "contentHash": "568a6wcTivauIhbeWcCwfWwIn7UV7MeHEBvFB2uzGIpM2OhJ4eM/FZ8KS0yhPoNxnSpjGzz7x7CIjTxhslojQA==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.11.0", + "Microsoft.CodeAnalysis.Common": "[4.14.0]" + } + }, + "Microsoft.Diagnostics.NETCore.Client": { + "type": "Transitive", + "resolved": "0.2.510501", + "contentHash": "juoqJYMDs+lRrrZyOkXXMImJHneCF23cuvO4waFRd2Ds7j+ZuGIPbJm0Y/zz34BdeaGiiwGWraMUlln05W1PCQ==", + "dependencies": { + "Microsoft.Extensions.Logging": "6.0.0" + } + }, + "Microsoft.Diagnostics.Runtime": { + "type": "Transitive", + "resolved": "3.1.512801", + "contentHash": "0lMUDr2oxNZa28D6NH5BuSQEe5T9tZziIkvkD44YkkCGQXPJqvFjLq5ZQq1hYLl3RjQJrY+hR0jFgap+EWPDTw==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.410101" + } + }, + "Microsoft.Diagnostics.Tracing.TraceEvent": { + "type": "Transitive", + "resolved": "3.1.21", + "contentHash": "/OrJFKaojSR6TkUKtwh8/qA9XWNtxLrXMqvEb89dBSKCWjaGVTbKMYodIUgF5deCEtmd6GXuRerciXGl5bhZ7Q==", + "dependencies": { + "Microsoft.Diagnostics.NETCore.Client": "0.2.510501", + "System.Reflection.TypeExtensions": "4.7.0" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "10.0.9", + "contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/HggWBbTwy8TgebGSX5DBZ24ndhzi93sHUBDvP1IxbZD7FDokYzdAr6+vbWGjw2XAfR2EJ1sfKUotpjHnFWPxA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "dzXN0+V1AyjOe2xcJ86Qbo233KHuLEY0njf/P2Kw8SfJU+d45HNS2ctJdnEnrWbM9Ye2eFgaC5Mj9otRMU6IsQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Primitives": "6.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "9+PnzmQFfEFNR9J2aDTfJGGupShHjOuGw4VUv+JB044biSHrnmCIMD+mJHmb2H7YryrfBEXDurxQ47gJZdCKNQ==" + }, + "Perfolizer": { + "type": "Transitive", + "resolved": "0.6.1", + "contentHash": "CR1QmWg4XYBd1Pb7WseP+sDmV8nGPwvmowKynExTqr3OuckIGVMhvmN4LC5PGzfXqDlR295+hz/T7syA1CxEqA==", + "dependencies": { + "Pragmastat": "3.2.4" + } + }, + "Pragmastat": { + "type": "Transitive", + "resolved": "3.2.4", + "contentHash": "I5qFifWw/gaTQT52MhzjZpkm/JPlfjSeO/DTZJjO7+hTKI+0aGRgOgZ3NN6D96dDuuqbIAZSeA5RimtHjqrA2A==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "cuzLM2MWutf9ZBEMPYYfd0DXwYdvntp7VCT6a/wvbKCa2ZuvGmW74xi+YBa2mrfEieAXqM4TNKlMmSnfAfpUoQ==" + }, + "System.Management": { + "type": "Transitive", + "resolved": "9.0.5", + "contentHash": "n6o9PZm9p25+zAzC3/48K0oHnaPKTInRrxqFq1fi/5TPbMLjuoCm/h//mS3cUmSy+9AO1Z+qsC/Ilt/ZFatv5Q==", + "dependencies": { + "System.CodeDom": "9.0.5" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA==" + }, + "requestflow": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "[8.0.1, )", + "RequestFlow.Abstractions": "[1.0.0, )" + } + }, + "requestflow.abstractions": { + "type": "Project" + }, + "requestflow.benchmarks.core": { + "type": "Project", + "dependencies": { + "BenchmarkDotNet": "[0.15.8, )", + "Microsoft.Extensions.DependencyInjection": "[10.0.9, )", + "RequestFlow.Benchmarks.Harness": "[1.0.0, )" + } + }, + "requestflow.benchmarks.harness": { + "type": "Project", + "dependencies": { + "BenchmarkDotNet": "[0.15.8, )" + } + }, + "requestflow.benchmarks.requestflowfixtures": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[10.0.9, )", + "RequestFlow": "[1.0.0, )", + "RequestFlow.Benchmarks.Core": "[1.0.0, )" + } + } + } + } +} \ No newline at end of file