From 5da4a5893b73277ca3fe7fcb28fab568e3b934c5 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Fri, 8 May 2026 10:30:24 +0800 Subject: [PATCH] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E7=B4=A7=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E5=9B=9E=E5=BD=92=E9=97=A8=E6=A7=9B=E5=B9=B6=E5=BF=BD?= =?UTF-8?q?=E7=95=A5=E5=9F=BA=E5=87=86=E4=BA=A7=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新 BenchmarkDotNet 生成目录忽略规则,避免本地基准产物污染工作树 - 补充 CQRS benchmark 回归要求与性能目标,要求相关改动后复跑 request 基准 - 更新 cqrs-rewrite 跟踪文档并记录最新 request 基准结果 --- .gitignore | 1 + GFramework.Cqrs.Benchmarks/README.md | 12 +++++-- .../todos/cqrs-rewrite-migration-tracking.md | 34 ++++++++++++++----- .../traces/cqrs-rewrite-migration-trace.md | 25 ++++++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 01581041..a1551cb3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ ai-libs/ .codex # tool .venv/ +BenchmarkDotNet.Artifacts/ diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 389f4e86..9fc28cbd 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -15,7 +15,7 @@ - `Messaging/Fixture.cs` - 运行前输出并校验场景配置 - `Messaging/RequestBenchmarks.cs` - - direct handler、`GFramework.Cqrs` runtime、`ai-libs/Mediator` source-generated concrete path 与 `MediatR` 的 request steady-state dispatch 对比 + - direct handler、NuGet `Mediator` source-generated concrete path、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestLifetimeBenchmarks.cs` - `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestPipelineBenchmarks.cs` @@ -39,10 +39,18 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro 也可以通过 `BenchmarkDotNet` 过滤器只运行某一类场景。 +## 当前约束 + +- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容 +- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主,就必须至少复跑: + - `RequestBenchmarks.SendRequest_*` + - `RequestLifetimeBenchmarks.SendRequest_*` +- 当前性能目标不是超过 source-generated `Mediator`,而是让默认 request steady-state 路径尽量接近它,并至少稳定快于基于反射 / 扫描的 `MediatR` + ## 后续扩展方向 - request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 -- `ai-libs/Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 +- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 - stream handler 生命周期矩阵 - 带真实显式作用域边界的 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 166fd5b4..c1910998 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,7 +7,7 @@ CQRS 迁移与收敛。 ## 当前恢复点 -- 恢复点编号:`CQRS-REWRITE-RP-101` +- 恢复点编号:`CQRS-REWRITE-RP-102` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #339` - 当前结论: @@ -38,20 +38,23 @@ CQRS 迁移与收敛。 - 当前 `RP-100` 已使用 `$gframework-pr-review` 复核 `PR #339` latest-head review:收口 `RegisterCqrsStreamPipelineBehavior()` 的异常契约文档、为 `StreamPipelineInvocation.GetContinuation(...)` 补齐并发 continuation 缓存说明、抽取 `MicrosoftDiContainer` 的 CQRS 行为注册公共逻辑,并顺手修复当前 branch diff 内 `ICqrsRequestInvokerProvider.cs` 的 XML 缩进格式问题 - 当前 `RP-101` 已按用户新增 benchmark 诉求收口 request 热路径:为 `IIocContainer` 新增不激活实例的 `HasRegistration(Type)`、让 dispatcher 在 `0 pipeline` 场景下跳过空行为解析,并为 `MicrosoftDiContainer` 的热路径查询补齐 debug-level 守卫,避免无效日志字符串分配 - 当前 `RP-101` 已把 `GFramework.Cqrs.Benchmarks` 的 `Mediator` 对照组收口为官方 NuGet 引用(`Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`),不再使用本地 `ai-libs/Mediator` project reference;`RequestBenchmarks` 现已新增 source-generated concrete `Mediator` 对照方法,并通过 `RequestLifetimeBenchmarks` 复核 hot path 收口后的新基线 -- `ai-plan` active 入口现以 `RP-101` 为最新恢复锚点;`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 + - 当前 `RP-102` 已将 `BenchmarkDotNet.Artifacts/` 收口为默认忽略路径,并把 request steady-state / lifetime benchmark 复跑升级为 CQRS 性能相关改动的默认回归门槛;当前阶段目标明确为“持续逼近 source-generated `Mediator`,并至少稳定超过反射版 `MediatR`” +- `ai-plan` active 入口现以 `RP-102` 为最新恢复锚点;`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 - 当前分支为 `feat/cqrs-optimization` - 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`5dc2dd25`, 2026-05-08 09:08:37 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线 -- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`;本轮待提交工作树包含 9 个跟踪文件修改,另有 `BenchmarkDotNet.Artifacts/` 生成输出未纳入提交,仍明显低于 `$gframework-batch-boot 50` 的文件阈值 +- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `10 files / 298 lines`;本轮待提交工作树以 `.gitignore`、benchmark README 与 active tracking/trace 更新为主,仍明显低于 `$gframework-batch-boot 50` 的文件阈值 - `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外 - `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵 - `GFramework.Cqrs.Benchmarks` 当前以 NuGet 方式引用 `Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`;`ai-libs/Mediator` 只保留为本地源码/README 对照资料,不再参与 benchmark 项目编译 -- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:约 `5.969 ns / 32 B`、`6.242 ns / 32 B`、`53.818 ns / 232 B`、`85.504 ns / 32 B` -- 当前 request lifetime benchmark 已从旧坏值显著收敛:`Singleton` 下 `GFramework.Cqrs` 约 `84.066 ns / 32 B`(旧值 `301.731 ns / 440 B`),`Transient` 下约 `90.652 ns / 56 B`(旧值 `287.863 ns / 464 B`) +- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:约 `5.300 ns / 32 B`、`4.964 ns / 32 B`、`57.993 ns / 232 B`、`83.823 ns / 32 B` +- 当前 request lifetime benchmark 已从旧坏值显著收敛:`Singleton` 下 `GFramework.Cqrs` 约 `83.183 ns / 32 B`(旧值 `301.731 ns / 440 B`),`Transient` 下约 `86.243 ns / 56 B`(旧值 `287.863 ns / 464 B`) - 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配 - 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间 +- 当前性能回归门槛已收紧为:只要改动触达 `GFramework.Cqrs` request dispatch、DI 热路径、invoker/provider、pipeline 或 benchmark 宿主,就必须至少复跑 `RequestBenchmarks.SendRequest_*` 与 `RequestLifetimeBenchmarks.SendRequest_*` +- 当前阶段的性能验收目标已明确为:默认 request steady-state 路径不要求超过 source-generated `Mediator`,但必须持续逼近它,并至少稳定快于基于反射 / 扫描的 `MediatR` - `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime` - 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline - `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试 @@ -91,7 +94,8 @@ CQRS 迁移与收敛。 - `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness - 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线 - 当前 `Mediator` 对照组仅先接入 steady-state request;若要把 `Transient` / `Scoped` 生命周期矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime build config,而不是在同一编译产物里混用多个 lifetime -- `BenchmarkDotNet.Artifacts/` 是本轮本地运行生成的未跟踪输出;若后续需要提交新的基准报告,应先确认仓库是否要保留该批产物,而不是默认把生成目录纳入版本控制 +- `BenchmarkDotNet.Artifacts/` 现已加入仓库忽略规则;若后续确实需要提交新的基准报告,应显式挑选结果文件或改走文档归档,而不是直接纳入整个生成目录 +- 当前 `GFramework.Cqrs` request steady-state 仍慢于 `MediatR`;在“至少超过反射版 `MediatR`”这个阶段目标达成前,任何相关改动都不能只看功能 build/test 结果,必须附带 benchmark 回归数据 - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 - legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成” @@ -101,6 +105,18 @@ CQRS 迁移与收敛。 ## 最近权威验证 +- `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 -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:按新性能回归门槛复跑后,steady-state request 对照约为 baseline `5.300 ns / 32 B`、`Mediator` `4.964 ns / 32 B`、`MediatR` `57.993 ns / 232 B`、`GFramework.Cqrs` `83.823 ns / 32 B` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:按新性能回归门槛复跑后,`Singleton` 下 `GFramework.Cqrs` / `MediatR` 约 `83.183 ns / 32 B` vs `60.915 ns / 232 B`;`Transient` 下约 `86.243 ns / 56 B` vs `59.644 ns / 232 B` +- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 - `dotnet pack GFramework.sln -c Release --no-restore -o /tmp/gframework-pack-validation -p:IncludeSymbols=false` - 结果:通过 - 备注:当前本地产物仅包含 14 个预期发布包,未生成 `GFramework.Cqrs.Benchmarks.*.nupkg` @@ -221,9 +237,9 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 benchmark/`Mediator` 对齐,下一批建议把 `Mediator` 的 compile-time lifetime 矩阵扩展到 `RequestLifetimeBenchmarks`,避免只有 `MediatR` 参与生命周期对照 -2. 若继续压 request steady-state 开销,下一批优先评估 `GFramework.Cqrs` 默认 request 路径吸收 generated invoker provider 的可行性,而不是只在单独 benchmark 类里保留 handwritten generated 对照 -3. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #339` stale review threads;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核 +1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先对 `CqrsDispatcher.SendAsync(...)` / request invoker 绑定 / handler 调用适配做更细粒度热点拆分,并在每次改动后立即复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks` +2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批优先评估默认 request 路径吸收 generated invoker/provider 或继续裁掉 dispatch binding / delegate 适配层的剩余常量开销 +3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 矩阵,而不是先横向堆更多低价值场景 ## 活跃文档 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 a4a75b1a..3e90d543 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-08 +### 阶段:性能回归门槛收紧与 benchmark 产物忽略收口(CQRS-REWRITE-RP-102) + +- 延续 `RP-101` 后的 benchmark 基线,本轮没有继续改 runtime 热路径,而是先把性能治理规则补齐,避免后续优化波次出现“功能通过但 steady-state request 变慢”的回退 +- 本轮主线程决策: + - 将 `BenchmarkDotNet.Artifacts/` 加入仓库 `.gitignore`,避免本地 benchmark 生成目录反复污染工作树 + - 在 `GFramework.Cqrs.Benchmarks/README.md` 明确写下新的默认回归门槛:只要改动触达 request dispatch、DI 热路径、invoker/provider、pipeline 或 benchmark 宿主,就必须至少复跑 `RequestBenchmarks.SendRequest_*` 与 `RequestLifetimeBenchmarks.SendRequest_*` + - 在 `cqrs-rewrite` active tracking 中把当前阶段目标升级为“持续逼近 source-generated `Mediator`,并至少稳定超过反射版 `MediatR`”,不再只把 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 -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:按新门槛复跑后,steady-state request 对照约为 baseline `5.300 ns / 32 B`、`Mediator` `4.964 ns / 32 B`、`MediatR` `57.993 ns / 232 B`、`GFramework.Cqrs` `83.823 ns / 32 B` + - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:按新门槛复跑后,`Singleton` 下 `GFramework.Cqrs` / `MediatR` 约 `83.183 ns / 32 B` vs `60.915 ns / 232 B`;`Transient` 下约 `86.243 ns / 56 B` vs `59.644 ns / 232 B` + - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check` + - 结果:通过 + - `git diff --check` + - 结果:通过 +- 本轮结论: + - `BenchmarkDotNet.Artifacts/` 现在不再是工作树噪音源 + - request benchmark 已从“偶尔人工观察”升级为 CQRS 性能波次的默认回归门槛 + - 当前离“至少超过反射版 `MediatR`”还有明确差距,所以下一批优化必须围绕 request steady-state 常量开销继续下钻,而不是只增加更多 benchmark 维度 + ### 阶段:request 热路径 benchmark 收口与 NuGet `Mediator` 对照补齐(CQRS-REWRITE-RP-101) - 延续 `$gframework-batch-boot 50`,本轮先按 `origin/main` 复核 branch diff 基线: