Merge pull request #341 from GeWuYou/feat/cqrs-optimization

Feat/cqrs optimization
This commit is contained in:
gewuyou 2026-05-08 16:12:20 +08:00 committed by GitHub
commit 7ca21af92d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 1372 additions and 49 deletions

View File

@ -12,6 +12,10 @@ batches until a clear stop condition is met.
Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`; it does not replace it.
Context budget is a first-class stop signal. Do not keep batching merely because a file-count threshold still has
headroom if the active conversation, loaded repo artifacts, validation output, and pending recovery updates suggest the
agent is approaching its safe working-context limit.
## Startup Workflow
1. Execute the normal `gframework-boot` startup sequence first:
@ -28,6 +32,11 @@ Treat `AGENTS.md` as the source of truth. This skill extends `gframework-boot`;
- repeated test refactor pattern
- module-by-module documentation refresh
- other repetitive multi-file cleanup
4. Before the first implementation batch, estimate whether the current task is likely to stay below roughly 80% of the
agent's safe working-context budget through one more full batch cycle:
- include already loaded `AGENTS.md`, skills, `ai-plan` files, recent command output, active diffs, and expected validation output
- if another batch would probably push the conversation near the limit, plan to stop after the current batch even if
branch-size thresholds still have room
## Baseline Selection
@ -67,8 +76,15 @@ For shorthand numeric thresholds, use a fixed default baseline:
Choose one primary stop condition before the first batch and restate it to the user.
When the user does not explicitly override the priority order, use:
1. context-budget safety
2. semantic batch boundary / reviewability
3. the user-requested local metric such as files, lines, warnings, or time
Common stop conditions:
- the next batch would likely push the agent above roughly 80% of its safe working-context budget
- branch diff vs baseline approaches a file-count threshold
- warnings-only build reaches a target count
- a specific hotspot list is exhausted
@ -76,6 +92,9 @@ Common stop conditions:
If multiple stop conditions exist, rank them and treat one as primary.
Treat file-count or line-count thresholds as coarse repository-scope signals, not as a proxy for AI context health.
When they disagree with context-budget safety, context-budget safety wins.
## Shorthand Stop-Condition Syntax
`gframework-batch-boot` may be invoked with shorthand numeric thresholds when the user clearly wants a branch-size stop
@ -108,6 +127,7 @@ When shorthand is used:
- current branch and active topic
- selected baseline
- current stop-condition metric
- current context-budget posture and whether one more batch is safe
- next candidate slices
2. Keep the critical path local.
3. Delegate only bounded slices with explicit ownership:
@ -128,6 +148,7 @@ When shorthand is used:
- integrate or verify the result
- rerun the required validation
- recompute the primary stop-condition metric
- reassess whether one more batch would likely push the agent near or beyond roughly 80% context usage
- decide immediately whether to continue or stop
7. Do not require the user to manually trigger every round unless:
- the next slice is ambiguous
@ -158,6 +179,7 @@ For multi-batch work, keep recovery artifacts current.
Stop the loop when any of the following becomes true:
- the next batch would likely push the agent near or beyond roughly 80% of its safe working-context budget
- the primary stop condition has been reached or exceeded
- the remaining slices are no longer low-risk
- validation failures indicate the task is no longer repetitive
@ -165,6 +187,7 @@ Stop the loop when any of the following becomes true:
When stopping, report:
- whether context budget was the deciding factor
- which baseline was used
- the exact metric value at stop time
- completed batches

View File

@ -36,14 +36,18 @@ Treat `AGENTS.md` as the source of truth. Use this skill to enforce a startup se
- `simple`: one concern, one file or module, no parallel discovery required
- `medium`: a small number of modules, some read-only exploration helpful, critical path still easy to keep local
- `complex`: cross-module design, migration, large refactor, or work likely to exceed one context window
11. Apply the delegation policy from `AGENTS.md`:
11. Estimate the current context-budget posture before substantive execution:
- account for loaded startup artifacts, active `ai-plan` files, visible diffs, open validation output, and likely next-step output volume
- if the task already appears near roughly 80% of a safe working-context budget, prefer closing the current batch,
refreshing recovery artifacts, and stopping at the next natural semantic boundary instead of starting a fresh broad slice
12. Apply the delegation policy from `AGENTS.md`:
- Keep the critical path local
- Use `explorer` with `gpt-5.1-codex-mini` for narrow read-only questions, tracing, inventory, and comparisons
- Use `worker` with `gpt-5.4` only for bounded implementation tasks with explicit ownership
- Do not delegate purely for ceremony; delegate only when it materially shortens the task or controls context growth
12. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
13. Before editing files, tell the user what you read, how you classified the task, whether subagents will be used,
and the first implementation step.
13. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
14. Proceed with execution, validation, and documentation updates required by `AGENTS.md`.
## Task Tracking
@ -69,6 +73,8 @@ For multi-step, cross-module, or interruption-prone work, maintain the repositor
first, then search the mapped active topics before scanning the broader public area.
- If the current branch and the mapped active topics describe the same feature area, prefer resuming those topics first.
- If the repository state suggests in-flight work but no recovery document matches, reconstruct the safest next step from code, tests, and Git state before asking the user for clarification.
- If the current turn already carries heavy recovery context, broad diffs, or long validation output, prefer a
recovery-point update and a clean stop over starting another large slice just because the code task itself remains open.
## Example Triggers

View File

@ -185,6 +185,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
/// </summary>
private IServiceProvider? _provider;
/// <summary>
/// 冻结后可复用的服务类型可见性索引。
/// 容器冻结后注册集合不再变化,因此 <see cref="HasRegistration(Type)" /> 可以安全复用该索引。
/// </summary>
private FrozenServiceTypeIndex? _frozenServiceTypeIndex;
/// <summary>
/// 容器冻结状态标志true表示容器已冻结不可修改
/// </summary>
@ -1044,6 +1050,11 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
EnterReadLockOrThrowDisposed();
try
{
if (_frozenServiceTypeIndex is not null)
{
return _frozenServiceTypeIndex.Contains(type);
}
return HasRegistrationCore(type);
}
finally
@ -1139,6 +1150,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_provider = null;
_frozenServiceTypeIndex = null;
_frozen = false;
_logger.Info("Container cleared");
}
@ -1166,6 +1178,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
_provider = GetServicesUnsafe.BuildServiceProvider();
_frozenServiceTypeIndex = FrozenServiceTypeIndex.Create(GetServicesUnsafe);
_frozen = true;
_logger.Info("IOC Container frozen - ServiceProvider built");
}
@ -1175,6 +1188,59 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
}
}
/// <summary>
/// 保存冻结后按服务键可见的精确服务类型与开放泛型定义集合。
/// </summary>
/// <remarks>
/// 该索引只回答“按当前服务键语义是否可见”,因此与 <see cref="Get(Type)" /> /
/// <see cref="GetAll(Type)" /> 一样不会退化为更宽松的可赋值匹配。
/// </remarks>
private sealed class FrozenServiceTypeIndex(HashSet<Type> exactServiceTypes, HashSet<Type> openGenericServiceTypes)
{
private readonly HashSet<Type> _exactServiceTypes = exactServiceTypes;
private readonly HashSet<Type> _openGenericServiceTypes = openGenericServiceTypes;
/// <summary>
/// 基于冻结时最终确定的服务描述符集合创建索引。
/// </summary>
/// <param name="descriptors">冻结时的服务描述符序列。</param>
/// <returns>供存在性判断热路径复用的服务键索引。</returns>
public static FrozenServiceTypeIndex Create(IEnumerable<ServiceDescriptor> descriptors)
{
ArgumentNullException.ThrowIfNull(descriptors);
var exactServiceTypes = new HashSet<Type>();
var openGenericServiceTypes = new HashSet<Type>();
foreach (var descriptor in descriptors)
{
var serviceType = descriptor.ServiceType;
exactServiceTypes.Add(serviceType);
if (serviceType.IsGenericTypeDefinition)
{
openGenericServiceTypes.Add(serviceType);
}
}
return new FrozenServiceTypeIndex(exactServiceTypes, openGenericServiceTypes);
}
/// <summary>
/// 判断当前索引是否声明了目标服务键。
/// </summary>
/// <param name="requestedType">要检查的服务类型。</param>
/// <returns>命中精确服务键或可闭合的开放泛型服务键时返回 <see langword="true" />。</returns>
public bool Contains(Type requestedType)
{
ArgumentNullException.ThrowIfNull(requestedType);
return _exactServiceTypes.Contains(requestedType) ||
requestedType.IsConstructedGenericType &&
_openGenericServiceTypes.Contains(requestedType.GetGenericTypeDefinition());
}
}
/// <summary>
/// 获取底层的服务集合
/// 提供对内部IServiceCollection的访问权限用于高级配置和自定义操作
@ -1250,6 +1316,7 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null)
_disposed = true;
(_provider as IDisposable)?.Dispose();
_provider = null;
_frozenServiceTypeIndex = null;
GetServicesUnsafe.Clear();
_registeredInstances.Clear();
_frozen = false;

View File

@ -3,10 +3,13 @@
using System;
using System.Linq;
using GFramework.Core.Abstractions.Logging;
using GFramework.Core.Ioc;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Internal;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using LegacyICqrsRuntime = GFramework.Core.Abstractions.Cqrs.ICqrsRuntime;
namespace GFramework.Cqrs.Benchmarks.Messaging;
@ -31,11 +34,91 @@ internal static class BenchmarkHostFactory
ArgumentNullException.ThrowIfNull(configure);
var container = new MicrosoftDiContainer();
RegisterCqrsInfrastructure(container);
configure(container);
container.Freeze();
return container;
}
/// <summary>
/// 为 benchmark 宿主补齐默认 CQRS runtime seam确保它既能手工注册 handler也能走真实的程序集注册入口。
/// </summary>
/// <param name="container">当前 benchmark 拥有的 GFramework 容器。</param>
/// <remarks>
/// `RegisterCqrsHandlersFromAssembly(...)` 依赖预先可见的 runtime / registrar / registration service 实例绑定。
/// benchmark 宿主直接使用裸 <see cref="MicrosoftDiContainer" />,因此需要在配置阶段先补齐这组基础设施,
/// 避免各个 benchmark 用例各自复制同一段前置接线逻辑。
/// </remarks>
private static void RegisterCqrsInfrastructure(MicrosoftDiContainer container)
{
ArgumentNullException.ThrowIfNull(container);
if (container.Get<ICqrsRuntime>() is null)
{
var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher");
var notificationPublisher = container.Get<GFramework.Cqrs.Notification.INotificationPublisher>();
var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher);
container.Register(runtime);
RegisterLegacyRuntimeAlias(container, runtime);
}
else if (container.Get<LegacyICqrsRuntime>() is null)
{
RegisterLegacyRuntimeAlias(container, container.GetRequired<ICqrsRuntime>());
}
if (container.Get<ICqrsHandlerRegistrar>() is null)
{
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
var registrar = GFramework.Cqrs.CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger);
container.Register<ICqrsHandlerRegistrar>(registrar);
}
if (container.Get<ICqrsRegistrationService>() is null)
{
var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService");
var registrar = container.GetRequired<ICqrsHandlerRegistrar>();
var registrationService = GFramework.Cqrs.CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger);
container.Register<ICqrsRegistrationService>(registrationService);
}
}
/// <summary>
/// 只激活当前 benchmark 场景明确拥有的 generated registry避免同一程序集里的其他 benchmark registry
/// 扩大冻结后服务索引与 dispatcher descriptor 基线。
/// </summary>
/// <typeparam name="TRegistry">当前 benchmark 需要接入的 generated registry 类型。</typeparam>
/// <param name="container">承载 generated registry 注册结果的 GFramework benchmark 容器。</param>
internal static void RegisterGeneratedBenchmarkRegistry<TRegistry>(MicrosoftDiContainer container)
where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry
{
ArgumentNullException.ThrowIfNull(container);
var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar");
CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger);
}
/// <summary>
/// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。
/// </summary>
/// <param name="container">承载 runtime 别名的 benchmark 容器。</param>
/// <param name="runtime">当前正式 CQRS runtime 实例。</param>
/// <exception cref="InvalidOperationException">
/// <paramref name="runtime" /> 未同时实现 legacy CQRS runtime 契约。
/// </exception>
private static void RegisterLegacyRuntimeAlias(MicrosoftDiContainer container, ICqrsRuntime runtime)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(runtime);
if (runtime is not LegacyICqrsRuntime legacyRuntime)
{
throw new InvalidOperationException(
$"The registered {typeof(ICqrsRuntime).FullName} must also implement {typeof(LegacyICqrsRuntime).FullName}. Actual runtime type: {runtime.GetType().FullName}.");
}
container.Register<LegacyICqrsRuntime>(legacyRuntime);
}
/// <summary>
/// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。
/// </summary>

View File

@ -0,0 +1,100 @@
// 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;
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultRequestBenchmarkRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为默认 request steady-state benchmark 提供 hand-written generated registry
/// 以便验证“默认宿主吸收 generated request invoker provider”后的热路径收益。
/// </summary>
public sealed class GeneratedDefaultRequestBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsRequestInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
new(
typeof(IRequestHandler<
RequestBenchmarks.BenchmarkRequest,
RequestBenchmarks.BenchmarkResponse>),
typeof(GeneratedDefaultRequestBenchmarkRegistry).GetMethod(
nameof(InvokeBenchmarkRequestHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated default request benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
typeof(RequestBenchmarks.BenchmarkRequest),
typeof(RequestBenchmarks.BenchmarkResponse),
Descriptor)
];
/// <summary>
/// 把默认 request benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
/// </summary>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddSingleton(
typeof(IRequestHandler<RequestBenchmarks.BenchmarkRequest, RequestBenchmarks.BenchmarkResponse>),
typeof(RequestBenchmarks.BenchmarkRequestHandler));
logger.Debug("Registered generated default request benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
/// </summary>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
{
if (requestType == typeof(RequestBenchmarks.BenchmarkRequest) &&
responseType == typeof(RequestBenchmarks.BenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated invoker provider 为默认 request benchmark 产出的开放静态调用入口。
/// </summary>
public static ValueTask<RequestBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
object handler,
object request,
CancellationToken cancellationToken)
{
var typedHandler = (IRequestHandler<
RequestBenchmarks.BenchmarkRequest,
RequestBenchmarks.BenchmarkResponse>)handler;
var typedRequest = (RequestBenchmarks.BenchmarkRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -0,0 +1,96 @@
// 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;
/// <summary>
/// 为默认 stream steady-state benchmark 提供 hand-written generated registry
/// 以便验证“默认 stream 宿主吸收 generated stream invoker provider”后的完整枚举收益。
/// </summary>
public sealed class GeneratedDefaultStreamingBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsStreamInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
new(
typeof(IStreamRequestHandler<
StreamingBenchmarks.BenchmarkStreamRequest,
StreamingBenchmarks.BenchmarkResponse>),
typeof(GeneratedDefaultStreamingBenchmarkRegistry).GetMethod(
nameof(InvokeBenchmarkStreamHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated default streaming benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
typeof(StreamingBenchmarks.BenchmarkStreamRequest),
typeof(StreamingBenchmarks.BenchmarkResponse),
Descriptor)
];
/// <summary>
/// 把默认 stream benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。
/// </summary>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddSingleton(
typeof(IStreamRequestHandler<StreamingBenchmarks.BenchmarkStreamRequest, StreamingBenchmarks.BenchmarkResponse>),
typeof(StreamingBenchmarks.BenchmarkStreamHandler));
logger.Debug("Registered generated default streaming benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
/// </summary>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
{
if (requestType == typeof(StreamingBenchmarks.BenchmarkStreamRequest) &&
responseType == typeof(StreamingBenchmarks.BenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated stream invoker provider 为默认 stream benchmark 产出的开放静态调用入口。
/// </summary>
public static object InvokeBenchmarkStreamHandler(
object handler,
object request,
CancellationToken cancellationToken)
{
var typedHandler = (IStreamRequestHandler<
StreamingBenchmarks.BenchmarkStreamRequest,
StreamingBenchmarks.BenchmarkResponse>)handler;
var typedRequest = (StreamingBenchmarks.BenchmarkStreamRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -0,0 +1,100 @@
// 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;
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedRequestPipelineBenchmarkRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 request pipeline benchmark 提供 handwritten generated registry
/// 让默认 pipeline 宿主也能走真实的 generated request invoker provider 接线路径。
/// </summary>
public sealed class GeneratedRequestPipelineBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsRequestInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsRequestInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsRequestInvokerDescriptor Descriptor =
new(
typeof(IRequestHandler<
RequestPipelineBenchmarks.BenchmarkRequest,
RequestPipelineBenchmarks.BenchmarkResponse>),
typeof(GeneratedRequestPipelineBenchmarkRegistry).GetMethod(
nameof(InvokeBenchmarkRequestHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated request pipeline benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry(
typeof(RequestPipelineBenchmarks.BenchmarkRequest),
typeof(RequestPipelineBenchmarks.BenchmarkResponse),
Descriptor)
];
/// <summary>
/// 将 request pipeline benchmark handler 注册为单例,保持与当前矩阵宿主一致的生命周期语义。
/// </summary>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
services.AddSingleton(
typeof(IRequestHandler<RequestPipelineBenchmarks.BenchmarkRequest, RequestPipelineBenchmarks.BenchmarkResponse>),
typeof(RequestPipelineBenchmarks.BenchmarkRequestHandler));
logger.Debug("Registered generated request pipeline benchmark handler.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated request invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标请求/响应类型对返回 generated request invoker 描述符。
/// </summary>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsRequestInvokerDescriptor? descriptor)
{
if (requestType == typeof(RequestPipelineBenchmarks.BenchmarkRequest) &&
responseType == typeof(RequestPipelineBenchmarks.BenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated invoker provider 为 request pipeline benchmark 产出的开放静态调用入口。
/// </summary>
public static ValueTask<RequestPipelineBenchmarks.BenchmarkResponse> InvokeBenchmarkRequestHandler(
object handler,
object request,
CancellationToken cancellationToken)
{
var typedHandler = (IRequestHandler<
RequestPipelineBenchmarks.BenchmarkRequest,
RequestPipelineBenchmarks.BenchmarkResponse>)handler;
var typedRequest = (RequestPipelineBenchmarks.BenchmarkRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -0,0 +1,110 @@
// 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;
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedStreamLifetimeBenchmarkRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
/// 为 stream 生命周期矩阵 benchmark 提供 hand-written generated registry
/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的完整枚举成本。
/// </summary>
public sealed class GeneratedStreamLifetimeBenchmarkRegistry :
GFramework.Cqrs.ICqrsHandlerRegistry,
GFramework.Cqrs.ICqrsStreamInvokerProvider,
GFramework.Cqrs.IEnumeratesCqrsStreamInvokerDescriptors
{
private static readonly GFramework.Cqrs.CqrsStreamInvokerDescriptor Descriptor =
new(
typeof(IStreamRequestHandler<
StreamLifetimeBenchmarks.BenchmarkStreamRequest,
StreamLifetimeBenchmarks.BenchmarkResponse>),
typeof(GeneratedStreamLifetimeBenchmarkRegistry).GetMethod(
nameof(InvokeBenchmarkStreamHandler),
BindingFlags.Public | BindingFlags.Static)
?? throw new InvalidOperationException("Missing generated stream lifetime benchmark method."));
private static readonly IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> Descriptors =
[
new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry(
typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest),
typeof(StreamLifetimeBenchmarks.BenchmarkResponse),
Descriptor)
];
/// <summary>
/// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。
/// </summary>
/// <param name="services">当前 generated registry 拥有的服务集合。</param>
/// <param name="logger">用于记录 generated registry 注册行为的日志器。</param>
/// <remarks>
/// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。
/// 因此 registry 只负责暴露 generated descriptor不在这里抢先注册 handler避免把默认单例注册混入比较结果。
/// </remarks>
public void Register(IServiceCollection services, ILogger logger)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(logger);
logger.Debug("Registered generated stream lifetime benchmark descriptors.");
}
/// <summary>
/// 返回当前 provider 暴露的全部 generated stream invoker 描述符。
/// </summary>
public IReadOnlyList<GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry> GetDescriptors()
{
return Descriptors;
}
/// <summary>
/// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。
/// </summary>
/// <param name="requestType">待匹配的请求类型。</param>
/// <param name="responseType">待匹配的响应类型。</param>
/// <param name="descriptor">命中时返回的 generated descriptor。</param>
/// <returns>命中当前 benchmark 的请求/响应类型对时返回 <see langword="true" />。</returns>
public bool TryGetDescriptor(
Type requestType,
Type responseType,
out GFramework.Cqrs.CqrsStreamInvokerDescriptor? descriptor)
{
if (requestType == typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest) &&
responseType == typeof(StreamLifetimeBenchmarks.BenchmarkResponse))
{
descriptor = Descriptor;
return true;
}
descriptor = null;
return false;
}
/// <summary>
/// 模拟 generated stream invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。
/// </summary>
/// <param name="handler">当前请求对应的 handler 实例。</param>
/// <param name="request">待分发的流式请求。</param>
/// <param name="cancellationToken">调用方传入的取消令牌。</param>
/// <returns>交给目标 stream handler 处理后的异步枚举。</returns>
public static object InvokeBenchmarkStreamHandler(
object handler,
object request,
CancellationToken cancellationToken)
{
var typedHandler = (IStreamRequestHandler<
StreamLifetimeBenchmarks.BenchmarkStreamRequest,
StreamLifetimeBenchmarks.BenchmarkResponse>)handler;
var typedRequest = (StreamLifetimeBenchmarks.BenchmarkStreamRequest)request;
return typedHandler.Handle(typedRequest, cancellationToken);
}
}

View File

@ -61,12 +61,12 @@ public class RequestBenchmarks
MinLevel = LogLevel.Fatal
};
Fixture.Setup("Request", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
_baselineHandler);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultRequestBenchmarkRegistry>(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
@ -91,7 +91,14 @@ public class RequestBenchmarks
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
try
{
BenchmarkCleanupHelper.DisposeAll(_container, _mediatrServiceProvider, _mediatorServiceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>

View File

@ -83,7 +83,7 @@ public class RequestInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestInvokerBenchmarkRegistry>(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,

View File

@ -69,8 +69,7 @@ public class RequestPipelineBenchmarks
_baselineHandler = new BenchmarkRequestHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IRequestHandler<BenchmarkRequest, BenchmarkResponse>>(
_baselineHandler);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedRequestPipelineBenchmarkRegistry>(container);
RegisterGFrameworkPipelineBehaviors(container, PipelineCount);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(

View File

@ -83,7 +83,7 @@ public class StreamInvokerBenchmarks
_generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamInvokerBenchmarkRegistry>(container);
});
_generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_generatedContainer,

View File

@ -0,0 +1,279 @@
// 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;
/// <summary>
/// 对比 stream 完整枚举在不同 handler 生命周期下的额外开销。
/// </summary>
/// <remarks>
/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。
/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起,
/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。
/// </remarks>
[Config(typeof(Config))]
public class StreamLifetimeBenchmarks
{
private MicrosoftDiContainer _container = null!;
private ICqrsRuntime _runtime = null!;
private ServiceProvider _serviceProvider = null!;
private IMediator _mediatr = null!;
private BenchmarkStreamHandler _baselineHandler = null!;
private BenchmarkStreamRequest _request = null!;
/// <summary>
/// 控制当前 benchmark 使用的 handler 生命周期。
/// </summary>
[Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)]
public HandlerLifetime Lifetime { get; set; }
/// <summary>
/// 可公平比较的 benchmark handler 生命周期集合。
/// </summary>
public enum HandlerLifetime
{
/// <summary>
/// 复用单个 handler 实例。
/// </summary>
Singleton,
/// <summary>
/// 每次建流都重新解析新的 handler 实例。
/// </summary>
Transient
}
/// <summary>
/// 配置 stream 生命周期 benchmark 的公共输出格式。
/// </summary>
private sealed class Config : ManualConfig
{
public Config()
{
AddJob(Job.Default);
AddColumnProvider(DefaultColumnProviders.Instance);
AddColumn(new CustomColumn("Scenario", static (_, _) => "StreamLifetime"));
AddDiagnoser(MemoryDiagnoser.Default);
WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Declared));
}
}
/// <summary>
/// 构建当前生命周期下的 GFramework 与 MediatR stream 对照宿主。
/// </summary>
[GlobalSetup]
public void Setup()
{
LoggerFactoryResolver.Provider = new ConsoleLoggerFactoryProvider
{
MinLevel = LogLevel.Fatal
};
Fixture.Setup($"StreamLifetime/{Lifetime}", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkStreamHandler();
_request = new BenchmarkStreamRequest(Guid.NewGuid(), 3);
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedStreamLifetimeBenchmarkRegistry>(container);
RegisterGFrameworkHandler(container, Lifetime);
});
// 容器内已提前保留默认 runtime 以支撑 generated registry 接线;
// 这里额外创建带生命周期后缀的 runtime只是为了区分不同 benchmark 矩阵的 dispatcher 日志。
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
LoggerFactoryResolver.Provider.CreateLogger(nameof(StreamLifetimeBenchmarks) + "." + Lifetime));
_serviceProvider = BenchmarkHostFactory.CreateMediatRServiceProvider(
configure: null,
typeof(StreamLifetimeBenchmarks),
static candidateType => candidateType == typeof(BenchmarkStreamHandler),
ResolveMediatRLifetime(Lifetime));
_mediatr = _serviceProvider.GetRequiredService<IMediator>();
}
/// <summary>
/// 释放当前生命周期矩阵持有的 benchmark 宿主资源,并清理 dispatcher 缓存。
/// </summary>
[GlobalCleanup]
public void Cleanup()
{
try
{
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>
/// 直接调用 handler 并完整枚举,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。
/// </summary>
[Benchmark(Baseline = true)]
public async ValueTask Stream_Baseline()
{
await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false))
{
_ = response;
}
}
/// <summary>
/// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。
/// </summary>
[Benchmark]
public async ValueTask Stream_GFrameworkCqrs()
{
await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None)
.ConfigureAwait(false))
{
_ = response;
}
}
/// <summary>
/// 通过 MediatR 创建并完整枚举 stream作为外部对照。
/// </summary>
[Benchmark]
public async ValueTask Stream_MediatR()
{
await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false))
{
_ = response;
}
}
/// <summary>
/// 按生命周期把 benchmark stream handler 注册到 GFramework 容器。
/// </summary>
/// <param name="container">当前 benchmark 拥有并负责释放的容器。</param>
/// <param name="lifetime">待比较的 handler 生命周期。</param>
/// <remarks>
/// 先通过 generated registry 提供静态 descriptor再显式覆盖 handler 生命周期,
/// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。
/// </remarks>
private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime)
{
ArgumentNullException.ThrowIfNull(container);
switch (lifetime)
{
case HandlerLifetime.Singleton:
container.RegisterSingleton<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
BenchmarkStreamHandler>();
return;
case HandlerLifetime.Transient:
container.RegisterTransient<
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
BenchmarkStreamHandler>();
return;
default:
throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.");
}
}
/// <summary>
/// 将 benchmark 生命周期映射为 MediatR 组装所需的 <see cref="ServiceLifetime" />。
/// </summary>
/// <param name="lifetime">待比较的 handler 生命周期。</param>
/// <returns>当前生命周期对应的 MediatR 注册方式。</returns>
private static ServiceLifetime ResolveMediatRLifetime(HandlerLifetime lifetime)
{
return lifetime switch
{
HandlerLifetime.Singleton => ServiceLifetime.Singleton,
HandlerLifetime.Transient => ServiceLifetime.Transient,
_ => throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime.")
};
}
/// <summary>
/// Benchmark stream request。
/// </summary>
/// <param name="Id">请求标识。</param>
/// <param name="ItemCount">返回元素数量。</param>
public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) :
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest<BenchmarkResponse>,
MediatR.IStreamRequest<BenchmarkResponse>;
/// <summary>
/// Benchmark stream response。
/// </summary>
/// <param name="Id">响应标识。</param>
public sealed record BenchmarkResponse(Guid Id);
/// <summary>
/// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。
/// </summary>
public sealed class BenchmarkStreamHandler :
GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>,
MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>
{
/// <summary>
/// 处理 GFramework.CQRS stream request。
/// </summary>
/// <param name="request">当前 benchmark stream 请求。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
public IAsyncEnumerable<BenchmarkResponse> Handle(
BenchmarkStreamRequest request,
CancellationToken cancellationToken)
{
return EnumerateAsync(request, cancellationToken);
}
/// <summary>
/// 处理 MediatR stream request。
/// </summary>
/// <param name="request">当前 benchmark stream 请求。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>完整枚举所需的低噪声异步响应序列。</returns>
IAsyncEnumerable<BenchmarkResponse> MediatR.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>.Handle(
BenchmarkStreamRequest request,
CancellationToken cancellationToken)
{
return EnumerateAsync(request, cancellationToken);
}
/// <summary>
/// 为生命周期矩阵构造稳定、低噪声的异步响应序列。
/// </summary>
/// <param name="request">当前 benchmark 请求。</param>
/// <param name="cancellationToken">用于中断异步枚举的取消令牌。</param>
/// <returns>按固定元素数量返回的异步响应序列。</returns>
private static async IAsyncEnumerable<BenchmarkResponse> EnumerateAsync(
BenchmarkStreamRequest request,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
for (var index = 0; index < request.ItemCount; index++)
{
cancellationToken.ThrowIfCancellationRequested();
yield return new BenchmarkResponse(request.Id);
await Task.CompletedTask.ConfigureAwait(false);
}
}
}
}

View File

@ -18,6 +18,9 @@ using GFramework.Cqrs.Abstractions.Cqrs;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
[assembly: GFramework.Cqrs.CqrsHandlerRegistryAttribute(
typeof(GFramework.Cqrs.Benchmarks.Messaging.GeneratedDefaultStreamingBenchmarkRegistry))]
namespace GFramework.Cqrs.Benchmarks.Messaging;
/// <summary>
@ -59,12 +62,12 @@ public class StreamingBenchmarks
MinLevel = LogLevel.Fatal
};
Fixture.Setup("StreamRequest", handlerCount: 1, pipelineCount: 0);
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
_baselineHandler = new BenchmarkStreamHandler();
_container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container =>
{
container.RegisterSingleton<GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler<BenchmarkStreamRequest, BenchmarkResponse>>(
_baselineHandler);
BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry<GeneratedDefaultStreamingBenchmarkRegistry>(container);
});
_runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(
_container,
@ -86,7 +89,14 @@ public class StreamingBenchmarks
[GlobalCleanup]
public void Cleanup()
{
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
try
{
BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider);
}
finally
{
BenchmarkDispatcherCacheHelper.ClearDispatcherCaches();
}
}
/// <summary>

View File

@ -15,11 +15,13 @@
- `Messaging/Fixture.cs`
- 运行前输出并校验场景配置
- `Messaging/RequestBenchmarks.cs`
- direct handler、NuGet `Mediator` source-generated concrete path、`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`
- `Singleton / Transient` 两类 handler 生命周期下direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 request steady-state dispatch 对比
- `Messaging/StreamLifetimeBenchmarks.cs`
- `Singleton / Transient` 两类 handler 生命周期下direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream 完整枚举对比
- `Messaging/RequestPipelineBenchmarks.cs`
- `0 / 1 / 4` 个 pipeline 行为下direct handler、`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`
- `Initialization``ColdStart` 两组 request startup 成本对比,补齐与 `Mediator` comparison benchmark 更接近的 startup 维度
- `Messaging/RequestInvokerBenchmarks.cs`
@ -29,7 +31,7 @@
- `Messaging/NotificationBenchmarks.cs`
- `GFramework.Cqrs` runtime 与 `MediatR` 的单处理器 notification publish 对比
- `Messaging/StreamingBenchmarks.cs`
- direct handler、`GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比
- direct handler、已接上 handwritten generated stream invoker provider 的 `GFramework.Cqrs` runtime 与 `MediatR` 的 stream request 完整枚举对比
## 最小使用方式
@ -51,6 +53,5 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
- request / stream 的真实 source-generator 产物与 handwritten generated provider 对照
- `Mediator` 的 transient / scoped compile-time lifetime 矩阵对照
- stream handler 生命周期矩阵
- 带真实显式作用域边界的 scoped host 对照
- generated invoker provider 与纯反射 dispatch / 建流对比继续扩展到更多场景

