build-perf-diagnostics

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Performance Analysis Methodology

性能分析方法论

  1. Generate a binlog:
    dotnet build /bl:{} -m
  2. Replay to diagnostic log with performance summary:
    bash
    dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary
  3. Read the performance summary (at the end of
    full.log
    ):
    bash
    grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log
  4. Find expensive targets and tasks: The PerformanceSummary section lists all targets/tasks sorted by cumulative time
  5. Check for node utilization: grep for scheduling and node messages
    bash
    grep -i "node.*assigned\|building with\|scheduler" full.log | head -30
  6. Check analyzers: grep for analyzer timing
    bash
    grep -i "analyzer.*elapsed\|Total analyzer execution time\|CompilerAnalyzerDriver" full.log
  1. 生成binlog日志
    dotnet build /bl:{} -m
  2. 重放日志并生成带性能摘要的诊断日志
    bash
    dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary
  3. 查看性能摘要(位于
    full.log
    末尾):
    bash
    grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log
  4. 定位耗时久的目标和任务:PerformanceSummary部分会按累计时间排序列出所有目标/任务
  5. 检查节点利用率:搜索调度和节点相关消息
    bash
    grep -i "node.*assigned\|building with\|scheduler" full.log | head -30
  6. 检查分析器耗时:搜索分析器计时信息
    bash
    grep -i "analyzer.*elapsed\|Total analyzer execution time\|CompilerAnalyzerDriver" full.log

Key Metrics and Thresholds

关键指标与阈值

  • Build duration: what's "normal" — small project <10s, medium <60s, large <5min
  • Node utilization: ideal is >80% active time across nodes. Low utilization = serialization bottleneck
  • Single target domination: if one target is >50% of build time, investigate
  • Analyzer time vs compile time: analyzers should be <30% of Csc task time. If higher, consider removing expensive analyzers
  • RAR time: ResolveAssemblyReference >5s is concerning. >15s is pathological
  • 构建时长:“正常”标准——小型项目<10秒,中型<60秒,大型<5分钟
  • 节点利用率:理想状态下所有节点的活跃时间占比>80%。利用率低意味着存在序列化瓶颈
  • 单目标占比过高:若某个目标占构建时间的50%以上,需深入调查
  • 分析器时间与编译时间占比:分析器耗时应低于Csc任务时间的30%。若占比过高,考虑移除耗时久的分析器
  • RAR耗时:ResolveAssemblyReference耗时超过5秒需引起注意,超过15秒则属于严重问题

Common Bottlenecks

常见瓶颈

1. ResolveAssemblyReference (RAR) Slowness

