From b0102b52061cc87c5be6cb0d34187ecbf3eb4160 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 8 May 2026 17:42:48 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=20notification?= =?UTF-8?q?=20publisher=20fan-out=20=E5=9F=BA=E5=87=86=E5=AF=B9=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增默认顺序发布器与 TaskWhenAllNotificationPublisher 的 fixed 4 handler fan-out benchmark 对照 - 更新 benchmark README 与 cqrs-rewrite 恢复文档,记录 RP-114 的性能结论与下一步 --- .../Messaging/NotificationFanOutBenchmarks.cs | 25 +++++++++++++++---- GFramework.Cqrs.Benchmarks/README.md | 2 +- .../todos/cqrs-rewrite-migration-tracking.md | 10 +++++--- .../traces/cqrs-rewrite-migration-trace.md | 24 ++++++++++++++++++ 4 files changed, 52 insertions(+), 9 deletions(-) diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs index eaec091f..5ae7e606 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs @@ -14,6 +14,7 @@ using GFramework.Core.Abstractions.Logging; using GFramework.Core.Ioc; using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; +using GFramework.Cqrs.Notification; using MediatR; using Microsoft.Extensions.DependencyInjection; using GeneratedMediator = Mediator.Mediator; @@ -28,7 +29,8 @@ namespace GFramework.Cqrs.Benchmarks.Messaging; public class NotificationFanOutBenchmarks { private MicrosoftDiContainer _container = null!; - private ICqrsRuntime _runtime = null!; + private ICqrsRuntime _sequentialRuntime = null!; + private ICqrsRuntime _taskWhenAllRuntime = null!; private ServiceProvider _mediatrServiceProvider = null!; private ServiceProvider _mediatorServiceProvider = null!; private IPublisher _mediatrPublisher = null!; @@ -78,9 +80,13 @@ public class NotificationFanOutBenchmarks container.RegisterSingleton, BenchmarkNotificationHandler3>(); container.RegisterSingleton, BenchmarkNotificationHandler4>(); }); - _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _sequentialRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationFanOutBenchmarks))); + _taskWhenAllRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger($"{nameof(NotificationFanOutBenchmarks)}.{nameof(TaskWhenAllNotificationPublisher)}"), + new TaskWhenAllNotificationPublisher()); _mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( services => @@ -127,12 +133,21 @@ public class NotificationFanOutBenchmarks } /// - /// 通过 GFramework.CQRS runtime 发布固定 4 处理器的 notification。 + /// 通过默认顺序发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。 /// [Benchmark] - public ValueTask PublishNotification_GFrameworkCqrs() + public ValueTask PublishNotification_GFrameworkCqrsSequential() { - return _runtime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + return _sequentialRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + } + + /// + /// 通过内置 Task.WhenAll(...) 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。 + /// + [Benchmark] + public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll() + { + return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); } /// diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 3e66c2c9..28c130a5 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -31,7 +31,7 @@ - `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` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比 + - 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 完整枚举对比 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 c4b3b16f..9b1f576b 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,10 +7,14 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-113` +- 恢复点编号:`CQRS-REWRITE-RP-114` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #341` - 当前结论: + - 当前 `RP-114` 已继续沿用 `$gframework-batch-boot 50`,并沿着 `RP-113` 刚落地的 notification publisher 能力切片继续补 benchmark:`NotificationFanOutBenchmarks` 现同时纳入 `GFramework.Cqrs` 默认顺序发布器与新内置 `TaskWhenAllNotificationPublisher`,用于量化“能力差距收口后,固定 4 handler fan-out 的成本变化” + - `RP-114` 的 short-job 结果显示:fixed `4 handler` fan-out 下,默认顺序发布器约 `427.453 ns / 408 B`,内置 `TaskWhenAllNotificationPublisher` 约 `472.574 ns / 496 B`,`MediatR` 约 `225.940 ns / 1256 B`,NuGet `Mediator` concrete runtime 约 `3.854 ns / 0 B`;这说明当前内置并行 publisher 的主要价值是语义补齐,而不是 steady-state fan-out 性能收益 + - 这一批保持 runtime 公开 API 与 notification 语义不变,只扩 benchmark 对照口径与恢复文档;原因是 `RP-113` 已经把并行 publisher 能力落到 production path,当前更高价值的是先证明它相对默认顺序发布器、`Mediator` 与 `MediatR` 的成本位置,而不是立即继续扩第二个 publisher strategy + - 当前分支相对 `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 策略 - `TaskWhenAllNotificationPublisher` 当前语义明确为:零处理器静默完成,单处理器直接透传,多处理器并行启动并等待全部结束;它不保留默认顺序发布器的“首个异常立即停止”语义,而是把全部处理器的失败/取消结果收敛到同一个返回任务 - 本轮同时补齐 `CqrsNotificationPublisherTests` 对新内置策略的回归,并更新 `GFramework.Cqrs/README.md` 与 `docs/zh-CN/core/cqrs.md`,把切换方式和语义边界写回用户可见文档;当前已提交 branch diff 仍明显低于 `$gframework-batch-boot 50` 的停止阈值 @@ -365,8 +369,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 当前 turn 仍可继续自动推进;若下一批继续沿用 `$gframework-batch-boot 50` 且仍留在 notification 线,优先补 `TaskWhenAllNotificationPublisher` 的 benchmark / 异常聚合文档细化,或继续评估是否需要第二个内置 publisher strategy,而不是回头再加同层级 fan-out 对照 -2. 若下一轮要切回更高价值热点,优先重新审视 request dispatch 常量开销或 notification publisher 策略配置面,而不是新增 generated notification invoker/provider 这一类 steady-state 收益信号偏弱的 runtime seam +1. 若 `RP-114` 的 fan-out benchmark 证实内置 `TaskWhenAllNotificationPublisher` 在固定多处理器场景下明显优于默认顺序发布器,可继续评估是否需要补单处理器 / 异常路径的对照或把这组结果吸收到用户文档 +2. 当前 benchmark 已证明 `TaskWhenAllNotificationPublisher` 的价值主要在并行完成与异常聚合语义,而不是吞吐收益;下一轮优先评估 notification publisher 配置面 / 文档边界,或回到 request dispatch 常量开销,而不是新增 generated notification invoker/provider 这类 steady-state 收益信号偏弱的 runtime seam 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 f742d121..35f8fc3e 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,30 @@ ## 2026-05-08 +### 阶段:`TaskWhenAll` notification publisher fan-out benchmark(CQRS-REWRITE-RP-114) + +- 延续 `$gframework-batch-boot 50`,本轮不再扩新的 notification runtime 能力,而是沿着 `RP-113` 刚落地的内置并行 publisher 继续补验证口径: + - 当前分支相对 `origin/main`(`7ca21af9`, `2026-05-08 16:12:20 +0800`)的累计 branch diff 启动时为 `9 files`,明显低于 `50` 文件阈值 + - `RP-112` 只量化了默认顺序发布器的 fixed `4 handler` fan-out 成本;`RP-113` 已把 `TaskWhenAllNotificationPublisher` 引入 production runtime,但还没有 benchmark 说明“能力差距收口后,代价是多少” +- 本轮主线程决策: + - 在 `GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.cs` 同时保留 `baseline`、默认顺序 `GFramework.Cqrs`、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` concrete runtime 与 `MediatR` 五组对照 + - 复用同一个冻结 `MicrosoftDiContainer` 创建两个 `ICqrsRuntime`,确保变量集中在 notification publisher 策略,而不是 handler 注册或容器形状差异 + - 更新 `GFramework.Cqrs.Benchmarks/README.md` 与 active tracking,使默认恢复入口直接记录新的 benchmark 口径 +- 本轮权威验证: + - `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 "*NotificationFanOutBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:fixed `4 handler` fan-out 对照当前约为 baseline `7.424 ns / 0 B`、`Mediator` `3.854 ns / 0 B`、`MediatR` `225.940 ns / 1256 B`、`GFramework.Cqrs` 默认顺序发布器 `427.453 ns / 408 B`、内置 `TaskWhenAllNotificationPublisher` `472.574 ns / 496 B` + - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/NotificationFanOutBenchmarks.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` + - 结果:通过 +- 本轮结论: + - 当前 benchmark 说明 `TaskWhenAllNotificationPublisher` 的主要价值是补齐“等待全部处理器并聚合异常”的 notification 语义,而不是在 fixed `4 handler` fan-out steady-state 下带来吞吐收益;它比默认顺序发布器额外增加了约 `45 ns` 与 `88 B` + - 这组结果足以支持后续把 notification 线的重心转回 API 配置面、使用边界与文档语义,而不是继续机械堆新的 runtime seam 或期待 `TaskWhenAll` 自带性能红利 + - 当前 turn 仍可继续自动推进,但默认停止规则仍以“上下文预算优先、单批可评审边界次之”为准 + ### 阶段:内置 `TaskWhenAll` notification publisher(CQRS-REWRITE-RP-113) - 延续 `$gframework-batch-boot 50`,本轮不再继续堆 notification benchmark 维度,而是直接把上一批已经量化清楚的 capability gap 收口到 runtime: