Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
4f5d351
Add find-repeated-build-work skill to dotnet-msbuild
YuliiaKovalova Oct 13, 2026
39e935e
Address repeated-build-work review feedback
YuliiaKovalova Oct 13, 2026
edceb93
Verify repeated-work fixes remain connected to builds
YuliiaKovalova Oct 13, 2026
1ba9b01
Clarify binlog capture and preferred MCP analysis
YuliiaKovalova Oct 13, 2026
c7f5d27
Address repeated-work review summary findings
YuliiaKovalova Oct 13, 2026
5c47b1b
Close repeated-build-work grader gaps
YuliiaKovalova Oct 13, 2026
1b62da9
Preserve repeated-work outputs and parallelism routing
YuliiaKovalova Oct 14, 2026
b7d1ec0
Exercise the submitted shared-project build end to end
YuliiaKovalova Oct 14, 2026
666524f
Clarify cross-command and second-build routing boundaries
YuliiaKovalova Oct 14, 2026
28339f6
Preserve unchanged shared generation timestamps
YuliiaKovalova Oct 14, 2026
bad8932
Check generated timestamps at the build boundary
YuliiaKovalova Oct 14, 2026
5449a22
Cover generic routing and schema output preservation
YuliiaKovalova Oct 14, 2026
bac5703
Accept CLI properties and partition instance diagnostics
YuliiaKovalova Oct 14, 2026
94e7dab
Exercise per-project generation hooks in the race grader
YuliiaKovalova Oct 14, 2026
fe492e1
Preserve build invocation and exercise submitted generators
YuliiaKovalova Oct 14, 2026
3243b0d
Check effective Compile items and accept offline summaries
YuliiaKovalova Oct 14, 2026
76d4ca8
Prioritize isolated evaluation and restore routing exclusions
YuliiaKovalova Oct 14, 2026
6d1d8d8
Merge branch 'main' into yuliiakovalova/find-repeated-build-work
YuliiaKovalova Oct 16, 2026
ed8ff5a
Merge main and reconcile build-performance routing
YuliiaKovalova Oct 16, 2026
7e39802
Fix conditional graders and broaden repeated-work eval coverage
YuliiaKovalova Oct 16, 2026
bc10c27
Enforce schema incrementality and correct batching savings
YuliiaKovalova Oct 16, 2026
8a152de
Merge branch 'main' into yuliiakovalova/find-repeated-build-work
YuliiaKovalova Oct 19, 2026
952d24c
Fix orchestrated and conditional MSBuild graders
YuliiaKovalova Oct 19, 2026
c0f6200
Accept implicit schema output directory creation
YuliiaKovalova Oct 19, 2026
d8f24a5
Merge remote-tracking branch 'origin/main' into HEAD
YuliiaKovalova Oct 21, 2026
087dbd0
Migrate repeated-build-work eval to Vally 0.18
YuliiaKovalova Oct 22, 2026
2bf9eca
Fix dormancy metadata lookup in grader contract test
YuliiaKovalova Oct 22, 2026
fd81af9
Evaluate conditional Git stamp assignments through MSBuild
YuliiaKovalova Oct 22, 2026
823b6b8
Replay CI build arguments in Git stamp probe
YuliiaKovalova Oct 22, 2026
fcb10c5
Keep MSBuild routing menu within runtime budget
YuliiaKovalova Oct 29, 2026
9a2004b
Merge branch 'main' into yuliiakovalova/find-repeated-build-work
YuliiaKovalova Oct 29, 2026
c0b8b62
Judge fallback guards and schema hooks by MSBuild behavior
YuliiaKovalova Oct 29, 2026
0ea5362
Trim repeated-build-work PR to skill-specific scope
YuliiaKovalova Oct 29, 2026
feaf8ae
Validate schema import reachability through MSBuild
YuliiaKovalova Oct 29, 2026
52bea49
Exercise the SDK build graph in Git-stamp grader
YuliiaKovalova Oct 29, 2026
92104cf
Verify compiled attributes and unchanged-build compilation
YuliiaKovalova Oct 30, 2026
157821c
Accept native revision orchestrators and renamed schema items
YuliiaKovalova Oct 30, 2026
8304af9
Validate schema rewrites through native MSBuild execution
YuliiaKovalova Oct 30, 2026
777c892
Reject added schemas without restricting helper imports
YuliiaKovalova Oct 30, 2026
1ace78e
Accept cached shared generation and reproduce the writer race
YuliiaKovalova Oct 30, 2026
da63404
Evaluate equivalent revision lookups using native Git
YuliiaKovalova Oct 30, 2026
f97403d
Strengthen the unchanged-build dormancy boundary
YuliiaKovalova Oct 30, 2026
931e9c6
Preserve grader script entry points and compilation history
YuliiaKovalova Oct 30, 2026
185e834
Validate native generation without a legacy helper requirement
YuliiaKovalova Oct 30, 2026
4fe6b22
Accept native clocks and SDK revision metadata
YuliiaKovalova Oct 30, 2026
9a4e320
Restart shared build probes across UTC rollover
YuliiaKovalova Oct 30, 2026
c60fd4d
Verify stamp changes against the complete warm cache
YuliiaKovalova Oct 30, 2026
414667e
Verify submitted build-info UTC defaults
YuliiaKovalova Oct 30, 2026
b996f90
Clarify shared-value fallback and property inheritance
YuliiaKovalova Oct 30, 2026
e436649
Clarify repeated-work routing and no-op decisions
YuliiaKovalova Nov 2, 2026
9b4f4b9
Preserve the complete generated schema source set
YuliiaKovalova Nov 2, 2026
7d7957d
Allow SDK sources beside generated schema outputs
YuliiaKovalova Nov 2, 2026
b208b7e
Keep submitted generated sources visible to native grading
YuliiaKovalova Nov 2, 2026
5e9eaeb
Clarify the within-build repeated-work skill scope
YuliiaKovalova Nov 2, 2026
c875608
Observe schema compiler inputs after SDK imports
YuliiaKovalova Nov 3, 2026
e46cdd4
Clarify once-only execution routing boundaries
YuliiaKovalova Nov 4, 2026
9ef40c0
Harden native repeated-work evaluation probes
YuliiaKovalova Nov 4, 2026
3f2d9f4
Scope negligible-repeat Copy expectations to available evidence
YuliiaKovalova Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/dotnet-msbuild/agents/build-perf.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ If asked, apply fixes and re-run the build to measure improvement.
## Specialized Skills Reference
Load these skills for detailed guidance on specific optimization areas:
- `build-perf-diagnostics` — Performance metrics and common bottlenecks
- `find-repeated-build-work` — Work repeated in every project or built several times in one build
- `incremental-build` — Incremental build optimization
- `build-parallelism` — Parallelism and graph build
- `eval-performance` — Evaluation performance
Expand Down
1 change: 1 addition & 0 deletions plugins/dotnet-msbuild/agents/msbuild.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ This agent has access to a comprehensive set of troubleshooting and optimization
### Performance Skills
- `build-perf-baseline` — Performance baseline methodology and systematic optimization
- `build-perf-diagnostics` — Performance bottleneck identification
- `find-repeated-build-work` — Work repeated in every project or built several times in one build
- `incremental-build` — Incremental build optimization
- `build-parallelism` — Parallelism and graph build
- `eval-performance` — Evaluation performance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: build-perf-diagnostics
description: "Diagnose MSBuild build performance bottlenecks using binary log analysis. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target/Task Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems."
description: "Diagnose MSBuild build performance bottlenecks. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline and Target/Task Performance Summary interpretation. Use after build-perf-baseline has established measurements. DO NOT USE FOR: repeated per-project or per-item work (use find-repeated-build-work), establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems."
license: MIT
---

Expand Down Expand Up @@ -109,7 +109,7 @@ Step-by-step workflow using text log replay:
```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.
This shows all targets and tasks sorted by cumulative time — equivalent to finding expensive targets/tasks. Compare `N calls` with the projects and TFMs that genuinely need each operation. An invariant command repeated once in every project can be waste even when its count equals the project count; legitimate per-TFM work can exceed that count (see `find-repeated-build-work`).
3. **Find per-project build times**:
```bash
grep "done building project\|Project Performance Summary" full.log
Expand Down
107 changes: 107 additions & 0 deletions plugins/dotnet-msbuild/skills/find-repeated-build-work/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: find-repeated-build-work
description: "Remove work MSBuild repeats inside one build. USE FOR: same Exec or task in every project (git stamp), performance summary calls above project count, shared project rebuilt via global properties, per-item Exec, racy run-once guards. DO NOT USE FOR: no-op rebuild reruns, restores, slow evaluation."
license: MIT
---

# Find Repeated Build Work

MSBuild runs targets once per project instance. Work that does not depend on the project, but sits in `Directory.Build.targets` or a shared target, therefore runs once per project: a `git rev-parse` in 40 projects is 40 process launches. A project built with different global properties is a different instance, so a shared library can compile several times in one build. The binlog's `N calls` column shows both. This skill finds the repeats, separates them from work that is legitimately per project, and removes only what is safe to remove.

