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] =?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 基线: