mcpField
B
build-perf-baseline

build-perf-baseline

github.com/dotnet开发5.8k3.6k 次安装1 天前更新
代码仓库

关于 build-perf-baseline

面向 .NET 开发者的构建性能技能,指导测量冷构建、热构建和空操作构建基线,并通过产物输出、确定性构建、依赖图和并行度优化构建。

主要功能

  1. 01测量冷、热和空操作基线
  2. 02使用 binlog 分析性能
  3. 03配置产物输出目录
  4. 04确定性构建指导
  5. 05调整依赖图与并行度

适用场景

  1. 01比较优化前后的构建性能
  2. 02排查增量或空操作构建缓慢
  3. 03调整大型多项目构建图

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill build-perf-baseline

安装后在 Claude Code 中输入/build-perf-baseline确认已加载。

详细介绍

构建性能基线与优化

概述

优化构建之前,需要先建立基线。没有测量,优化就只是猜测。本技能介绍如何建立基线并运用系统化的优化技术。

相关技能:

  • build-perf-diagnostics — 基于 binlog 识别瓶颈
  • incremental-build — Inputs/Outputs 和最新状态检查
  • build-parallelism — 并行构建和图构建调优
  • eval-performance — glob 和导入链优化

步骤 1:建立性能基线

测量三种场景,了解时间花在了哪里。保持 SDK、 配置、机器、环境变量、还原状态和构建命令一致。每种场景至少运行三次;每次测量前都要重复其准备步骤,并报告中位数及观测范围。单次计时不能构成基线。准备工作应在计时区间之外完成。将每次测量的 binlog 保存为唯一名称,并放在生成的输出目录之外。

冷构建(首次构建)

不存在之前的构建输出。测量包括还原、编译和所有目标在内的完整端到端耗时。

每次冷构建测量前,都要恢复相同的源代码状态,使用被测配置运行 dotnet clean,并仅删除已确认可丢弃的被测项目输出目录和中间目录。除了 bin 和 obj,还要包括自定义构件路径;删除前须获得批准。在下一次构建计时前,确认这些输出不存在。

这是一次输出冷启动构建,并不一定意味着 NuGet、操作系统或编译器服务器缓存是冷的。为所有测量选择并记录一致的缓存/服务器策略;不要清除共享缓存。如果要测量未缓存的还原,请为每次测量使用单独的空软件包缓存。

# After repeating the cold setup; use a unique log name for each sample
dotnet build /bl:cold-build-1.binlog -m
热构建(增量构建)

已有构建输出,部分文件已更改。测量增量构建的效果。

每次热构建测量前,都要恢复相同的基线源代码内容,并在不计时的情况下成功构建。然后,对同一个源文件应用相同的小幅且与构建相关的编辑,再计时构建。所有测量中,已更改的文件集合和编辑内容都应保持一致;不要让编辑累积。下一次测量前,恢复基线内容,并在计时区间之外重新构建。准备构建和被测构建之间不要执行清理。

# Untimed setup after restoring baseline source contents
dotnet build -m

# Apply the same controlled source edit, then measure with a unique log name
dotnet build /bl:warm-build-1.binlog -m
无操作构建(没有任何更改)

已有构建输出,没有任何更改。编译和正确实现增量处理的目标应跳过;将计时结果与此构建的其他测量结果进行比较。

每次无操作构建测量前,都要恢复相同的基线源代码内容,并成功运行一次不计时的准备构建。然后,在不编辑文件、不触碰输入、不清理输出或更改属性的情况下,测量一次内容相同的构建。还原以及缓存/服务器策略应与其他测量保持一致。

# Untimed setup after restoring baseline source contents
dotnet build -m

# Rebuild immediately without changes; use a unique log name for each sample
dotnet build /bl:noop-build-1.binlog -m
良好表现的标准
场景预期行为
冷构建完整编译,运行所有目标。这是绝对基线
热构建仅重新编译已更改的项目。耗时与更改范围成正比
无操作构建编译和正确实现增量处理的自定义目标均跳过;将耗时与此仓库反复测得的热构建和冷构建结果进行比较

警示信号:

  • 无操作构建的耗时反复接近热构建/冷构建,或编译目标再次运行 → 调查增量处理(参见 incremental-build)
  • 热构建重新编译所有内容 → 项目依赖链导致完整重建
  • 还原占据冷构建测量的大部分时间 → 在更改项目结构前,分别测量 dotnet restore 和 dotnet build --no-restore

不要使用通用的时长或百分比阈值来判定瓶颈。 应依据受控测量结果以及构建自身的目标/任务 耗时对成本进行排序。

捕获分析器证据

binlog 会显示编译器/任务的耗时,但要获取细粒度的分析器耗时,需要运行分析器报告。在 SDK/编译器支持时,捕获:

dotnet build /bl:analyzers.binlog /p:ReportAnalyzer=true

在 MSBuild Structured Log Viewer 中打开 binlog,并检查编译器任务下的分析器摘要。如果无法获取细粒度耗时,可使用 /p:RunAnalyzers=false 对其他条件完全相同的样本进行比较,作为归因实验;不要把禁用分析器当作解决方案。在 CI 中保留分析器强制检查。

记录基线

在优化前后以结构化方式记录基线:

| Scenario    | Before  | After   | Improvement |
|-------------|---------|---------|-------------|
| Cold build  | 2m 15s  |         |             |
| Warm build  | 1m 40s  |         |             |
| No-op build | 45s     |         |             |

步骤 2:Artifacts 输出布局

UseArtifactsOutput 功能(在 .NET 8 中引入)会更改输出目录结构,以避免 bin/obj 冲突问题并改善缓存效果。

启用 Artifacts 输出

<PropertyGroup>
  <UseArtifactsOutput>true</UseArtifactsOutput>
</PropertyGroup>
前后对比
# Traditional layout (before)
src/
  MyLib/
    bin/Debug/net8.0/MyLib.dll
    obj/Debug/net8.0/...
  MyApp/
    bin/Debug/net8.0/MyApp.dll

# Artifacts layout (after)
artifacts/
  bin/MyLib/debug/MyLib.dll
  bin/MyApp/debug/MyApp.dll
  obj/MyLib/debug/...
  obj/MyApp/debug/...
优势
  • 避免 bin/obj 冲突:每个项目和配置都会自动获得唯一的路径
  • 更易缓存:只需在 CI 中缓存/还原单个 artifacts/ 目录
  • .gitignore 更简洁:只需忽略 artifacts/
  • 安全支持多目标框架:每个 TFM 都有自己的子目录
自定义

<PropertyGroup>
  <ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath>
</PropertyGroup>

步骤 3:确定性构建

确定性构建在输入相同时会生成逐字节完全相同的输出。这对于构建缓存和可复现性至关重要。

启用确定性构建

<PropertyGroup>
  
  <Deterministic>true</Deterministic>

  
  <ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
</PropertyGroup>
确定性构建的影响
  • 从 PE 标头中移除时间戳
  • 在 PDB 中使用一致的文件路径
  • 相同的输入会生成完全相同的输出
对性能的意义
  • 构建缓存:如果输出具有确定性,就可以跨构建和计算机缓存并重用这些输出
  • CI 优化:通过比较输入,跳过未更改项目的重新构建
  • 分布式构建:可安全地在共享存储中缓存编译结果

步骤 4:精简依赖关系图

减少不必要的项目引用可以缩短关键路径,并减少需要构建的内容。

审查依赖关系图
# Visualize the dependency graph
dotnet build /bl:graph.binlog

# In the binlog, check project references and build times
# Look for projects that are referenced but could be trimmed
技巧
移除冗余的传递引用

<ItemGroup>
  <ProjectReference Include="..\Core\Core.csproj" />
  <ProjectReference Include="..\Utils\Utils.csproj" />
</ItemGroup>


<ItemGroup>
  <ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
仅用于构建顺序的引用

如果需要先构建某个项目,但不需要它的程序集输出:


<ProjectReference Include="..\CodeGen\CodeGen.csproj"
                  ReferenceOutputAssembly="false" />
阻止传递流动

如果某个依赖项是内部实现细节,不应传递给使用方:


<ProjectReference Include="..\InternalHelpers\InternalHelpers.csproj"
                  PrivateAssets="all" />
禁用传递项目引用

用于显式管理依赖项(适用于超大型代码仓库的极端措施):

<PropertyGroup>
  <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
</PropertyGroup>

注意:这要求显式列出所有依赖项。仅在大型代码仓库中,且传递闭包导致过度重新构建时使用。