View File

@ -43,6 +43,63 @@ internal sealed class CqrsDispatcherContextValidationTests
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证 request 上下文校验失败时,<see cref="GFramework.Cqrs.Abstractions.Cqrs.ICqrsRuntime.SendAsync{TResponse}" />
/// 不会在调用点同步抛出,而是返回一个 faulted <see cref="ValueTask{TResult}" /> 保持既有异步失败语义。
/// </summary>
[Test]
public void SendAsync_Should_Return_Faulted_ValueTask_When_Context_Preparation_Fails()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
.Returns(new ContextAwareRequestHandler());
container
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
.Returns(false);
});
ValueTask<int> dispatch = default;
Assert.That(
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
Throws.Nothing);
Assert.That(
async () => await dispatch.ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext"));
}
/// <summary>
/// 验证 request handler 缺失时dispatcher 仍返回 faulted <see cref="ValueTask{TResult}" />
/// 而不是在调用点同步抛出异常。
/// </summary>
[Test]
public void SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()
{
var runtime = CreateRuntime(
container =>
{
container
.Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler<ContextAwareRequest, int>)))
.Returns((object?)null);
container
.Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
.Returns(false);
container
.Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior<ContextAwareRequest, int>)))
.Returns(Array.Empty<object>());
});
ValueTask<int> dispatch = default;
Assert.That(
() => { dispatch = runtime.SendAsync(new FakeCqrsContext(), new ContextAwareRequest()); },
Throws.Nothing);
Assert.That(
async () => await dispatch.ConfigureAwait(false),
Throws.InvalidOperationException.With.Message.Contains("No CQRS request handler registered"));
}
/// <summary>
/// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 <see cref="GFramework.Core.Abstractions.Architectures.IArchitectureContext" /> 时,
/// dispatcher 会在发布前显式失败。

