test(cqrs-benchmarks): 补齐 RequestStartup 的 Mediator 对照

- 新增 RequestStartupBenchmarks 的 Mediator 初始化与冷启动 benchmark 路径

- 更新 request 与 handler 契约以复用单文件内的 Mediator concrete host 对照
This commit is contained in:
gewuyou 2026-05-11 12:39:37 +08:00
parent 337ffbd580
commit 8990749d91
2 changed files with 365 additions and 14 deletions

View File

@ -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;
/// <summary>
/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线
/// 对比 request 宿主在 GFramework.CQRS、NuGet `Mediator` 与 MediatR 之间的初始化与首次分发成本
/// </summary>
[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!;
/// <summary>
@ -63,6 +66,8 @@ public class RequestStartupBenchmarks
_serviceProvider = CreateMediatRServiceProvider();
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
_mediatorServiceProvider = CreateMediatorServiceProvider();
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
_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);
}
/// <summary>
@ -109,6 +114,16 @@ public class RequestStartupBenchmarks
return _runtime;
}
/// <summary>
/// 返回已构建宿主中的 `Mediator` concrete mediator作为 source-generated 对照组的初始化句柄。
/// </summary>
[Benchmark]
[BenchmarkCategory("Initialization")]
public GeneratedMediator Initialization_Mediator()
{
return _mediator;
}
/// <summary>
/// 在新宿主上首次发送 request作为 MediatR 的 cold-start baseline。
/// </summary>
@ -133,6 +148,18 @@ public class RequestStartupBenchmarks
return await runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 在新的 `Mediator` 宿主上首次发送 request量化 source-generated concrete path 的 cold-start 成本。
/// </summary>
[Benchmark]
[BenchmarkCategory("ColdStart")]
public async ValueTask<BenchmarkResponse> ColdStart_Mediator()
{
using var serviceProvider = CreateMediatorServiceProvider();
var mediator = serviceProvider.GetRequiredService<GeneratedMediator>();
return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
/// </summary>
@ -170,6 +197,14 @@ public class RequestStartupBenchmarks
ServiceLifetime.Transient);
}
/// <summary>
/// 构建只承载当前 benchmark request 的最小 `Mediator` 对照宿主。
/// </summary>
private static ServiceProvider CreateMediatorServiceProvider()
{
return BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
}
/// <summary>
/// 为 benchmark 创建稳定的 fatal 级 logger避免把日志成本混入 startup 测量。
/// </summary>
@ -188,6 +223,7 @@ public class RequestStartupBenchmarks
/// <param name="Id">请求标识。</param>
public sealed record BenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
Mediator.IRequest<BenchmarkResponse>,
MediatR.IRequest<BenchmarkResponse>;
/// <summary>
@ -197,10 +233,11 @@ public class RequestStartupBenchmarks
public sealed record BenchmarkResponse(Guid Id);
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。
/// 同时实现 GFramework.CQRS、NuGet `Mediator` 与 MediatR 契约的最小 request handler。
/// </summary>
public sealed class BenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
{
/// <summary>
@ -211,6 +248,16 @@ public class RequestStartupBenchmarks
return ValueTask.FromResult(new BenchmarkResponse(request.Id));
}
/// <summary>
/// 处理 NuGet `Mediator` request。
/// </summary>
ValueTask<BenchmarkResponse> Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>.Handle(
BenchmarkRequest request,
CancellationToken cancellationToken)
{
return Handle(request, cancellationToken);
}
/// <summary>
/// 处理 MediatR request。
/// </summary>

View File

