From 8990749d910d6c27af8059d03c8b91a7146cf15a Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Mon, 11 May 2026 12:39:37 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs-benchmarks):=20=E8=A1=A5=E9=BD=90=20R?= =?UTF-8?q?equestStartup=20=E7=9A=84=20Mediator=20=E5=AF=B9=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 RequestStartupBenchmarks 的 Mediator 初始化与冷启动 benchmark 路径 - 更新 request 与 handler 契约以复用单文件内的 Mediator concrete host 对照 --- .../Messaging/RequestStartupBenchmarks.cs | 53 ++- .../Cqrs/CqrsDispatcherCacheTests.cs | 326 +++++++++++++++++- 2 files changed, 365 insertions(+), 14 deletions(-) diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs index b37fa20c..2c685fed 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -17,11 +17,12 @@ using GFramework.Cqrs.Abstractions.Cqrs; using MediatR; using Microsoft.Extensions.DependencyInjection; using ILogger = GFramework.Core.Abstractions.Logging.ILogger; +using GeneratedMediator = Mediator.Mediator; namespace GFramework.Cqrs.Benchmarks.Messaging; /// -/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线。 +/// 对比 request 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次分发成本。 /// [Config(typeof(Config))] public class RequestStartupBenchmarks @@ -31,7 +32,9 @@ public class RequestStartupBenchmarks private MicrosoftDiContainer _container = null!; private ServiceProvider _serviceProvider = null!; + private ServiceProvider _mediatorServiceProvider = null!; private IMediator _mediatr = null!; + private GeneratedMediator _mediator = null!; private ICqrsRuntime _runtime = null!; /// @@ -63,6 +66,8 @@ public class RequestStartupBenchmarks _serviceProvider = CreateMediatRServiceProvider(); _mediatr = _serviceProvider.GetRequiredService(); + _mediatorServiceProvider = CreateMediatorServiceProvider(); + _mediator = _mediatorServiceProvider.GetRequiredService(); _container = CreateGFrameworkContainer(); _runtime = CreateGFrameworkRuntime(_container); } @@ -86,7 +91,7 @@ public class RequestStartupBenchmarks [GlobalCleanup] public void Cleanup() { - BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider, _mediatorServiceProvider); } /// @@ -109,6 +114,16 @@ public class RequestStartupBenchmarks return _runtime; } + /// + /// 返回已构建宿主中的 `Mediator` concrete mediator,作为 source-generated 对照组的初始化句柄。 + /// + [Benchmark] + [BenchmarkCategory("Initialization")] + public GeneratedMediator Initialization_Mediator() + { + return _mediator; + } + /// /// 在新宿主上首次发送 request,作为 MediatR 的 cold-start baseline。 /// @@ -133,6 +148,18 @@ public class RequestStartupBenchmarks return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false); } + /// + /// 在新的 `Mediator` 宿主上首次发送 request,量化 source-generated concrete path 的 cold-start 成本。 + /// + [Benchmark] + [BenchmarkCategory("ColdStart")] + public async ValueTask ColdStart_Mediator() + { + using var serviceProvider = CreateMediatorServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false); + } + /// /// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。 /// @@ -170,6 +197,14 @@ public class RequestStartupBenchmarks ServiceLifetime.Transient); } + /// + /// 构建只承载当前 benchmark request 的最小 `Mediator` 对照宿主。 + /// + private static ServiceProvider CreateMediatorServiceProvider() + { + return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null); + } + /// /// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。 /// @@ -188,6 +223,7 @@ public class RequestStartupBenchmarks /// 请求标识。 public sealed record BenchmarkRequest(Guid Id) : GFramework.Cqrs.Abstractions.Cqrs.IRequest, + Mediator.IRequest, MediatR.IRequest; /// @@ -197,10 +233,11 @@ public class RequestStartupBenchmarks public sealed record BenchmarkResponse(Guid Id); /// - /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。 + /// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。 /// public sealed class BenchmarkRequestHandler : GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, + Mediator.IRequestHandler, MediatR.IRequestHandler { /// @@ -211,6 +248,16 @@ public class RequestStartupBenchmarks return ValueTask.FromResult(new BenchmarkResponse(request.Id)); } + /// + /// 处理 NuGet `Mediator` request。 + /// + ValueTask Mediator.IRequestHandler.Handle( + BenchmarkRequest request, + CancellationToken cancellationToken) + { + return Handle(request, cancellationToken); + } + /// /// 处理 MediatR request。 /// diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs index 0a71dac9..952d31b9 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherCacheTests.cs @@ -309,6 +309,60 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor, + /// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。 + /// + [Test] + public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors() + { + var requestBindings = GetCacheField("RequestDispatchBindings"); + var behaviorType = typeof(IPipelineBehavior); + using var zeroBehaviorContainer = CreateFrozenContainer( + new DispatcherCacheFixtureOptions + { + IncludeRequestPipelineCacheBehavior = false + }); + var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer); + var behaviorContext = new ArchitectureContext(_container!); + var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext); + var behaviorDispatcher = GetDispatcherFromContext(behaviorContext); + + await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest()); + + var bindingAfterZeroBehaviorDispatch = GetPairCacheValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int)); + var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1); + + await behaviorContext.SendRequestAsync(new DispatcherPipelineCacheRequest()); + + var bindingAfterBehaviorDispatch = GetPairCacheValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int)); + var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineCacheRequest), + typeof(int), + 1); + + Assert.Multiple(() => + { + Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null); + Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch)); + Assert.That(executorAfterZeroBehaviorDispatch, Is.Null); + Assert.That(executorAfterBehaviorDispatch, Is.Not.Null); + AssertRequestBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false); + AssertRequestBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true); + }); + } + /// /// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。 /// @@ -374,6 +428,60 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证同一 stream dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor, + /// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。 + /// + [Test] + public async Task Dispatcher_Should_Create_Stream_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors() + { + var streamBindings = GetCacheField("StreamDispatchBindings"); + var behaviorType = typeof(IStreamPipelineBehavior); + using var zeroBehaviorContainer = CreateFrozenContainer( + new DispatcherCacheFixtureOptions + { + IncludeStreamPipelineCacheBehavior = false + }); + var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer); + var behaviorContext = new ArchitectureContext(_container!); + var zeroBehaviorDispatcher = GetDispatcherFromContext(zeroBehaviorContext); + var behaviorDispatcher = GetDispatcherFromContext(behaviorContext); + + await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherCacheStreamRequest())); + + var bindingAfterZeroBehaviorDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherCacheStreamRequest), + typeof(int)); + var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue( + streamBindings, + typeof(DispatcherCacheStreamRequest), + typeof(int), + 1); + + await DrainAsync(behaviorContext.CreateStream(new DispatcherCacheStreamRequest())); + + var bindingAfterBehaviorDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherCacheStreamRequest), + typeof(int)); + var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue( + streamBindings, + typeof(DispatcherCacheStreamRequest), + typeof(int), + 1); + + Assert.Multiple(() => + { + Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null); + Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch)); + Assert.That(executorAfterZeroBehaviorDispatch, Is.Null); + Assert.That(executorAfterBehaviorDispatch, Is.Not.Null); + AssertStreamBehaviorPresenceEquals(zeroBehaviorDispatcher, behaviorType, false); + AssertStreamBehaviorPresenceEquals(behaviorDispatcher, behaviorType, true); + }); + } + /// /// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。 /// @@ -492,6 +600,71 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证同一 request dispatch binding 先在零行为 dispatcher 上命中后, + /// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Request_Context_When_Shared_Binding_Switches_From_Zero_Pipeline() + { + DispatcherPipelineContextRefreshState.Reset(); + + var requestBindings = GetCacheField("RequestDispatchBindings"); + using var zeroBehaviorContainer = CreateFrozenContainer( + new DispatcherCacheFixtureOptions + { + IncludeRequestPipelineContextRefreshBehavior = false + }); + var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer); + var behaviorContext = new ArchitectureContext(_container!); + + await zeroBehaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("without-behavior")); + + var bindingAfterZeroBehaviorDispatch = GetPairCacheValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int)); + var executorAfterZeroBehaviorDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + + await behaviorContext.SendRequestAsync(new DispatcherPipelineContextRefreshRequest("with-behavior")); + + var bindingAfterBehaviorDispatch = GetPairCacheValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int)); + var executorAfterBehaviorDispatch = GetRequestPipelineExecutorValue( + requestBindings, + typeof(DispatcherPipelineContextRefreshRequest), + typeof(int), + 1); + var behaviorSnapshots = DispatcherPipelineContextRefreshState.BehaviorSnapshots.ToArray(); + var handlerSnapshots = DispatcherPipelineContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null); + Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch)); + Assert.That(executorAfterZeroBehaviorDispatch, Is.Null); + Assert.That(executorAfterBehaviorDispatch, Is.Not.Null); + + Assert.That(behaviorSnapshots, Has.Length.EqualTo(1)); + Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior")); + Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + /// /// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler, /// 并为当次实例重新注入当前架构上下文。 @@ -632,6 +805,108 @@ internal sealed class CqrsDispatcherCacheTests }); } + /// + /// 验证同一 stream dispatch binding 先在零行为 dispatcher 上命中后, + /// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler,并为当前上下文重新注入架构实例。 + /// + [Test] + public async Task Dispatcher_Should_Reinject_Current_Stream_Context_When_Shared_Binding_Switches_From_Zero_Pipeline() + { + DispatcherStreamContextRefreshState.Reset(); + + var streamBindings = GetCacheField("StreamDispatchBindings"); + using var zeroBehaviorContainer = CreateFrozenContainer( + new DispatcherCacheFixtureOptions + { + IncludeStreamPipelineContextRefreshBehavior = false + }); + var zeroBehaviorContext = new ArchitectureContext(zeroBehaviorContainer); + var behaviorContext = new ArchitectureContext(_container!); + + await DrainAsync(zeroBehaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("without-behavior"))); + + var bindingAfterZeroBehaviorDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + var executorAfterZeroBehaviorDispatch = GetStreamPipelineExecutorValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int), + 1); + + await DrainAsync(behaviorContext.CreateStream(new DispatcherStreamContextRefreshRequest("with-behavior"))); + + var bindingAfterBehaviorDispatch = GetPairCacheValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int)); + var executorAfterBehaviorDispatch = GetStreamPipelineExecutorValue( + streamBindings, + typeof(DispatcherStreamContextRefreshRequest), + typeof(int), + 1); + var behaviorSnapshots = DispatcherStreamContextRefreshState.BehaviorSnapshots.ToArray(); + var handlerSnapshots = DispatcherStreamContextRefreshState.HandlerSnapshots.ToArray(); + + Assert.Multiple(() => + { + Assert.That(bindingAfterZeroBehaviorDispatch, Is.Not.Null); + Assert.That(bindingAfterBehaviorDispatch, Is.SameAs(bindingAfterZeroBehaviorDispatch)); + Assert.That(executorAfterZeroBehaviorDispatch, Is.Null); + Assert.That(executorAfterBehaviorDispatch, Is.Not.Null); + + Assert.That(behaviorSnapshots, Has.Length.EqualTo(1)); + Assert.That(behaviorSnapshots[0].DispatchId, Is.EqualTo("with-behavior")); + Assert.That(behaviorSnapshots[0].Context, Is.SameAs(behaviorContext)); + + Assert.That(handlerSnapshots, Has.Length.EqualTo(2)); + Assert.That(handlerSnapshots[0].DispatchId, Is.EqualTo("without-behavior")); + Assert.That(handlerSnapshots[0].Context, Is.SameAs(zeroBehaviorContext)); + Assert.That(handlerSnapshots[1].DispatchId, Is.EqualTo("with-behavior")); + Assert.That(handlerSnapshots[1].Context, Is.SameAs(behaviorContext)); + Assert.That(handlerSnapshots[1].Context, Is.Not.SameAs(handlerSnapshots[0].Context)); + Assert.That(handlerSnapshots[1].InstanceId, Is.Not.EqualTo(handlerSnapshots[0].InstanceId)); + }); + } + + /// + /// 描述缓存测试 fixture 需要启用的可选 pipeline 行为集合, + /// 用于构造“同一静态 binding 对应不同 dispatcher 注册可见性”的组合场景。 + /// + private sealed class DispatcherCacheFixtureOptions + { + /// + /// 获取是否注册 。 + /// + public bool IncludeRequestPipelineCacheBehavior { get; init; } = true; + + /// + /// 获取是否注册 。 + /// + public bool IncludeRequestPipelineContextRefreshBehavior { get; init; } = true; + + /// + /// 获取是否注册 request 顺序验证所需的两层 pipeline 行为。 + /// + public bool IncludeRequestPipelineOrderBehaviors { get; init; } = true; + + /// + /// 获取是否注册 。 + /// + public bool IncludeStreamPipelineCacheBehavior { get; init; } = true; + + /// + /// 获取是否注册 。 + /// + public bool IncludeStreamPipelineContextRefreshBehavior { get; init; } = true; + + /// + /// 获取是否注册 stream 顺序验证所需的两层 pipeline 行为。 + /// + public bool IncludeStreamPipelineOrderBehaviors { get; init; } = true; + } + /// /// 通过反射读取 dispatcher 的静态缓存对象。 /// @@ -679,10 +954,11 @@ internal sealed class CqrsDispatcherCacheTests /// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器, /// 用于验证 dispatcher 的实例级缓存不会跨容器共享。 /// - private static MicrosoftDiContainer CreateFrozenContainer() + /// 控制当前隔离容器要启用哪些可选 pipeline 行为的配置。 + private static MicrosoftDiContainer CreateFrozenContainer(DispatcherCacheFixtureOptions? options = null) { var container = new MicrosoftDiContainer(); - ConfigureDispatcherCacheFixture(container); + ConfigureDispatcherCacheFixture(container, options); container.Freeze(); return container; @@ -692,16 +968,44 @@ internal sealed class CqrsDispatcherCacheTests /// 组装当前 fixture 依赖的 CQRS 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。 /// /// 待补齐 CQRS 注册的目标容器。 - private static void ConfigureDispatcherCacheFixture(MicrosoftDiContainer container) + /// 控制是否跳过特定 pipeline 行为注册的可选配置。 + private static void ConfigureDispatcherCacheFixture( + MicrosoftDiContainer container, + DispatcherCacheFixtureOptions? options = null) { - container.RegisterCqrsPipelineBehavior(); - container.RegisterCqrsPipelineBehavior(); - container.RegisterCqrsPipelineBehavior(); - container.RegisterCqrsPipelineBehavior(); - container.RegisterCqrsStreamPipelineBehavior(); - container.RegisterCqrsStreamPipelineBehavior(); - container.RegisterCqrsStreamPipelineBehavior(); - container.RegisterCqrsStreamPipelineBehavior(); + options ??= new DispatcherCacheFixtureOptions(); + + if (options.IncludeRequestPipelineCacheBehavior) + { + container.RegisterCqrsPipelineBehavior(); + } + + if (options.IncludeRequestPipelineContextRefreshBehavior) + { + container.RegisterCqrsPipelineBehavior(); + } + + if (options.IncludeRequestPipelineOrderBehaviors) + { + container.RegisterCqrsPipelineBehavior(); + container.RegisterCqrsPipelineBehavior(); + } + + if (options.IncludeStreamPipelineCacheBehavior) + { + container.RegisterCqrsStreamPipelineBehavior(); + } + + if (options.IncludeStreamPipelineContextRefreshBehavior) + { + container.RegisterCqrsStreamPipelineBehavior(); + } + + if (options.IncludeStreamPipelineOrderBehaviors) + { + container.RegisterCqrsStreamPipelineBehavior(); + container.RegisterCqrsStreamPipelineBehavior(); + } CqrsTestRuntime.RegisterHandlers( container,