View File

@ -7,6 +7,7 @@ using GFramework.Core.Architectures;
using GFramework.Core.Ioc;
using GFramework.Core.Logging;
using GFramework.Cqrs.Abstractions.Cqrs;
using GFramework.Cqrs.Internal;
namespace GFramework.Cqrs.Tests.Cqrs;
@ -99,6 +100,32 @@ internal sealed class CqrsGeneratedRequestInvokerProviderTests
Is.EqualTo([typeof(GeneratedStreamInvokerProviderRegistry)]));
}
/// <summary>
/// 验证 direct generated-registry 激活入口只会接入指定 registry而不会顺手把同一测试程序集里的其他 registry 一并注册。
/// </summary>
[Test]
public void RegisterGeneratedRegistry_Should_Register_Only_The_Selected_Provider()
{
var container = new MicrosoftDiContainer();
var logger = LoggerFactoryResolver.Provider.CreateLogger(nameof(CqrsGeneratedRequestInvokerProviderTests));
CqrsHandlerRegistrar.RegisterGeneratedRegistry(
container,
typeof(GeneratedRequestInvokerProviderRegistry),
logger);
var requestProviders = container.GetAll<ICqrsRequestInvokerProvider>();
var streamProviders = container.GetAll<ICqrsStreamInvokerProvider>();
Assert.Multiple(() =>
{
Assert.That(
requestProviders.Select(static provider => provider.GetType()),
Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)]));
Assert.That(streamProviders, Is.Empty);
});
}
/// <summary>
/// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时,
/// registrar 仍会把 generated stream invoker provider 注册到容器中。

