mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-08 17:44:29 +08:00
Merge pull request #341 from GeWuYou/feat/cqrs-optimization
Feat/cqrs optimization
This commit is contained in:
commit
7ca21af92d
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,
|
||||
|
||||
279
GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
Normal file
279
GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 / 建流对比继续扩展到更多场景
|
||||
|
||||
@ -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 会在发布前显式失败。
|
||||
|
||||
@ -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 注册到容器中。
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
7
GFramework.Cqrs/Properties/AssemblyInfo.cs
Normal file
7
GFramework.Cqrs/Properties/AssemblyInfo.cs
Normal 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")]
|
||||
@ -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 review:benchmark 宿主改为定向激活当前场景的 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)` 零行为探测方案
|
||||
|
||||
## 活跃文档
|
||||
|
||||
|
||||
@ -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 生命周期矩阵 benchmark(CQRS-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`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user