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)