View File

@ -105,36 +105,43 @@ internal sealed class CqrsDispatcher(
/// <param name="request">请求对象。</param>
/// <param name="cancellationToken">取消令牌。</param>
/// <returns>请求响应。</returns>
public async ValueTask<TResponse> SendAsync<TResponse>(
public ValueTask<TResponse> SendAsync<TResponse>(
ICqrsContext context,
IRequest<TResponse> request,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
var requestType = request.GetType();
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException(
$"No CQRS request handler registered for {requestType.FullName}.");
PrepareHandler(handler, context);
if (!container.HasRegistration(dispatchBinding.BehaviorType))
try
{
return await dispatchBinding.RequestInvoker(handler, request, cancellationToken).ConfigureAwait(false);
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(request);
var requestType = request.GetType();
var dispatchBinding = GetRequestDispatchBinding<TResponse>(requestType);
var handler = container.Get(dispatchBinding.HandlerType)
?? throw new InvalidOperationException(
$"No CQRS request handler registered for {requestType.FullName}.");
PrepareHandler(handler, context);
if (!container.HasRegistration(dispatchBinding.BehaviorType))
{
return dispatchBinding.RequestInvoker(handler, request, cancellationToken);
}
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
foreach (var behavior in behaviors)
{
PrepareHandler(behavior, context);
}
return dispatchBinding.GetPipelineExecutor(behaviors.Count)
.Invoke(handler, behaviors, request, cancellationToken);
}
var behaviors = container.GetAll(dispatchBinding.BehaviorType);
foreach (var behavior in behaviors)
catch (Exception exception)
{
PrepareHandler(behavior, context);
// 保留旧 async 实现的 faulted-ValueTask 失败语义,同时继续复用 direct-return 的热路径。
return ValueTask.FromException<TResponse>(exception);
}
return await dispatchBinding.GetPipelineExecutor(behaviors.Count)
.Invoke(handler, behaviors, request, cancellationToken)
.ConfigureAwait(false);
}
/// <summary>

View File

@ -68,6 +68,36 @@ internal static class CqrsHandlerRegistrar
}
}
/// <summary>
/// 直接激活并注册单个 generated registry避免调用方为了只接入一个 benchmark registry
/// 而额外扫描同一程序集里的其他 registry / handler。
/// </summary>
/// <param name="container">承载 generated registry 注册结果的目标容器。</param>
/// <param name="registryType">要直接激活的 generated registry 类型。</param>
/// <param name="logger">当前注册过程使用的日志记录器。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="container" />、<paramref name="registryType" /> 或 <paramref name="logger" /> 为 <see langword="null" />。
/// </exception>
/// <exception cref="InvalidOperationException">指定 registry 类型不满足 generated registry 运行时契约。</exception>
internal static void RegisterGeneratedRegistry(
IIocContainer container,
Type registryType,
ILogger logger)
{
ArgumentNullException.ThrowIfNull(container);
ArgumentNullException.ThrowIfNull(registryType);
ArgumentNullException.ThrowIfNull(logger);
var assemblyName = GetAssemblySortKey(registryType.Assembly);
if (!TryCreateGeneratedRegistry(registryType, assemblyName, logger, out var registry))
{
throw new InvalidOperationException(
$"Unable to activate generated CQRS handler registry {registryType.FullName} in assembly {assemblyName}.");
}
RegisterGeneratedRegistries(container.GetServicesUnsafe, [registry], assemblyName, logger);
}
/// <summary>
/// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。
/// </summary>

View File

@ -0,0 +1,7 @@
// Copyright (c) 2025-2026 GeWuYou
// SPDX-License-Identifier: Apache-2.0
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GFramework.Cqrs.Tests")]
[assembly: InternalsVisibleTo("GFramework.Cqrs.Benchmarks")]

View File

