test(cqrs): 补充 notification publisher fan-out 基准对照

- 新增默认顺序发布器与 TaskWhenAllNotificationPublisher 的 fixed 4 handler fan-out benchmark 对照

- 更新 benchmark README 与 cqrs-rewrite 恢复文档,记录 RP-114 的性能结论与下一步
This commit is contained in:
gewuyou 2026-05-08 17:42:48 +08:00
parent 7ff4b628a1
commit b0102b5206
4 changed files with 52 additions and 9 deletions

View File

@ -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<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler3>();
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, 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
}
/// <summary>
/// 通过 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
/// 通过默认顺序发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
/// </summary>
[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);
}
/// <summary>
/// 通过内置 <c>Task.WhenAll(...)</c> 发布器的 GFramework.CQRS runtime 发布固定 4 处理器的 notification。
/// </summary>
[Benchmark]
public ValueTask PublishNotification_GFrameworkCqrsTaskWhenAll()
{
return _taskWhenAllRuntime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None);
}
/// <summary>

View File

@ -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 完整枚举对比

View File

@ -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)` 零行为探测方案
## 活跃文档

View File

@ -2,6 +2,30 @@
## 2026-05-08
### 阶段:`TaskWhenAll` notification publisher fan-out benchmarkCQRS-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 publisherCQRS-REWRITE-RP-113
- 延续 `$gframework-batch-boot 50`,本轮不再继续堆 notification benchmark 维度,而是直接把上一批已经量化清楚的 capability gap 收口到 runtime