@ -309,6 +309,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 request dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
/// </summary>
[Test]
public async Task Dispatcher_Should_Create_Request_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
{
var requestBindings = GetCacheField("RequestDispatchBindings");
var behaviorType = typeof(IPipelineBehavior<DispatcherPipelineCacheRequest, int>);
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);
});
}
/// <summary>
/// 验证 stream pipeline executor 会按行为数量在 binding 内首次创建并在后续建流中复用。
/// </summary>
@ -374,6 +428,60 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 stream dispatch binding 先走零行为直连路径时不会提前创建 pipeline executor
/// 后续另一 dispatcher 命中相同 binding 且存在行为时,仍会按实际行为数量补建缓存 executor。
/// </summary>
[Test]
public async Task Dispatcher_Should_Create_Stream_Pipeline_Executor_Only_When_Shared_Binding_Sees_Behaviors()
{
var streamBindings = GetCacheField("StreamDispatchBindings");
var behaviorType = typeof(IStreamPipelineBehavior<DispatcherCacheStreamRequest, int>);
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);
});
}
/// <summary>
/// 验证复用缓存的 request pipeline executor 后,行为顺序和最终处理器顺序保持不变。
/// </summary>
@ -492,6 +600,71 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 request dispatch binding 先在零行为 dispatcher 上命中后,
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler并为当前上下文重新注入架构实例。
/// </summary>
[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));
});
}
/// <summary>
/// 验证缓存的 notification dispatch binding 在重复分发时仍会重新解析 handler
/// 并为当次实例重新注入当前架构上下文。
@ -632,6 +805,108 @@ internal sealed class CqrsDispatcherCacheTests
});
}
/// <summary>
/// 验证同一 stream dispatch binding 先在零行为 dispatcher 上命中后,
/// 后续切换到存在行为的 dispatcher 仍会重新解析 behavior/handler并为当前上下文重新注入架构实例。
/// </summary>
[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));
});
}
/// <summary>
/// 描述缓存测试 fixture 需要启用的可选 pipeline 行为集合,
/// 用于构造“同一静态 binding 对应不同 dispatcher 注册可见性”的组合场景。
/// </summary>
private sealed class DispatcherCacheFixtureOptions
{
/// <summary>
/// 获取是否注册 <see cref="DispatcherPipelineCacheBehavior" />。
/// </summary>
public bool IncludeRequestPipelineCacheBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherPipelineContextRefreshBehavior" />。
/// </summary>
public bool IncludeRequestPipelineContextRefreshBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 request 顺序验证所需的两层 pipeline 行为。
/// </summary>
public bool IncludeRequestPipelineOrderBehaviors { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherStreamPipelineCacheBehavior" />。
/// </summary>
public bool IncludeStreamPipelineCacheBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 <see cref="DispatcherStreamPipelineContextRefreshBehavior" />。
/// </summary>
public bool IncludeStreamPipelineContextRefreshBehavior { get; init; } = true;
/// <summary>
/// 获取是否注册 stream 顺序验证所需的两层 pipeline 行为。
/// </summary>
public bool IncludeStreamPipelineOrderBehaviors { get; init; } = true;
}
/// <summary>
/// 通过反射读取 dispatcher 的静态缓存对象。
/// </summary>
@ -679,10 +954,11 @@ internal sealed class CqrsDispatcherCacheTests
/// 创建与当前 fixture 注册形状一致、但拥有独立 runtime 实例的冻结容器,
/// 用于验证 dispatcher 的实例级缓存不会跨容器共享。
/// </summary>
private static MicrosoftDiContainer CreateFrozenContainer()
/// <param name="options">控制当前隔离容器要启用哪些可选 pipeline 行为的配置。</param>
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 容器注册形状,确保默认上下文与隔离容器复用同一份装配基线。
/// </summary>
/// <param name="container">待补齐 CQRS 注册的目标容器。</param>
private static void ConfigureDispatcherCacheFixture(MicrosoftDiContainer container)
/// <param name="options">控制是否跳过特定 pipeline 行为注册的可选配置。</param>
private static void ConfigureDispatcherCacheFixture(
MicrosoftDiContainer container,
DispatcherCacheFixtureOptions? options = null)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
options ??= new DispatcherCacheFixtureOptions();
if (options.IncludeRequestPipelineCacheBehavior)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineCacheBehavior>();
}
if (options.IncludeRequestPipelineContextRefreshBehavior)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineContextRefreshBehavior>();
}
if (options.IncludeRequestPipelineOrderBehaviors)
{
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderOuterBehavior>();
container.RegisterCqrsPipelineBehavior<DispatcherPipelineOrderInnerBehavior>();
}
if (options.IncludeStreamPipelineCacheBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineCacheBehavior>();
}
if (options.IncludeStreamPipelineContextRefreshBehavior)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineContextRefreshBehavior>();
}
if (options.IncludeStreamPipelineOrderBehaviors)
{
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderOuterBehavior>();
container.RegisterCqrsStreamPipelineBehavior<DispatcherStreamPipelineOrderInnerBehavior>();
}
CqrsTestRuntime.RegisterHandlers(
container,