mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-11 20:38:58 +08:00
test(cqrs-benchmarks): 隔离并发 benchmark 运行工件
- 新增 benchmark 入口 artifacts suffix 解析与独立 host 工作目录重启逻辑 - 更新 benchmark README 并发运行约定,补充隔离命令示例 - 更新 cqrs-rewrite 恢复文档,记录并发验证结果与后续恢复点
This commit is contained in:
parent
594798dcb9
commit
0baa662ae4
@ -1,6 +1,11 @@
|
||||
// Copyright (c) 2025-2026 GeWuYou
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using BenchmarkDotNet.Configs;
|
||||
using BenchmarkDotNet.Loggers;
|
||||
using BenchmarkDotNet.Running;
|
||||
|
||||
@ -11,13 +16,243 @@ namespace GFramework.Cqrs.Benchmarks;
|
||||
/// </summary>
|
||||
internal static class Program
|
||||
{
|
||||
private const string ArtifactsSuffixOption = "--artifacts-suffix";
|
||||
private const string ArtifactsSuffixEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ARTIFACTS_SUFFIX";
|
||||
private const string ArtifactsPathEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ARTIFACTS_PATH";
|
||||
private const string IsolatedHostEnvironmentVariable = "GFRAMEWORK_CQRS_BENCHMARK_ISOLATED_HOST";
|
||||
private const string DefaultArtifactsDirectoryName = "BenchmarkDotNet.Artifacts";
|
||||
private const string IsolatedHostDirectoryName = "host";
|
||||
|
||||
/// <summary>
|
||||
/// 运行当前程序集中的全部 benchmark。
|
||||
/// </summary>
|
||||
/// <param name="args">透传给 BenchmarkDotNet 的命令行参数。</param>
|
||||
/// <param name="args">仓库入口参数与透传给 BenchmarkDotNet 的命令行参数。</param>
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
var invocation = ParseInvocation(args);
|
||||
|
||||
ConsoleLogger.Default.WriteLine("Running GFramework.Cqrs benchmarks");
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
|
||||
|
||||
if (invocation.ArtifactsSuffix is not null &&
|
||||
!string.Equals(
|
||||
Environment.GetEnvironmentVariable(IsolatedHostEnvironmentVariable),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Environment.Exit(RunFromIsolatedHost(invocation, args));
|
||||
}
|
||||
|
||||
if (invocation.ArtifactsPath is null)
|
||||
{
|
||||
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(invocation.BenchmarkDotNetArguments);
|
||||
return;
|
||||
}
|
||||
|
||||
ConsoleLogger.Default.WriteLine(
|
||||
$"Using isolated BenchmarkDotNet artifacts path: {invocation.ArtifactsPath}");
|
||||
|
||||
BenchmarkSwitcher
|
||||
.FromAssembly(typeof(Program).Assembly)
|
||||
.Run(invocation.BenchmarkDotNetArguments, DefaultConfig.Instance.WithArtifactsPath(invocation.ArtifactsPath));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析仓库自定义参数,并生成实际传递给 BenchmarkDotNet 的参数与隔离后的 artifacts 路径。
|
||||
/// </summary>
|
||||
/// <param name="args">当前进程收到的完整命令行参数。</param>
|
||||
/// <returns>入口解析后的 benchmark 调用选项。</returns>
|
||||
/// <exception cref="ArgumentException">自定义参数缺失值或包含非法路径片段时抛出。</exception>
|
||||
private static BenchmarkInvocation ParseInvocation(string[] args)
|
||||
{
|
||||
var benchmarkDotNetArguments = new List<string>(args.Length);
|
||||
string? commandLineSuffix = null;
|
||||
|
||||
for (var index = 0; index < args.Length; index++)
|
||||
{
|
||||
var argument = args[index];
|
||||
if (!string.Equals(argument, ArtifactsSuffixOption, StringComparison.Ordinal))
|
||||
{
|
||||
benchmarkDotNetArguments.Add(argument);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (index == args.Length - 1)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {ArtifactsSuffixOption} option requires a suffix value.",
|
||||
nameof(args));
|
||||
}
|
||||
|
||||
if (commandLineSuffix is not null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {ArtifactsSuffixOption} option can only be provided once.",
|
||||
nameof(args));
|
||||
}
|
||||
|
||||
// 剥离仓库自定义参数,避免将它误传给 BenchmarkDotNet 自身的命令行解析器。
|
||||
commandLineSuffix = args[++index];
|
||||
}
|
||||
|
||||
var artifactsPath = ResolveArtifactsPath(commandLineSuffix);
|
||||
return new BenchmarkInvocation(benchmarkDotNetArguments.ToArray(), commandLineSuffix, artifactsPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 benchmark 入口重启到独立的宿主工作目录,避免多个并发进程共享同一份 auto-generated build 目录。
|
||||
/// </summary>
|
||||
/// <param name="invocation">当前入口解析后的 benchmark 调用选项。</param>
|
||||
/// <param name="originalArgs">原始命令行参数,用于透传给隔离后的宿主进程。</param>
|
||||
/// <returns>隔离后宿主进程的退出码。</returns>
|
||||
private static int RunFromIsolatedHost(BenchmarkInvocation invocation, string[] originalArgs)
|
||||
{
|
||||
var artifactsPath = invocation.ArtifactsPath
|
||||
?? throw new ArgumentNullException(nameof(invocation), "An isolated benchmark host requires an artifacts path.");
|
||||
|
||||
var currentAssemblyPath = typeof(Program).Assembly.Location;
|
||||
var sourceHostDirectory = AppContext.BaseDirectory;
|
||||
var isolatedHostDirectory = Path.Combine(artifactsPath, IsolatedHostDirectoryName);
|
||||
|
||||
PrepareIsolatedHostDirectory(sourceHostDirectory, isolatedHostDirectory);
|
||||
|
||||
var isolatedAssemblyPath = Path.Combine(
|
||||
isolatedHostDirectory,
|
||||
Path.GetFileName(currentAssemblyPath));
|
||||
|
||||
var startInfo = new ProcessStartInfo("dotnet")
|
||||
{
|
||||
WorkingDirectory = isolatedHostDirectory,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add(isolatedAssemblyPath);
|
||||
foreach (var argument in originalArgs)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
startInfo.Environment[IsolatedHostEnvironmentVariable] = "1";
|
||||
startInfo.Environment[ArtifactsPathEnvironmentVariable] = artifactsPath;
|
||||
|
||||
ConsoleLogger.Default.WriteLine(
|
||||
$"Launching isolated benchmark host in: {isolatedHostDirectory}");
|
||||
|
||||
using var process = Process.Start(startInfo) ??
|
||||
throw new InvalidOperationException("Failed to launch the isolated benchmark host process.");
|
||||
|
||||
process.WaitForExit();
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据命令行或环境变量中的 suffix 生成当前 benchmark 运行的独立 artifacts 目录。
|
||||
/// </summary>
|
||||
/// <param name="commandLineSuffix">命令行显式提供的 suffix。</param>
|
||||
/// <returns>隔离后的 artifacts 目录;若未提供 suffix,则返回 <see langword="null"/>。</returns>
|
||||
private static string? ResolveArtifactsPath(string? commandLineSuffix)
|
||||
{
|
||||
var explicitArtifactsPath = Environment.GetEnvironmentVariable(ArtifactsPathEnvironmentVariable);
|
||||
if (!string.IsNullOrWhiteSpace(explicitArtifactsPath))
|
||||
{
|
||||
return Path.GetFullPath(explicitArtifactsPath);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(commandLineSuffix))
|
||||
{
|
||||
var validatedCommandLineSuffix = ValidateArtifactsSuffix(
|
||||
commandLineSuffix,
|
||||
ArtifactsSuffixOption);
|
||||
|
||||
return Path.GetFullPath(Path.Combine(DefaultArtifactsDirectoryName, validatedCommandLineSuffix));
|
||||
}
|
||||
|
||||
var environmentSuffix = Environment.GetEnvironmentVariable(ArtifactsSuffixEnvironmentVariable);
|
||||
if (string.IsNullOrWhiteSpace(environmentSuffix))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var validatedEnvironmentSuffix = ValidateArtifactsSuffix(
|
||||
environmentSuffix,
|
||||
ArtifactsSuffixEnvironmentVariable);
|
||||
|
||||
return Path.GetFullPath(Path.Combine(DefaultArtifactsDirectoryName, validatedEnvironmentSuffix));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验自定义 suffix,避免路径穿越、分隔符注入或不可移植字符污染 BenchmarkDotNet 的输出目录。
|
||||
/// </summary>
|
||||
/// <param name="suffix">待校验的后缀值。</param>
|
||||
/// <param name="sourceName">后缀来源名称,用于错误提示。</param>
|
||||
/// <returns>可安全用于单级目录名的后缀。</returns>
|
||||
/// <exception cref="ArgumentException">当后缀为空或包含未允许字符时抛出。</exception>
|
||||
private static string ValidateArtifactsSuffix(string suffix, string sourceName)
|
||||
{
|
||||
var trimmedSuffix = suffix.Trim();
|
||||
if (trimmedSuffix.Length == 0)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
$"The {sourceName} value must not be empty.",
|
||||
nameof(suffix));
|
||||
}
|
||||
|
||||
foreach (var character in trimmedSuffix)
|
||||
{
|
||||
if (char.IsAsciiLetterOrDigit(character) || character is '.' or '-' or '_')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new ArgumentException(
|
||||
$"The {sourceName} value '{trimmedSuffix}' contains unsupported characters. " +
|
||||
"Only ASCII letters, digits, '.', '-' and '_' are allowed.",
|
||||
nameof(suffix));
|
||||
}
|
||||
|
||||
return trimmedSuffix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将当前 benchmark 宿主输出复制到独立目录,确保并发运行时的 auto-generated benchmark 项目不会写入同一路径。
|
||||
/// </summary>
|
||||
/// <param name="sourceHostDirectory">当前 benchmark 宿主输出目录。</param>
|
||||
/// <param name="isolatedHostDirectory">当前 suffix 对应的独立宿主目录。</param>
|
||||
private static void PrepareIsolatedHostDirectory(string sourceHostDirectory, string isolatedHostDirectory)
|
||||
{
|
||||
Directory.CreateDirectory(isolatedHostDirectory);
|
||||
CopyDirectoryRecursively(sourceHostDirectory, isolatedHostDirectory);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归复制 benchmark 宿主输出目录,覆盖同名文件以支持同一 suffix 的重复运行。
|
||||
/// </summary>
|
||||
/// <param name="sourceDirectory">源目录。</param>
|
||||
/// <param name="destinationDirectory">目标目录。</param>
|
||||
private static void CopyDirectoryRecursively(string sourceDirectory, string destinationDirectory)
|
||||
{
|
||||
foreach (var directory in Directory.GetDirectories(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativeDirectory = Path.GetRelativePath(sourceDirectory, directory);
|
||||
Directory.CreateDirectory(Path.Combine(destinationDirectory, relativeDirectory));
|
||||
}
|
||||
|
||||
foreach (var file in Directory.GetFiles(sourceDirectory, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relativeFile = Path.GetRelativePath(sourceDirectory, file);
|
||||
var destinationFile = Path.Combine(destinationDirectory, relativeFile);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)!);
|
||||
File.Copy(file, destinationFile, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示一次 benchmark 入口调用在剥离仓库自定义参数后的最终配置。
|
||||
/// </summary>
|
||||
/// <param name="BenchmarkDotNetArguments">实际传递给 BenchmarkDotNet 的命令行参数。</param>
|
||||
/// <param name="ArtifactsSuffix">当前运行声明的隔离后缀;若未声明则为 <see langword="null"/>。</param>
|
||||
/// <param name="ArtifactsPath">本次运行的 artifacts 目录;若未隔离则为 <see langword="null"/>。</param>
|
||||
private readonly record struct BenchmarkInvocation(
|
||||
string[] BenchmarkDotNetArguments,
|
||||
string? ArtifactsSuffix,
|
||||
string? ArtifactsPath);
|
||||
}
|
||||
|
||||
@ -56,9 +56,17 @@ dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.cspro
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --filter "*StreamLifetimeBenchmarks.Stream_*"
|
||||
```
|
||||
|
||||
如果需要在两个终端里并发复核不同的过滤 benchmark,请为每个进程追加不同的 `--artifacts-suffix <suffix>`,把 `BenchmarkDotNet` auto-generated build 与 artifacts 输出隔离到不同目录;这只是运行入口的目录隔离约定,不是 benchmark 业务逻辑本身的要求。例如:
|
||||
|
||||
```bash
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*"
|
||||
dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-lifetime-b --filter "*StreamLifetimeBenchmarks.Stream_*"
|
||||
```
|
||||
|
||||
## 当前约束
|
||||
|
||||
- `BenchmarkDotNet.Artifacts/` 属于本地生成输出,默认加入仓库忽略,不作为常规提交内容
|
||||
- 当两个带 `--filter` 的 benchmark 进程需要并发运行时,必须为它们分别传入不同的 `--artifacts-suffix <suffix>`,避免多个 `BenchmarkDotNet` 进程写入同一份 auto-generated build / artifacts 目录;这个约束只服务于本地输出隔离,不代表 benchmark 场景之间存在额外业务依赖
|
||||
- `RequestLifetimeBenchmarks` 现在复用与默认 generated-provider 路径一致的 benchmark 宿主接线;它比较的是生命周期切换后的 handler 解析与 dispatch 成本,不单独引入另一套 runtime 发现口径
|
||||
- `RequestLifetimeBenchmarks` 的 `Scoped` 场景会在每次 request 分发时显式创建并释放真实 DI 作用域,用来观察 scoped handler 绑定到 request 边界后的解析与 dispatch 成本
|
||||
- `StreamLifetimeBenchmarks` 现在按 direct handler、`GFramework.Cqrs` reflection、`GFramework.Cqrs` generated、`MediatR` 四层口径组织,并额外区分 `FirstItem` 与 `DrainAll` 两种观测方式,用于把 stream 建流/首个元素成本与完整枚举成本拆开观察
|
||||
|
||||
@ -7,23 +7,22 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 当前恢复点
|
||||
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-130`
|
||||
- 恢复点编号:`CQRS-REWRITE-RP-131`
|
||||
- 当前阶段:`Phase 8`
|
||||
- 当前 PR 锚点:`待重新抓取`
|
||||
- 当前结论:
|
||||
- 当前 `RP-130` 延续 `$gframework-batch-boot 50`,并在上一波 `StreamingBenchmarks`、`StreamInvokerBenchmarks`、`RequestLifetimeBenchmarks` 扩口径之后,继续把 `StreamLifetimeBenchmarks` 的生命周期矩阵补齐到真实 `Scoped` stream 作用域,同时把 benchmark `README` 与当前矩阵同步
|
||||
- 本轮继续沿用已复核的基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`);当前分支连同本轮变更相对 `origin/main` 的累计 branch diff 约为 `9 files changed, 953 insertions(+), 83 deletions(-)`,离 `$gframework-batch-boot 50` 还有明显余量
|
||||
- 本轮写面收敛在 benchmark 层三处:`GFramework.Cqrs.Benchmarks/Messaging/BenchmarkHostFactory.cs`、`StreamLifetimeBenchmarks.cs`、`GFramework.Cqrs.Benchmarks/README.md`;没有扩散到 `GFramework.Cqrs` runtime 或测试项目
|
||||
- `BenchmarkHostFactory` 现在提供 `CreateScopedGFrameworkStream<TResponse>(...)` 与 `CreateScopedMediatRStream<TResponse>(...)`,通过显式 scope 包裹整个 async stream 枚举周期,避免 scoped handler 在建流后提前释放或错误回落到根容器语义
|
||||
- `StreamLifetimeBenchmarks` 当前矩阵已完整覆盖 `Singleton / Scoped / Transient` 与 `FirstItem / DrainAll`:
|
||||
- `Singleton` 下 `DrainAll` 仍表现为 generated 略优于 reflection,约 `131.96 ns` 对 `134.26 ns`
|
||||
- `Scoped` 下已经可以稳定产出真实作用域矩阵;`FirstItem` 约为 baseline `56.24 ns`、`MediatR` `338.82 ns`、reflection `612.49 ns`、generated `628.65 ns`,`DrainAll` 约为 baseline `81.20 ns`、`MediatR` `428.66 ns`、generated `692.05 ns`、reflection `716.61 ns`
|
||||
- `Transient` 下 `FirstItem` 约为 generated `107.79 ns`、reflection `112.60 ns`,但 `DrainAll` 仍是 reflection `131.41 ns` 略优于 generated `135.95 ns`
|
||||
- `README` 已同步三类 benchmark 现状:`RequestLifetimeBenchmarks` 与 `StreamLifetimeBenchmarks` 都包含真实 `Scoped` 生命周期,`StreamingBenchmarks` / `StreamInvokerBenchmarks` 都显式区分 `FirstItem / DrainAll`,且 `StreamInvokerBenchmarks` 的 `DrainAll` short-job 输出被明确标注为 smoke-only,不作为稳定排序结论
|
||||
- 本轮再次确认:此前并行运行两个 BenchmarkDotNet `dotnet run --no-build` 过滤命令时出现的冲突属于 benchmark 工件/生成目录层面的运行隔离问题,而不是 `Fixture`、`StreamInvokerBenchmarks` 或 `StreamLifetimeBenchmarks` 的业务逻辑错误
|
||||
- 当前 `RP-131` 继续沿用 `$gframework-batch-boot 50`,并把上一波定位到的 BenchmarkDotNet 并行运行冲突真正收口到 benchmark 入口层:`Program.cs` 现在支持 `--artifacts-suffix <suffix>`,并在声明 suffix 时自动把当前 benchmark 运行重启到独立的 host 工作目录
|
||||
- 本轮继续沿用已复核的基线:`origin/main` 与本地 `main` 当前都在 `699d0b48`(`2026-05-09 18:39:38 +0800`);当前分支相对 `origin/main` 的累计 branch diff 仍约为 `9 files changed, 953 insertions(+), 83 deletions(-)`,离 `$gframework-batch-boot 50` 还有明显余量
|
||||
- 本轮写面收敛在 benchmark 入口与文档两处:`GFramework.Cqrs.Benchmarks/Program.cs` 与 `GFramework.Cqrs.Benchmarks/README.md`;没有扩散到 benchmark 业务逻辑文件、`GFramework.Cqrs` runtime 或测试项目
|
||||
- `Program.cs` 当前约定为:
|
||||
- 解析并剥离仓库自定义参数 `--artifacts-suffix <suffix>`,避免把它误传给 BenchmarkDotNet CLI
|
||||
- 支持通过环境变量回退复用同一套 suffix / artifacts path 约定
|
||||
- 当 suffix 存在时,先把当前 benchmark 宿主输出复制到 `BenchmarkDotNet.Artifacts/<suffix>/host/`,再从该隔离宿主目录重启运行,使 BenchmarkDotNet 自动生成的 `GFramework.Cqrs.Benchmarks-Job-*` 项目、`OutDir` 与最终 results 都落到 suffix 私有目录下
|
||||
- 并发 smoke 已直接证明这套隔离生效:`RequestLifetimeBenchmarks.SendRequest_*` 与 `StreamInvokerBenchmarks.Stream_*` 在两个终端并发 short-job 时,分别落到 `BenchmarkDotNet.Artifacts/req-lifetime-a/host/...` 与 `BenchmarkDotNet.Artifacts/stream-invoker-b/host/...`,不再共享同一 `.../bin/Release/net10.0/GFramework.Cqrs.Benchmarks-Job-JWUHXL-1/` 生成目录,也没有再出现 `.dll.config being used by another process`
|
||||
- `README` 已同步新的运行约定:当两个带 `--filter` 的 benchmark 需要并发执行时,必须为每个进程传入不同的 `--artifacts-suffix`;该约束只服务于本地输出隔离,不代表 benchmark 业务语义本身需要额外依赖
|
||||
- 下一推荐步骤:
|
||||
- 下一批优先切到 `GFramework.Cqrs.Benchmarks` 的 benchmark-run/config 隔离层,避免两个过滤 benchmark 并行执行时再次共享自动生成目录或 artifacts 路径
|
||||
- 完成运行隔离后,再决定是否单开一波更稳定的 `StreamInvokerBenchmarks` `DrainAll` 复核,或继续把 scoped host 对照扩展到更多 stream 子场景
|
||||
- 若继续 benchmark 线,可重新利用新的并发运行隔离约定,单开一波更稳定的 `StreamInvokerBenchmarks` `DrainAll` 排序复核,或并行推进其他不冲突的 benchmark smoke
|
||||
- 若上下文预算仍允许,下一批更适合继续保持在 `GFramework.Cqrs.Benchmarks` 单模块,避免过早把 review 面重新扩到 runtime 或测试层
|
||||
- 更早的 `RP-123` 及之前阶段细节以下方 trace 与归档为准,active 入口不再重复展开旧阶段流水。
|
||||
- 当前分支相对 `origin/main` 的累计 branch diff 启动时为 `9 files`,仍明显低于 `$gframework-batch-boot 50` 的停止阈值;这一批继续保持单模块、低风险、可直接评审的 benchmark 边界
|
||||
- 当前 `RP-113` 已继续沿用 `$gframework-batch-boot 50`,并把 notification 线从 benchmark 对照推进到实际 runtime 能力:新增公开内置 `TaskWhenAllNotificationPublisher`,让 `GFramework.Cqrs` 在保留默认顺序发布器的同时,提供与 `Mediator` `TaskWhenAllPublisher` 对齐的并行 notification publish 策略
|
||||
@ -159,6 +158,20 @@ CQRS 迁移与收敛。
|
||||
|
||||
## 最近权威验证
|
||||
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:覆盖 benchmark 入口 `--artifacts-suffix` 隔离实现、`README` 命令示例与 `ai-plan` 更新后的最小 Release build
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:本次 auto-generated benchmark 项目已在 `BenchmarkDotNet.Artifacts/req-lifetime-a/host/...` 下执行;`Singleton` 约为 baseline `5.189 ns`、`MediatR` `52.765 ns`、`GFramework.Cqrs` `60.938 ns`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-invoker-b --filter "*StreamInvokerBenchmarks.Stream_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:本次 auto-generated benchmark 项目已在 `BenchmarkDotNet.Artifacts/stream-invoker-b/host/...` 下执行;`DrainAll` 约为 baseline `77.82 ns`、generated `130.37 ns`、reflection `139.08 ns`、`MediatR` `245.23 ns`
|
||||
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Program.cs GFramework.Cqrs.Benchmarks/README.md ai-plan/public/cqrs-rewrite/todos/cqrs-rewrite-migration-tracking.md ai-plan/public/cqrs-rewrite/traces/cqrs-rewrite-migration-trace.md`
|
||||
- 结果:通过
|
||||
- `git --git-dir=<repo>/.git/worktrees/GFramework-cqrs --work-tree=. diff --check`
|
||||
- 结果:通过
|
||||
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:覆盖 `BenchmarkHostFactory` scoped stream helper、`StreamLifetimeBenchmarks` scoped 生命周期矩阵与 `README` 同步后的最小 Release build
|
||||
|
||||
@ -2,6 +2,38 @@
|
||||
|
||||
## 2026-05-11
|
||||
|
||||
### 阶段:benchmark 并发运行隔离入口(CQRS-REWRITE-RP-131)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在 `RP-130` 已确认冲突来自 BenchmarkDotNet 工件/生成目录层后,本轮继续保持写面只在 `GFramework.Cqrs.Benchmarks` 单模块,优先收口 benchmark 入口而不是回头改 benchmark 业务逻辑
|
||||
- 本轮主线程与 worker 边界:
|
||||
- `README.md` 由 worker 独占,补 `--artifacts-suffix <suffix>` 的使用约定与两条并发 smoke 命令示例
|
||||
- `Program.cs` 起初交给独立 worker,但其回传超时;主线程随后接管该单文件实现,并主动关闭未返回的 worker,避免继续消耗上下文预算
|
||||
- 本轮主线程关键修正:
|
||||
- 首版只设置 `IConfig.ArtifactsPath`,虽然把结果导出目录隔离到了 `BenchmarkDotNet.Artifacts/<suffix>`,但并行 smoke 立刻暴露出 auto-generated benchmark 项目仍写回共享 `bin/Release/net10.0/GFramework.Cqrs.Benchmarks-Job-JWUHXL-1/` 目录,`RequestLifetimeBenchmarks` 再次命中 `.dll.config being used by another process`
|
||||
- 因此本轮把实现升级为“独立宿主目录重启”模型:当存在 `--artifacts-suffix` 时,`Program.cs` 会先把当前 benchmark 宿主输出复制到 `BenchmarkDotNet.Artifacts/<suffix>/host/`,再从该目录重新启动同一个程序集,并通过环境变量把隔离后的 artifacts path 传递给子进程
|
||||
- 最终子进程里的 BenchmarkDotNet restore/build/output 路径均落到 `BenchmarkDotNet.Artifacts/<suffix>/host/GFramework.Cqrs.Benchmarks-Job-...`,从根上切断同名 job 目录在并发进程之间的共享
|
||||
- 本轮验证:
|
||||
- `dotnet build GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release`
|
||||
- 结果:通过,`0 warning / 0 error`
|
||||
- 备注:中途曾引入 `MA0015`,已在同一轮把 `ThrowIfNull` 改成显式局部变量判空后清零
|
||||
- `python3 scripts/license-header.py --check --paths GFramework.Cqrs.Benchmarks/Program.cs GFramework.Cqrs.Benchmarks/README.md`
|
||||
- 结果:通过
|
||||
- `git diff --check`
|
||||
- 结果:通过
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix req-lifetime-a --filter "*RequestLifetimeBenchmarks.SendRequest_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:实际执行路径已切到 `BenchmarkDotNet.Artifacts/req-lifetime-a/host/...`
|
||||
- `dotnet run --project GFramework.Cqrs.Benchmarks/GFramework.Cqrs.Benchmarks.csproj -c Release --no-build -- --artifacts-suffix stream-invoker-b --filter "*StreamInvokerBenchmarks.Stream_*" --job short --warmupCount 1 --iterationCount 1 --launchCount 1`
|
||||
- 结果:通过
|
||||
- 备注:实际执行路径已切到 `BenchmarkDotNet.Artifacts/stream-invoker-b/host/...`
|
||||
- 两条命令并发运行时未再出现 `.dll.config being used by another process`,说明冲突已经从入口层实质收口
|
||||
- 本轮结论:
|
||||
- `--artifacts-suffix` 现在不只是“结果目录标签”,而是完整的并发运行隔离开关:它会同时隔离 BenchmarkDotNet 最终导出目录与 auto-generated benchmark 项目工作目录
|
||||
- 这条修复只触及 benchmark 入口与文档,不影响 `Fixture`、generated registry、runtime 宿主或 benchmark 业务语义,因此评审面仍保持单模块、低风险边界
|
||||
- 下一恢复点:
|
||||
- 若继续 benchmark 线,可直接复用新的并发隔离能力,同时跑互不冲突的 filtered short-job,而不必再人为串行化所有 BenchmarkDotNet smoke
|
||||
- 优先候选仍是 `StreamInvokerBenchmarks` `DrainAll` 更稳定作业复核,或其他仅触达 `GFramework.Cqrs.Benchmarks` 的对照矩阵扩展
|
||||
|
||||
### 阶段:stream lifetime scoped 矩阵与 README 同步(CQRS-REWRITE-RP-130)
|
||||
|
||||
- 延续 `$gframework-batch-boot 50`,在上一波把 `StreamingBenchmarks`、`StreamInvokerBenchmarks` 与 `RequestLifetimeBenchmarks` 扩成双观测 / scoped-request 基线后,本轮先不回到 runtime,而是只补齐 `StreamLifetimeBenchmarks` 的真实 `Scoped` stream 生命周期矩阵,并同步 benchmark `README`
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user