关于 find-repeated-work-within-one-build
此技能指导开发者查找单次 MSBuild 构建中的冗余工作。它依据执行次数和 binlog 证据,区分可消除的重复任务与必要的项目、框架或条目级工作,并提出保持行为不变的修复方案。
主要功能
- 01分析 binlog 执行次数
- 02区分重复与必要工作
- 03识别重复项目实例
- 04估算可避免的任务耗时
- 05保持构建行为与增量性
- 06用新 binlog 验证修复
适用场景
- 01调查跨项目重复版本探测
- 02诊断由全局属性导致的重复构建
- 03减少逐条目启动进程
- 04评估优化是否值得实施
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill find-repeated-work-within-one-build安装后在 Claude Code 中输入/find-repeated-work-within-one-build确认已加载。
详细介绍
查找单次生成中的重复工作
MSBuild 会为每个项目实例运行一次目标。因此,不依赖项目、但位于 Directory.Build.targets 或共享目标中的工作,会针对每个项目运行一次:在 40 个项目中执行 git rev-parse,就会启动 40 个进程。使用不同全局属性生成的项目属于不同实例,因此一个共享库可能在一次生成中编译多次。二进制日志中的 N calls 列会显示这两种情况。此技能用于查找重复工作,将其与确实需要按项目执行的工作区分开,并且只移除可以安全移除的部分。
适用场景
- 相同的
Exec或任务(版本标记、工具探测、文件生成)在每个项目中运行 - Target 或 Task Performance Summary 显示的调用次数远高于需要执行该工作的项目数
- 同一项目在一次生成中,使用不会改变其输出的全局属性集反复执行生成目标
Exec为列表中的每个项都启动一个进程- “每次生成只运行一次”的保护机制(
Exists检查、标志属性)仍会重复运行,或在使用-m时因文件占用而失败
不适用场景
- 目标在第二次、且未发生更改的生成中再次运行:使用
incremental-build - 任何目标运行之前的评估过程缓慢:使用
eval-performance - 必需的项目变体在共享的 bin/obj 目录中发生冲突:使用
check-bin-obj-clash - 节点空闲或依赖链是串行的:使用
build-parallelism - 在单独执行
dotnet restore、dotnet build和dotnet test命令时,NuGet 还原重复执行:使用eliminate-redundant-restores - 未明确指出原因的“为什么我的生成这么慢”:从
build-perf-baseline开始,然后使用build-perf-diagnostics
工作流
步骤 1:统计执行次数
如果用户提供了包含执行次数的性能摘要,可将其作为有效输入;不需要原始 binlog。如果用户无法运行构建,或禁止执行构建或生成新日志,请依据该摘要和仓库文件进行分析。将无法获取的耗时或重叠证据标记为 unknown,将拟议的修复标记为 not validated;不要执行构建或收集另一份日志。
如果既没有可用的执行次数,也没有提供 binlog,则仅在可以且获准执行构建时捕获一份。使用 binlog-generation 技能:在原始 MSBuild 调用中添加 /bl:{},保留其配置、全局属性、并行度和还原行为。在 PowerShell 中,将整个开关引用为 '-bl:{}'。如果由脚本驱动构建,应为脚本底层的 MSBuild 调用添加监测,而不是将其替换为不同的构建命令。确认 binlog 已生成并记录其路径。如果不允许捕获,则报告缺少的证据,而不要运行构建。
对于可用的 binlog,优先使用随附的 binlog MCP 服务(在 binlog MCP 命名空间下提供,名称为 Microsoft.AITools.BinlogMcp)。发现其可用工具,并查询该 binlog 中项目、目标和任务的执行次数与耗时。不要将二进制文件当作文本读取。
如果 MCP 不可用,或其工具无法提供执行次数,请将捕获的 binlog 重放为文本性能摘要,并读取 Target and Task Performance Summaries 和 Project Performance Summary 中的 N calls 列:
dotnet msbuild build.binlog -noconlog "-flp:v=diag;logfile=full.log;performancesummary"
统计所需的不同计算,而不只是使用者的数量。分别推导每种工作的预期次数:Csc 通常每个项目、每个目标框架运行一次,而本地复制工作会随文件数量增加,并且可能在多个目标中运行。项目数乘以 TFM 数并不是所有任务通用的上限;如果没有依据,就将无法确定的预期次数标记为 unknown,不要声称更多执行次数属于冗余。多目标项目可能会显示一次外部构建,加上每个 TFM 一次内部构建,因此,三个 TFM 对应四次项目调用,并不一定是重复执行。调查额外实例前,应先考虑所需的 TFM 和全局属性。
不变的工具版本或修订版本探测,即使多个项目使用其结果,也只需对等效输入范围计算一次。将目标条件限制为项目的子集,并不会使结果变成按项目计算。请确认输入、工作目录、相关属性和输出语义都等效;仅凭命令文本相同并不能证明其具有不变性。尤其是,每个项目独立生成的签名身份具有刻意的不确定性:不要缓存、复用或跨项目或构建共享同一身份。除非有文档说明的工具模式能够在不改变约定的情况下生成所有不同结果,否则应保留每次所需的执行。
第 2 步:对每个候选项进行分类
| 模式 | 信号 | 修复方法 | 切勿 |
|---|---|---|---|
每个项目的不变量 Exec 或任务(提交哈希、工具版本、生成的文件) | 多个使用方需要在等效输入下获得相同的确定性结果 | 在每个项目的目标之外计算一次:由构建脚本传入 -p:Name=value,或先运行预构建步骤或编排器目标 | 使用标志属性或 Exists(stamp) 保护:属性按项目实例区分,而 -m 工作节点是独立进程,因此保护条件无法去重,且会引发竞态 |
| 同一项目被构建多次 | 一个项目路径对应 N 次调用,且全局属性各不相同(例如 MSBuild 任务上的 Properties="ComponentName=%(Component.Filename)")。任何额外的全局属性,即使未使用,也会创建一个新实例 | 停止传递该属性,或在共享的 ProjectReference 上添加 GlobalPropertiesToRemove 元数据(在 MSBuild 任务上使用 RemoveProperties) | 删除项目实际读取的属性 |
每项启动一个进程(对 %(Item.Identity) 进行批处理的 Exec) | Exec 调用次数等于项数 | 确认工具支持后,改为调用一次并传入列表或响应文件;保留 Inputs/Outputs(使用 1:1 转换时,MSBuild 仍只会将过期项传入目标) | 合并时删除 Inputs/Outputs |
| 合理的每项目、每 TFM 或每项工作 | 数量与所需的项目、框架或输出项范围一致 | 保持不变 | 对不相关的任务类型套用统一的数量上限 |
第 3 步:估算收益
对于消除的不变量计算,按 (调用次数 - 预期次数) x 平均毫秒数 估算可避免的任务总时间。按项批处理不会消除必要的输入处理:按 (旧进程启动次数 - 新进程启动次数) x 启动毫秒数 估算,同时保留每个输入的处理成本和所有必需的元数据分组。如果已知额外的列表或批处理开销,也应计入。如果无法获得启动成本,应将该估算标为未知,而不是把整个平均任务时长都视为可消除的部分。
实际耗时的节省取决于实际并行情况和关键路径。同一目标中按项批处理的 Exec 调用会在同一个节点上顺序运行,因此节省的启动时间不会因其他繁忙节点而除以节点数。只有当移除的工作确实在多个节点上并行时,才按有效并行度折算。使用 binlog 时间线确认并行情况;缺少此类证据时,应将实际耗时节省标为未知。不在关键路径上的工作可能不会带来任何实际耗时收益。对于仅为提升性能而进行的改动,如果可避免时间总量的上限仍不到构建实际耗时的约 1%,应报告该改动不值得进行。无论计时收益如何,正确性竞态仍需修复。
对于基于成本而建议不作改动的情况,应给出可避免时间总量的上限;如果提供了构建经过时间,还应给出 100 x 上限 / 经过时间,并明确这是上限百分比,而非实测的实际耗时收益。应区分技术上冗余但不值得更改的工作与必须保持区分的工作。
步骤 4:修复问题,但不改变行为
- 确保在值的第一个使用者运行之前,该值已可用。
- 对于没有共享值的构建,保留回退条件
Condition="'$(X)' == ''"。导入的属性或环境属性也可以满足该条件;该值不必是全局属性。子构建会继承现有的全局属性(除非这些属性被移除),但不会继承在父构建中计算得出的普通属性。通过MSBuild任务的Properties转发父级计算出的结果,或者在每个子构建中先加载共享值,再执行回退;否则每个子构建都可能执行自己的回退。 - 将计算逻辑定义在一处。
- 内容未变时,保留生成文件的时间戳。使用仅在内容不同时写入的生成器,或者先生成到临时文件,仅在内容发生变化时替换;去重不得导致依赖项在内容未变的重新运行中重新编译。
<Target Name="StampCommit" BeforeTargets="CoreCompile">
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommit" />
</Exec>
</Target>
<Target Name="StampCommit" BeforeTargets="CoreCompile">
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true" Condition="'$(GitCommit)' == ''">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommit" />
</Exec>
</Target>
如果 Source Link 已提供 SourceRevisionId,则仅当它在使用者运行之前可用,并且能保留现有的戳记表示形式时,才可用它替代 Exec。SourceRevisionId 通常包含完整的提交 ID;除非保留原有的缩写约定,否则不要用它替代 git rev-parse --short HEAD。
步骤 5:验证并如实报告
- 如果可以且获准执行,则对同一调用捕获新的 binlog。调用次数必须降至预期次数。否则,不要运行构建或收集另一份日志;应报告该修复仅为建议,尚未验证。
- 确认输出未发生变化(戳记值、生成文件内容)。对于生成的源文件,还要检查第二次内容未变的构建是否仍保持增量构建。
- 将运行时间报告为至少三次运行的中位数;如果无法运行构建,请写明“未验证”。
输出
对于 on-target 请求,回答应与证据相称:提供至少含一行的简洁发现表、简短说明决定性的计算或保留约束,并单独给出一行不带格式的 Verdict:。不要加粗该行,也不要将它附在段落后面。对于无法获得的运行时间或关键路径证据,使用 unknown;区分估算的任务总耗时与实测的墙钟时间节省。如果按约定必须执行每一次操作,即使任务耗时未知,也应报告可避免时间为零。
| 工作 | 调用次数 | 预期次数 | 可避免毫秒数 | 原因 | 修复方法 | 是否在关键路径上? |
|---|
验证后:Verdict: removed N repeated executions, measured wall-clock saving X s。
未经验证:Verdict: proposed removal of N repeated executions; estimated task-time saving X s; wall-clock not validated。
如果没有值得更改的内容:Verdict: no repeated work worth changing。
常见误区
| 误区 | 解决方案 |
|---|---|
将 Csc 或 copy-local 次数视为浪费 | 根据项目数乘以 TFM 数比较 Csc,并根据所需文件和目标比较复制次数;不要假设两者的预期次数相同 |
使用 Exists(file) 或标志属性进行保护 | 将相关操作移出每个项目都会执行的路径 |
| 假设父级局部属性会被子构建继承 | 显式转发共享值,或在每个子构建中先加载共享值,再执行回退;否则回退会在这些子构建中重复执行。现有的全局属性已会被继承,除非被移除。 |
| 根据任务总耗时声称节省了时间 | 检查实际重叠情况和关键路径;不要用无关忙碌节点的时间去除顺序执行的任务耗时 |
合并逐项执行的 Exec,导致增量性丢失 | 在目标上保留 Inputs 和 Outputs |