From 120a1487f53e7ec0deb2535491c2e9763863e9ee Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 11:38:27 +0800
Subject: [PATCH 1/7] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E5=8F=A3=E8=AF=B7?=
=?UTF-8?q?=E6=B1=82=E7=83=AD=E8=B7=AF=E5=BE=84=E5=B8=B8=E9=87=8F=E5=BC=80?=
=?UTF-8?q?=E9=94=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 优化 CqrsDispatcher.SendAsync 的 direct-return ValueTask 路径,移除 dispatcher 自身的异步状态机开销
- 引入 MicrosoftDiContainer 冻结后服务键索引,收敛 HasRegistration(Type) 的重复描述符扫描
- 更新 cqrs-rewrite active tracking 与 trace,记录 RP-104 的基线、验证结果与下一批建议
---
GFramework.Core/Ioc/MicrosoftDiContainer.cs | 67 +++++++++++++++++++
GFramework.Cqrs/Internal/CqrsDispatcher.cs | 9 ++-
.../todos/cqrs-rewrite-migration-tracking.md | 38 ++++++++---
.../traces/cqrs-rewrite-migration-trace.md | 40 +++++++++++
4 files changed, 141 insertions(+), 13 deletions(-)
diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
index b9896950..461487c2 100644
--- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs
+++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs
@@ -185,6 +185,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
///
private IServiceProvider? _provider;
+ ///
+ /// 冻结后可复用的服务类型可见性索引。
+ /// 容器冻结后注册集合不再变化,因此 可以安全复用该索引。
+ ///
+ private FrozenServiceTypeIndex? _frozenServiceTypeIndex;
+
///
/// 容器冻结状态标志,true表示容器已冻结不可修改
///
@@ -1044,6 +1050,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
EnterReadLockOrThrowDisposed();
try
{
+ if (_frozenServiceTypeIndex is not null)
+ {
+ return _frozenServiceTypeIndex.Contains(type);
+ }
+
return HasRegistrationCore(type);
}
finally
@@ -1139,6 +1150,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_provider = null;
+ _frozenServiceTypeIndex = null;
_frozen = false;
_logger.Info("Container cleared");
}
@@ -1166,6 +1178,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
_provider = GetServicesUnsafe.BuildServiceProvider();
+ _frozenServiceTypeIndex = FrozenServiceTypeIndex.Create(GetServicesUnsafe);
_frozen = true;
_logger.Info("IOC Container frozen - ServiceProvider built");
}
@@ -1175,6 +1188,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
+ ///
+ /// 保存冻结后按服务键可见的精确服务类型与开放泛型定义集合。
+ ///
+ ///
+ /// 该索引只回答“按当前服务键语义是否可见”,因此与 /
+ /// 一样不会退化为更宽松的可赋值匹配。
+ ///
+ private sealed class FrozenServiceTypeIndex(HashSet exactServiceTypes, HashSet openGenericServiceTypes)
+ {
+ private readonly HashSet _exactServiceTypes = exactServiceTypes;
+ private readonly HashSet _openGenericServiceTypes = openGenericServiceTypes;
+
+ ///
+ /// 基于冻结时最终确定的服务描述符集合创建索引。
+ ///
+ /// 冻结时的服务描述符序列。
+ /// 供存在性判断热路径复用的服务键索引。
+ public static FrozenServiceTypeIndex Create(IEnumerable descriptors)
+ {
+ ArgumentNullException.ThrowIfNull(descriptors);
+
+ var exactServiceTypes = new HashSet();
+ var openGenericServiceTypes = new HashSet();
+
+ foreach (var descriptor in descriptors)
+ {
+ var serviceType = descriptor.ServiceType;
+ exactServiceTypes.Add(serviceType);
+
+ if (serviceType.IsGenericTypeDefinition)
+ {
+ openGenericServiceTypes.Add(serviceType);
+ }
+ }
+
+ return new FrozenServiceTypeIndex(exactServiceTypes, openGenericServiceTypes);
+ }
+
+ ///
+ /// 判断当前索引是否声明了目标服务键。
+ ///
+ /// 要检查的服务类型。
+ /// 命中精确服务键或可闭合的开放泛型服务键时返回 。
+ public bool Contains(Type requestedType)
+ {
+ ArgumentNullException.ThrowIfNull(requestedType);
+
+ return _exactServiceTypes.Contains(requestedType) ||
+ requestedType.IsConstructedGenericType &&
+ _openGenericServiceTypes.Contains(requestedType.GetGenericTypeDefinition());
+ }
+ }
+
///
/// 获取底层的服务集合
/// 提供对内部IServiceCollection的访问权限,用于高级配置和自定义操作
@@ -1250,6 +1316,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
_disposed = true;
(_provider as IDisposable)?.Dispose();
_provider = null;
+ _frozenServiceTypeIndex = null;
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_frozen = false;
diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
index a5ba1c80..86caab3d 100644
--- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs
+++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
@@ -105,7 +105,7 @@ internal sealed class CqrsDispatcher(
/// 请求对象。
/// 取消令牌。
/// 请求响应。
- public async ValueTask SendAsync(
+ public ValueTask SendAsync(
ICqrsContext context,
IRequest request,
CancellationToken cancellationToken = default)
@@ -122,7 +122,7 @@ internal sealed class CqrsDispatcher(
PrepareHandler(handler, context);
if (!container.HasRegistration(dispatchBinding.BehaviorType))
{
- return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
+ return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
}
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
@@ -132,9 +132,8 @@ internal sealed class CqrsDispatcher(
PrepareHandler(behavior, context);
}
- return await dispatchBinding.GetPipelineExecutor(behaviors.Count)
- .Invoke(handler, behaviors, request, cancellationToken)
- .ConfigureAwait(false);
+ return dispatchBinding.GetPipelineExecutor(behaviors.Count)
+ .Invoke(handler, behaviors, request, cancellationToken);
}
///
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 56bae975..6ada6984 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-103`
+- 恢复点编号:`CQRS-REWRITE-RP-104`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前结论:
@@ -40,23 +40,26 @@ CQRS 迁移与收敛。
- 当前 `RP-102` 已把 `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 收口后的新基线
- 当前 `RP-102` 已将 `BenchmarkDotNet.Artifacts/` 收口为默认忽略路径,并把 request steady-state / lifetime benchmark 复跑升级为 CQRS 性能相关改动的默认回归门槛;当前阶段目标明确为“持续逼近 source-generated `Mediator`,并至少稳定超过反射版 `MediatR`”
- 当前 `RP-103` 已使用 `$gframework-pr-review` 复核 `PR #340` latest-head review:修复 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext` 因 strict mock 未配置 `HasRegistration(Type)` 产生的 CI 失败,收紧 `MicrosoftDiContainer.HasRegistration(Type)` 到与 `GetAll(Type)` 一致的服务键可见性语义,补齐 `IIocContainer.HasRegistration(Type)` 的异常/XML 契约与 `docs/zh-CN/core/ioc.md` 的用户接入说明,并同步 benchmark 注释与 active tracking/trace 到当前 PR 锚点
-- `ai-plan` active 入口现以 `RP-103` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-104` 已继续沿用 `$gframework-batch-boot 50` 压 request 热路径:先把 `CqrsDispatcher.SendAsync(...)` 改成 direct-return `ValueTask`,移除 dispatcher 自身的 `async/await` 状态机;再让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,避免每次命中零 pipeline request 都线性扫描全部描述符;本轮 benchmark 表明第一刀显著压低 steady-state / lifetime request,第二刀在当前短跑下主要确认“无回退、收益不明显”
+- `ai-plan` active 入口现以 `RP-104` 为最新恢复锚点;`PR #340`、`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 仍为 `10 files / 298 lines`;本轮待提交工作树以 `.gitignore`、benchmark README 与 active tracking/trace 更新为主,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
+- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
+- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`;本轮待提交工作树目前集中在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Core/Ioc/MicrosoftDiContainer.cs` 与 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.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`)
+- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `6.141 ns / 32 B`、`6.674 ns / 32 B`、`61.803 ns / 232 B`、`70.298 ns / 32 B`
+- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `73.005 ns / 32 B`,`Transient` 下约 `74.757 ns / 56 B`;相较 `RP-103` 前的 `83.183 ns / 32 B` 与 `86.243 ns / 56 B` 已继续下降
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
- `docs/zh-CN/core/ioc.md` 已新增 `HasRegistration(Type)` 的使用语义、热路径用途与“按服务键而非可赋值关系判断”的示例说明
- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间
+- 本轮 `SendAsync(...)` 的 direct-return `ValueTask` 改动已证明确实是有效热点:同样的短跑配置下,`GFramework.Cqrs` steady-state request 从约 `83.823 ns` 下探到 `69-70 ns` 区间
+- 冻结后 `HasRegistration(Type)` 服务键索引化在当前短跑下没有带来同等量级的可见收益,但也没有引入功能回退或额外分配;后续若继续压零 pipeline request,应优先重新评估“默认 request 路径进一步吸收 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`
@@ -138,6 +141,25 @@ CQRS 迁移与收敛。
- `git diff --check`
- 结果:通过
- 备注:当前仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式错误
+- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
+ - 结果:通过,`52/52` passed
+- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"`
+ - 结果:通过,`14/14` passed
+- `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 "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:本轮两批热路径收口后的最新 steady-state request 对照约为 baseline `6.141 ns / 32 B`、`Mediator` `6.674 ns / 32 B`、`MediatR` `61.803 ns / 232 B`、`GFramework.Cqrs` `70.298 ns / 32 B`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.706 ns / 52.197 ns / 73.005 ns`,`Transient` 下 = `4.571 ns / 50.175 ns / 74.757 ns`
+- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
+ - 备注:仍仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式问题
- `dotnet pack GFramework.sln -c Release --no-restore -o /tmp/gframework-pack-validation -p:IncludeSymbols=false`
- 结果:通过
- 备注:当前本地产物仅包含 14 个预期发布包,未生成 `GFramework.Cqrs.Benchmarks.*.nupkg`
@@ -258,8 +280,8 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先对 `CqrsDispatcher.SendAsync(...)` / request invoker 绑定 / handler 调用适配做更细粒度热点拆分,并在每次改动后立即复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks`
-2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批优先评估默认 request 路径吸收 generated invoker/provider 或继续裁掉 dispatch binding / delegate 适配层的剩余常量开销
+1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先集中评估默认 request 路径进一步吸收 generated invoker/provider 的空间,而不是继续堆叠同层级容器微优化
+2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批应拆分 `RequestDispatchBinding` / handler 调用适配层的剩余常量开销,并在每次改动后立即复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks`
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 a215f20d..002ee574 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,46 @@
## 2026-05-08
+### 阶段:request 热路径继续收口(CQRS-REWRITE-RP-104)
+
+- 延续 `$gframework-batch-boot 50`,本轮先重新按 `origin/main` 复核 branch diff 基线:
+ - `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
+ - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`
+ - 当前工作树在真正落代码前只有活跃文档更新,仍明显低于 `$gframework-batch-boot 50` 的文件阈值,因此继续自动推进下一批 request 热路径收口
+- 本轮接受的只读探索结论:
+ - `RequestBenchmarks` / `RequestInvokerBenchmarks` 的下一个低风险热点仍在“每次发送都必经的容器查询与短生命周期对象创建”,不是重新回到更高风险的语义层重构
+ - 候选优先级排序为:`SendAsync` 自身状态机开销、`HasRegistration + GetAll` / 服务键扫描,以及 pipeline continuation 的临时对象
+- 本轮主线程决策:
+ - 先以最小行为改动切第一刀:把 `CqrsDispatcher.SendAsync(...)` 从 `async/await` 改为 direct-return `ValueTask`,让零 pipeline request 常见路径不再为 dispatcher 自身生成额外状态机
+ - 在第一刀验证通过且 benchmark 明显改善后,再切第二刀:让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,而不是每次线性扫描全部 `ServiceDescriptor`
+ - 第二刀完成后停止继续叠第三刀,因为当前批次已经能清晰区分“有效收益”和“无回退但收益不明显”的因果,不再为了追逐更小常量开销降低评审清晰度
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
+ - 结果:通过,`52/52` passed
+ - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"`
+ - 结果:通过,`14/14` passed
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:最新 steady-state request 对照约为 baseline `6.141 ns / 32 B`、`Mediator` `6.674 ns / 32 B`、`MediatR` `61.803 ns / 232 B`、`GFramework.Cqrs` `70.298 ns / 32 B`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.706 ns / 52.197 ns / 73.005 ns`,`Transient` 下 = `4.571 ns / 50.175 ns / 74.757 ns`
+ - `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+ - 备注:仍仅有 `GFramework.sln` 的历史 CRLF 警告,无本轮新增格式问题
+- 本轮结论:
+ - 第一刀有效:`CqrsDispatcher.SendAsync(...)` 的 direct-return `ValueTask` 把 `GFramework.Cqrs` steady-state request 从 `RP-103` 记录的约 `83.823 ns` 压到约 `70.298 ns`
+ - 第二刀保守有效:冻结后 `HasRegistration(Type)` 索引化没有带来同量级的可见收益,但也没有造成功能回退、额外分配或测试破坏
+ - 下一批若继续压 request hot path,应优先评估默认 request 路径吸收 generated invoker/provider,而不是继续围绕同层级容器存在性判断做微调
+
### 阶段:PR #340 latest-head review 收口(CQRS-REWRITE-RP-103)
- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR,并确认当前锚点已从 `PR #339` 更新为 `PR #340`
From d9547dae4b4877eb96ba9df038a2053bdb4687e2 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 12:23:05 +0800
Subject: [PATCH 2/7] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E5=8F=A3=E9=BB=98?=
=?UTF-8?q?=E8=AE=A4=E8=AF=B7=E6=B1=82=E5=9F=BA=E5=87=86=E5=AE=BF=E4=B8=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 handwritten generated request registry,并让默认 RequestBenchmarks 通过真实程序集注册路径接上 generated invoker provider
- 补齐 benchmark 最小宿主所需的 CQRS runtime、registrar 与 registration service 基础设施接线
- 更新 CQRS 迁移 tracking 与 trace,记录 RP-105 的 benchmark 结论和当前恢复点
---
.../Messaging/BenchmarkHostFactory.cs | 45 ++++++++
...eneratedDefaultRequestBenchmarkRegistry.cs | 100 ++++++++++++++++++
.../Messaging/RequestBenchmarks.cs | 13 ++-
GFramework.Cqrs.Benchmarks/README.md | 2 +-
.../todos/cqrs-rewrite-migration-tracking.md | 24 ++++-
.../traces/cqrs-rewrite-migration-trace.md | 30 ++++++
6 files changed, 205 insertions(+), 9 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
index 886f2c71..a872b5a8 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
@@ -3,10 +3,12 @@
using System;
using System.Linq;
+using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
+using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
namespace GFramework.Cqrs.Benchmarks.Messaging;
@@ -31,11 +33,54 @@ internal static class BenchmarkHostFactory
ArgumentNullException.ThrowIfNull(configure);
var container = new MicrosoftDiContainer();
+ RegisterCqrsInfrastructure(container);
configure(container);
container.Freeze();
return container;
}
+ ///
+ /// 为 benchmark 宿主补齐默认 CQRS runtime seam,确保它既能手工注册 handler,也能走真实的程序集注册入口。
+ ///
+ /// 当前 benchmark 拥有的 GFramework 容器。
+ ///
+ /// `RegisterCqrsHandlersFromAssembly(...)` 依赖预先可见的 runtime / registrar / registration service 实例绑定。
+ /// benchmark 宿主直接使用裸 ,因此需要在配置阶段先补齐这组基础设施,
+ /// 避免各个 benchmark 用例各自复制同一段前置接线逻辑。
+ ///
+ private static void RegisterCqrsInfrastructure(MicrosoftDiContainer container)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ if (container.Get() is null)
+ {
+ var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
+ var notificationPublisher = container.Get();
+ var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
+ container.Register(runtime);
+ container.Register((LegacyICqrsRuntime)runtime);
+ }
+ else if (container.Get() is null)
+ {
+ container.Register((LegacyICqrsRuntime)container.GetRequired());
+ }
+
+ if (container.Get() is null)
+ {
+ var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
+ var registrar = GFramework.Cqrs.CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
+ container.Register(registrar);
+ }
+
+ if (container.Get() is null)
+ {
+ var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
+ var registrar = container.GetRequired();
+ var registrationService = GFramework.Cqrs.CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
+ container.Register(registrationService);
+ }
+ }
+
///
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
///
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs
new file mode 100644
index 00000000..59646dc1
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs
@@ -0,0 +1,100 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
+ typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultRequestBenchmarkRegistry))]
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 为默认 request steady-state benchmark 提供 hand-written generated registry,
+/// 以便验证“默认宿主吸收 generated request invoker provider”后的热路径收益。
+///
+public sealed class GeneratedDefaultRequestBenchmarkRegistry :
+ GFramework.Cqrs.ICqrsHandlerRegistry,
+ GFramework.Cqrs.ICqrsRequestInvokerProvider,
+ GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
+{
+ private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
+ new(
+ typeof(IRequestHandler<
+ RequestBenchmarks.BenchmarkRequest,
+ RequestBenchmarks.BenchmarkResponse>),
+ typeof(GeneratedDefaultRequestBenchmarkRegistry).GetMethod(
+ nameof(InvokeBenchmarkRequestHandler),
+ BindingFlags.Public | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Missing generated default request benchmark method."));
+
+ private static readonly IReadOnlyList Descriptors =
+ [
+ new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
+ typeof(RequestBenchmarks.BenchmarkRequest),
+ typeof(RequestBenchmarks.BenchmarkResponse),
+ Descriptor)
+ ];
+
+ ///
+ /// 把默认 request benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddSingleton(
+ typeof(IRequestHandler),
+ typeof(RequestBenchmarks.BenchmarkRequestHandler));
+ logger.Debug("Registered generated default request benchmark handler.");
+ }
+
+ ///
+ /// 返回当前 provider 暴露的全部 generated request invoker 描述符。
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return Descriptors;
+ }
+
+ ///
+ /// 为目标请求/响应类型对返回 generated request invoker 描述符。
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(RequestBenchmarks.BenchmarkRequest) &&
+ responseType == typeof(RequestBenchmarks.BenchmarkResponse))
+ {
+ descriptor = Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ /// 模拟 generated invoker provider 为默认 request benchmark 产出的开放静态调用入口。
+ ///
+ public static ValueTask InvokeBenchmarkRequestHandler(
+ object handler,
+ object request,
+ CancellationToken cancellationToken)
+ {
+ var typedHandler = (IRequestHandler<
+ RequestBenchmarks.BenchmarkRequest,
+ RequestBenchmarks.BenchmarkResponse>)handler;
+ var typedRequest = (RequestBenchmarks.BenchmarkRequest)request;
+ return typedHandler.Handle(typedRequest, cancellationToken);
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
index 37840a94..a84c6d1a 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
@@ -61,12 +61,12 @@ public class RequestBenchmarks
MinLevel = LogLevel.Fatal
};
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterSingleton>(
- _baselineHandler);
+ container.RegisterCqrsHandlersFromAssembly(typeof(RequestBenchmarks).Assembly);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
@@ -91,7 +91,14 @@ public class RequestBenchmarks
[GlobalCleanup]
public void Cleanup()
{
- BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
+ try
+ {
+ BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
+ }
+ finally
+ {
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+ }
}
///
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index 9fc28cbd..20558821 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、NuGet `Mediator` source-generated concrete path、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
+ - 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 / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestPipelineBenchmarks.cs`
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 6ada6984..01865d69 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-104`
+- 恢复点编号:`CQRS-REWRITE-RP-105`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前结论:
@@ -41,18 +41,19 @@ CQRS 迁移与收敛。
- 当前 `RP-102` 已将 `BenchmarkDotNet.Artifacts/` 收口为默认忽略路径,并把 request steady-state / lifetime benchmark 复跑升级为 CQRS 性能相关改动的默认回归门槛;当前阶段目标明确为“持续逼近 source-generated `Mediator`,并至少稳定超过反射版 `MediatR`”
- 当前 `RP-103` 已使用 `$gframework-pr-review` 复核 `PR #340` latest-head review:修复 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext` 因 strict mock 未配置 `HasRegistration(Type)` 产生的 CI 失败,收紧 `MicrosoftDiContainer.HasRegistration(Type)` 到与 `GetAll(Type)` 一致的服务键可见性语义,补齐 `IIocContainer.HasRegistration(Type)` 的异常/XML 契约与 `docs/zh-CN/core/ioc.md` 的用户接入说明,并同步 benchmark 注释与 active tracking/trace 到当前 PR 锚点
- 当前 `RP-104` 已继续沿用 `$gframework-batch-boot 50` 压 request 热路径:先把 `CqrsDispatcher.SendAsync(...)` 改成 direct-return `ValueTask`,移除 dispatcher 自身的 `async/await` 状态机;再让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,避免每次命中零 pipeline request 都线性扫描全部描述符;本轮 benchmark 表明第一刀显著压低 steady-state / lifetime request,第二刀在当前短跑下主要确认“无回退、收益不明显”
-- `ai-plan` active 入口现以 `RP-104` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state:为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider;本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B`,`Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B` 与 `73.157 ns / 56 B`
+- `ai-plan` active 入口现以 `RP-105` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
-- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`;本轮待提交工作树目前集中在 `GFramework.Cqrs/Internal/CqrsDispatcher.cs`、`GFramework.Core/Ioc/MicrosoftDiContainer.cs` 与 active tracking/trace,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
+- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `4 files / 154 lines`;本批待提交工作树额外集中在 `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`、`GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`,约 `4 files / 160 changed lines`,合并后仍明显低于 `$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` 四方对照:最新约 `6.141 ns / 32 B`、`6.674 ns / 32 B`、`61.803 ns / 232 B`、`70.298 ns / 32 B`
-- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `73.005 ns / 32 B`,`Transient` 下约 `74.757 ns / 56 B`;相较 `RP-103` 前的 `83.183 ns / 32 B` 与 `86.243 ns / 56 B` 已继续下降
+- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `5.013 ns / 32 B`、`5.747 ns / 32 B`、`51.588 ns / 232 B`、`65.296 ns / 32 B`
+- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `68.772 ns / 32 B`,`Transient` 下约 `73.157 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 已继续下降
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
@@ -60,6 +61,7 @@ CQRS 迁移与收敛。
- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间
- 本轮 `SendAsync(...)` 的 direct-return `ValueTask` 改动已证明确实是有效热点:同样的短跑配置下,`GFramework.Cqrs` steady-state request 从约 `83.823 ns` 下探到 `69-70 ns` 区间
- 冻结后 `HasRegistration(Type)` 服务键索引化在当前短跑下没有带来同等量级的可见收益,但也没有引入功能回退或额外分配;后续若继续压零 pipeline request,应优先重新评估“默认 request 路径进一步吸收 generated invoker/provider”而不是继续堆叠同层级微优化
+- 默认 `RequestBenchmarks` 现在已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated request invoker provider,不再只代表纯反射 request binding 路径
- 当前性能回归门槛已收紧为:只要改动触达 `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`
@@ -155,6 +157,18 @@ CQRS 迁移与收敛。
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.706 ns / 52.197 ns / 73.005 ns`,`Transient` 下 = `4.571 ns / 50.175 ns / 74.757 ns`
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
+- `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.013 ns / 32 B`、`Mediator` `5.747 ns / 32 B`、`MediatR` `51.588 ns / 232 B`、`GFramework.Cqrs` `65.296 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`
+ - 结果:通过
+ - 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.817 ns / 48.177 ns / 68.772 ns`,`Transient` 下 = `4.841 ns / 51.753 ns / 73.157 ns`
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
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 002ee574..bdf12ccc 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,36 @@
## 2026-05-08
+### 阶段:默认 request benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-105)
+
+- 延续 `$gframework-batch-boot 50`,本轮先确认失败试验已手工回退回 `RP-104` 的已验证状态,再重新评估“默认 request 路径继续逼近 source-generated `Mediator`”的下一刀
+- 本轮接受的只读探索结论:
+ - 继续在 `CqrsDispatcher` 或 `MicrosoftDiContainer` 上堆叠同层级微优化的性价比已经下降,而且上一轮“总是 `GetAll(Type)`”的试验已被 benchmark 明确否决
+ - 默认 `RequestBenchmarks` 虽然已包含 `Mediator` 对照,但当前 GFramework 组仍只注册了单个 handler 实例,没有走 `RegisterCqrsHandlersFromAssembly(...)` + generated registry/provider 的真实宿主接线路径
+ - `RequestInvokerBenchmarks` 已证明 generated request invoker provider 路径比纯反射 binding 更接近目标,因此下一批最小切片应先把这条收益吸收到默认 steady-state request benchmark
+- 本轮主线程决策:
+ - 在 `BenchmarkHostFactory` 内补齐 benchmark 最小宿主的 CQRS 基础设施预接线:runtime、legacy alias、registrar、registration service
+ - 新增 `GeneratedDefaultRequestBenchmarkRegistry`,用 handwritten generated registry + `ICqrsRequestInvokerProvider` + `IEnumeratesCqrsRequestInvokerDescriptors` 为 `RequestBenchmarks.BenchmarkRequest` 提供真实的 generated request invoker descriptor
+ - 让 `RequestBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(RequestBenchmarks).Assembly)` 建容器,并在 `Setup/Cleanup` 前后显式清理 dispatcher 静态缓存,避免前一组 benchmark 污染默认 request steady-state 结果
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+ - `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.013 ns / 32 B`、`Mediator` `5.747 ns / 32 B`、`MediatR` `51.588 ns / 232 B`、`GFramework.Cqrs` `65.296 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` 约 `68.772 ns / 32 B` vs `48.177 ns / 232 B`;`Transient` 下约 `73.157 ns / 56 B` vs `51.753 ns / 232 B`
+- 本轮结论:
+ - 默认 request benchmark 现在终于测到了“默认宿主已吸收 generated request invoker provider”后的真实 steady-state,而不再只是纯反射 request binding
+ - 这条宿主层收口在不改 runtime 语义的前提下,把 `GFramework.Cqrs` steady-state request 从约 `70.298 ns` 再压到约 `65.296 ns`
+ - lifetime 矩阵也同步改善到 `68.772 ns / 73.157 ns`,说明默认 request 宿主吸收 generated provider 不只是 benchmark 口径变化,而是对常见 handler 生命周期也有稳定收益
+ - 下一批若继续沿用 `$gframework-batch-boot 50`,应优先转向 pipeline 路径或 handler 解析热路径中仍未吸收 generated/provider 收益的常量开销,而不是回头重试已被否决的 `GetAll(Type)` 零行为探测方案
+
### 阶段:request 热路径继续收口(CQRS-REWRITE-RP-104)
- 延续 `$gframework-batch-boot 50`,本轮先重新按 `origin/main` 复核 branch diff 基线:
From c82e981b7e0d492d81318d59f441f6e923867a79 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 12:38:18 +0800
Subject: [PATCH 3/7] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E5=8F=A3=E8=AF=B7?=
=?UTF-8?q?=E6=B1=82=E7=AE=A1=E7=BA=BF=E5=9F=BA=E5=87=86=E5=AE=BF=E4=B8=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 request pipeline benchmark 的 handwritten generated request registry,并通过真实程序集注册路径接上 generated invoker provider
- 更新 RequestPipelineBenchmarks 宿主接线与 benchmark README,统一默认 request 与 pipeline 场景的 generated-provider 口径
- 更新 CQRS 迁移 tracking 与 trace,记录 RP-106 的基线、验证结果与下一恢复点
---
...neratedRequestPipelineBenchmarkRegistry.cs | 100 ++++++++++++++++++
.../Messaging/RequestPipelineBenchmarks.cs | 3 +-
GFramework.Cqrs.Benchmarks/README.md | 2 +-
.../todos/cqrs-rewrite-migration-tracking.md | 21 ++--
.../traces/cqrs-rewrite-migration-trace.md | 39 +++++++
5 files changed, 153 insertions(+), 12 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs
new file mode 100644
index 00000000..5844cee1
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs
@@ -0,0 +1,100 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
+ typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestPipelineBenchmarkRegistry))]
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 为 request pipeline benchmark 提供 handwritten generated registry,
+/// 让默认 pipeline 宿主也能走真实的 generated request invoker provider 接线路径。
+///
+public sealed class GeneratedRequestPipelineBenchmarkRegistry :
+ GFramework.Cqrs.ICqrsHandlerRegistry,
+ GFramework.Cqrs.ICqrsRequestInvokerProvider,
+ GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
+{
+ private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
+ new(
+ typeof(IRequestHandler<
+ RequestPipelineBenchmarks.BenchmarkRequest,
+ RequestPipelineBenchmarks.BenchmarkResponse>),
+ typeof(GeneratedRequestPipelineBenchmarkRegistry).GetMethod(
+ nameof(InvokeBenchmarkRequestHandler),
+ BindingFlags.Public | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Missing generated request pipeline benchmark method."));
+
+ private static readonly IReadOnlyList Descriptors =
+ [
+ new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
+ typeof(RequestPipelineBenchmarks.BenchmarkRequest),
+ typeof(RequestPipelineBenchmarks.BenchmarkResponse),
+ Descriptor)
+ ];
+
+ ///
+ /// 将 request pipeline benchmark handler 注册为单例,保持与当前矩阵宿主一致的生命周期语义。
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddSingleton(
+ typeof(IRequestHandler),
+ typeof(RequestPipelineBenchmarks.BenchmarkRequestHandler));
+ logger.Debug("Registered generated request pipeline benchmark handler.");
+ }
+
+ ///
+ /// 返回当前 provider 暴露的全部 generated request invoker 描述符。
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return Descriptors;
+ }
+
+ ///
+ /// 为目标请求/响应类型对返回 generated request invoker 描述符。
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(RequestPipelineBenchmarks.BenchmarkRequest) &&
+ responseType == typeof(RequestPipelineBenchmarks.BenchmarkResponse))
+ {
+ descriptor = Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ /// 模拟 generated invoker provider 为 request pipeline benchmark 产出的开放静态调用入口。
+ ///
+ public static ValueTask InvokeBenchmarkRequestHandler(
+ object handler,
+ object request,
+ CancellationToken cancellationToken)
+ {
+ var typedHandler = (IRequestHandler<
+ RequestPipelineBenchmarks.BenchmarkRequest,
+ RequestPipelineBenchmarks.BenchmarkResponse>)handler;
+ var typedRequest = (RequestPipelineBenchmarks.BenchmarkRequest)request;
+ return typedHandler.Handle(typedRequest, cancellationToken);
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
index 07dbc0c5..23d703ec 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
@@ -69,8 +69,7 @@ public class RequestPipelineBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterSingleton>(
- _baselineHandler);
+ container.RegisterCqrsHandlersFromAssembly(typeof(RequestPipelineBenchmarks).Assembly);
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index 20558821..35d7c81e 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -19,7 +19,7 @@
- `Messaging/RequestLifetimeBenchmarks.cs`
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestPipelineBenchmarks.cs`
- - `0 / 1 / 4` 个 pipeline 行为下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
+ - `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`
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 01865d69..c075d643 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-105`
+- 恢复点编号:`CQRS-REWRITE-RP-106`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前结论:
@@ -42,18 +42,21 @@ CQRS 迁移与收敛。
- 当前 `RP-103` 已使用 `$gframework-pr-review` 复核 `PR #340` latest-head review:修复 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext` 因 strict mock 未配置 `HasRegistration(Type)` 产生的 CI 失败,收紧 `MicrosoftDiContainer.HasRegistration(Type)` 到与 `GetAll(Type)` 一致的服务键可见性语义,补齐 `IIocContainer.HasRegistration(Type)` 的异常/XML 契约与 `docs/zh-CN/core/ioc.md` 的用户接入说明,并同步 benchmark 注释与 active tracking/trace 到当前 PR 锚点
- 当前 `RP-104` 已继续沿用 `$gframework-batch-boot 50` 压 request 热路径:先把 `CqrsDispatcher.SendAsync(...)` 改成 direct-return `ValueTask`,移除 dispatcher 自身的 `async/await` 状态机;再让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,避免每次命中零 pipeline request 都线性扫描全部描述符;本轮 benchmark 表明第一刀显著压低 steady-state / lifetime request,第二刀在当前短跑下主要确认“无回退、收益不明显”
- 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state:为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider;本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B`,`Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B` 与 `73.157 ns / 56 B`
-- `ai-plan` active 入口现以 `RP-105` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-106` 已把同一套 generated-provider 宿主收口扩展到 `RequestPipelineBenchmarks`:新增 handwritten `GeneratedRequestPipelineBenchmarkRegistry`,并让 `RequestPipelineBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` + benchmark CQRS 基础设施预接线;本轮 benchmark 表明 `0 pipeline` steady-state 进一步收敛到约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 在短跑噪音下维持约 `555.083 ns / 896 B`
+- `ai-plan` active 入口现以 `RP-106` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
-- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `4 files / 154 lines`;本批待提交工作树额外集中在 `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`、`GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`,约 `4 files / 160 changed lines`,合并后仍明显低于 `$gframework-batch-boot 50` 的文件阈值
+- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `8 files / 358 lines`
+- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`,新增 generated-provider pipeline 宿主接线后仍明显低于 `$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.013 ns / 32 B`、`5.747 ns / 32 B`、`51.588 ns / 232 B`、`65.296 ns / 32 B`
-- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `68.772 ns / 32 B`,`Transient` 下约 `73.157 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 已继续下降
+- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `5.680 ns / 32 B`、`6.565 ns / 32 B`、`54.737 ns / 232 B`、`63.644 ns / 32 B`
+- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.896 ns / 32 B`,`Transient` 下约 `72.880 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 已继续下降
+- 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline` 约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 约 `555.083 ns / 896 B`
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
@@ -61,7 +64,7 @@ CQRS 迁移与收敛。
- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间
- 本轮 `SendAsync(...)` 的 direct-return `ValueTask` 改动已证明确实是有效热点:同样的短跑配置下,`GFramework.Cqrs` steady-state request 从约 `83.823 ns` 下探到 `69-70 ns` 区间
- 冻结后 `HasRegistration(Type)` 服务键索引化在当前短跑下没有带来同等量级的可见收益,但也没有引入功能回退或额外分配;后续若继续压零 pipeline request,应优先重新评估“默认 request 路径进一步吸收 generated invoker/provider”而不是继续堆叠同层级微优化
-- 默认 `RequestBenchmarks` 现在已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated request invoker provider,不再只代表纯反射 request binding 路径
+- 默认 `RequestBenchmarks` 与 `RequestPipelineBenchmarks` 现在都已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated request invoker provider,不再只代表纯反射 request binding 路径
- 当前性能回归门槛已收紧为:只要改动触达 `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`
@@ -294,9 +297,9 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先集中评估默认 request 路径进一步吸收 generated invoker/provider 的空间,而不是继续堆叠同层级容器微优化
-2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批应拆分 `RequestDispatchBinding` / handler 调用适配层的剩余常量开销,并在每次改动后立即复跑 `RequestBenchmarks` 与 `RequestLifetimeBenchmarks`
-3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 矩阵,而不是先横向堆更多低价值场景
+1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先查看 `RequestLifetimeBenchmarks` 与 stream/notification 对照里是否还有未吸收 generated-provider 宿主收益的低风险切片
+2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批应继续围绕 request dispatch / pipeline 路径的剩余常量开销下钻,并在每次改动后立即复跑 `RequestBenchmarks`、`RequestLifetimeBenchmarks` 与 `RequestPipelineBenchmarks`
+3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 或 stream 对照矩阵,而不是回头重试已被 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 bdf12ccc..7f910b24 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,45 @@
## 2026-05-08
+### 阶段:request pipeline benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-106)
+
+- 延续 `$gframework-batch-boot 50`,本轮基于 `RP-105` 已验证的默认 request 宿主接线继续推进,并先复核 branch diff 基线:
+ - `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
+ - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `8 files / 358 lines`
+ - 当前工作树待提交改动只集中在 `RequestPipelineBenchmarks`、对应 handwritten generated registry 与 benchmark `README`,因此继续自动推进下一批 pipeline 宿主收口
+- 本轮接受的只读探索结论:
+ - `RP-105` 已证明“让默认 request 宿主真实接上 generated request invoker provider”能稳定压低 steady-state request,因此 pipeline benchmark 仍保留旧的“直接注册单个 handler”路径会让口径不对齐
+ - 之前已被 benchmark 否决的“总是 `GetAll(Type)` 做零 pipeline 探测”不应回头重试;下一刀更合理的是把 pipeline benchmark 也切到真实程序集注册入口
+ - `RequestPipelineBenchmarks` 只需要补一份与 `RequestBenchmarks` 对称的 handwritten generated registry,就能最小化改动并保持 runtime 语义不变
+- 本轮主线程决策:
+ - 新增 `GeneratedRequestPipelineBenchmarkRegistry`,用 handwritten generated registry + `ICqrsRequestInvokerProvider` + `IEnumeratesCqrsRequestInvokerDescriptors` 为 `RequestPipelineBenchmarks.BenchmarkRequest` 提供真实的 generated request invoker descriptor
+ - 让 `RequestPipelineBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(RequestPipelineBenchmarks).Assembly)` 建容器,只把 pipeline 行为数量矩阵保留在 benchmark 自己的显式注册里
+ - 更新 `GFramework.Cqrs.Benchmarks/README.md`,明确 request pipeline benchmark 也已接上 handwritten generated request invoker provider
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+ - `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.680 ns / 32 B`、`Mediator` `6.565 ns / 32 B`、`MediatR` `54.737 ns / 232 B`、`GFramework.Cqrs` `63.644 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` 约 `69.896 ns / 32 B` vs `57.469 ns / 232 B`;`Transient` 下约 `72.880 ns / 56 B` vs `55.106 ns / 232 B`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestPipelineBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:第一次短跑为 `PipelineCount=0` `64.928 ns / 32 B`、`PipelineCount=1` `366.468 ns / 536 B`、`PipelineCount=4` `547.800 ns / 896 B`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestPipelineBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:复跑确认后为 `PipelineCount=0` `64.755 ns / 32 B`、`PipelineCount=1` `353.141 ns / 536 B`、`PipelineCount=4` `555.083 ns / 896 B`
+- 本轮结论:
+ - request pipeline benchmark 现在已与默认 request steady-state 使用同一条 generated-provider 宿主接线路径,后续再看 `0 / 1 / 4` 行为矩阵时不再混入“默认 request 已吸收 generated invoker,而 pipeline 还停在纯反射宿主”的口径偏差
+ - `0 pipeline` steady-state 继续下探到约 `64.755 ns / 32 B`,与 `RP-105` 的默认 request benchmark 收敛方向一致,说明这条宿主接线收益能稳定复用到 pipeline benchmark
+ - `1 pipeline` 与 `4 pipeline` 结果在当前 short job 配置下存在噪音,但没有出现清晰的新增分配或显著退化;因此本轮适合作为低风险宿主收口批次接受
+ - 下一批若继续沿用 `$gframework-batch-boot 50`,应优先查看 request lifetime、stream 或 notification benchmark 中是否还存在未吸收 generated-provider 宿主收益的对称切片,而不是回头重试已被 benchmark 否决的 runtime 微优化
+
### 阶段:默认 request benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-105)
- 延续 `$gframework-batch-boot 50`,本轮先确认失败试验已手工回退回 `RP-104` 的已验证状态,再重新评估“默认 request 路径继续逼近 source-generated `Mediator`”的下一刀
From 24462b0035d65128880f41b1c26598b8ced83756 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 12:47:24 +0800
Subject: [PATCH 4/7] =?UTF-8?q?perf(cqrs):=20=E6=94=B6=E5=8F=A3=E9=BB=98?=
=?UTF-8?q?=E8=AE=A4=E6=B5=81=E5=BC=8F=E5=9F=BA=E5=87=86=E5=AE=BF=E4=B8=BB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增默认 stream benchmark 的 handwritten generated registry,并通过真实程序集注册路径接上 generated stream invoker provider
- 更新 StreamingBenchmarks 宿主接线、README 与 RP-107 recovery 文档,统一 request、pipeline、stream 默认宿主口径
- 更新 gframework-boot 与 gframework-batch-boot 技能,改为以上下文预算接近约 80% 为默认优先停止信号
---
.agents/skills/gframework-batch-boot/SKILL.md | 25 ++++-
.agents/skills/gframework-boot/SKILL.md | 12 ++-
...eratedDefaultStreamingBenchmarkRegistry.cs | 96 +++++++++++++++++++
.../Messaging/StreamingBenchmarks.cs | 16 +++-
GFramework.Cqrs.Benchmarks/README.md | 2 +-
.../todos/cqrs-rewrite-migration-tracking.md | 22 +++--
.../traces/cqrs-rewrite-migration-trace.md | 37 +++++++
7 files changed, 193 insertions(+), 17 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs
diff --git a/.agents/skills/gframework-batch-boot/SKILL.md b/.agents/skills/gframework-batch-boot/SKILL.md
index 860ca496..857d8964 100644
--- a/.agents/skills/gframework-batch-boot/SKILL.md
+++ b/.agents/skills/gframework-batch-boot/SKILL.md
@@ -12,6 +12,10 @@ batches until a clear stop condition is met.
Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`; it does not replace it.
+Context budget is a first-class stop signal. Do not keep batching merely because a file-count threshold still has
+headroom if the active conversation, loaded repo artifacts, validation output, and pending recovery updates suggest the
+agent is approaching its safe working-context limit.
+
## Startup Workflow
1. Execute the normal `gframework-boot` startup sequence first:
@@ -28,6 +32,11 @@ Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`;
- repeated test refactor pattern
- module-by-module documentation refresh
- other repetitive multi-file cleanup
+4. Before the first implementation batch, estimate whether the current task is likely to stay below roughly 80% of the
+ agent's safe working-context budget through one more full batch cycle:
+ - include already loaded `AGENTS.md`, skills, `ai-plan` files, recent command output, active diffs, and expected validation output
+ - if another batch would probably push the conversation near the limit, plan to stop after the current batch even if
+ branch-size thresholds still have room
## Baseline Selection
@@ -67,8 +76,15 @@ For shorthand numeric thresholds, use a fixed default baseline:
Choose one primary stop condition before the first batch and restate it to the user.
+When the user does not explicitly override the priority order, use:
+
+1. context-budget safety
+2. semantic batch boundary / reviewability
+3. the user-requested local metric such as files, lines, warnings, or time
+
Common stop conditions:
+- the next batch would likely push the agent above roughly 80% of its safe working-context budget
- branch diff vs baseline approaches a file-count threshold
- warnings-only build reaches a target count
- a specific hotspot list is exhausted
@@ -76,6 +92,9 @@ Common stop conditions:
If multiple stop conditions exist, rank them and treat one as primary.
+Treat file-count or line-count thresholds as coarse repository-scope signals, not as a proxy for AI context health.
+When they disagree with context-budget safety, context-budget safety wins.
+
## Shorthand Stop-Condition Syntax
`gframework-batch-boot` may be invoked with shorthand numeric thresholds when the user clearly wants a branch-size stop
@@ -108,6 +127,7 @@ When shorthand is used:
- current branch and active topic
- selected baseline
- current stop-condition metric
+ - current context-budget posture and whether one more batch is safe
- next candidate slices
2. Keep the critical path local.
3. Delegate only bounded slices with explicit ownership:
@@ -127,7 +147,8 @@ When shorthand is used:
6. After each completed batch:
- integrate or verify the result
- rerun the required validation
- - recompute the primary stop-condition metric
+ - recompute the primary stop-condition metric
+ - reassess whether one more batch would likely push the agent near or beyond roughly 80% context usage
- decide immediately whether to continue or stop
7. Do not require the user to manually trigger every round unless:
- the next slice is ambiguous
@@ -158,6 +179,7 @@ For multi-batch work, keep recovery artifacts current.
Stop the loop when any of the following becomes true:
+- the next batch would likely push the agent near or beyond roughly 80% of its safe working-context budget
- the primary stop condition has been reached or exceeded
- the remaining slices are no longer low-risk
- validation failures indicate the task is no longer repetitive
@@ -165,6 +187,7 @@ Stop the loop when any of the following becomes true:
When stopping, report:
+- whether context budget was the deciding factor
- which baseline was used
- the exact metric value at stop time
- completed batches
diff --git a/.agents/skills/gframework-boot/SKILL.md b/.agents/skills/gframework-boot/SKILL.md
index 55e9b55f..563651bd 100644
--- a/.agents/skills/gframework-boot/SKILL.md
+++ b/.agents/skills/gframework-boot/SKILL.md
@@ -36,14 +36,18 @@ Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup se
- `simple`: one concern, one file or module, no parallel discovery required
- `medium`: a small number of modules, some read-only exploration helpful, critical path still easy to keep local
- `complex`: cross-module design, migration, large refactor, or work likely to exceed one context window
-11. Apply the delegation policy from `AGENTS.md`:
+11. Estimate the current context-budget posture before substantive execution:
+ - account for loaded startup artifacts, active `ai-plan` files, visible diffs, open validation output, and likely next-step output volume
+ - if the task already appears near roughly 80% of a safe working-context budget, prefer closing the current batch,
+ refreshing recovery artifacts, and stopping at the next natural semantic boundary instead of starting a fresh broad slice
+12. Apply the delegation policy from `AGENTS.md`:
- Keep the critical path local
- Use `explorer` with `gpt-5.1-codex-mini` for narrow read-only questions, tracing, inventory, and comparisons
- Use `worker` with `gpt-5.4` only for bounded implementation tasks with explicit ownership
- Do not delegate purely for ceremony; delegate only when it materially shortens the task or controls context growth
-12. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
+13. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
and the first implementation step.
-13. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
+14. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
## Task Tracking
@@ -69,6 +73,8 @@ For multi-step, cross-module, or interruption-prone work, maintain the repositor
first, then search the mapped active topics before scanning the broader public area.
- If the current branch and the mapped active topics describe the same feature area, prefer resuming those topics first.
- If the repository state suggests in-flight work but no recovery document matches, reconstruct the safest next step from code, tests, and Git state before asking the user for clarification.
+- If the current turn already carries heavy recovery context, broad diffs, or long validation output, prefer a
+ recovery-point update and a clean stop over starting another large slice just because the code task itself remains open.
## Example Triggers
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs
new file mode 100644
index 00000000..57a1b9a2
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs
@@ -0,0 +1,96 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 为默认 stream steady-state benchmark 提供 hand-written generated registry,
+/// 以便验证“默认 stream 宿主吸收 generated stream invoker provider”后的完整枚举收益。
+///
+public sealed class GeneratedDefaultStreamingBenchmarkRegistry :
+ GFramework.Cqrs.ICqrsHandlerRegistry,
+ GFramework.Cqrs.ICqrsStreamInvokerProvider,
+ GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
+{
+ private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
+ new(
+ typeof(IStreamRequestHandler<
+ StreamingBenchmarks.BenchmarkStreamRequest,
+ StreamingBenchmarks.BenchmarkResponse>),
+ typeof(GeneratedDefaultStreamingBenchmarkRegistry).GetMethod(
+ nameof(InvokeBenchmarkStreamHandler),
+ BindingFlags.Public | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Missing generated default streaming benchmark method."));
+
+ private static readonly IReadOnlyList Descriptors =
+ [
+ new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
+ typeof(StreamingBenchmarks.BenchmarkStreamRequest),
+ typeof(StreamingBenchmarks.BenchmarkResponse),
+ Descriptor)
+ ];
+
+ ///
+ /// 把默认 stream benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ services.AddSingleton(
+ typeof(IStreamRequestHandler),
+ typeof(StreamingBenchmarks.BenchmarkStreamHandler));
+ logger.Debug("Registered generated default streaming benchmark handler.");
+ }
+
+ ///
+ /// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return Descriptors;
+ }
+
+ ///
+ /// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
+ ///
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(StreamingBenchmarks.BenchmarkStreamRequest) &&
+ responseType == typeof(StreamingBenchmarks.BenchmarkResponse))
+ {
+ descriptor = Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ /// 模拟 generated stream invoker provider 为默认 stream benchmark 产出的开放静态调用入口。
+ ///
+ public static object InvokeBenchmarkStreamHandler(
+ object handler,
+ object request,
+ CancellationToken cancellationToken)
+ {
+ var typedHandler = (IStreamRequestHandler<
+ StreamingBenchmarks.BenchmarkStreamRequest,
+ StreamingBenchmarks.BenchmarkResponse>)handler;
+ var typedRequest = (StreamingBenchmarks.BenchmarkStreamRequest)request;
+ return typedHandler.Handle(typedRequest, cancellationToken);
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
index 8b3b1d94..cd6e4a83 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
@@ -18,6 +18,9 @@ using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
+[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
+ typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultStreamingBenchmarkRegistry))]
+
namespace GFramework.Cqrs.Benchmarks.Messaging;
///
@@ -59,12 +62,12 @@ public class StreamingBenchmarks
MinLevel = LogLevel.Fatal
};
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkStreamHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterSingleton>(
- _baselineHandler);
+ container.RegisterCqrsHandlersFromAssembly(typeof(StreamingBenchmarks).Assembly);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
@@ -86,7 +89,14 @@ public class StreamingBenchmarks
[GlobalCleanup]
public void Cleanup()
{
- BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ try
+ {
+ BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ }
+ finally
+ {
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+ }
}
///
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index 35d7c81e..bf0fc053 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -29,7 +29,7 @@
- `Messaging/NotificationBenchmarks.cs`
- `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比
- `Messaging/StreamingBenchmarks.cs`
- - direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比
+ - 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 c075d643..751b8130 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-106`
+- 恢复点编号:`CQRS-REWRITE-RP-107`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前结论:
@@ -43,20 +43,23 @@ CQRS 迁移与收敛。
- 当前 `RP-104` 已继续沿用 `$gframework-batch-boot 50` 压 request 热路径:先把 `CqrsDispatcher.SendAsync(...)` 改成 direct-return `ValueTask`,移除 dispatcher 自身的 `async/await` 状态机;再让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,避免每次命中零 pipeline request 都线性扫描全部描述符;本轮 benchmark 表明第一刀显著压低 steady-state / lifetime request,第二刀在当前短跑下主要确认“无回退、收益不明显”
- 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state:为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider;本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B`,`Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B` 与 `73.157 ns / 56 B`
- 当前 `RP-106` 已把同一套 generated-provider 宿主收口扩展到 `RequestPipelineBenchmarks`:新增 handwritten `GeneratedRequestPipelineBenchmarkRegistry`,并让 `RequestPipelineBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` + benchmark CQRS 基础设施预接线;本轮 benchmark 表明 `0 pipeline` steady-state 进一步收敛到约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 在短跑噪音下维持约 `555.083 ns / 896 B`
-- `ai-plan` active 入口现以 `RP-106` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-107` 已把默认 stream steady-state 宿主也切到 generated-provider 路径:新增 handwritten `GeneratedDefaultStreamingBenchmarkRegistry`,让 `StreamingBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` 并在 setup/cleanup 清理 dispatcher cache;同时将 `gframework-boot` / `gframework-batch-boot` 的默认停止规则改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”,不再把 changed files 误当作唯一阈值
+- `ai-plan` active 入口现以 `RP-107` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
-- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `8 files / 358 lines`
-- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`,新增 generated-provider pipeline 宿主接线后仍明显低于 `$gframework-batch-boot 50` 的文件阈值
+- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `10 files / 507 lines`
+- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs`、`GFramework.Cqrs.Benchmarks/README.md`、`.agents/skills/gframework-batch-boot/SKILL.md` 与 `.agents/skills/gframework-boot/SKILL.md`
+- 当前批次后的默认停止依据已改为 AI 上下文预算:若下一轮预计会让活动对话、已加载 recovery 文档、验证输出与当前 diff 接近约 `80%` 安全上下文占用,应在当前自然批次边界停止,即使 branch diff 仍有余量
- `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.680 ns / 32 B`、`6.565 ns / 32 B`、`54.737 ns / 232 B`、`63.644 ns / 32 B`
-- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.896 ns / 32 B`,`Transient` 下约 `72.880 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 已继续下降
+- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `5.608 ns / 32 B`、`5.445 ns / 32 B`、`57.071 ns / 232 B`、`64.825 ns / 32 B`
+- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.275 ns / 32 B`,`Transient` 下约 `74.301 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 仍维持同一收敛区间
- 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline` 约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 约 `555.083 ns / 896 B`
+- 当前 stream steady-state benchmark 也已切到 generated-provider 宿主接线路径:baseline 约 `5.535 ns / 32 B`、`MediatR` 约 `59.499 ns / 232 B`、`GFramework.Cqrs` 约 `66.778 ns / 32 B`
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
@@ -64,7 +67,8 @@ CQRS 迁移与收敛。
- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间
- 本轮 `SendAsync(...)` 的 direct-return `ValueTask` 改动已证明确实是有效热点:同样的短跑配置下,`GFramework.Cqrs` steady-state request 从约 `83.823 ns` 下探到 `69-70 ns` 区间
- 冻结后 `HasRegistration(Type)` 服务键索引化在当前短跑下没有带来同等量级的可见收益,但也没有引入功能回退或额外分配;后续若继续压零 pipeline request,应优先重新评估“默认 request 路径进一步吸收 generated invoker/provider”而不是继续堆叠同层级微优化
-- 默认 `RequestBenchmarks` 与 `RequestPipelineBenchmarks` 现在都已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated request invoker provider,不再只代表纯反射 request binding 路径
+- 默认 `RequestBenchmarks`、`RequestPipelineBenchmarks` 与 `StreamingBenchmarks` 现在都已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated invoker provider,不再只代表纯反射 binding 路径
+- `gframework-boot` 与 `gframework-batch-boot` 现明确把“上下文预算接近约 80%”视为默认优先停止信号,branch diff files / lines 仅保留为次级仓库范围指标
- 当前性能回归门槛已收紧为:只要改动触达 `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`
@@ -297,8 +301,8 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先查看 `RequestLifetimeBenchmarks` 与 stream/notification 对照里是否还有未吸收 generated-provider 宿主收益的低风险切片
-2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批应继续围绕 request dispatch / pipeline 路径的剩余常量开销下钻,并在每次改动后立即复跑 `RequestBenchmarks`、`RequestLifetimeBenchmarks` 与 `RequestPipelineBenchmarks`
+1. 当前 turn 已接近默认的上下文预算停止线;本次提交后应停止,并在新的 turn 里从 `RP-107` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片
+2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification 或更高价值的 request dispatch 常量开销热点,而不是再机械按 changed files 追加小批次
3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 或 stream 对照矩阵,而不是回头重试已被 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 7f910b24..1c276d20 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,43 @@
## 2026-05-08
+### 阶段:默认 stream benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-107)
+
+- 延续 `$gframework-batch-boot 50`,但本轮按用户新增要求把默认停止依据改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”;在真正落代码前先复核:
+ - `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
+ - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `10 files / 507 lines`
+ - 当前 turn 已加载 `AGENTS.md`、`gframework-batch-boot` / `gframework-boot`、active tracking/trace、上一轮 benchmark 结果与多次 validation 输出,因此继续一个自然批次可以接受,但不应在本次提交后继续无界循环
+- 本轮接受的只读探索结论:
+ - 默认 request / request pipeline 宿主都已吸收 generated provider,但 `StreamingBenchmarks` 仍停在“直接注册单个 stream handler”的旧宿主路径,口径与 `StreamInvokerBenchmarks` / 默认 request 组不对称
+ - 默认 stream steady-state 场景已经足够独立,适合用一份新的 handwritten generated stream registry 最小化收口,而不用再修改 runtime 语义
+ - 用户要求把停止条件从 changed files 改成 AI 上下文预算,因此 skill 文档本身也属于这一批必须一起落下的恢复边界更新
+- 本轮主线程决策:
+ - 新增 `GeneratedDefaultStreamingBenchmarkRegistry`,用 handwritten generated registry + `ICqrsStreamInvokerProvider` + `IEnumeratesCqrsStreamInvokerDescriptors` 为 `StreamingBenchmarks.BenchmarkStreamRequest` 提供真实的 generated stream invoker descriptor
+ - 让 `StreamingBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(StreamingBenchmarks).Assembly)` 建容器,并在 `Setup/Cleanup` 前后显式清理 dispatcher 静态缓存
+ - 更新 `GFramework.Cqrs.Benchmarks/README.md`,明确默认 stream steady-state benchmark 也已接上 handwritten generated stream invoker provider
+ - 更新 `.agents/skills/gframework-batch-boot/SKILL.md` 与 `.agents/skills/gframework-boot/SKILL.md`,明确“上下文预算接近约 80% 时优先停止,branch diff 文件/行数只作次级仓库范围信号”
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+ - `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.608 ns / 32 B`、`Mediator` `5.445 ns / 32 B`、`MediatR` `57.071 ns / 232 B`、`GFramework.Cqrs` `64.825 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` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `4.446 ns / 51.331 ns / 69.275 ns`;`Transient` 下约 `4.918 ns / 56.382 ns / 74.301 ns`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamingBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:默认 stream steady-state 对照约为 baseline `5.535 ns / 32 B`、`MediatR` `59.499 ns / 232 B`、`GFramework.Cqrs` `66.778 ns / 32 B`
+- 本轮结论:
+ - 默认 stream steady-state benchmark 现在也已切到 generated-provider 宿主路径,request / pipeline / stream 三个默认宿主场景的 benchmark 口径终于对齐
+ - `StreamingBenchmarks` 的 `GFramework.Cqrs` 结果约 `66.778 ns / 32 B`,仍慢于 `MediatR`,但没有新增分配或明显回退,说明这次宿主收口是低风险可接受的
+ - 更重要的是,默认停止依据已从“branch diff 文件数是否触顶”改成“AI 上下文预算是否接近约 80%”;结合当前 turn 已加载的大量 recovery/validation/benchmark 输出,本次提交后应主动停止,而不是继续机械扩批
+ - 下一轮若继续性能线,应从 `RP-107` 恢复点重新进入,并优先挑选新的高价值热点族,而不是沿着当前 turn 再追加更多同类宿主收口
+
### 阶段:request pipeline benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-106)
- 延续 `$gframework-batch-boot 50`,本轮基于 `RP-105` 已验证的默认 request 宿主接线继续推进,并先复核 branch diff 基线:
From 39ac61c095bbb94f24c86f238b98bf7cec664a25 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 13:03:00 +0800
Subject: [PATCH 5/7] =?UTF-8?q?fix(cqrs):=20=E8=A1=A5=E9=BD=90=E6=B5=81?=
=?UTF-8?q?=E5=BC=8F=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=9F=BA=E5=87=86?=
=?UTF-8?q?=E7=9F=A9=E9=98=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 stream handler 的 Singleton 和 Transient 生命周期 benchmark,并沿用 generated-provider 宿主接线
- 更新 CQRS benchmark README 与 active ai-plan 恢复点,记录 RP-108 的验证结果和下一步建议
---
...eneratedStreamLifetimeBenchmarkRegistry.cs | 110 +++++++
.../Messaging/StreamLifetimeBenchmarks.cs | 277 ++++++++++++++++++
GFramework.Cqrs.Benchmarks/README.md | 3 +-
.../todos/cqrs-rewrite-migration-tracking.md | 32 +-
.../traces/cqrs-rewrite-migration-trace.md | 40 +++
5 files changed, 454 insertions(+), 8 deletions(-)
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs
create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs
new file mode 100644
index 00000000..977a14a5
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs
@@ -0,0 +1,110 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Threading;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using Microsoft.Extensions.DependencyInjection;
+
+[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
+ typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedStreamLifetimeBenchmarkRegistry))]
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 为 stream 生命周期矩阵 benchmark 提供 hand-written generated registry,
+/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的完整枚举成本。
+///
+public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
+ GFramework.Cqrs.ICqrsHandlerRegistry,
+ GFramework.Cqrs.ICqrsStreamInvokerProvider,
+ GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
+{
+ private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
+ new(
+ typeof(IStreamRequestHandler<
+ StreamLifetimeBenchmarks.BenchmarkStreamRequest,
+ StreamLifetimeBenchmarks.BenchmarkResponse>),
+ typeof(GeneratedStreamLifetimeBenchmarkRegistry).GetMethod(
+ nameof(InvokeBenchmarkStreamHandler),
+ BindingFlags.Public | BindingFlags.Static)
+ ?? throw new InvalidOperationException("Missing generated stream lifetime benchmark method."));
+
+ private static readonly IReadOnlyList Descriptors =
+ [
+ new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
+ typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest),
+ typeof(StreamLifetimeBenchmarks.BenchmarkResponse),
+ Descriptor)
+ ];
+
+ ///
+ /// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。
+ ///
+ /// 当前 generated registry 拥有的服务集合。
+ /// 用于记录 generated registry 注册行为的日志器。
+ ///
+ /// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。
+ /// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。
+ ///
+ public void Register(IServiceCollection services, ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(services);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ logger.Debug("Registered generated stream lifetime benchmark descriptors.");
+ }
+
+ ///
+ /// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
+ ///
+ public IReadOnlyList GetDescriptors()
+ {
+ return Descriptors;
+ }
+
+ ///
+ /// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
+ ///
+ /// 待匹配的请求类型。
+ /// 待匹配的响应类型。
+ /// 命中时返回的 generated descriptor。
+ /// 命中当前 benchmark 的请求/响应类型对时返回 。
+ public bool TryGetDescriptor(
+ Type requestType,
+ Type responseType,
+ out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
+ {
+ if (requestType == typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest) &&
+ responseType == typeof(StreamLifetimeBenchmarks.BenchmarkResponse))
+ {
+ descriptor = Descriptor;
+ return true;
+ }
+
+ descriptor = null;
+ return false;
+ }
+
+ ///
+ /// 模拟 generated stream invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。
+ ///
+ /// 当前请求对应的 handler 实例。
+ /// 待分发的流式请求。
+ /// 调用方传入的取消令牌。
+ /// 交给目标 stream handler 处理后的异步枚举。
+ public static object InvokeBenchmarkStreamHandler(
+ object handler,
+ object request,
+ CancellationToken cancellationToken)
+ {
+ var typedHandler = (IStreamRequestHandler<
+ StreamLifetimeBenchmarks.BenchmarkStreamRequest,
+ StreamLifetimeBenchmarks.BenchmarkResponse>)handler;
+ var typedRequest = (StreamLifetimeBenchmarks.BenchmarkStreamRequest)request;
+ return typedHandler.Handle(typedRequest, cancellationToken);
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
new file mode 100644
index 00000000..c121dfd8
--- /dev/null
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
@@ -0,0 +1,277 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Columns;
+using BenchmarkDotNet.Configs;
+using BenchmarkDotNet.Diagnosers;
+using BenchmarkDotNet.Jobs;
+using BenchmarkDotNet.Order;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using GFramework.Core.Abstractions.Logging;
+using GFramework.Core.Ioc;
+using GFramework.Core.Logging;
+using GFramework.Cqrs.Abstractions.Cqrs;
+using MediatR;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace GFramework.Cqrs.Benchmarks.Messaging;
+
+///
+/// 对比 stream 完整枚举在不同 handler 生命周期下的额外开销。
+///
+///
+/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
+/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起,
+/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。
+///
+[Config(typeof(Config))]
+public class StreamLifetimeBenchmarks
+{
+ private MicrosoftDiContainer _container = null!;
+ private ICqrsRuntime _runtime = null!;
+ private ServiceProvider _serviceProvider = null!;
+ private IMediator _mediatr = null!;
+ private BenchmarkStreamHandler _baselineHandler = null!;
+ private BenchmarkStreamRequest _request = null!;
+
+ ///
+ /// 控制当前 benchmark 使用的 handler 生命周期。
+ ///
+ [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
+ public HandlerLifetime Lifetime { get; set; }
+
+ ///
+ /// 可公平比较的 benchmark handler 生命周期集合。
+ ///
+ public enum HandlerLifetime
+ {
+ ///
+ /// 复用单个 handler 实例。
+ ///
+ Singleton,
+
+ ///
+ /// 每次建流都重新解析新的 handler 实例。
+ ///
+ Transient
+ }
+
+ ///
+ /// 配置 stream 生命周期 benchmark 的公共输出格式。
+ ///
+ private sealed class Config : ManualConfig
+ {
+ public Config()
+ {
+ AddJob(Job.Default);
+ AddColumnProvider(DefaultColumnProviders.Instance);
+ AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamLifetime"));
+ AddDiagnoser(MemoryDiagnoser.Default);
+ WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
+ }
+ }
+
+ ///
+ /// 构建当前生命周期下的 GFramework 与 MediatR stream 对照宿主。
+ ///
+ [GlobalSetup]
+ public void Setup()
+ {
+ LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
+ {
+ MinLevel = LogLevel.Fatal
+ };
+ Fixture.Setup($"StreamLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+
+ _baselineHandler = new BenchmarkStreamHandler();
+ _request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
+
+ _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
+ {
+ container.RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly);
+ RegisterGFrameworkHandler(container, Lifetime);
+ });
+ _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
+ _container,
+ LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime));
+
+ _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
+ configure: null,
+ typeof(StreamLifetimeBenchmarks),
+ static candidateType => candidateType == typeof(BenchmarkStreamHandler),
+ ResolveMediatRLifetime(Lifetime));
+ _mediatr = _serviceProvider.GetRequiredService();
+ }
+
+ ///
+ /// 释放当前生命周期矩阵持有的 benchmark 宿主资源,并清理 dispatcher 缓存。
+ ///
+ [GlobalCleanup]
+ public void Cleanup()
+ {
+ try
+ {
+ BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
+ }
+ finally
+ {
+ BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
+ }
+ }
+
+ ///
+ /// 直接调用 handler 并完整枚举,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
+ ///
+ [Benchmark(Baseline = true)]
+ public async ValueTask Stream_Baseline()
+ {
+ await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false))
+ {
+ _ = response;
+ }
+ }
+
+ ///
+ /// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。
+ ///
+ [Benchmark]
+ public async ValueTask Stream_GFrameworkCqrs()
+ {
+ await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None)
+ .ConfigureAwait(false))
+ {
+ _ = response;
+ }
+ }
+
+ ///
+ /// 通过 MediatR 创建并完整枚举 stream,作为外部对照。
+ ///
+ [Benchmark]
+ public async ValueTask Stream_MediatR()
+ {
+ await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false))
+ {
+ _ = response;
+ }
+ }
+
+ ///
+ /// 按生命周期把 benchmark stream handler 注册到 GFramework 容器。
+ ///
+ /// 当前 benchmark 拥有并负责释放的容器。
+ /// 待比较的 handler 生命周期。
+ ///
+ /// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期,
+ /// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。
+ ///
+ private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ switch (lifetime)
+ {
+ case HandlerLifetime.Singleton:
+ container.RegisterSingleton<
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
+ BenchmarkStreamHandler>();
+ return;
+
+ case HandlerLifetime.Transient:
+ container.RegisterTransient<
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
+ BenchmarkStreamHandler>();
+ return;
+
+ default:
+ throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
+ }
+ }
+
+ ///
+ /// 将 benchmark 生命周期映射为 MediatR 组装所需的 。
+ ///
+ /// 待比较的 handler 生命周期。
+ /// 当前生命周期对应的 MediatR 注册方式。
+ private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
+ {
+ return lifetime switch
+ {
+ HandlerLifetime.Singleton => ServiceLifetime.Singleton,
+ HandlerLifetime.Transient => ServiceLifetime.Transient,
+ _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
+ };
+ }
+
+ ///
+ /// Benchmark stream request。
+ ///
+ /// 请求标识。
+ /// 返回元素数量。
+ public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest,
+ MediatR.IStreamRequest;
+
+ ///
+ /// Benchmark stream response。
+ ///
+ /// 响应标识。
+ public sealed record BenchmarkResponse(Guid Id);
+
+ ///
+ /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
+ ///
+ public sealed class BenchmarkStreamHandler :
+ GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler,
+ MediatR.IStreamRequestHandler
+ {
+ ///
+ /// 处理 GFramework.CQRS stream request。
+ ///
+ /// 当前 benchmark stream 请求。
+ /// 用于中断异步枚举的取消令牌。
+ /// 完整枚举所需的低噪声异步响应序列。
+ public IAsyncEnumerable Handle(
+ BenchmarkStreamRequest request,
+ CancellationToken cancellationToken)
+ {
+ return EnumerateAsync(request, cancellationToken);
+ }
+
+ ///
+ /// 处理 MediatR stream request。
+ ///
+ /// 当前 benchmark stream 请求。
+ /// 用于中断异步枚举的取消令牌。
+ /// 完整枚举所需的低噪声异步响应序列。
+ IAsyncEnumerable MediatR.IStreamRequestHandler.Handle(
+ BenchmarkStreamRequest request,
+ CancellationToken cancellationToken)
+ {
+ return EnumerateAsync(request, cancellationToken);
+ }
+
+ ///
+ /// 为生命周期矩阵构造稳定、低噪声的异步响应序列。
+ ///
+ /// 当前 benchmark 请求。
+ /// 用于中断异步枚举的取消令牌。
+ /// 按固定元素数量返回的异步响应序列。
+ private static async IAsyncEnumerable EnumerateAsync(
+ BenchmarkStreamRequest request,
+ [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
+ {
+ for (var index = 0; index < request.ItemCount; index++)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ yield return new BenchmarkResponse(request.Id);
+ await Task.CompletedTask.ConfigureAwait(false);
+ }
+ }
+ }
+}
diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md
index bf0fc053..f589a0e3 100644
--- a/GFramework.Cqrs.Benchmarks/README.md
+++ b/GFramework.Cqrs.Benchmarks/README.md
@@ -18,6 +18,8 @@
- 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 / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
+- `Messaging/StreamLifetimeBenchmarks.cs`
+ - `Singleton / Transient` 两类 handler 生命周期下,direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举对比
- `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`
@@ -51,6 +53,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `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 751b8130..d5bdfec3 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-107`
+- 恢复点编号:`CQRS-REWRITE-RP-108`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前结论:
@@ -44,14 +44,15 @@ CQRS 迁移与收敛。
- 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state:为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider;本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B`,`Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B` 与 `73.157 ns / 56 B`
- 当前 `RP-106` 已把同一套 generated-provider 宿主收口扩展到 `RequestPipelineBenchmarks`:新增 handwritten `GeneratedRequestPipelineBenchmarkRegistry`,并让 `RequestPipelineBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` + benchmark CQRS 基础设施预接线;本轮 benchmark 表明 `0 pipeline` steady-state 进一步收敛到约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 在短跑噪音下维持约 `555.083 ns / 896 B`
- 当前 `RP-107` 已把默认 stream steady-state 宿主也切到 generated-provider 路径:新增 handwritten `GeneratedDefaultStreamingBenchmarkRegistry`,让 `StreamingBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` 并在 setup/cleanup 清理 dispatcher cache;同时将 `gframework-boot` / `gframework-batch-boot` 的默认停止规则改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”,不再把 changed files 误当作唯一阈值
-- `ai-plan` active 入口现以 `RP-107` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
+ - 当前 `RP-108` 已补齐 stream handler `Singleton / Transient` 生命周期矩阵 benchmark:新增 `StreamLifetimeBenchmarks` 与 `GeneratedStreamLifetimeBenchmarkRegistry`,让 stream 生命周期对照沿用 generated-provider 宿主接线而不是退回纯反射路径;本轮 benchmark 表明 `Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
+- `ai-plan` active 入口现以 `RP-108` 为最新恢复锚点;`PR #340`、`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
-- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `10 files / 507 lines`
-- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs`、`GFramework.Cqrs.Benchmarks/README.md`、`.agents/skills/gframework-batch-boot/SKILL.md` 与 `.agents/skills/gframework-boot/SKILL.md`
+- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines`
+- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`
- 当前批次后的默认停止依据已改为 AI 上下文预算:若下一轮预计会让活动对话、已加载 recovery 文档、验证输出与当前 diff 接近约 `80%` 安全上下文占用,应在当前自然批次边界停止,即使 branch diff 仍有余量
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵
@@ -60,6 +61,7 @@ CQRS 迁移与收敛。
- 当前 request lifetime benchmark 已继续收敛:`Singleton` 下 `GFramework.Cqrs` 最新约 `69.275 ns / 32 B`,`Transient` 下约 `74.301 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B` 与 `74.757 ns / 56 B` 仍维持同一收敛区间
- 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline` 约 `64.755 ns / 32 B`,`1 pipeline` 约 `353.141 ns / 536 B`,`4 pipeline` 约 `555.083 ns / 896 B`
- 当前 stream steady-state benchmark 也已切到 generated-provider 宿主接线路径:baseline 约 `5.535 ns / 32 B`、`MediatR` 约 `59.499 ns / 232 B`、`GFramework.Cqrs` 约 `66.778 ns / 32 B`
+- 当前 stream lifetime benchmark 已补齐 `Singleton / Transient` 两档矩阵,并沿用 generated-provider 宿主接线:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path,避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
@@ -110,6 +112,7 @@ 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
+- 当前 stream 生命周期矩阵尚未接入 `Mediator` concrete runtime;若要继续对齐 `Mediator` 官方 benchmark 的 compile-time lifetime 设计,需要为 stream 场景补专门的 build-time 配置,而不是在当前统一宿主里临时拼接
- `BenchmarkDotNet.Artifacts/` 现已加入仓库忽略规则;若后续确实需要提交新的基准报告,应显式挑选结果文件或改走文档归档,而不是直接纳入整个生成目录
- 当前 `GFramework.Cqrs` request steady-state 仍慢于 `MediatR`;在“至少超过反射版 `MediatR`”这个阶段目标达成前,任何相关改动都不能只看功能 build/test 结果,必须附带 benchmark 回归数据
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
@@ -147,6 +150,21 @@ CQRS 迁移与收敛。
- 备注:按新性能回归门槛复跑后,`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`
- 结果:通过
+- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+- `git diff --check`
+ - 结果:通过
+- `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.336 ns / 32 B`、`Mediator` `5.564 ns / 32 B`、`MediatR` `53.307 ns / 232 B`、`GFramework.Cqrs` `64.745 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` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `4.309 ns / 51.923 ns / 67.981 ns`;`Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns`
+- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- `git diff --check`
- 结果:通过
- 备注:当前仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式错误
@@ -301,9 +319,9 @@ CQRS 迁移与收敛。
## 下一推荐步骤
-1. 当前 turn 已接近默认的上下文预算停止线;本次提交后应停止,并在新的 turn 里从 `RP-107` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片
-2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification 或更高价值的 request dispatch 常量开销热点,而不是再机械按 changed files 追加小批次
-3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再扩 `Mediator` 的 compile-time lifetime 或 stream 对照矩阵,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案
+1. 当前 turn 已到新的自然批次边界;本次提交后应停止,并在新的 turn 里从 `RP-108` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片
+2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification publish 或更高价值的 request dispatch 常量开销热点,而不是继续堆同层级 benchmark 宿主补齐
+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 1c276d20..103b92b0 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,46 @@
## 2026-05-08
+### 阶段:stream handler 生命周期矩阵 benchmark(CQRS-REWRITE-RP-108)
+
+- 延续 `$gframework-batch-boot 50`,本轮继续使用 `origin/main` 作为 branch diff 基线,并先复核:
+ - `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
+ - 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines`
+ - 当前 turn 虽然仍低于 `50 files` 阈值,但已加载多轮 recovery / benchmark 输出;因此只允许再推进一个单模块、低风险 benchmark 切片
+- 本轮接受的只读探索结论:
+ - `RequestLifetimeBenchmarks` 已覆盖 request 的 `Singleton / Transient` 生命周期矩阵,但 stream 侧仍缺少对称的 handler 生命周期对照
+ - `StreamingBenchmarks` 已在 `RP-107` 切到 generated-provider 宿主,适合作为 stream 生命周期矩阵的宿主基础;继续退回纯反射路径会让“生命周期变量”和“descriptor 路径变量”混在一起
+ - 如果让 generated registry 顺手注册默认单例 handler,会破坏生命周期矩阵的变量控制,因此 registry 只能暴露 descriptor,不能抢先锁死 handler 生命周期
+- 本轮主线程决策:
+ - 新增 `StreamLifetimeBenchmarks`,对齐 request 生命周期矩阵,只比较 `Singleton / Transient` 两档,继续明确把 `Scoped` 留给未来显式 scoped host
+ - 新增 `GeneratedStreamLifetimeBenchmarkRegistry`,只提供 handwritten generated stream invoker descriptor,不直接注册 handler
+ - 让 `StreamLifetimeBenchmarks` 使用 `RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly)` 建立 generated-provider 宿主,再显式按 benchmark 参数注册 `Singleton / Transient` handler 生命周期
+ - 更新 `GFramework.Cqrs.Benchmarks/README.md`,把 stream 生命周期矩阵列为已覆盖场景,并从“后续扩展方向”里移除这项待办
+- 本轮验证过程的重要补充:
+ - 首次并行触发 `RequestBenchmarks` / `RequestLifetimeBenchmarks` / `StreamLifetimeBenchmarks` 时,在同一 autogenerated BenchmarkDotNet 目录下复现了文件已存在冲突与 bootstrap 异常;这是 benchmark 基础设施层面的并行目录竞争,不是代码缺陷
+ - 改为串行重跑后三组 benchmark 全部稳定通过,因此本轮将“BenchmarkDotNet 在当前仓库里不应并行运行多条 `dotnet run --project ... --filter ...` 会话”视为有效执行约束
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
+ - 结果:通过
+ - `git diff --check`
+ - 结果:通过
+ - `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.336 ns / 32 B`、`Mediator` `5.564 ns / 32 B`、`MediatR` `53.307 ns / 232 B`、`GFramework.Cqrs` `64.745 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` 下 baseline / `MediatR` / `GFramework.Cqrs` 约 `4.309 ns / 51.923 ns / 67.981 ns`;`Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns`
+ - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
+ - 结果:通过
+ - 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR` 约 `80.144 ns / 137.515 ns / 229.242 ns`,`Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
+- 本轮结论:
+ - stream 生命周期矩阵现在已与 request 生命周期矩阵对称,且继续沿用 generated-provider 宿主路径,没有把变量退化回纯反射 binding
+ - `GFramework.Cqrs` 在 stream `Singleton / Transient` 两档下都明显快于 `MediatR`,同时保持接近 baseline 的分配规模;`Transient` 仅从 `240 B` 小幅增至 `264 B`
+ - 真正的停止依据仍是上下文预算安全。虽然 branch diff 只有 `14 files`,但当前 turn 已包含多轮 benchmark 输出和恢复文档,因此本批提交后应主动停止
+ - 下一轮若继续性能线,更值得优先看 notification publish 或更高价值的 request 常量开销热点,而不是继续做同层级 benchmark 宿主补齐
+
### 阶段:默认 stream benchmark 吸收 generated provider 宿主(CQRS-REWRITE-RP-107)
- 延续 `$gframework-batch-boot 50`,但本轮按用户新增要求把默认停止依据改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”;在真正落代码前先复核:
From 9bd8c34693fd46101e0aebfeb4a756f14c91e716 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 14:10:06 +0800
Subject: [PATCH 6/7] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E5=8F=A3PR=E5=AE=A1?=
=?UTF-8?q?=E6=9F=A5=E9=81=97=E7=95=99=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 benchmark 宿主误激活同程序集其他 generated registry 的接线路径,收窄服务索引与 descriptor 基线
- 恢复 CqrsDispatcher.SendAsync 的 faulted ValueTask 失败语义,并补充相关回归测试
- 补充 legacy runtime alias 的防守式类型检查、stream lifetime 注释与 cqrs-rewrite 恢复文档验证记录
---
.agents/skills/gframework-batch-boot/SKILL.md | 2 +-
.../Messaging/BenchmarkHostFactory.cs | 42 ++++++++++++++-
.../Messaging/RequestBenchmarks.cs | 2 +-
.../Messaging/RequestInvokerBenchmarks.cs | 2 +-
.../Messaging/RequestPipelineBenchmarks.cs | 2 +-
.../Messaging/StreamInvokerBenchmarks.cs | 2 +-
.../Messaging/StreamLifetimeBenchmarks.cs | 4 +-
.../Messaging/StreamingBenchmarks.cs | 2 +-
.../CqrsDispatcherContextValidationTests.cs | 51 +++++++++++++++++++
...qrsGeneratedRequestInvokerProviderTests.cs | 27 ++++++++++
GFramework.Cqrs/Internal/CqrsDispatcher.cs | 48 +++++++++--------
.../Internal/CqrsHandlerRegistrar.cs | 30 +++++++++++
GFramework.Cqrs/Properties/AssemblyInfo.cs | 7 +++
.../todos/cqrs-rewrite-migration-tracking.md | 5 +-
.../traces/cqrs-rewrite-migration-trace.md | 29 +++++++++++
15 files changed, 224 insertions(+), 31 deletions(-)
create mode 100644 GFramework.Cqrs/Properties/AssemblyInfo.cs
diff --git a/.agents/skills/gframework-batch-boot/SKILL.md b/.agents/skills/gframework-batch-boot/SKILL.md
index 857d8964..e10173f7 100644
--- a/.agents/skills/gframework-batch-boot/SKILL.md
+++ b/.agents/skills/gframework-batch-boot/SKILL.md
@@ -147,7 +147,7 @@ When shorthand is used:
6. After each completed batch:
- integrate or verify the result
- rerun the required validation
- - recompute the primary stop-condition metric
+ - recompute the primary stop-condition metric
- reassess whether one more batch would likely push the agent near or beyond roughly 80% context usage
- decide immediately whether to continue or stop
7. Do not require the user to manually trigger every round unless:
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
index a872b5a8..fb93cc32 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
@@ -6,6 +6,7 @@ using System.Linq;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
+using GFramework.Cqrs.Internal;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
@@ -58,11 +59,11 @@ internal static class BenchmarkHostFactory
var notificationPublisher = container.Get();
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
container.Register(runtime);
- container.Register((LegacyICqrsRuntime)runtime);
+ RegisterLegacyRuntimeAlias(container, runtime);
}
else if (container.Get() is null)
{
- container.Register((LegacyICqrsRuntime)container.GetRequired());
+ RegisterLegacyRuntimeAlias(container, container.GetRequired());
}
if (container.Get() is null)
@@ -81,6 +82,43 @@ internal static class BenchmarkHostFactory
}
}
+ ///
+ /// 只激活当前 benchmark 场景明确拥有的 generated registry,避免同一程序集里的其他 benchmark registry
+ /// 扩大冻结后服务索引与 dispatcher descriptor 基线。
+ ///
+ /// 当前 benchmark 需要接入的 generated registry 类型。
+ /// 承载 generated registry 注册结果的 GFramework benchmark 容器。
+ internal static void RegisterGeneratedBenchmarkRegistry(MicrosoftDiContainer container)
+ where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry
+ {
+ ArgumentNullException.ThrowIfNull(container);
+
+ var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
+ CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger);
+ }
+
+ ///
+ /// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。
+ ///
+ /// 承载 runtime 别名的 benchmark 容器。
+ /// 当前正式 CQRS runtime 实例。
+ ///
+ /// 未同时实现 legacy CQRS runtime 契约。
+ ///
+ private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+ ArgumentNullException.ThrowIfNull(runtime);
+
+ if (runtime is not LegacyICqrsRuntime legacyRuntime)
+ {
+ throw new InvalidOperationException(
+ $"The registered {typeof(ICqrsRuntime).FullName} must also implement {typeof(LegacyICqrsRuntime).FullName}. Actual runtime type: {runtime.GetType().FullName}.");
+ }
+
+ container.Register(legacyRuntime);
+ }
+
///
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
///
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
index a84c6d1a..9d946e72 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs
@@ -66,7 +66,7 @@ public class RequestBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(RequestBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
index 87e326e5..4b8589ed 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs
@@ -83,7 +83,7 @@ public class RequestInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
index 23d703ec..a2883955 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs
@@ -69,7 +69,7 @@ public class RequestPipelineBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(RequestPipelineBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
index a552a233..deacac21 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs
@@ -83,7 +83,7 @@ public class StreamInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
index c121dfd8..7d761d03 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
@@ -93,9 +93,11 @@ public class StreamLifetimeBenchmarks
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
RegisterGFrameworkHandler(container, Lifetime);
});
+ // 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
+ // 这里额外创建带生命周期后缀的 runtime,只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime));
diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
index cd6e4a83..8b886d29 100644
--- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
+++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs
@@ -67,7 +67,7 @@ public class StreamingBenchmarks
_baselineHandler = new BenchmarkStreamHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
- container.RegisterCqrsHandlersFromAssembly(typeof(StreamingBenchmarks).Assembly);
+ BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
index 5eec00eb..9e1d0678 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
@@ -43,6 +43,57 @@ internal sealed class CqrsDispatcherContextValidationTests
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
+ ///
+ /// 验证 request 上下文校验失败时,
+ /// 不会在调用点同步抛出,而是返回一个 faulted 保持既有异步失败语义。
+ ///
+ [Test]
+ public void SendAsync_Should_Return_Faulted_ValueTask_When_Context_Preparation_Fails()
+ {
+ var runtime = CreateRuntime(
+ container =>
+ {
+ container
+ .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler)))
+ .Returns(new ContextAwareRequestHandler());
+ container
+ .Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior)))
+ .Returns(false);
+ });
+
+ ValueTask dispatch = default;
+ Assert.That(
+ () => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
+ Throws.Nothing);
+ Assert.That(
+ async () => await dispatch.ConfigureAwait(false),
+ Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
+ }
+
+ ///
+ /// 验证 request handler 缺失时,dispatcher 仍返回 faulted ,
+ /// 而不是在调用点同步抛出异常。
+ ///
+ [Test]
+ public void SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()
+ {
+ var runtime = CreateRuntime(
+ container =>
+ {
+ container
+ .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler)))
+ .Returns((object?)null);
+ });
+
+ ValueTask dispatch = default;
+ Assert.That(
+ () => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
+ Throws.Nothing);
+ Assert.That(
+ async () => await dispatch.ConfigureAwait(false),
+ Throws.InvalidOperationException.With.Message.Contains("No CQRS request handler registered"));
+ }
+
///
/// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 时,
/// dispatcher 会在发布前显式失败。
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
index 07204bbf..15273d96 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs
@@ -7,6 +7,7 @@ using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
+using GFramework.Cqrs.Internal;
namespace GFramework.Cqrs.Tests.Cqrs;
@@ -99,6 +100,32 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)]));
}
+ ///
+ /// 验证 direct generated-registry 激活入口只会接入指定 registry,而不会顺手把同一测试程序集里的其他 registry 一并注册。
+ ///
+ [Test]
+ public void RegisterGeneratedRegistry_Should_Register_Only_The_Selected_Provider()
+ {
+ var container = new MicrosoftDiContainer();
+ var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsGeneratedRequestInvokerProviderTests));
+
+ CqrsHandlerRegistrar.RegisterGeneratedRegistry(
+ container,
+ typeof(GeneratedRequestInvokerProviderRegistry),
+ logger);
+
+ var requestProviders = container.GetAll();
+ var streamProviders = container.GetAll();
+
+ Assert.Multiple(() =>
+ {
+ Assert.That(
+ requestProviders.Select(static provider => provider.GetType()),
+ Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
+ Assert.That(streamProviders, Is.Empty);
+ });
+ }
+
///
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
/// registrar 仍会把 generated stream invoker provider 注册到容器中。
diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
index 86caab3d..aa1e6d24 100644
--- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs
+++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs
@@ -110,30 +110,38 @@ internal sealed class CqrsDispatcher(
IRequest request,
CancellationToken cancellationToken = default)
{
- ArgumentNullException.ThrowIfNull(context);
- ArgumentNullException.ThrowIfNull(request);
-
- var requestType = request.GetType();
- var dispatchBinding = GetRequestDispatchBinding(requestType);
- var handler = container.Get(dispatchBinding.HandlerType)
- ?? throw new InvalidOperationException(
- $"No CQRS request handler registered for {requestType.FullName}.");
-
- PrepareHandler(handler, context);
- if (!container.HasRegistration(dispatchBinding.BehaviorType))
+ try
{
- return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
+ ArgumentNullException.ThrowIfNull(context);
+ ArgumentNullException.ThrowIfNull(request);
+
+ var requestType = request.GetType();
+ var dispatchBinding = GetRequestDispatchBinding(requestType);
+ var handler = container.Get(dispatchBinding.HandlerType)
+ ?? throw new InvalidOperationException(
+ $"No CQRS request handler registered for {requestType.FullName}.");
+
+ PrepareHandler(handler, context);
+ if (!container.HasRegistration(dispatchBinding.BehaviorType))
+ {
+ return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
+ }
+
+ var behaviors = container.GetAll(dispatchBinding.BehaviorType);
+
+ foreach (var behavior in behaviors)
+ {
+ PrepareHandler(behavior, context);
+ }
+
+ return dispatchBinding.GetPipelineExecutor(behaviors.Count)
+ .Invoke(handler, behaviors, request, cancellationToken);
}
-
- var behaviors = container.GetAll(dispatchBinding.BehaviorType);
-
- foreach (var behavior in behaviors)
+ catch (Exception exception)
{
- PrepareHandler(behavior, context);
+ // 保留旧 async 实现的 faulted-ValueTask 失败语义,同时继续复用 direct-return 的热路径。
+ return ValueTask.FromException(exception);
}
-
- return dispatchBinding.GetPipelineExecutor(behaviors.Count)
- .Invoke(handler, behaviors, request, cancellationToken);
}
///
diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
index 3607771c..a332927c 100644
--- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
+++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs
@@ -68,6 +68,36 @@ internal static class CqrsHandlerRegistrar
}
}
+ ///
+ /// 直接激活并注册单个 generated registry,避免调用方为了只接入一个 benchmark registry
+ /// 而额外扫描同一程序集里的其他 registry / handler。
+ ///
+ /// 承载 generated registry 注册结果的目标容器。
+ /// 要直接激活的 generated registry 类型。
+ /// 当前注册过程使用的日志记录器。
+ ///
+ /// 、 或 为 。
+ ///
+ /// 指定 registry 类型不满足 generated registry 运行时契约。
+ internal static void RegisterGeneratedRegistry(
+ IIocContainer container,
+ Type registryType,
+ ILogger logger)
+ {
+ ArgumentNullException.ThrowIfNull(container);
+ ArgumentNullException.ThrowIfNull(registryType);
+ ArgumentNullException.ThrowIfNull(logger);
+
+ var assemblyName = GetAssemblySortKey(registryType.Assembly);
+ if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
+ {
+ throw new InvalidOperationException(
+ $"Unable to activate generated CQRS handler registry {registryType.FullName} in assembly {assemblyName}.");
+ }
+
+ RegisterGeneratedRegistries(container.GetServicesUnsafe, [registry], assemblyName, logger);
+ }
+
///
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
///
diff --git a/GFramework.Cqrs/Properties/AssemblyInfo.cs b/GFramework.Cqrs/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..0572e1c6
--- /dev/null
+++ b/GFramework.Cqrs/Properties/AssemblyInfo.cs
@@ -0,0 +1,7 @@
+// Copyright (c) 2025-2026 GeWuYou
+// SPDX-License-Identifier: Apache-2.0
+
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("GFramework.Cqrs.Tests")]
+[assembly: InternalsVisibleTo("GFramework.Cqrs.Benchmarks")]
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 d5bdfec3..5f2766c4 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,11 @@ CQRS 迁移与收敛。
## 当前恢复点
-- 恢复点编号:`CQRS-REWRITE-RP-108`
+- 恢复点编号:`CQRS-REWRITE-RP-109`
- 当前阶段:`Phase 8`
-- 当前 PR 锚点:`PR #340`
+- 当前 PR 锚点:`PR #341`
- 当前结论:
+ - 当前 `RP-109` 已使用 `$gframework-pr-review` 复核 `PR #341` latest-head review:benchmark 宿主改为定向激活当前场景的 generated registry,避免同一 benchmark 程序集里的其他 registry 扩大冻结服务索引与 `HasRegistration` 基线;`BenchmarkHostFactory` 为 legacy runtime alias 注册补齐防守式类型检查与 stream lifetime 运行时注释;`CqrsDispatcher.SendAsync(...)` 在保留 direct-return 热路径的同时恢复 faulted `ValueTask` 失败语义,并补齐 generated registry 定向接线与 request fault 语义回归测试;`.agents/skills/gframework-batch-boot/SKILL.md` 的 MD005 缩进也已顺手修正
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
- `RP-078` 已补齐 mixed fallback metadata 在 runtime 不允许多个 fallback attribute 实例时的单字符串 attribute 回退回归
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 103b92b0..a0ef3c32 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,35 @@
## 2026-05-08
+### 阶段:PR #341 latest-head review 收口(CQRS-REWRITE-RP-109)
+
+- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR,并确认当前锚点已从 `PR #340` 更新为 `PR #341`
+- 本轮 latest-head review 结论:
+ - `CodeRabbit` 仍有 `BenchmarkHostFactory.cs` 的 legacy runtime 硬转型、`StreamLifetimeBenchmarks.cs` 的注释缺口,以及 `.agents/skills/gframework-batch-boot/SKILL.md` 的 `MD005` 缩进问题
+ - `Greptile` 指出的两条仍然成立:benchmark 项目里通过 `RegisterCqrsHandlersFromAssembly(typeof(...).Assembly)` 会把同程序集的其他 generated registry 一并激活,扩大 benchmark 宿主的服务索引基线;`CqrsDispatcher.SendAsync(...)` 直接去掉 `async/await` 后也把原本的 faulted-`ValueTask` 失败语义改成了同步抛出
+- 本轮主线程决策:
+ - 在 `GFramework.Cqrs.Internal.CqrsHandlerRegistrar` 新增 direct generated-registry 激活入口,并通过 `InternalsVisibleTo` 暴露给 `GFramework.Cqrs.Benchmarks`,让 benchmark 宿主只激活当前场景的 generated registry
+ - 把 `RequestBenchmarks`、`RequestPipelineBenchmarks`、`StreamingBenchmarks`、`StreamLifetimeBenchmarks` 以及 request/stream invoker benchmark 的 generated 宿主全部切到定向 registry 接线,避免同程序集其他 registry 扩大冻结索引和 descriptor 预热基线
+ - 在 `BenchmarkHostFactory` 里用防守式类型检查注册 legacy runtime alias,并补充 stream lifetime runtime 二次创建的注释
+ - 让 `CqrsDispatcher.SendAsync(...)` 通过 `ValueTask.FromException(...)` 恢复旧的 faulted-`ValueTask` 失败语义,同时保留成功路径的 direct-return 热路径
+ - 补齐 `CqrsGeneratedRequestInvokerProviderTests` 与 `CqrsDispatcherContextValidationTests` 的 targeted 回归,并顺手修正 batch boot skill 的 markdown 缩进
+- 本轮权威验证:
+ - `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
+ - 结果:通过,`1 warning / 0 error`
+ - 备注:仅出现 `MSB3026` 单次复制重试告警,随后成功产出 `net10.0` 目标;未出现编译失败或新增代码警告
+ - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
+ - 结果:通过,`0 warning / 0 error`
+ - `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherContextValidationTests|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"`
+ - 结果:通过,`24/24` passed
+ - 备注:首轮并行验证时因与 build 同时运行触发 MSBuild 输出文件锁竞争;改为串行重跑同一命令后稳定通过
+ - `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Properties/AssemblyInfo.cs GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs GFramework.Cqrs/Internal/CqrsDispatcher.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
+ - 结果:通过
+ - 备注:仓库脚本默认内部调用未绑定 worktree 的 `git ls-files`,因此本轮按修改文件列表显式 `--paths` 校验
+ - `git diff --check`
+ - 结果:通过
+- 本轮下一步:
+ - 运行 `GFramework.Cqrs` / `GFramework.Cqrs.Benchmarks` 的 Release build、相关 targeted tests、license header check 与 `git diff --check`
+
### 阶段:stream handler 生命周期矩阵 benchmark(CQRS-REWRITE-RP-108)
- 延续 `$gframework-batch-boot 50`,本轮继续使用 `origin/main` 作为 branch diff 基线,并先复核:
From 769d03643470bdd9bf70591bf8252938a032ae10 Mon Sep 17 00:00:00 2001
From: gewuyou <95328647+GeWuYou@users.noreply.github.com>
Date: Fri, 8 May 2026 15:06:24 +0800
Subject: [PATCH 7/7] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E5=8F=A3PR341?=
=?UTF-8?q?=E5=89=A9=E4=BD=99review=E5=B0=BE=E9=A1=B9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 修复 request faulted ValueTask 回归测试对 pipeline 探测顺序的隐式依赖,补齐 HasRegistration 与 GetAll 的防御性 mock
- 更新 cqrs-rewrite tracking 与 trace,记录 PR #341 latest-head review 的 stale thread 复核结论与本轮验证结果
---
.../CqrsDispatcherContextValidationTests.cs | 6 +++++
.../todos/cqrs-rewrite-migration-tracking.md | 17 +++++++++++-
.../traces/cqrs-rewrite-migration-trace.md | 26 +++++++++++++++++--
3 files changed, 46 insertions(+), 3 deletions(-)
diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
index 9e1d0678..7452eb84 100644
--- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
+++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs
@@ -83,6 +83,12 @@ internal sealed class CqrsDispatcherContextValidationTests
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler)))
.Returns((object?)null);
+ container
+ .Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior)))
+ .Returns(false);
+ container
+ .Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior)))
+ .Returns(Array.Empty