From 6e1eaf8f5cd7084ce607cd9d1b5e6590bc120d63 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 09:36:48 +0800 Subject: [PATCH] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E8=B0=83=E7=94=A8=E5=99=A8=E7=94=9F=E6=88=90=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=9F=BA=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 request reflection 与 generated invoker provider 的 steady-state 对照基准 - 引入 handwritten generated registry/provider 以走通真实 registrar 与 dispatcher 预热链路 - 更新 benchmark README 与 cqrs-rewrite RP-088 跟踪记录 --- ...eneratedRequestInvokerBenchmarkRegistry.cs | 98 +++++++ .../Messaging/RequestInvokerBenchmarks.cs | 260 ++++++++++++++++++ GFramework.Cqrs.Benchmarks/README.md | 4 +- .../todos/cqrs-rewrite-migration-tracking.md | 10 +- .../traces/cqrs-rewrite-migration-trace.md | 31 +++ 5 files changed, 399 insertions(+), 4 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestInvokerBenchmarkRegistry.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestInvokerBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestInvokerBenchmarkRegistry.cs new file mode 100644 index 00000000..97460fed --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestInvokerBenchmarkRegistry.cs @@ -0,0 +1,98 @@ +// 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; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为 benchmark 手写一个“生成后等价物” registry,用于驱动真实的 generated invoker provider 运行时接线路径。 +/// +public sealed class GeneratedRequestInvokerBenchmarkRegistry : + GFramework.Cqrs.ICqrsHandlerRegistry, + GFramework.Cqrs.ICqrsRequestInvokerProvider, + GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors +{ + private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor = + new( + typeof(IRequestHandler< + RequestInvokerBenchmarks.GeneratedBenchmarkRequest, + RequestInvokerBenchmarks.GeneratedBenchmarkResponse>), + typeof(GeneratedRequestInvokerBenchmarkRegistry).GetMethod( + nameof(InvokeGeneratedRequestHandler), + BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException("Missing generated request invoker benchmark method.")); + + private static readonly IReadOnlyList Descriptors = + [ + new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry( + typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest), + typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse), + Descriptor) + ]; + + /// + /// 将 generated benchmark request handler 注册到目标服务集合。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IRequestHandler< + RequestInvokerBenchmarks.GeneratedBenchmarkRequest, + RequestInvokerBenchmarks.GeneratedBenchmarkResponse>), + typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequestHandler)); + logger.Debug("Registered generated request invoker 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(RequestInvokerBenchmarks.GeneratedBenchmarkRequest) && + responseType == typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 模拟 generated invoker provider 产出的开放静态调用入口。 + /// + public static ValueTask InvokeGeneratedRequestHandler( + object handler, + object request, + CancellationToken cancellationToken) + { + var typedHandler = (IRequestHandler< + RequestInvokerBenchmarks.GeneratedBenchmarkRequest, + RequestInvokerBenchmarks.GeneratedBenchmarkResponse>)handler; + var typedRequest = (RequestInvokerBenchmarks.GeneratedBenchmarkRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs new file mode 100644 index 00000000..d3eb2759 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -0,0 +1,260 @@ +// 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.Reflection; +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; + +[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute( + typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestInvokerBenchmarkRegistry))] + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比 request steady-state dispatch 在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。 +/// +[Config(typeof(Config))] +public class RequestInvokerBenchmarks +{ + private MicrosoftDiContainer _reflectionContainer = null!; + private ICqrsRuntime _reflectionRuntime = null!; + private MicrosoftDiContainer _generatedContainer = null!; + private ICqrsRuntime _generatedRuntime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private ReflectionBenchmarkRequestHandler _baselineHandler = null!; + private ReflectionBenchmarkRequest _reflectionRequest = null!; + private GeneratedBenchmarkRequest _generatedRequest = null!; + private MediatRBenchmarkRequest _mediatrRequest = null!; + + /// + /// 配置 request invoker benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestInvoker")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 reflection / generated / MediatR 三组 request dispatch 对照宿主。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("RequestInvoker", handlerCount: 1, pipelineCount: 0); + ClearDispatcherCaches(); + + _baselineHandler = new ReflectionBenchmarkRequestHandler(); + _reflectionRequest = new ReflectionBenchmarkRequest(Guid.NewGuid()); + _generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid()); + _mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid()); + + _reflectionContainer = new MicrosoftDiContainer(); + _reflectionContainer.RegisterTransient, ReflectionBenchmarkRequestHandler>(); + _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _reflectionContainer, + LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Reflection")); + + _generatedContainer = new MicrosoftDiContainer(); + _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly); + _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _generatedContainer, + LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Generated")); + + var services = new ServiceCollection(); + services.AddSingleton, MediatRBenchmarkRequestHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestInvokerBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主,并清理静态 dispatcher 缓存。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + ClearDispatcherCaches(); + } + + /// + /// 直接调用最小 request handler,作为 dispatch 额外开销 baseline。 + /// + [Benchmark(Baseline = true)] + public ValueTask SendRequest_Baseline() + { + return _baselineHandler.Handle(_reflectionRequest, CancellationToken.None); + } + + /// + /// 通过 GFramework.CQRS 反射 request binding 路径发送 request。 + /// + [Benchmark] + public ValueTask SendRequest_GFrameworkReflection() + { + return _reflectionRuntime.SendAsync(BenchmarkContext.Instance, _reflectionRequest, CancellationToken.None); + } + + /// + /// 通过 generated request invoker provider 预热后的 GFramework.CQRS runtime 发送 request。 + /// + [Benchmark] + public ValueTask SendRequest_GFrameworkGenerated() + { + return _generatedRuntime.SendAsync(BenchmarkContext.Instance, _generatedRequest, CancellationToken.None); + } + + /// + /// 通过 MediatR 发送 request,作为外部对照。 + /// + [Benchmark] + public Task SendRequest_MediatR() + { + return _mediatr.Send(_mediatrRequest, CancellationToken.None); + } + + /// + /// 清空 dispatcher 静态缓存,避免上一轮基准残留的 generated metadata 影响当前对照。 + /// + private static void ClearDispatcherCaches() + { + ClearDispatcherCache("NotificationDispatchBindings"); + ClearDispatcherCache("RequestDispatchBindings"); + ClearDispatcherCache("StreamDispatchBindings"); + ClearDispatcherCache("GeneratedRequestInvokers"); + ClearDispatcherCache("GeneratedStreamInvokers"); + } + + /// + /// 通过反射定位并清空 dispatcher 的指定缓存字段。 + /// + /// 要清理的静态缓存字段名。 + private static void ClearDispatcherCache(string fieldName) + { + var field = typeof(GFramework.Cqrs.CqrsRuntimeFactory).Assembly + .GetType("GFramework.Cqrs.Internal.CqrsDispatcher", throwOnError: true)! + .GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new InvalidOperationException($"Missing dispatcher cache field {fieldName}."); + var cache = field.GetValue(null) + ?? throw new InvalidOperationException($"Dispatcher cache field {fieldName} returned null."); + var clearMethod = cache.GetType().GetMethod("Clear", BindingFlags.Public | BindingFlags.Instance) + ?? throw new InvalidOperationException( + $"Dispatcher cache field {fieldName} does not expose a Clear method."); + _ = clearMethod.Invoke(cache, null); + } + + /// + /// Reflection runtime request。 + /// + /// 请求标识。 + public sealed record ReflectionBenchmarkRequest(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.IRequest; + + /// + /// Reflection runtime response。 + /// + /// 响应标识。 + public sealed record ReflectionBenchmarkResponse(Guid Id); + + /// + /// Generated runtime request。 + /// + /// 请求标识。 + public sealed record GeneratedBenchmarkRequest(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.IRequest; + + /// + /// Generated runtime response。 + /// + /// 响应标识。 + public sealed record GeneratedBenchmarkResponse(Guid Id); + + /// + /// MediatR request。 + /// + /// 请求标识。 + public sealed record MediatRBenchmarkRequest(Guid Id) : MediatR.IRequest; + + /// + /// MediatR response。 + /// + /// 响应标识。 + public sealed record MediatRBenchmarkResponse(Guid Id); + + /// + /// Reflection runtime 的最小 request handler。 + /// + public sealed class ReflectionBenchmarkRequestHandler : + GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler + { + /// + /// 处理 reflection benchmark request。 + /// + public ValueTask Handle( + ReflectionBenchmarkRequest request, + CancellationToken cancellationToken) + { + return ValueTask.FromResult(new ReflectionBenchmarkResponse(request.Id)); + } + } + + /// + /// Generated runtime 的最小 request handler。 + /// + public sealed class GeneratedBenchmarkRequestHandler : + GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler + { + /// + /// 处理 generated benchmark request。 + /// + public ValueTask Handle( + GeneratedBenchmarkRequest request, + CancellationToken cancellationToken) + { + return ValueTask.FromResult(new GeneratedBenchmarkResponse(request.Id)); + } + } + + /// + /// MediatR 对照组的最小 request handler。 + /// + public sealed class MediatRBenchmarkRequestHandler : + MediatR.IRequestHandler + { + /// + /// 处理 MediatR benchmark request。 + /// + public Task Handle( + MediatRBenchmarkRequest request, + CancellationToken cancellationToken) + { + return Task.FromResult(new MediatRBenchmarkResponse(request.Id)); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index e27c68b4..c98a11fb 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -20,6 +20,8 @@ - `0 / 1 / 4` 个 pipeline 行为下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/RequestStartupBenchmarks.cs` - `Initialization` 与 `ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度 +- `Messaging/RequestInvokerBenchmarks.cs` + - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Messaging/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 - `Messaging/StreamingBenchmarks.cs` @@ -37,4 +39,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro - generated invoker provider 与纯反射 dispatch 对比 - registration / service lifetime 矩阵 -- request / stream 的 generated provider 与 concrete runtime 对照 +- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 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 bcf9df6c..cc0fff4b 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-087` +- 恢复点编号:`CQRS-REWRITE-RP-088` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -22,7 +22,8 @@ CQRS 迁移与收敛。 - `RP-084` 已引入独立 `GFramework.Cqrs.Benchmarks` 项目,作为持续吸收 `Mediator` benchmark 组织方式的第一落点 - `RP-085` 已补齐 stream request benchmark,对齐 `Mediator` messaging benchmark 的第二个核心场景 - `RP-086` 已补齐 request pipeline `0 / 1 / 4` 数量矩阵,开始把 benchmark 关注点从单纯 messaging steady-state 扩展到行为编排开销 - - 当前 `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` + - `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` + - 当前 `RP-088` 已补齐 request invoker reflection / generated-provider 对照,开始直接量化 dispatcher 预热 generated descriptor 的收益 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -63,6 +64,9 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:包含新增 `RequestStartupBenchmarks` 后再次复核通过 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:包含新增 `RequestInvokerBenchmarks` 与 handwritten generated registry/provider 后再次复核通过 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 - `git diff --check` @@ -115,7 +119,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 generated invoker provider、registration / service lifetime 与更贴近 `Mediator` concrete runtime 的对照 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、stream generated provider 与更贴近 `Mediator` concrete runtime 的对照 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 b0416b2c..602f5154 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 @@ -399,3 +399,34 @@ 1. 提交本轮 request startup benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先评估 generated invoker provider 与 registration / service lifetime 矩阵 2. 若要更贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否在 benchmark 项目中同时接入 `Mediator` concrete runtime 对照,而不只保留 `MediatR` + +### 阶段:request invoker reflection / generated 对照(CQRS-REWRITE-RP-088) + +- 继续沿用 `$gframework-batch-boot 50`,当前 branch diff 相对 `origin/main` 仍明显低于阈值 +- 本轮目标:不再只比较 `GFramework.Cqrs` 与 `MediatR` 的外层框架差异,而是开始直接量化 `GFramework.Cqrs` 内部 reflection request binding 与 generated invoker provider 路径的 steady-state 差异 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestInvokerBenchmarkRegistry.cs` + - `GFramework.Cqrs.Benchmarks/README.md` 中的 generated invoker 场景说明 +- 设计取舍: + - 采用 benchmark 内手写的 generated registry/provider“等价物”,而不是当轮就把真实 `GFramework.Cqrs.SourceGenerators` 接到 benchmark 项目中,目的是先走通真实的 registrar -> descriptor 预热 -> dispatcher generated path,同时把写入面控制在低风险范围 + - generated 对照使用程序集级 `CqrsHandlerRegistryAttribute` + `ICqrsRequestInvokerProvider` + `IEnumeratesCqrsRequestInvokerDescriptors`,确保运行时语义与生产路径一致 + - 在 benchmark 生命周期前后清理 dispatcher 静态缓存,避免 generated descriptor 预热状态跨场景泄漏,污染 reflection 对照 +- 结论: + - 当前 benchmark 项目已经能区分 `GFramework.Cqrs` 的 reflection request 路径、generated request 路径与 `MediatR` 外部对照 + - 后续若继续贴近 `Mediator` comparison benchmark,下一批更适合扩到 registration / service lifetime、stream generated provider,或再决定是否接入 `Mediator` concrete runtime + +### 验证(RP-088) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +### 当前 stop-condition 度量(RP-088) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:提交前 branch diff 仍远低于 `50` 文件阈值,可继续推进下一批 benchmark 对照切片 + +### 当前下一步(RP-088) + +1. 提交本轮 request invoker benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先评估 registration / service lifetime 或 stream generated provider +2. 若要继续贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否把 `Mediator` concrete runtime 本身接入 benchmark 项目,而不是长期只保留 `MediatR`