diff --git a/.agents/skills/gframework-batch-boot/SKILL.md b/.agents/skills/gframework-batch-boot/SKILL.md index 860ca496..e10173f7 100644 --- a/.agents/skills/gframework-batch-boot/SKILL.md +++ b/.agents/skills/gframework-batch-boot/SKILL.md @@ -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 diff --git a/.agents/skills/gframework-boot/SKILL.md b/.agents/skills/gframework-boot/SKILL.md index 55e9b55f..563651bd 100644 --- a/.agents/skills/gframework-boot/SKILL.md +++ b/.agents/skills/gframework-boot/SKILL.md @@ -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 diff --git a/GFramework.Core/Ioc/MicrosoftDiContainer.cs b/GFramework.Core/Ioc/MicrosoftDiContainer.cs index b9896950..461487c2 100644 --- a/GFramework.Core/Ioc/MicrosoftDiContainer.cs +++ b/GFramework.Core/Ioc/MicrosoftDiContainer.cs @@ -185,6 +185,12 @@ public class MicrosoftDiContainer(IServiceCollection? serviceCollection = null) /// private IServiceProvider? _provider; + /// + /// 冻结后可复用的服务类型可见性索引。 + /// 容器冻结后注册集合不再变化,因此 可以安全复用该索引。 + /// + private FrozenServiceTypeIndex? _frozenServiceTypeIndex; + /// /// 容器冻结状态标志,true表示容器已冻结不可修改 /// @@ -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) } } + /// + /// 保存冻结后按服务键可见的精确服务类型与开放泛型定义集合。 + /// + /// + /// 该索引只回答“按当前服务键语义是否可见”,因此与 / + /// 一样不会退化为更宽松的可赋值匹配。 + /// + private sealed class FrozenServiceTypeIndex(HashSet exactServiceTypes, HashSet openGenericServiceTypes) + { + private readonly HashSet _exactServiceTypes = exactServiceTypes; + private readonly HashSet _openGenericServiceTypes = openGenericServiceTypes; + + /// + /// 基于冻结时最终确定的服务描述符集合创建索引。 + /// + /// 冻结时的服务描述符序列。 + /// 供存在性判断热路径复用的服务键索引。 + public static FrozenServiceTypeIndex Create(IEnumerable descriptors) + { + ArgumentNullException.ThrowIfNull(descriptors); + + var exactServiceTypes = new HashSet(); + var openGenericServiceTypes = new HashSet(); + + foreach (var descriptor in descriptors) + { + var serviceType = descriptor.ServiceType; + exactServiceTypes.Add(serviceType); + + if (serviceType.IsGenericTypeDefinition) + { + openGenericServiceTypes.Add(serviceType); + } + } + + return new FrozenServiceTypeIndex(exactServiceTypes, openGenericServiceTypes); + } + + /// + /// 判断当前索引是否声明了目标服务键。 + /// + /// 要检查的服务类型。 + /// 命中精确服务键或可闭合的开放泛型服务键时返回 + public bool Contains(Type requestedType) + { + ArgumentNullException.ThrowIfNull(requestedType); + + return _exactServiceTypes.Contains(requestedType) || + requestedType.IsConstructedGenericType && + _openGenericServiceTypes.Contains(requestedType.GetGenericTypeDefinition()); + } + } + /// /// 获取底层的服务集合 /// 提供对内部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; diff --git a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs index 886f2c71..fb93cc32 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs @@ -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; } + /// + /// 为 benchmark 宿主补齐默认 CQRS runtime seam,确保它既能手工注册 handler,也能走真实的程序集注册入口。 + /// + /// 当前 benchmark 拥有的 GFramework 容器。 + /// + /// `RegisterCqrsHandlersFromAssembly(...)` 依赖预先可见的 runtime / registrar / registration service 实例绑定。 + /// benchmark 宿主直接使用裸 ,因此需要在配置阶段先补齐这组基础设施, + /// 避免各个 benchmark 用例各自复制同一段前置接线逻辑。 + /// + private static void RegisterCqrsInfrastructure(MicrosoftDiContainer container) + { + ArgumentNullException.ThrowIfNull(container); + + if (container.Get() is null) + { + var runtimeLogger = LoggerFactoryResolver.Provider.CreateLogger("CqrsDispatcher"); + var notificationPublisher = container.Get(); + var runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime(container, runtimeLogger, notificationPublisher); + container.Register(runtime); + RegisterLegacyRuntimeAlias(container, runtime); + } + else if (container.Get() is null) + { + RegisterLegacyRuntimeAlias(container, container.GetRequired()); + } + + if (container.Get() is null) + { + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + var registrar = GFramework.Cqrs.CqrsRuntimeFactory.CreateHandlerRegistrar(container, registrarLogger); + container.Register(registrar); + } + + if (container.Get() is null) + { + var registrationLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsRegistrationService"); + var registrar = container.GetRequired(); + var registrationService = GFramework.Cqrs.CqrsRuntimeFactory.CreateRegistrationService(registrar, registrationLogger); + container.Register(registrationService); + } + } + + /// + /// 只激活当前 benchmark 场景明确拥有的 generated registry,避免同一程序集里的其他 benchmark registry + /// 扩大冻结后服务索引与 dispatcher descriptor 基线。 + /// + /// 当前 benchmark 需要接入的 generated registry 类型。 + /// 承载 generated registry 注册结果的 GFramework benchmark 容器。 + internal static void RegisterGeneratedBenchmarkRegistry(MicrosoftDiContainer container) + where TRegistry : class, GFramework.Cqrs.ICqrsHandlerRegistry + { + ArgumentNullException.ThrowIfNull(container); + + var registrarLogger = LoggerFactoryResolver.Provider.CreateLogger("DefaultCqrsHandlerRegistrar"); + CqrsHandlerRegistrar.RegisterGeneratedRegistry(container, typeof(TRegistry), registrarLogger); + } + + /// + /// 为旧命名空间下的 CQRS runtime 契约注册兼容别名。 + /// + /// 承载 runtime 别名的 benchmark 容器。 + /// 当前正式 CQRS runtime 实例。 + /// + /// 未同时实现 legacy CQRS runtime 契约。 + /// + 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(legacyRuntime); + } + /// /// 创建只承载当前 benchmark handler 集合的最小 MediatR 宿主。 /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs new file mode 100644 index 00000000..59646dc1 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultRequestBenchmarkRegistry.cs @@ -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; + +/// +/// 为默认 request steady-state benchmark 提供 hand-written generated registry, +/// 以便验证“默认宿主吸收 generated request invoker provider”后的热路径收益。 +/// +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 Descriptors = + [ + new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry( + typeof(RequestBenchmarks.BenchmarkRequest), + typeof(RequestBenchmarks.BenchmarkResponse), + Descriptor) + ]; + + /// + /// 把默认 request benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddSingleton( + typeof(IRequestHandler), + typeof(RequestBenchmarks.BenchmarkRequestHandler)); + logger.Debug("Registered generated default request benchmark handler."); + } + + /// + /// 返回当前 provider 暴露的全部 generated request invoker 描述符。 + /// + public IReadOnlyList GetDescriptors() + { + return Descriptors; + } + + /// + /// 为目标请求/响应类型对返回 generated request invoker 描述符。 + /// + 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; + } + + /// + /// 模拟 generated invoker provider 为默认 request benchmark 产出的开放静态调用入口。 + /// + public static ValueTask 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); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs new file mode 100644 index 00000000..57a1b9a2 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedDefaultStreamingBenchmarkRegistry.cs @@ -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; + +/// +/// 为默认 stream steady-state benchmark 提供 hand-written generated registry, +/// 以便验证“默认 stream 宿主吸收 generated stream invoker provider”后的完整枚举收益。 +/// +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 Descriptors = + [ + new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry( + typeof(StreamingBenchmarks.BenchmarkStreamRequest), + typeof(StreamingBenchmarks.BenchmarkResponse), + Descriptor) + ]; + + /// + /// 把默认 stream benchmark handler 注册为单例,保持与原先 steady-state 宿主一致的生命周期语义。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddSingleton( + typeof(IStreamRequestHandler), + typeof(StreamingBenchmarks.BenchmarkStreamHandler)); + logger.Debug("Registered generated default streaming benchmark handler."); + } + + /// + /// 返回当前 provider 暴露的全部 generated stream invoker 描述符。 + /// + public IReadOnlyList GetDescriptors() + { + return Descriptors; + } + + /// + /// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。 + /// + 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; + } + + /// + /// 模拟 generated stream invoker provider 为默认 stream benchmark 产出的开放静态调用入口。 + /// + 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); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs new file mode 100644 index 00000000..5844cee1 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedRequestPipelineBenchmarkRegistry.cs @@ -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; + +/// +/// 为 request pipeline benchmark 提供 handwritten generated registry, +/// 让默认 pipeline 宿主也能走真实的 generated request invoker provider 接线路径。 +/// +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 Descriptors = + [ + new GFramework.Cqrs.CqrsRequestInvokerDescriptorEntry( + typeof(RequestPipelineBenchmarks.BenchmarkRequest), + typeof(RequestPipelineBenchmarks.BenchmarkResponse), + Descriptor) + ]; + + /// + /// 将 request pipeline benchmark handler 注册为单例,保持与当前矩阵宿主一致的生命周期语义。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + services.AddSingleton( + typeof(IRequestHandler), + typeof(RequestPipelineBenchmarks.BenchmarkRequestHandler)); + logger.Debug("Registered generated request pipeline benchmark handler."); + } + + /// + /// 返回当前 provider 暴露的全部 generated request invoker 描述符。 + /// + public IReadOnlyList GetDescriptors() + { + return Descriptors; + } + + /// + /// 为目标请求/响应类型对返回 generated request invoker 描述符。 + /// + 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; + } + + /// + /// 模拟 generated invoker provider 为 request pipeline benchmark 产出的开放静态调用入口。 + /// + public static ValueTask 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); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs new file mode 100644 index 00000000..977a14a5 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/GeneratedStreamLifetimeBenchmarkRegistry.cs @@ -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; + +/// +/// 为 stream 生命周期矩阵 benchmark 提供 hand-written generated registry, +/// 以便在默认 generated-provider 宿主路径上比较不同 handler 生命周期的完整枚举成本。 +/// +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 Descriptors = + [ + new GFramework.Cqrs.CqrsStreamInvokerDescriptorEntry( + typeof(StreamLifetimeBenchmarks.BenchmarkStreamRequest), + typeof(StreamLifetimeBenchmarks.BenchmarkResponse), + Descriptor) + ]; + + /// + /// 参与程序集注册入口,但不在这里直接写入 handler 生命周期。 + /// + /// 当前 generated registry 拥有的服务集合。 + /// 用于记录 generated registry 注册行为的日志器。 + /// + /// 生命周期矩阵需要让 benchmark 主体显式控制 `Singleton / Transient` 变量。 + /// 因此 registry 只负责暴露 generated descriptor,不在这里抢先注册 handler,避免把默认单例注册混入比较结果。 + /// + public void Register(IServiceCollection services, ILogger logger) + { + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(logger); + + logger.Debug("Registered generated stream lifetime benchmark descriptors."); + } + + /// + /// 返回当前 provider 暴露的全部 generated stream invoker 描述符。 + /// + public IReadOnlyList GetDescriptors() + { + return Descriptors; + } + + /// + /// 为目标流式请求/响应类型对返回 generated stream invoker 描述符。 + /// + /// 待匹配的请求类型。 + /// 待匹配的响应类型。 + /// 命中时返回的 generated descriptor。 + /// 命中当前 benchmark 的请求/响应类型对时返回 + 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; + } + + /// + /// 模拟 generated stream invoker provider 为生命周期矩阵 benchmark 产出的开放静态调用入口。 + /// + /// 当前请求对应的 handler 实例。 + /// 待分发的流式请求。 + /// 调用方传入的取消令牌。 + /// 交给目标 stream handler 处理后的异步枚举。 + 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); + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs index 37840a94..9d946e72 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestBenchmarks.cs @@ -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>( - _baselineHandler); + BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(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(); + } } /// diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs index 87e326e5..4b8589ed 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestInvokerBenchmarks.cs @@ -83,7 +83,7 @@ public class RequestInvokerBenchmarks _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => { - container.RegisterCqrsHandlersFromAssembly(typeof(RequestInvokerBenchmarks).Assembly); + BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container); }); _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _generatedContainer, diff --git a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs index 07dbc0c5..a2883955 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/RequestPipelineBenchmarks.cs @@ -69,8 +69,7 @@ public class RequestPipelineBenchmarks _baselineHandler = new BenchmarkRequestHandler(); _container = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => { - container.RegisterSingleton>( - _baselineHandler); + BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container); RegisterGFrameworkPipelineBehaviors(container, PipelineCount); }); _runtime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs index a552a233..deacac21 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamInvokerBenchmarks.cs @@ -83,7 +83,7 @@ public class StreamInvokerBenchmarks _generatedContainer = BenchmarkHostFactory.CreateFrozenGFrameworkContainer(container => { - container.RegisterCqrsHandlersFromAssembly(typeof(StreamInvokerBenchmarks).Assembly); + BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(container); }); _generatedRuntime = GFramework.Cqrs.CqrsRuntimeFactory.CreateRuntime( _generatedContainer, diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs new file mode 100644 index 00000000..7d761d03 --- /dev/null +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamLifetimeBenchmarks.cs @@ -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; + +/// +/// 对比 stream 完整枚举在不同 handler 生命周期下的额外开销。 +/// +/// +/// 当前矩阵只覆盖 `Singleton` 与 `Transient`。 +/// `Scoped` 仍依赖真实的显式作用域边界;在当前“单根容器最小宿主”模型下直接加入 scoped 会把枚举宿主成本与生命周期成本混在一起, +/// 因此保持与 request 生命周期矩阵相同的边界,留待后续 scoped host 基线具备后再扩展。 +/// +[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!; + + /// + /// 控制当前 benchmark 使用的 handler 生命周期。 + /// + [Params(HandlerLifetime.Singleton, HandlerLifetime.Transient)] + public HandlerLifetime Lifetime { get; set; } + + /// + /// 可公平比较的 benchmark handler 生命周期集合。 + /// + public enum HandlerLifetime + { + /// + /// 复用单个 handler 实例。 + /// + Singleton, + + /// + /// 每次建流都重新解析新的 handler 实例。 + /// + Transient + } + + /// + /// 配置 stream 生命周期 benchmark 的公共输出格式。 + /// + 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)); + } + } + + /// + /// 构建当前生命周期下的 GFramework 与 MediatR stream 对照宿主。 + /// + [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(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(); + } + + /// + /// 释放当前生命周期矩阵持有的 benchmark 宿主资源,并清理 dispatcher 缓存。 + /// + [GlobalCleanup] + public void Cleanup() + { + try + { + BenchmarkCleanupHelper.DisposeAll(_container, _serviceProvider); + } + finally + { + BenchmarkDispatcherCacheHelper.ClearDispatcherCaches(); + } + } + + /// + /// 直接调用 handler 并完整枚举,作为不同生命周期矩阵下的 dispatch 额外开销 baseline。 + /// + [Benchmark(Baseline = true)] + public async ValueTask Stream_Baseline() + { + await foreach (var response in _baselineHandler.Handle(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 GFramework.CQRS runtime 创建并完整枚举 stream。 + /// + [Benchmark] + public async ValueTask Stream_GFrameworkCqrs() + { + await foreach (var response in _runtime.CreateStream(BenchmarkContext.Instance, _request, CancellationToken.None) + .ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 通过 MediatR 创建并完整枚举 stream,作为外部对照。 + /// + [Benchmark] + public async ValueTask Stream_MediatR() + { + await foreach (var response in _mediatr.CreateStream(_request, CancellationToken.None).ConfigureAwait(false)) + { + _ = response; + } + } + + /// + /// 按生命周期把 benchmark stream handler 注册到 GFramework 容器。 + /// + /// 当前 benchmark 拥有并负责释放的容器。 + /// 待比较的 handler 生命周期。 + /// + /// 先通过 generated registry 提供静态 descriptor,再显式覆盖 handler 生命周期, + /// 可以把比较变量收敛到 handler 解析成本,而不是 descriptor 发现路径本身。 + /// + private static void RegisterGFrameworkHandler(MicrosoftDiContainer container, HandlerLifetime lifetime) + { + ArgumentNullException.ThrowIfNull(container); + + switch (lifetime) + { + case HandlerLifetime.Singleton: + container.RegisterSingleton< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + BenchmarkStreamHandler>(); + return; + + case HandlerLifetime.Transient: + container.RegisterTransient< + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + BenchmarkStreamHandler>(); + return; + + default: + throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, "Unsupported benchmark handler lifetime."); + } + } + + /// + /// 将 benchmark 生命周期映射为 MediatR 组装所需的 。 + /// + /// 待比较的 handler 生命周期。 + /// 当前生命周期对应的 MediatR 注册方式。 + 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.") + }; + } + + /// + /// Benchmark stream request。 + /// + /// 请求标识。 + /// 返回元素数量。 + public sealed record BenchmarkStreamRequest(Guid Id, int ItemCount) : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequest, + MediatR.IStreamRequest; + + /// + /// Benchmark stream response。 + /// + /// 响应标识。 + public sealed record BenchmarkResponse(Guid Id); + + /// + /// 同时实现 GFramework.CQRS 与 MediatR 契约的最小 stream handler。 + /// + public sealed class BenchmarkStreamHandler : + GFramework.Cqrs.Abstractions.Cqrs.IStreamRequestHandler, + MediatR.IStreamRequestHandler + { + /// + /// 处理 GFramework.CQRS stream request。 + /// + /// 当前 benchmark stream 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 完整枚举所需的低噪声异步响应序列。 + public IAsyncEnumerable Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 处理 MediatR stream request。 + /// + /// 当前 benchmark stream 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 完整枚举所需的低噪声异步响应序列。 + IAsyncEnumerable MediatR.IStreamRequestHandler.Handle( + BenchmarkStreamRequest request, + CancellationToken cancellationToken) + { + return EnumerateAsync(request, cancellationToken); + } + + /// + /// 为生命周期矩阵构造稳定、低噪声的异步响应序列。 + /// + /// 当前 benchmark 请求。 + /// 用于中断异步枚举的取消令牌。 + /// 按固定元素数量返回的异步响应序列。 + private static async IAsyncEnumerable 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); + } + } + } +} diff --git a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs index 8b3b1d94..8b886d29 100644 --- a/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs +++ b/GFramework.Cqrs.Benchmarks/Messaging/StreamingBenchmarks.cs @@ -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; /// @@ -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>( - _baselineHandler); + BenchmarkHostFactory.RegisterGeneratedBenchmarkRegistry(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(); + } } /// diff --git a/GFramework.Cqrs.Benchmarks/README.md b/GFramework.Cqrs.Benchmarks/README.md index 9fc28cbd..f589a0e3 100644 --- a/GFramework.Cqrs.Benchmarks/README.md +++ b/GFramework.Cqrs.Benchmarks/README.md @@ -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 / 建流对比继续扩展到更多场景 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs index 5eec00eb..7452eb84 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsDispatcherContextValidationTests.cs @@ -43,6 +43,63 @@ internal sealed class CqrsDispatcherContextValidationTests Throws.InvalidOperationException.With.Message.Contains("does not implement IArchitectureContext")); } + /// + /// 验证 request 上下文校验失败时, + /// 不会在调用点同步抛出,而是返回一个 faulted 保持既有异步失败语义。 + /// + [Test] + public void SendAsync_Should_Return_Faulted_ValueTask_When_Context_Preparation_Fails() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler))) + .Returns(new ContextAwareRequestHandler()); + container + .Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior))) + .Returns(false); + }); + + ValueTask 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")); + } + + /// + /// 验证 request handler 缺失时,dispatcher 仍返回 faulted , + /// 而不是在调用点同步抛出异常。 + /// + [Test] + public void SendAsync_Should_Return_Faulted_ValueTask_When_Handler_Is_Missing() + { + var runtime = CreateRuntime( + container => + { + container + .Setup(currentContainer => currentContainer.Get(typeof(IRequestHandler))) + .Returns((object?)null); + container + .Setup(currentContainer => currentContainer.HasRegistration(typeof(IPipelineBehavior))) + .Returns(false); + container + .Setup(currentContainer => currentContainer.GetAll(typeof(IPipelineBehavior))) + .Returns(Array.Empty()); + }); + + ValueTask 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")); + } + /// /// 验证当 notification handler 需要上下文注入、但当前 CQRS 上下文不实现 时, /// dispatcher 会在发布前显式失败。 diff --git a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs index 07204bbf..15273d96 100644 --- a/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs +++ b/GFramework.Cqrs.Tests/Cqrs/CqrsGeneratedRequestInvokerProviderTests.cs @@ -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)])); } + /// + /// 验证 direct generated-registry 激活入口只会接入指定 registry,而不会顺手把同一测试程序集里的其他 registry 一并注册。 + /// + [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(); + var streamProviders = container.GetAll(); + + Assert.Multiple(() => + { + Assert.That( + requestProviders.Select(static provider => provider.GetType()), + Is.EqualTo([typeof(GeneratedRequestInvokerProviderRegistry)])); + Assert.That(streamProviders, Is.Empty); + }); + } + /// /// 验证当实现类型隐藏、但 stream handler interface 仍可直接表达时, /// registrar 仍会把 generated stream invoker provider 注册到容器中。 diff --git a/GFramework.Cqrs/Internal/CqrsDispatcher.cs b/GFramework.Cqrs/Internal/CqrsDispatcher.cs index a5ba1c80..aa1e6d24 100644 --- a/GFramework.Cqrs/Internal/CqrsDispatcher.cs +++ b/GFramework.Cqrs/Internal/CqrsDispatcher.cs @@ -105,36 +105,43 @@ internal sealed class CqrsDispatcher( /// 请求对象。 /// 取消令牌。 /// 请求响应。 - public async ValueTask SendAsync( + public ValueTask SendAsync( ICqrsContext context, IRequest request, CancellationToken cancellationToken = default) { - ArgumentNullException.ThrowIfNull(context); - ArgumentNullException.ThrowIfNull(request); - - var requestType = request.GetType(); - var dispatchBinding = GetRequestDispatchBinding(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(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(exception); } - - return await dispatchBinding.GetPipelineExecutor(behaviors.Count) - .Invoke(handler, behaviors, request, cancellationToken) - .ConfigureAwait(false); } /// diff --git a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs index 3607771c..a332927c 100644 --- a/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs +++ b/GFramework.Cqrs/Internal/CqrsHandlerRegistrar.cs @@ -68,6 +68,36 @@ internal static class CqrsHandlerRegistrar } } + /// + /// 直接激活并注册单个 generated registry,避免调用方为了只接入一个 benchmark registry + /// 而额外扫描同一程序集里的其他 registry / handler。 + /// + /// 承载 generated registry 注册结果的目标容器。 + /// 要直接激活的 generated registry 类型。 + /// 当前注册过程使用的日志记录器。 + /// + /// 。 + /// + /// 指定 registry 类型不满足 generated registry 运行时契约。 + 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); + } + /// /// 优先使用程序集级源码生成注册器完成 CQRS 映射注册。 /// diff --git a/GFramework.Cqrs/Properties/AssemblyInfo.cs b/GFramework.Cqrs/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..0572e1c6 --- /dev/null +++ b/GFramework.Cqrs/Properties/AssemblyInfo.cs @@ -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")] diff --git a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md index 56bae975..59291782 100644 --- a/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md +++ b/ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md @@ -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)` 零行为探测方案 ## 活跃文档 diff --git a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md index a215f20d..d2219d2e 100644 --- a/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md +++ b/ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md @@ -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(...)` 恢复旧的 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`