diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs index 57d46d23..61f3a13d 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -218,6 +219,59 @@ internal static class BenchmarkHostFactory return await mediator.Send(request, cancellationToken).ConfigureAwait(false); } + /// + /// 在真实的 request 级作用域内创建一次 GFramework.CQRS stream,并让该作用域覆盖整个异步枚举周期。 + /// + /// stream 响应元素类型。 + /// 冻结后的 benchmark 根容器,用于创建 request 作用域并提供注册元数据。 + /// 当前 request 级 runtime 复用的日志器。 + /// 当前 CQRS 分发上下文。 + /// 要创建 stream 的 request。 + /// 取消令牌。 + /// 绑定到单次显式作用域的异步响应序列。 + /// + /// stream 与 request 的区别在于:handler 解析发生在建流时,但 scoped 依赖必须一直存活到枚举完成。 + /// 因此这里返回一个包装后的 async iterator,把 scope 的释放时机推迟到调用方结束枚举之后, + /// 避免 `Scoped` handler 退化成“建流后立刻释放 scope,再在根容器语义下继续枚举”的错误模型。 + /// + internal static IAsyncEnumerable CreateScopedGFrameworkStream( + MicrosoftDiContainer rootContainer, + ILogger runtimeLogger, + ICqrsContext context, + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(rootContainer); + ArgumentNullException.ThrowIfNull(runtimeLogger); + ArgumentNullException.ThrowIfNull(context); + ArgumentNullException.ThrowIfNull(request); + + return EnumerateScopedGFrameworkStreamAsync(rootContainer, runtimeLogger, context, request, cancellationToken); + } + + /// + /// 在真实的 request 级作用域内创建一次 MediatR stream,并让该作用域覆盖整个异步枚举周期。 + /// + /// stream 响应元素类型。 + /// 当前 benchmark 的根 。 + /// 要创建 stream 的 request。 + /// 取消令牌。 + /// 绑定到单次显式作用域的异步响应序列。 + /// + /// 这里与 scoped request helper 保持同一组边界约束,但把 scope 生命周期延长到 stream 完整枚举结束, + /// 确保 `Scoped` handler 与依赖不会在首个元素产出前后被提前释放。 + /// + internal static IAsyncEnumerable CreateScopedMediatRStream( + ServiceProvider rootServiceProvider, + MediatR.IStreamRequest request, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(rootServiceProvider); + ArgumentNullException.ThrowIfNull(request); + + return EnumerateScopedMediatRStreamAsync(rootServiceProvider, request, cancellationToken); + } + /// /// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。 /// @@ -251,4 +305,47 @@ internal static class BenchmarkHostFactory interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == openGenericContract); } + + /// + /// 在单个显式作用域内创建并枚举 GFramework.CQRS stream。 + /// + private static async IAsyncEnumerable EnumerateScopedGFrameworkStreamAsync( + MicrosoftDiContainer rootContainer, + ILogger runtimeLogger, + ICqrsContext context, + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest request, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + using var scope = rootContainer.CreateScope(); + var scopedContainer = new ScopedBenchmarkContainer(rootContainer, scope); + var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + scopedContainer, + runtimeLogger); + var stream = runtime.CreateStream(context, request, cancellationToken); + + await foreach (var response in stream.ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return response; + } + } + + /// + /// 在单个显式作用域内创建并枚举 MediatR stream。 + /// + private static async IAsyncEnumerable EnumerateScopedMediatRStreamAsync( + ServiceProvider rootServiceProvider, + MediatR.IStreamRequest request, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + using var scope = rootServiceProvider.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var stream = mediator.CreateStream(request, cancellationToken); + + await foreach (var response in stream.ConfigureAwait(false)) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return response; + } + } } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs index 286af43e..3f1c1738 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs @@ -25,9 +25,9 @@ namespace GFramework.Cqrs.Benchmarks.Messaging; /// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。 /// /// -/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。 -/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起, -/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。 +/// 当前矩阵覆盖 `Singleton`、`Scoped` 与 `Transient`。 +/// 其中 `Scoped` 会在每次建流与枚举期间显式创建并持有真实的 DI 作用域, +/// 避免把 scoped handler 错误地下沉到根容器解析,或在异步枚举尚未结束时提前释放作用域。 /// 当前只保留 与 /// 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本, /// 以避免把更多观测策略与 的生命周期对照目标混在一起。 @@ -45,11 +45,13 @@ public class StreamLifetimeBenchmarks private ReflectionBenchmarkStreamRequest _reflectionRequest = null!; private GeneratedBenchmarkStreamRequest _generatedRequest = null!; private MediatRBenchmarkStreamRequest _mediatrRequest = null!; + private ILogger _reflectionRuntimeLogger = null!; + private ILogger _generatedRuntimeLogger = null!; /// /// 控制当前 benchmark 使用的 handler 生命周期。 /// - [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)] + [Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)] public HandlerLifetime Lifetime { get; set; } /// @@ -68,6 +70,11 @@ public class StreamLifetimeBenchmarks /// Singleton, + /// + /// 每次建流在显式作用域内解析并复用 handler 实例,且作用域会覆盖整个枚举周期。 + /// + Scoped, + /// /// 每次建流都重新解析新的 handler 实例。 /// @@ -122,14 +129,21 @@ public class StreamLifetimeBenchmarks _reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3); _generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3); _mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3); + _reflectionRuntimeLogger = + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime); + _generatedRuntimeLogger = + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime); _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => { RegisterReflectionHandler(container, Lifetime); }); - _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( - _reflectionContainer, - LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime)); + if (Lifetime != HandlerLifetime.Scoped) + { + _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _reflectionContainer, + _reflectionRuntimeLogger); + } _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => { @@ -138,16 +152,22 @@ public class StreamLifetimeBenchmarks }); // 容器内已提前保留默认 runtime 以支撑 generated registry 接线; // 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。 - _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( - _generatedContainer, - LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime)); + if (Lifetime != HandlerLifetime.Scoped) + { + _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _generatedContainer, + _generatedRuntimeLogger); + } _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( configure: null, typeof(StreamLifetimeBenchmarks), static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler), ResolveMediatRLifetime(Lifetime)); - _mediatr = _serviceProvider.GetRequiredService(); + if (Lifetime != HandlerLifetime.Scoped) + { + _mediatr = _serviceProvider.GetRequiredService(); + } } /// @@ -181,6 +201,18 @@ public class StreamLifetimeBenchmarks [Benchmark] public ValueTask Stream_GFrameworkReflection() { + if (Lifetime == HandlerLifetime.Scoped) + { + return ObserveAsync( + BenchmarkHostFactory.CreateScopedGFrameworkStream( + _reflectionContainer, + _reflectionRuntimeLogger, + BenchmarkContext.Instance, + _reflectionRequest, + CancellationToken.None), + Observation); + } + return ObserveAsync( _reflectionRuntime.CreateStream( BenchmarkContext.Instance, @@ -195,6 +227,18 @@ public class StreamLifetimeBenchmarks [Benchmark] public ValueTask Stream_GFrameworkGenerated() { + if (Lifetime == HandlerLifetime.Scoped) + { + return ObserveAsync( + BenchmarkHostFactory.CreateScopedGFrameworkStream( + _generatedContainer, + _generatedRuntimeLogger, + BenchmarkContext.Instance, + _generatedRequest, + CancellationToken.None), + Observation); + } + return ObserveAsync( _generatedRuntime.CreateStream( BenchmarkContext.Instance, @@ -209,6 +253,16 @@ public class StreamLifetimeBenchmarks [Benchmark] public ValueTask Stream_MediatR() { + if (Lifetime == HandlerLifetime.Scoped) + { + return ObserveAsync( + BenchmarkHostFactory.CreateScopedMediatRStream( + _serviceProvider, + _mediatrRequest, + CancellationToken.None), + Observation); + } + return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation); } @@ -229,6 +283,12 @@ public class StreamLifetimeBenchmarks ReflectionBenchmarkStreamHandler>(); return; + case HandlerLifetime.Scoped: + container.RegisterScoped< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + ReflectionBenchmarkStreamHandler>(); + return; + case HandlerLifetime.Transient: container.RegisterTransient< GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, @@ -261,6 +321,12 @@ public class StreamLifetimeBenchmarks GeneratedBenchmarkStreamHandler>(); return; + case HandlerLifetime.Scoped: + container.RegisterScoped< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + GeneratedBenchmarkStreamHandler>(); + return; + case HandlerLifetime.Transient: container.RegisterTransient< GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, @@ -282,6 +348,7 @@ public class StreamLifetimeBenchmarks return lifetime switch { HandlerLifetime.Singleton => ServiceLifetime.Singleton, + HandlerLifetime.Scoped => ServiceLifetime.Scoped, HandlerLifetime.Transient => ServiceLifetime.Transient, _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.") }; diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index ba189b58..51dc8155 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -17,9 +17,9 @@ - `Messaging/RequestBenchmarks.cs` - 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、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 + - `Singleton / Scoped / Transient` 三类 handler 生命周期下,direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析 - `Messaging/StreamLifetimeBenchmarks.cs` - - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举分层对照 + - `Singleton / Scoped / Transient` 三类 handler 生命周期下,direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 分层对照,并同时提供 `FirstItem / DrainAll` 两种观测口径 - `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` @@ -27,13 +27,13 @@ - `Messaging/RequestInvokerBenchmarks.cs` - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/StreamInvokerBenchmarks.cs` - - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比 + - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径 - `Messaging/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比 - `Messaging/NotificationFanOutBenchmarks.cs` - fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比 - `Messaging/StreamingBenchmarks.cs` - - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比 + - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径 ## 最小使用方式 @@ -60,8 +60,10 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro - `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容 - `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径 +- `RequestLifetimeBenchmarks` 的 `Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本 - `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem` 与 `DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察 -- 当前短跑结果显示,`StreamLifetimeBenchmarks` 在 `Singleton` 下无论 `FirstItem` 还是 `DrainAll` 都表现为 generated 略优于 reflection;在 `Transient` 下,`FirstItem` 仍是 reflection 略优于 generated,但 `DrainAll` 已转为 generated 优于 reflection。这说明当前差值主要集中在建流到首个元素之间的瞬时成本,而不是完整枚举阶段整体退化 +- `StreamingBenchmarks` 与 `StreamInvokerBenchmarks` 都同时暴露 `FirstItem` 与 `DrainAll`;阅读结果时应把它们分别理解为“建流到首个元素”的固定成本观测与“完整枚举整个 stream”的总成本观测 +- `StreamInvokerBenchmarks` 当前的 `DrainAll` short-job 输出只适合做 smoke 复核,确认矩阵和路径可以正常跑通;它不应直接写成 reflection、generated 或 `MediatR` 之间的稳定性能结论,若要做排序判断,应复跑默认作业或更完整的 benchmark 批次 - 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主,就应至少复跑能覆盖该路径的过滤场景;request 热路径通常先看: - `RequestBenchmarks.SendRequest_*` - `RequestLifetimeBenchmarks.SendRequest_*` @@ -75,5 +77,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro - 若继续优化 stream lifetime,可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次 - request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 - `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 -- 带真实显式作用域边界的 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 58ff9be8..61544c29 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,21 +7,23 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-129` +- 恢复点编号:`CQRS-REWRITE-RP-130` - 当前阶段:`Phase 8` - 当前 PR 锚点:`待重新抓取` - 当前结论: -- 当前 `RP-129` 继续沿用 `$gframework-batch-boot 50`,但本轮按 `gframework-multi-agent-batch` 的职责边界组织了一波 `3` 路互不冲突的 benchmark worker:`StreamingBenchmarks` 观测口径拆分、`StreamInvokerBenchmarks` 观测口径拆分、`RequestLifetimeBenchmarks` 的真实 scoped-host 生命周期矩阵 -- 本轮启动前已重新按 skill 规则复核基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`),且启动时 `origin/main...HEAD` 的累计 branch diff 为 `0 files / 0 lines`;旧 active 入口里 `21 files` 的数字已不再可作为当前波次基线 -- 本轮写面限定在 benchmark 子系统:`GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs`、`StreamInvokerBenchmarks.cs`、`RequestLifetimeBenchmarks.cs`、`BenchmarkHostFactory.cs`,以及新增的 `ScopedBenchmarkContainer.cs`;没有扩散到 `GFramework.Cqrs` runtime、测试项目或公共文档 -- `StreamingBenchmarks` 已从单一完整枚举口径扩成 `FirstItem / DrainAll` 双观测模式;worker smoke 结果表明默认 generated-provider steady-state stream 宿主在两种口径下都能稳定跑通,当前 short-job 约为 `FirstItem: baseline 62.14 ns / GFramework 118.83 ns / MediatR 182.16 ns`,`DrainAll: baseline 94.34 ns / GFramework 149.57 ns / MediatR 280.77 ns` -- `StreamInvokerBenchmarks` 现也具备 `FirstItem / DrainAll` 双观测模式,并已完成串行 smoke 运行;当前 short-job 下,`FirstItem` 口径里 generated lane 约 `59.44 ns`、reflection 约 `52.90 ns`,说明 tracking 里提到的 “generated 在首项前略慢于 reflection” 信号在更窄的 invoker 场景里仍可见 -- `RequestLifetimeBenchmarks` 当前矩阵已从 `Singleton / Transient` 扩到 `Singleton / Scoped / Transient`,且 `Scoped` 不再退化为根容器解析,而是通过 `BenchmarkHostFactory` + `ScopedBenchmarkContainer` 在每次 request 分发时显式创建并释放真实作用域;本轮 short-job 下 `Scoped` 口径约为 baseline `5.60 ns / 32 B`、`MediatR` `170.94 ns / 648 B`、`GFramework.Cqrs` `575.92 ns / 3400 B` -- 本轮首次并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时触发自动生成目录争用;已按仓库规则改为串行重跑同一命令,并以串行结果作为权威 smoke 验证 -- 当前可恢复结论收口为两点:一是 stream benchmark 线已从 `StreamLifetimeBenchmarks` 继续下探到 default steady-state 与 invoker 两个更窄口径;二是 request lifetime 线已经拥有真实 scoped-host 基线,后续若继续扩 `StreamLifetimeBenchmarks` 的 scoped 口径,不必再先做宿主适配 +- 当前 `RP-130` 延续 `$gframework-batch-boot 50`,并在上一波 `StreamingBenchmarks`、`StreamInvokerBenchmarks`、`RequestLifetimeBenchmarks` 扩口径之后,继续把 `StreamLifetimeBenchmarks` 的生命周期矩阵补齐到真实 `Scoped` stream 作用域,同时把 benchmark `README` 与当前矩阵同步 +- 本轮继续沿用已复核的基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`);当前分支连同本轮变更相对 `origin/main` 的累计 branch diff 约为 `9 files changed, 953 insertions(+), 83 deletions(-)`,离 `$gframework-batch-boot 50` 还有明显余量 +- 本轮写面收敛在 benchmark 层三处:`GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`、`StreamLifetimeBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/README.md`;没有扩散到 `GFramework.Cqrs` runtime 或测试项目 +- `BenchmarkHostFactory` 现在提供 `CreateScopedGFrameworkStream(...)` 与 `CreateScopedMediatRStream(...)`,通过显式 scope 包裹整个 async stream 枚举周期,避免 scoped handler 在建流后提前释放或错误回落到根容器语义 +- `StreamLifetimeBenchmarks` 当前矩阵已完整覆盖 `Singleton / Scoped / Transient` 与 `FirstItem / DrainAll`: + - `Singleton` 下 `DrainAll` 仍表现为 generated 略优于 reflection,约 `131.96 ns` 对 `134.26 ns` + - `Scoped` 下已经可以稳定产出真实作用域矩阵;`FirstItem` 约为 baseline `56.24 ns`、`MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns`,`DrainAll` 约为 baseline `81.20 ns`、`MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns` + - `Transient` 下 `FirstItem` 约为 generated `107.79 ns`、reflection `112.60 ns`,但 `DrainAll` 仍是 reflection `131.41 ns` 略优于 generated `135.95 ns` +- `README` 已同步三类 benchmark 现状:`RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 都包含真实 `Scoped` 生命周期,`StreamingBenchmarks` / `StreamInvokerBenchmarks` 都显式区分 `FirstItem / DrainAll`,且 `StreamInvokerBenchmarks` 的 `DrainAll` short-job 输出被明确标注为 smoke-only,不作为稳定排序结论 +- 本轮再次确认:此前并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时出现的冲突属于 benchmark 工件/生成目录层面的运行隔离问题,而不是 `Fixture`、`StreamInvokerBenchmarks` 或 `StreamLifetimeBenchmarks` 的业务逻辑错误 - 下一推荐步骤: - - 先回到 `ai-plan/public/cqrs-rewrite/**` 与 `GFramework.Cqrs.Benchmarks/README.md`,把本轮基线从旧的 `21 files / PR #345` 状态更新到当前 `699d0b48` 基线和新的 benchmark 结论 - - 然后优先复核 `StreamInvokerBenchmarks` 当前 short-job 输出里 `DrainAll` 口径的异常排序是否只是 smoke 配置噪音,必要时把下一批切回更稳定的 benchmark job 或补更窄的 helper-level 对照,而不是直接据此下结论 +- 下一批优先切到 `GFramework.Cqrs.Benchmarks` 的 benchmark-run/config 隔离层,避免两个过滤 benchmark 并行执行时再次共享自动生成目录或 artifacts 路径 +- 完成运行隔离后,再决定是否单开一波更稳定的 `StreamInvokerBenchmarks` `DrainAll` 复核,或继续把 scoped host 对照扩展到更多 stream 子场景 - 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准,active 入口不再重复展开旧阶段流水。 - 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界 - 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略 @@ -157,6 +159,17 @@ CQRS 迁移与收敛。 ## 最近权威验证 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:覆盖 `BenchmarkHostFactory` scoped stream helper、`StreamLifetimeBenchmarks` scoped 生命周期矩阵与 `README` 同步后的最小 Release build +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`StreamLifetimeBenchmarks` 已稳定跑出 `24` 个 case;`Scoped + DrainAll` 当前约为 baseline `81.20 ns / 280 B`、`MediatR` `428.66 ns / 1224 B`、generated `692.05 ns / 3888 B`、reflection `716.61 ns / 3888 B` +- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` + - 结果:通过 +- `git --git-dir=/.git/worktrees/GFramework-cqrs --work-tree=. diff --check` + - 结果:通过 + - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:覆盖 `StreamLifetimeBenchmarks` 的代码收口与 benchmark `README` 同步后的最小 Release build 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 7a12cbaf..b48a495c 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,31 @@ ## 2026-05-11 +### 阶段:stream lifetime scoped 矩阵与 README 同步(CQRS-REWRITE-RP-130) + +- 延续 `$gframework-batch-boot 50`,在上一波把 `StreamingBenchmarks`、`StreamInvokerBenchmarks` 与 `RequestLifetimeBenchmarks` 扩成双观测 / scoped-request 基线后,本轮先不回到 runtime,而是只补齐 `StreamLifetimeBenchmarks` 的真实 `Scoped` stream 生命周期矩阵,并同步 benchmark `README` +- 本轮主线程验收与修正: + - 接受 worker 留在 `BenchmarkHostFactory.cs` 的 scoped stream helper 方向,但把实现收口为“创建 scope -> 在 scope 内创建 runtime / mediator -> 让 scope 覆盖完整 async stream 枚举周期”的包装迭代器,而不是只在建流阶段短暂持有作用域 + - `StreamLifetimeBenchmarks.cs` 现仅在 `Lifetime == Scoped` 时改走新的 scoped stream helper;`Singleton / Transient` 仍保持原有最小 steady-state 宿主,不把 scoped 宿主额外成本误混进其他矩阵 + - `GFramework.Cqrs.Benchmarks/README.md` 已同步 `RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 的 `Scoped` 现状,并把 `StreamInvokerBenchmarks` `DrainAll` 标成 smoke-only 结论 +- 本轮验证: + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` + - 结果:通过 + - `git diff --check` + - 结果:通过 + - `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 --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`24` 个 case 全部跑通;`Scoped + FirstItem` 约为 baseline `56.24 ns`、`MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns`;`Scoped + DrainAll` 约为 baseline `81.20 ns`、`MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns` +- 本轮结论: + - `StreamLifetimeBenchmarks` 现在已经具备与 `RequestLifetimeBenchmarks` 对称的真实 scoped-host 作用域边界,后续不必再先修 benchmark 宿主才能观察 stream scoped lifetime + - `Scoped` 成本当前明显高于 `Singleton / Transient`,说明这条矩阵已经把“真实 scope 生命周期”本身的常量开销带入观测;这正是本轮想要确认的边界,而不是回归或异常 + - 本轮并没有改变 `GFramework.Cqrs` runtime 或业务逻辑,只是把 benchmark 宿主语义与文档描述对齐到当前事实 +- 下一恢复点: + - 优先切到 benchmark 运行隔离层,只动 `GFramework.Cqrs.Benchmarks/Program.cs` 与必要的 `README` 说明,解决两个过滤 benchmark 并行运行时共享 auto-generated build/artifacts 目录的问题 + - 新开的 explorer 已确认根因集中在 `Program.cs` 直接把 `args` 原样交给 `BenchmarkSwitcher.Run(args)`,当前没有为每次运行注入唯一 `ArtifactsPath`;不建议下一批回头改 `Fixture.cs` 或 benchmark 业务逻辑 + ### 阶段:benchmark 多 worker 波次与 scoped-host 基线(CQRS-REWRITE-RP-129) - 本轮从 `$gframework-batch-boot 50` 启动,但按 `gframework-multi-agent-batch` 规则把非阻塞工作拆成三条互不冲突的 benchmark 切片: