test(cqrs-benchmarks): 补齐 stream scoped 生命周期矩阵

- 新增真实 scoped stream benchmark helper,确保作用域覆盖完整枚举周期

- 扩展 StreamLifetimeBenchmarks 到 Singleton、Scoped、Transient 全矩阵并记录 scoped 结果

- 更新 benchmark README 与 cqrs-rewrite 恢复文档,收口当前验证结论与下一批方向
This commit is contained in:
gewuyou 2026-05-11 08:43:02 +08:00
parent 11a6b6abe4
commit 594798dcb9
5 changed files with 231 additions and 28 deletions

View File

@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -218,6 +219,59 @@ internal static class BenchmarkHostFactory
return await mediator.Send(request, cancellationToken).ConfigureAwait(false); return await mediator.Send(request, cancellationToken).ConfigureAwait(false);
} }
/// <summary>
/// 在真实的 request 级作用域内创建一次 GFramework.CQRS stream并让该作用域覆盖整个异步枚举周期。
/// </summary>
/// <typeparam name="TResponse">stream 响应元素类型。</typeparam>
/// <param name="rootContainer">冻结后的 benchmark 根容器,用于创建 request 作用域并提供注册元数据。</param>
/// <param name="runtimeLogger">当前 request 级 runtime 复用的日志器。</param>
/// <param name="context">当前 CQRS 分发上下文。</param>
/// <param name="request">要创建 stream 的 request。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>绑定到单次显式作用域的异步响应序列。</returns>
/// <remarks>
/// stream 与 request 的区别在于handler 解析发生在建流时,但 scoped 依赖必须一直存活到枚举完成。
/// 因此这里返回一个包装后的 async iterator把 scope 的释放时机推迟到调用方结束枚举之后,
/// 避免 `Scoped` handler 退化成“建流后立刻释放 scope再在根容器语义下继续枚举”的错误模型。
/// </remarks>
internal static IAsyncEnumerable<TResponse> CreateScopedGFrameworkStream<TResponse>(
MicrosoftDiContainer rootContainer,
ILogger runtimeLogger,
ICqrsContext context,
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(rootContainer);
ArgumentNullException.ThrowIfNull(runtimeLogger);
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
return EnumerateScopedGFrameworkStreamAsync(rootContainer, runtimeLogger, context, request, cancellationToken);
}
/// <summary>
/// 在真实的 request 级作用域内创建一次 MediatR stream并让该作用域覆盖整个异步枚举周期。
/// </summary>
/// <typeparam name="TResponse">stream 响应元素类型。</typeparam>
/// <param name="rootServiceProvider">当前 benchmark 的根 <see cref="ServiceProvider" />。</param>
/// <param name="request">要创建 stream 的 request。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>绑定到单次显式作用域的异步响应序列。</returns>
/// <remarks>
/// 这里与 scoped request helper 保持同一组边界约束,但把 scope 生命周期延长到 stream 完整枚举结束,
/// 确保 `Scoped` handler 与依赖不会在首个元素产出前后被提前释放。
/// </remarks>
internal static IAsyncEnumerable<TResponse> CreateScopedMediatRStream<TResponse>(
ServiceProvider rootServiceProvider,
MediatR.IStreamRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(rootServiceProvider);
ArgumentNullException.ThrowIfNull(request);
return EnumerateScopedMediatRStreamAsync(rootServiceProvider, request, cancellationToken);
}
/// <summary> /// <summary>
/// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。 /// 创建承载 NuGet `Mediator` source-generated concrete mediator 的最小对照宿主。
/// </summary> /// </summary>
@ -251,4 +305,47 @@ internal static class BenchmarkHostFactory
interfaceType.IsGenericType && interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == openGenericContract); interfaceType.GetGenericTypeDefinition() == openGenericContract);
} }
/// <summary>
/// 在单个显式作用域内创建并枚举 GFramework.CQRS stream。
/// </summary>
private static async IAsyncEnumerable<TResponse> EnumerateScopedGFrameworkStreamAsync<TResponse>(
MicrosoftDiContainer rootContainer,
ILogger runtimeLogger,
ICqrsContext context,
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<TResponse> request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
using var scope = rootContainer.CreateScope();
var scopedContainer = new ScopedBenchmarkContainer(rootContainer, scope);
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
scopedContainer,
runtimeLogger);
var stream = runtime.CreateStream(context, request, cancellationToken);
await foreach (var response in stream.ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
yield return response;
}
}
/// <summary>
/// 在单个显式作用域内创建并枚举 MediatR stream。
/// </summary>
private static async IAsyncEnumerable<TResponse> EnumerateScopedMediatRStreamAsync<TResponse>(
ServiceProvider rootServiceProvider,
MediatR.IStreamRequest<TResponse> request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
using var scope = rootServiceProvider.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var stream = mediator.CreateStream(request, cancellationToken);
await foreach (var response in stream.ConfigureAwait(false))
{
cancellationToken.ThrowIfCancellationRequested();
yield return response;
}
}
} }

