diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs new file mode 100644 index 00000000..b6f86181 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -0,0 +1,217 @@ +// 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.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; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比 request 宿主的初始化与首次分发成本,作为后续吸收 `Mediator` comparison benchmark 设计的 startup 基线。 +/// +[Config(typeof(Config))] +public class RequestStartupBenchmarks +{ + private static readonly ILogger RuntimeLogger = CreateLogger(nameof(RequestStartupBenchmarks)); + private static readonly BenchmarkRequest Request = new(Guid.NewGuid()); + + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + + /// + /// 配置 request startup benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestStartup")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 steady-state 初始化 benchmark 复用的宿主对象。 + /// + [GlobalSetup] + public void Setup() + { + Fixture.Setup("RequestStartup", handlerCount: 1, pipelineCount: 0); + + _serviceProvider = CreateMediatRServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 解析 MediatR mediator,作为 startup 句柄解析成本的 baseline。 + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("Initialization")] + public IMediator Initialization_MediatR() + { + return _serviceProvider.GetRequiredService(); + } + + /// + /// 创建 GFramework.CQRS runtime,作为同层级 startup 句柄创建成本的对照。 + /// + [Benchmark] + [BenchmarkCategory("Initialization")] + public ICqrsRuntime Initialization_GFrameworkCqrs() + { + return CreateGFrameworkRuntime(); + } + + /// + /// 在新宿主上首次发送 request,作为 MediatR 的 cold-start baseline。 + /// + [Benchmark(Baseline = true)] + [BenchmarkCategory("ColdStart")] + public async Task ColdStart_MediatR() + { + using var serviceProvider = CreateMediatRServiceProvider(); + var mediator = serviceProvider.GetRequiredService(); + return await mediator.Send(Request, CancellationToken.None).ConfigureAwait(false); + } + + /// + /// 在清空 dispatcher 静态缓存后,于新宿主上首次发送 request,量化 GFramework.CQRS 的 first-hit 成本。 + /// + [Benchmark] + [BenchmarkCategory("ColdStart")] + public ValueTask ColdStart_GFrameworkCqrs() + { + ClearDispatcherCaches(); + var runtime = CreateGFrameworkRuntime(); + return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None); + } + + /// + /// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。 + /// + private static ICqrsRuntime CreateGFrameworkRuntime() + { + var container = new MicrosoftDiContainer(); + container.RegisterTransient, BenchmarkRequestHandler>(); + return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger); + } + + /// + /// 构建只承载当前 benchmark request 的最小 MediatR 对照宿主。 + /// + private static ServiceProvider CreateMediatRServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkRequestHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestStartupBenchmarks).Assembly)); + return services.BuildServiceProvider(); + } + + /// + /// 为 benchmark 创建稳定的 fatal 级 logger,避免把日志成本混入 startup 测量。 + /// + private static ILogger CreateLogger(string categoryName) + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + return LoggerFactoryResolver.Provider.CreateLogger(categoryName); + } + + /// + /// 清空 dispatcher 静态缓存,避免同一进程中的前一轮分发污染 cold-start 结果。 + /// + 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); + } + + /// + /// 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)); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 024cede8..e27c68b4 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -18,6 +18,8 @@ - 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/RequestStartupBenchmarks.cs` + - `Initialization` 与 `ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度 - `Messaging/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 - `Messaging/StreamingBenchmarks.cs` @@ -34,5 +36,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro ## 后续扩展方向 - generated invoker provider 与纯反射 dispatch 对比 -- cold-start 与 registration 成本对比 -- request / stream 的 initialization 与首轮 dispatch 对比 +- registration / service lifetime 矩阵 +- request / stream 的 generated provider 与 concrete runtime 对照 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 76832404..bcf9df6c 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-086` +- 恢复点编号:`CQRS-REWRITE-RP-087` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -21,7 +21,8 @@ CQRS 迁移与收敛。 - `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-086` 已补齐 request pipeline `0 / 1 / 4` 数量矩阵,开始把 benchmark 关注点从单纯 messaging steady-state 扩展到行为编排开销 + - `RP-086` 已补齐 request pipeline `0 / 1 / 4` 数量矩阵,开始把 benchmark 关注点从单纯 messaging steady-state 扩展到行为编排开销 + - 当前 `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -59,6 +60,9 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:包含新增 `RequestPipelineBenchmarks` 后再次复核通过 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:包含新增 `RequestStartupBenchmarks` 后再次复核通过 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 - `git diff --check` @@ -111,7 +115,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 cold-start / initialization 与 generated invoker provider 对照 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 generated invoker provider、registration / service lifetime 与更贴近 `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 ad1f2077..b0416b2c 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 @@ -369,3 +369,33 @@ 1. 提交本轮 request pipeline benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 initialization / cold-start 场景 2. 当需要验证 dispatcher 预热与 source generator 收益时,引入 generated invoker provider 对照,并评估是否同时接入 `Mediator` concrete runtime 作为更贴近设计哲学的外部参照 + +### 阶段:request startup 基线(CQRS-REWRITE-RP-087) + +- 继续沿用 `$gframework-batch-boot 50`,当前 branch diff 相对 `origin/main` 仍明显低于阈值 +- 本轮目标:把 benchmark 从 steady-state dispatch 再向前推进一层,补齐与 `ai-libs/Mediator/benchmarks/Mediator.Benchmarks/Messaging/Comparison/*` 更接近的 startup 维度 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/README.md` 中的 startup 场景说明 +- 设计取舍: + - `Initialization` 只测“从已配置宿主解析/创建 runtime 句柄”的成本,不把完整架构初始化混入 benchmark + - `ColdStart` 只测新宿主上的首次 request send;`GFramework.Cqrs` 侧在每次 benchmark 前通过反射清空 dispatcher 静态缓存,避免把热缓存误当 first-hit + - `ColdStart_MediatR` 改为真正 `await` 完任务后再释放 `ServiceProvider`,以满足 `Meziantou.Analyzer` 对资源生命周期的要求,并避免 benchmark 本身含有错误宿主释放语义 +- 结论: + - 当前 benchmark 项目已经覆盖 `Request`、`Notification`、`StreamRequest`、`RequestPipeline`、`RequestStartup` + - 后续若继续贴近 `Mediator` comparison benchmark,下一批最有价值的是 generated invoker provider、registration / service lifetime 与 concrete runtime 外部对照,而不是继续只加同层 steady-state case + +### 验证(RP-087) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + +### 当前 stop-condition 度量(RP-087) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:提交前 branch diff 仍远低于 `50` 文件阈值,可继续下一批 benchmark 或低风险 runtime 对照切片 + +### 当前下一步(RP-087) + +1. 提交本轮 request startup benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先评估 generated invoker provider 与 registration / service lifetime 矩阵 +2. 若要更贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否在 benchmark 项目中同时接入 `Mediator` concrete runtime 对照,而不只保留 `MediatR`