Skills eval-performance
Guide for diagnosing and improving MSBuild project evaluation performance. Only activate in MSBuild/.NET build context. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems. INVOKES: dotnet msbuild -pp:full.xml for preprocessing, /clp:PerformanceSummary.
git clone https://github.com/dotnet/skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dotnet/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/dotnet-msbuild/skills/eval-performance" ~/.claude/skills/dotnet-skills-eval-performance && rm -rf "$T"
plugins/dotnet-msbuild/skills/eval-performance/SKILL.mdMSBuild Evaluation Phases
For a comprehensive overview of MSBuild's evaluation and execution model, see Build process overview.
- Initial properties: environment variables, global properties, reserved properties
- Imports and property evaluation: process
, evaluate<Import>
top-to-bottom<PropertyGroup> - Item definition evaluation:
metadata defaults<ItemDefinitionGroup> - Item evaluation:
with<ItemGroup>
,Include
,Remove
, glob expansionUpdate - UsingTask evaluation: register custom tasks
Key insight: evaluation happens BEFORE any targets run. Slow evaluation = slow build start even when nothing needs compiling.
Diagnosing Evaluation Performance
Using binlog
- Replay the binlog:
dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log - Search for evaluation events:
grep -i 'Evaluation started\|Evaluation finished' full.log - Multiple evaluations for the same project = overbuilding
- Look for "Project evaluation started/finished" messages and their timestamps
Using /pp (preprocess)
dotnet msbuild -pp:full.xml MyProject.csproj- Shows the fully expanded project with ALL imports inlined
- Use to understand: what's imported, import depth, total content volume
- Large preprocessed output (>10K lines) = heavy evaluation
Using /clp:PerformanceSummary
- Add to build command for timing breakdown
- Shows evaluation time separately from target/task execution
Expensive Glob Patterns
- Globs like
walk the entire directory tree**/*.cs - Default SDK globs are optimized, but custom globs may not be
- Problem: globbing over
,node_modules/
,.git/
,bin/
— millions of filesobj/ - Fix: use
to exclude large directories<DefaultItemExcludes> - Fix: be specific with glob paths:
instead ofsrc/**/*.cs**/*.cs - Fix: use
only as last resort (lose SDK defaults)<EnableDefaultItems>false</EnableDefaultItems> - Check: grep for Compile items in the diagnostic log → if Compile items include unexpected files, globs are too broad
Import Chain Analysis
- Deep import chains (>20 levels) slow evaluation
- Each import: file I/O + parse + evaluate
- Common causes: NuGet packages adding .props/.targets, framework SDK imports, Directory.Build chains
- Diagnosis:
output → search for/pp
comments to see import tree<!-- Importing - Fix: reduce transitive package imports where possible, consolidate imports
Multiple Evaluations
- A project evaluated multiple times = wasted work
- Common causes: referenced from multiple other projects with different global properties
- Each unique set of global properties = separate evaluation
- Diagnosis:
→ if count > 1, check for differing global propertiesgrep 'Evaluation started.*ProjectName' full.log - Fix: normalize global properties, use graph build (
)/graph
TreatAsLocalProperty
- Prevents property values from flowing to child projects via MSBuild task
- Overuse: declaring many TreatAsLocalProperty entries adds evaluation overhead
- Correct use: only when you genuinely need to override an inherited property
Property Function Cost
- Property functions execute during evaluation
- Most are cheap (string operations)
- Expensive:
during evaluation — reads file on every evaluation$([System.IO.File]::ReadAllText(...)) - Expensive: network calls, heavy computation
- Rule: property functions should be fast and side-effect-free
Optimization Checklist
- Check preprocessed output size:
dotnet msbuild -pp:full.xml - Verify evaluation count: should be 1 per project per TFM
- Exclude large directories from globs
- Avoid file I/O in property functions during evaluation
- Minimize import depth
- Use graph build to reduce redundant evaluations
- Check for unnecessary UsingTask declarations