View File

@ -25,9 +25,9 @@ namespace GFramework.Cqrs.Benchmarks.Messaging;
/// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。 /// 对比 stream 在不同 handler 生命周期与观测方式下的额外开销。
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// 当前矩阵覆盖 `Singleton` 与 `Transient`。 /// 当前矩阵覆盖 `Singleton`、`Scoped` 与 `Transient`。
/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起 /// 其中 `Scoped` 会在每次建流与枚举期间显式创建并持有真实的 DI 作用域
/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展 /// 避免把 scoped handler 错误地下沉到根容器解析,或在异步枚举尚未结束时提前释放作用域
/// <see cref="StreamObservation" /> 当前只保留 <see cref="StreamObservation.FirstItem" /> 与 /// <see cref="StreamObservation" /> 当前只保留 <see cref="StreamObservation.FirstItem" /> 与
/// <see cref="StreamObservation.DrainAll" /> 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本, /// <see cref="StreamObservation.DrainAll" /> 两种模式,分别用于观察建流到首个元素的固定成本与完整枚举的总成本,
/// 以避免把更多观测策略与 <see cref="StreamLifetimeBenchmarks" /> 的生命周期对照目标混在一起。 /// 以避免把更多观测策略与 <see cref="StreamLifetimeBenchmarks" /> 的生命周期对照目标混在一起。
@ -45,11 +45,13 @@ public class StreamLifetimeBenchmarks
private ReflectionBenchmarkStreamRequest _reflectionRequest = null!; private ReflectionBenchmarkStreamRequest _reflectionRequest = null!;
private GeneratedBenchmarkStreamRequest _generatedRequest = null!; private GeneratedBenchmarkStreamRequest _generatedRequest = null!;
private MediatRBenchmarkStreamRequest _mediatrRequest = null!; private MediatRBenchmarkStreamRequest _mediatrRequest = null!;
private ILogger _reflectionRuntimeLogger = null!;
private ILogger _generatedRuntimeLogger = null!;
/// <summary> /// <summary>
/// 控制当前 benchmark 使用的 handler 生命周期。 /// 控制当前 benchmark 使用的 handler 生命周期。
/// </summary> /// </summary>
[Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)] [Params(HandlerLifetime.Singleton, HandlerLifetime.Scoped, HandlerLifetime.Transient)]
public HandlerLifetime Lifetime { get; set; } public HandlerLifetime Lifetime { get; set; }
/// <summary> /// <summary>
@ -68,6 +70,11 @@ public class StreamLifetimeBenchmarks
/// </summary> /// </summary>
Singleton, Singleton,
/// <summary>
/// 每次建流在显式作用域内解析并复用 handler 实例,且作用域会覆盖整个枚举周期。
/// </summary>
Scoped,
/// <summary> /// <summary>
/// 每次建流都重新解析新的 handler 实例。 /// 每次建流都重新解析新的 handler 实例。
/// </summary> /// </summary>
@ -122,14 +129,21 @@ public class StreamLifetimeBenchmarks
_reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3); _reflectionRequest = new ReflectionBenchmarkStreamRequest(Guid.NewGuid(), 3);
_generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3); _generatedRequest = new GeneratedBenchmarkStreamRequest(Guid.NewGuid(), 3);
_mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3); _mediatrRequest = new MediatRBenchmarkStreamRequest(Guid.NewGuid(), 3);
_reflectionRuntimeLogger =
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime);
_generatedRuntimeLogger =
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime);
_reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => _reflectionContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{ {
RegisterReflectionHandler(container, Lifetime); RegisterReflectionHandler(container, Lifetime);
}); });
_reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( if (Lifetime != HandlerLifetime.Scoped)
_reflectionContainer, {
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Reflection." + Lifetime)); _reflectionRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_reflectionContainer,
_reflectionRuntimeLogger);
}
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{ {
@ -138,16 +152,22 @@ public class StreamLifetimeBenchmarks
}); });
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线; // 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
// 这里额外创建带生命周期后缀的 runtime只是为了区分不同 benchmark 矩阵的 dispatcher 日志。 // 这里额外创建带生命周期后缀的 runtime只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( if (Lifetime != HandlerLifetime.Scoped)
_generatedContainer, {
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + ".Generated." + Lifetime)); _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,
_generatedRuntimeLogger);
}
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider( _serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null, configure: null,
typeof(StreamLifetimeBenchmarks), typeof(StreamLifetimeBenchmarks),
static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler), static candidateType => candidateType == typeof(MediatRBenchmarkStreamHandler),
ResolveMediatRLifetime(Lifetime)); ResolveMediatRLifetime(Lifetime));
_mediatr = _serviceProvider.GetRequiredService<IMediator>(); if (Lifetime != HandlerLifetime.Scoped)
{
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
}
} }
/// <summary> /// <summary>
@ -181,6 +201,18 @@ public class StreamLifetimeBenchmarks
[Benchmark] [Benchmark]
public ValueTask Stream_GFrameworkReflection() public ValueTask Stream_GFrameworkReflection()
{ {
if (Lifetime == HandlerLifetime.Scoped)
{
return ObserveAsync(
BenchmarkHostFactory.CreateScopedGFrameworkStream(
_reflectionContainer,
_reflectionRuntimeLogger,
BenchmarkContext.Instance,
_reflectionRequest,
CancellationToken.None),
Observation);
}
return ObserveAsync( return ObserveAsync(
_reflectionRuntime.CreateStream( _reflectionRuntime.CreateStream(
BenchmarkContext.Instance, BenchmarkContext.Instance,
@ -195,6 +227,18 @@ public class StreamLifetimeBenchmarks
[Benchmark] [Benchmark]
public ValueTask Stream_GFrameworkGenerated() public ValueTask Stream_GFrameworkGenerated()
{ {
if (Lifetime == HandlerLifetime.Scoped)
{
return ObserveAsync(
BenchmarkHostFactory.CreateScopedGFrameworkStream(
_generatedContainer,
_generatedRuntimeLogger,
BenchmarkContext.Instance,
_generatedRequest,
CancellationToken.None),
Observation);
}
return ObserveAsync( return ObserveAsync(
_generatedRuntime.CreateStream( _generatedRuntime.CreateStream(
BenchmarkContext.Instance, BenchmarkContext.Instance,
@ -209,6 +253,16 @@ public class StreamLifetimeBenchmarks
[Benchmark] [Benchmark]
public ValueTask Stream_MediatR() public ValueTask Stream_MediatR()
{ {
if (Lifetime == HandlerLifetime.Scoped)
{
return ObserveAsync(
BenchmarkHostFactory.CreateScopedMediatRStream(
_serviceProvider,
_mediatrRequest,
CancellationToken.None),
Observation);
}
return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation); return ObserveAsync(_mediatr.CreateStream(_mediatrRequest, CancellationToken.None), Observation);
} }
@ -229,6 +283,12 @@ public class StreamLifetimeBenchmarks
ReflectionBenchmarkStreamHandler>(); ReflectionBenchmarkStreamHandler>();
return; return;
case HandlerLifetime.Scoped:
container.RegisterScoped<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
ReflectionBenchmarkStreamHandler>();
return;
case HandlerLifetime.Transient: case HandlerLifetime.Transient:
container.RegisterTransient< container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>, GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<ReflectionBenchmarkStreamRequest, ReflectionBenchmarkResponse>,
@ -261,6 +321,12 @@ public class StreamLifetimeBenchmarks
GeneratedBenchmarkStreamHandler>(); GeneratedBenchmarkStreamHandler>();
return; return;
case HandlerLifetime.Scoped:
container.RegisterScoped<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
GeneratedBenchmarkStreamHandler>();
return;
case HandlerLifetime.Transient: case HandlerLifetime.Transient:
container.RegisterTransient< container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>, GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<GeneratedBenchmarkStreamRequest, GeneratedBenchmarkResponse>,
@ -282,6 +348,7 @@ public class StreamLifetimeBenchmarks
return lifetime switch return lifetime switch
{ {
HandlerLifetime.Singleton => ServiceLifetime.Singleton, HandlerLifetime.Singleton => ServiceLifetime.Singleton,
HandlerLifetime.Scoped => ServiceLifetime.Scoped,
HandlerLifetime.Transient => ServiceLifetime.Transient, HandlerLifetime.Transient => ServiceLifetime.Transient,
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.") _ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
}; };

View File

@ -17,9 +17,9 @@
- `Messaging/RequestBenchmarks.cs` - `Messaging/RequestBenchmarks.cs`
- direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - direct handler、NuGet `Mediator` source-generated concrete path、已接上 handwritten generated request invoker provider 的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestLifetimeBenchmarks.cs` - `Messaging/RequestLifetimeBenchmarks.cs`
- `Singleton / Transient` 两类 handler 生命周期下direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `Singleton / Scoped / Transient` 三类 handler 生命周期下direct handler、已对齐 generated-provider 宿主接线的默认 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比;其中 `Scoped` 通过真实 request 级作用域宿主执行,不再把 scoped handler 退化为根容器解析
- `Messaging/StreamLifetimeBenchmarks.cs` - `Messaging/StreamLifetimeBenchmarks.cs`
- `Singleton / Transient` 两类 handler 生命周期下direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举分层对照 - `Singleton / Scoped / Transient` 三类 handler 生命周期下direct handler、`GFramework.Cqrs` reflection stream binding、接上 generated stream registry 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 分层对照,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/RequestPipelineBenchmarks.cs` - `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比 - `0 / 1 / 4` 个 pipeline 行为下direct handler、已接上 handwritten generated request invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/RequestStartupBenchmarks.cs` - `Messaging/RequestStartupBenchmarks.cs`
@ -27,13 +27,13 @@
- `Messaging/RequestInvokerBenchmarks.cs` - `Messaging/RequestInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比 - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/StreamInvokerBenchmarks.cs` - `Messaging/StreamInvokerBenchmarks.cs`
- direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 完整枚举对比 - direct handler、`GFramework.Cqrs` reflection runtime、handwritten generated-invoker runtime 与 `MediatR` 的 stream 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
- `Messaging/NotificationBenchmarks.cs` - `Messaging/NotificationBenchmarks.cs`
- `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比 - `GFramework.Cqrs` runtime、NuGet `Mediator` source-generated concrete path 与 `MediatR` 的单处理器 notification publish 对比
- `Messaging/NotificationFanOutBenchmarks.cs` - `Messaging/NotificationFanOutBenchmarks.cs`
- fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比 - fixed `4 handler` notification fan-out 的 baseline、`GFramework.Cqrs` 默认顺序发布器、内置 `TaskWhenAllNotificationPublisher`、NuGet `Mediator` source-generated concrete path 与 `MediatR` publish 对比
- `Messaging/StreamingBenchmarks.cs` - `Messaging/StreamingBenchmarks.cs`
- direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比 - direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 对比,并同时提供 `FirstItem / DrainAll` 两种观测口径
## 最小使用方式 ## 最小使用方式
@ -60,8 +60,10 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容 - `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
- `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径 - `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径
- `RequestLifetimeBenchmarks``Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本
- `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem``DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察 - `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem``DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
- 当前短跑结果显示,`StreamLifetimeBenchmarks``Singleton` 下无论 `FirstItem` 还是 `DrainAll` 都表现为 generated 略优于 reflection`Transient` 下,`FirstItem` 仍是 reflection 略优于 generated`DrainAll` 已转为 generated 优于 reflection。这说明当前差值主要集中在建流到首个元素之间的瞬时成本而不是完整枚举阶段整体退化 - `StreamingBenchmarks``StreamInvokerBenchmarks` 都同时暴露 `FirstItem``DrainAll`;阅读结果时应把它们分别理解为“建流到首个元素”的固定成本观测与“完整枚举整个 stream”的总成本观测
- `StreamInvokerBenchmarks` 当前的 `DrainAll` short-job 输出只适合做 smoke 复核,确认矩阵和路径可以正常跑通;它不应直接写成 reflection、generated 或 `MediatR` 之间的稳定性能结论,若要做排序判断,应复跑默认作业或更完整的 benchmark 批次
- 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主就应至少复跑能覆盖该路径的过滤场景request 热路径通常先看: - 只要变更影响 `GFramework.Cqrs` request dispatch、DI 解析热路径、invoker/provider、pipeline 或 benchmark 宿主就应至少复跑能覆盖该路径的过滤场景request 热路径通常先看:
- `RequestBenchmarks.SendRequest_*` - `RequestBenchmarks.SendRequest_*`
- `RequestLifetimeBenchmarks.SendRequest_*` - `RequestLifetimeBenchmarks.SendRequest_*`
@ -75,5 +77,4 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- 若继续优化 stream lifetime可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次 - 若继续优化 stream lifetime可优先复核 `Transient + FirstItem` 下 generated 与 reflection 的小幅差值是否稳定,再决定继续压 generated 宿主的建流瞬时成本,还是把后续对照切回 `StreamInvokerBenchmarks` / `Mediator` concrete runtime 批次
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照 - request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照 - `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
- 带真实显式作用域边界的 scoped host 对照
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景 - generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景

View File

@ -7,21 +7,23 @@ CQRS 迁移与收敛。
## 当前恢复点 ## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-129` - 恢复点编号:`CQRS-REWRITE-RP-130`
- 当前阶段:`Phase 8` - 当前阶段:`Phase 8`
- 当前 PR 锚点:`待重新抓取` - 当前 PR 锚点:`待重新抓取`
- 当前结论: - 当前结论:
- 当前 `RP-129` 继续沿用 `$gframework-batch-boot 50`,但本轮按 `gframework-multi-agent-batch` 的职责边界组织了一波 `3` 路互不冲突的 benchmark worker`StreamingBenchmarks` 观测口径拆分、`StreamInvokerBenchmarks` 观测口径拆分、`RequestLifetimeBenchmarks` 的真实 scoped-host 生命周期矩阵 - 当前 `RP-130` 延续 `$gframework-batch-boot 50`,并在上一波 `StreamingBenchmarks``StreamInvokerBenchmarks``RequestLifetimeBenchmarks` 扩口径之后,继续把 `StreamLifetimeBenchmarks` 的生命周期矩阵补齐到真实 `Scoped` stream 作用域,同时把 benchmark `README` 与当前矩阵同步
- 本轮启动前已重新按 skill 规则复核基线:`origin/main` 与本地 `main` 当前都在 `699d0b48``2026-05-09 18:39:38 +0800`),且启动时 `origin/main...HEAD` 的累计 branch diff 为 `0 files / 0 lines`;旧 active 入口里 `21 files` 的数字已不再可作为当前波次基线 - 本轮继续沿用已复核的基线:`origin/main` 与本地 `main` 当前都在 `699d0b48``2026-05-09 18:39:38 +0800`);当前分支连同本轮变更相对 `origin/main` 的累计 branch diff 约为 `9 files changed, 953 insertions(+), 83 deletions(-)`,离 `$gframework-batch-boot 50` 还有明显余量
- 本轮写面限定在 benchmark 子系统:`GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs``StreamInvokerBenchmarks.cs``RequestLifetimeBenchmarks.cs``BenchmarkHostFactory.cs`,以及新增的 `ScopedBenchmarkContainer.cs`;没有扩散到 `GFramework.Cqrs` runtime、测试项目或公共文档 - 本轮写面收敛在 benchmark 层三处:`GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs``StreamLifetimeBenchmarks.cs``GFramework.Cqrs.Benchmarks/README.md`;没有扩散到 `GFramework.Cqrs` runtime 或测试项目
- `StreamingBenchmarks` 已从单一完整枚举口径扩成 `FirstItem / DrainAll` 双观测模式worker smoke 结果表明默认 generated-provider steady-state stream 宿主在两种口径下都能稳定跑通,当前 short-job 约为 `FirstItem: baseline 62.14 ns / GFramework 118.83 ns / MediatR 182.16 ns``DrainAll: baseline 94.34 ns / GFramework 149.57 ns / MediatR 280.77 ns` - `BenchmarkHostFactory` 现在提供 `CreateScopedGFrameworkStream<TResponse>(...)``CreateScopedMediatRStream<TResponse>(...)`,通过显式 scope 包裹整个 async stream 枚举周期,避免 scoped handler 在建流后提前释放或错误回落到根容器语义
- `StreamInvokerBenchmarks` 现也具备 `FirstItem / DrainAll` 双观测模式,并已完成串行 smoke 运行;当前 short-job 下,`FirstItem` 口径里 generated lane 约 `59.44 ns`、reflection 约 `52.90 ns`,说明 tracking 里提到的 “generated 在首项前略慢于 reflection” 信号在更窄的 invoker 场景里仍可见 - `StreamLifetimeBenchmarks` 当前矩阵已完整覆盖 `Singleton / Scoped / Transient``FirstItem / DrainAll`
- `RequestLifetimeBenchmarks` 当前矩阵已从 `Singleton / Transient` 扩到 `Singleton / Scoped / Transient`,且 `Scoped` 不再退化为根容器解析,而是通过 `BenchmarkHostFactory` + `ScopedBenchmarkContainer` 在每次 request 分发时显式创建并释放真实作用域;本轮 short-job 下 `Scoped` 口径约为 baseline `5.60 ns / 32 B``MediatR` `170.94 ns / 648 B``GFramework.Cqrs` `575.92 ns / 3400 B` - `Singleton``DrainAll` 仍表现为 generated 略优于 reflection`131.96 ns``134.26 ns`
- 本轮首次并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时触发自动生成目录争用;已按仓库规则改为串行重跑同一命令,并以串行结果作为权威 smoke 验证 - `Scoped` 下已经可以稳定产出真实作用域矩阵;`FirstItem` 约为 baseline `56.24 ns``MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns``DrainAll` 约为 baseline `81.20 ns``MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns`
- 当前可恢复结论收口为两点:一是 stream benchmark 线已从 `StreamLifetimeBenchmarks` 继续下探到 default steady-state 与 invoker 两个更窄口径;二是 request lifetime 线已经拥有真实 scoped-host 基线,后续若继续扩 `StreamLifetimeBenchmarks` 的 scoped 口径,不必再先做宿主适配 - `Transient``FirstItem` 约为 generated `107.79 ns`、reflection `112.60 ns`,但 `DrainAll` 仍是 reflection `131.41 ns` 略优于 generated `135.95 ns`
- `README` 已同步三类 benchmark 现状:`RequestLifetimeBenchmarks``StreamLifetimeBenchmarks` 都包含真实 `Scoped` 生命周期,`StreamingBenchmarks` / `StreamInvokerBenchmarks` 都显式区分 `FirstItem / DrainAll`,且 `StreamInvokerBenchmarks``DrainAll` short-job 输出被明确标注为 smoke-only不作为稳定排序结论
- 本轮再次确认:此前并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时出现的冲突属于 benchmark 工件/生成目录层面的运行隔离问题,而不是 `Fixture``StreamInvokerBenchmarks``StreamLifetimeBenchmarks` 的业务逻辑错误
- 下一推荐步骤: - 下一推荐步骤:
- 先回到 `ai-plan/public/cqrs-rewrite/**``GFramework.Cqrs.Benchmarks/README.md`,把本轮基线从旧的 `21 files / PR #345` 状态更新到当前 `699d0b48` 基线和新的 benchmark 结论 - 下一批优先切到 `GFramework.Cqrs.Benchmarks` 的 benchmark-run/config 隔离层,避免两个过滤 benchmark 并行执行时再次共享自动生成目录或 artifacts 路径
- 然后优先复核 `StreamInvokerBenchmarks` 当前 short-job 输出里 `DrainAll` 口径的异常排序是否只是 smoke 配置噪音,必要时把下一批切回更稳定的 benchmark job 或补更窄的 helper-level 对照,而不是直接据此下结论 - 完成运行隔离后,再决定是否单开一波更稳定的 `StreamInvokerBenchmarks` `DrainAll` 复核,或继续把 scoped host 对照扩展到更多 stream 子场景
- 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准active 入口不再重复展开旧阶段流水。 - 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准active 入口不再重复展开旧阶段流水。
- 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界 - 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界
- 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略 - 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略
@ -157,6 +159,17 @@ CQRS 迁移与收敛。
## 最近权威验证 ## 最近权威验证
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:覆盖 `BenchmarkHostFactory` scoped stream helper、`StreamLifetimeBenchmarks` scoped 生命周期矩阵与 `README` 同步后的最小 Release build
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`StreamLifetimeBenchmarks` 已稳定跑出 `24` 个 case`Scoped + DrainAll` 当前约为 baseline `81.20 ns / 280 B``MediatR` `428.66 ns / 1224 B`、generated `692.05 ns / 3888 B`、reflection `716.61 ns / 3888 B`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
- 结果:通过
- `git --git-dir=<repo>/.git/worktrees/GFramework-cqrs --work-tree=. diff --check`
- 结果:通过
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release` - `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error` - 结果:通过,`0 warning / 0 error`
- 备注:覆盖 `StreamLifetimeBenchmarks` 的代码收口与 benchmark `README` 同步后的最小 Release build - 备注:覆盖 `StreamLifetimeBenchmarks` 的代码收口与 benchmark `README` 同步后的最小 Release build

View File

@ -2,6 +2,31 @@
## 2026-05-11 ## 2026-05-11
### 阶段stream lifetime scoped 矩阵与 README 同步CQRS-REWRITE-RP-130
- 延续 `$gframework-batch-boot 50`,在上一波把 `StreamingBenchmarks``StreamInvokerBenchmarks``RequestLifetimeBenchmarks` 扩成双观测 / scoped-request 基线后,本轮先不回到 runtime而是只补齐 `StreamLifetimeBenchmarks` 的真实 `Scoped` stream 生命周期矩阵,并同步 benchmark `README`
- 本轮主线程验收与修正:
- 接受 worker 留在 `BenchmarkHostFactory.cs` 的 scoped stream helper 方向,但把实现收口为“创建 scope -> 在 scope 内创建 runtime / mediator -> 让 scope 覆盖完整 async stream 枚举周期”的包装迭代器,而不是只在建流阶段短暂持有作用域
- `StreamLifetimeBenchmarks.cs` 现仅在 `Lifetime == Scoped` 时改走新的 scoped stream helper`Singleton / Transient` 仍保持原有最小 steady-state 宿主,不把 scoped 宿主额外成本误混进其他矩阵
- `GFramework.Cqrs.Benchmarks/README.md` 已同步 `RequestLifetimeBenchmarks``StreamLifetimeBenchmarks``Scoped` 现状,并把 `StreamInvokerBenchmarks` `DrainAll` 标成 smoke-only 结论
- 本轮验证:
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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 --no-build -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`24` 个 case 全部跑通;`Scoped + FirstItem` 约为 baseline `56.24 ns``MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns``Scoped + DrainAll` 约为 baseline `81.20 ns``MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns`
- 本轮结论:
- `StreamLifetimeBenchmarks` 现在已经具备与 `RequestLifetimeBenchmarks` 对称的真实 scoped-host 作用域边界,后续不必再先修 benchmark 宿主才能观察 stream scoped lifetime
- `Scoped` 成本当前明显高于 `Singleton / Transient`,说明这条矩阵已经把“真实 scope 生命周期”本身的常量开销带入观测;这正是本轮想要确认的边界,而不是回归或异常
- 本轮并没有改变 `GFramework.Cqrs` runtime 或业务逻辑,只是把 benchmark 宿主语义与文档描述对齐到当前事实
- 下一恢复点:
- 优先切到 benchmark 运行隔离层,只动 `GFramework.Cqrs.Benchmarks/Program.cs` 与必要的 `README` 说明,解决两个过滤 benchmark 并行运行时共享 auto-generated build/artifacts 目录的问题
- 新开的 explorer 已确认根因集中在 `Program.cs` 直接把 `args` 原样交给 `BenchmarkSwitcher.Run(args)`,当前没有为每次运行注入唯一 `ArtifactsPath`;不建议下一批回头改 `Fixture.cs` 或 benchmark 业务逻辑
### 阶段benchmark 多 worker 波次与 scoped-host 基线CQRS-REWRITE-RP-129 ### 阶段benchmark 多 worker 波次与 scoped-host 基线CQRS-REWRITE-RP-129
- 本轮从 `$gframework-batch-boot 50` 启动,但按 `gframework-multi-agent-batch` 规则把非阻塞工作拆成三条互不冲突的 benchmark 切片: - 本轮从 `$gframework-batch-boot 50` 启动,但按 `gframework-multi-agent-batch` 规则把非阻塞工作拆成三条互不冲突的 benchmark 切片: