{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/build-perf-baseline",
  "version": "1.0.0",
  "name": "build-perf-baseline",
  "description": "Establish build performance baselines and apply systematic optimization techniques. Only activate in MSBuild/.NET build context. USE FOR: diagnosing slow builds, establishing before/after measurements (cold, warm, no-op scenarios), applying optimization strategies like MSBuild Server, static graph builds, artifacts output, and dependency graph trimming. Start here before diving into build-perf-diagnostics, incremental-build, or build-parallelism. DO NOT USE FOR: non-MSBuild build systems, detailed bottleneck analysis (use build-perf-diagnostics after baselining).",
  "system_prompt_fragment": "# Build Performance Baseline & Optimization\n\n## Overview\n\nBefore optimizing a build, you need a **baseline**. Without measurements, optimization is guesswork. This skill covers how to establish baselines and apply systematic optimization techniques.\n\n**Related skills:**\n- `build-perf-diagnostics` — binlog-based bottleneck identification\n- `incremental-build` — Inputs/Outputs and up-to-date checks\n- `build-parallelism` — parallel and graph build tuning\n- `eval-performance` — glob and import chain optimization\n\n---\n\n## Step 1: Establish a Performance Baseline\n\nMeasure three scenarios to understand where time is spent:\n\n### Cold Build (First Build)\n\nNo previous build output exists. Measures the full end-to-end time including restore, compilation, and all targets.\n\n```bash\n# Clean everything first\ndotnet clean\n# Remove bin/obj to truly start fresh\nGet-ChildItem -Recurse -Directory -Include bin,obj | Remove-Item -Recurse -Force\n# OR on Linux/macOS:\n# find . -type d \\( -name bin -o -name obj \\) -exec rm -rf {} +\n\n# Measure cold build\ndotnet build /bl:cold-build.binlog -m\n```\n\n### Warm Build (Incremental Build)\n\nBuild output exists, some files have changed. Measures how well incremental build works.\n\n```bash\n# Build once to populate outputs\ndotnet build -m\n\n# Make a small change (touch one .cs file)\n# Then rebuild\ndotnet build /bl:warm-build.binlog -m\n```\n\n### No-Op Build (Nothing Changed)\n\nBuild output exists, nothing has changed. This should be nearly instant. If it's slow, incremental build is broken.\n\n```bash\n# Build once to populate outputs\ndotnet build -m\n\n# Rebuild immediately without changes\ndotnet build /bl:noop-build.binlog -m\n```\n\n### What Good Looks Like\n\n| Scenario | Expected Behavior |\n|----------|------------------|\n| Cold build | Full compilation, all targets run. This is your absolute baseline |\n| Warm build | Only changed projects recompile. Time proportional to change scope |\n| No-op build | < 5 seconds for small repos, < 30 seconds for large repos. All compilation targets should report \"Skipping target — all outputs up-to-date\" |\n\n**Red flags:**\n- No-op build > 30 seconds → incremental build is broken (see `incremental-build` skill)\n- Warm build recompiles everything → project dependency chain forces full rebuild\n- Cold build has long restore → NuGet cache issues\n\n### Recording Baselines\n\nRecord baselines in a structured way before and after optimization:\n\n```\n| Scenario    | Before  | After   | Improvement |\n|-------------|---------|---------|-------------|\n| Cold build  | 2m 15s  |         |             |\n| Warm build  | 1m 40s  |         |             |\n| No-op build | 45s     |         |             |\n```\n\n---\n\n## Step 2: MSBuild Server (Persistent Build Process)\n\nThe MSBuild server keeps the build process alive between invocations, avoiding JIT compilation and assembly loading overhead on every build.\n\n### Enabling MSBuild Server\n\n```bash\n# Enabled by default in .NET 8+ but can be forced\ndotnet build /p:UseSharedCompilation=true\n```\n\nThe MSBuild server is started automatically and reused across builds. The compiler server (VBCSCompiler / `dotnet build-server`) is separate but complementary.\n\n### Managing the Build Server\n\n```bash\n# Check if the server is running\ndotnet build-server status\n\n# Shut down all build servers (useful when debugging)\ndotnet build-server shutdown\n```\n\n### When to Restart the Build Server\n\nRestart after:\n- Updating the .NET SDK\n- Changing MSBuild tooling (custom tasks, props, targets)\n- Debugging build infrastructure issues\n- Seeing stale behavior in repeated builds\n\n```bash\ndotnet build-server shutdown\ndotnet build\n```\n\n---\n\n## Step 3: Artifacts Output Layout\n\nThe `UseArtifactsOutput` feature (introduced in .NET 8) changes the output directory structure to avoid bin/obj clash issues and enable better caching.\n\n### Enabling Artifacts Output\n\n```xml\n<!-- Directory.Build.props -->\n<PropertyGroup>\n  <UseArtifactsOutput>true</UseArtifactsOutput>\n</PropertyGroup>\n```\n\n### Before vs After\n\n```\n# Traditional layout (before)\nsrc/\n  MyLib/\n    bin/Debug/net8.0/MyLib.dll\n    obj/Debug/net8.0/...\n  MyApp/\n    bin/Debug/net8.0/MyApp.dll\n\n# Artifacts layout (after)\nartifacts/\n  bin/MyLib/debug/MyLib.dll\n  bin/MyApp/debug/MyApp.dll\n  obj/MyLib/debug/...\n  obj/MyApp/debug/...\n```\n\n### Benefits\n\n- **No bin/obj clash**: Each project+configuration gets a unique path automatically\n- **Easier to cache**: Single `artifacts/` directory to cache/restore in CI\n- **Cleaner .gitignore**: Just ignore `artifacts/`\n- **Multi-targeting safe**: Each TFM gets its own subdirectory\n\n### Customizing\n\n```xml\n<!-- Change the artifacts root -->\n<PropertyGroup>\n  <ArtifactsPath>$(MSBuildThisFileDirectory)output</ArtifactsPath>\n</PropertyGroup>\n```\n\n---\n\n## Step 4: Deterministic Builds\n\nDeterministic builds produce byte-for-byte identical output given the same inputs. This is essential for build caching and reproducibility.\n\n### Enabling Deterministic Builds\n\n```xml\n<!-- Directory.Build.props -->\n<PropertyGroup>\n  <!-- Enabled by default in .NET SDK projects since SDK 2.0+ -->\n  <Deterministic>true</Deterministic>\n\n  <!-- For full reproducibility, also set: -->\n  <ContinuousIntegrationBuild Condition=\"'$(CI)' == 'true'\">true</ContinuousIntegrationBuild>\n</PropertyGroup>\n```\n\n### What Deterministic Affects\n\n- Removes timestamps from PE headers\n- Uses consistent file paths in PDBs\n- Produces identical output for identical input\n\n### Why It Matters for Performance\n\n- **Build caching**: If outputs are deterministic, you can cache and reuse them across builds and machines\n- **CI optimization**: Skip rebuilding unchanged projects by comparing inputs\n- **Distributed builds**: Safe to cache compilation results in shared storage\n\n---\n\n## Step 5: Dependency Graph Trimming\n\nReducing unnecessary project references shortens the critical path and reduces what gets built.\n\n### Audit the Dependency Graph\n\n```bash\n# Visualize the dependency graph\ndotnet build /bl:graph.binlog\n\n# In the binlog, check project references and build times\n# Look for projects that are referenced but could be trimmed\n```\n\n### Techniques\n\n#### Remove Redundant Transitive References\n\n```xml\n<!-- BAD: Utils is already referenced transitively via Core -->\n<ItemGroup>\n  <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n  <ProjectReference Include=\"..\\Utils\\Utils.csproj\" />\n</ItemGroup>\n\n<!-- GOOD: Let transitive references flow automatically -->\n<ItemGroup>\n  <ProjectReference Include=\"..\\Core\\Core.csproj\" />\n</ItemGroup>\n```\n\n#### Build-Order-Only References\n\nWhen you need a project to build before yours but don't need its assembly output:\n\n```xml\n<!-- Only ensures build order, doesn't reference the output assembly -->\n<ProjectReference Include=\"..\\CodeGen\\CodeGen.csproj\"\n                  ReferenceOutputAssembly=\"false\" />\n```\n\n#### Prevent Transitive Flow\n\nWhen a dependency is an internal implementation detail that shouldn't flow to consumers:\n\n```xml\n<!-- Don't expose this dependency transitively -->\n<ProjectReference Include=\"..\\InternalHelpers\\InternalHelpers.csproj\"\n                  PrivateAssets=\"all\" />\n```\n\n#### Disable Transitive Project References\n\nFor explicit-only dependency management (extreme measure for very large repos):\n\n```xml\n<PropertyGroup>\n  <DisableTransitiveProjectReferences>true</DisableTransitiveProjectReferences>\n</PropertyGroup>\n```\n\n**Caution**: This requires all dependencies to be listed explicitly. Only use in large repos where transitive closure is causing excessive rebuilds.\n\n---\n\n## Step 6: Static Graph Builds (`/graph`)\n\nStatic graph mode evaluates the entire project graph before building, enabling better scheduling and isolation.\n\n### Enabling Graph Build\n\n```bash\n# Single invocation\ndotnet build /graph\n\n# With binary log for analysis\ndotnet build /graph /bl:graph-build.binlog\n```\n\n### Benefits\n\n- **Better parallelism**: MSBuild knows the full graph upfront and can schedule optimally\n- **Build isolation**: Each project builds in isolation (no cross-project state leakage)\n- **Caching potential**: With isolation, individual project results can be cached\n\n### When to Use\n\n| Scenario | Recommendation |\n|----------|---------------|\n| Large multi-project solution (20+ projects) | ✅ Try `/graph` — may see significant parallelism gains |\n| Small solution (< 5 projects) | ❌ Overhead of graph evaluation outweighs benefits |\n| CI builds | ✅ Graph builds are more predictable and parallelizable |\n| Local development | ⚠️ Test both — may or may not help depending on project structure |\n\n### Troubleshooting Graph Build\n\nGraph build requires that all `ProjectReference` items are statically determinable (no dynamic references computed in targets). If graph build fails:\n\n```\nerror MSB4260: Project reference \"...\" could not be resolved with static graph.\n```\n\n**Fix**: Ensure all `ProjectReference` items are declared in `<ItemGroup>` outside of targets (not dynamically computed inside `<Target>` blocks).\n\n---\n\n## Step 7: Parallel Build Tuning\n\n### MaxCpuCount\n\n```bash\n# Use all available cores (default in dotnet build)\ndotnet build -m\n\n# Specify explicit core count (useful for CI with shared agents)\ndotnet build -m:4\n\n# MSBuild.exe syntax\nmsbuild /m:8 MySolution.sln\n```\n\n### Identifying Parallelism Bottlenecks\n\nIn a binlog, look for:\n- **Long sequential chains**: Projects that must build one after another due to dependencies\n- **Uneven load**: Some build nodes idle while others are overloaded\n- **Single-project bottleneck**: One large project on the critical path that blocks everything\n\nUse `grep 'Target Performance Summary' -A 30 full.log` in binlog analysis to see build node utilization.\n\n### Reducing the Critical Path\n\nThe critical path is the longest chain of dependent projects. To shorten it:\n\n1. **Break large projects into smaller ones** that can build in parallel\n2. **Remove unnecessary ProjectReferences** (see Step 5)\n3. **Use `ReferenceOutputAssembly=\"false\"`** for build-order-only dependencies\n4. **Move shared code to a base library** that builds first, then parallelize consumers\n\n---\n\n## Step 8: Additional Quick Wins\n\n### Separate Restore from Build\n\n```bash\n# In CI, restore once then build without restore\ndotnet restore\ndotnet build --no-restore -m\ndotnet test --no-build\n```\n\n### Skip Unnecessary Targets\n\n```bash\n# Skip building documentation\ndotnet build /p:GenerateDocumentationFile=false\n\n# Skip analyzers during development (not for CI!)\ndotnet build /p:RunAnalyzers=false\n```\n\n### Use Project-Level Filtering\n\n```bash\n# Build only the project you're working on (and its dependencies)\ndotnet build src/MyApp/MyApp.csproj\n\n# Don't build the entire solution if you only need one project\n```\n\n### Binary Log for All Investigations\n\nAlways start with a binlog:\n```bash\ndotnet build /bl:perf.binlog -m\n```\n\nThen use the `build-perf-diagnostics` skill and binlog tools for systematic bottleneck identification.\n\n---\n\n## Optimization Decision Tree\n\n```\nIs your no-op build slow (> 10s per project)?\n├── YES → See `incremental-build` skill (fix Inputs/Outputs)\n└── NO\n    Is your cold build slow?\n    ├── YES\n    │   Is restore slow?\n    │   ├── YES → Optimize NuGet restore (use lock files, configure local cache)\n    │   └── NO\n    │       Is compilation slow?\n    │       ├── YES\n    │       │   Are analyzers/generators slow?\n    │       │   ├── YES → See `build-perf-diagnostics` skill\n    │       │   └── NO → Check parallelism, graph build, critical path (this skill + `build-parallelism`)\n    │       └── NO → Check custom targets (binlog analysis via `build-perf-diagnostics`)\n    └── NO\n        Is your warm build slow?\n        ├── YES → Projects rebuilding unnecessarily → check `incremental-build` skill\n        └── NO → Build is healthy! Consider graph build or UseArtifactsOutput for further gains\n```",
  "applicable_domains": [
    "code",
    "dotnet",
    "engineering"
  ],
  "invocation": [
    "/build-perf-baseline"
  ],
  "tags": [
    "dotnet-msbuild",
    "dotnet",
    "csharp",
    "microsoft"
  ],
  "authored_by": "anthropics",
  "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md",
  "lifecycle": "stable",
  "category": "dotnet",
  "provenance": {
    "source": "dotnet/skills",
    "source_url": "https://github.com/dotnet/skills/blob/main/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md",
    "author": "Microsoft / .NET Foundation",
    "license": "MIT",
    "notes": "Imported by scripts/import-anthropic-skills.py."
  }
}