1. ResolveAssemblyReference(RAR)缓慢

  • Symptoms: RAR taking >5s per project
  • Root causes: too many assembly references, network-based reference paths, large assembly search paths
  • Fixes: reduce reference count, use
    <DesignTimeBuild>false</DesignTimeBuild>
    for RAR-heavy analysis, set
    <ResolveAssemblyReferencesSilent>true</ResolveAssemblyReferencesSilent>
    for diagnostic
  • Advanced:
    <DesignTimeBuild>
    and
    <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
  • Key insight: RAR runs unconditionally even on incremental builds because users may have installed targeting packs or GACed assemblies (see dotnet/msbuild#2015). With .NET Core micro-assemblies, the reference count is often very high.
  • Reduce transitive references: Set
    <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
    to avoid pulling in the full transitive closure (note: projects may need to add direct references for any types they consume). Use
    ReferenceOutputAssembly="false"
    on ProjectReferences that are only needed at build time (not API surface). Trim unused PackageReferences.
  • 症状:每个项目的RAR耗时超过5秒
  • 根本原因:程序集引用过多、基于网络的引用路径、过大的程序集搜索路径
  • 修复方案:减少引用数量,针对RAR密集型分析设置
    <DesignTimeBuild>false</DesignTimeBuild>
    ,开启诊断模式设置
    <ResolveAssemblyReferencesSilent>true</ResolveAssemblyReferencesSilent>
  • 进阶配置
    <DesignTimeBuild>
    <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
  • 关键见解:即使在增量构建中,RAR也会无条件运行,因为用户可能已安装目标包或全局程序集缓存(GAC)中的程序集(详见dotnet/msbuild#2015)。在.NET Core微程序集场景下,引用数量通常非常多。
  • 减少传递引用:设置
    <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>
    避免引入完整的传递依赖闭包(注意:项目可能需要为其使用的任何类型添加直接引用)。对仅在构建时需要(而非API层面)的ProjectReferences设置
    ReferenceOutputAssembly="false"
    。移除未使用的PackageReferences。

2. Roslyn Analyzers and Source Generators

2. Roslyn分析器与源代码生成器

  • Symptoms: Csc task takes much longer than expected for file count (>2× clean compile time)
  • Diagnosis: Check the Task Performance Summary in the replayed log for Csc task time; grep for analyzer timing messages; compare Csc duration with and without analyzers (
    /p:RunAnalyzers=false
    )
  • Fixes:
    • Conditionally disable in dev:
      <RunAnalyzers Condition="'$(ContinuousIntegrationBuild)' != 'true'">false</RunAnalyzers>
    • Per-configuration:
      <RunAnalyzers Condition="'$(Configuration)' == 'Debug'">false</RunAnalyzers>
    • Code-style only:
      <EnforceCodeStyleInBuild Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</EnforceCodeStyleInBuild>
    • Remove genuinely redundant analyzers from inner loop
    • Severity config in .editorconfig for less critical rules
  • Key principle: Preserve analyzer enforcement in CI. Never just "remove" analyzers — configure them conditionally.
  • GlobalPackageReference: Analyzers added via
    GlobalPackageReference
    in
    Directory.Packages.props
    apply to ALL projects. Consider if test projects need the same analyzer set as production code.
  • EnforceCodeStyleInBuild: When set to
    true
    in
    Directory.Build.props
    , forces code-style analysis on every build. Should be conditional on CI environment (
    ContinuousIntegrationBuild
    ) to avoid slowing dev inner loop.
  • 症状:Csc任务耗时远超文件数量对应的预期(是干净编译时间的2倍以上)
  • 诊断方式:在重放日志的Task Performance Summary中查看Csc任务耗时;搜索分析器计时消息;对比启用和禁用分析器时的Csc时长(
    /p:RunAnalyzers=false
  • 修复方案
    • 在开发环境中条件性禁用:
      <RunAnalyzers Condition="'$(ContinuousIntegrationBuild)' != 'true'">false</RunAnalyzers>
    • 按配置禁用:
      <RunAnalyzers Condition="'$(Configuration)' == 'Debug'">false</RunAnalyzers>
    • 仅保留代码风格检查:
      <EnforceCodeStyleInBuild Condition="'$(ContinuousIntegrationBuild)' == 'true'">true</EnforceCodeStyleInBuild>
    • 在开发循环中移除真正冗余的分析器
    • 在.editorconfig中配置次要规则的严重性
  • 核心原则:在CI环境中保留分析器强制检查。不要直接“移除”分析器,而是通过条件配置来管理。
  • GlobalPackageReference:通过
    Directory.Packages.props
    中的
    GlobalPackageReference
    添加的分析器会应用于所有项目。需考虑测试项目是否需要与生产代码相同的分析器集合。
  • EnforceCodeStyleInBuild:在
    Directory.Build.props
    中设置为
    true
    时,会在每次构建中强制执行代码风格分析。应基于CI环境(
    ContinuousIntegrationBuild
    )进行条件配置,避免拖慢开发循环。

3. Serialization Bottlenecks (Single-threaded targets)

3. 序列化瓶颈(单线程目标)

  • Symptoms: Performance summary shows most build time concentrated in a single project; diagnostic log shows idle nodes while one works
  • Common culprits: targets without proper dependency declaration, single project on critical path
  • Fixes: split large projects, optimize the critical path project, ensure proper
    BuildInParallel
  • 症状:性能摘要显示大部分构建时间集中在单个项目;诊断日志显示其他节点空闲时,某个节点在持续工作
  • 常见诱因:未正确声明依赖的目标、关键路径上的单个项目
  • 修复方案:拆分大型项目、优化关键路径上的项目、确保启用
    BuildInParallel

4. Excessive File I/O (Copy tasks)

4. 过多文件I/O(Copy任务)

  • Symptoms: Copy task shows high aggregate time
  • Root causes: copying thousands of files, copying across network drives, Copy task unintentionally running once per item (per-file) instead of as a single batch (see dotnet/msbuild#12884)
  • Fixes: use hardlinks (
    <CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
    ), reduce CopyToOutputDirectory items, use
    <UseCommonOutputDirectory>true</UseCommonOutputDirectory>
    when appropriate, set
    <SkipCopyUnchangedFiles>true</SkipCopyUnchangedFiles>
    , consider
    --artifacts-path
    (.NET 8+) for centralized output layout
  • Dev Drive: On Windows, switching to a Dev Drive (ReFS with copy-on-write and reduced Defender scans) can significantly reduce file I/O overhead for Copy-heavy builds. Recommend for both dev machines and self-hosted CI agents.
  • 症状:Copy任务总耗时较高
  • 根本原因:复制数千个文件、跨网络驱动器复制、Copy任务意外地按单个文件执行(而非批量执行,详见dotnet/msbuild#12884)
  • 修复方案:使用硬链接(
    <CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
    )、减少CopyToOutputDirectory项、在合适场景下使用
    <UseCommonOutputDirectory>true</UseCommonOutputDirectory>
    、设置
    <SkipCopyUnchangedFiles>true</SkipCopyUnchangedFiles>
    、考虑使用
    --artifacts-path
    (.NET 8+)实现集中式输出布局
  • Dev Drive:在Windows系统中,切换到Dev Drive(支持写时复制和减少Defender扫描的ReFS)可显著降低Copy密集型构建的文件I/O开销。推荐在开发机器和自托管CI代理上使用。

5. Evaluation Overhead

5. 评估开销

  • Symptoms: build starts slow before any compilation
  • Root causes: complex Directory.Build.props, wildcard globs scanning large directories, NuGetSdkResolver overhead (adds 180-400ms per project evaluation even when restored — see dotnet/msbuild#4025)
  • Fixes: reduce Directory.Build.props complexity, use
    <EnableDefaultItems>false</EnableDefaultItems>
    for legacy projects with explicit file lists, avoid NuGet-based SDK resolvers if possible
  • See:
    eval-performance
    skill for detailed guidance
  • 症状:构建启动缓慢,在编译开始前耗时久
  • 根本原因:复杂的Directory.Build.props、通配符扫描大型目录、NuGetSdkResolver开销(即使已还原,每个项目评估也会增加180-400毫秒,详见dotnet/msbuild#4025)
  • 修复方案:降低Directory.Build.props的复杂度,对使用显式文件列表的遗留项目设置
    <EnableDefaultItems>false</EnableDefaultItems>
    ,尽可能避免基于NuGet的SDK解析器
  • 参考:
    eval-performance
    技能获取详细指导

6. NuGet Restore in Build

6. 构建中执行NuGet还原

  • Symptoms: restore runs every build even when unnecessary
  • Fixes:
    • Separate restore from build:
      dotnet restore
      then
      dotnet build --no-restore
    • Enable static graph evaluation:
      <RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
      in Directory.Build.props — can save significant time in large builds (results are workload-dependent)
  • 症状:即使不需要,每次构建仍会运行还原
  • 修复方案
    • 将还原与构建分离:先执行
      dotnet restore
      ,再执行
      dotnet build --no-restore
    • 启用静态图评估:在Directory.Build.props中设置
      <RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
      ——可在大型构建中节省大量时间(效果取决于工作负载)

7. Large Project Count and Graph Shape

7. 项目数量过多与依赖图结构

  • Symptoms: many small projects, each takes minimal time but overhead adds up; deep dependency chains serialize the build
  • Consider: project consolidation, or use
    /graph
    mode for better scheduling
  • Graph shape matters: a wide dependency graph (few levels, many parallel branches) builds faster than a deep one (many levels, serialized). Refactoring from deep to wide can yield significant improvements in both clean and incremental build times.
  • Actions: look for unnecessary project dependencies, consider splitting a bottleneck project into two, or merging small leaf projects
  • 症状:大量小型项目,单个耗时极少但总开销累加;深层依赖链导致构建序列化
  • 建议:合并项目,或使用
    /graph
    模式优化调度
  • 依赖图结构很重要:宽依赖图(层级少、并行分支多)比深依赖图(层级多、序列化执行)构建速度更快。将深层结构重构为宽结构可显著提升干净构建和增量构建的速度。
  • 行动项:查找不必要的项目依赖,考虑将瓶颈项目拆分为两个,或合并小型叶子项目

Using Binlog Replay for Performance Analysis

使用Binlog重放进行性能分析

Step-by-step workflow using text log replay:
  1. Replay with performance summary:
    bash
    dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary
  2. Read target/task performance summaries (at the end of
    full.log
    ):
    bash
    grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log
    This shows all targets and tasks sorted by cumulative time — equivalent to finding expensive targets/tasks.
  3. Find per-project build times:
    bash
    grep "done building project\|Project Performance Summary" full.log
  4. Check parallelism (multi-node scheduling):
    bash
    grep -i "node.*assigned\|RequiresLeadingNewline\|Building with" full.log | head -30
  5. Check analyzer overhead:
    bash
    grep -i "Total analyzer execution time\|analyzer.*elapsed\|CompilerAnalyzerDriver" full.log
  6. Drill into a specific slow target:
    bash
    grep 'Target "CoreCompile"\|Target "ResolveAssemblyReferences"' full.log
使用文本日志重放的分步流程:
  1. 重放并生成性能摘要
    bash
    dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log;performancesummary
  2. 查看目标/任务性能摘要(位于
    full.log
    末尾):
    bash
    grep "Target Performance Summary\|Task Performance Summary" -A 50 full.log
    该命令会按累计时间排序列出所有目标和任务——等同于定位耗时久的目标/任务。
  3. 查找每个项目的构建时间
    bash
    grep "done building project\|Project Performance Summary" full.log
  4. 检查并行性(多节点调度):
    bash
    grep -i "node.*assigned\|RequiresLeadingNewline\|Building with" full.log | head -30
  5. 检查分析器开销
    bash
    grep -i "Total analyzer execution time\|analyzer.*elapsed\|CompilerAnalyzerDriver" full.log
  6. 深入分析特定缓慢目标
    bash
    grep 'Target "CoreCompile"\|Target "ResolveAssemblyReferences"' full.log

Quick Wins Checklist

快速优化检查清单

  • Use
    /maxcpucount
    (or
    -m
    ) for parallel builds
  • Separate restore from build (
    dotnet restore
    then
    dotnet build --no-restore
    )
  • Enable static graph restore (
    <RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
    )
  • Enable hardlinks for Copy (
    <CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
    )
  • Disable analyzers conditionally in dev inner loop:
    <RunAnalyzers Condition="'$(ContinuousIntegrationBuild)' != 'true'">false</RunAnalyzers>
  • Enable reference assemblies (
    <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
    )
  • Check for broken incremental builds (see
    incremental-build
    skill)
  • Check for bin/obj clashes (see
    check-bin-obj-clash
    skill)
  • Use graph build (
    /graph
    ) for multi-project solutions
  • Use
    --artifacts-path
    (.NET 8+) for centralized output layout
  • Enable Dev Drive (ReFS) on Windows dev machines and self-hosted CI
  • 使用
    /maxcpucount
    (或
    -m
    )进行并行构建
  • 将还原与构建分离(先
    dotnet restore
    dotnet build --no-restore
  • 启用静态图还原(在Directory.Build.props中设置
    <RestoreUseStaticGraphEvaluation>true</RestoreUseStaticGraphEvaluation>
  • 为Copy任务启用硬链接(
    <CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>true</CreateHardLinksForCopyFilesToOutputDirectoryIfPossible>
  • 在开发循环中条件性禁用分析器:
    <RunAnalyzers Condition="'$(ContinuousIntegrationBuild)' != 'true'">false</RunAnalyzers>
  • 启用引用程序集(
    <ProduceReferenceAssembly>true</ProduceReferenceAssembly>
  • 检查增量构建是否损坏(参考
    incremental-build
    技能)
  • 检查bin/obj目录冲突(参考
    check-bin-obj-clash
    技能)
  • 对多项目解决方案使用图构建(
    /graph
    )模式
  • 使用
    --artifacts-path
    (.NET 8+)实现集中式输出布局
  • 在Windows开发机器和自托管CI上启用Dev Drive(ReFS)

Impact Categorization

影响分类

When reporting findings, categorize by impact to help prioritize fixes:
  • 🔴 HIGH IMPACT (do first): Items consuming >10% of total build time, or a single target >50% of build time
  • 🟡 MEDIUM IMPACT: Items consuming 2-10% of build time
  • 🟢 QUICK WINS: Easy changes with modest impact (e.g., property flags in Directory.Build.props)
报告分析结果时,按影响程度分类以帮助确定修复优先级:
  • 🔴 高影响(优先处理):占总构建时间10%以上的项,或单个目标占构建时间50%以上
  • 🟡 中影响:占总构建时间2-10%的项
  • 🟢 快速优化:易于实施且有适度影响的变更(例如Directory.Build.props中的属性配置)