## When to Use

- The same `Exec` or task (version stamping, tool probes, file generation) runs in every project
- Target or Task Performance Summary shows a call count far above the number of projects that need the work
- One project appears many times in the Project Performance Summary with different global properties
- An `Exec` spawns one process per item in a list
- A "run once per build" guard (`Exists` check, flag property) still runs repeatedly or fails with file-in-use errors under `-m`

## When Not to Use

- Targets that run again on a second, unchanged build: use `incremental-build`
- Slow evaluation before any target runs: use `eval-performance`
- Idle nodes or a serial dependency chain: use `build-parallelism`
- Repeated restores across build steps: use `build-perf-diagnostics`
- Unranked "why is my build slow": start with `build-perf-baseline`, then `build-perf-diagnostics`

## Workflow

### Step 1: Count executions

If no binlog is supplied, use the `binlog-generation` skill to capture one: add `/bl:{}` to the original MSBuild invocation, preserving its configuration, global properties, parallelism and restore behavior. In PowerShell, quote the complete switch as `'-bl:{}'`. If a script drives the build, instrument its underlying MSBuild invocation rather than replace it with a different build command. Confirm the binlog exists and record its path.

For analysis, prefer the bundled **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace). Discover its available tools and query project, target and task execution counts and timings from the captured binlog. Do not read the binary file as text.

If MCP is unavailable or its tools do not expose execution counts, replay the captured binlog to a text performance summary and read the `N calls` column of the Target and Task Performance Summaries and the Project Performance Summary:

```bash
dotnet msbuild build.binlog -noconlog "-flp:v=diag;logfile=full.log;performancesummary"
```

Compare each count with the project instances that genuinely need the operation. TFM-dependent work such as `Csc` normally runs once per project and target framework; copy-local work may also run in several targets. A multi-targeted project can legitimately show one outer build plus one inner build per TFM, so four project calls for three TFMs are not automatically duplicates. Build-invariant work is different: an identical git lookup needs one computation for the build, so 38 calls in 38 projects can still be waste. Confirm that inputs, working directories and relevant properties do not change the result; identical command text alone does not prove invariance. Investigate extra instances only after accounting for required TFMs and global properties.

### Step 2: Classify each candidate

| Pattern | Signal | Fix | Never |
|---|---|---|---|
| Invariant per-project `Exec` or task (commit hash, tool version, generated file) | `Exec` calls equal the project count and the command line is identical | Compute it once outside the per-project targets: the build script passes `-p:Name=value`, or a pre-build step or orchestrator target runs first | A flag property or `Exists(stamp)` guard: properties are per project instance and `-m` worker nodes are separate processes, so the guard does not dedupe and races |
| Same project built several times | One project path with N calls and different global properties (for example `Properties="ComponentName=%(Component.Filename)"` on an `MSBuild` task). Any extra global property, even an unused one, creates a new instance | Stop passing the property, or add `GlobalPropertiesToRemove` metadata on the shared `ProjectReference` (`RemoveProperties` on the `MSBuild` task) | Removing a property the project actually reads |
| One process per item (`Exec` batched on `%(Item.Identity)`) | `Exec` calls equal the item count | One invocation that takes a list or response file, after checking the tool supports it; keep `Inputs`/`Outputs` (with a 1:1 transform MSBuild still passes only the stale items into the target) | Dropping `Inputs`/`Outputs` while merging |
| Legitimate per-project or per-TFM work | Count equals projects times target frameworks | Leave it alone | Changing it to look tidy |

### Step 3: Size the win

Avoidable aggregate task time is `(calls - expected) x average ms`. Wall-clock saving depends on actual overlap and the critical path. Batched per-item `Exec` calls in one target run sequentially on one node, so removing their startup costs can save nearly the full aggregate time. Only divide by effective parallelism when the duplicate calls actually overlap across nodes; unrelated busy nodes do not reduce a sequential saving. Use the binlog timeline to establish overlap, and mark wall-clock savings unknown when that evidence is missing. Work off the critical path can yield no wall-clock gain. For performance-only changes, if even the aggregate upper bound is below about 1% of build wall-clock, report it as not worth changing. Correctness races still need a fix regardless of the timing gain.
Comment thread
YuliiaKovalova marked this conversation as resolved.
Resolved

### Step 4: Fix without changing behavior

1. Keep the value available before its first consumer.
2. A fallback `Condition="'$(X)' == ''"` is valid only when the value arrives as a global property (command line or an orchestrator), because those are inherited by every project. It keeps single-project and IDE builds working.
3. Define the computation in one place.

```xml
<!-- Before: every project launches git -->
<Target Name="StampCommit" BeforeTargets="CoreCompile">
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommit" />
</Exec>
</Target>

<!-- After: the build script computes the hash once and passes -p:GitCommit=<hash> -->
<Target Name="StampCommit" BeforeTargets="CoreCompile">
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true" Condition="'$(GitCommit)' == ''">
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommit" />
</Exec>
</Target>
```

If the value is only a commit id for the informational version, source link already sets `SourceRevisionId` without an `Exec`.

### Step 5: Validate and report truthfully

1. Capture a new binlog of the same invocation. Calls must have dropped to the expected count.
2. Confirm outputs are unchanged (the stamped value, generated file contents).
3. Report timing as the median of at least three runs; if you could not run the build, say "not validated".

## Output

For on-target requests, emit a findings table with at least one row and a `Verdict:` line. Use `unknown` for unavailable timing or critical-path evidence; distinguish estimated aggregate task time from measured wall-clock savings.

| Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
|---|---|---|---|---|---|---|

After verification: `Verdict: removed N repeated executions, measured wall-clock saving X s`.

Without verification: `Verdict: proposed removal of N repeated executions; estimated task-time saving X s; wall-clock not validated`.

When no change is worthwhile: `Verdict: no repeated work worth changing`.

## Common Pitfalls

| Pitfall | Solution |
|---|---|
| Treating `Csc` or copy-local counts as waste | Compare with projects times target frameworks first |
| Guarding with `Exists(file)` or a flag property | Move the work out of the per-project path |
| Fallback condition on a property that is not a global property | Pass it as a global property, or the fallback never applies |
| Claiming a saving from total task time | Check actual overlap and the critical path; do not divide sequential work by unrelated busy nodes |
| Merging per-item `Exec` and losing incrementality | Keep `Inputs` and `Outputs` on the target |
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project DefaultTargets="BuildAll">
<PropertyGroup>
<BuildInfoPath>$(MSBuildThisFileDirectory)obj/buildinfo.g.cs</BuildInfoPath>
</PropertyGroup>
<ItemGroup>
<Component Include="Api/Api.csproj;Worker/Worker.csproj" />
</ItemGroup>

<Target Name="Restore">
<MSBuild Projects="@(Component)" Targets="Restore"
Properties="BuildInfoPath=$(BuildInfoPath)" BuildInParallel="true" />
</Target>

<Target Name="GenerateSharedBuildInfo"
Inputs="$(MSBuildThisFileDirectory)buildinfo.cs.in"
Outputs="$(BuildInfoPath)">
<MakeDir Directories="$(MSBuildThisFileDirectory)obj" />
<Copy SourceFiles="$(MSBuildThisFileDirectory)buildinfo.cs.in" DestinationFiles="$(BuildInfoPath)" />
</Target>

<Target Name="BuildAll" DependsOnTargets="GenerateSharedBuildInfo">
<MSBuild Projects="@(Component)" Targets="Build"
Properties="BuildInfoPath=$(BuildInfoPath)" BuildInParallel="true" />
</Target>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>
<ItemGroup>
<Compile Include="$(BuildInfoPath)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
internal static class BuildInfo
{
public const string Version = "3.2.0";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Performance summary from dotnet msbuild Build.proj -restore -m
(Api and Worker each compile the same generated buildinfo.g.cs; Time Elapsed 00:00:05.20)

Target Performance Summary:
12 ms GenerateSharedBuildInfo 1 calls
4310 ms CoreCompile 2 calls

Task Performance Summary:
8 ms Copy 1 calls
4290 ms Csc 2 calls

Project Performance Summary:
5200 ms Build.proj 1 calls
2140 ms Api/Api.csproj 1 calls
2210 ms Worker/Worker.csproj 1 calls
Loading
Loading