gewuyou 0baa662ae4 test(cqrs-benchmarks): 隔离并发 benchmark 运行工件
- 新增 benchmark 入口 artifacts suffix 解析与独立 host 工作目录重启逻辑

- 更新 benchmark README 并发运行约定,补充隔离命令示例

- 更新 cqrs-rewrite 恢复文档,记录并发验证结果与后续恢复点
2026-05-11 08:58:18 +08:00

89 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GFramework.Cqrs.Benchmarks
该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化。
## 目的
-`GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线
- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比
- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口
## 当前内容
- `Program.cs`
- benchmark 命令行入口
- `Messaging/Fixture.cs`
- 运行前输出并校验场景配置
- `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 / Scoped / Transient` 三类 handler 生命周期下direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析
- `Messaging/StreamLifetimeBenchmarks.cs`
- `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`
- `Initialization``ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度
- `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 对比,并同时提供 `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 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
## 最小使用方式
```bash
dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
```
上面的命令只验证 benchmark 工程当前可以正常编译。
如需实际运行 benchmark再执行
```bash
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release
```
如需只复核某一类场景,可把 `BenchmarkDotNet` 参数放在 `--` 之后,例如:
```bash
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*"
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks.Stream_*"
```
如果需要在两个终端里并发复核不同的过滤 benchmark请为每个进程追加不同的 `--artifacts-suffix <suffix>`,把 `BenchmarkDotNet` auto-generated build 与 artifacts 输出隔离到不同目录;这只是运行入口的目录隔离约定,不是 benchmark 业务逻辑本身的要求。例如:
```bash
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*"
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-lifetime-b --filter "*StreamLifetimeBenchmarks.Stream_*"
```
## 当前约束
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
- 当两个带 `--filter` 的 benchmark 进程需要并发运行时,必须为它们分别传入不同的 `--artifacts-suffix <suffix>`,避免多个 `BenchmarkDotNet` 进程写入同一份 auto-generated build / artifacts 目录;这个约束只服务于本地输出隔离,不代表 benchmark 场景之间存在额外业务依赖
- `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 建流/首个元素成本与完整枚举成本拆开观察
- `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_*`
- 只要变更影响 stream dispatch、建流绑定或相关宿主接线就应补跑
- `StreamingBenchmarks.Stream_*`
- `StreamLifetimeBenchmarks.Stream_*`
- 当前性能目标不是超过 source-generated `Mediator`,而是让默认 request steady-state 路径尽量接近它,并至少稳定快于基于反射 / 扫描的 `MediatR`
## 后续扩展方向
- 若继续优化 stream lifetime可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景