mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-06 16:16:44 +08:00
fix(cqrs): 修复 benchmark 对照宿主与冷启动基线
- 新增 BenchmarkHostFactory 统一 benchmark 最小宿主构建,并限制 MediatR 扫描到当前场景所需类型 - 修复 GFramework benchmark 容器未冻结导致的首次 handler 解析缺口,恢复 RequestStartupBenchmarks 冷启动结果 - 优化 request、pipeline、notification、stream 与 invoker benchmark 的生命周期对齐,减少无关程序集扫描噪音 - 更新 cqrs-rewrite 跟踪与追踪文档,记录 PR #326 benchmark review 收敛、根因和验证结果
This commit is contained in:
parent
f71791ae98
commit
2cb6216d05
93
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
Normal file
93
GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs
Normal file
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 为 benchmark 场景构建最小且可重复的 GFramework / MediatR 对照宿主。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 基准工程里的对照目标是“相同消息合同下的调度差异”,而不是程序集扫描量或容器生命周期差异。
|
||||
/// 因此这里统一封装两类宿主的最小注册形状,确保:
|
||||
/// 1. GFramework 容器在首次发送前已经冻结,可真实解析按类型注册的 handler;
|
||||
/// 2. MediatR 只扫描当前 benchmark 明确拥有的 handler / behavior 类型,避免整个程序集的额外注册污染结果。
|
||||
/// </remarks>
|
||||
internal static class BenchmarkHostFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建一个已经冻结的 GFramework benchmark 容器。
|
||||
/// </summary>
|
||||
/// <param name="configure">向容器写入 benchmark 所需 handler / pipeline 的注册动作。</param>
|
||||
/// <returns>已冻结、可立即用于 runtime 分发的容器。</returns>
|
||||
internal static MicrosoftDiContainer CreateFrozenGFrameworkContainer(Action<MicrosoftDiContainer> configure)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configure);
|
||||
|
||||
var container = new MicrosoftDiContainer();
|
||||
configure(container);
|
||||
container.Freeze();
|
||||
return container;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
|
||||
/// </summary>
|
||||
/// <param name="configure">补充当前场景的显式服务注册,例如手工单例 handler 或 pipeline 行为。</param>
|
||||
/// <param name="handlerAssemblyMarkerType">用于限定扫描程序集的标记类型。</param>
|
||||
/// <param name="handlerTypeFilter">
|
||||
/// 仅允许当前 benchmark 场景需要的 handler / behavior 类型通过扫描;
|
||||
/// 这样可保留 `AddMediatR` 的正常装配路径,同时避免整个基准程序集里的其他 handler 被一并注册。
|
||||
/// </param>
|
||||
/// <param name="lifetime">当前 benchmark 希望 MediatR 使用的默认注册生命周期。</param>
|
||||
/// <returns>只承载当前 benchmark 场景所需服务的 DI 宿主。</returns>
|
||||
internal static ServiceProvider CreateMediatRServiceProvider(
|
||||
Action<IServiceCollection>? configure,
|
||||
Type handlerAssemblyMarkerType,
|
||||
Func<Type, bool> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
|
||||
/// </summary>
|
||||
/// <param name="candidateType">待判断类型。</param>
|
||||
/// <param name="openGenericContract">目标开放泛型合同,例如 <see cref="MediatR.IRequestHandler{TRequest,TResponse}" />。</param>
|
||||
/// <returns>命中任一实现接口时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
internal static bool ImplementsOpenGenericContract(Type candidateType, Type openGenericContract)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(candidateType);
|
||||
ArgumentNullException.ThrowIfNull(openGenericContract);
|
||||
|
||||
return candidateType.GetInterfaces().Any(interfaceType =>
|
||||
interfaceType.IsGenericType &&
|
||||
interfaceType.GetGenericTypeDefinition() == openGenericContract);
|
||||
}
|
||||
}
|
||||
@ -58,21 +58,20 @@ public class NotificationBenchmarks
|
||||
};
|
||||
Fixture.Setup("Notification", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_container = new MicrosoftDiContainer();
|
||||
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.INotificationHandler<BenchmarkNotification>>(
|
||||
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<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>();
|
||||
services.AddMediatR(static options => options.RegisterServicesFromAssembly(typeof(NotificationBenchmarks).Assembly));
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
services => services.AddSingleton<MediatR.INotificationHandler<BenchmarkNotification>, BenchmarkNotificationHandler>(),
|
||||
typeof(NotificationBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkNotificationHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_publisher = _serviceProvider.GetRequiredService<IPublisher>();
|
||||
|
||||
_notification = new BenchmarkNotification(Guid.NewGuid());
|
||||
|
||||
@ -59,23 +59,21 @@ public class RequestBenchmarks
|
||||
};
|
||||
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_container = new MicrosoftDiContainer();
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
|
||||
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
|
||||
_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<MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, 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<IMediator>();
|
||||
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
@ -73,27 +73,27 @@ public class RequestInvokerBenchmarks
|
||||
_generatedRequest = new GeneratedBenchmarkRequest(Guid.NewGuid());
|
||||
_mediatrRequest = new MediatRBenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
_reflectionContainer = new MicrosoftDiContainer();
|
||||
_reflectionContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkRequestHandler>();
|
||||
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
|
||||
{
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<ReflectionBenchmarkRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkRequestHandler>();
|
||||
});
|
||||
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
|
||||
_reflectionContainer,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestInvokerBenchmarks) + ".Reflection"));
|
||||
|
||||
_generatedContainer = new MicrosoftDiContainer();
|
||||
_generatedContainer.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
|
||||
_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<MediatR.IRequestHandler<MediatRBenchmarkRequest, MediatRBenchmarkResponse>, 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<IMediator>();
|
||||
}
|
||||
|
||||
|
||||
@ -65,25 +65,30 @@ public class RequestPipelineBenchmarks
|
||||
};
|
||||
Fixture.Setup("RequestPipeline", handlerCount: 1, pipelineCount: PipelineCount);
|
||||
|
||||
_container = new MicrosoftDiContainer();
|
||||
_baselineHandler = new BenchmarkRequestHandler();
|
||||
|
||||
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
RegisterGFrameworkPipelineBehaviors(_container, PipelineCount);
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
|
||||
_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<MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, 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<IMediator>();
|
||||
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在每次 cold-start 迭代前清空 dispatcher 静态缓存,确保两组 benchmark 都重新命中首次绑定路径。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 使用 `IterationSetup` 而不是把缓存清理写在 benchmark 方法主体中,
|
||||
/// 可以把“清理静态缓存”留在测量边界之外,只保留宿主构建与首次发送本身。
|
||||
/// </remarks>
|
||||
[IterationSetup]
|
||||
public void ResetColdStartCaches()
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 startup benchmark 复用的宿主对象。
|
||||
/// </summary>
|
||||
@ -110,23 +126,10 @@ public class RequestStartupBenchmarks
|
||||
[BenchmarkCategory("ColdStart")]
|
||||
public ValueTask<BenchmarkResponse> ColdStart_GFrameworkCqrs()
|
||||
{
|
||||
var runtime = CreateColdStartRuntime();
|
||||
var runtime = CreateGFrameworkRuntime();
|
||||
return runtime.SendAsync(BenchmarkContext.Instance, Request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为 cold-start benchmark 构建全新的 runtime,并在构建前显式清空 dispatcher 静态缓存。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 这里把缓存清理与 runtime 构建绑定在同一阶段,避免把额外的反射缓存清理成本混入 benchmark 方法主体,
|
||||
/// 只保留“新宿主 + 首次分发”的对照。
|
||||
/// </summary>
|
||||
private static ICqrsRuntime CreateColdStartRuntime()
|
||||
{
|
||||
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
|
||||
return CreateGFrameworkRuntime();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建只承载当前 benchmark request 的最小 GFramework.CQRS runtime。
|
||||
/// </summary>
|
||||
@ -136,8 +139,10 @@ public class RequestStartupBenchmarks
|
||||
/// </remarks>
|
||||
private static ICqrsRuntime CreateGFrameworkRuntime()
|
||||
{
|
||||
var container = new MicrosoftDiContainer();
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
var container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static currentContainer =>
|
||||
{
|
||||
currentContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>, BenchmarkRequestHandler>();
|
||||
});
|
||||
return GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, RuntimeLogger);
|
||||
}
|
||||
|
||||
@ -146,14 +151,11 @@ public class RequestStartupBenchmarks
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -73,27 +73,27 @@ public class StreamInvokerBenchmarks
|
||||
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
|
||||
_reflectionContainer = new MicrosoftDiContainer();
|
||||
_reflectionContainer.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>, ReflectionBenchmarkStreamHandler>();
|
||||
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(static container =>
|
||||
{
|
||||
container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>, 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<MediatR.IStreamRequestHandler<MediatRBenchmarkStreamRequest, MediatRBenchmarkResponse>, 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<IMediator>();
|
||||
}
|
||||
|
||||
|
||||
@ -60,23 +60,21 @@ public class StreamingBenchmarks
|
||||
};
|
||||
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
|
||||
|
||||
_container = new MicrosoftDiContainer();
|
||||
_baselineHandler = new BenchmarkStreamHandler();
|
||||
|
||||
_container.RegisterTransient<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>, BenchmarkStreamHandler>();
|
||||
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
|
||||
{
|
||||
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>>(
|
||||
_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<MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>, 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<IMediator>();
|
||||
|
||||
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
|
||||
|
||||
@ -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 矩阵
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user