gewuyou 1420bd4340 fix(cqrs): 修复 benchmark scoped 宿主与 PR 恢复入口
- 修复 benchmark 入口的 artifacts 隔离判断与嵌套宿主目录保护,避免并发运行时宿主目录递归膨胀

- 优化 request 与 stream 生命周期 benchmark 的 scoped 宿主模型,复用单个 runtime 并仅在每次调用时创建真实 DI scope

- 补充 ScopedBenchmarkContainer 的 XML 合同说明,并收敛 README 与 ai-plan active 入口到当前 PR 恢复点
2026-05-11 10:50:50 +08:00

7.5 KiB
Raw Blame History

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
    • InitializationColdStart 两组 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 两种观测口径

最小使用方式

dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release

上面的命令只验证 benchmark 工程当前可以正常编译。

如需实际运行 benchmark再执行

dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release

如需只复核某一类场景,可把 BenchmarkDotNet 参数放在 -- 之后,例如:

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 业务逻辑本身的要求。例如:

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 发现口径
  • RequestLifetimeBenchmarksScoped 场景会复用单个 scoped runtime但在每次 request 分发时仍显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本,而不是把 runtime 构造常量成本混进生命周期对照
  • StreamLifetimeBenchmarks 现在按 direct handler、GFramework.Cqrs reflection、GFramework.Cqrs generated、MediatR 四层口径组织,并额外区分 FirstItemDrainAll 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
  • StreamingBenchmarksStreamInvokerBenchmarks 都同时暴露 FirstItemDrainAll;阅读结果时应把它们分别理解为“建流到首个元素”的固定成本观测与“完整枚举整个 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 / 建流对比继续扩展到更多场景