@ -7,10 +7,12 @@ CQRS 迁移与收敛。
## 当前恢复点
- 恢复点编号:`CQRS-REWRITE-RP-103`
- 恢复点编号:`CQRS-REWRITE-RP-110`
- 当前阶段:`Phase 8`
- 当前 PR 锚点:`PR #340`
- 当前 PR 锚点:`PR #341`
- 当前结论:
- 当前 `RP-110` 已再次使用 `$gframework-pr-review` 复核 `PR #341` latest-head review`BenchmarkHostFactory` 的 legacy runtime alias 防守式类型检查、benchmark 宿主定向 generated registry 激活、以及 `CqrsDispatcher.SendAsync(...)` 的 faulted `ValueTask` 失败语义在当前 head 均已实质收口;本轮仅继续接受仍然成立的 CodeRabbit nitpick`SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()` 补齐 `HasRegistration(...)` / `GetAll(...)` 防御性 mock并删除 trace 中重复 `本轮权威验证``本轮下一步` 段落
- 当前 `RP-109` 已使用 `$gframework-pr-review` 复核 `PR #341` latest-head reviewbenchmark 宿主改为定向激活当前场景的 generated registry避免同一 benchmark 程序集里的其他 registry 扩大冻结服务索引与 `HasRegistration` 基线;`BenchmarkHostFactory` 为 legacy runtime alias 注册补齐防守式类型检查与 stream lifetime 运行时注释;`CqrsDispatcher.SendAsync(...)` 在保留 direct-return 热路径的同时恢复 faulted `ValueTask` 失败语义,并补齐 generated registry 定向接线与 request fault 语义回归测试;`.agents/skills/gframework-batch-boot/SKILL.md` 的 MD005 缩进也已顺手修正
- `GFramework.Cqrs` 已完成对外部 `Mediator` 的生产级替代,当前主线已从“是否可替代”转向“仓库内部收口与能力深化顺序”
- `dispatch/invoker` 生成前移已扩展到 request / stream 路径,`RP-077` 已补齐 request invoker provider gate 与 stream gate 对称的 descriptor / descriptor entry runtime 合同回归
- `RP-078` 已补齐 mixed fallback metadata 在 runtime 不允许多个 fallback attribute 实例时的单字符串 attribute 回退回归
@ -40,23 +42,38 @@ CQRS 迁移与收敛。
- 当前 `RP-102` 已把 `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 收口后的新基线
- 当前 `RP-102` 已将 `BenchmarkDotNet.Artifacts/` 收口为默认忽略路径,并把 request steady-state / lifetime benchmark 复跑升级为 CQRS 性能相关改动的默认回归门槛;当前阶段目标明确为“持续逼近 source-generated `Mediator`,并至少稳定超过反射版 `MediatR`
- 当前 `RP-103` 已使用 `$gframework-pr-review` 复核 `PR #340` latest-head review修复 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext` 因 strict mock 未配置 `HasRegistration(Type)` 产生的 CI 失败,收紧 `MicrosoftDiContainer.HasRegistration(Type)` 到与 `GetAll(Type)` 一致的服务键可见性语义,补齐 `IIocContainer.HasRegistration(Type)` 的异常/XML 契约与 `docs/zh-CN/core/ioc.md` 的用户接入说明,并同步 benchmark 注释与 active tracking/trace 到当前 PR 锚点
- `ai-plan` active 入口现以 `RP-103` 为最新恢复锚点;`PR #340``PR #339``PR #334``PR #331``PR #326``PR #323``PR #307` 与其他更早阶段细节均以下方归档或说明为准
- 当前 `RP-104` 已继续沿用 `$gframework-batch-boot 50` 压 request 热路径:先把 `CqrsDispatcher.SendAsync(...)` 改成 direct-return `ValueTask`,移除 dispatcher 自身的 `async/await` 状态机;再让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,避免每次命中零 pipeline request 都线性扫描全部描述符;本轮 benchmark 表明第一刀显著压低 steady-state / lifetime request第二刀在当前短跑下主要确认“无回退、收益不明显”
- 当前 `RP-105` 已继续沿用 `$gframework-batch-boot 50` 压默认 request steady-state为 benchmark 最小宿主补齐 CQRS runtime / registrar / registration service 基础设施,让 `RequestBenchmarks` 不再只测反射路径,而是通过 handwritten generated registry + `RegisterCqrsHandlersFromAssembly(...)` 真实接上 generated request invoker provider本轮 benchmark 表明默认 request 路径进一步从约 `70.298 ns / 32 B` 压到约 `65.296 ns / 32 B``Singleton / Transient` lifetime 也同步收敛到约 `68.772 ns / 32 B``73.157 ns / 56 B`
- 当前 `RP-106` 已把同一套 generated-provider 宿主收口扩展到 `RequestPipelineBenchmarks`:新增 handwritten `GeneratedRequestPipelineBenchmarkRegistry`,并让 `RequestPipelineBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` + benchmark CQRS 基础设施预接线;本轮 benchmark 表明 `0 pipeline` steady-state 进一步收敛到约 `64.755 ns / 32 B``1 pipeline``353.141 ns / 536 B``4 pipeline` 在短跑噪音下维持约 `555.083 ns / 896 B`
- 当前 `RP-107` 已把默认 stream steady-state 宿主也切到 generated-provider 路径:新增 handwritten `GeneratedDefaultStreamingBenchmarkRegistry`,让 `StreamingBenchmarks` 改走 `RegisterCqrsHandlersFromAssembly(...)` 并在 setup/cleanup 清理 dispatcher cache同时将 `gframework-boot` / `gframework-batch-boot` 的默认停止规则改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”,不再把 changed files 误当作唯一阈值
- 当前 `RP-108` 已补齐 stream handler `Singleton / Transient` 生命周期矩阵 benchmark新增 `StreamLifetimeBenchmarks``GeneratedStreamLifetimeBenchmarkRegistry`,让 stream 生命周期对照沿用 generated-provider 宿主接线而不是退回纯反射路径;本轮 benchmark 表明 `Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR``80.144 ns / 137.515 ns / 229.242 ns``Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- `ai-plan` active 入口现以 `RP-108` 为最新恢复锚点;`PR #340``PR #339``PR #334``PR #331``PR #326``PR #323``PR #307` 与其他更早阶段细节均以下方归档或说明为准
## 当前活跃事实
- 当前分支为 `feat/cqrs-optimization`
- 本轮 `$gframework-batch-boot 50``origin/main` (`5dc2dd25`, 2026-05-08 09:08:37 +0800) 为基线;本地 `main` (`c2d22285`) 已落后,不作为 branch diff 基线
- 当前分支相对 `origin/main` 的累计 branch diff 仍为 `10 files / 298 lines`;本轮待提交工作树以 `.gitignore`、benchmark README 与 active tracking/trace 更新为主,仍明显低于 `$gframework-batch-boot 50` 的文件阈值
- 本轮 `$gframework-batch-boot 50``origin/main` (`4d6dbba6`, 2026-05-08 11:13:33 +0800) 为基线;本地 `main` 仍落后,不作为 branch diff 基线
- 当前已提交分支相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines`
- 本批待提交工作树集中在 `GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs``GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs``GFramework.Cqrs.Benchmarks/README.md`
- 当前批次后的默认停止依据已改为 AI 上下文预算:若下一轮预计会让活动对话、已加载 recovery 文档、验证输出与当前 diff 接近约 `80%` 安全上下文占用,应在当前自然批次边界停止,即使 branch diff 仍有余量
- `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.300 ns / 32 B``4.964 ns / 32 B``57.993 ns / 232 B``83.823 ns / 32 B`
- 当前 request lifetime benchmark 已从旧坏值显著收敛:`Singleton``GFramework.Cqrs``83.183 ns / 32 B`(旧值 `301.731 ns / 440 B``Transient` 下约 `86.243 ns / 56 B`(旧值 `287.863 ns / 464 B`
- 当前 request steady-state benchmark 已形成 baseline / `Mediator` / `MediatR` / `GFramework.Cqrs` 四方对照:最新约 `5.608 ns / 32 B``5.445 ns / 32 B``57.071 ns / 232 B``64.825 ns / 32 B`
- 当前 request lifetime benchmark 已继续收敛:`Singleton``GFramework.Cqrs` 最新约 `69.275 ns / 32 B``Transient` 下约 `74.301 ns / 56 B`;相较 `RP-104` 前的 `73.005 ns / 32 B``74.757 ns / 56 B` 仍维持同一收敛区间
- 当前 request pipeline benchmark 已改为与默认 request steady-state 相同的 generated-provider 宿主接线路径:`0 pipeline``64.755 ns / 32 B``1 pipeline``353.141 ns / 536 B``4 pipeline``555.083 ns / 896 B`
- 当前 stream steady-state benchmark 也已切到 generated-provider 宿主接线路径baseline 约 `5.535 ns / 32 B``MediatR``59.499 ns / 232 B``GFramework.Cqrs``66.778 ns / 32 B`
- 当前 stream lifetime benchmark 已补齐 `Singleton / Transient` 两档矩阵,并沿用 generated-provider 宿主接线:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR``80.144 ns / 137.515 ns / 229.242 ns``Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- 本轮已验证旧 benchmark 劣化的两个主热点:`0 pipeline` 场景下仍解析空行为列表,以及容器查询热路径在 debug 禁用时仍构造日志字符串;两者收口后,`GFramework.Cqrs` request 路径不再出现额外数百字节分配
- `HasRegistration(Type)` 现在只把“同一服务键已注册”或“开放泛型服务键可闭合到目标类型”视为命中,不再把“仅以具体实现类型自注册”的行为误判为接口服务已注册;该语义与 `Get(Type)` / `GetAll(Type)` 已重新对齐
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 已同步适配 `HasRegistration(Type)` fast-path避免 strict mock 因缺少新调用配置而在上下文失败语义断言前提前抛出 `Moq.MockException`
- `GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs` 现连“handler 缺失但仍返回 faulted `ValueTask`”这条 request 失败语义回归也显式为 `HasRegistration(Type)` / `GetAll(Type)` 预留了防御性 mock不再依赖 dispatcher 先判空 handler、后探测 pipeline 的内部顺序
- `docs/zh-CN/core/ioc.md` 已新增 `HasRegistration(Type)` 的使用语义、热路径用途与“按服务键而非可赋值关系判断”的示例说明
- 当前 request steady-state 仍落后于 source-generated `Mediator``MediatR`,但差距已从“额外数百字节分配 + 近 300ns”收敛到“零 pipeline fast-path 仍慢约 `31ns` / `3.6x``Mediator`”;下一批若继续压 request dispatch应优先评估默认路径吸收 generated invoker/provider 的空间
- 本轮 `SendAsync(...)` 的 direct-return `ValueTask` 改动已证明确实是有效热点:同样的短跑配置下,`GFramework.Cqrs` steady-state request 从约 `83.823 ns` 下探到 `69-70 ns` 区间
- 冻结后 `HasRegistration(Type)` 服务键索引化在当前短跑下没有带来同等量级的可见收益,但也没有引入功能回退或额外分配;后续若继续压零 pipeline request应优先重新评估“默认 request 路径进一步吸收 generated invoker/provider”而不是继续堆叠同层级微优化
- 默认 `RequestBenchmarks``RequestPipelineBenchmarks``StreamingBenchmarks` 现在都已通过 handwritten generated registry + 真实 `RegisterCqrsHandlersFromAssembly(...)` 宿主接线命中 generated invoker provider不再只代表纯反射 binding 路径
- `gframework-boot``gframework-batch-boot` 现明确把“上下文预算接近约 80%”视为默认优先停止信号branch diff files / lines 仅保留为次级仓库范围指标
- 当前性能回归门槛已收紧为:只要改动触达 `GFramework.Cqrs` request dispatch、DI 热路径、invoker/provider、pipeline 或 benchmark 宿主,就必须至少复跑 `RequestBenchmarks.SendRequest_*``RequestLifetimeBenchmarks.SendRequest_*`
- 当前阶段的性能验收目标已明确为:默认 request steady-state 路径不要求超过 source-generated `Mediator`,但必须持续逼近它,并至少稳定快于基于反射 / 扫描的 `MediatR`
- `GFramework.Core` 当前已通过内部 bridge request / handler 把 legacy `ICommand``IAsyncCommand``IQuery``IAsyncQuery` 接到统一 `ICqrsRuntime`
@ -98,6 +115,7 @@ CQRS 迁移与收敛。
- `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
- 当前 stream 生命周期矩阵尚未接入 `Mediator` concrete runtime若要继续对齐 `Mediator` 官方 benchmark 的 compile-time lifetime 设计,需要为 stream 场景补专门的 build-time 配置,而不是在当前统一宿主里临时拼接
- `BenchmarkDotNet.Artifacts/` 现已加入仓库忽略规则;若后续确实需要提交新的基准报告,应显式挑选结果文件或改走文档归档,而不是直接纳入整个生成目录
- 当前 `GFramework.Cqrs` request steady-state 仍慢于 `MediatR`;在“至少超过反射版 `MediatR`”这个阶段目标达成前,任何相关改动都不能只看功能 build/test 结果,必须附带 benchmark 回归数据
- 仓库内部仍保留旧 `Command` / `Query` API、`LegacyICqrsRuntime` alias 与部分历史命名语义,后续若不继续分批收口,容易混淆“对外替代已完成”与“内部收口未完成”
@ -110,6 +128,19 @@ CQRS 迁移与收敛。
## 最近权威验证
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:并行验证首轮曾因 `build``test` 同时写入同一输出 DLL 触发 `MSB3026` 单次复制重试;改为串行重跑同一命令后稳定通过
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherContextValidationTests"`
- 结果:通过,`6/6` passed
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs`
- 结果:通过
- `git diff --check`
- 结果:通过
- 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #341`latest-head 当前仍显示 `CodeRabbit 2` / `Greptile 2` open thread但其中 `BenchmarkHostFactory` / benchmark registry / faulted `ValueTask` 三类运行时 thread 已在本地失效,当前仅剩测试 mock 脆弱性与 trace 冗余仍值得继续收口
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #340`latest-head 当前显示 `CodeRabbit 2` / `Greptile 2` open thread`CTRF` 报告中唯一失败测试为 `CreateStream_Should_Throw_When_Stream_Pipeline_Behavior_Context_Does_Not_Implement_IArchitectureContext`
@ -135,9 +166,55 @@ CQRS 迁移与收敛。
- 备注:按新性能回归门槛复跑后,`Singleton``GFramework.Cqrs` / `MediatR``83.183 ns / 32 B` vs `60.915 ns / 232 B``Transient` 下约 `86.243 ns / 56 B` vs `59.644 ns / 232 B`
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.336 ns / 32 B``Mediator` `5.564 ns / 32 B``MediatR` `53.307 ns / 232 B``GFramework.Cqrs` `64.745 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` 下 baseline / `MediatR` / `GFramework.Cqrs``4.309 ns / 51.923 ns / 67.981 ns``Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR``80.144 ns / 137.515 ns / 229.242 ns``Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- `git diff --check`
- 结果:通过
- 备注:当前仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式错误
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Core.Tests/GFramework.Core.Tests.csproj -c Release --filter "FullyQualifiedName~MicrosoftDiContainerTests"`
- 结果:通过,`52/52` passed
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"`
- 结果:通过,`14/14` passed
- `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 "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:本轮两批热路径收口后的最新 steady-state request 对照约为 baseline `6.141 ns / 32 B``Mediator` `6.674 ns / 32 B``MediatR` `61.803 ns / 232 B``GFramework.Cqrs` `70.298 ns / 32 B`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.706 ns / 52.197 ns / 73.005 ns``Transient` 下 = `4.571 ns / 50.175 ns / 74.757 ns`
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.013 ns / 32 B``Mediator` `5.747 ns / 32 B``MediatR` `51.588 ns / 232 B``GFramework.Cqrs` `65.296 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`
- 结果:通过
- 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.817 ns / 48.177 ns / 68.772 ns``Transient` 下 = `4.841 ns / 51.753 ns / 73.157 ns`
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
- 结果:通过
- 备注:仍仅保留 `GFramework.sln` 的历史 CRLF 警告,无本轮新增 diff 格式问题
- `dotnet pack GFramework.sln -c Release --no-restore -o /tmp/gframework-pack-validation -p:IncludeSymbols=false`
- 结果:通过
- 备注:当前本地产物仅包含 14 个预期发布包,未生成 `GFramework.Cqrs.Benchmarks.*.nupkg`
@ -258,9 +335,9 @@ CQRS 迁移与收敛。
## 下一推荐步骤
1. 若继续沿用 `$gframework-batch-boot 50` 且优先处理性能,下一批先对 `CqrsDispatcher.SendAsync(...)` / request invoker 绑定 / handler 调用适配做更细粒度热点拆分,并在每次改动后立即复跑 `RequestBenchmarks``RequestLifetimeBenchmarks`
2. 若要把“至少超过反射版 `MediatR`”变成可执行目标,下一批优先评估默认 request 路径吸收 generated invoker/provider 或继续裁掉 dispatch binding / delegate 适配层的剩余常量开销
3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再`Mediator` 的 compile-time lifetime 矩阵,而不是先横向堆更多低价值场景
1. 当前 turn 已到新的自然批次边界;本次提交后应停止,并在新的 turn 里从 `RP-108` 恢复点继续,而不是在本轮继续启动新的 benchmark 宿主或 runtime 热点切片
2. 若下一轮继续沿用 `$gframework-batch-boot` 且优先处理性能,先看 notification publish 或更高价值的 request dispatch 常量开销热点,而不是继续堆同层级 benchmark 宿主补齐
3. 若 benchmark 对照需要继续贴近 `Mediator` 官方设计,再评估 `Mediator` 的 compile-time lifetime / stream 对照矩阵,或给 stream 引入 scoped host 基线,而不是回头重试已被 benchmark 否决的 `GetAll(Type)` 零行为探测方案
## 活跃文档

