test(cqrs): 补充请求调用器生成路径基准

- 新增 request reflection 与 generated invoker provider 的 steady-state 对照基准

- 引入 handwritten generated registry/provider 以走通真实 registrar 与 dispatcher 预热链路

- 更新 benchmark README 与 cqrs-rewrite RP-088 跟踪记录
This commit is contained in:
gewuyou 2026-05-06 09:36:48 +08:00
parent e0bbf13d88
commit 6e1eaf8f5c
5 changed files with 399 additions and 4 deletions

View File

@ -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;
/// <summary>
/// 为 benchmark 手写一个“生成后等价物” registry用于驱动真实的 generated invoker provider 运行时接线路径。
/// </summary>
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<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkRequest),
typeof(RequestInvokerBenchmarks.GeneratedBenchmarkResponse),
Descriptor)
];
/// <summary>
/// 将 generated benchmark request handler 注册到目标服务集合。
/// </summary>
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.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
/// </summary>
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;
}
/// <summary>
/// 模拟 generated invoker provider 产出的开放静态调用入口。
/// </summary>
public static ValueTask<RequestInvokerBenchmarks.GeneratedBenchmarkResponse> 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);
}
}

View File

@ -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;
/// <summary>
/// 对比 request steady-state dispatch 在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。
/// </summary>
[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!;
/// <summary>
/// 配置 request invoker benchmark 的公共输出格式。
/// </summary>
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));
}
}
/// <summary>
/// 构建 reflection / generated / MediatR 三组 request dispatch 对照宿主。
/// </summary>
[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<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, 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<MediatR.IRequestHandler<MediatRBenchmarkRequest, MediatRBenchmarkResponse>, MediatRBenchmarkRequestHandler>();
services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestInvokerBenchmarks).Assembly));
_serviceProvider = services.BuildServiceProvider();
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
}
/// <summary>
/// 释放 MediatR 对照组使用的 DI 宿主,并清理静态 dispatcher 缓存。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
_serviceProvider.Dispose();
ClearDispatcherCaches();
}
/// <summary>
/// 直接调用最小 request handler作为 dispatch 额外开销 baseline。
/// </summary>
[Benchmark(Baseline = true)]
public ValueTask<ReflectionBenchmarkResponse> SendRequest_Baseline()
{
return _baselineHandler.Handle(_reflectionRequest, CancellationToken.None);
}
/// <summary>
/// 通过 GFramework.CQRS 反射 request binding 路径发送 request。
/// </summary>
[Benchmark]
public ValueTask<ReflectionBenchmarkResponse> SendRequest_GFrameworkReflection()
{
return _reflectionRuntime.SendAsync(BenchmarkContext.Instance, _reflectionRequest, CancellationToken.None);
}
/// <summary>
/// 通过 generated request invoker provider 预热后的 GFramework.CQRS runtime 发送 request。
/// </summary>
[Benchmark]
public ValueTask<GeneratedBenchmarkResponse> SendRequest_GFrameworkGenerated()
{
return _generatedRuntime.SendAsync(BenchmarkContext.Instance, _generatedRequest, CancellationToken.None);
}
/// <summary>
/// 通过 MediatR 发送 request作为外部对照。
/// </summary>
[Benchmark]
public Task<MediatRBenchmarkResponse> SendRequest_MediatR()
{
return _mediatr.Send(_mediatrRequest, CancellationToken.None);
}
/// <summary>
/// 清空 dispatcher 静态缓存,避免上一轮基准残留的 generated metadata 影响当前对照。
/// </summary>
private static void ClearDispatcherCaches()
{
ClearDispatcherCache("NotificationDispatchBindings");
ClearDispatcherCache("RequestDispatchBindings");
ClearDispatcherCache("StreamDispatchBindings");
ClearDispatcherCache("GeneratedRequestInvokers");
ClearDispatcherCache("GeneratedStreamInvokers");
}
/// <summary>
/// 通过反射定位并清空 dispatcher 的指定缓存字段。
/// </summary>
/// <param name="fieldName">要清理的静态缓存字段名。</param>
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);
}
/// <summary>
/// Reflection runtime request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record ReflectionBenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<ReflectionBenchmarkResponse>;
/// <summary>
/// Reflection runtime response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record ReflectionBenchmarkResponse(Guid Id);
/// <summary>
/// Generated runtime request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record GeneratedBenchmarkRequest(Guid Id) :
GFramework.Cqrs.Abstractions.Cqrs.IRequest<GeneratedBenchmarkResponse>;
/// <summary>
/// Generated runtime response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record GeneratedBenchmarkResponse(Guid Id);
/// <summary>
/// MediatR request。
/// </summary>
/// <param name="Id">请求标识。</param>
public sealed record MediatRBenchmarkRequest(Guid Id) : MediatR.IRequest<MediatRBenchmarkResponse>;
/// <summary>
/// MediatR response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record MediatRBenchmarkResponse(Guid Id);
/// <summary>
/// Reflection runtime 的最小 request handler。
/// </summary>
public sealed class ReflectionBenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>
{
/// <summary>
/// 处理 reflection benchmark request。
/// </summary>
public ValueTask<ReflectionBenchmarkResponse> Handle(
ReflectionBenchmarkRequest request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult(new ReflectionBenchmarkResponse(request.Id));
}
}
/// <summary>
/// Generated runtime 的最小 request handler。
/// </summary>
public sealed class GeneratedBenchmarkRequestHandler :
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<GeneratedBenchmarkRequest, GeneratedBenchmarkResponse>
{
/// <summary>
/// 处理 generated benchmark request。
/// </summary>
public ValueTask<GeneratedBenchmarkResponse> Handle(
GeneratedBenchmarkRequest request,
CancellationToken cancellationToken)
{
return ValueTask.FromResult(new GeneratedBenchmarkResponse(request.Id));
}
}
/// <summary>
/// MediatR 对照组的最小 request handler。
/// </summary>
public sealed class MediatRBenchmarkRequestHandler :
MediatR.IRequestHandler<MediatRBenchmarkRequest, MediatRBenchmarkResponse>
{
/// <summary>
/// 处理 MediatR benchmark request。
/// </summary>
public Task<MediatRBenchmarkResponse> Handle(
MediatRBenchmarkRequest request,
CancellationToken cancellationToken)
{
return Task.FromResult(new MediatRBenchmarkResponse(request.Id));
}
}
}

View File

@ -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 对照

View File

@ -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=<worktree-git-dir> GIT_WORK_TREE=<worktree-root> 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 作为权威验证
## 活跃文档

View File

@ -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 metricbranch 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`