From a8f98e467dfa87333e843e27b2e9e3ae12610ba6 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 09:23:07 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E7=AE=A1=E9=81=93=E6=95=B0=E9=87=8F=E7=9F=A9=E9=98=B5?= =?UTF-8?q?=E5=9F=BA=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 request pipeline 0/1/4 数量矩阵基准并保持 GFramework.Cqrs 与 MediatR 对照 - 更新 benchmark README 说明当前场景覆盖与后续扩展方向 - 补充 cqrs-rewrite 跟踪与 trace 的 RP-086 恢复点和验证记录 --- .../Messaging/RequestPipelineBenchmarks.cs | 270 ++++++++++++++++++ GFramework.Cqrs.Benchmarks/README.md | 4 +- .../todos/cqrs-rewrite-migration-tracking.md | 10 +- .../traces/cqrs-rewrite-migration-trace.md | 35 +++ 4 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs new file mode 100644 index 00000000..1dc1b47a --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -0,0 +1,270 @@ +// 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.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; + +/// +/// 对比不同 pipeline 行为数量下,单个 request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。 +/// +[Config(typeof(Config))] +public class RequestPipelineBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private BenchmarkRequestHandler _baselineHandler = null!; + private BenchmarkRequest _request = null!; + + /// + /// 控制当前场景注册的 pipeline 行为数量,保持与 `Mediator` benchmark 常见的“无行为 / 少量行为 / 多行为”矩阵一致。 + /// + [Params(0, 1, 4)] + public int PipelineCount { get; set; } + + /// + /// 配置 request pipeline benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestPipeline")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 request pipeline dispatch 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount); + + _container = new MicrosoftDiContainer(); + _baselineHandler = new BenchmarkRequestHandler(); + + _container.RegisterTransient, BenchmarkRequestHandler>(); + RegisterGFrameworkPipelineBehaviors(_container, PipelineCount); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestPipelineBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkRequestHandler>(); + RegisterMediatRPipelineBehaviors(services, PipelineCount); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestPipelineBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + + _request = new BenchmarkRequest(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 直接调用 handler,作为 pipeline 编排之外的基线。 + /// + [Benchmark(Baseline = true)] + public ValueTask SendRequest_Baseline() + { + return _baselineHandler.Handle(_request, CancellationToken.None); + } + + /// + /// 通过 GFramework.CQRS runtime 发送 request,并按当前矩阵配置执行 pipeline。 + /// + [Benchmark] + public ValueTask SendRequest_GFrameworkCqrs() + { + return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None); + } + + /// + /// 通过 MediatR 发送 request,并按当前矩阵配置执行 pipeline,作为外部设计对照。 + /// + [Benchmark] + public Task SendRequest_MediatR() + { + return _mediatr.Send(_request, CancellationToken.None); + } + + /// + /// 按指定数量向 GFramework.CQRS 宿主注册最小 no-op pipeline 行为。 + /// + /// 当前 benchmark 使用的容器。 + /// 要注册的行为数量。 + /// 行为数量不在支持的矩阵内时抛出。 + private static void RegisterGFrameworkPipelineBehaviors(MicrosoftDiContainer container, int pipelineCount) + { + ArgumentNullException.ThrowIfNull(container); + + switch (pipelineCount) + { + case 0: + return; + case 1: + container.RegisterCqrsPipelineBehavior(); + return; + case 4: + container.RegisterCqrsPipelineBehavior(); + container.RegisterCqrsPipelineBehavior(); + container.RegisterCqrsPipelineBehavior(); + container.RegisterCqrsPipelineBehavior(); + return; + default: + throw new ArgumentOutOfRangeException(nameof(pipelineCount), pipelineCount, + "Only the 0/1/4 pipeline matrix is supported."); + } + } + + /// + /// 按指定数量向 MediatR 宿主注册最小 no-op pipeline 行为。 + /// + /// 当前 benchmark 使用的服务集合。 + /// 要注册的行为数量。 + /// 行为数量不在支持的矩阵内时抛出。 + private static void RegisterMediatRPipelineBehaviors(IServiceCollection services, int pipelineCount) + { + ArgumentNullException.ThrowIfNull(services); + + switch (pipelineCount) + { + case 0: + return; + case 1: + services.AddSingleton, BenchmarkPipelineBehavior1>(); + return; + case 4: + services.AddSingleton, BenchmarkPipelineBehavior1>(); + services.AddSingleton, BenchmarkPipelineBehavior2>(); + services.AddSingleton, BenchmarkPipelineBehavior3>(); + services.AddSingleton, BenchmarkPipelineBehavior4>(); + return; + default: + throw new ArgumentOutOfRangeException(nameof(pipelineCount), pipelineCount, + "Only the 0/1/4 pipeline matrix is supported."); + } + } + + /// + /// Benchmark request。 + /// + /// 请求标识。 + public sealed record BenchmarkRequest(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.IRequest, + MediatR.IRequest; + + /// + /// Benchmark response。 + /// + /// 响应标识。 + public sealed record BenchmarkResponse(Guid Id); + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 request handler。 + /// + public sealed class BenchmarkRequestHandler : + GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler, + MediatR.IRequestHandler + { + /// + /// 处理 GFramework.CQRS request。 + /// + public ValueTask Handle(BenchmarkRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(new BenchmarkResponse(request.Id)); + } + + /// + /// 处理 MediatR request。 + /// + Task MediatR.IRequestHandler.Handle( + BenchmarkRequest request, + CancellationToken cancellationToken) + { + return Task.FromResult(new BenchmarkResponse(request.Id)); + } + } + + /// + /// 为 benchmark 提供统一的 no-op pipeline 行为实现,尽量把测量焦点保持在调度器与行为编排本身。 + /// + public abstract class BenchmarkPipelineBehaviorBase : + GFramework.Cqrs.Abstractions.Cqrs.IPipelineBehavior, + MediatR.IPipelineBehavior + { + /// + /// 透传 GFramework.CQRS pipeline,避免引入额外业务逻辑噪音。 + /// + public ValueTask Handle( + BenchmarkRequest message, + GFramework.Cqrs.Abstractions.Cqrs.MessageHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(message, cancellationToken); + } + + /// + /// 透传 MediatR pipeline,保持与 GFramework.CQRS 相同的 no-op 语义。 + /// + Task MediatR.IPipelineBehavior.Handle( + BenchmarkRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + return next(); + } + } + + /// + /// pipeline 行为槽位 1。 + /// + public sealed class BenchmarkPipelineBehavior1 : BenchmarkPipelineBehaviorBase; + + /// + /// pipeline 行为槽位 2。 + /// + public sealed class BenchmarkPipelineBehavior2 : BenchmarkPipelineBehaviorBase; + + /// + /// pipeline 行为槽位 3。 + /// + public sealed class BenchmarkPipelineBehavior3 : BenchmarkPipelineBehaviorBase; + + /// + /// pipeline 行为槽位 4。 + /// + public sealed class BenchmarkPipelineBehavior4 : BenchmarkPipelineBehaviorBase; +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 8d281a50..024cede8 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -16,6 +16,8 @@ - 运行前输出并校验场景配置 - `Messaging/RequestBenchmarks.cs` - 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 对比 - `Messaging/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 - `Messaging/StreamingBenchmarks.cs` @@ -31,6 +33,6 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro ## 后续扩展方向 -- pipeline behavior 数量矩阵 - generated invoker provider 与纯反射 dispatch 对比 - cold-start 与 registration 成本对比 +- request / stream 的 initialization 与首轮 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 40336c70..76832404 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-085` +- 恢复点编号:`CQRS-REWRITE-RP-086` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -20,7 +20,8 @@ CQRS 迁移与收敛。 - 当前 `RP-082` 已补齐基础 generation gate 的 request handler runtime contract 缺失分支 - `RP-083` 已补齐 mixed direct / reflected-implementation request 与 stream invoker provider 发射顺序回归 - `RP-084` 已引入独立 `GFramework.Cqrs.Benchmarks` 项目,作为持续吸收 `Mediator` benchmark 组织方式的第一落点 - - 当前 `RP-085` 已补齐 stream request benchmark,对齐 `Mediator` messaging benchmark 的第二个核心场景 + - `RP-085` 已补齐 stream request benchmark,对齐 `Mediator` messaging benchmark 的第二个核心场景 + - 当前 `RP-086` 已补齐 request pipeline `0 / 1 / 4` 数量矩阵,开始把 benchmark 关注点从单纯 messaging steady-state 扩展到行为编排开销 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -55,6 +56,9 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:包含新增 `StreamingBenchmarks` 后再次复核通过 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:包含新增 `RequestPipelineBenchmarks` 后再次复核通过 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 - `git diff --check` @@ -107,7 +111,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 request pipeline 数量矩阵、cold-start / initialization 与 generated invoker provider 对照 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 cold-start / initialization 与 generated invoker provider 对照 3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build、targeted test 或 benchmark project build 作为权威验证 ## 活跃文档 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 76680e79..ad1f2077 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 @@ -334,3 +334,38 @@ 1. 继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 request pipeline 数量矩阵,随后再评估 cold-start / initialization 2. 当需要验证 generated invoker provider 的实际收益时,把 request benchmark 扩展为 reflection / generated provider 对照,而不是只停留在框架间对比 + +### 阶段:request pipeline 数量矩阵(CQRS-REWRITE-RP-086) + +- 继续沿用 `$gframework-batch-boot 50`,当前 branch diff 相对 `origin/main` 仍明显低于阈值 +- 本轮把 benchmark 关注点从单纯 messaging steady-state 扩展到 request pipeline 编排行为,原因是: + - `ai-libs/Mediator` 的对照价值已经不只在 request / notification / stream 三个入口本身,还在 pipeline 包装策略与生命周期取舍 + - `GFramework.Cqrs.Internal.CqrsDispatcher` 已按 `behaviorCount` 缓存 `RequestPipelineExecutor` 形状,因此单独量化 `0 / 1 / 4` 个行为的 steady-state 开销有直接信息密度 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/README.md` 中的 request pipeline 场景说明 +- 设计取舍: + - 采用 `0 / 1 / 4` 个 pipeline 行为,而不是立即扩到更大的参数空间,先锁定最有代表性的无行为 / 少量行为 / 常见多行为矩阵 + - 使用最小 no-op 行为族,不引入日志、计时或上下文刷新逻辑,避免把测量结果污染成业务行为成本 + - `GFramework.Cqrs` 与 `MediatR` 侧都只注册当前 benchmark 请求对应的闭合行为类型,确保矩阵反映编排成本而非程序集扫描差异 +- 接受的只读 subagent 结论: + - 下一批 benchmark 继续优先考虑 `cold-start / initialization` 与 `generated provider` 对照,而不是立即照搬 `Mediator` 的 large-project 维度 + - 当前 `GFramework.Cqrs.Benchmarks` 仍未接入 `Mediator` 包和 `GFramework.Cqrs.SourceGenerators`,因此本轮不扩成 `Mediator_IMediator` / generated-provider 对照,避免 scope 失控 +- 结论: + - 当前 benchmark 项目已经覆盖 `Request`、`Notification`、`StreamRequest` 与 `RequestPipeline` + - 后续若要继续贴近 `Mediator` 的 comparison benchmark,最值得优先补的是 initialization / first-hit 与 generated invoker provider,而不是继续横向堆更多 steady-state messaging 入口 + +### 验证(RP-086) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +### 当前 stop-condition 度量(RP-086) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:提交前基于 `HEAD` 的 branch diff 仍为 `14` files,距离 `50` 文件阈值仍有明显余量 + +### 当前下一步(RP-086) + +1. 提交本轮 request pipeline benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 initialization / cold-start 场景 +2. 当需要验证 dispatcher 预热与 source generator 收益时,引入 generated invoker provider 对照,并评估是否同时接入 `Mediator` concrete runtime 作为更贴近设计哲学的外部参照