步骤 5:静态图构建(/graph)

静态图模式会在构建前评估整个项目图,从而实现更好的调度和隔离。

启用图构建
# Single invocation
dotnet build /graph

# With binary log for analysis
dotnet build /graph /bl:graph-build.binlog
优势
  • 更好的并行性:MSBuild 会预先了解完整的依赖关系图,并能进行最佳调度
  • 构建隔离:每个项目都会独立构建(不会泄漏跨项目状态)
  • 缓存潜力:隔离后,可以缓存单个项目的结果
适用场景
场景建议
大型多项目解决方案(20 个以上项目)✅ 尝试 /graph——并行性可能会显著提升
小型解决方案(少于 5 个项目)❌ 图评估的开销超过其收益
CI 构建✅ 图构建更可预测,也更易并行化
本地开发⚠️ 两种方式都测试一下——是否有帮助取决于项目结构
图构建故障排除

图构建要求所有 ProjectReference 项都能静态确定(不能在目标中动态计算引用)。如果图构建失败:

error MSB4260: Project reference "..." could not be resolved with static graph.

修复方法:确保所有 ProjectReference 项都在目标之外的 <ItemGroup> 中声明(而不是在 <Target> 块中动态计算)。


步骤 6:并行构建调优

MaxCpuCount
# Use all available cores (default in dotnet build)
dotnet build -m

# Specify explicit core count (useful for CI with shared agents)
dotnet build -m:4

# MSBuild.exe syntax
msbuild /m:8 MySolution.sln
识别并行瓶颈

在 binlog 中,查找:

  • 较长的顺序链:由于依赖关系,必须依次构建的项目
  • 负载不均:某些构建节点处于空闲状态,而其他节点负载过重
  • 单项目瓶颈:关键路径上的一个大型项目阻塞了所有任务

在 binlog 分析中使用 grep 'Target Performance Summary' -A 30 full.log 查看构建节点的利用率。

缩短关键路径

关键路径是依赖项目构成的最长链。要缩短它:

  1. 将大型项目拆分为较小的项目,以便并行构建
  2. 移除不必要的 ProjectReferences(见第 5 步)
  3. 对仅用于确定构建顺序的依赖项,使用 ReferenceOutputAssembly="false"
  4. 将共享代码移至基础库,先构建该库,再并行构建使用它的项目

第 7 步:其他快速优化

将还原与构建分开
# In CI, restore once then build without restore
dotnet restore
dotnet build --no-restore -m
dotnet test --no-build
跳过不必要的目标
# Skip building documentation
dotnet build /p:GenerateDocumentationFile=false

# Attribution experiment only: compare against the same build with analyzers
dotnet build /p:RunAnalyzers=false

使用这些开关,在更改配置前测量其影响。 不要根据此基线步骤建议永久禁用分析器; 应将测得的分析器瓶颈交由 build-perf-diagnostics 处理,并保留 CI 强制检查。

使用项目级筛选
# Build only the project you're working on (and its dependencies)
dotnet build src/MyApp/MyApp.csproj

# Don't build the entire solution if you only need one project
所有调查均使用二进制日志

始终从 binlog 开始:

dotnet build /bl:perf.binlog -m

然后使用 build-perf-diagnostics 技能和 binlog 工具,系统地识别瓶颈。


优化决策树

Is your repeated no-op build disproportionately close to warm/cold samples,
or are compile/custom targets rerunning?
├── YES → See `incremental-build` skill (inspect Inputs/Outputs and skip reasons)
└── NO
    Is your cold build slow?
    ├── YES
    │   Is restore slow?
    │   ├── YES → Optimize NuGet restore (use lock files, configure local cache)
    │   └── NO
    │       Is compilation slow?
    │       ├── YES
    │       │   Are analyzers/generators slow?
    │       │   ├── YES → See `build-perf-diagnostics` skill
    │       │   └── NO → Check parallelism, graph build, critical path (this skill + `build-parallelism`)
    │       └── NO → Check custom targets (binlog analysis via `build-perf-diagnostics`)
    └── NO
        Is your warm build slow?
        ├── YES → Projects rebuilding unnecessarily → check `incremental-build` skill
        └── NO → Baseline is healthy; adopt graph build or UseArtifactsOutput only for a measured need

评论

支持 Markdown