mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-09 10:19:00 +08:00
perf(cqrs): 优化请求分发热路径并补充 Mediator 对照基准
- 优化 dispatcher 在零 pipeline 场景下跳过空行为解析,减少请求热路径分配 - 修复 MicrosoftDiContainer 热路径的无效 debug 字符串构造,并新增非激活注册检测回归测试 - 新增基于 NuGet 的 Mediator 对照基准并更新 CQRS 重写跟踪文档
This commit is contained in:
parent
e44c56fb46
commit
18018966f9
@ -252,6 +252,18 @@ public interface IIocContainer : IContextAware, IDisposable
|
||||
/// </remarks>
|
||||
bool Contains<T>() where T : class;
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求解析出实例。
|
||||
/// </summary>
|
||||
/// <param name="type">要检查的服务类型。</param>
|
||||
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
/// <remarks>
|
||||
/// 该入口面向“先判断是否值得解析实例”的热路径优化场景。
|
||||
/// 与 <see cref="Contains{T}" /> 不同,它不会为了判断结果而激活服务实例,因此可避免把瞬态对象创建、
|
||||
/// 多服务枚举或日志分配混入仅需存在性判断的调用链中。
|
||||
/// </remarks>
|
||||
bool HasRegistration(Type type);
|
||||
|
||||
/// <summary>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// </summary>
|
||||
|
||||
@ -419,6 +419,32 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(_container.Contains<TestService>(), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试显式服务不存在时,HasRegistration 应返回 false,且不会要求先冻结或解析实例。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasRegistration_WithNoMatchingService_Should_ReturnFalse()
|
||||
{
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.False);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试 HasRegistration 能识别开放泛型 CQRS pipeline 行为对闭合请求/响应对的可见性。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void HasRegistration_Should_ReturnTrue_For_Closed_Service_Satisfied_By_Open_Generic_Registration()
|
||||
{
|
||||
_container.GetServicesUnsafe.AddSingleton(
|
||||
typeof(IPipelineBehavior<,>),
|
||||
typeof(OpenGenericHasRegistrationBehavior<,>));
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
|
||||
|
||||
_container.Freeze();
|
||||
|
||||
Assert.That(_container.HasRegistration(typeof(IPipelineBehavior<HasRegistrationRequest, int>)), Is.True);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试当实例存在时检查实例包含关系应返回 true 的功能
|
||||
/// </summary>
|
||||
@ -902,4 +928,32 @@ public class MicrosoftDiContainerTests
|
||||
Assert.That(lockField, Is.Not.Null);
|
||||
return (ReaderWriterLockSlim)lockField!.GetValue(container)!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 HasRegistration 回归使用的最小请求类型。
|
||||
/// </summary>
|
||||
private sealed class HasRegistrationRequest : IRequest<int>
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 供 HasRegistration 回归使用的开放泛型 pipeline 行为。
|
||||
/// </summary>
|
||||
/// <typeparam name="TRequest">请求类型。</typeparam>
|
||||
/// <typeparam name="TResponse">响应类型。</typeparam>
|
||||
private sealed class OpenGenericHasRegistrationBehavior<TRequest, TResponse> :
|
||||
IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : IRequest<TResponse>
|
||||
{
|
||||
/// <summary>
|
||||
/// 透传到下一个 pipeline 节点,不额外改变请求语义。
|
||||
/// </summary>
|
||||
public ValueTask<TResponse> Handle(
|
||||
TRequest request,
|
||||
MessageHandlerDelegate<TRequest, TResponse> next,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return next(request, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -706,9 +706,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
var result = _provider!.GetService(type);
|
||||
_logger.Debug(result != null
|
||||
? $"Retrieved instance: {type.Name}"
|
||||
: $"No instance found for type: {type.Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug(result != null
|
||||
? $"Retrieved instance: {type.Name}"
|
||||
: $"No instance found for type: {type.Name}");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
finally
|
||||
@ -792,7 +795,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices<T>().ToList();
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {typeof(T).Name}");
|
||||
}
|
||||
return services;
|
||||
}
|
||||
finally
|
||||
@ -821,7 +827,10 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
|
||||
var services = _provider!.GetServices(type).ToList();
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
|
||||
if (_logger.IsDebugEnabled())
|
||||
{
|
||||
_logger.Debug($"Retrieved {services.Count} instances of {type.Name}");
|
||||
}
|
||||
return services.Where(o => o != null).Cast<object>().ToList();
|
||||
}
|
||||
finally
|
||||
@ -1023,6 +1032,26 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查容器中是否存在可赋值给指定服务类型的注册项,而不要求先解析实例。
|
||||
/// </summary>
|
||||
/// <param name="type">要检查的服务类型。</param>
|
||||
/// <returns>若存在显式注册或开放泛型映射可满足该服务类型,则返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
public bool HasRegistration(Type type)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(type);
|
||||
ThrowIfDisposed();
|
||||
EnterReadLockOrThrowDisposed();
|
||||
try
|
||||
{
|
||||
return HasRegistrationCore(type);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断容器中是否包含某个具体的实例对象
|
||||
/// 通过已注册实例集合进行快速查找
|
||||
@ -1043,6 +1072,50 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在当前容器状态下检查指定服务类型是否存在可见注册。
|
||||
/// </summary>
|
||||
/// <param name="requestedType">要检查的服务类型。</param>
|
||||
/// <returns>存在可满足该类型的注册时返回 <see langword="true" />;否则返回 <see langword="false" />。</returns>
|
||||
/// <remarks>
|
||||
/// 该检查只回答“是否可能解析到服务”,不会为了判断结果而激活实例。
|
||||
/// 预冻结阶段只基于当前服务描述符推断;冻结后则同样只观察描述符,
|
||||
/// 避免把瞬态/多实例解析成本混入热路径中的存在性判断。
|
||||
/// </remarks>
|
||||
private bool HasRegistrationCore(Type requestedType)
|
||||
{
|
||||
foreach (var descriptor in GetServicesUnsafe)
|
||||
{
|
||||
if (CanSatisfyServiceType(descriptor.ServiceType, requestedType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个服务描述符声明的服务类型是否能满足当前请求类型。
|
||||
/// </summary>
|
||||
/// <param name="registeredServiceType">注册时声明的服务类型。</param>
|
||||
/// <param name="requestedType">调用方请求的服务类型。</param>
|
||||
/// <returns>若当前注册可用于解析 <paramref name="requestedType" />,则返回 <see langword="true" />。</returns>
|
||||
private static bool CanSatisfyServiceType(Type registeredServiceType, Type requestedType)
|
||||
{
|
||||
if (registeredServiceType == requestedType || requestedType.IsAssignableFrom(registeredServiceType))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (requestedType.IsConstructedGenericType && registeredServiceType.IsGenericTypeDefinition)
|
||||
{
|
||||
return requestedType.GetGenericTypeDefinition() == registeredServiceType;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空容器中的所有实例和服务注册
|
||||
/// 只有在容器未冻结状态下才能执行清空操作
|
||||
|
||||
@ -18,6 +18,11 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
|
||||
<PackageReference Include="Mediator.Abstractions" Version="3.0.2" />
|
||||
<PackageReference Include="Mediator.SourceGenerator" Version="3.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MediatR" Version="13.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||
|
||||
@ -75,6 +75,24 @@ internal static class BenchmarkHostFactory
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建承载 `ai-libs/Mediator` source-generated concrete mediator 的最小对照宿主。
|
||||
/// </summary>
|
||||
/// <param name="configure">补充当前场景的显式服务注册。</param>
|
||||
/// <returns>可直接解析 generated `Mediator.Mediator` 的 DI 宿主。</returns>
|
||||
/// <remarks>
|
||||
/// 当前 benchmark 只把 `Mediator` 作为单例 steady-state 对照组接入,
|
||||
/// 因为它的 lifetime 由 source generator 在编译期塑形;若后续需要 `Transient` / `Scoped` 矩阵,
|
||||
/// 应按 `ai-libs/Mediator/benchmarks` 的做法拆成独立 build config,而不是在同一编译产物里混用多个 lifetime。
|
||||
/// </remarks>
|
||||
internal static ServiceProvider CreateMediatorServiceProvider(Action<IServiceCollection>? configure)
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
configure?.Invoke(services);
|
||||
services.AddMediator();
|
||||
return services.BuildServiceProvider();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断某个类型是否正好实现了指定的闭合或开放 MediatR 合同。
|
||||
/// </summary>
|
||||
|
||||
@ -16,19 +16,22 @@ using GFramework.Core.Logging;
|
||||
using GFramework.Cqrs.Abstractions.Cqrs;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using GeneratedMediator = Mediator.Mediator;
|
||||
|
||||
namespace GFramework.Cqrs.Benchmarks.Messaging;
|
||||
|
||||
/// <summary>
|
||||
/// 对比单个 request 在直接调用、GFramework.CQRS runtime 与 MediatR 之间的 steady-state dispatch 开销。
|
||||
/// 对比单个 request 在直接调用、GFramework.CQRS runtime、`ai-libs/Mediator` 与 MediatR 之间的 steady-state dispatch 开销。
|
||||
/// </summary>
|
||||
[Config(typeof(Config))]
|
||||
public class RequestBenchmarks
|
||||
{
|
||||
private MicrosoftDiContainer _container = null!;
|
||||
private ICqrsRuntime _runtime = null!;
|
||||
private ServiceProvider _serviceProvider = null!;
|
||||
private ServiceProvider _mediatrServiceProvider = null!;
|
||||
private ServiceProvider _mediatorServiceProvider = null!;
|
||||
private IMediator _mediatr = null!;
|
||||
private GeneratedMediator _mediator = null!;
|
||||
private BenchmarkRequestHandler _baselineHandler = null!;
|
||||
private BenchmarkRequest _request = null!;
|
||||
|
||||
@ -69,23 +72,26 @@ public class RequestBenchmarks
|
||||
_container,
|
||||
LoggerFactoryResolver.Provider.CreateLogger(nameof(RequestBenchmarks)));
|
||||
|
||||
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
_mediatrServiceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
|
||||
configure: null,
|
||||
typeof(RequestBenchmarks),
|
||||
static candidateType => candidateType == typeof(BenchmarkRequestHandler),
|
||||
ServiceLifetime.Singleton);
|
||||
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
|
||||
_mediatr = _mediatrServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
_mediatorServiceProvider = BenchmarkHostFactory.CreateMediatorServiceProvider(configure: null);
|
||||
_mediator = _mediatorServiceProvider.GetRequiredService<GeneratedMediator>();
|
||||
|
||||
_request = new BenchmarkRequest(Guid.NewGuid());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 释放 MediatR 对照组使用的 DI 宿主。
|
||||
/// 释放 MediatR 与 `Mediator` 对照组使用的 DI 宿主。
|
||||
/// </summary>
|
||||
[GlobalCleanup]
|
||||
public void Cleanup()
|
||||
{
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
|
||||
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -115,12 +121,22 @@ public class RequestBenchmarks
|
||||
return _mediatr.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 `ai-libs/Mediator` 的 source-generated concrete mediator 发送 request,作为高性能对照组。
|
||||
/// </summary>
|
||||
[Benchmark]
|
||||
public ValueTask<BenchmarkResponse> SendRequest_Mediator()
|
||||
{
|
||||
return _mediator.Send(_request, CancellationToken.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Benchmark request。
|
||||
/// </summary>
|
||||
/// <param name="Id">请求标识。</param>
|
||||
public sealed record BenchmarkRequest(Guid Id) :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequest<BenchmarkResponse>,
|
||||
Mediator.IRequest<BenchmarkResponse>,
|
||||
MediatR.IRequest<BenchmarkResponse>;
|
||||
|
||||
/// <summary>
|
||||
@ -134,6 +150,7 @@ public class RequestBenchmarks
|
||||
/// </summary>
|
||||
public sealed class BenchmarkRequestHandler :
|
||||
GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
Mediator.IRequestHandler<BenchmarkRequest, BenchmarkResponse>,
|
||||
MediatR.IRequestHandler<BenchmarkRequest, BenchmarkResponse>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
- `Messaging/Fixture.cs`
|
||||
- 运行前输出并校验场景配置
|
||||
- `Messaging/RequestBenchmarks.cs`
|
||||
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- direct handler、`GFramework.Cqrs` runtime、`ai-libs/Mediator` source-generated concrete path 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestLifetimeBenchmarks.cs`
|
||||
- `Singleton / Transient` 两类 handler 生命周期下,direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
|
||||
- `Messaging/RequestPipelineBenchmarks.cs`
|
||||
@ -42,6 +42,7 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
|
||||
## 后续扩展方向
|
||||
|
||||
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
|
||||
- `ai-libs/Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
|
||||
- stream handler 生命周期矩阵
|
||||
- 带真实显式作用域边界的 scoped host 对照
|
||||
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景
|
||||
|
||||
@ -120,13 +120,17 @@ internal sealed class CqrsDispatcher(
|
||||
$"No CQRS request handler registered for {requestType.FullName}.");
|
||||
|
||||
PrepareHandler(handler, context);
|
||||
if (!container.HasRegistration(dispatchBinding.BehaviorType))
|
||||
{
|
||||
return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
|
||||
foreach (var behavior in behaviors)
|
||||
{
|
||||
PrepareHandler(behavior, context);
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, request, cancellationToken)
|
||||
@ -159,13 +163,17 @@ internal sealed class CqrsDispatcher(
|
||||
$"No CQRS stream handler registered for {requestType.FullName}.");
|
||||
|
||||
PrepareHandler(handler, context);
|
||||
if (!container.HasRegistration(dispatchBinding.BehaviorType))
|
||||
{
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.StreamInvoker(handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
|
||||
|
||||
foreach (var behavior in behaviors)
|
||||
{
|
||||
PrepareHandler(behavior, context);
|
||||
|
||||
if (behaviors.Count == 0)
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.StreamInvoker(handler, request, cancellationToken);
|
||||
}
|
||||
|
||||
return (IAsyncEnumerable<TResponse>)dispatchBinding.GetPipelineExecutor(behaviors.Count)
|
||||
.Invoke(handler, behaviors, dispatchBinding.StreamInvoker, request, cancellationToken);
|
||||
|
||||
@ -7,7 +7,7 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-100`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-101`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`PR #339`
|
||||
- 当前结论:
|
||||
@ -36,15 +36,22 @@ CQRS 迁移与收敛。
|
||||
- 当前 `RP-098` 已再次使用 `$gframework-pr-review` 复核 `PR #334` latest-head review,并收口 `LegacyCqrsDispatchHelper.TryResolveDispatchContext(...)` 过宽吞掉 `InvalidOperationException` 的真实运行时诊断退化问题;现在仅把“上下文尚未就绪”视为允许 fallback 的信号,并为 fallback / 异常冒泡分别补齐回归测试
|
||||
- `RP-099` 已补齐 `GFramework.Cqrs` 的最小 stream pipeline seam:新增 `IStreamPipelineBehavior<,>` / `StreamMessageHandlerDelegate<,>`、`RegisterCqrsStreamPipelineBehavior<TBehavior>()`、dispatcher 侧 stream pipeline executor 缓存与 generated stream invoker 兼容回归,以及 `Architecture` 公开注册入口与对应文档说明
|
||||
- 当前 `RP-100` 已使用 `$gframework-pr-review` 复核 `PR #339` latest-head review:收口 `RegisterCqrsStreamPipelineBehavior<TBehavior>()` 的异常契约文档、为 `StreamPipelineInvocation.GetContinuation(...)` 补齐并发 continuation 缓存说明、抽取 `MicrosoftDiContainer` 的 CQRS 行为注册公共逻辑,并顺手修复当前 branch diff 内 `ICqrsRequestInvokerProvider.cs` 的 XML 缩进格式问题
|
||||
- `ai-plan` active 入口现以 `RP-100` 为最新恢复锚点;`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
|
||||
- 当前 `RP-101` 已按用户新增 benchmark 诉求收口 request 热路径:为 `IIocContainer` 新增不激活实例的 `HasRegistration(Type)`、让 dispatcher 在 `0 pipeline` 场景下跳过空行为解析,并为 `MicrosoftDiContainer` 的热路径查询补齐 debug-level 守卫,避免无效日志字符串分配
|
||||
- 当前 `RP-101` 已把 `GFramework.Cqrs.Benchmarks` 的 `Mediator` 对照组收口为官方 NuGet 引用(`Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`),不再使用本地 `ai-libs/Mediator` project reference;`RequestBenchmarks` 现已新增 source-generated concrete `Mediator` 对照方法,并通过 `RequestLifetimeBenchmarks` 复核 hot path 收口后的新基线
|
||||
- `ai-plan` active 入口现以 `RP-101` 为最新恢复锚点;`PR #339`、`PR #334`、`PR #331`、`PR #326`、`PR #323`、`PR #307` 与其他更早阶段细节均以下方归档或说明为准
|
||||
|
||||
## 当前活跃事实
|
||||
|
||||
- 当前分支为 `feat/cqrs-optimization`
|
||||
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`2c58d8b6`, 2026-05-07 13:24:46 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
|
||||
- 当前分支相对 `origin/main` 的累计 branch diff 为 `31 files changed, 1176 insertions(+), 15 deletions(-)`,仍明显低于 `$gframework-batch-boot 50` 的文件 / 行数阈值
|
||||
- 本轮 `$gframework-batch-boot 50` 以 `origin/main` (`5dc2dd25`, 2026-05-08 09:08:37 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
|
||||
- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`;本轮待提交工作树包含 9 个跟踪文件修改,另有 `BenchmarkDotNet.Artifacts/` 生成输出未纳入提交,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
|
||||
- `GFramework.Cqrs.Benchmarks` 作为 benchmark 基础设施项目,必须持续排除在 NuGet / GitHub Packages 发布集合之外
|
||||
- `GFramework.Cqrs.Benchmarks` 现已覆盖 request steady-state、pipeline 数量矩阵、startup、request/stream generated invoker,以及 request handler `Singleton / Transient` 生命周期矩阵
|
||||
- `GFramework.Cqrs.Benchmarks` 当前以 NuGet 方式引用 `Mediator.Abstractions` / `Mediator.SourceGenerator` `3.0.2`;`ai-libs/Mediator` 只保留为本地源码/README 对照资料,不再参与 benchmark 项目编译
|
||||
- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:约 `5.969 ns / 32 B`、`6.242 ns / 32 B`、`53.818 ns / 232 B`、`85.504 ns / 32 B`
|
||||
- 当前 request lifetime benchmark 已从旧坏值显著收敛:`Singleton` 下 `GFramework.Cqrs` 约 `84.066 ns / 32 B`(旧值 `301.731 ns / 440 B`),`Transient` 下约 `90.652 ns / 56 B`(旧值 `287.863 ns / 464 B`)
|
||||
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
|
||||
- 当前 request steady-state 仍落后于 source-generated `Mediator` 与 `MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x` 于 `Mediator`”;下一批若继续压 request dispatch,应优先评估默认路径吸收 generated invoker/provider 的空间
|
||||
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand`、`IAsyncCommand`、`IQuery`、`IAsyncQuery` 接到统一 `ICqrsRuntime`
|
||||
- 标准 `Architecture` 初始化路径会自动扫描 `GFramework.Core` 程序集中的 legacy bridge handler,因此旧 `SendCommand(...)` / `SendQuery(...)` 无需改变用法即可进入统一 pipeline
|
||||
- `CommandExecutor`、`QueryExecutor`、`AsyncQueryExecutor` 仍保留“无 runtime 时直接执行”的回退路径,用于不依赖容器的隔离单元测试
|
||||
@ -83,6 +90,8 @@ CQRS 迁移与收敛。
|
||||
- 若后续新增 benchmark / example / tooling 项目但未同步校验发布面,solution 级 `dotnet pack` 仍可能在 tag 发布前才暴露异常包
|
||||
- `RequestStartupBenchmarks` 为了量化真正的单次 cold-start,引入了 `InvocationCount=1` / `UnrollFactor=1` 的专用 job;该配置会触发 BenchmarkDotNet 的 `MinIterationTime` 提示,后续若要做稳定基线比较,还需要决定是否引入批量外层循环或自定义 cold-start harness
|
||||
- 当前 benchmark 宿主仍刻意保持“单根容器最小宿主”模型;若要公平比较 `Scoped` handler 生命周期,需要先引入显式 scope 创建与 scope 内首次解析的对照基线
|
||||
- 当前 `Mediator` 对照组仅先接入 steady-state request;若要把 `Transient` / `Scoped` 生命周期矩阵也纳入同一组对照,需要按 `Mediator` 官方 benchmark 的做法拆分 compile-time lifetime build config,而不是在同一编译产物里混用多个 lifetime
|
||||
- `BenchmarkDotNet.Artifacts/` 是本轮本地运行生成的未跟踪输出;若后续需要提交新的基准报告,应先确认仓库是否要保留该批产物,而不是默认把生成目录纳入版本控制
|
||||
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
|
||||
- 若继续扩大 generated invoker 覆盖面,需要持续区分“可静态表达的合同”与 `PreciseReflectedRegistrationSpec` 等仍需保守回退的场景
|
||||
- legacy bridge 当前只为已有 `Command` / `Query` 兼容入口接到统一 request pipeline;若后续要继续对齐 `Mediator`,仍需要单独设计 stream pipeline、telemetry 与 facade 公开面,而不是把这次 bridge 当成“全部收口完成”
|
||||
@ -100,6 +109,14 @@ CQRS 迁移与收敛。
|
||||
- 备注:共享脚本确认 actual package set 与预期 14 个发布包完全一致
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
|
||||
- 结果:通过,`51/51` passed
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:最新 steady-state request 对照约为 baseline `5.969 ns / 32 B`、`Mediator` `6.242 ns / 32 B`、`MediatR` `53.818 ns / 232 B`、`GFramework.Cqrs` `85.504 ns / 32 B`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton` 下 `GFramework.Cqrs` / `MediatR` 约 `84.066 ns / 32 B` vs `56.096 ns / 232 B`;`Transient` 下约 `90.652 ns / 56 B` vs `57.207 ns / 232 B`
|
||||
- `python3 scripts/license-header.py --check`
|
||||
- 结果:通过
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestStartupBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
@ -204,9 +221,9 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 下一推荐步骤
|
||||
|
||||
1. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #339` stale review threads;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核
|
||||
2. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 `Mediator` 能力吸收,下一批建议从 `notification publisher` 策略或 facade 公开入口中选择一个独立切片推进
|
||||
3. 若后续要增强 stream observability,优先评估是否需要元素级 hook,而不是直接复用当前建流级 seam 承载更多语义
|
||||
1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理 benchmark/`Mediator` 对齐,下一批建议把 `Mediator` 的 compile-time lifetime 矩阵扩展到 `RequestLifetimeBenchmarks`,避免只有 `MediatR` 参与生命周期对照
|
||||
2. 若继续压 request steady-state 开销,下一批优先评估 `GFramework.Cqrs` 默认 request 路径吸收 generated invoker provider 的可行性,而不是只在单独 benchmark 类里保留 handwritten generated 对照
|
||||
3. 在 GitHub 上 resolve / reply 已被当前分支实质吸收的 `PR #339` stale review threads;若 head 更新后线程数量继续变化,再用 `$gframework-pr-review` 复核
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -1,5 +1,49 @@
|
||||
# CQRS 重写迁移追踪
|
||||
|
||||
## 2026-05-08
|
||||
|
||||
### 阶段:request 热路径 benchmark 收口与 NuGet `Mediator` 对照补齐(CQRS-REWRITE-RP-101)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,本轮先按 `origin/main` 复核 branch diff 基线:
|
||||
- `origin/main` = `5dc2dd25`,提交时间 `2026-05-08 09:08:37 +0800`
|
||||
- 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`
|
||||
- 当前工作树仅新增 9 个跟踪文件修改,另有 `BenchmarkDotNet.Artifacts/` 本地生成输出未纳入提交范围,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
|
||||
- 用户新增的 benchmark 诉求有两部分:
|
||||
- 解释 `BenchmarkDotNet.Artifacts/results` 里为什么 `GFramework.Cqrs` request 路径表现显著差于对照组
|
||||
- 把 `martinothamar/Mediator` 加入 benchmark 对照,但必须使用官方 NuGet 包,不允许接本地 `ai-libs/Mediator` project reference
|
||||
- 本轮主线程先回到 runtime hot path 与 benchmark 宿主做最小成本排查,确认旧坏值的两个主要根因:
|
||||
- `CqrsDispatcher.SendAsync(...)` / `CreateStream(...)` 在 `0 pipeline` 场景下仍无条件执行 `container.GetAll(dispatchBinding.BehaviorType)`,即使根本没有行为注册,也会多走一次容器解析与空集合分配
|
||||
- `MicrosoftDiContainer.Get(Type)` / `GetAll<T>()` / `GetAll(Type)` 在 debug logging 关闭时仍会先构造日志字符串,导致 benchmark 默认 `Fatal` 级别下仍持续产生无效分配
|
||||
- 本轮主线程决策:
|
||||
- 为 `IIocContainer` 新增不激活实例的 `HasRegistration(Type)`,并由 `MicrosoftDiContainer` 提供支持开放泛型匹配的非激活查询实现
|
||||
- 让 `CqrsDispatcher` 在 request / stream 的 `0 pipeline` 场景先走 `HasRegistration(...)` fast-path;没有行为注册时直接调用已准备好的 request / stream invoker,不再解析空行为列表
|
||||
- 为 `MicrosoftDiContainer` 的热路径查询补 `IsDebugEnabled()` 守卫,避免 benchmark 常态配置下的无效日志字符串构造
|
||||
- 在 benchmark 项目中通过 NuGet 接入 `Mediator.Abstractions` 与 `Mediator.SourceGenerator` `3.0.2`,并让 `RequestBenchmarks` 使用 source-generated concrete `Mediator.Mediator` 作为新对照组
|
||||
- 保持 `ai-libs/Mediator` 只作为本地源码 / README 参考资料,不参与编译或项目引用
|
||||
- 本轮新增 / 更新的验证与回归覆盖:
|
||||
- `GFramework.Core.Tests/Ioc/MicrosoftDiContainerTests.cs` 新增 `HasRegistration(...)` 回归,覆盖“无匹配注册返回 false”与“开放泛型注册可满足封闭请求行为类型”两个分支
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs` 现在同时对照 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs`
|
||||
- `GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs` 新增 `CreateMediatorServiceProvider(...)`,统一最小宿主构建方式
|
||||
- 本轮权威验证:
|
||||
- `dotnet build GFramework.Core/GFramework.Core.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
|
||||
- 结果:通过,`51/51` passed
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:steady-state request 对照约为 baseline `5.969 ns / 32 B`、`Mediator` `6.242 ns / 32 B`、`MediatR` `53.818 ns / 232 B`、`GFramework.Cqrs` `85.504 ns / 32 B`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:`Singleton` 下 `GFramework.Cqrs` 从旧值 `301.731 ns / 440 B` 收敛到 `84.066 ns / 32 B`;`Transient` 下从旧值 `287.863 ns / 464 B` 收敛到 `90.652 ns / 56 B`
|
||||
- 本轮结论:
|
||||
- `GFramework.Cqrs` 之前“垫底很多”的主要原因不是抽象层级本身,而是 request 热路径残留了两个可避免的分配热点:空 pipeline 解析与禁用日志下的字符串构造
|
||||
- 收口后,`GFramework.Cqrs` 仍慢于 `MediatR` 与 source-generated `Mediator`,但已经去掉了旧 benchmark 中最明显的异常分配和 300ns 级退化
|
||||
- 下一批若继续沿用 `$gframework-batch-boot 50` 压 request steady-state,最值得优先评估的是让默认 request 路径进一步吸收 generated invoker/provider 的收益,而不是继续扩大更多横向对照项
|
||||
|
||||
## 2026-05-07
|
||||
|
||||
### 阶段:PR #339 stream pipeline seam review 收口(CQRS-REWRITE-RP-100)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user