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.

install
source · Clone the upstream repo
git clone https://github.com/dotnet/skills
Claude Code · Install into ~/.claude/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"
manifest: plugins/dotnet-msbuild/skills/eval-performance/SKILL.md
source content

MSBuild Evaluation Phases

For a comprehensive overview of MSBuild's evaluation and execution model, see Build process overview.

  1. Initial properties: environment variables, global properties, reserved properties
  2. Imports and property evaluation: process
    <Import>
    , evaluate
    <PropertyGroup>
    top-to-bottom
  3. Item definition evaluation:
    <ItemDefinitionGroup>
    metadata defaults
  4. Item evaluation:
    <ItemGroup>
    with
    Include
    ,
    Remove
    ,
    Update
    , glob expansion
  5. 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

  1. Replay the binlog:
    dotnet msbuild build.binlog -noconlog -fl -flp:v=diag;logfile=full.log
  2. Search for evaluation events:
    grep -i 'Evaluation started\|Evaluation finished' full.log
  3. Multiple evaluations for the same project = overbuilding
  4. 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
    **/*.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)
  • 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:
    /pp
    output → search for
    <!-- Importing
    comments to see import tree
  • 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:
    grep 'Evaluation started.*ProjectName' full.log
    → if count > 1, check for differing global properties
  • 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:
    $([System.IO.File]::ReadAllText(...))
    during evaluation — reads file on every evaluation
  • 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