# Dotnet Performance Profiling And Optimization

> Use when a .NET process is slow, hung, memory-heavy, or deadlocked, or when analyzing C#/ASP.NET Core code for performance anti-patterns across memory, async, LINQ, database, JSON, caching, DI, concurrency, HttpClient, exceptions, response, strings, startup, and metrics.

- Skill: `mccteam/dotnet-performance-profiling-and-optimization` (Agent Skill, multi-file: 13 files)
- Install (CLI): `npx skillmds@latest add mccteam/dotnet-performance-profiling-and-optimization`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mccteam/dotnet-performance-profiling-and-optimization/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: mccteam (https://skillmd.com/u/mccteam)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mccteam/dotnet-performance-profiling-and-optimization

---


# .NET Performance: Diagnostic & Code Review

Unified C#/.NET performance skill targeting **.NET 8 and .NET 10**. Two modes: live process diagnostics (Mode A) and static code optimization review with fixes (Mode B).

## Step 0 — Detect the target framework

Before recommending APIs, follow `../../references/detect-target-framework.md`. Many .NET 9+ APIs (`HybridCache`, `MemoryExtensions.Split` for spans, `Dictionary.GetAlternateLookup`, `params ReadOnlySpan<T>`) do **not** exist on .NET 8 — the references below mark the floor for each pattern, and you must downgrade to the .NET 8 fallback when the target is `net8.0`. Stephen Toub's posts are the primary benchmark source:
[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) ·
[Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/).

## References

Load on demand:
- [references/memory-model-gc.md](references/memory-model-gc.md) — stack vs heap, generations, LOH, boxing, GC tuning
- [references/code-patterns.md](references/code-patterns.md) — LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings (conceptual framing)
- [references/categories.md](references/categories.md) — 14 optimization category definitions with checks and grep patterns (Mode B spine)
- [references/grep-patterns.md](references/grep-patterns.md) — consolidated anti-pattern grep library for Phase 1 scanning
- [references/measurement-guide.md](references/measurement-guide.md) — BenchmarkDotNet, k6, dotnet-counters, KPI targets, CI/CD

Pattern catalogs (with measured impact numbers, ❌/✅ pairs, and per-topic Detection recipes):
- [references/critical-patterns.md](references/critical-patterns.md) — 17 🔴 patterns: deadlocks, order-of-magnitude regressions, excessive allocations
- [references/async-patterns.md](references/async-patterns.md) — sync-over-async, ValueTask hot paths, Channels, false sharing
- [references/memory-and-strings.md](references/memory-and-strings.md) — `u8` literals, `Span.Split`/`TryWrite`, compound `+=`, chained `.Replace()`
- [references/collections-and-linq.md](references/collections-and-linq.md) — `FrozenDictionary`, `GetAlternateLookup`, `CollectionsMarshal.GetValueRefOrAddDefault`, hoisting static data
- [references/regex-patterns.md](references/regex-patterns.md) — `[GeneratedRegex]`, `IsMatch`, `EnumerateMatches`, `NonBacktracking`
- [references/io-and-serialization.md](references/io-and-serialization.md) — `HttpCompletionOption.ResponseHeadersRead`, `useAsync` `FileStream`, `Memory<byte>` overloads
- [references/structural-patterns.md](references/structural-patterns.md) — sealed-class devirtualization (absence pattern, scale-based severity)

Reference loading guide for Mode B by signal:

| Signal in Code | Load |
|---|---|
| `async`, `await`, `Task`, `ValueTask` | `async-patterns.md` |
| `Span<`, `Memory<`, `stackalloc`, `string.Substring`, `+=` in loops, `params` | `memory-and-strings.md` |
| `Regex`, `[GeneratedRegex]`, `Regex.Match`, `RegexOptions.Compiled` | `regex-patterns.md` |
| `Dictionary<`, `List<`, `.ToList()`, LINQ chains, `static readonly Dictionary<` | `collections-and-linq.md` |
| `JsonSerializer`, `HttpClient`, `Stream`, `FileStream` | `io-and-serialization.md` |
| Any code review on a hot path | always check `critical-patterns.md` first |
| Codebase-wide scans (sealed classes, static `Dictionary` → `FrozenDictionary`) | `structural-patterns.md` |

## Iron Rule

**Always measure first, change second, and re-measure third.**

Never claim an optimization without before/after evidence from the same scenario.

| Rationalization | Reality |
|---|---|
| "This is obviously slow" | The runtime, JIT, and libraries often invalidate intuition. |
| "struct means stack" | Value types are stored inline — not always on the stack. |
| "All LINQ is slow" | .NET 9+ improved many LINQ paths. Measure before rewriting. |
| "GC.Collect will fix it" | Forced collection treats symptoms, not cause. |
| "Too small to matter" | MEDIUM+ impact is cumulative across the request pipeline. |
| "I'll change the DI lifetime while I'm here" | DI lifetime changes require explicit user approval. |
| "Need to refactor to optimize" | Optimization fixes must be surgical. Refactoring is a separate task. |
| "Tests pass so fix is correct" | Tests passing = behavior preserved. Still verify the metric improved. |

## Mode Selection

| Situation | Mode |
|---|---|
| Live process: slow, high CPU/memory, hung, deadlocked, GC pauses | **A – Diagnostic** |
| Asking how GC, heap, boxing, or LINQ overhead works in .NET | **A – Diagnostic** (conceptual) |
| Code to analyze for anti-patterns, then fix | **B – Code Review** |
| Both a running process AND code to fix | Start with **A**, then **B** on hot paths identified |

**Not for:** Visual Studio, Rider, PerfView, speedscope, or GUI-first workflows.

---

## Mode A: Diagnostic (Live Process)

### Investigation Order

1. **`dotnet-counters`** — always start here for live triage.
2. **`dotnet-stack`** — immediately if process is stuck, hung, or deadlocked.
3. **`dotnet-trace`** — if CPU or allocation hot paths matter.
4. **`dotnet-gcdump`** — if heap growth matters more than call paths.
5. **`dotnet-dump`** — if SOS heap inspection or postmortem analysis is needed.
6. After live evidence identifies a candidate routine, apply patterns from [references/code-patterns.md](references/code-patterns.md) and [references/memory-model-gc.md](references/memory-model-gc.md).
7. Use BenchmarkDotNet if the change is isolated and needs microbenchmark comparison.
8. Re-run the original live capture to prove the real workload improved.

### CLI Tool Selection

| Question | Tool | What it answers |
|---|---|---|
| Is the process allocating, GCing, or saturating CPU? | `dotnet-counters` | Live counters and trend direction |
| Is the process hung or deadlocked right now? | `dotnet-stack` | Current managed stack snapshot |
| Which call paths consume CPU or allocate heavily? | `dotnet-trace` | Sampled execution and runtime events |
| Which object types dominate managed heap? | `dotnet-gcdump` | Heap composition and type totals |
| Need SOS heap inspection or thread state? | `dotnet-dump` | Full dump plus CLI analysis |
| Did a code change improve an isolated routine? | BenchmarkDotNet | Reproducible microbenchmark comparison |

### Minimal CLI Commands

```bash
dotnet-counters monitor -p <PID> --counters System.Runtime
dotnet-counters monitor -n <ProcessName> --counters System.Runtime,Microsoft.AspNetCore.Hosting
dotnet-stack report -p <PID>
dotnet-trace collect -p <PID> --duration 00:00:30
dotnet-trace report <trace.nettrace> topN
dotnet-gcdump collect -p <PID>
dotnet-gcdump report <file.gcdump>
dotnet-dump collect -p <PID> --type Heap
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
```

Minimal BenchmarkDotNet pattern:
```csharp
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net90)]
public class CandidateBench
{
    [Benchmark(Baseline = true)]
    public int Original() => OriginalImpl();

    [Benchmark]
    public int Candidate() => CandidateImpl();
}
```
```bash
dotnet run -c Release
```

### Reference Loading Guide

| User question | Load first |
|---|---|
| "How do stack and heap really work in .NET?" | `references/memory-model-gc.md` |
| "Why is GC pausing or why is LOH churn hurting us?" | `references/memory-model-gc.md` |
| "How should I optimize this LINQ?" | `references/code-patterns.md` |
| "Can I move this to the stack with stackalloc or Span?" | `references/code-patterns.md` |
| "Should this be a struct, ref struct, readonly struct, or class?" | Both |
| "Why is this boxing?" | `references/code-patterns.md` |

### Diagnostic Output

Report: measured symptom + evidence (counter values, trace hotspots, heap stats) · chosen tool and why · relevant tradeoff (allocation vs copy, deferred vs eager, stack vs pool) · before/after result, or explicitly state if still unverified.

---

## Mode B: Code Review (Static Analysis)

### Target

`$ARGUMENTS` is the optimization target:
- **File path**: Analyze that file and its close dependencies.
- **Directory**: Analyze all C# files in that directory.
- **"all"**: Scan the solution with Grep, deep-dive the worst offenders.
- **`--fix`** anywhere: Skip confirmation and apply fixes after analysis.
- **Empty**: Check `git diff --name-only HEAD~5 -- '*.cs'` for recently changed files. If none, ask the user.

### Phase 1: Discovery

1. **Glob** to find `.cs` files matching the target.
2. **Read** file contents. For files under 500 lines, read the whole file first — visual inspection catches patterns faster than grep, then grep confirms counts.
3. **Detect signals** in the code (async, Span, Regex, Dictionary, JsonSerializer, etc.) and **load matching pattern catalogs** from the per-topic references listed at the top of this file.
4. **Grep** for anti-patterns. Run the recipes in [references/grep-patterns.md](references/grep-patterns.md) plus the per-topic Detection sections in the catalogs you loaded.
5. **Emit a scan execution checklist** before classifying — list each recipe and the hit count. **0 hits is valid and valuable** (confirms good practice).

### Phase 2: Analysis (Read-Only)

Check each file against all 14 categories. Record per finding: **file path, line number, current pattern, recommended pattern, impact level, category**.

Read [references/categories.md](references/categories.md) for detailed check definitions.

#### Compound Allocation Check

Single-line grep recipes miss multi-allocation patterns. After running scan recipes, look for:

1. **Branched `.Replace()` chains** — methods that call `.Replace()` across multiple `if/else` branches. Report total allocation count across all branches, not just per-line.
2. **Cross-method chaining** — public method A calls B (which does 3 regex replaces) then calls C (which allocates). Report the total chain cost as one finding, not per-method.
3. **Compound `+=` with embedded allocating calls** — `result += $"...{Foo().ToLower()}"` is 2+ allocations (interpolation + `ToLower` + concatenation). Flag the compound cost, not just `.ToLower()`.
4. **`string.Format` specificity** — distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate only the actionable sites.

#### Cross-File Consistency Check

If an optimized pattern is found in one file, check whether sibling files (same directory, same interface, same base class) use the un-optimized equivalent. Flag as MEDIUM with the optimized file as evidence.

#### Verify-the-Inverse Rule

For absence patterns (e.g., unsealed classes, static `Dictionary` not converted to `FrozenDictionary`, `RegexOptions.Compiled` not migrated to `[GeneratedRegex]`), always count both sides and report the **N-of-M ratio**, not just the count of bad cases. The ratio determines severity:

- 0/185 sealed → systematic codebase-wide issue
- 12/15 sealed → consistency fix on the remaining 3
- 50/100 sealed → mid-migration; flag the laggards

| # | Category | Code | Focus |
|---|---|---|---|
| 1 | Memory Allocation | MEM | Span, ArrayPool, pooling, stackalloc, string optimization, collections |
| 2 | Async Anti-Patterns | ASYNC | Blocking, ValueTask, CancellationToken, IAsyncEnumerable, Channel |
| 3 | LINQ Inefficiencies | LINQ | Count vs Any, multiple enumeration, filter/project order |
| 4 | Database | DB | EF Core, CosmosDB patterns, N+1, partition keys, RU cost |
| 5 | JSON Serialization | JSON | Options reuse, source generators, serializer boundaries |
| 6 | Caching | CACHE | HybridCache, stampede protection, output cache, size limits |
| 7 | DI Lifetimes | DI | Captive dependencies, lifetime mismatches, IOptions patterns |
| 8 | Concurrency | CONC | Lock contention, throttling, thread safety, Channel patterns |
| 9 | HttpClient | HTTP | IHttpClientFactory, resilience, response disposal |
| 10 | Exception Control Flow | EXC | Try/catch for expected paths, broad catches |
| 11 | Response Optimization | RESP | Compression, pagination, ETags |
| 12 | String Optimization | STR | Concatenation loops, ToLower/ToUpper, String.Format |
| 13 | Startup & Pipeline | STARTUP | Middleware ordering, compression, health checks, PGO |
| 14 | Metrics & Observability | METRICS | IMeterFactory, histograms, tag cardinality, OpenTelemetry |

### Phase 3: Report

```
## Performance Analysis Report

### Summary
- Files analyzed: N
- Total findings: N
- Critical (HIGH): N | Moderate (MEDIUM): N | Minor (LOW): N

### Findings by Category

#### [CATEGORY_NAME] (N findings)

| # | Impact | File:Line | Issue | Recommendation |
|---|--------|-----------|-------|----------------|
| 1 | HIGH   | `path/File.cs:42` | Current anti-pattern | Recommended fix |

### Prioritized Action List
1. [HIGH] Fix blocking async calls in X — thread pool starvation risk
2. [MEDIUM] Switch to ArrayPool in Z — reduces GC pressure on upload path
```

**Impact levels:**
- **HIGH**: Measurable gain, prevents starvation, fixes correctness, reduces P95 latency. Examples: blocking async, missing CancellationToken, N+1 queries, captive dependencies.
- **MEDIUM**: Reduces allocations, GC pressure, or unnecessary work. Examples: ArrayPool, StringBuilder, FrozenDictionary.
- **LOW**: Minor improvements, cold-path optimizations. Examples: initial collection capacity, Count() vs Any().

**Scale-based severity escalation.** When the same anti-pattern appears across many instances, escalate:

- 1–10 instances → report at the pattern's base severity
- 11–50 instances → escalate LOW patterns to MEDIUM
- 50+ instances → MEDIUM with elevated priority; flag as a codebase-wide systematic issue

Always report **exact counts from scan recipes**, not estimates. Group findings by severity (HIGH → MEDIUM → LOW), not by file. Merge related findings that share the same fix (e.g., all `.ToLower()` calls in one finding, not split per file).

### Phase 4: Optimization (Apply Fixes)

After presenting the report:
- If `--fix` in `$ARGUMENTS`, proceed directly.
- Otherwise ask: "Would you like me to apply these optimizations? I'll work one category at a time, starting with HIGH impact. You can specify categories or findings (e.g., 'fix ASYNC and MEM' or 'fix #1, #3')."

**Before any fix:**
1. **Read actual code context** around the grep match — false positives exist (`.Result` in `Task.FromResult` is NOT blocking).
2. Confirm the finding is real. If uncertain, flag as "needs manual review."

**Applying fixes:**
1. One category at a time, highest impact first. Use Edit tool with brief before/after summary.
2. After each category: `dotnet build --no-restore`
3. After all changes: `dotnet test`
4. If build or tests fail, diagnose before continuing.

---

## Analyzer Radar

- `CA1826`, `CA1827`, `CA1829`, `CA1836`, `CA1851`, `CA1860` — LINQ and enumeration
- `CA1845`, `CA1846`, `CA1858` — string and span-friendly APIs
- `CA1834`, `CA1865`–`CA1867` — StringBuilder char overloads
- `CA1870` — cached `SearchValues<T>`

These are clues, not goals. Apply where measured hot paths justify it.

## Pattern Guardrails

- Do not say "put it on the stack" as a blanket goal. Explain lifetime, copies, boxing, and escape rules.
- Do not suggest `stackalloc` for unbounded sizes, large buffers, or loop-carried allocations.
- Do not recommend `Span<T>` for data that crosses `await`, escapes to the heap, or lives in object fields — use `Memory<T>`.
- Do not recommend converting every `class` to a `struct` — large, mutable, or frequently boxed types often get worse.
- Do not blanket-rewrite LINQ to loops — use analyzer-backed fixes first.
- Do not recommend pooling without ownership rules — returned pooled arrays must not be reused by the caller.
- Do not recommend `GC.Collect()` except for rare justified lifecycle boundaries with measurement.

## Red Flags — STOP and Confirm

Stop and ask before:
- Changing `Program.cs` or the middleware pipeline
- Adding a new NuGet package
- Changing any DI service lifetime registration
- Replacing the serializer in the HTTP pipeline
- Modifying API response shapes or route patterns
- Changing error handling patterns

## Constraints

- NEVER add NuGet packages without user approval
- NEVER change DI lifetimes without explaining implications and getting confirmation
- NEVER modify `Program.cs` or middleware pipeline without explicit approval
- NEVER change API contracts, route patterns, or response shapes
- ALWAYS preserve existing tests; update only if behavior intentionally changes
- ALWAYS use the Grep tool for searches, never bash `grep` or `find`

Consult the project's CLAUDE.md or AGENTS.md for project-specific rules and constraints.