View File

@ -2,6 +2,243 @@
## 2026-05-08
### 阶段PR #341 latest-head review 尾声收口CQRS-REWRITE-RP-110
- 再次使用 `$gframework-pr-review` 抓取 `PR #341` latest-head review确认当前 open thread 已收敛到:
- `BenchmarkHostFactory.cs` 的 legacy runtime alias 防守式类型检查 thread但当前 head 已存在 `RegisterLegacyRuntimeAlias(...)` 的显式类型校验与实际类型信息异常,属于 GitHub 未 resolve 的 stale thread
- `RequestBenchmarks.cs` / `CqrsDispatcher.cs` 的 Greptile thread对应“程序集级 registry 扩散”与“faulted ValueTask 失败语义”均已在当前 head 修复,属于 stale thread
- 仍然成立且值得当前收口的只剩 `CqrsDispatcherContextValidationTests.cs` 的 strict mock 脆弱性,以及本 trace 中 `本轮下一步``本轮权威验证` 重复的问题
- 本轮主线程决策:
- 为 `SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing()` 补齐 `HasRegistration(...)``GetAll(...)` 的防御性 mock降低该测试对 dispatcher 内部检查顺序的隐式耦合
- 删除 `RP-109` 记录中重复 `本轮权威验证``本轮下一步` 段落,保持默认恢复入口只保留仍有价值的恢复信息
- 本轮权威验证:
- `python3 .agents/skills/gframework-pr-review/scripts/fetch_current_pr_review.py --format json --json-output /tmp/current-pr-review.json`
- 结果:通过
- 备注:确认当前分支对应 `PR #341`latest-head 当前仍显示 `CodeRabbit 2` / `Greptile 2` open thread但其中运行时/benchmark 两条已在本地失效
- `dotnet build GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- 备注:并行验证首轮曾因 `build``test` 同时写入同一输出 DLL 触发 `MSB3026` 单次复制重试;改为串行重跑同一命令后稳定通过
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherContextValidationTests"`
- 结果:通过,`6/6` passed
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs`
- 结果:通过
- `git diff --check`
- 结果:通过
- 备注:仅剩 `GFramework.sln` 的历史 CRLF 提示,无本轮新增 diff 格式问题
### 阶段PR #341 latest-head review 收口CQRS-REWRITE-RP-109
- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR并确认当前锚点已从 `PR #340` 更新为 `PR #341`
- 本轮 latest-head review 结论:
- `CodeRabbit` 仍有 `BenchmarkHostFactory.cs` 的 legacy runtime 硬转型、`StreamLifetimeBenchmarks.cs` 的注释缺口,以及 `.agents/skills/gframework-batch-boot/SKILL.md``MD005` 缩进问题
- `Greptile` 指出的两条仍然成立benchmark 项目里通过 `RegisterCqrsHandlersFromAssembly(typeof(...).Assembly)` 会把同程序集的其他 generated registry 一并激活,扩大 benchmark 宿主的服务索引基线;`CqrsDispatcher.SendAsync(...)` 直接去掉 `async/await` 后也把原本的 faulted-`ValueTask` 失败语义改成了同步抛出
- 本轮主线程决策:
- 在 `GFramework.Cqrs.Internal.CqrsHandlerRegistrar` 新增 direct generated-registry 激活入口,并通过 `InternalsVisibleTo` 暴露给 `GFramework.Cqrs.Benchmarks`,让 benchmark 宿主只激活当前场景的 generated registry
- 把 `RequestBenchmarks``RequestPipelineBenchmarks``StreamingBenchmarks``StreamLifetimeBenchmarks` 以及 request/stream invoker benchmark 的 generated 宿主全部切到定向 registry 接线,避免同程序集其他 registry 扩大冻结索引和 descriptor 预热基线
- 在 `BenchmarkHostFactory` 里用防守式类型检查注册 legacy runtime alias并补充 stream lifetime runtime 二次创建的注释
- 让 `CqrsDispatcher.SendAsync(...)` 通过 `ValueTask.FromException<TResponse>(...)` 恢复旧的 faulted-`ValueTask` 失败语义,同时保留成功路径的 direct-return 热路径
- 补齐 `CqrsGeneratedRequestInvokerProviderTests``CqrsDispatcherContextValidationTests` 的 targeted 回归,并顺手修正 batch boot skill 的 markdown 缩进
- 本轮权威验证:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`1 warning / 0 error`
- 备注:仅出现 `MSB3026` 单次复制重试告警,随后成功产出 `net10.0` 目标;未出现编译失败或新增代码警告
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --filter "FullyQualifiedName~CqrsDispatcherContextValidationTests|FullyQualifiedName~CqrsGeneratedRequestInvokerProviderTests"`
- 结果:通过,`24/24` passed
- 备注:首轮并行验证时因与 build 同时运行触发 MSBuild 输出文件锁竞争;改为串行重跑同一命令后稳定通过
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs/Properties/AssemblyInfo.cs GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs GFramework.Cqrs/Internal/CqrsDispatcher.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs`
- 结果:通过
- 备注:仓库脚本默认内部调用未绑定 worktree 的 `git ls-files`,因此本轮按修改文件列表显式 `--paths` 校验
- `git diff --check`
- 结果:通过
### 阶段stream handler 生命周期矩阵 benchmarkCQRS-REWRITE-RP-108
- 延续 `$gframework-batch-boot 50`,本轮继续使用 `origin/main` 作为 branch diff 基线,并先复核:
- `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
- 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `14 files / 507 lines`
- 当前 turn 虽然仍低于 `50 files` 阈值,但已加载多轮 recovery / benchmark 输出;因此只允许再推进一个单模块、低风险 benchmark 切片
- 本轮接受的只读探索结论:
- `RequestLifetimeBenchmarks` 已覆盖 request 的 `Singleton / Transient` 生命周期矩阵,但 stream 侧仍缺少对称的 handler 生命周期对照
- `StreamingBenchmarks` 已在 `RP-107` 切到 generated-provider 宿主,适合作为 stream 生命周期矩阵的宿主基础继续退回纯反射路径会让“生命周期变量”和“descriptor 路径变量”混在一起
- 如果让 generated registry 顺手注册默认单例 handler会破坏生命周期矩阵的变量控制因此 registry 只能暴露 descriptor不能抢先锁死 handler 生命周期
- 本轮主线程决策:
- 新增 `StreamLifetimeBenchmarks`,对齐 request 生命周期矩阵,只比较 `Singleton / Transient` 两档,继续明确把 `Scoped` 留给未来显式 scoped host
- 新增 `GeneratedStreamLifetimeBenchmarkRegistry`,只提供 handwritten generated stream invoker descriptor不直接注册 handler
- 让 `StreamLifetimeBenchmarks` 使用 `RegisterCqrsHandlersFromAssembly(typeof(StreamLifetimeBenchmarks).Assembly)` 建立 generated-provider 宿主,再显式按 benchmark 参数注册 `Singleton / Transient` handler 生命周期
- 更新 `GFramework.Cqrs.Benchmarks/README.md`,把 stream 生命周期矩阵列为已覆盖场景,并从“后续扩展方向”里移除这项待办
- 本轮验证过程的重要补充:
- 首次并行触发 `RequestBenchmarks` / `RequestLifetimeBenchmarks` / `StreamLifetimeBenchmarks` 时,在同一 autogenerated BenchmarkDotNet 目录下复现了文件已存在冲突与 bootstrap 异常;这是 benchmark 基础设施层面的并行目录竞争,不是代码缺陷
- 改为串行重跑后三组 benchmark 全部稳定通过因此本轮将“BenchmarkDotNet 在当前仓库里不应并行运行多条 `dotnet run --project ... --filter ...` 会话”视为有效执行约束
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.336 ns / 32 B``Mediator` `5.564 ns / 32 B``MediatR` `53.307 ns / 232 B``GFramework.Cqrs` `64.745 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` 下 baseline / `MediatR` / `GFramework.Cqrs``4.309 ns / 51.923 ns / 67.981 ns``Transient` 下约 `5.029 ns / 54.435 ns / 76.437 ns`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamLifetimeBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:`Singleton` 下 baseline / `GFramework.Cqrs` / `MediatR``80.144 ns / 137.515 ns / 229.242 ns``Transient` 下约 `77.198 ns / 144.998 ns / 228.185 ns`
- 本轮结论:
- stream 生命周期矩阵现在已与 request 生命周期矩阵对称,且继续沿用 generated-provider 宿主路径,没有把变量退化回纯反射 binding
- `GFramework.Cqrs` 在 stream `Singleton / Transient` 两档下都明显快于 `MediatR`,同时保持接近 baseline 的分配规模;`Transient` 仅从 `240 B` 小幅增至 `264 B`
- 真正的停止依据仍是上下文预算安全。虽然 branch diff 只有 `14 files`,但当前 turn 已包含多轮 benchmark 输出和恢复文档,因此本批提交后应主动停止
- 下一轮若继续性能线,更值得优先看 notification publish 或更高价值的 request 常量开销热点,而不是继续做同层级 benchmark 宿主补齐
### 阶段:默认 stream benchmark 吸收 generated provider 宿主CQRS-REWRITE-RP-107
- 延续 `$gframework-batch-boot 50`但本轮按用户新增要求把默认停止依据改为“AI 上下文预算优先,建议在预计接近约 80% 安全上下文占用前收口”;在真正落代码前先复核:
- `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
- 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `10 files / 507 lines`
- 当前 turn 已加载 `AGENTS.md``gframework-batch-boot` / `gframework-boot`、active tracking/trace、上一轮 benchmark 结果与多次 validation 输出,因此继续一个自然批次可以接受,但不应在本次提交后继续无界循环
- 本轮接受的只读探索结论:
- 默认 request / request pipeline 宿主都已吸收 generated provider`StreamingBenchmarks` 仍停在“直接注册单个 stream handler”的旧宿主路径口径与 `StreamInvokerBenchmarks` / 默认 request 组不对称
- 默认 stream steady-state 场景已经足够独立,适合用一份新的 handwritten generated stream registry 最小化收口,而不用再修改 runtime 语义
- 用户要求把停止条件从 changed files 改成 AI 上下文预算,因此 skill 文档本身也属于这一批必须一起落下的恢复边界更新
- 本轮主线程决策:
- 新增 `GeneratedDefaultStreamingBenchmarkRegistry`,用 handwritten generated registry + `ICqrsStreamInvokerProvider` + `IEnumeratesCqrsStreamInvokerDescriptors``StreamingBenchmarks.BenchmarkStreamRequest` 提供真实的 generated stream invoker descriptor
- 让 `StreamingBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(StreamingBenchmarks).Assembly)` 建容器,并在 `Setup/Cleanup` 前后显式清理 dispatcher 静态缓存
- 更新 `GFramework.Cqrs.Benchmarks/README.md`,明确默认 stream steady-state benchmark 也已接上 handwritten generated stream invoker provider
- 更新 `.agents/skills/gframework-batch-boot/SKILL.md``.agents/skills/gframework-boot/SKILL.md`,明确“上下文预算接近约 80% 时优先停止branch diff 文件/行数只作次级仓库范围信号”
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.608 ns / 32 B``Mediator` `5.445 ns / 32 B``MediatR` `57.071 ns / 232 B``GFramework.Cqrs` `64.825 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` 下 baseline / `MediatR` / `GFramework.Cqrs``4.446 ns / 51.331 ns / 69.275 ns``Transient` 下约 `4.918 ns / 56.382 ns / 74.301 ns`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*StreamingBenchmarks*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:默认 stream steady-state 对照约为 baseline `5.535 ns / 32 B``MediatR` `59.499 ns / 232 B``GFramework.Cqrs` `66.778 ns / 32 B`
- 本轮结论:
- 默认 stream steady-state benchmark 现在也已切到 generated-provider 宿主路径request / pipeline / stream 三个默认宿主场景的 benchmark 口径终于对齐
- `StreamingBenchmarks``GFramework.Cqrs` 结果约 `66.778 ns / 32 B`,仍慢于 `MediatR`,但没有新增分配或明显回退,说明这次宿主收口是低风险可接受的
- 更重要的是默认停止依据已从“branch diff 文件数是否触顶”改成“AI 上下文预算是否接近约 80%”;结合当前 turn 已加载的大量 recovery/validation/benchmark 输出,本次提交后应主动停止,而不是继续机械扩批
- 下一轮若继续性能线,应从 `RP-107` 恢复点重新进入,并优先挑选新的高价值热点族,而不是沿着当前 turn 再追加更多同类宿主收口
### 阶段request pipeline benchmark 吸收 generated provider 宿主CQRS-REWRITE-RP-106
- 延续 `$gframework-batch-boot 50`,本轮基于 `RP-105` 已验证的默认 request 宿主接线继续推进,并先复核 branch diff 基线:
- `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
- 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 为 `8 files / 358 lines`
- 当前工作树待提交改动只集中在 `RequestPipelineBenchmarks`、对应 handwritten generated registry 与 benchmark `README`,因此继续自动推进下一批 pipeline 宿主收口
- 本轮接受的只读探索结论:
- `RP-105` 已证明“让默认 request 宿主真实接上 generated request invoker provider”能稳定压低 steady-state request因此 pipeline benchmark 仍保留旧的“直接注册单个 handler”路径会让口径不对齐
- 之前已被 benchmark 否决的“总是 `GetAll(Type)` 做零 pipeline 探测”不应回头重试;下一刀更合理的是把 pipeline benchmark 也切到真实程序集注册入口
- `RequestPipelineBenchmarks` 只需要补一份与 `RequestBenchmarks` 对称的 handwritten generated registry就能最小化改动并保持 runtime 语义不变
- 本轮主线程决策:
- 新增 `GeneratedRequestPipelineBenchmarkRegistry`,用 handwritten generated registry + `ICqrsRequestInvokerProvider` + `IEnumeratesCqrsRequestInvokerDescriptors``RequestPipelineBenchmarks.BenchmarkRequest` 提供真实的 generated request invoker descriptor
- 让 `RequestPipelineBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(RequestPipelineBenchmarks).Assembly)` 建容器,只把 pipeline 行为数量矩阵保留在 benchmark 自己的显式注册里
- 更新 `GFramework.Cqrs.Benchmarks/README.md`,明确 request pipeline benchmark 也已接上 handwritten generated request invoker provider
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.680 ns / 32 B``Mediator` `6.565 ns / 32 B``MediatR` `54.737 ns / 232 B``GFramework.Cqrs` `63.644 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``69.896 ns / 32 B` vs `57.469 ns / 232 B``Transient` 下约 `72.880 ns / 56 B` vs `55.106 ns / 232 B`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestPipelineBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:第一次短跑为 `PipelineCount=0` `64.928 ns / 32 B``PipelineCount=1` `366.468 ns / 536 B``PipelineCount=4` `547.800 ns / 896 B`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release -- --filter "*RequestPipelineBenchmarks.SendRequest_GFrameworkCqrs*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:复跑确认后为 `PipelineCount=0` `64.755 ns / 32 B``PipelineCount=1` `353.141 ns / 536 B``PipelineCount=4` `555.083 ns / 896 B`
- 本轮结论:
- request pipeline benchmark 现在已与默认 request steady-state 使用同一条 generated-provider 宿主接线路径,后续再看 `0 / 1 / 4` 行为矩阵时不再混入“默认 request 已吸收 generated invoker而 pipeline 还停在纯反射宿主”的口径偏差
- `0 pipeline` steady-state 继续下探到约 `64.755 ns / 32 B`,与 `RP-105` 的默认 request benchmark 收敛方向一致,说明这条宿主接线收益能稳定复用到 pipeline benchmark
- `1 pipeline``4 pipeline` 结果在当前 short job 配置下存在噪音,但没有出现清晰的新增分配或显著退化;因此本轮适合作为低风险宿主收口批次接受
- 下一批若继续沿用 `$gframework-batch-boot 50`,应优先查看 request lifetime、stream 或 notification benchmark 中是否还存在未吸收 generated-provider 宿主收益的对称切片,而不是回头重试已被 benchmark 否决的 runtime 微优化
### 阶段:默认 request benchmark 吸收 generated provider 宿主CQRS-REWRITE-RP-105
- 延续 `$gframework-batch-boot 50`,本轮先确认失败试验已手工回退回 `RP-104` 的已验证状态,再重新评估“默认 request 路径继续逼近 source-generated `Mediator`”的下一刀
- 本轮接受的只读探索结论:
- 继续在 `CqrsDispatcher``MicrosoftDiContainer` 上堆叠同层级微优化的性价比已经下降,而且上一轮“总是 `GetAll(Type)`”的试验已被 benchmark 明确否决
- 默认 `RequestBenchmarks` 虽然已包含 `Mediator` 对照,但当前 GFramework 组仍只注册了单个 handler 实例,没有走 `RegisterCqrsHandlersFromAssembly(...)` + generated registry/provider 的真实宿主接线路径
- `RequestInvokerBenchmarks` 已证明 generated request invoker provider 路径比纯反射 binding 更接近目标,因此下一批最小切片应先把这条收益吸收到默认 steady-state request benchmark
- 本轮主线程决策:
- 在 `BenchmarkHostFactory` 内补齐 benchmark 最小宿主的 CQRS 基础设施预接线runtime、legacy alias、registrar、registration service
- 新增 `GeneratedDefaultRequestBenchmarkRegistry`,用 handwritten generated registry + `ICqrsRequestInvokerProvider` + `IEnumeratesCqrsRequestInvokerDescriptors``RequestBenchmarks.BenchmarkRequest` 提供真实的 generated request invoker descriptor
- 让 `RequestBenchmarks` 改用 `RegisterCqrsHandlersFromAssembly(typeof(RequestBenchmarks).Assembly)` 建容器,并在 `Setup/Cleanup` 前后显式清理 dispatcher 静态缓存,避免前一组 benchmark 污染默认 request steady-state 结果
- 本轮权威验证:
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs GFramework.Cqrs.Benchmarks/README.md`
- 结果:通过
- `git diff --check`
- 结果:通过
- `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.013 ns / 32 B``Mediator` `5.747 ns / 32 B``MediatR` `51.588 ns / 232 B``GFramework.Cqrs` `65.296 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``68.772 ns / 32 B` vs `48.177 ns / 232 B``Transient` 下约 `73.157 ns / 56 B` vs `51.753 ns / 232 B`
- 本轮结论:
- 默认 request benchmark 现在终于测到了“默认宿主已吸收 generated request invoker provider”后的真实 steady-state而不再只是纯反射 request binding
- 这条宿主层收口在不改 runtime 语义的前提下,把 `GFramework.Cqrs` steady-state request 从约 `70.298 ns` 再压到约 `65.296 ns`
- lifetime 矩阵也同步改善到 `68.772 ns / 73.157 ns`,说明默认 request 宿主吸收 generated provider 不只是 benchmark 口径变化,而是对常见 handler 生命周期也有稳定收益
- 下一批若继续沿用 `$gframework-batch-boot 50`,应优先转向 pipeline 路径或 handler 解析热路径中仍未吸收 generated/provider 收益的常量开销,而不是回头重试已被否决的 `GetAll(Type)` 零行为探测方案
### 阶段request 热路径继续收口CQRS-REWRITE-RP-104
- 延续 `$gframework-batch-boot 50`,本轮先重新按 `origin/main` 复核 branch diff 基线:
- `origin/main` = `4d6dbba6`,提交时间 `2026-05-08 11:13:33 +0800`
- 当前分支 `feat/cqrs-optimization` 相对 `origin/main` 的累计 branch diff 仍为 `0 files / 0 lines`
- 当前工作树在真正落代码前只有活跃文档更新,仍明显低于 `$gframework-batch-boot 50` 的文件阈值,因此继续自动推进下一批 request 热路径收口
- 本轮接受的只读探索结论:
- `RequestBenchmarks` / `RequestInvokerBenchmarks` 的下一个低风险热点仍在“每次发送都必经的容器查询与短生命周期对象创建”,不是重新回到更高风险的语义层重构
- 候选优先级排序为:`SendAsync` 自身状态机开销、`HasRegistration + GetAll` / 服务键扫描,以及 pipeline continuation 的临时对象
- 本轮主线程决策:
- 先以最小行为改动切第一刀:把 `CqrsDispatcher.SendAsync(...)``async/await` 改为 direct-return `ValueTask`,让零 pipeline request 常见路径不再为 dispatcher 自身生成额外状态机
- 在第一刀验证通过且 benchmark 明显改善后,再切第二刀:让 `MicrosoftDiContainer.HasRegistration(Type)` 在冻结后复用预构建的服务键索引,而不是每次线性扫描全部 `ServiceDescriptor`
- 第二刀完成后停止继续叠第三刀,因为当前批次已经能清晰区分“有效收益”和“无回退但收益不明显”的因果,不再为了追逐更小常量开销降低评审清晰度
- 本轮权威验证:
- `dotnet build GFramework.Cqrs/GFramework.Cqrs.csproj -c Release`
- 结果:通过,`0 warning / 0 error`
- `dotnet build GFramework.Core/GFramework.Core.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"`
- 结果:通过,`52/52` passed
- `dotnet test GFramework.Cqrs.Tests/GFramework.Cqrs.Tests.csproj -c Release --no-build --filter "FullyQualifiedName~CqrsDispatcherCacheTests|FullyQualifiedName~CqrsDispatcherContextValidationTests"`
- 结果:通过,`14/14` passed
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:最新 steady-state request 对照约为 baseline `6.141 ns / 32 B``Mediator` `6.674 ns / 32 B``MediatR` `61.803 ns / 232 B``GFramework.Cqrs` `70.298 ns / 32 B`
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
- 结果:通过
- 备注:最新 lifetime request 对照约为 `Singleton` 下 baseline / `MediatR` / `GFramework.Cqrs` = `4.706 ns / 52.197 ns / 73.005 ns``Transient` 下 = `4.571 ns / 50.175 ns / 74.757 ns`
- `env GIT_DIR=... GIT_WORK_TREE=... python3 scripts/license-header.py --check`
- 结果:通过
- `git diff --check`
- 结果:通过
- 备注:仍仅有 `GFramework.sln` 的历史 CRLF 警告,无本轮新增格式问题
- 本轮结论:
- 第一刀有效:`CqrsDispatcher.SendAsync(...)` 的 direct-return `ValueTask``GFramework.Cqrs` steady-state request 从 `RP-103` 记录的约 `83.823 ns` 压到约 `70.298 ns`
- 第二刀保守有效:冻结后 `HasRegistration(Type)` 索引化没有带来同量级的可见收益,但也没有造成功能回退、额外分配或测试破坏
- 下一批若继续压 request hot path应优先评估默认 request 路径吸收 generated invoker/provider而不是继续围绕同层级容器存在性判断做微调
### 阶段PR #340 latest-head review 收口CQRS-REWRITE-RP-103
- 使用 `$gframework-pr-review` 抓取 `feat/cqrs-optimization` 当前公开 PR并确认当前锚点已从 `PR #339` 更新为 `PR #340`