From 96729ddcf10ba75895bb6649737042ee1e38cf24 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 08:57:59 +0800 Subject: [PATCH 01/10] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E5=9F=BA?= =?UTF-8?q?=E5=87=86=E4=B8=8E=E7=94=9F=E6=88=90=E5=99=A8=E5=9B=9E=E5=BD=92?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增独立的 GFramework.Cqrs.Benchmarks 项目并引入 request、notification 对比场景 - 补充 request 与 stream invoker provider 的 mixed direct/reflected 顺序回归测试 - 更新 solution、meta-package 排除规则与 CQRS ai-plan 恢复点 --- GFramework.Cqrs.Benchmarks/CustomColumn.cs | 71 +++++ .../GFramework.Cqrs.Benchmarks.csproj | 29 ++ .../Messaging/BenchmarkContext.cs | 17 + .../Messaging/Fixture.cs | 35 ++ .../Messaging/NotificationBenchmarks.cs | 136 ++++++++ .../Messaging/RequestBenchmarks.cs | 154 +++++++++ GFramework.Cqrs.Benchmarks/Program.cs | 23 ++ GFramework.Cqrs.Benchmarks/README.md | 35 ++ .../Cqrs/CqrsHandlerRegistryGeneratorTests.cs | 298 ++++++++++++++++++ GFramework.csproj | 3 + GFramework.sln | 14 + .../todos/cqrs-rewrite-migration-tracking.md | 16 +- .../traces/cqrs-rewrite-migration-trace.md | 84 +++++ 13 files changed, 912 insertions(+), 3 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/CustomColumn.cs create mode 100644 GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs create mode 100644 GFramework.Cqrs.Benchmarks/Program.cs create mode 100644 GFramework.Cqrs.Benchmarks/README.md diff --git a/GFramework.Cqrs.Benchmarks/CustomColumn.cs b/GFramework.Cqrs.Benchmarks/CustomColumn.cs new file mode 100644 index 00000000..23a73f2e --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/CustomColumn.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Reports; +using BenchmarkDotNet.Running; +using System; + +namespace GFramework.Cqrs.Benchmarks; + +/// +/// 为 CQRS benchmark 结果补充可读的场景标签列。 +/// +/// 列名。 +/// 从 benchmark case 提取列值的委托。 +public sealed class CustomColumn(string columnName, Func getValue) : IColumn +{ + /// + public string Id => $"{nameof(CustomColumn)}.{ColumnName}"; + + /// + public string ColumnName { get; } = columnName; + + /// + public bool AlwaysShow => true; + + /// + public ColumnCategory Category => ColumnCategory.Params; + + /// + public int PriorityInCategory => 0; + + /// + public bool IsNumeric => false; + + /// + public UnitType UnitType => UnitType.Dimensionless; + + /// + public string Legend => $"Custom '{ColumnName}' tag column"; + + /// + public bool IsDefault(Summary summary, BenchmarkCase benchmarkCase) + { + return false; + } + + /// + public bool IsAvailable(Summary summary) + { + return true; + } + + /// + public string GetValue(Summary summary, BenchmarkCase benchmarkCase) + { + return getValue(summary, benchmarkCase); + } + + /// + public string GetValue(Summary summary, BenchmarkCase benchmarkCase, SummaryStyle style) + { + return GetValue(summary, benchmarkCase); + } + + /// + public override string ToString() + { + return ColumnName; + } +} diff --git a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj new file mode 100644 index 00000000..6dd185ba --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj @@ -0,0 +1,29 @@ + + + + + + Exe + net10.0 + disable + enable + false + + + + + + + + + + + + + + + + diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs new file mode 100644 index 00000000..ff0efb46 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using GFramework.Cqrs.Abstractions.Cqrs; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为纯 runtime benchmark 提供最小 CQRS 上下文标记,避免把完整架构上下文初始化成本混入 steady-state dispatch。 +/// +internal sealed class BenchmarkContext : ICqrsContext +{ + /// + /// 共享的最小 CQRS 上下文实例。 + /// + public static BenchmarkContext Instance { get; } = new(); +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs b/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs new file mode 100644 index 00000000..de42f49f --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Loggers; +using System; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为 CQRS benchmark 运行打印并验证当前场景配置,避免矩阵配置与实际运行环境漂移。 +/// +internal static class Fixture +{ + /// + /// 输出当前 benchmark 配置并验证关键环境变量。 + /// + /// 当前 benchmark 场景名称。 + /// 当前场景的处理器数量。 + /// 当前场景的 pipeline 行为数量。 + public static void Setup(string scenario, int handlerCount, int pipelineCount) + { + ConsoleLogger.Default.WriteLineHeader("GFramework.Cqrs benchmark config"); + ConsoleLogger.Default.WriteLineInfo($"Scenario = {scenario}"); + ConsoleLogger.Default.WriteLineInfo($"HandlerCount = {handlerCount}"); + ConsoleLogger.Default.WriteLineInfo($"PipelineCount = {pipelineCount}"); + + var environmentScenario = Environment.GetEnvironmentVariable("GFRAMEWORK_CQRS_BENCHMARK_SCENARIO"); + if (!string.IsNullOrWhiteSpace(environmentScenario) && + !string.Equals(environmentScenario, scenario, StringComparison.Ordinal)) + { + throw new InvalidOperationException( + $"Scenario mismatch. Expected '{environmentScenario}', actual '{scenario}'."); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs new file mode 100644 index 00000000..7cc98c74 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -0,0 +1,136 @@ +// 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; + +/// +/// 对比单处理器 notification 在 GFramework.CQRS 与 MediatR 之间的 publish 开销。 +/// +[Config(typeof(Config))] +public class NotificationBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IPublisher _publisher = null!; + private BenchmarkNotification _notification = null!; + + /// + /// 配置 notification benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "Notification")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 notification publish 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0); + + _container = new MicrosoftDiContainer(); + _container.RegisterTransient, BenchmarkNotificationHandler>(); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkNotificationHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _publisher = _serviceProvider.GetRequiredService(); + + _notification = new BenchmarkNotification(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 通过 GFramework.CQRS runtime 发布 notification。 + /// + [Benchmark(Baseline = true)] + public ValueTask PublishNotification_GFrameworkCqrs() + { + return _runtime.PublishAsync(BenchmarkContext.Instance, _notification, CancellationToken.None); + } + + /// + /// 通过 MediatR 发布 notification,作为外部设计对照。 + /// + [Benchmark] + public Task PublishNotification_MediatR() + { + return _publisher.Publish(_notification, CancellationToken.None); + } + + /// + /// Benchmark notification。 + /// + /// 通知标识。 + public sealed record BenchmarkNotification(Guid Id) : + GFramework.Cqrs.Abstractions.Cqrs.INotification, + MediatR.INotification; + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 notification handler。 + /// + public sealed class BenchmarkNotificationHandler : + GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler, + MediatR.INotificationHandler + { + /// + /// 处理 GFramework.CQRS notification。 + /// + public ValueTask Handle(BenchmarkNotification notification, CancellationToken cancellationToken) + { + return ValueTask.CompletedTask; + } + + /// + /// 处理 MediatR notification。 + /// + Task MediatR.INotificationHandler.Handle( + BenchmarkNotification notification, + CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs new file mode 100644 index 00000000..e925f794 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -0,0 +1,154 @@ +// 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; + +/// +/// 对比单个 request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。 +/// +[Config(typeof(Config))] +public class RequestBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private BenchmarkRequestHandler _baselineHandler = null!; + private BenchmarkRequest _request = null!; + + /// + /// 配置 request benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "Request")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 request dispatch 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0); + + _container = new MicrosoftDiContainer(); + _baselineHandler = new BenchmarkRequestHandler(); + + _container.RegisterTransient, BenchmarkRequestHandler>(); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkRequestHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + + _request = new BenchmarkRequest(Guid.NewGuid()); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 直接调用 handler,作为 dispatch 额外开销的 baseline。 + /// + [Benchmark(Baseline = true)] + public ValueTask SendRequest_Baseline() + { + return _baselineHandler.Handle(_request, CancellationToken.None); + } + + /// + /// 通过 GFramework.CQRS runtime 发送 request。 + /// + [Benchmark] + public ValueTask SendRequest_GFrameworkCqrs() + { + return _runtime.SendAsync(BenchmarkContext.Instance, _request, CancellationToken.None); + } + + /// + /// 通过 MediatR 发送 request,作为外部设计对照。 + /// + [Benchmark] + public Task SendRequest_MediatR() + { + return _mediatr.Send(_request, CancellationToken.None); + } + + /// + /// 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/Program.cs b/GFramework.Cqrs.Benchmarks/Program.cs new file mode 100644 index 00000000..44324baa --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Program.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; + +namespace GFramework.Cqrs.Benchmarks; + +/// +/// 提供 GFramework.CQRS benchmark 的统一命令行入口。 +/// +internal static class Program +{ + /// + /// 运行当前程序集中的全部 benchmark。 + /// + /// 透传给 BenchmarkDotNet 的命令行参数。 + private static void Main(string[] args) + { + ConsoleLogger.Default.WriteLine("Running GFramework.Cqrs benchmarks"); + BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md new file mode 100644 index 00000000..3ea73f65 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -0,0 +1,35 @@ +# GFramework.Cqrs.Benchmarks + +该模块承载 `GFramework.Cqrs` 的独立性能基准工程,用于持续比较运行时 dispatch、publish、cold-start 与后续 generator / pipeline 收口的成本变化。 + +## 目的 + +- 为 `GFramework.Cqrs` 建立独立于 NUnit 集成测试的 BenchmarkDotNet 基线 +- 参考 `ai-libs/Mediator/benchmarks` 的场景组织方式,逐步补齐 request、notification、stream 与初始化成本对比 +- 为后续吸收 `Mediator` 的 dispatch 设计、fixture 组织和对比矩阵提供可重复验证入口 + +## 当前内容 + +- `Program.cs` + - benchmark 命令行入口 +- `Messaging/Fixture.cs` + - 运行前输出并校验场景配置 +- `Messaging/RequestBenchmarks.cs` + - direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 +- `Messaging/NotificationBenchmarks.cs` + - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 + +## 最小使用方式 + +```bash +dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release +``` + +也可以通过 `BenchmarkDotNet` 过滤器只运行某一类场景。 + +## 后续扩展方向 + +- pipeline behavior 数量矩阵 +- generated invoker provider 与纯反射 dispatch 对比 +- stream request benchmark +- cold-start 与 registration 成本对比 diff --git a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs index 545577f2..8d100aa8 100644 --- a/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs +++ b/GFramework.SourceGenerators.Tests/Cqrs/CqrsHandlerRegistryGeneratorTests.cs @@ -2003,6 +2003,120 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string MixedRequestInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest + { + ValueTask Handle(TRequest request, CancellationToken cancellationToken); + } + + public interface INotificationHandler where TNotification : INotification { } + public interface IStreamRequestHandler where TRequest : IStreamRequest { } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsRequestInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsRequestInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsRequestInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsRequestInvokerDescriptor + { + public CqrsRequestInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsRequestInvokerDescriptorEntry + { + public CqrsRequestInvokerDescriptorEntry(Type requestType, Type responseType, CqrsRequestInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsRequestInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record AlphaRequest() : IRequest; + + public sealed record BetaRequest() : IRequest; + + public sealed class AlphaHandler : IRequestHandler + { + public ValueTask Handle(AlphaRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult("alpha"); + } + } + + public sealed class Container + { + private sealed class HiddenBetaHandler : IRequestHandler + { + public ValueTask Handle(BetaRequest request, CancellationToken cancellationToken) + { + return ValueTask.FromResult(42); + } + } + } + } + """; + private const string HiddenImplementationStreamInvokerProviderSource = """ using System; using System.Collections.Generic; @@ -2108,6 +2222,122 @@ public class CqrsHandlerRegistryGeneratorTests } """; + private const string MixedStreamInvokerProviderSource = """ + using System; + using System.Collections.Generic; + using System.Reflection; + using System.Threading; + using System.Threading.Tasks; + + namespace Microsoft.Extensions.DependencyInjection + { + public interface IServiceCollection { } + + public static class ServiceCollectionServiceExtensions + { + public static void AddTransient(IServiceCollection services, Type serviceType, Type implementationType) { } + } + } + + namespace GFramework.Core.Abstractions.Logging + { + public interface ILogger + { + void Debug(string msg); + } + } + + namespace GFramework.Cqrs.Abstractions.Cqrs + { + public interface IRequest { } + public interface INotification { } + public interface IStreamRequest { } + + public interface IRequestHandler where TRequest : IRequest { } + public interface INotificationHandler where TNotification : INotification { } + + public interface IStreamRequestHandler where TRequest : IStreamRequest + { + IAsyncEnumerable Handle(TRequest request, CancellationToken cancellationToken); + } + } + + namespace GFramework.Cqrs + { + public interface ICqrsHandlerRegistry + { + void Register(Microsoft.Extensions.DependencyInjection.IServiceCollection services, GFramework.Core.Abstractions.Logging.ILogger logger); + } + + public interface ICqrsStreamInvokerProvider + { + bool TryGetDescriptor(Type requestType, Type responseType, out CqrsStreamInvokerDescriptor? descriptor); + } + + public interface IEnumeratesCqrsStreamInvokerDescriptors + { + IReadOnlyList GetDescriptors(); + } + + public sealed class CqrsStreamInvokerDescriptor + { + public CqrsStreamInvokerDescriptor(Type handlerType, MethodInfo invokerMethod) { } + } + + public sealed class CqrsStreamInvokerDescriptorEntry + { + public CqrsStreamInvokerDescriptorEntry(Type requestType, Type responseType, CqrsStreamInvokerDescriptor descriptor) + { + RequestType = requestType; + ResponseType = responseType; + Descriptor = descriptor; + } + + public Type RequestType { get; } + + public Type ResponseType { get; } + + public CqrsStreamInvokerDescriptor Descriptor { get; } + } + + [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] + public sealed class CqrsHandlerRegistryAttribute : Attribute + { + public CqrsHandlerRegistryAttribute(Type registryType) { } + } + } + + namespace TestApp + { + using GFramework.Cqrs.Abstractions.Cqrs; + + public sealed record AlphaStream() : IStreamRequest; + + public sealed record BetaStream() : IStreamRequest; + + public sealed class AlphaStreamHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(AlphaStream request, CancellationToken cancellationToken) + { + yield return 1; + await Task.CompletedTask; + } + } + + public sealed class Container + { + private sealed class HiddenBetaStreamHandler : IStreamRequestHandler + { + public async IAsyncEnumerable Handle(BetaStream request, CancellationToken cancellationToken) + { + yield return "beta"; + await Task.CompletedTask; + } + } + } + } + """; + private const string PreciseReflectedRequestInvokerProviderBoundarySource = """ using System; using System.Collections.Generic; @@ -3052,6 +3282,40 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当同一轮生成同时包含 direct registration 与“隐藏实现类型 + 可见 handler interface”注册时, + /// request invoker provider 会按稳定实现排序生成连续描述符和方法编号。 + /// + [Test] + public void Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations() + { + var generatedSource = RunGenerator(MixedRequestInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.AlphaRequest), typeof(string), new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(typeof(global::TestApp.BetaRequest), typeof(int), new global::GFramework.Cqrs.CqrsRequestInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeRequestHandler1), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "private static global::System.Threading.Tasks.ValueTask InvokeRequestHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "private static global::System.Threading.Tasks.ValueTask InvokeRequestHandler1(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler)handler;")); + }); + } + /// /// 验证当 runtime 缺少 ICqrsRequestInvokerProvider 时, /// 生成器会整体跳过 request invoker provider 元数据发射,而不是输出半套 descriptor 成员。 @@ -3265,6 +3529,40 @@ public class CqrsHandlerRegistryGeneratorTests }); } + /// + /// 验证当同一轮生成同时包含 direct registration 与“隐藏实现类型 + 可见 handler interface”注册时, + /// stream invoker provider 会按稳定实现排序生成连续描述符和方法编号。 + /// + [Test] + public void Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations() + { + var generatedSource = RunGenerator(MixedStreamInvokerProviderSource); + + Assert.Multiple(() => + { + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.AlphaStream), typeof(int), new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler0), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "new global::GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(typeof(global::TestApp.BetaStream), typeof(string), new global::GFramework.Cqrs.CqrsStreamInvokerDescriptor(typeof(global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler), typeof(__GFrameworkGeneratedCqrsHandlerRegistry).GetMethod(nameof(InvokeStreamHandler1), global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static)!))")); + Assert.That( + generatedSource, + Does.Contain( + "private static object InvokeStreamHandler0(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "private static object InvokeStreamHandler1(object handler, object request, global::System.Threading.CancellationToken cancellationToken)")); + Assert.That( + generatedSource, + Does.Contain( + "var typedHandler = (global::GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler)handler;")); + }); + } + /// /// 验证当 runtime 缺少 ICqrsStreamInvokerProvider 时, /// 生成器会整体跳过 stream invoker provider 元数据发射,而不是保留孤立的 descriptor 成员。 diff --git a/GFramework.csproj b/GFramework.csproj index 296cd4bf..56e714c8 100644 --- a/GFramework.csproj +++ b/GFramework.csproj @@ -74,6 +74,7 @@ + @@ -123,6 +124,7 @@ + @@ -158,6 +160,7 @@ + diff --git a/GFramework.sln b/GFramework.sln index b2fe4c40..e5c8ffdc 100644 --- a/GFramework.sln +++ b/GFramework.sln @@ -50,6 +50,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.SourceGener EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Game.SourceGenerators", "GFramework.Game.SourceGenerators\GFramework.Game.SourceGenerators.csproj", "{9D3AADF0-55E6-4F80-B9C5-875F63E170D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GFramework.Cqrs.Benchmarks", "GFramework.Cqrs.Benchmarks\GFramework.Cqrs.Benchmarks.csproj", "{5609D017-E481-431B-874A-7D06BFF698F9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -360,6 +362,18 @@ Global {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x64.Build.0 = Release|Any CPU {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x86.ActiveCfg = Release|Any CPU {9D3AADF0-55E6-4F80-B9C5-875F63E170D8}.Release|x86.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x64.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Debug|x86.Build.0 = Debug|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|Any CPU.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x64.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x64.Build.0 = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x86.ActiveCfg = Release|Any CPU + {5609D017-E481-431B-874A-7D06BFF698F9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE 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 efc871e8..17cf7319 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-082` +- 恢复点编号:`CQRS-REWRITE-RP-084` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -18,6 +18,8 @@ CQRS 迁移与收敛。 - `RP-080` 已将基础 generation gate 回归扩展到 notification handler interface、stream handler interface 与 registry attribute 缺失分支 - `RP-081` 已继续补齐基础 generation gate 的 logging 与 DI runtime contract 缺失分支 - 当前 `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 组织方式的第一落点 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -45,6 +47,14 @@ CQRS 迁移与收敛。 - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 - `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - 结果:通过,`0 warning / 0 error` +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` + - 结果:通过,`2/2` passed +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - 结果:通过,`5/5` passed - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator"` @@ -93,8 +103,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进代码切片,优先复核基础 generation gate 之外的 runtime contract 或 fallback selection 分支;基础 gate 的可安全构造缺失分支已覆盖 -3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build 或 targeted test 作为权威验证 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 pipeline、stream、cold-start 与 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 0dec487b..5a8f4129 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 @@ -212,3 +212,87 @@ 1. 若 review 重新触发后仍有 latest-head open thread,继续以 `PR #323` 为当前唯一 PR 恢复锚点复核 2. 后续若继续推进代码切片,优先复核基础 generation gate 之外的 runtime contract 或 fallback selection 分支 + +## 2026-05-06 + +### 阶段:mixed invoker provider 排序回归(CQRS-REWRITE-RP-083) + +- 使用 `$gframework-batch-boot 50` 继续 `feat/cqrs-optimization` 的 CQRS 收口批次 +- 批次目标:在 branch diff 相对 `origin/main` 接近 `50` 个文件前,继续补齐低风险的 generator runtime contract / emission 回归 +- 本轮基线选择: + - `origin/main a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` + - `main a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` + - 当前分支 `feat/cqrs-optimization a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` +- 启动时 branch diff vs `origin/main` 为 `0` files / `0` lines,因此继续选择低风险测试回归切片 +- 本轮复核 `CreateGeneratedRegistrySourceShape` 与 invoker emission 路径后确认: + - 现有测试已覆盖 request / stream provider 的单一 direct 场景、单一 reflected-implementation 场景、precise reflected 跳过边界,以及各项 runtime contract 缺失分支 + - 尚未锁定“同一 registry 同时包含 direct registration 与 reflected-implementation registration”时的 descriptor 顺序与 `Invoke*HandlerN` 编号稳定性 +- 已补齐: + - `Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations` + - `Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations` + - 两组 source fixture:`MixedRequestInvokerProviderSource`、`MixedStreamInvokerProviderSource` +- 通过新增回归,显式锁定以下约束: + - provider descriptor 条目按稳定实现排序输出 + - `InvokeRequestHandler0/1` 与 `InvokeStreamHandler0/1` 的方法编号随 emission 顺序连续增长 + - 隐藏实现类型不会破坏 direct registration 与 reflected-implementation registration 的混合发射 + +### 验证(RP-083) + +- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` + - 结果:通过,`2/2` passed + +### 当前 stop-condition 度量(RP-083) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:active batch 尚未提交时,基于 `HEAD` 的 branch diff 仍显示 `0` files / `0` lines;提交本批后再以新 `HEAD` 复算累计 branch diff + +### 当前下一步(RP-083) + +1. 提交本轮 mixed invoker provider 排序回归后,复算 branch diff vs `origin/main`,确认 `50` 文件阈值仍有充足余量 +2. 若继续推进代码切片,优先复核 invoker provider 之外的 runtime contract 或 fallback selection 分支 + +### 阶段:benchmark 基础设施引入(CQRS-REWRITE-RP-084) + +- 用户明确将当前长期分支目标上提为:系统性吸收 `ai-libs/Mediator` 的实现思路与设计哲学,并将可取部分纳入 `GFramework.Cqrs` +- 本轮据此调整批次目标,不再把关注点收缩到单个 generator 回归,而是建立能持续比较和吸收设计差异的 benchmark 基础设施 +- 参考 `ai-libs/Mediator` 的 benchmark 设计后,本轮采纳的核心结构包括: + - 独立 benchmark 项目壳,而非扩展现有 NUnit 测试项目 + - 共享 `Fixture` 输出并校验场景配置 + - `Request` / `Notification` 两个 messaging 场景作为首批最小落点 + - 自定义列 `CustomColumn`,为后续矩阵扩展保留可读结果标签 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj` + - `GFramework.Cqrs.Benchmarks/Program.cs` + - `GFramework.Cqrs.Benchmarks/CustomColumn.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/Fixture.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkContext.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/README.md` +- 设计取舍: + - 使用最小 `ICqrsContext` marker,避免把完整 `ArchitectureContext` 初始化成本混入 steady-state dispatch + - 直接复用 `GFramework.Cqrs.CqrsRuntimeFactory` 与 `MicrosoftDiContainer`,让基准聚焦于 runtime dispatch / publish + - 外部对照组先接入 `MediatR`,保持与 `Mediator` benchmark 的对照哲学一致;但本轮仍只做最小 request / notification 场景 + - 暂不把 source generator benchmark、cold-start 独立工程或完整 pipeline / stream 矩阵一起引入,避免首批 scope 失控 +- 兼容性修正: + - 在根 `GFramework.csproj` 中显式排除 `GFramework.Cqrs.Benchmarks/**`,避免 meta-package 意外编译 benchmark 源码 + - 将 benchmark 项目加入 `GFramework.sln`,保持仓库级工作流完整 + +### 验证(RP-084) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 + +### 当前 stop-condition 度量(RP-084) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:本轮仍在 `50` 文件阈值以内,可继续按 benchmark 场景或 CQRS runtime 对照能力分批推进 + +### 当前下一步(RP-084) + +1. 继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 pipeline、stream、cold-start 与 generated invoker provider 对照场景 +2. 当后续有具体 runtime 优化切片时,用该 benchmark 项目验证是否真正吸收到了 `Mediator` 的低开销 dispatch 设计收益 From e6f98cb4afedb38b4491f49314b8e1f1c9aa753c Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 09:14:33 +0800 Subject: [PATCH 02/10] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E6=B5=81?= =?UTF-8?q?=E5=BC=8F=E8=AF=B7=E6=B1=82=E5=9F=BA=E5=87=86=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 StreamingBenchmarks 并对齐 baseline、GFramework.Cqrs 与 MediatR 的完整枚举对照 - 更新 benchmark README 与 CQRS ai-plan 恢复点,记录 stream 场景落地 --- .../Messaging/StreamingBenchmarks.cs | 183 ++++++++++++++++++ GFramework.Cqrs.Benchmarks/README.md | 3 +- .../todos/cqrs-rewrite-migration-tracking.md | 10 +- .../traces/cqrs-rewrite-migration-trace.md | 38 ++++ 4 files changed, 230 insertions(+), 4 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs new file mode 100644 index 00000000..658d7486 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -0,0 +1,183 @@ +// 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.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; + +/// +/// 对比单个 stream request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的完整枚举开销。 +/// +[Config(typeof(Config))] +public class StreamingBenchmarks +{ + private MicrosoftDiContainer _container = null!; + private ICqrsRuntime _runtime = null!; + private ServiceProvider _serviceProvider = null!; + private IMediator _mediatr = null!; + private BenchmarkStreamHandler _baselineHandler = null!; + private BenchmarkStreamRequest _request = null!; + + /// + /// 配置 stream benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamRequest")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 stream dispatch 所需的最小 runtime 宿主和对照对象。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0); + + _container = new MicrosoftDiContainer(); + _baselineHandler = new BenchmarkStreamHandler(); + + _container.RegisterTransient, BenchmarkStreamHandler>(); + _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _container, + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamingBenchmarks))); + + var services = new ServiceCollection(); + services.AddSingleton, BenchmarkStreamHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamingBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + + _request = new BenchmarkStreamRequest(Guid.NewGuid(), 3); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + } + + /// + /// 直接调用 handler 并完整枚举响应序列,作为 stream dispatch 额外开销的 baseline。 + /// + [Benchmark(Baseline = true)] + public async ValueTask Stream_Baseline() + { + await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。 + /// + [Benchmark] + public async ValueTask Stream_GFrameworkCqrs() + { + await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None) + .ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 MediatR 创建并完整枚举 stream,作为外部设计对照。 + /// + [Benchmark] + public async ValueTask Stream_MediatR() + { + await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// Benchmark stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest, + MediatR.IStreamRequest; + + /// + /// 复用 request benchmark 的响应结构,保持跨场景可比性。 + /// + /// 响应标识。 + public sealed record BenchmarkResponse(Guid Id); + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。 + /// + public sealed class BenchmarkStreamHandler : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + MediatR.IStreamRequestHandler + { + /// + /// 处理 GFramework.CQRS stream request。 + /// + public IAsyncEnumerable Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 处理 MediatR stream request。 + /// + IAsyncEnumerable MediatR.IStreamRequestHandler.Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 为 benchmark 构造稳定、低噪声的异步响应序列。 + /// + private static async IAsyncEnumerable EnumerateAsync( + BenchmarkStreamRequest request, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + for (int index = 0; index < request.ItemCount; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return new BenchmarkResponse(request.Id); + await Task.CompletedTask.ConfigureAwait(false); + } + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 3ea73f65..8d281a50 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/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 +- `Messaging/StreamingBenchmarks.cs` + - direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比 ## 最小使用方式 @@ -31,5 +33,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro - pipeline behavior 数量矩阵 - generated invoker provider 与纯反射 dispatch 对比 -- stream request benchmark - cold-start 与 registration 成本对比 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 17cf7319..40336c70 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-084` +- 恢复点编号:`CQRS-REWRITE-RP-085` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -19,7 +19,8 @@ CQRS 迁移与收敛。 - `RP-081` 已继续补齐基础 generation gate 的 logging 与 DI runtime contract 缺失分支 - 当前 `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-084` 已引入独立 `GFramework.Cqrs.Benchmarks` 项目,作为持续吸收 `Mediator` benchmark 组织方式的第一落点 + - 当前 `RP-085` 已补齐 stream request benchmark,对齐 `Mediator` messaging benchmark 的第二个核心场景 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -51,6 +52,9 @@ CQRS 迁移与收敛。 - 结果:通过,`2/2` passed - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:包含新增 `StreamingBenchmarks` 后再次复核通过 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 - `git diff --check` @@ -103,7 +107,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 pipeline、stream、cold-start 与 generated invoker provider 对照 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 request pipeline 数量矩阵、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 5a8f4129..76680e79 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 @@ -296,3 +296,41 @@ 1. 继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 pipeline、stream、cold-start 与 generated invoker provider 对照场景 2. 当后续有具体 runtime 优化切片时,用该 benchmark 项目验证是否真正吸收到了 `Mediator` 的低开销 dispatch 设计收益 + +### 阶段:stream request benchmark 对照(CQRS-REWRITE-RP-085) + +- 继续沿用 `$gframework-batch-boot 50`,当前 branch diff 相对 `origin/main` 仍明显低于阈值 +- 在 `RP-084` 已建立独立 benchmark 项目后,本轮优先补齐 `ai-libs/Mediator/benchmarks/Mediator.Benchmarks/Messaging/StreamingBenchmarks.cs` 对应的最小 stream 场景 +- 选择 stream 作为第二批 benchmark 的原因: + - 已有独立的 `CreateStream` runtime 路径和单独的 stream invoker provider 元数据契约 + - 与 `Mediator` 的 messaging benchmark 分层直接对应 + - 不需要像 pipeline / cold-start 那样先进一步澄清运行时或宿主边界 +- 本轮新增: + - `GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/README.md` 中的 stream 场景说明 +- 设计约束: + - 保持与前一批一致的三路对照:`Baseline`、`GFramework.Cqrs`、`MediatR` + - 基准测量“完整枚举 3 个元素”的全量消费成本,而不是只测创建异步枚举器 + - 使用最小 `ICqrsContext` marker,继续避免把完整 `ArchitectureContext` 初始化成本混入 steady-state stream dispatch +- 结论: + - 当前 benchmark 项目已经覆盖 `Request`、`Notification`、`StreamRequest` 三个核心 messaging steady-state 场景 + - 下一批更适合转向 request pipeline 数量矩阵或 cold-start / initialization,而不是继续扩同层次的 messaging 基线 + +### 验证(RP-085) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 + +### 当前 stop-condition 度量(RP-085) + +- primary metric:branch diff files vs `origin/main` +- 当前说明:新增 stream benchmark 后仍处于 `50` 文件阈值以内,适合继续下一批 request pipeline 或 cold-start 场景 + +### 当前下一步(RP-085) + +1. 继续扩展 `GFramework.Cqrs.Benchmarks`,优先补齐 request pipeline 数量矩阵,随后再评估 cold-start / initialization +2. 当需要验证 generated invoker provider 的实际收益时,把 request benchmark 扩展为 reflection / generated provider 对照,而不是只停留在框架间对比 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 03/10] =?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 作为更贴近设计哲学的外部参照 From e0bbf13d880a58fb9afa7118c9922c62f211f079 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 09:30:17 +0800 Subject: [PATCH 04/10] =?UTF-8?q?test(cqrs):=20=E8=A1=A5=E5=85=85=E8=AF=B7?= =?UTF-8?q?=E6=B1=82=E5=90=AF=E5=8A=A8=E9=98=B6=E6=AE=B5=E5=9F=BA=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 request initialization 与 cold-start 基准并对齐当前 runtime 启动口径 - 通过清理 dispatcher 静态缓存隔离 GFramework.Cqrs 首次分发测量结果 - 更新 benchmark README 与 cqrs-rewrite RP-087 跟踪记录 --- .../Messaging/RequestStartupBenchmarks.cs | 217 ++++++++++++++++++ GFramework.Cqrs.Benchmarks/README.md | 6 +- .../todos/cqrs-rewrite-migration-tracking.md | 10 +- .../traces/cqrs-rewrite-migration-trace.md | 30 +++ 4 files changed, 258 insertions(+), 5 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs 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` 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 05/10] =?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` From 449eeb96062b5e6c622a42bc483934f9aa6c1844 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 09:46:52 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat(cqrs):=20=E8=A1=A5=E9=BD=90=20stream?= =?UTF-8?q?=20invoker=20=E5=9F=BA=E5=87=86=E5=AF=B9=E7=85=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 stream generated invoker benchmark 与手写 registry,对照 reflection runtime、generated runtime 和 MediatR 的完整枚举开销 - 更新 benchmark README,补充 generated stream invoker provider 的场景说明与后续扩展方向 - 更新 cqrs-rewrite 跟踪与 trace,记录 RP-089 的基线、验证结果和下一批建议 --- ...GeneratedStreamInvokerBenchmarkRegistry.cs | 94 ++++++ .../Messaging/StreamInvokerBenchmarks.cs | 307 ++++++++++++++++++ GFramework.Cqrs.Benchmarks/README.md | 3 + .../todos/cqrs-rewrite-migration-tracking.md | 12 +- .../traces/cqrs-rewrite-migration-trace.md | 34 ++ 5 files changed, 448 insertions(+), 2 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamInvokerBenchmarkRegistry.cs create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamInvokerBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamInvokerBenchmarkRegistry.cs new file mode 100644 index 00000000..433da49f --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamInvokerBenchmarkRegistry.cs @@ -0,0 +1,94 @@ +// 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; + +/// +/// 为 benchmark 手写一个“生成后等价物” stream registry,用于驱动真实的 generated stream invoker provider 运行时接线路径。 +/// +public sealed class GeneratedStreamInvokerBenchmarkRegistry : + GFramework.Cqrs.ICqrsHandlerRegistry, + GFramework.Cqrs.ICqrsStreamInvokerProvider, + GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors +{ + private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor = + new( + typeof(IStreamRequestHandler< + StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest, + StreamInvokerBenchmarks.GeneratedBenchmarkResponse>), + typeof(GeneratedStreamInvokerBenchmarkRegistry).GetMethod( + nameof(InvokeGeneratedStreamHandler), + BindingFlags.Public | BindingFlags.Static) + ?? throw new InvalidOperationException("Missing generated stream invoker benchmark method.")); + + private static readonly IReadOnlyList Descriptors = + [ + new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry( + typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest), + typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse), + Descriptor) + ]; + + /// + /// 将 generated benchmark stream handler 注册到目标服务集合。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddTransient( + typeof(IStreamRequestHandler< + StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest, + StreamInvokerBenchmarks.GeneratedBenchmarkResponse>), + typeof(StreamInvokerBenchmarks.GeneratedBenchmarkStreamHandler)); + logger.Debug("Registered generated stream invoker 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(StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest) && + responseType == typeof(StreamInvokerBenchmarks.GeneratedBenchmarkResponse)) + { + descriptor = Descriptor; + return true; + } + + descriptor = null; + return false; + } + + /// + /// 模拟 generated stream invoker provider 产出的开放静态调用入口。 + /// + public static object InvokeGeneratedStreamHandler(object handler, object request, CancellationToken cancellationToken) + { + var typedHandler = (IStreamRequestHandler< + StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest, + StreamInvokerBenchmarks.GeneratedBenchmarkResponse>)handler; + var typedRequest = (StreamInvokerBenchmarks.GeneratedBenchmarkStreamRequest)request; + return typedHandler.Handle(typedRequest, cancellationToken); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs new file mode 100644 index 00000000..90c80b92 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -0,0 +1,307 @@ +// 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.GeneratedStreamInvokerBenchmarkRegistry))] + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 对比 stream 完整枚举在 direct handler、GFramework 反射路径、GFramework generated invoker 路径与 MediatR 之间的开销差异。 +/// +[Config(typeof(Config))] +public class StreamInvokerBenchmarks +{ + 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 ReflectionBenchmarkStreamHandler _baselineHandler = null!; + private ReflectionBenchmarkStreamRequest _reflectionRequest = null!; + private GeneratedBenchmarkStreamRequest _generatedRequest = null!; + private MediatRBenchmarkStreamRequest _mediatrRequest = null!; + + /// + /// 配置 stream invoker benchmark 的公共输出格式。 + /// + private sealed class Config : ManualConfig + { + public Config() + { + AddJob(Job.Default); + AddColumnProvider(DefaultColumnProviders.Instance); + AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamInvoker")); + AddDiagnoser(MemoryDiagnoser.Default); + WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); + } + } + + /// + /// 构建 reflection / generated / MediatR 三组 stream dispatch 对照宿主。 + /// + [GlobalSetup] + public void Setup() + { + LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider + { + MinLevel = LogLevel.Fatal + }; + Fixture.Setup("StreamInvoker", handlerCount: 1, pipelineCount: 0); + ClearDispatcherCaches(); + + _baselineHandler = new ReflectionBenchmarkStreamHandler(); + _reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3); + _generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3); + _mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3); + + _reflectionContainer = new MicrosoftDiContainer(); + _reflectionContainer.RegisterTransient, ReflectionBenchmarkStreamHandler>(); + _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _reflectionContainer, + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Reflection")); + + _generatedContainer = new MicrosoftDiContainer(); + _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly); + _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( + _generatedContainer, + LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Generated")); + + var services = new ServiceCollection(); + services.AddSingleton, MediatRBenchmarkStreamHandler>(); + services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamInvokerBenchmarks).Assembly)); + _serviceProvider = services.BuildServiceProvider(); + _mediatr = _serviceProvider.GetRequiredService(); + } + + /// + /// 释放 MediatR 对照组使用的 DI 宿主,并清理静态 dispatcher 缓存。 + /// + [GlobalCleanup] + public void Cleanup() + { + _serviceProvider.Dispose(); + ClearDispatcherCaches(); + } + + /// + /// 直接调用最小 stream handler 并完整枚举,作为 dispatch 额外开销 baseline。 + /// + [Benchmark(Baseline = true)] + public async ValueTask Stream_Baseline() + { + await foreach (var response in _baselineHandler.Handle(_reflectionRequest, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 GFramework.CQRS 反射 stream binding 路径创建并完整枚举 stream。 + /// + [Benchmark] + public async ValueTask Stream_GFrameworkReflection() + { + await foreach (var response in _reflectionRuntime.CreateStream(BenchmarkContext.Instance, _reflectionRequest, CancellationToken.None) + .ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 generated stream invoker provider 预热后的 GFramework.CQRS runtime 创建并完整枚举 stream。 + /// + [Benchmark] + public async ValueTask Stream_GFrameworkGenerated() + { + await foreach (var response in _generatedRuntime.CreateStream(BenchmarkContext.Instance, _generatedRequest, CancellationToken.None) + .ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 MediatR 创建并完整枚举 stream,作为外部对照。 + /// + [Benchmark] + public async ValueTask Stream_MediatR() + { + await foreach (var response in _mediatr.CreateStream(_mediatrRequest, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 清空 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 stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record ReflectionBenchmarkStreamRequest(Guid Id, int ItemCount) : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest; + + /// + /// Reflection runtime stream response。 + /// + /// 响应标识。 + public sealed record ReflectionBenchmarkResponse(Guid Id); + + /// + /// Generated runtime stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record GeneratedBenchmarkStreamRequest(Guid Id, int ItemCount) : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest; + + /// + /// Generated runtime stream response。 + /// + /// 响应标识。 + public sealed record GeneratedBenchmarkResponse(Guid Id); + + /// + /// MediatR stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record MediatRBenchmarkStreamRequest(Guid Id, int ItemCount) : + MediatR.IStreamRequest; + + /// + /// MediatR stream response。 + /// + /// 响应标识。 + public sealed record MediatRBenchmarkResponse(Guid Id); + + /// + /// Reflection runtime 的最小 stream request handler。 + /// + public sealed class ReflectionBenchmarkStreamHandler : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler + { + /// + /// 处理 reflection benchmark stream request。 + /// + public IAsyncEnumerable Handle( + ReflectionBenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync( + request.Id, + request.ItemCount, + static id => new ReflectionBenchmarkResponse(id), + cancellationToken); + } + } + + /// + /// Generated runtime 的最小 stream request handler。 + /// + public sealed class GeneratedBenchmarkStreamHandler : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler + { + /// + /// 处理 generated benchmark stream request。 + /// + public IAsyncEnumerable Handle( + GeneratedBenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync( + request.Id, + request.ItemCount, + static id => new GeneratedBenchmarkResponse(id), + cancellationToken); + } + } + + /// + /// MediatR 对照组的最小 stream request handler。 + /// + public sealed class MediatRBenchmarkStreamHandler : + MediatR.IStreamRequestHandler + { + /// + /// 处理 MediatR benchmark stream request。 + /// + public IAsyncEnumerable Handle( + MediatRBenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync( + request.Id, + request.ItemCount, + static id => new MediatRBenchmarkResponse(id), + cancellationToken); + } + } + + /// + /// 为三组 stream benchmark 构造相同形状的低噪声异步枚举,避免枚举体差异干扰 invoker 对照。 + /// + private static async IAsyncEnumerable EnumerateAsync( + Guid id, + int itemCount, + Func responseFactory, + [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) + { + for (var index = 0; index < itemCount; index++) + { + cancellationToken.ThrowIfCancellationRequested(); + yield return responseFactory(id); + await Task.CompletedTask.ConfigureAwait(false); + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index c98a11fb..3ac5fccb 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -22,6 +22,8 @@ - `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/StreamInvokerBenchmarks.cs` + - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比 - `Messaging/NotificationBenchmarks.cs` - `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比 - `Messaging/StreamingBenchmarks.cs` @@ -38,5 +40,6 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro ## 后续扩展方向 - generated invoker provider 与纯反射 dispatch 对比 +- generated stream invoker provider 与纯反射建流对比 - registration / service lifetime 矩阵 - 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 cc0fff4b..4f8c6f36 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-088` +- 恢复点编号:`CQRS-REWRITE-RP-089` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #323` - 当前结论: @@ -24,6 +24,7 @@ CQRS 迁移与收敛。 - `RP-086` 已补齐 request pipeline `0 / 1 / 4` 数量矩阵,开始把 benchmark 关注点从单纯 messaging steady-state 扩展到行为编排开销 - `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` - 当前 `RP-088` 已补齐 request invoker reflection / generated-provider 对照,开始直接量化 dispatcher 预热 generated descriptor 的收益 + - 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径 - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 ## 当前活跃事实 @@ -67,6 +68,13 @@ CQRS 迁移与收敛。 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:包含新增 `RequestInvokerBenchmarks` 与 handwritten generated registry/provider 后再次复核通过 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:包含新增 `StreamInvokerBenchmarks` 与 handwritten generated stream registry/provider 后再次复核通过 +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 - `git diff --check` @@ -119,7 +127,7 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、stream generated provider 与更贴近 `Mediator` concrete runtime 的对照 +2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、notification publish strategy 或更贴近 `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 602f5154..1889617e 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 @@ -429,4 +429,38 @@ ### 当前下一步(RP-088) 1. 提交本轮 request invoker benchmark 后,继续扩展 `GFramework.Cqrs.Benchmarks`,优先评估 registration / service lifetime 或 stream generated provider + +### 阶段:stream invoker reflection / generated 对照(CQRS-REWRITE-RP-089) + +- 使用 `$gframework-batch-boot 30` 继续 `feat/cqrs-optimization` 的 CQRS 收口批次 +- 本轮基线选择: + - `origin/main c01abac0`,committer date `2026-05-06 09:40:08 +0800` + - `main a8c6c11e`,committer date `2026-05-05 13:14:24 +0800` +- 启动时 branch diff vs `origin/main` 为 `18` files / `2100` lines,低于 `30` 文件阈值,因此继续选择单模块、低风险 benchmark 切片 +- 复核 `GFramework.Cqrs.Benchmarks` 与 `ai-libs/Mediator/benchmarks` 后确认: + - `RP-088` 已把 generated descriptor 预热收益量化到 request dispatch 路径 + - stream benchmark 仍停留在 direct handler / reflection runtime / `MediatR` 三路对照,尚未量化 generated stream invoker provider 的收益 + - 虽然 `Mediator` 参考基准大量使用 service lifetime 矩阵,但当前 `GFramework.Cqrs.Benchmarks` 尚未建立对称的 scoped host 模式;直接扩 lifetime 会引入超出本批风险预算的宿主语义变化 +- 本轮因此优先选择 request 对称切片,而不是 service lifetime 扩展: + - 新增 `Messaging/StreamInvokerBenchmarks.cs` + - 新增 `Messaging/GeneratedStreamInvokerBenchmarkRegistry.cs` + - 更新 `GFramework.Cqrs.Benchmarks/README.md` +- 设计约束: + - 继续沿用 handwritten generated registry/provider 模式,避免把 benchmark 基础设施与真实 source-generator 输出耦合 + - 复用与 `RP-088` 相同的 dispatcher 缓存清理策略,确保 reflection / generated 路径对照不受静态缓存残留污染 + - 使用统一的异步枚举体工厂,让三组 stream handler 共享同一枚举成本基线,把变量收敛到 invoker/provider 接线路径 + +### 当前下一步(RP-089) + +1. 完成本轮 benchmark 项目 Release build、license header 检查与 diff 校验后,更新 active tracking 的权威验证列表 +2. 若 branch diff 仍明显低于 `30` 文件阈值,可继续评估 notification publish strategy 或更贴近 `Mediator` concrete runtime 的单批对照 + +### 验证(RP-089) + +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` + - 结果:通过 +- `git diff --check` + - 结果:通过 2. 若要继续贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否把 `Mediator` concrete runtime 本身接入 benchmark 项目,而不是长期只保留 `MediatR` From 2ac02c1a6fabf583335338edd85a713d76f91486 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 11:07:33 +0800 Subject: [PATCH 07/10] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20benchma?= =?UTF-8?q?rk=20review=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 RequestStartupBenchmarks 的 baseline 分组、初始化阶段对齐与 MediatR 重复注册问题 - 新增共享 dispatcher cache helper,并统一 benchmark 宿主的 MediatR logging/license 过滤配置 - 更新 cqrs-rewrite 跟踪与 trace,记录 PR #326 锚点、验证去重和 startup benchmark 的残留运行风险 --- .../GFramework.Cqrs.Benchmarks.csproj | 3 +- .../BenchmarkDispatcherCacheHelper.cs | 49 ++++++++++++ .../Messaging/NotificationBenchmarks.cs | 5 ++ .../Messaging/RequestBenchmarks.cs | 5 ++ .../Messaging/RequestInvokerBenchmarks.cs | 40 ++-------- .../Messaging/RequestPipelineBenchmarks.cs | 5 ++ .../Messaging/RequestStartupBenchmarks.cs | 75 +++++++++---------- .../Messaging/StreamInvokerBenchmarks.cs | 40 ++-------- .../Messaging/StreamingBenchmarks.cs | 5 ++ .../todos/cqrs-rewrite-migration-tracking.md | 40 ++++------ .../traces/cqrs-rewrite-migration-trace.md | 4 +- 11 files changed, 136 insertions(+), 135 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/BenchmarkDispatcherCacheHelper.cs diff --git a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj index 6dd185ba..c7d764fa 100644 --- a/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj +++ b/GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj @@ -14,9 +14,10 @@ - + + diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkDispatcherCacheHelper.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkDispatcherCacheHelper.cs new file mode 100644 index 00000000..64b5aec8 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkDispatcherCacheHelper.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Reflection; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 提供 benchmark 共享的 dispatcher 静态缓存清理入口。 +/// +/// +/// `GFramework.Cqrs` runtime 会把反射绑定与 generated invoker 元数据缓存在静态字段中。 +/// benchmark 需要在同一进程内重复比较 cold-start、reflection 与 generated 路径时, +/// 显式清空这些缓存,避免前一组 benchmark 污染后续结果。 +/// +internal static class BenchmarkDispatcherCacheHelper +{ + /// + /// 清空 dispatcher 上与 benchmark 对照相关的全部静态缓存。 + /// + public static void ClearDispatcherCaches() + { + ClearDispatcherCache("NotificationDispatchBindings"); + ClearDispatcherCache("RequestDispatchBindings"); + ClearDispatcherCache("StreamDispatchBindings"); + ClearDispatcherCache("GeneratedRequestInvokers"); + ClearDispatcherCache("GeneratedStreamInvokers"); + } + + /// + /// 通过反射定位并清空 dispatcher 的指定缓存字段。 + /// + /// 要清理的静态缓存字段名。 + /// 指定缓存字段不存在、返回空值或未暴露清理方法。 + internal 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); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 7cc98c74..27dbf173 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -65,6 +65,11 @@ public class NotificationBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks))); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, BenchmarkNotificationHandler>(); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly)); _serviceProvider = services.BuildServiceProvider(); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index e925f794..72650e9d 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -68,6 +68,11 @@ public class RequestBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks))); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, BenchmarkRequestHandler>(); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestBenchmarks).Assembly)); _serviceProvider = services.BuildServiceProvider(); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index d3eb2759..d8aa8f60 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -9,7 +9,6 @@ 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; @@ -67,7 +66,7 @@ public class RequestInvokerBenchmarks MinLevel = LogLevel.Fatal }; Fixture.Setup("RequestInvoker", handlerCount: 1, pipelineCount: 0); - ClearDispatcherCaches(); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); _baselineHandler = new ReflectionBenchmarkRequestHandler(); _reflectionRequest = new ReflectionBenchmarkRequest(Guid.NewGuid()); @@ -87,6 +86,11 @@ public class RequestInvokerBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Generated")); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, MediatRBenchmarkRequestHandler>(); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestInvokerBenchmarks).Assembly)); _serviceProvider = services.BuildServiceProvider(); @@ -100,7 +104,7 @@ public class RequestInvokerBenchmarks public void Cleanup() { _serviceProvider.Dispose(); - ClearDispatcherCaches(); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } /// @@ -139,36 +143,6 @@ public class RequestInvokerBenchmarks 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。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index 1dc1b47a..8ff41523 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -75,6 +75,11 @@ public class RequestPipelineBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestPipelineBenchmarks))); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, BenchmarkRequestHandler>(); RegisterMediatRPipelineBehaviors(services, PipelineCount); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestPipelineBenchmarks).Assembly)); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs index b6f86181..8ca1caf7 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -8,7 +8,6 @@ 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; @@ -17,6 +16,7 @@ using GFramework.Core.Logging; using GFramework.Cqrs.Abstractions.Cqrs; using MediatR; using Microsoft.Extensions.DependencyInjection; +using ILogger = GFramework.Core.Abstractions.Logging.ILogger; namespace GFramework.Cqrs.Benchmarks.Messaging; @@ -31,6 +31,7 @@ public class RequestStartupBenchmarks private ServiceProvider _serviceProvider = null!; private IMediator _mediatr = null!; + private ICqrsRuntime _runtime = null!; /// /// 配置 request startup benchmark 的公共输出格式。 @@ -41,8 +42,9 @@ public class RequestStartupBenchmarks { AddJob(Job.Default); AddColumnProvider(DefaultColumnProviders.Instance); - AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestStartup")); + AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestStartup"), TargetMethodColumn.Method, CategoriesColumn.Default); AddDiagnoser(MemoryDiagnoser.Default); + AddLogicalGroupRules(BenchmarkLogicalGroupRule.ByCategory); WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared)); } } @@ -57,10 +59,11 @@ public class RequestStartupBenchmarks _serviceProvider = CreateMediatRServiceProvider(); _mediatr = _serviceProvider.GetRequiredService(); + _runtime = CreateGFrameworkRuntime(); } /// - /// 释放 MediatR 对照组使用的 DI 宿主。 + /// 释放 startup benchmark 复用的宿主对象。 /// [GlobalCleanup] public void Cleanup() @@ -69,23 +72,23 @@ public class RequestStartupBenchmarks } /// - /// 解析 MediatR mediator,作为 startup 句柄解析成本的 baseline。 + /// 返回已构建宿主中的 MediatR mediator,作为 initialization 组的句柄解析 baseline。 /// [Benchmark(Baseline = true)] [BenchmarkCategory("Initialization")] public IMediator Initialization_MediatR() { - return _serviceProvider.GetRequiredService(); + return _mediatr; } /// - /// 创建 GFramework.CQRS runtime,作为同层级 startup 句柄创建成本的对照。 + /// 返回已构建宿主中的 GFramework.CQRS runtime,确保与 MediatR baseline 处于相同初始化阶段。 /// [Benchmark] [BenchmarkCategory("Initialization")] public ICqrsRuntime Initialization_GFrameworkCqrs() { - return CreateGFrameworkRuntime(); + return _runtime; } /// @@ -101,20 +104,36 @@ public class RequestStartupBenchmarks } /// - /// 在清空 dispatcher 静态缓存后,于新宿主上首次发送 request,量化 GFramework.CQRS 的 first-hit 成本。 + /// 在新 runtime 上首次发送 request,量化 GFramework.CQRS 的 first-hit 成本。 /// [Benchmark] [BenchmarkCategory("ColdStart")] public ValueTask ColdStart_GFrameworkCqrs() { - ClearDispatcherCaches(); - var runtime = CreateGFrameworkRuntime(); + var runtime = CreateColdStartRuntime(); return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None); } + /// + /// 为 cold-start benchmark 构建全新的 runtime,并在构建前显式清空 dispatcher 静态缓存。 + /// + /// + /// 这里把缓存清理与 runtime 构建绑定在同一阶段,避免把额外的反射缓存清理成本混入 benchmark 方法主体, + /// 只保留“新宿主 + 首次分发”的对照。 + /// + private static ICqrsRuntime CreateColdStartRuntime() + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + return CreateGFrameworkRuntime(); + } + /// /// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。 /// + /// + /// 该 benchmark 故意保持与 MediatR 对照组同样的“单 handler 最小宿主”模型, + /// 因此这里继续使用单点手工注册,而不引入依赖完整 CQRS 注册协调器的程序集扫描路径。 + /// private static ICqrsRuntime CreateGFrameworkRuntime() { var container = new MicrosoftDiContainer(); @@ -128,7 +147,11 @@ public class RequestStartupBenchmarks private static ServiceProvider CreateMediatRServiceProvider() { var services = new ServiceCollection(); - services.AddSingleton, BenchmarkRequestHandler>(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestStartupBenchmarks).Assembly)); return services.BuildServiceProvider(); } @@ -145,36 +168,6 @@ public class RequestStartupBenchmarks 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。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index 90c80b92..09c4074b 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -9,7 +9,6 @@ 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; @@ -67,7 +66,7 @@ public class StreamInvokerBenchmarks MinLevel = LogLevel.Fatal }; Fixture.Setup("StreamInvoker", handlerCount: 1, pipelineCount: 0); - ClearDispatcherCaches(); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); _baselineHandler = new ReflectionBenchmarkStreamHandler(); _reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3); @@ -87,6 +86,11 @@ public class StreamInvokerBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Generated")); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, MediatRBenchmarkStreamHandler>(); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamInvokerBenchmarks).Assembly)); _serviceProvider = services.BuildServiceProvider(); @@ -100,7 +104,7 @@ public class StreamInvokerBenchmarks public void Cleanup() { _serviceProvider.Dispose(); - ClearDispatcherCaches(); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } /// @@ -153,36 +157,6 @@ public class StreamInvokerBenchmarks } } - /// - /// 清空 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 stream request。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs index 658d7486..4b9bb42e 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -69,6 +69,11 @@ public class StreamingBenchmarks LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamingBenchmarks))); var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); services.AddSingleton, BenchmarkStreamHandler>(); services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamingBenchmarks).Assembly)); _serviceProvider = services.BuildServiceProvider(); 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 4f8c6f36..6446d895 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 @@ -9,7 +9,7 @@ CQRS 迁移与收敛。 - 恢复点编号:`CQRS-REWRITE-RP-089` - 当前阶段:`Phase 8` -- 当前 PR 锚点:`PR #323` +- 当前 PR 锚点:`PR #326` - 当前结论: - `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序” - `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归 @@ -25,18 +25,20 @@ CQRS 迁移与收敛。 - `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` - 当前 `RP-088` 已补齐 request invoker reflection / generated-provider 对照,开始直接量化 dispatcher 预热 generated descriptor 的收益 - 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径 - - `ai-plan` active 入口现以 `PR #323` 和 `RP-082` 为唯一权威恢复锚点;`PR #307`、其他更早 PR 与阶段细节均以下方归档或说明为准 + - `ai-plan` active 入口现以 `PR #326` 和 `RP-089` 为唯一权威恢复锚点;`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 -- 当前分支对应 `PR #323`,状态为 `OPEN` +- 当前分支对应 `PR #326`,状态为 `OPEN` - latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛 +- `RequestStartupBenchmarks` 已修复 baseline 分组冲突、MediatR 13 logging/license 构造失败与重复注册问题,但 `ColdStart_GFrameworkCqrs` 仍存在 `No CQRS request handler registered` 的运行级残留 - 远端 `CTRF` 最新汇总为 `2274/2274` passed - `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 ## 当前风险 - 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证 +- `RequestStartupBenchmarks` 的 GFramework cold-start 路径在清空 dispatcher 缓存后仍未恢复 request handler 绑定,当前无法产出完整 startup 对照数据 - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 @@ -44,7 +46,7 @@ CQRS 迁移与收敛。 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output ` - 结果:通过 - - 备注:确认当前分支对应 `PR #323`,本轮剩余 open AI feedback 主要集中在 `ai-plan` PR 锚点收敛 + - 备注:确认当前分支对应 `PR #326`,本轮剩余 open AI feedback 主要集中在 benchmark 对照语义与 `ai-plan` 结构收敛 - `git diff --check` - 结果:通过 - `python3 scripts/license-header.py --check` @@ -56,27 +58,13 @@ CQRS 迁移与收敛。 - 结果:通过,`2/2` passed - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` -- `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` 后再次复核通过 -- `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 后再次复核通过 -- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - - 结果:通过,`0 warning / 0 error` - - 备注:包含新增 `StreamInvokerBenchmarks` 与 handwritten generated stream registry/provider 后再次复核通过 -- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - - 结果:通过 -- `git diff --check` - - 结果:通过 + - 备注:先后覆盖 `StreamingBenchmarks`、`RequestPipelineBenchmarks`、`RequestStartupBenchmarks`、`RequestInvokerBenchmarks` 与 `StreamInvokerBenchmarks` 的引入后复核 +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:部分通过 + - 备注:`Initialization_MediatR` 与 `ColdStart_MediatR` 已可实际运行;`ColdStart_GFrameworkCqrs` 仍因 `No CQRS request handler registered` 无法产出完整对照 - `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - 结果:通过 + - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行,避免脚本内部 plain `git ls-files` 误判仓库上下文 - `git diff --check` - 结果:通过 - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` @@ -126,9 +114,9 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 继续处理 `PR #323` 的剩余 review 收尾,优先保持 `ai-plan` active 入口与 trace 的单一锚点一致 -2. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、notification publish strategy 或更贴近 `Mediator` concrete runtime 的对照 -3. 在进入下一批 runtime / generator 收敛前,保持最小 Release build、targeted test 或 benchmark project build 作为权威验证 +1. 继续处理 `PR #326` 的剩余 review 收尾,优先保持 benchmark 对照语义与 `ai-plan` active 入口一致 +2. 优先定位 `RequestStartupBenchmarks.ColdStart_GFrameworkCqrs` 在清空 dispatcher 缓存后的 request handler 绑定缺口,再决定是调整最小宿主注册方式还是补充专用 benchmark fixture +3. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、notification publish strategy 或更贴近 `Mediator` concrete runtime 的对照 ## 活跃文档 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 1889617e..6f9a8104 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 @@ -454,6 +454,7 @@ 1. 完成本轮 benchmark 项目 Release build、license header 检查与 diff 校验后,更新 active tracking 的权威验证列表 2. 若 branch diff 仍明显低于 `30` 文件阈值,可继续评估 notification publish strategy 或更贴近 `Mediator` concrete runtime 的单批对照 +3. 若要继续贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否把 `Mediator` concrete runtime 本身接入 benchmark 项目,而不是长期只保留 `MediatR` ### 验证(RP-089) @@ -463,4 +464,5 @@ - 结果:通过 - `git diff --check` - 结果:通过 -2. 若要继续贴近 `Mediator` 的 comparison benchmark 设计哲学,评估是否把 `Mediator` concrete runtime 本身接入 benchmark 项目,而不是长期只保留 `MediatR` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:部分通过;`MediatR` startup benchmark 已恢复真实测量,`ColdStart_GFrameworkCqrs` 仍因 `No CQRS request handler registered` 失败 From f71791ae98d83f76e467ce1b2a82bc02ebce1345 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 11:48:15 +0800 Subject: [PATCH 08/10] =?UTF-8?q?ci(cqrs):=20=E6=96=B0=E5=A2=9E=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=20benchmark=20=E5=B7=A5=E4=BD=9C=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增仅支持 workflow_dispatch 的 Benchmark workflow,默认只验证 benchmark 项目 Release build - 补充可选 benchmark_filter 输入与 BenchmarkDotNet 工件上传,支持按场景手动执行基准测试 - 更新 cqrs-rewrite 跟踪与 trace,记录手动 benchmark workflow 的用途与当前 startup benchmark 残留风险 --- .github/workflows/benchmark.yml | 69 +++++++++++++++++++ .../todos/cqrs-rewrite-migration-tracking.md | 10 ++- .../traces/cqrs-rewrite-migration-trace.md | 6 ++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/benchmark.yml diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml new file mode 100644 index 00000000..76b32324 --- /dev/null +++ b/.github/workflows/benchmark.yml @@ -0,0 +1,69 @@ +# Copyright (c) 2025-2026 GeWuYou +# SPDX-License-Identifier: Apache-2.0 + +name: Benchmark + +on: + workflow_dispatch: + inputs: + benchmark_filter: + description: '可选的 BenchmarkDotNet 过滤器;留空时仅执行 benchmark 项目 Release build' + required: false + default: '' + type: string + +permissions: + contents: read + +jobs: + benchmark: + name: Benchmark Build Or Run + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup .NET 10 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: | + ~/.nuget/packages + ~/.local/share/NuGet + key: ${{ runner.os }}-nuget-benchmarks-${{ hashFiles('GFramework.Cqrs.Benchmarks/*.csproj', 'GFramework.Cqrs/*.csproj', 'GFramework.Cqrs.Abstractions/*.csproj', 'GFramework.Core/*.csproj', 'GFramework.Core.Abstractions/*.csproj', '**/nuget.config') }} + + - name: Restore benchmark project + run: dotnet restore GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj + + - name: Build benchmark project + run: dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-restore + + - name: Report build-only mode + if: ${{ inputs.benchmark_filter == '' }} + run: | + echo "No benchmark filter provided." + echo "Workflow completed after validating the benchmark project build." + + - name: Run filtered benchmarks + if: ${{ inputs.benchmark_filter != '' }} + run: | + set -euo pipefail + dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- \ + --filter "${{ inputs.benchmark_filter }}" + + - name: Upload BenchmarkDotNet artifacts + if: ${{ always() && inputs.benchmark_filter != '' }} + uses: actions/upload-artifact@v7 + with: + name: benchmark-artifacts + path: | + BenchmarkDotNet.Artifacts/** + GFramework.Cqrs.Benchmarks/bin/Release/net10.0/BenchmarkDotNet.Artifacts/** + if-no-files-found: ignore 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 6446d895..fd9ab98d 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 @@ -32,6 +32,7 @@ CQRS 迁移与收敛。 - 当前分支对应 `PR #326`,状态为 `OPEN` - latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛 - `RequestStartupBenchmarks` 已修复 baseline 分组冲突、MediatR 13 logging/license 构造失败与重复注册问题,但 `ColdStart_GFrameworkCqrs` 仍存在 `No CQRS request handler registered` 的运行级残留 +- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行 - 远端 `CTRF` 最新汇总为 `2274/2274` passed - `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 @@ -44,14 +45,17 @@ CQRS 迁移与收敛。 ## 最近权威验证 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` + - 备注:用于验证新增手动 benchmark workflow 依赖的 benchmark 项目入口仍可在 Release 下编译 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output ` - 结果:通过 - 备注:确认当前分支对应 `PR #326`,本轮剩余 open AI feedback 主要集中在 benchmark 对照语义与 `ai-plan` 结构收敛 -- `git diff --check` - - 结果:通过 - `python3 scripts/license-header.py --check` - 结果:通过 - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 +- `git diff --check` + - 结果:通过 - `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - 结果:通过,`0 warning / 0 error` - `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` @@ -116,7 +120,7 @@ CQRS 迁移与收敛。 1. 继续处理 `PR #326` 的剩余 review 收尾,优先保持 benchmark 对照语义与 `ai-plan` active 入口一致 2. 优先定位 `RequestStartupBenchmarks.ColdStart_GFrameworkCqrs` 在清空 dispatcher 缓存后的 request handler 绑定缺口,再决定是调整最小宿主注册方式还是补充专用 benchmark fixture -3. 若继续推进“吸收 Mediator 设计哲学”的切片,优先扩展 benchmark 场景矩阵到 registration / service lifetime、notification publish strategy 或更贴近 `Mediator` concrete runtime 的对照 +3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行命中当前已知 startup 残留 ## 活跃文档 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 6f9a8104..47980e8a 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 @@ -466,3 +466,9 @@ - 结果:通过 - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` - 结果:部分通过;`MediatR` startup benchmark 已恢复真实测量,`ColdStart_GFrameworkCqrs` 仍因 `No CQRS request handler registered` 失败 + +### 阶段:手动 benchmark workflow(CQRS-REWRITE-RP-089) + +- 新增 `.github/workflows/benchmark.yml`,提供仅 `workflow_dispatch` 触发的 benchmark 入口 +- workflow 默认只执行 `GFramework.Cqrs.Benchmarks` 的 Release build,避免在当前已知 `RequestStartupBenchmarks` 残留未清时默认运行失败 +- 只有在手动输入 `benchmark_filter` 时才执行 BenchmarkDotNet,并上传 `BenchmarkDotNet.Artifacts` 供后续比较 From 2cb6216d0553859637e877b9ae012ef960fd888e Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 12:09:20 +0800 Subject: [PATCH 09/10] =?UTF-8?q?fix(cqrs):=20=E4=BF=AE=E5=A4=8D=20benchma?= =?UTF-8?q?rk=20=E5=AF=B9=E7=85=A7=E5=AE=BF=E4=B8=BB=E4=B8=8E=E5=86=B7?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=9F=BA=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BenchmarkHostFactory 统一 benchmark 最小宿主构建,并限制 MediatR 扫描到当前场景所需类型 - 修复 GFramework benchmark 容器未冻结导致的首次 handler 解析缺口,恢复 RequestStartupBenchmarks 冷启动结果 - 优化 request、pipeline、notification、stream 与 invoker benchmark 的生命周期对齐,减少无关程序集扫描噪音 - 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #326 benchmark review 收敛、根因和验证结果 --- .../Messaging/BenchmarkHostFactory.cs | 93 +++++++++++++++++++ .../Messaging/NotificationBenchmarks.cs | 21 ++--- .../Messaging/RequestBenchmarks.cs | 22 ++--- .../Messaging/RequestInvokerBenchmarks.cs | 26 +++--- .../Messaging/RequestPipelineBenchmarks.cs | 33 ++++--- .../Messaging/RequestStartupBenchmarks.cs | 52 ++++++----- .../Messaging/StreamInvokerBenchmarks.cs | 26 +++--- .../Messaging/StreamingBenchmarks.cs | 22 ++--- .../todos/cqrs-rewrite-migration-tracking.md | 22 +++-- .../traces/cqrs-rewrite-migration-trace.md | 44 +++++++++ 10 files changed, 254 insertions(+), 107 deletions(-) create mode 100644 GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs new file mode 100644 index 00000000..b0248eb6 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs @@ -0,0 +1,93 @@ +// Copyright (c) 2025-2026 GeWuYou +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Linq; +using GFramework.Core.Ioc; +using GFramework.Cqrs.Abstractions.Cqrs; +using MediatR; +using Microsoft.Extensions.DependencyInjection; + +namespace GFramework.Cqrs.Benchmarks.Messaging; + +/// +/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。 +/// +/// +/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。 +/// 因此这里统一封装两类宿主的最小注册形状,确保: +/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler; +/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。 +/// +internal static class BenchmarkHostFactory +{ + /// + /// 创建一个已经冻结的 GFramework benchmark 容器。 + /// + /// 向容器写入 benchmark 所需 handler / pipeline 的注册动作。 + /// 已冻结、可立即用于 runtime 分发的容器。 + internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action configure) + { + ArgumentNullException.ThrowIfNull(configure); + + var container = new MicrosoftDiContainer(); + configure(container); + container.Freeze(); + return container; + } + + /// + /// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。 + /// + /// 补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。 + /// 用于限定扫描程序集的标记类型。 + /// + /// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描; + /// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。 + /// + /// 当前 benchmark 希望 MediatR 使用的默认注册生命周期。 + /// 只承载当前 benchmark 场景所需服务的 DI 宿主。 + internal static ServiceProvider CreateMediatRServiceProvider( + Action? configure, + Type handlerAssemblyMarkerType, + Func handlerTypeFilter, + ServiceLifetime lifetime = ServiceLifetime.Transient) + { + ArgumentNullException.ThrowIfNull(handlerAssemblyMarkerType); + ArgumentNullException.ThrowIfNull(handlerTypeFilter); + + var services = new ServiceCollection(); + services.AddLogging(static builder => + Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( + builder, + "LuckyPennySoftware.MediatR.License", + Microsoft.Extensions.Logging.LogLevel.None)); + + configure?.Invoke(services); + + services.AddMediatR(options => + { + options.Lifetime = lifetime; + options.TypeEvaluator = handlerTypeFilter; + options.RegisterServicesFromAssembly(handlerAssemblyMarkerType.Assembly); + }); + + return services.BuildServiceProvider(); + } + + /// + /// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。 + /// + /// 待判断类型。 + /// 目标开放泛型合同,例如 。 + /// 命中任一实现接口时返回 ;否则返回 + internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract) + { + ArgumentNullException.ThrowIfNull(candidateType); + ArgumentNullException.ThrowIfNull(openGenericContract); + + return candidateType.GetInterfaces().Any(interfaceType => + interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == openGenericContract); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs index 27dbf173..42ee16b9 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/NotificationBenchmarks.cs @@ -58,21 +58,20 @@ public class NotificationBenchmarks }; Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0); - _container = new MicrosoftDiContainer(); - _container.RegisterTransient, BenchmarkNotificationHandler>(); + _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterSingleton>( + new BenchmarkNotificationHandler()); + }); _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(NotificationBenchmarks))); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, BenchmarkNotificationHandler>(); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + services => services.AddSingleton, BenchmarkNotificationHandler>(), + typeof(NotificationBenchmarks), + static candidateType => candidateType == typeof(BenchmarkNotificationHandler), + ServiceLifetime.Singleton); _publisher = _serviceProvider.GetRequiredService(); _notification = new BenchmarkNotification(Guid.NewGuid()); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 72650e9d..731f2a23 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -59,23 +59,21 @@ public class RequestBenchmarks }; Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0); - _container = new MicrosoftDiContainer(); _baselineHandler = new BenchmarkRequestHandler(); - - _container.RegisterTransient, BenchmarkRequestHandler>(); + _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterSingleton>( + _baselineHandler); + }); _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks))); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, BenchmarkRequestHandler>(); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(RequestBenchmarks), + static candidateType => candidateType == typeof(BenchmarkRequestHandler), + ServiceLifetime.Singleton); _mediatr = _serviceProvider.GetRequiredService(); _request = new BenchmarkRequest(Guid.NewGuid()); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index d8aa8f60..d026ff25 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -73,27 +73,27 @@ public class RequestInvokerBenchmarks _generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid()); _mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid()); - _reflectionContainer = new MicrosoftDiContainer(); - _reflectionContainer.RegisterTransient, ReflectionBenchmarkRequestHandler>(); + _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container => + { + container.RegisterTransient, ReflectionBenchmarkRequestHandler>(); + }); _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _reflectionContainer, LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Reflection")); - _generatedContainer = new MicrosoftDiContainer(); - _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly); + _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly); + }); _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _generatedContainer, LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Generated")); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, MediatRBenchmarkRequestHandler>(); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestInvokerBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(RequestInvokerBenchmarks), + static candidateType => candidateType == typeof(MediatRBenchmarkRequestHandler), + ServiceLifetime.Singleton); _mediatr = _serviceProvider.GetRequiredService(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index 8ff41523..058019cd 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -65,25 +65,30 @@ public class RequestPipelineBenchmarks }; Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount); - _container = new MicrosoftDiContainer(); _baselineHandler = new BenchmarkRequestHandler(); - - _container.RegisterTransient, BenchmarkRequestHandler>(); - RegisterGFrameworkPipelineBehaviors(_container, PipelineCount); + _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterSingleton>( + _baselineHandler); + RegisterGFrameworkPipelineBehaviors(container, PipelineCount); + }); _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestPipelineBenchmarks))); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, BenchmarkRequestHandler>(); - RegisterMediatRPipelineBehaviors(services, PipelineCount); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestPipelineBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + services => + { + RegisterMediatRPipelineBehaviors(services, PipelineCount); + }, + typeof(RequestPipelineBenchmarks), + static candidateType => + candidateType == typeof(BenchmarkRequestHandler) || + candidateType == typeof(BenchmarkPipelineBehavior1) || + candidateType == typeof(BenchmarkPipelineBehavior2) || + candidateType == typeof(BenchmarkPipelineBehavior3) || + candidateType == typeof(BenchmarkPipelineBehavior4), + ServiceLifetime.Singleton); _mediatr = _serviceProvider.GetRequiredService(); _request = new BenchmarkRequest(Guid.NewGuid()); diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs index 8ca1caf7..494e6296 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestStartupBenchmarks.cs @@ -40,7 +40,10 @@ public class RequestStartupBenchmarks { public Config() { - AddJob(Job.Default); + AddJob(Job.Default + .WithId("ColdStart") + .WithInvocationCount(1) + .WithUnrollFactor(1)); AddColumnProvider(DefaultColumnProviders.Instance); AddColumn(new CustomColumn("Scenario", static (_, _) => "RequestStartup"), TargetMethodColumn.Method, CategoriesColumn.Default); AddDiagnoser(MemoryDiagnoser.Default); @@ -62,6 +65,19 @@ public class RequestStartupBenchmarks _runtime = CreateGFrameworkRuntime(); } + /// + /// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保两组 benchmark 都重新命中首次绑定路径。 + /// + /// + /// 使用 `IterationSetup` 而不是把缓存清理写在 benchmark 方法主体中, + /// 可以把“清理静态缓存”留在测量边界之外,只保留宿主构建与首次发送本身。 + /// + [IterationSetup] + public void ResetColdStartCaches() + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } + /// /// 释放 startup benchmark 复用的宿主对象。 /// @@ -110,23 +126,10 @@ public class RequestStartupBenchmarks [BenchmarkCategory("ColdStart")] public ValueTask ColdStart_GFrameworkCqrs() { - var runtime = CreateColdStartRuntime(); + var runtime = CreateGFrameworkRuntime(); return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None); } - /// - /// 为 cold-start benchmark 构建全新的 runtime,并在构建前显式清空 dispatcher 静态缓存。 - /// - /// - /// 这里把缓存清理与 runtime 构建绑定在同一阶段,避免把额外的反射缓存清理成本混入 benchmark 方法主体, - /// 只保留“新宿主 + 首次分发”的对照。 - /// - private static ICqrsRuntime CreateColdStartRuntime() - { - BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); - return CreateGFrameworkRuntime(); - } - /// /// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。 /// @@ -136,8 +139,10 @@ public class RequestStartupBenchmarks /// private static ICqrsRuntime CreateGFrameworkRuntime() { - var container = new MicrosoftDiContainer(); - container.RegisterTransient, BenchmarkRequestHandler>(); + var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer => + { + currentContainer.RegisterTransient, BenchmarkRequestHandler>(); + }); return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger); } @@ -146,14 +151,11 @@ public class RequestStartupBenchmarks /// private static ServiceProvider CreateMediatRServiceProvider() { - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(RequestStartupBenchmarks).Assembly)); - return services.BuildServiceProvider(); + return BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(RequestStartupBenchmarks), + static candidateType => candidateType == typeof(BenchmarkRequestHandler), + ServiceLifetime.Transient); } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index 09c4074b..bfec83f0 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -73,27 +73,27 @@ public class StreamInvokerBenchmarks _generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3); _mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3); - _reflectionContainer = new MicrosoftDiContainer(); - _reflectionContainer.RegisterTransient, ReflectionBenchmarkStreamHandler>(); + _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container => + { + container.RegisterTransient, ReflectionBenchmarkStreamHandler>(); + }); _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _reflectionContainer, LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Reflection")); - _generatedContainer = new MicrosoftDiContainer(); - _generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly); + _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly); + }); _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _generatedContainer, LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamInvokerBenchmarks) + ".Generated")); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, MediatRBenchmarkStreamHandler>(); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamInvokerBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(StreamInvokerBenchmarks), + static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler), + ServiceLifetime.Singleton); _mediatr = _serviceProvider.GetRequiredService(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs index 4b9bb42e..1c22d5f3 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -60,23 +60,21 @@ public class StreamingBenchmarks }; Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0); - _container = new MicrosoftDiContainer(); _baselineHandler = new BenchmarkStreamHandler(); - - _container.RegisterTransient, BenchmarkStreamHandler>(); + _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => + { + container.RegisterSingleton>( + _baselineHandler); + }); _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _container, LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamingBenchmarks))); - var services = new ServiceCollection(); - services.AddLogging(static builder => - Microsoft.Extensions.Logging.FilterLoggingBuilderExtensions.AddFilter( - builder, - "LuckyPennySoftware.MediatR.License", - Microsoft.Extensions.Logging.LogLevel.None)); - services.AddSingleton, BenchmarkStreamHandler>(); - services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(StreamingBenchmarks).Assembly)); - _serviceProvider = services.BuildServiceProvider(); + _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( + configure: null, + typeof(StreamingBenchmarks), + static candidateType => candidateType == typeof(BenchmarkStreamHandler), + ServiceLifetime.Singleton); _mediatr = _serviceProvider.GetRequiredService(); _request = new BenchmarkStreamRequest(Guid.NewGuid(), 3); 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 fd9ab98d..ddef2523 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-089` +- 恢复点编号:`CQRS-REWRITE-RP-090` - 当前阶段:`Phase 8` - 当前 PR 锚点:`PR #326` - 当前结论: @@ -25,13 +25,15 @@ CQRS 迁移与收敛。 - `RP-087` 已补齐 request startup benchmark,把 initialization 与 cold-start 维度正式纳入 `GFramework.Cqrs.Benchmarks` - 当前 `RP-088` 已补齐 request invoker reflection / generated-provider 对照,开始直接量化 dispatcher 预热 generated descriptor 的收益 - 当前 `RP-089` 已补齐 stream invoker reflection / generated-provider 对照,使 generated descriptor 预热收益从 request 扩展到 stream 路径 - - `ai-plan` active 入口现以 `PR #326` 和 `RP-089` 为唯一权威恢复锚点;`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 + - 当前 `RP-090` 已收敛 `PR #326` benchmark review:统一 benchmark 最小宿主构建、冻结 GFramework 容器、限制 MediatR 扫描范围,并恢复 request startup cold-start 对照 + - `ai-plan` active 入口现以 `PR #326` 和 `RP-090` 为唯一权威恢复锚点;`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准 ## 当前活跃事实 - 当前分支对应 `PR #326`,状态为 `OPEN` -- latest-head review 仍以 `ai-plan` 恢复文档收敛为主要待闭环项;代码与测试侧的本地有效问题已收敛 -- `RequestStartupBenchmarks` 已修复 baseline 分组冲突、MediatR 13 logging/license 构造失败与重复注册问题,但 `ColdStart_GFrameworkCqrs` 仍存在 `No CQRS request handler registered` 的运行级残留 +- latest-head review 已从 benchmark 运行级缺陷收敛到剩余文档入口与是否继续接受 benchmark 语义细化的判断 +- benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型 +- `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered` - 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行 - 远端 `CTRF` 最新汇总为 `2274/2274` passed - `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 @@ -39,12 +41,18 @@ CQRS 迁移与收敛。 ## 当前风险 - 顶层 `GFramework.sln` / `GFramework.csproj` 在 WSL 下仍可能受 Windows NuGet fallback 配置影响,完整 solution 级验证成本高于模块级验证 -- `RequestStartupBenchmarks` 的 GFramework cold-start 路径在清空 dispatcher 缓存后仍未恢复 request handler 绑定,当前无法产出完整 startup 对照数据 +- `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness - 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成” - 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景 ## 最近权威验证 +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`ColdStart_GFrameworkCqrs` 已恢复出数,最新本地输出约 `220-292 us`,MediatR 对照约 `575-616 us`;当前仅剩 BenchmarkDotNet 对单次 cold-start 场景的 `MinIterationTime` 提示 +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:确认冻结后的 GFramework 最小宿主与受限扫描的 MediatR 最小宿主均可完成 steady-state request 对照 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - 备注:用于验证新增手动 benchmark workflow 依赖的 benchmark 项目入口仍可在 Release 下编译 @@ -119,8 +127,8 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 1. 继续处理 `PR #326` 的剩余 review 收尾,优先保持 benchmark 对照语义与 `ai-plan` active 入口一致 -2. 优先定位 `RequestStartupBenchmarks.ColdStart_GFrameworkCqrs` 在清空 dispatcher 缓存后的 request handler 绑定缺口,再决定是调整最小宿主注册方式还是补充专用 benchmark fixture -3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行命中当前已知 startup 残留 +2. 决定是否继续细化 `RequestStartupBenchmarks` 的 cold-start harness,降低 `InvocationCount=1` 带来的 `MinIterationTime` 提示噪音 +3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行整个 benchmark 矩阵 ## 活跃文档 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 47980e8a..f3274372 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 @@ -1,5 +1,49 @@ # CQRS 重写迁移追踪 +## 2026-05-06 + +### 阶段:benchmark 对照宿主收敛与 startup cold-start 恢复(CQRS-REWRITE-RP-090) + +- 使用 `$gframework-pr-review` 拉取 `PR #326` latest-head review 后,主线程确认仍有效的 benchmark 反馈集中在三类问题: + - `RequestBenchmarks` 的 GFramework / MediatR handler 生命周期不对齐 + - `RequestStartupBenchmarks` 把容器构建、程序集扫描范围和缓存清理阶段混在一起,导致 cold-start 对照不公平 + - benchmark 工程里的 `MicrosoftDiContainer` 多处以 `ImplementationType` 方式注册 handler,但未在 runtime 分发前 `Freeze()`,首次真实解析路径存在隐藏失败风险 +- 本轮本地复核的关键根因: + - `MicrosoftDiContainer.Get(Type)` 在未冻结时只读取 `ImplementationInstance`,不会实例化 `ImplementationType` + - `ColdStart_GFrameworkCqrs` 清空 dispatcher 静态缓存后,首次发送必须走真实 handler 解析,因此会稳定触发 `No CQRS request handler registered` + - 多个 benchmark 同时采用“手工 MediatR 注册 + `RegisterServicesFromAssembly(...)` 全程序集扫描”,容易把无关 handler / behavior 一并纳入对照,且存在重复注册漂移 +- 本轮决策: + - 新增 `Messaging/BenchmarkHostFactory.cs`,统一 benchmark 最小宿主构建规则 + - GFramework benchmark 宿主统一先注册再 `Freeze()`,保证 steady-state 与 cold-start 都走真实可解析容器 + - MediatR benchmark 宿主统一通过 `TypeEvaluator` 限制到当前场景所需 handler / behavior 类型,保留正常 `AddMediatR` 组装路径,同时移除全程序集扫描噪音 + - `RequestStartupBenchmarks` 采用专用 `ColdStart` job,设置 `InvocationCount=1` 与 `WithUnrollFactor(1)`,并把 dispatcher cache reset 放到 `IterationSetup` +- 已修改的 benchmark 范围: + - `RequestBenchmarks` + - `RequestPipelineBenchmarks` + - `RequestStartupBenchmarks` + - `StreamingBenchmarks` + - `NotificationBenchmarks` + - `RequestInvokerBenchmarks` + - `StreamInvokerBenchmarks` +- 结果: + - `ColdStart_GFrameworkCqrs` 已恢复出有效结果,不再出现 `No CQRS request handler registered` + - `RequestBenchmarks`、`RequestStartupBenchmarks` 在本地均可实际运行 + - `RequestStartupBenchmarks` 目前仍会收到 BenchmarkDotNet 对单次 cold-start 场景的 `MinIterationTime` 提示;这是测量形状带来的工具提示,不再是运行级失败 + +### 验证(RP-090) + +- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/gframework-current-pr-review.json` + - 结果:通过 + - 备注:确认当前分支对应 `PR #326`,仍有效的 open AI feedback 集中在 benchmark 对照语义与 active 文档收敛 +- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` + - 结果:通过,`0 warning / 0 error` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:`ColdStart_GFrameworkCqrs` 已恢复,最新本地输出约 `220-292 us`,`ColdStart_MediatR` 约 `575-616 us` +- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` + - 结果:通过 + - 备注:steady-state request 对照可正常运行,未再触发 MediatR 重复注册或 GFramework 首次解析失败 + ## 2026-04-30 ### 阶段:历史 PR #307 active 入口收敛(CQRS-REWRITE-RP-076) From 6d619b9a1fd642cb47c0cda6f88caa3b8935a2e7 Mon Sep 17 00:00:00 2001 From: gewuyou <95328647+GeWuYou@users.noreply.github.com> Date: Wed, 6 May 2026 12:57:56 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix(cqrs):=20=E6=94=B6=E6=95=9B=20benchma?= =?UTF-8?q?rk=20review=20=E6=94=B6=E5=B0=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复 benchmark workflow 过滤器输入的 shell 注入风险 - 统一 request 与 stream invoker 基准中 MediatR handler 的生命周期基线 - 更新 request pipeline benchmark 的缓存清理与空行为类型声明 - 压缩 cqrs-rewrite active 跟踪与 trace,记录本轮 PR review 收尾结论 --- .github/workflows/benchmark.yml | 4 +- .../Messaging/RequestInvokerBenchmarks.cs | 2 +- .../Messaging/RequestPipelineBenchmarks.cs | 18 ++++- .../Messaging/StreamInvokerBenchmarks.cs | 2 +- .../todos/cqrs-rewrite-migration-tracking.md | 76 ++----------------- .../traces/cqrs-rewrite-migration-trace.md | 25 +++++- 6 files changed, 50 insertions(+), 77 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 76b32324..c89b2382 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -53,10 +53,12 @@ jobs: - name: Run filtered benchmarks if: ${{ inputs.benchmark_filter != '' }} + env: + BENCHMARK_FILTER: ${{ inputs.benchmark_filter }} run: | set -euo pipefail dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- \ - --filter "${{ inputs.benchmark_filter }}" + --filter "$BENCHMARK_FILTER" - name: Upload BenchmarkDotNet artifacts if: ${{ always() && inputs.benchmark_filter != '' }} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index d026ff25..a943e095 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -93,7 +93,7 @@ public class RequestInvokerBenchmarks configure: null, typeof(RequestInvokerBenchmarks), static candidateType => candidateType == typeof(MediatRBenchmarkRequestHandler), - ServiceLifetime.Singleton); + ServiceLifetime.Transient); _mediatr = _serviceProvider.GetRequiredService(); } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index 058019cd..ece2b977 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -64,6 +64,7 @@ public class RequestPipelineBenchmarks MinLevel = LogLevel.Fatal }; Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); _baselineHandler = new BenchmarkRequestHandler(); _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => @@ -101,6 +102,7 @@ public class RequestPipelineBenchmarks public void Cleanup() { _serviceProvider.Dispose(); + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); } /// @@ -261,20 +263,28 @@ public class RequestPipelineBenchmarks /// /// pipeline 行为槽位 1。 /// - public sealed class BenchmarkPipelineBehavior1 : BenchmarkPipelineBehaviorBase; + public sealed class BenchmarkPipelineBehavior1 : BenchmarkPipelineBehaviorBase + { + } /// /// pipeline 行为槽位 2。 /// - public sealed class BenchmarkPipelineBehavior2 : BenchmarkPipelineBehaviorBase; + public sealed class BenchmarkPipelineBehavior2 : BenchmarkPipelineBehaviorBase + { + } /// /// pipeline 行为槽位 3。 /// - public sealed class BenchmarkPipelineBehavior3 : BenchmarkPipelineBehaviorBase; + public sealed class BenchmarkPipelineBehavior3 : BenchmarkPipelineBehaviorBase + { + } /// /// pipeline 行为槽位 4。 /// - public sealed class BenchmarkPipelineBehavior4 : BenchmarkPipelineBehaviorBase; + public sealed class BenchmarkPipelineBehavior4 : BenchmarkPipelineBehaviorBase + { + } } diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index bfec83f0..d6ce01ba 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -93,7 +93,7 @@ public class StreamInvokerBenchmarks configure: null, typeof(StreamInvokerBenchmarks), static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler), - ServiceLifetime.Singleton); + ServiceLifetime.Transient); _mediatr = _serviceProvider.GetRequiredService(); } 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 ddef2523..f7b6f7b9 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 @@ -31,10 +31,10 @@ CQRS 迁移与收敛。 ## 当前活跃事实 - 当前分支对应 `PR #326`,状态为 `OPEN` -- latest-head review 已从 benchmark 运行级缺陷收敛到剩余文档入口与是否继续接受 benchmark 语义细化的判断 +- latest-head review 现仍有少量 open thread,但本地复核后,仍成立的问题已收敛到 benchmark 对照公平性、workflow 输入安全性与 active 文档压缩 - benchmark 场景现统一通过 `BenchmarkHostFactory` 构建最小宿主:GFramework 侧在 runtime 分发前显式 `Freeze()` 容器,MediatR 侧只扫描当前场景需要的 handler / behavior 类型 - `RequestStartupBenchmarks` 已恢复 `ColdStart_GFrameworkCqrs` 结果产出,不再命中 `No CQRS request handler registered` -- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行 +- 已新增手动触发的 benchmark workflow;默认只验证 benchmark 项目 Release build,只有显式提供过滤器时才执行 BenchmarkDotNet 运行;过滤器输入现通过环境变量传入 shell,避免 workflow_dispatch 输入直接插值到命令行 - 远端 `CTRF` 最新汇总为 `2274/2274` passed - `MegaLinter` 当前只暴露 `dotnet-format` 的 `Restore operation failed` 环境噪音,尚未提供本地仍成立的文件级格式诊断 @@ -50,74 +50,12 @@ CQRS 迁移与收敛。 - `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` - 结果:通过 - 备注:`ColdStart_GFrameworkCqrs` 已恢复出数,最新本地输出约 `220-292 us`,MediatR 对照约 `575-616 us`;当前仅剩 BenchmarkDotNet 对单次 cold-start 场景的 `MinIterationTime` 提示 -- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` - - 结果:通过 - - 备注:确认冻结后的 GFramework 最小宿主与受限扫描的 MediatR 最小宿主均可完成 steady-state request 对照 - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - 结果:通过,`0 warning / 0 error` - - 备注:用于验证新增手动 benchmark workflow 依赖的 benchmark 项目入口仍可在 Release 下编译 + - 备注:用于验证本轮 request invoker / pipeline / stream invoker 调整与 benchmark workflow 改动后的 Release 编译结果 - `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output ` - 结果:通过 - - 备注:确认当前分支对应 `PR #326`,本轮剩余 open AI feedback 主要集中在 benchmark 对照语义与 `ai-plan` 结构收敛 -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 -- `git diff --check` - - 结果:通过 -- `dotnet build GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Request_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_In_Stable_Order_For_Mixed_Direct_And_Reflected_Implementations"` - - 结果:通过,`2/2` passed -- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - - 结果:通过,`0 warning / 0 error` - - 备注:先后覆盖 `StreamingBenchmarks`、`RequestPipelineBenchmarks`、`RequestStartupBenchmarks`、`RequestInvokerBenchmarks` 与 `StreamInvokerBenchmarks` 的引入后复核 -- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1` - - 结果:部分通过 - - 备注:`Initialization_MediatR` 与 `ColdStart_MediatR` 已可实际运行;`ColdStart_GFrameworkCqrs` 仍因 `No CQRS request handler registered` 无法产出完整对照 -- `GIT_DIR= GIT_WORK_TREE= python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行,避免脚本内部 plain `git ls-files` 误判仓库上下文 -- `git diff --check` - - 结果:通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Enumerator|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Stream_Invoker_Provider_Metadata_When_Runtime_Lacks_Stream_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_Stream_Invoker_Provider_Metadata_When_Runtime_Contract_Is_Available"` - - 结果:通过,`5/5` passed -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Entry_Type|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Provider_Interface|FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Emit_Request_Invoker_Provider_Metadata_When_Runtime_Lacks_Request_Descriptor_Enumerator"` - - 结果:通过,`4/4` passed -- `dotnet build GFramework.Cqrs.SourceGenerators/GFramework.Cqrs.SourceGenerators.csproj -c Release` - - 结果:通过,`0 warning / 0 error` -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行,避免脚本内部 plain `git ls-files` 误判仓库上下文 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Emits_String_Fallback_Metadata_For_Mixed_Fallback_When_Runtime_Disallows_Multiple_Fallback_Attributes"` - - 结果:通过,`1/1` passed -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 -- `git diff --check` - - 结果:通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Generate_Registry_When_Runtime_Lacks_Handler_Registry_Interface"` - - 结果:通过,`1/1` passed -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 -- `git diff --check` - - 结果:通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Generate_Registry_When_Runtime_Lacks_Required_Generation_Contract"` - - 结果:通过,`4/4` passed -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 -- `git diff --check` - - 结果:通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Generate_Registry_When_Runtime_Lacks_Required_Generation_Contract"` - - 结果:通过,`6/6` passed -- `python3 scripts/license-header.py --check` - - 结果:通过 - - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 -- `git diff --check` - - 结果:通过 -- `dotnet test GFramework.SourceGenerators.Tests/GFramework.SourceGenerators.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsHandlerRegistryGeneratorTests.Does_Not_Generate_Registry_When_Runtime_Lacks_Required_Generation_Contract"` - - 结果:通过,`7/7` passed + - 备注:确认当前分支对应 `PR #326`,本轮剩余 open AI feedback 以 workflow 输入安全、benchmark 对照公平性与 active 文档压缩为主 - `python3 scripts/license-header.py --check` - 结果:通过 - 备注:当前 WSL worktree 需要显式绑定 `GIT_DIR` / `GIT_WORK_TREE` 后运行 @@ -126,9 +64,9 @@ CQRS 迁移与收敛。 ## 下一推荐步骤 -1. 继续处理 `PR #326` 的剩余 review 收尾,优先保持 benchmark 对照语义与 `ai-plan` active 入口一致 -2. 决定是否继续细化 `RequestStartupBenchmarks` 的 cold-start harness,降低 `InvocationCount=1` 带来的 `MinIterationTime` 提示噪音 -3. 若需要在 CI 中手动复核 benchmark,优先使用新增 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行整个 benchmark 矩阵 +1. 重新运行 `$gframework-pr-review`,确认本轮 workflow / benchmark / active 文档修复是否已消化当前 latest-head open threads +2. 若 `PR #326` 仍剩基准语义类反馈,优先判断它们属于真实对照偏差还是有意保留的 benchmark 设计取舍 +3. 若需要在 CI 中手动复核 benchmark,继续使用 workflow 的 `benchmark_filter` 输入按场景筛选,避免默认运行整个 benchmark 矩阵 ## 活跃文档 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 f3274372..4c0fb380 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 @@ -44,6 +44,29 @@ - 结果:通过 - 备注:steady-state request 对照可正常运行,未再触发 MediatR 重复注册或 GFramework 首次解析失败 +### 阶段:PR #326 review 收尾补丁(CQRS-REWRITE-RP-090) + +- 再次使用 `$gframework-pr-review` 复核 `PR #326` latest-head open threads 后,主线程确认本轮仍成立且适合在当前 PR 内收敛的问题集中在四类: + - `.github/workflows/benchmark.yml` 的 `benchmark_filter` 直接插值到 shell,存在 workflow_dispatch 输入注入风险 + - `RequestInvokerBenchmarks` 与 `StreamInvokerBenchmarks` 的 MediatR handler 生命周期仍为 `Singleton`,与 GFramework 反射 / generated 路径的 transient 语义不一致 + - `RequestPipelineBenchmarks` 未在场景切换前后清理 dispatcher 缓存,且四个空 pipeline behavior 类型仍使用非法的分号类声明 + - `ai-plan/public/cqrs-rewrite` active 文档仍保留旧失败结论与重复日期标题,和“active 入口只保留最新权威恢复点”的约束不一致 +- 本轮刻意未扩展处理的 review: + - `MicrosoftDiContainer` 的释放契约建议会扩大到核心 Ioc 接口与全仓库生命周期语义,不适合作为 benchmark review 顺手改动 + - `RequestStartupBenchmarks` 的“手工单点注册 vs 受限程序集扫描”差异目前属于有意保留的最小宿主模型,代码注释已明确该设计边界 +- 已修改: + - `.github/workflows/benchmark.yml` + - `GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs` + - `GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs` + - `ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md` + - `ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md` +- 预期结果: + - 手动 benchmark workflow 的过滤器输入不再直接参与 shell 解析 + - request / stream invoker 三路对照的 handler 生命周期重新回到同一基线 + - request pipeline benchmark 在 `0 / 1 / 4` 场景切换时不再复用旧 dispatcher cache + - active tracking / trace 更符合 boot 恢复入口所要求的“只保留最新权威结论”形状 + ## 2026-04-30 ### 阶段:历史 PR #307 active 入口收敛(CQRS-REWRITE-RP-076) @@ -257,7 +280,7 @@ 1. 若 review 重新触发后仍有 latest-head open thread,继续以 `PR #323` 为当前唯一 PR 恢复锚点复核 2. 后续若继续推进代码切片,优先复核基础 generation gate 之外的 runtime contract 或 fallback selection 分支 -## 2026-05-06 +## 2026-05-06(RP-083 ~ RP-089) ### 阶段:mixed invoker provider 排序回归(CQRS-REWRITE-RP-083)