diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs new file mode 100644 index 00000000..977a14a5 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Threading; +using GFramework.Core.Abstractions.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using Microsoft.Extensions.DependencyInjection; + +[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute( + typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedStreamLifetimeBenchmarkRegistry))] + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为 stream 生命周期矩阵 benchmark 提供 hand-written generated registry, +/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的完整枚举成本。 +/// +public sealed class GeneratedStreamLifetimeBenchmarkRegistry : + GFramework.Cqrs.ICqrsHandlerRegistry, + GFramework.Cqrs.ICqrsStreamInvokerProvider, + GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors +{ + private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor = + new( + typeof(IStreamRequestHandler< + StreamLifetimeBenchmarks.BenchmarkStreamRequest, + StreamLifetimeBenchmarks.BenchmarkResponse>), + typeof(GeneratedStreamLifetimeBenchmarkRegistry).GetMethod( + nameof(InvokeBenchmarkStreamHandler), + BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException("Missing generated stream lifetime benchmark method.")); + + private static readonly IReadOnlyList Descriptors = + [ + new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry( + typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest), + typeof(StreamLifetimeBenchmarks.BenchmarkResponse), + Descriptor) + ]; + + /// + /// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。 + /// + /// 当前 generated registry 拥有的服务集合。 + /// 用于记录 generated registry 注册行为的日志器。 + /// + /// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。 + /// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + logger.Debug("Registered generated stream lifetime benchmark descriptors."); + } + + /// + /// 返回当前 provider 暴露的全部 generated stream invoker 描述符。 + /// + public IReadOnlyList GetDescriptors() + { + return Descriptors; + } + + /// + /// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。 + /// + /// 待匹配的请求类型。 + /// 待匹配的响应类型。 + /// 命中时返回的 generated descriptor。 + /// 命中当前 benchmark 的请求/响应类型对时返回 + public bool TryGetDescriptor( + Type requestType, + Type responseType, + out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor) + { + if (requestType == typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest) && + responseType == typeof(StreamLifetimeBenchmarks.BenchmarkResponse)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 模拟 generated stream invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。 + /// + /// 当前请求对应的 handler 实例。 + /// 待分发的流式请求。 + /// 调用方传入的取消令牌。 + /// 交给目标 stream handler 处理后的异步枚举。 + public static object InvokeBenchmarkStreamHandler( + object handler, + object request, + CancellationToken cancellationToken) + { + var typedHandler = (IStreamRequestHandler< + StreamLifetimeBenchmarks.BenchmarkStreamRequest, + StreamLifetimeBenchmarks.BenchmarkResponse>)handler; + var typedRequest = (StreamLifetimeBenchmarks.BenchmarkStreamRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs new file mode 100644 index 00000000..c121dfd8 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs @@ -0,0 +1,277 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnosers; +using BenchmarkDotNet.Jobs; +using BenchmarkDotNet.Order; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using GFramework.Core.Abstractions.Logging; +using GFramework.Core.Ioc; +using GFramework.Core.Logging; +using GFramework.Cqrs.Abstractions.Cqrs; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比 stream 完整枚举在不同 handler 生命周期下的额外开销。 +/// +/// +/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。 +/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起, +/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。 +/// +[Config(typeof(Config))] +public class StreamLifetimeBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private BenchmarkStreamHandler _baselineHandler = null!; + private BenchmarkStreamRequest _request = null!; + + /// + /// 控制当前 benchmark 使用的 handler 生命周期。 + /// + [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)] + public HandlerLifetime Lifetime { get; set; } + + /// + /// 可公平比较的 benchmark handler 生命周期集合。 + /// + public enum HandlerLifetime + { + /// + /// 复用单个 handler 实例。 + /// + Singleton, + + /// + /// 每次建流都重新解析新的 handler 实例。 + /// + Transient + } + + /// + /// 配置 stream 生命周期 benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamLifetime")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建当前生命周期下的 GFramework 与 MediatR stream 对照宿主。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup($"StreamLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + + _baselineHandler = new BenchmarkStreamHandler(); + _request = new BenchmarkStreamRequest(Guid.NewGuid(), 3); + + _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly); + RegisterGFrameworkHandler(container, Lifetime); + }); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime)); + + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(StreamLifetimeBenchmarks), + static candidateType => candidateType == typeof(BenchmarkStreamHandler), + ResolveMediatRLifetime(Lifetime)); + _mediatr = _serviceProvider.GetRequiredService(); + } + + /// + /// 释放当前生命周期矩阵持有的 benchmark 宿主资源,并清理 dispatcher 缓存。 + /// + [GlobalCleanup] + public void Cleanup() + { + try + { + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + } + finally + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } + } + + /// + /// 直接调用 handler 并完整枚举,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。 + /// + [Benchmark(Baseline = true)] + public async ValueTask Stream_Baseline() + { + await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。 + /// + [Benchmark] + public async ValueTask Stream_GFrameworkCqrs() + { + await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None) + .ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 MediatR 创建并完整枚举 stream,作为外部对照。 + /// + [Benchmark] + public async ValueTask Stream_MediatR() + { + await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 按生命周期把 benchmark stream handler 注册到 GFramework 容器。 + /// + /// 当前 benchmark 拥有并负责释放的容器。 + /// 待比较的 handler 生命周期。 + /// + /// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期, + /// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。 + /// + private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime) + { + ArgumentNullException.ThrowIfNull(container); + + switch (lifetime) + { + case HandlerLifetime.Singleton: + container.RegisterSingleton< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + BenchmarkStreamHandler>(); + return; + + case HandlerLifetime.Transient: + container.RegisterTransient< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + BenchmarkStreamHandler>(); + return; + + default: + throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime."); + } + } + + /// + /// 将 benchmark 生命周期映射为 MediatR 组装所需的 。 + /// + /// 待比较的 handler 生命周期。 + /// 当前生命周期对应的 MediatR 注册方式。 + private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime) + { + return lifetime switch + { + HandlerLifetime.Singleton => ServiceLifetime.Singleton, + HandlerLifetime.Transient => ServiceLifetime.Transient, + _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.") + }; + } + + /// + /// Benchmark stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest, + MediatR.IStreamRequest; + + /// + /// Benchmark stream response。 + /// + /// 响应标识。 + public sealed record BenchmarkResponse(Guid Id); + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。 + /// + public sealed class BenchmarkStreamHandler : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + MediatR.IStreamRequestHandler + { + /// + /// 处理 GFramework.CQRS stream request。 + /// + /// 当前 benchmark stream 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 完整枚举所需的低噪声异步响应序列。 + public IAsyncEnumerable Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 处理 MediatR stream request。 + /// + /// 当前 benchmark stream 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 完整枚举所需的低噪声异步响应序列。 + IAsyncEnumerable MediatR.IStreamRequestHandler.Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 为生命周期矩阵构造稳定、低噪声的异步响应序列。 + /// + /// 当前 benchmark 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 按固定元素数量返回的异步响应序列。 + private static async IAsyncEnumerable EnumerateAsync( + BenchmarkStreamRequest request, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + for (var index = 0; index < request.ItemCount; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new BenchmarkResponse(request.Id); + await Task.CompletedTask.ConfigureAwait(false); + } + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index bf0fc053..f589a0e3 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -18,6 +18,8 @@ - direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestLifetimeBenchmarks.cs` - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 +- `Messaging/StreamLifetimeBenchmarks.cs` + - `Singleton / Transient` 两类 handler 生命周期下,direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举对比 - `Messaging/RequestPipelineBenchmarks.cs` - `0 / 1 / 4` 个 pipeline 行为下,direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestStartupBenchmarks.cs` @@ -51,6 +53,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro - request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 - `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 -- stream handler 生命周期矩阵 - 带真实显式作用域边界的 scoped host 对照 - generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景 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 751b8130..d5bdfec3 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-107` +- 恢复点编号:`CQRS-REWRITE-RP-108` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #340` - 当前结论: @@ -44,14 +44,15 @@ CQRS 迁移与收敛。 - 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state:为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider;本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B`,`Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B` 与 `73.157 ns / 56 B` - 当前 `RP-106` 已把同一套 generated-provider 宿主收口扩展到 `RequestPipelineBenchmarks`:新增 handwritten `GeneratedRequestPipelineBenchmarkRegistry`,并让 `RequestPipelineBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` + benchmark CQRS 基础设施预接线;本轮 benchmark 表明 `0 pipeline` steady-state 进一步收敛到约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 在短跑噪音下维持约 `555.083 ns / 896 B` - 当前 `RP-107` 已把默认 stream steady-state 宿主也切到 generated-provider 路径:新增 handwritten `GeneratedDefaultStreamingBenchmarkRegistry`,让 `StreamingBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` 并在 setup/cleanup 清理 dispatcher cache;同时将 `gframework-boot` / `gframework-batch-boot` 的默认停止规则改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”,不再把 changed files 误当作唯一阈值 -- `ai-plan` active 入口现以 `RP-107` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 + - 当前 `RP-108` 已补齐 stream handler `Singleton / Transient` 生命周期矩阵 benchmark:新增 `StreamLifetimeBenchmarks` 与 `GeneratedStreamLifetimeBenchmarkRegistry`,让 stream 生命周期对照沿用 generated-provider 宿主接线而不是退回纯反射路径;本轮 benchmark 表明 `Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns` +- `ai-plan` active 入口现以 `RP-108` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 - 当前分支为 `feat/cqrs-optimization` - 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线 -- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `10 files / 507 lines` -- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs`、`GFramework.Cqrs.Benchmarks/README.md`、`.agents/skills/gframework-batch-boot/SKILL.md` 与 `.agents/skills/gframework-boot/SKILL.md` +- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines` +- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md` - 当前批次后的默认停止依据已改为 AI 上下文预算:若下一轮预计会让活动对话、已加载 recovery 文档、验证输出与当前 diff 接近约 `80%` 安全上下文占用,应在当前自然批次边界停止,即使 branch diff 仍有余量 - `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外 - `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵 @@ -60,6 +61,7 @@ CQRS 迁移与收敛。 - 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.275 ns / 32 B`,`Transient` 下约 `74.301 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 仍维持同一收敛区间 - 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline` 约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 约 `555.083 ns / 896 B` - 当前 stream steady-state benchmark 也已切到 generated-provider 宿主接线路径:baseline 约 `5.535 ns / 32 B`、`MediatR` 约 `59.499 ns / 232 B`、`GFramework.Cqrs` 约 `66.778 ns / 32 B` +- 当前 stream lifetime benchmark 已补齐 `Singleton / Transient` 两档矩阵,并沿用 generated-provider 宿主接线:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns` - 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配 - `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐 - `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException` @@ -110,6 +112,7 @@ CQRS 迁移与收敛。 - `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness - 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线 - 当前 `Mediator` 对照组仅先接入 steady-state request;若要把 `Transient` / `Scoped` 生命周期矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime build config,而不是在同一编译产物里混用多个 lifetime +- 当前 stream 生命周期矩阵尚未接入 `Mediator` concrete runtime;若要继续对齐 `Mediator` 官方 benchmark 的 compile-time lifetime 设计,需要为 stream 场景补专门的 build-time 配置,而不是在当前统一宿主里临时拼接 - `BenchmarkDotNet.Artifacts/` 现已加入仓库忽略规则;若后续确实需要提交新的基准报告,应显式挑选结果文件或改走文档归档,而不是直接纳入整个生成目录 - 当前 `GFramework.Cqrs` request steady-state 仍慢于 `MediatR`;在“至少超过反射版 `MediatR`”这个阶段目标达成前,任何相关改动都不能只看功能 build/test 结果,必须附带 benchmark 回归数据 - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” @@ -147,6 +150,21 @@ CQRS 迁移与收敛。 - 备注:按新性能回归门槛复跑后,`Singleton` 下 `GFramework.Cqrs` / `MediatR` 约 `83.183 ns / 32 B` vs `60.915 ns / 232 B`;`Transient` 下约 `86.243 ns / 56 B` vs `59.644 ns / 232 B` - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` - 结果:通过 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 +- `git diff --check` + - 结果:通过 +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:steady-state request 对照约为 baseline `5.336 ns / 32 B`、`Mediator` `5.564 ns / 32 B`、`MediatR` `53.307 ns / 232 B`、`GFramework.Cqrs` `64.745 ns / 32 B` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `4.309 ns / 51.923 ns / 67.981 ns`;`Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns` - `git diff --check` - 结果:通过 - 备注:当前仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式错误 @@ -301,9 +319,9 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 当前 turn 已接近默认的上下文预算停止线;本次提交后应停止,并在新的 turn 里从 `RP-107` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片 -2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification 或更高价值的 request dispatch 常量开销热点,而不是再机械按 changed files 追加小批次 -3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 或 stream 对照矩阵,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案 +1. 当前 turn 已到新的自然批次边界;本次提交后应停止,并在新的 turn 里从 `RP-108` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片 +2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification publish 或更高价值的 request dispatch 常量开销热点,而不是继续堆同层级 benchmark 宿主补齐 +3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再评估 `Mediator` 的 compile-time lifetime / stream 对照矩阵,或给 stream 引入 scoped host 基线,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案 ## 活跃文档 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 1c276d20..103b92b0 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 @@ -2,6 +2,46 @@ ## 2026-05-08 +### 阶段:stream handler 生命周期矩阵 benchmark(CQRS-REWRITE-RP-108) + +- 延续 `$gframework-batch-boot 50`,本轮继续使用 `origin/main` 作为 branch diff 基线,并先复核: + - `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800` + - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines` + - 当前 turn 虽然仍低于 `50 files` 阈值,但已加载多轮 recovery / benchmark 输出;因此只允许再推进一个单模块、低风险 benchmark 切片 +- 本轮接受的只读探索结论: + - `RequestLifetimeBenchmarks` 已覆盖 request 的 `Singleton / Transient` 生命周期矩阵,但 stream 侧仍缺少对称的 handler 生命周期对照 + - `StreamingBenchmarks` 已在 `RP-107` 切到 generated-provider 宿主,适合作为 stream 生命周期矩阵的宿主基础;继续退回纯反射路径会让“生命周期变量”和“descriptor 路径变量”混在一起 + - 如果让 generated registry 顺手注册默认单例 handler,会破坏生命周期矩阵的变量控制,因此 registry 只能暴露 descriptor,不能抢先锁死 handler 生命周期 +- 本轮主线程决策: + - 新增 `StreamLifetimeBenchmarks`,对齐 request 生命周期矩阵,只比较 `Singleton / Transient` 两档,继续明确把 `Scoped` 留给未来显式 scoped host + - 新增 `GeneratedStreamLifetimeBenchmarkRegistry`,只提供 handwritten generated stream invoker descriptor,不直接注册 handler + - 让 `StreamLifetimeBenchmarks` 使用 `RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly)` 建立 generated-provider 宿主,再显式按 benchmark 参数注册 `Singleton / Transient` handler 生命周期 + - 更新 `GFramework.Cqrs.Benchmarks/README.md`,把 stream 生命周期矩阵列为已覆盖场景,并从“后续扩展方向”里移除这项待办 +- 本轮验证过程的重要补充: + - 首次并行触发 `RequestBenchmarks` / `RequestLifetimeBenchmarks` / `StreamLifetimeBenchmarks` 时,在同一 autogenerated BenchmarkDotNet 目录下复现了文件已存在冲突与 bootstrap 异常;这是 benchmark 基础设施层面的并行目录竞争,不是代码缺陷 + - 改为串行重跑后三组 benchmark 全部稳定通过,因此本轮将“BenchmarkDotNet 在当前仓库里不应并行运行多条 `dotnet run --project ... --filter ...` 会话”视为有效执行约束 +- 本轮权威验证: + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:steady-state request 对照约为 baseline `5.336 ns / 32 B`、`Mediator` `5.564 ns / 32 B`、`MediatR` `53.307 ns / 232 B`、`GFramework.Cqrs` `64.745 ns / 32 B` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `4.309 ns / 51.923 ns / 67.981 ns`;`Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns` +- 本轮结论: + - stream 生命周期矩阵现在已与 request 生命周期矩阵对称,且继续沿用 generated-provider 宿主路径,没有把变量退化回纯反射 binding + - `GFramework.Cqrs` 在 stream `Singleton / Transient` 两档下都明显快于 `MediatR`,同时保持接近 baseline 的分配规模;`Transient` 仅从 `240 B` 小幅增至 `264 B` + - 真正的停止依据仍是上下文预算安全。虽然 branch diff 只有 `14 files`,但当前 turn 已包含多轮 benchmark 输出和恢复文档,因此本批提交后应主动停止 + - 下一轮若继续性能线,更值得优先看 notification publish 或更高价值的 request 常量开销热点,而不是继续做同层级 benchmark 宿主补齐 + ### 阶段:默认 stream benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-107) - 延续 `$gframework-batch-boot 50`,但本轮按用户新增要求把默认停止依据改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”;在真正落代码前先复核: