Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 31 additions & 0 deletions plugins/dotnet-msbuild/skills/binlog-generation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ dotnet build /bl
dotnet build /bl
```

## One build = one binlog

Add `/bl:{}` to **every** MSBuild invocation separately — never reuse a name and
never rely on bare `/bl`:

- Building several configurations, projects, or retrying a failed build? Each
command still gets its own `/bl:{}` so the logs never overwrite each other.

```bash
dotnet build -c Debug /bl:{} # unique file
dotnet build -c Release /bl:{} # another unique file
```

## Verify the binlog exists

After the build, confirm a `.binlog` was actually produced before moving on to
analysis — a build that fails *before* MSBuild starts (e.g. a bad argument)
writes no binlog:

```bash
ls -1 *.binlog # bash
dir /b *.binlog # Windows cmd
```

```powershell
Get-ChildItem *.binlog # PowerShell
```

Note the resulting path so `binlog-failure-analysis` or `build-perf-diagnostics`
can consume it.

## When a Specific Filename Is Required

If the binlog filename needs to be known upfront (e.g., for CI artifact upload), or if `{}` is not available in the installed MSBuild version, pick a name that won't collide with existing files:
Expand Down
22 changes: 21 additions & 1 deletion plugins/dotnet-msbuild/skills/build-parallelism/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,29 @@
---
name: build-parallelism
description: "Guide for optimizing MSBuild build parallelism and multi-project scheduling. USE FOR: builds not utilizing all CPU cores, speeding up multi-project solutions, evaluating graph build mode (/graph), build time not improving with -m flag, understanding project dependency topology. Note: /maxcpucount default is 1 (sequential) — always use -m for parallel builds. Covers /maxcpucount, graph build for better scheduling and isolation, BuildInParallel on MSBuild task, reducing unnecessary ProjectReferences, solution filters (.slnf) for building subsets. DO NOT USE FOR: single-project builds, incremental build issues (use incremental-build), compilation slowness within a project (use build-perf-diagnostics), non-MSBuild build systems."
description: "Diagnose and fix under-parallelized MSBuild builds. USE WHEN a multi-project solution build is slower than expected, doesn't speed up when you add cores, pegs a single core while others idle, or you want to know why `-m` isn't helping. Note: `/maxcpucount` default is 1 (sequential) — always pass `-m` for parallel builds. Covers finding the critical path (longest serial ProjectReference chain), graph build (`/graph`), BuildInParallel, and solution filters (`.slnf`). DO NOT USE FOR: single-project builds, incremental issues (use incremental-build), compilation slowness inside one project (use build-perf-diagnostics), non-MSBuild build systems."
license: MIT
---

## Diagnose a slow parallel build (start here)

Work this checklist in order — it targets the usual root cause (a serial
dependency chain that no number of cores can parallelize):

1. **Confirm parallelism is even on.** Rebuild with `dotnet build -m /bl:{}`
(PowerShell: `dotnet build -m -bl:{{}}`). `-m` with no number uses all logical
processors; without `-m` MSBuild runs a single node (sequential).
2. **Find the critical path.** From the binlog, read per-project timings and the
node timeline. If total build time ≈ the sum of the projects on one
dependency chain, that chain — not CPU count — is the bottleneck.
3. **Name the chain explicitly**, e.g. `Core → Api → Web → Tests`. A long serial
chain stays serial no matter how large `-m` is, because each project waits on
its predecessor.
4. **Look for unnecessary `ProjectReference` edges** that lengthen the chain — a
reference that only needs build order (not the output assembly), or one that
could be a `PackageReference`, forces serialization it doesn't need.
5. **Recommend flattening**: break false dependencies so independent projects
build concurrently, and consider `/graph` for better scheduling.

## MSBuild Parallelism Model

- `/maxcpucount` (or `-m`): number of worker nodes (processes)
Expand Down
172 changes: 11 additions & 161 deletions plugins/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: check-bin-obj-clash
description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, missing outputs in multi-project builds, multi-targeting builds where project.assets.json conflicts. Diagnoses when multiple projects or TFMs write to the same bin/obj directories due to shared OutputPath, missing AppendTargetFrameworkToOutputPath, or extra global properties like PublishReadyToRun creating redundant evaluations, or SetTargetFramework metadata on a ProjectReference to a single-targeting project. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems."
description: "Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, or missing/overwritten outputs in multi-project or multi-targeting builds where bin/obj (or project.assets.json) collide. Common causes: shared OutputPath, missing AppendTargetFrameworkToOutputPath, extra global properties (e.g. PublishReadyToRun), or SetTargetFramework on a ProjectReference to a single-targeting project. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems."
license: MIT
---

Expand Down Expand Up @@ -75,141 +75,20 @@ Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluati

Use this only when the MCP server cannot be started.

### Step 2: Replay the Binary Log to Text
Replay the binlog to a diagnostic text log, then grep for the same signals the MCP tools surface:

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

### Step 3: List All Projects
Then extract the clash signals:

```bash
grep -i 'done building project\|Building project' full.log | grep -oP '"[^"]+\.csproj"' | sort -u
```

This lists all project files that participated in the build.

### Step 4: Check for Multiple Evaluations per Project

Multiple evaluations for the same project indicate multi-targeting or multiple build configurations:

```bash
# Count how many times each project was evaluated
grep -c 'Evaluation started' full.log
grep 'Evaluation started.*\.csproj' full.log
```

### Step 5: Check Global Properties for Each Evaluation

For each project, query the build properties to understand the build configuration:

```bash
# Search the diagnostic log for evaluated property values
grep -i 'TargetFramework\|Configuration\|Platform\|RuntimeIdentifier' full.log | head -40
```

Look for properties like `TargetFramework`, `Configuration`, `Platform`, and `RuntimeIdentifier` that should differentiate output paths.

Also check **solution-related properties** to identify multi-solution builds:
- `SolutionFileName`, `SolutionName`, `SolutionPath`, `SolutionDir`, `SolutionExt` — differ when a project is built from multiple solutions
- `CurrentSolutionConfigurationContents` — the number of project entries reveals which solution an evaluation belongs to (e.g., 1 project vs ~49 projects)

Look for **extra global properties that don't affect output paths** but create distinct MSBuild project instances:
- `PublishReadyToRun` — a publish setting that doesn't change `OutputPath` or `IntermediateOutputPath`, but MSBuild treats it as a distinct project instance, preventing result caching and causing redundant target execution (e.g., `CopyFilesToOutputDirectory` running again)
- Any other global property that differs between evaluations but doesn't contribute to path differentiation

### Filter Out Non-Build Evaluations

When analyzing clashes, filter evaluations based on the type of clash you're investigating:

1. **For OutputPath clashes**: Exclude restore-phase evaluations (where `MSBuildRestoreSessionId` global property is set). These don't write to output directories.

2. **For IntermediateOutputPath clashes**: Include restore-phase evaluations, as NuGet restore writes `project.assets.json` to the intermediate output path.

3. **Always exclude `BuildProjectReferences=false`**: These are P2P metadata queries, not actual builds that write files.

### Step 6: Get Output Paths for Each Project

Query each project's output path properties:

```bash
# From the diagnostic log - search for OutputPath assignments
grep -i 'OutputPath\s*=\|IntermediateOutputPath\s*=\|BaseOutputPath\s*=\|BaseIntermediateOutputPath\s*=' full.log | head -40

# Or query a specific project directly
dotnet msbuild MyProject.csproj -getProperty:OutputPath
dotnet msbuild MyProject.csproj -getProperty:IntermediateOutputPath
dotnet msbuild MyProject.csproj -getProperty:BaseOutputPath
dotnet msbuild MyProject.csproj -getProperty:BaseIntermediateOutputPath
```

### Step 7: Identify Clashes

Compare the `OutputPath` and `IntermediateOutputPath` values across all evaluations:

1. **Normalize paths** - Convert to absolute paths and normalize separators
2. **Group by path** - Find evaluations that share the same OutputPath or IntermediateOutputPath
3. **Report clashes** - Any group with more than one evaluation indicates a clash

### Step 8: Verify Clashes via CopyFilesToOutputDirectory (Optional)
- **Projects & evaluations** — list evaluation starts and count them per project: `grep 'Evaluation started' full.log | grep -oiE '"[^"]+\.[a-z]+proj"' | sort | uniq -c`. Matching the full **quoted path** keeps same-named projects in different directories distinct (and tolerates spaces in paths); a path with a count ≥ 2 was evaluated more than once (multi-targeting or extra global properties). (`grep -c` alone only totals evaluations across the whole log, so it can't reveal per-project duplication.)
- **Output paths** — `grep -iE 'OutputPath[[:space:]]*=|IntermediateOutputPath[[:space:]]*=|BaseOutputPath[[:space:]]*=|BaseIntermediateOutputPath[[:space:]]*=' full.log | sort -u`, or query a project directly: `dotnet msbuild MyProject.csproj -getProperty:OutputPath` (and `IntermediateOutputPath`, `BaseIntermediateOutputPath`).
- **Distinguishing global properties** — `grep -iE 'TargetFramework|Configuration|Platform|RuntimeIdentifier|SolutionFileName|PublishReadyToRun' full.log`. See the [Global Properties to Check](#global-properties-to-check-when-comparing-evaluations) table for which affect the path and which just fork a redundant instance.
- **Corroborating evidence (optional)** — `grep 'Target "CopyFilesToOutputDirectory"' full.log` plus `grep 'SkipUnchangedFiles' full.log` show a second instance writing (or skipping a masked write) to the same path; a long vs ~0 ms `CoreCompile` distinguishes the real build from a redundant instance.

As additional evidence for OutputPath clashes, check if multiple project builds execute the `CopyFilesToOutputDirectory` target to the same path. Note that not all clashes manifest here - compilation outputs and other targets may also conflict.

```bash
# Search for CopyFilesToOutputDirectory target execution per project
grep 'Target "CopyFilesToOutputDirectory"' full.log

# Look for Copy task messages showing file destinations
grep 'Copying file from\|SkipUnchangedFiles' full.log | head -30
```

Look for evidence of clashes in the messages:
- `Copying file from "..." to "..."` - Active file writes
- `Did not copy from file "..." to file "..." because the "SkipUnchangedFiles" parameter was set to "true"` - Indicates a second build attempted to write to the same location

The `SkipUnchangedFiles` skip message often masks clashes - the build succeeds but is vulnerable to race conditions in parallel builds.

### Step 9: Check CoreCompile Execution Patterns (Optional)

To understand which project instance did the actual compilation vs redundant work, check `CoreCompile`:

```bash
grep 'Target "CoreCompile"' full.log
```

Compare the durations:
- The instance with a long `CoreCompile` duration (e.g., seconds) is the **primary build** that did the actual compilation
- Instances where `CoreCompile` was skipped (duration ~0-10ms) are **redundant builds** — they didn't recompile but may still run other targets like `CopyFilesToOutputDirectory` that write to the same output directory

This helps distinguish the "real" build from redundant instances created by extra global properties or multi-solution builds.

### Caveat: Multi-Solution Builds

When analyzing multi-solution builds, note that the diagnostic log interleaves output from all projects. To determine which solution a project instance belongs to, search for `SolutionFileName` property assignments in the diagnostic log:

```bash
grep -i "SolutionFileName\|CurrentSolutionConfigurationContents" full.log | head -20
```

### Expected Output Structure

For each evaluation, collect:
- Project file path
- Evaluation ID
- TargetFramework (if multi-targeting)
- Configuration
- OutputPath
- IntermediateOutputPath

### Clash Detection Logic

```
For each unique OutputPath:
- If multiple evaluations share it → CLASH

For each unique IntermediateOutputPath:
- If multiple evaluations share it → CLASH
```
**Then identify clashes:** normalize paths to absolute, group evaluations by `OutputPath` and by `IntermediateOutputPath`, and exclude `BuildProjectReferences=false` (P2P queries) — plus, for `OutputPath` only, `MSBuildRestoreSessionId` restore evaluations. Any group with more than one remaining evaluation is a clash.

## Common Causes and Fixes

Expand Down Expand Up @@ -405,40 +284,11 @@ Injecting the TFM the project already targets is **path-neutral** — the projec

See the `msbuild-antipatterns` skill (AP-23) for the authoring-time smell and rationale.

## Example Workflow

```bash
# 1. Replay the binlog
dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log

# 2. List projects
grep 'done building project' full.log | grep -oP '"[^"]+\.csproj"' | sort -u

# 3. Check OutputPath for each evaluation
grep -i 'OutputPath\s*=' full.log | sort -u
# e.g. OutputPath = bin\Debug\net8.0\
# OutputPath = bin\Debug\net9.0\

# 4. Check IntermediateOutputPath
grep -i 'IntermediateOutputPath\s*=' full.log | sort -u
# e.g. IntermediateOutputPath = obj\Debug\net8.0\
# IntermediateOutputPath = obj\Debug\net9.0\

# 5. Compare paths → No clash (paths differ by TargetFramework)
```

## Tips

- Use `grep -i 'OutputPath\s*=' full.log | sort -u` to quickly find all OutputPath property assignments
- Check `BaseOutputPath` and `BaseIntermediateOutputPath` as they form the root of output paths
- The SDK default paths include `$(TargetFramework)` - clashes often occur when projects override these defaults
- Remember that paths may be relative - normalize to absolute paths before comparing
- **Cross-project IntermediateOutputPath clashes cannot be fixed with `AppendTargetFrameworkToOutputPath`** - files like `project.assets.json` are written directly to the intermediate path
- For multi-targeting clashes within the same project, `AppendTargetFrameworkToOutputPath=true` is the correct fix
- Common error messages indicating path clashes:
- `Cannot create a file when that file already exists` (NuGet restore)
- `The process cannot access the file because it is being used by another process`
- Intermittent build failures that succeed on retry
- The SDK default paths include `$(TargetFramework)` — clashes often occur when projects override these defaults; normalize relative paths to absolute before comparing.
- **Cross-project `IntermediateOutputPath` clashes cannot be fixed with `AppendTargetFrameworkToOutputPath`** — files like `project.assets.json` are written directly to the intermediate path. For multi-targeting clashes *within the same project*, `AppendTargetFrameworkToOutputPath=true` is the correct fix.
- Error messages that indicate a path clash: `Cannot create a file when that file already exists` (NuGet restore), `The process cannot access the file because it is being used by another process`, or intermittent failures that succeed on retry.

### Global Properties to Check When Comparing Evaluations

Expand Down
37 changes: 33 additions & 4 deletions plugins/dotnet-msbuild/skills/eval-performance/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@ description: "Guide for diagnosing and improving MSBuild project evaluation perf
license: MIT
---

# Diagnosing MSBuild Evaluation Performance

Evaluation is the work MSBuild does *before* any target runs — reading project
files, processing imports, expanding globs. This skill helps you **find and
confirm** evaluation bottlenecks. Measure first; recommend a change only when a
measurement proves it is warranted.

## Confirm the problem before changing anything

Engage only when evaluation is *measurably* the bottleneck. Do NOT act when:

- **The slowness is during compilation or target execution, not evaluation.**
That is not an evaluation problem — use `build-perf-diagnostics` instead.
- **The complaint is "rebuilds too much" / incremental build.** Use
`incremental-build` instead.
- **You have no measurement.** If no binlog or timing summary shows evaluation
is slow, gather one first (see below). Do not guess from reading project files.
- **A pattern below appears but evaluation is already fast.** Broad globs, deep
imports, or `EnableDefaultItems` are only worth flagging when the numbers show
they cost real time. A project that evaluates quickly needs no change.

When a pattern is present but unmeasured, **report it as an observation and let
the user decide** — do not rewrite working configuration to match a "best
practice" without evidence it costs measurable evaluation time. Prefer the
smallest, most targeted change; never disable SDK defaults as a first move.

## MSBuild Evaluation Phases

For a comprehensive overview of MSBuild's evaluation and execution model, see [Build process overview](https://learn.microsoft.com/en-us/visualstudio/msbuild/build-process-overview).
Expand Down Expand Up @@ -51,12 +77,15 @@ Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the

## Expensive Glob Patterns

Only pursue these remedies once a measurement shows item evaluation is slow and
the globs are the cause; a custom glob that isn't walking large trees is fine.

- Globs like `**/*.cs` walk the entire directory tree
- Default SDK globs are optimized, but custom globs may not be
- Problem: globbing over `node_modules/`, `.git/`, `bin/`, `obj/` — millions of files
- Fix: use `<DefaultItemExcludes>` to exclude large directories
- Fix: be specific with glob paths: `src/**/*.cs` instead of `**/*.cs`
- Fix: use `<EnableDefaultItems>false</EnableDefaultItems>` only as last resort (lose SDK defaults)
- Remedy: use `<DefaultItemExcludes>` to exclude large directories
- Remedy: be specific with glob paths: `src/**/*.cs` instead of `**/*.cs`
- Remedy: use `<EnableDefaultItems>false</EnableDefaultItems>` only as a last resort (loses SDK defaults) — prefer the two options above first
- Check: grep for Compile items in the diagnostic log → if Compile items include unexpected files, globs are too broad

## Import Chain Analysis
Expand All @@ -65,7 +94,7 @@ Use the **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the
- Each import: file I/O + parse + evaluate
- Common causes: NuGet packages adding .props/.targets, framework SDK imports, Directory.Build chains
- Diagnosis: `/pp` output → search for `<!-- Importing` comments to see import tree
- Fix: reduce transitive package imports where possible, consolidate imports
- Remedy (only if the chain is measurably costly): reduce transitive package imports where possible, consolidate imports

## Multiple Evaluations

Expand Down
Loading
Loading