fix(cqrs): 修复 benchmark 对照宿主与冷启动基线

- 新增 BenchmarkHostFactory 统一 benchmark 最小宿主构建,并限制 MediatR 扫描到当前场景所需类型

- 修复 GFramework benchmark 容器未冻结导致的首次 handler 解析缺口,恢复 RequestStartupBenchmarks 冷启动结果

- 优化 request、pipeline、notification、stream 与 invoker benchmark 的生命周期对齐,减少无关程序集扫描噪音

- 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #326 benchmark review 收敛、根因和验证结果
This commit is contained in:
gewuyou 2026-05-06 12:09:20 +08:00
parent f71791ae98
commit 2cb6216d05
10 changed files with 254 additions and 107 deletions

View File

@ -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;
/// <summary>
/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。
/// </summary>
/// <remarks>
/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。
/// 因此这里统一封装两类宿主的最小注册形状,确保:
/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler
/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。
/// </remarks>
internal static class BenchmarkHostFactory
{
/// <summary>
/// 创建一个已经冻结的 GFramework benchmark 容器。
/// </summary>
/// <param name="configure">向容器写入 benchmark 所需 handler / pipeline 的注册动作。</param>
/// <returns>已冻结、可立即用于 runtime 分发的容器。</returns>
internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action<MicrosoftDiContainer> configure)
{
ArgumentNullException.ThrowIfNull(configure);
var container = new MicrosoftDiContainer();
configure(container);
container.Freeze();
return container;
}
/// <summary>
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
/// </summary>
/// <param name="configure">补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。</param>
/// <param name="handlerAssemblyMarkerType">用于限定扫描程序集的标记类型。</param>
/// <param name="handlerTypeFilter">
/// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描;
/// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。
/// </param>
/// <param name="lifetime">当前 benchmark 希望 MediatR 使用的默认注册生命周期。</param>
/// <returns>只承载当前 benchmark 场景所需服务的 DI 宿主。</returns>
internal static ServiceProvider CreateMediatRServiceProvider(
Action<IServiceCollection>? configure,
Type handlerAssemblyMarkerType,
Func<Type, bool> 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();
}
/// <summary>
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
/// </summary>
/// <param name="candidateType">待判断类型。</param>
/// <param name="openGenericContract">目标开放泛型合同,例如 <see cref="MediatR.IRequestHandler{TRequest,TResponse}" />。</param>
/// <returns>命中任一实现接口时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract)
{
ArgumentNullException.ThrowIfNull(candidateType);
ArgumentNullException.ThrowIfNull(openGenericContract);
return candidateType.GetInterfaces().Any(interfaceType =>
interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == openGenericContract);
}
}

View File

@ -58,21 +58,20 @@ public class NotificationBenchmarks
};
Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0);
_container = new MicrosoftDiContainer();
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>>(
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<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly));
_serviceProvider = services.BuildServiceProvider();
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
services => services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>(),
typeof(NotificationBenchmarks),
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
ServiceLifetime.Singleton);
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
_notification = new BenchmarkNotification(Guid.NewGuid());

View File

@ -59,23 +59,21 @@ public class RequestBenchmarks
};
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
_container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkRequestHandler();
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
_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<MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, 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<IMediator>();
_request = new BenchmarkRequest(Guid.NewGuid());

View File

@ -73,27 +73,27 @@ public class RequestInvokerBenchmarks
_generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid());
_mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid());
_reflectionContainer = new MicrosoftDiContainer();
_reflectionContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkRequestHandler>();
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, 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<MediatR.IRequestHandler<MediatRBenchmarkRequest, MediatRBenchmarkResponse>, 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<IMediator>();
}

View File

@ -65,25 +65,30 @@ public class RequestPipelineBenchmarks
};
Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount);
_container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkRequestHandler();
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
RegisterGFrameworkPipelineBehaviors(_container, PipelineCount);
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
_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<MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, 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<IMediator>();
_request = new BenchmarkRequest(Guid.NewGuid());

View File

@ -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();
}
/// <summary>
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保两组 benchmark 都重新命中首次绑定路径。
/// </summary>
/// <remarks>
/// 使用 `IterationSetup` 而不是把缓存清理写在 benchmark 方法主体中,
/// 可以把“清理静态缓存”留在测量边界之外,只保留宿主构建与首次发送本身。
/// </remarks>
[IterationSetup]
public void ResetColdStartCaches()
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
/// <summary>
/// 释放 startup benchmark 复用的宿主对象。
/// </summary>
@ -110,23 +126,10 @@ public class RequestStartupBenchmarks
[BenchmarkCategory("ColdStart")]
public ValueTask<BenchmarkResponse> ColdStart_GFrameworkCqrs()
{
var runtime = CreateColdStartRuntime();
var runtime = CreateGFrameworkRuntime();
return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None);
}
/// <summary>
/// 为 cold-start benchmark 构建全新的 runtime并在构建前显式清空 dispatcher 静态缓存。
/// </summary>
/// <remarks>
/// 这里把缓存清理与 runtime 构建绑定在同一阶段,避免把额外的反射缓存清理成本混入 benchmark 方法主体,
/// 只保留“新宿主 + 首次分发”的对照。
/// </summary>
private static ICqrsRuntime CreateColdStartRuntime()
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
return CreateGFrameworkRuntime();
}
/// <summary>
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
/// </summary>
@ -136,8 +139,10 @@ public class RequestStartupBenchmarks
/// </remarks>
private static ICqrsRuntime CreateGFrameworkRuntime()
{
var container = new MicrosoftDiContainer();
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer =>
{
currentContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
});
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
}
@ -146,14 +151,11 @@ public class RequestStartupBenchmarks
/// </summary>
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);
}
/// <summary>

View File

@ -73,27 +73,27 @@ public class StreamInvokerBenchmarks
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
_reflectionContainer = new MicrosoftDiContainer();
_reflectionContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkStreamHandler>();
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
{
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>, 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<MediatR.IStreamRequestHandler<MediatRBenchmarkStreamRequest, MediatRBenchmarkResponse>, 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<IMediator>();
}

View File

@ -60,23 +60,21 @@ public class StreamingBenchmarks
};
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
_container = new MicrosoftDiContainer();
_baselineHandler = new BenchmarkStreamHandler();
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamHandler>();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>>(
_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<MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>, 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<IMediator>();
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);

View File

@ -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 矩阵
## 活跃文档

View File

@ -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