From 2cb6216d0553859637e877b9ae012ef960fd888e Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Wed, 6 May 2026 12:09:20 +0800
Subject: [PATCH] =?UTF-8?q?fix(cqrs):=20=E4=BF=AE=E5=A4=8D=20benchmark=20?=
=?UTF-8?q?=E5=AF=B9=E7=85=A7=E5=AE=BF=E4=B8=BB=E4=B8=8E=E5=86=B7=E5=90=AF?=
=?UTF-8?q?=E5=8A=A8=E5=9F=BA=E7=BA=BF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 BenchmarkHostFactory 统一 benchmark 最小宿主构建,并限制 MediatR 扫描到当前场景所需类型
- 修复 GFramework benchmark 容器未冻结导致的首次 handler 解析缺口,恢复 RequestStartupBenchmarks 冷启动结果
- 优化 request、pipeline、notification、stream 与 invoker benchmark 的生命周期对齐,减少无关程序集扫描噪音
- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #326 benchmark review 收敛、根因和验证结果
---
.../Messaging/BenchmarkHostFactory.cs | 93 +++++++++++++++++++
.../Messaging/NotificationBenchmarks.cs | 21 ++---
.../Messaging/RequestBenchmarks.cs | 22 ++---
.../Messaging/RequestInvokerBenchmarks.cs | 26 +++---
.../Messaging/RequestPipelineBenchmarks.cs | 33 ++++---
.../Messaging/RequestStartupBenchmarks.cs | 52 ++++++-----
.../Messaging/StreamInvokerBenchmarks.cs | 26 +++---
.../Messaging/StreamingBenchmarks.cs | 22 ++---
.../todos/cqrs-rewrite-migration-tracking.md | 22 +++--
.../traces/cqrs-rewrite-migration-trace.md | 44 +++++++++
10 files changed, 254 insertions(+), 107 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
new file mode 100644
index 00000000..b0248eb6
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
@@ -0,0 +1,93 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Linq;
+using GFramework.Core.Ioc;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。
+///
+///
+/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。
+/// 因此这里统一封装两类宿主的最小注册形状,确保:
+/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler;
+/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。
+///
+internal static class BenchmarkHostFactory
+{
+ ///
+ /// 创建一个已经冻结的 GFramework benchmark 容器。
+ ///
+ /// 向容器写入 benchmark 所需 handler / pipeline 的注册动作。
+ /// 已冻结、可立即用于 runtime 分发的容器。
+ internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action configure)
+ {
+ ArgumentNullException.ThrowIfNull(configure);
+
+ var container = new MicrosoftDiContainer();
+ configure(container);
+ container.Freeze();
+ return container;
+ }
+
+ ///
+ /// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
+ ///
+ /// 补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。
+ /// 用于限定扫描程序集的标记类型。
+ ///
+ /// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描;
+ /// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。
+ ///
+ /// 当前 benchmark 希望 MediatR 使用的默认注册生命周期。
+ /// 只承载当前 benchmark 场景所需服务的 DI 宿主。
+ internal static ServiceProvider CreateMediatRServiceProvider(
+ Action? configure,
+ Type handlerAssemblyMarkerType,
+ Func handlerTypeFilter,
+ ServiceLifetime lifetime = ServiceLifetime.Transient)
+ {
+ ArgumentNullException.ThrowIfNull(handlerAssemblyMarkerType);
+ ArgumentNullException.ThrowIfNull(handlerTypeFilter);
+
+ var services = new ServiceCollection();
+ services.AddLogging(static builder =>
+ Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
+ builder,
+ "LuckyPennySoftware.MediatR.License",
+ Microsoft.Extensions.Logging.LogLevel.None));
+
+ configure?.Invoke(services);
+
+ services.AddMediatR(options =>
+ {
+ options.Lifetime = lifetime;
+ options.TypeEvaluator = handlerTypeFilter;
+ options.RegisterServicesFromAssembly(handlerAssemblyMarkerType.Assembly);
+ });
+
+ return services.BuildServiceProvider();
+ }
+
+ ///
+ /// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
+ ///
+ /// 待判断类型。
+ /// 目标开放泛型合同,例如 。
+ /// 命中任一实现接口时返回 ;否则返回 。
+ internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract)
+ {
+ ArgumentNullException.ThrowIfNull(candidateType);
+ ArgumentNullException.ThrowIfNull(openGenericContract);
+
+ return candidateType.GetInterfaces().Any(interfaceType =>
+ interfaceType.IsGenericType &&
+ interfaceType.GetGenericTypeDefinition() == openGenericContract);
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs
index 27dbf173..42ee16b9 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs
@@ -58,21 +58,20 @@ public class NotificationBenchmarks
};
Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0);
- _container = new MicrosoftDiContainer();
- _container.RegisterTransient, BenchmarkNotificationHandler>();
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterSingleton>(
+ new BenchmarkNotificationHandler());
+ });
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks)));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, BenchmarkNotificationHandler>();
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ services => services.AddSingleton, BenchmarkNotificationHandler>(),
+ typeof(NotificationBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
+ ServiceLifetime.Singleton);
_publisher = _serviceProvider.GetRequiredService();
_notification = new BenchmarkNotification(Guid.NewGuid());
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
index 72650e9d..731f2a23 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
@@ -59,23 +59,21 @@ public class RequestBenchmarks
};
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
- _container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkRequestHandler();
-
- _container.RegisterTransient, BenchmarkRequestHandler>();
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterSingleton>(
+ _baselineHandler);
+ });
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks)));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, BenchmarkRequestHandler>();
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(RequestBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkRequestHandler),
+ ServiceLifetime.Singleton);
_mediatr = _serviceProvider.GetRequiredService();
_request = new BenchmarkRequest(Guid.NewGuid());
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
index d8aa8f60..d026ff25 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
@@ -73,27 +73,27 @@ public class RequestInvokerBenchmarks
_generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid());
_mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid());
- _reflectionContainer = new MicrosoftDiContainer();
- _reflectionContainer.RegisterTransient, ReflectionBenchmarkRequestHandler>();
+ _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
+ {
+ container.RegisterTransient, ReflectionBenchmarkRequestHandler>();
+ });
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_reflectionContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Reflection"));
- _generatedContainer = new MicrosoftDiContainer();
- _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
+ _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
+ });
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Generated"));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, MediatRBenchmarkRequestHandler>();
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestInvokerBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(RequestInvokerBenchmarks),
+ static candidateType => candidateType == typeof(MediatRBenchmarkRequestHandler),
+ ServiceLifetime.Singleton);
_mediatr = _serviceProvider.GetRequiredService();
}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
index 8ff41523..058019cd 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
@@ -65,25 +65,30 @@ public class RequestPipelineBenchmarks
};
Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount);
- _container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkRequestHandler();
-
- _container.RegisterTransient, BenchmarkRequestHandler>();
- RegisterGFrameworkPipelineBehaviors(_container, PipelineCount);
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterSingleton>(
+ _baselineHandler);
+ RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
+ });
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestPipelineBenchmarks)));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, BenchmarkRequestHandler>();
- RegisterMediatRPipelineBehaviors(services, PipelineCount);
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestPipelineBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ services =>
+ {
+ RegisterMediatRPipelineBehaviors(services, PipelineCount);
+ },
+ typeof(RequestPipelineBenchmarks),
+ static candidateType =>
+ candidateType == typeof(BenchmarkRequestHandler) ||
+ candidateType == typeof(BenchmarkPipelineBehavior1) ||
+ candidateType == typeof(BenchmarkPipelineBehavior2) ||
+ candidateType == typeof(BenchmarkPipelineBehavior3) ||
+ candidateType == typeof(BenchmarkPipelineBehavior4),
+ ServiceLifetime.Singleton);
_mediatr = _serviceProvider.GetRequiredService();
_request = new BenchmarkRequest(Guid.NewGuid());
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
index 8ca1caf7..494e6296 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs
@@ -40,7 +40,10 @@ public class RequestStartupBenchmarks
{
public Config()
{
- AddJob(Job.Default);
+ AddJob(Job.Default
+ .WithId("ColdStart")
+ .WithInvocationCount(1)
+ .WithUnrollFactor(1));
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestStartup"), TargetMethodColumn.Method, CategoriesColumn.Default);
AddDiagnoser(MemoryDiagnoser.Default);
@@ -62,6 +65,19 @@ public class RequestStartupBenchmarks
_runtime = CreateGFrameworkRuntime();
}
+ ///
+ /// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保两组 benchmark 都重新命中首次绑定路径。
+ ///
+ ///
+ /// 使用 `IterationSetup` 而不是把缓存清理写在 benchmark 方法主体中,
+ /// 可以把“清理静态缓存”留在测量边界之外,只保留宿主构建与首次发送本身。
+ ///
+ [IterationSetup]
+ public void ResetColdStartCaches()
+ {
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+ }
+
///
/// 释放 startup benchmark 复用的宿主对象。
///
@@ -110,23 +126,10 @@ public class RequestStartupBenchmarks
[BenchmarkCategory("ColdStart")]
public ValueTask ColdStart_GFrameworkCqrs()
{
- var runtime = CreateColdStartRuntime();
+ var runtime = CreateGFrameworkRuntime();
return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None);
}
- ///
- /// 为 cold-start benchmark 构建全新的 runtime,并在构建前显式清空 dispatcher 静态缓存。
- ///
- ///
- /// 这里把缓存清理与 runtime 构建绑定在同一阶段,避免把额外的反射缓存清理成本混入 benchmark 方法主体,
- /// 只保留“新宿主 + 首次分发”的对照。
- ///
- private static ICqrsRuntime CreateColdStartRuntime()
- {
- BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
- return CreateGFrameworkRuntime();
- }
-
///
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
///
@@ -136,8 +139,10 @@ public class RequestStartupBenchmarks
///
private static ICqrsRuntime CreateGFrameworkRuntime()
{
- var container = new MicrosoftDiContainer();
- container.RegisterTransient, BenchmarkRequestHandler>();
+ var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer =>
+ {
+ currentContainer.RegisterTransient, BenchmarkRequestHandler>();
+ });
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
}
@@ -146,14 +151,11 @@ public class RequestStartupBenchmarks
///
private static ServiceProvider CreateMediatRServiceProvider()
{
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestStartupBenchmarks).Assembly));
- return services.BuildServiceProvider();
+ return BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(RequestStartupBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkRequestHandler),
+ ServiceLifetime.Transient);
}
///
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
index 09c4074b..bfec83f0 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
@@ -73,27 +73,27 @@ public class StreamInvokerBenchmarks
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
- _reflectionContainer = new MicrosoftDiContainer();
- _reflectionContainer.RegisterTransient, ReflectionBenchmarkStreamHandler>();
+ _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
+ {
+ container.RegisterTransient, ReflectionBenchmarkStreamHandler>();
+ });
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_reflectionContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Reflection"));
- _generatedContainer = new MicrosoftDiContainer();
- _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
+ _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
+ });
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Generated"));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, MediatRBenchmarkStreamHandler>();
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamInvokerBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(StreamInvokerBenchmarks),
+ static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler),
+ ServiceLifetime.Singleton);
_mediatr = _serviceProvider.GetRequiredService();
}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
index 4b9bb42e..1c22d5f3 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
@@ -60,23 +60,21 @@ public class StreamingBenchmarks
};
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
- _container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkStreamHandler();
-
- _container.RegisterTransient, BenchmarkStreamHandler>();
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterSingleton>(
+ _baselineHandler);
+ });
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamingBenchmarks)));
- var services = new ServiceCollection();
- services.AddLogging(static builder =>
- Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter(
- builder,
- "LuckyPennySoftware.MediatR.License",
- Microsoft.Extensions.Logging.LogLevel.None));
- services.AddSingleton, BenchmarkStreamHandler>();
- services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamingBenchmarks).Assembly));
- _serviceProvider = services.BuildServiceProvider();
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(StreamingBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkStreamHandler),
+ ServiceLifetime.Singleton);
_mediatr = _serviceProvider.GetRequiredService();
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
index fd9ab98d..ddef2523 100644
--- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
+++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md
@@ -7,7 +7,7 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-089`
+- 恢复点编号:`CQRS-REWRITE-RP-090`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #326`
- 当前结论:
@@ -25,13 +25,15 @@ CQRS 迁移与收敛。
- `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks`
- 当前 `RP-088` 已补齐 request invoker reflection / generated-provider 对照,开始直接量化 dispatcher 预热 generated descriptor 的收益
- 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径
- - `ai-plan` active 入口现以 `PR #326` 和 `RP-089` 为唯一权威恢复锚点;`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照
+ - `ai-plan` active 入口现以 `PR #326` 和 `RP-090` 为唯一权威恢复锚点;`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支对应 `PR #326`,状态为 `OPEN`
-- latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛
-- `RequestStartupBenchmarks` 已修复 baseline 分组冲突、MediatR 13 logging/license 构造失败与重复注册问题,但 `ColdStart_GFrameworkCqrs` 仍存在 `No CQRS request handler registered` 的运行级残留
+- latest-head review 已从 benchmark 运行级缺陷收敛到剩余文档入口与是否继续接受 benchmark 语义细化的判断
+- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型
+- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered`
- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行
- 远端 `CTRF` 最新汇总为 `2274/2274` passed
- `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断
@@ -39,12 +41,18 @@ CQRS 迁移与收敛。
## 当前风险
- 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证
-- `RequestStartupBenchmarks` 的 GFramework cold-start 路径在清空 dispatcher 缓存后仍未恢复 request handler 绑定,当前无法产出完整 startup 对照数据
+- `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
## 最近权威验证
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`ColdStart_GFrameworkCqrs` 已恢复出数,最新本地输出约 `220-292 us`,MediatR 对照约 `575-616 us`;当前仅剩 BenchmarkDotNet 对单次 cold-start 场景的 `MinIterationTime` 提示
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:确认冻结后的 GFramework 最小宿主与受限扫描的 MediatR 最小宿主均可完成 steady-state request 对照
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:用于验证新增手动 benchmark workflow 依赖的 benchmark 项目入口仍可在 Release 下编译
@@ -119,8 +127,8 @@ CQRS 迁移与收敛。
## 下一推荐步骤
1. 继续处理 `PR #326` 的剩余 review 收尾,优先保持 benchmark 对照语义与 `ai-plan` active 入口一致
-2. 优先定位 `RequestStartupBenchmarks.ColdStart_GFrameworkCqrs` 在清空 dispatcher 缓存后的 request handler 绑定缺口,再决定是调整最小宿主注册方式还是补充专用 benchmark fixture
-3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行命中当前已知 startup 残留
+2. 决定是否继续细化 `RequestStartupBenchmarks` 的 cold-start harness,降低 `InvocationCount=1` 带来的 `MinIterationTime` 提示噪音
+3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行整个 benchmark 矩阵
## 活跃文档
diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
index 47980e8a..f3274372 100644
--- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
+++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md
@@ -1,5 +1,49 @@
# CQRS 重写迁移追踪
+## 2026-05-06
+
+### 阶段:benchmark 对照宿主收敛与 startup cold-start 恢复(CQRS-REWRITE-RP-090)
+
+- 使用 `$gframework-pr-review` 拉取 `PR #326` latest-head review 后,主线程确认仍有效的 benchmark 反馈集中在三类问题:
+ - `RequestBenchmarks` 的 GFramework / MediatR handler 生命周期不对齐
+ - `RequestStartupBenchmarks` 把容器构建、程序集扫描范围和缓存清理阶段混在一起,导致 cold-start 对照不公平
+ - benchmark 工程里的 `MicrosoftDiContainer` 多处以 `ImplementationType` 方式注册 handler,但未在 runtime 分发前 `Freeze()`,首次真实解析路径存在隐藏失败风险
+- 本轮本地复核的关键根因:
+ - `MicrosoftDiContainer.Get(Type)` 在未冻结时只读取 `ImplementationInstance`,不会实例化 `ImplementationType`
+ - `ColdStart_GFrameworkCqrs` 清空 dispatcher 静态缓存后,首次发送必须走真实 handler 解析,因此会稳定触发 `No CQRS request handler registered`
+ - 多个 benchmark 同时采用“手工 MediatR 注册 + `RegisterServicesFromAssembly(...)` 全程序集扫描”,容易把无关 handler / behavior 一并纳入对照,且存在重复注册漂移
+- 本轮决策:
+ - 新增 `Messaging/BenchmarkHostFactory.cs`,统一 benchmark 最小宿主构建规则
+ - GFramework benchmark 宿主统一先注册再 `Freeze()`,保证 steady-state 与 cold-start 都走真实可解析容器
+ - MediatR benchmark 宿主统一通过 `TypeEvaluator` 限制到当前场景所需 handler / behavior 类型,保留正常 `AddMediatR` 组装路径,同时移除全程序集扫描噪音
+ - `RequestStartupBenchmarks` 采用专用 `ColdStart` job,设置 `InvocationCount=1` 与 `WithUnrollFactor(1)`,并把 dispatcher cache reset 放到 `IterationSetup`
+- 已修改的 benchmark 范围:
+ - `RequestBenchmarks`
+ - `RequestPipelineBenchmarks`
+ - `RequestStartupBenchmarks`
+ - `StreamingBenchmarks`
+ - `NotificationBenchmarks`
+ - `RequestInvokerBenchmarks`
+ - `StreamInvokerBenchmarks`
+- 结果:
+ - `ColdStart_GFrameworkCqrs` 已恢复出有效结果,不再出现 `No CQRS request handler registered`
+ - `RequestBenchmarks`、`RequestStartupBenchmarks` 在本地均可实际运行
+ - `RequestStartupBenchmarks` 目前仍会收到 BenchmarkDotNet 对单次 cold-start 场景的 `MinIterationTime` 提示;这是测量形状带来的工具提示,不再是运行级失败
+
+### 验证(RP-090)
+
+- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json`
+ - 结果:通过
+ - 备注:确认当前分支对应 `PR #326`,仍有效的 open AI feedback 集中在 benchmark 对照语义与 active 文档收敛
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`ColdStart_GFrameworkCqrs` 已恢复,最新本地输出约 `220-292 us`,`ColdStart_MediatR` 约 `575-616 us`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:steady-state request 对照可正常运行,未再触发 MediatR 重复注册或 GFramework 首次解析失败
+
## 2026-04-30
### 阶段:历史 PR #307 active 入口收敛(CQRS-REWRITE-RP-076)