analyzing-dotnet-performance
Scans .NET code for ~50 performance anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I/O with tiered severity classification. Use when analyzing .NET code for optimization opportunities, reviewing hot paths, or auditing allocation-heavy pa
Install
npx skills add https://github.com/dotnet/skills/tree/main/plugins/dotnet-diag/skills/analyzing-dotnet-performance
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install dotnet-skills@llmmart
git clone https://github.com/dotnet/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole dotnet/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
.NET Performance Patterns
Scan C#/.NET code for performance anti-patterns and produce prioritized findings with concrete fixes. Patterns sourced from the official .NET performance blog series, distilled to customer-actionable guidance.
When to Use
- Reviewing C#/.NET code for performance optimization opportunities
- Auditing hot paths for allocation-heavy or inefficient patterns
- Systematic scan of a codebase for known anti-patterns before release
- Second-opinion analysis after manual performance review
When Not to Use
- Algorithmic complexity analysis — this skill targets API usage patterns, not algorithm design
- Code not on a hot path with no performance requirements — avoid premature optimization
Inputs
| Input | Required | Description |
|---|---|---|
| Source code | Yes | C# files, code blocks, or repository paths to scan |
| Hot-path context | Recommended | Which code paths are performance-critical |
| Target framework | Recommended | .NET version (some patterns require .NET 8+) |
| Scan depth | Optional | critical-only, standard (default), or comprehensive |
Workflow
Step 1: Load Critical Reference
Resolve bundled paths from the directory that contains this SKILL.md, not from the user's workspace. Load this reference file first:
references/critical-patterns.md
If a direct read fails, list this skill's references/ directory once and retry only when the listing shows the expected file. Do not use workspace file or text search to locate the skill installation.
Step 2: Detect Code Signals and Select Topic Recipes
Scan the code for signals that indicate which pattern categories to check. Use the ## Detection section from the critical reference when available and the inline recipes in Step 3 for initial signal detection.
After detecting signals, load only the topic-specific references selected by scan depth:
critical-only: No additional references (use onlycritical-patterns.md)standard(default): Load references matching detected signals from this list:references/async-patterns.md— async/Task/ValueTask signalsreferences/memory-and-strings.md— Span/Memory/string allocation signalsreferences/regex-patterns.md— Regex signalsreferences/collections-and-linq.md— Dictionary/List/LINQ signalsreferences/io-and-serialization.md— JsonSerializer/HttpClient/Stream signalsreferences/structural-patterns.md— always loaded (unsealed classes checked regardless)
comprehensive: Load all six topic-specific references above
For coverage reporting, the selected references are references/critical-patterns.md plus only the topic-specific references selected above. If any selected reference remains unavailable after retry, use the inline recipes in Step 3 for the missing coverage. Include Reference coverage: reduced; unavailable: <paths>; used inline recipes for missing references. in the final report, with <paths> replaced by the missing relative paths of the selected references only.
Use the ## Detection sections from loaded reference files and the inline recipes in Step 3 for categories whose reference files are unavailable.
| Signal in Code | Topic |
|---|---|
async, await, Task, ValueTask |
Async patterns |
Span<, Memory<, stackalloc, ArrayPool, string.Substring, .Replace(, .ToLower(), += in loops, params |
Memory & strings |
Regex, [GeneratedRegex], Regex.Match, RegexOptions.Compiled |
Regex patterns |
Dictionary<, List<, .ToList(), .Where(, .Select(, LINQ methods, static readonly Dictionary< |
Collections & LINQ |
JsonSerializer, HttpClient, Stream, FileStream |
I/O & serialization |
Always check structural patterns (unsealed classes) regardless of signals.
Scan depth controls scope:
critical-only: Only critical patterns (deadlocks, >10x regressions)standard(default): Critical + detected topic patternscomprehensive: All pattern categories
Step 3: Scan and Report
For files under 500 lines, read the entire file first — you'll spot most patterns faster than running individual grep recipes. Use grep to confirm counts and catch patterns you might miss visually.
For each relevant pattern category, run the detection recipes below. Report exact counts, not estimates.
Core scan recipes (run these when reference files aren't available):
# Strings & memory
grep -n '\.IndexOf(\"' FILE # Missing StringComparison
grep -n '\.Substring(' FILE # Substring allocations
grep -En '\.(StartsWith|EndsWith|Contains)\s*\(' FILE # Missing StringComparison
grep -n '\.ToLower()\|\.ToUpper()' FILE # Culture-sensitive + allocation
grep -n '\.Replace(' FILE # Chained Replace allocations
grep -n 'params ' FILE # params array allocation
# Collections & LINQ
grep -n '\.Select\|\.Where\|\.OrderBy\|\.GroupBy' FILE # LINQ on hot path
grep -n '\.All\|\.Any' FILE # LINQ on string/char
grep -n 'new Dictionary<\|new List<' FILE # Per-call allocation
grep -n 'static readonly Dictionary<' FILE # FrozenDictionary candidate
# Regex
grep -n 'RegexOptions.Compiled' FILE # Compiled regex budget
grep -n 'new Regex(' FILE # Per-call regex
grep -n 'GeneratedRegex' FILE # Positive: source-gen regex
# Structural
grep -n 'public class \|internal class ' FILE # Unsealed classes
grep -n 'sealed class' FILE # Already sealed
grep -n ': IEquatable' FILE # Positive: struct equality
Rules:
- Run every relevant recipe for the detected pattern categories
- Emit a scan execution checklist before classifying findings — list each recipe and the hit count
- A result of 0 hits is valid and valuable (confirms good practice)
- If reference files were loaded, also run their
## Detectionrecipes
Verify-the-Inverse Rule: For absence patterns, always count both sides and report the ratio (e.g., "N of M classes are sealed"). The ratio determines severity — 0/185 is systematic, 12/15 is a consistency fix.
Step 3b: 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 🟡 Moderate with the optimized file as evidence.
Step 3c: Compound Allocation Check
After running scan recipes, look for these multi-allocation patterns that single-line recipes miss:
- Branched
.Replace()chains: Methods that call.Replace()across multipleif/elsebranches — report total allocation count across all branches, not just per-line. - Cross-method chaining: When a public method delegates to another method that itself allocates intermediates (e.g., A calls B which does 3 regex replaces, then A calls C), report the total chain cost as one finding.
- Compound
+=with embedded allocating calls: Lines likeresult += $"...{Foo().ToLower()}"are 2+ allocations (interpolation + ToLower + concatenation) — flag the compound cost, not just the.ToLower(). string.Formatspecificity: Distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate the actionable sites.
Step 4: Classify and Prioritize Findings
Assign each finding a severity:
| Severity | Criteria | Action |
|---|---|---|
| 🔴 Critical | Deadlocks, crashes, security vulnerabilities, >10x regression | Must fix |
| 🟡 Moderate | 2-10x improvement opportunity, best practice for hot paths | Should fix on hot paths |
| ℹ️ Info | Pattern applies but code may not be on a hot path | Consider if profiling shows impact |
Prioritization rules:
- If the user identified hot-path code, elevate all findings in that code to their maximum severity
- If hot-path context is unknown, report 🔴 Critical findings unconditionally; report 🟡 Moderate findings with a note: "Impactful if this code is on a hot path"
- Never suggest micro-optimizations on code that is clearly not performance-sensitive
Scale-based severity escalation: When the same pattern appears across many instances, escalate severity:
- 1-10 instances of the same anti-pattern → report at the pattern's base severity
- 11-50 instances → escalate ℹ️ Info patterns to 🟡 Moderate
- 50+ instances → escalate to 🟡 Moderate with elevated priority; flag as a codebase-wide systematic issue
Always report exact counts (from scan recipes), not estimates or agent summaries.
Step 5: Generate Findings
Keep findings compact. Each finding is one short block — not an essay. Group by severity (🔴 → 🟡 → ℹ️), not by file.
Format per finding:
#### ID. Title (N instances)
**Impact:** one-line impact statement
**Files:** file1.cs:L1, file2.cs:L2, ... (list locations, don't build tables)
**Fix:** one-line description of the change (e.g., "Add `StringComparison.Ordinal` parameter")
**Caveat:** only if non-obvious (version requirement, correctness risk)
Rules for compact output:
- No ❌/✅ code blocks for trivial fixes (adding a keyword, parameter, or type change). A one-line fix description suffices.
- Only include code blocks for non-obvious transformations (e.g., replacing a LINQ chain with a foreach loop, or hoisting a closure).
- File locations as inline comma-separated list, not a table. Use
File.cs:L42format. - No explanatory prose beyond the Impact line — the severity icon already conveys urgency.
- Merge related findings that share the same fix (e.g., all
.ToLower()calls go in one finding, not split by file). - Positive findings in a bullet list, not a table. One line per pattern:
✅ Pattern — evidence.
End with a summary table and disclaimer:
| Severity | Count | Top Issue |
|----------|-------|-----------|
| 🔴 Critical | N | ... |
| 🟡 Moderate | N | ... |
| ℹ️ Info | N | ... |
> ⚠️ **Disclaimer:** These results are generated by an AI assistant and are non-deterministic. Findings may include false positives, miss real issues, or suggest changes that are incorrect for your specific context. Always verify recommendations with benchmarks and human review before applying changes to production code.
Validation
Before delivering results, verify:
- All critical patterns were checked (from reference files or inline recipes)
- Topic-specific recipes run only when matching signals detected
- Each finding includes a concrete code fix
- Scan execution checklist is complete (all recipes run)
- Summary table included at end
Common Pitfalls
| Pitfall | Correct Approach |
|---|---|
Flagging every Dictionary as needing FrozenDictionary |
Only flag if the dictionary is never mutated after construction |
Suggesting Span<T> in async methods |
Use Memory<T> in async code; Span<T> only in sync hot paths |
| Reporting LINQ outside hot paths | Only flag LINQ in identified hot paths or tight loops; LINQ is acceptable in code that runs infrequently. Since .NET 7, LINQ Min/Max/Sum/Average are vectorized — blanket bans on LINQ are misguided |
Suggesting ConfigureAwait(false) in app code |
Only applicable in library code; not primarily a performance concern |
Recommending ValueTask everywhere |
Only for hot paths with frequent synchronous completion |
Flagging new HttpClient() in DI services |
Check if IHttpClientFactory is already in use |
Suggesting [GeneratedRegex] for dynamic patterns |
Only flag when the pattern string is a compile-time literal |
Suggesting CollectionsMarshal.AsSpan broadly |
Only for ultra-hot paths with benchmarked evidence; adds complexity and fragility |
Suggesting unsafe code for micro-optimizations |
Avoid unsafe except where absolutely necessary — do not recommend it for micro-optimizations that don't matter. Safe alternatives like Span<T>, stackalloc in safe context, and ArrayPool cover the vast majority of performance needs |
Files (skills)
-
references
-
async-patterns.md 2.9 KB
# Async & Concurrency Patterns ### Don't Expose Async Wrappers for Sync Methods 🟡 **AVOID** wrapping sync methods with `Task.Run` in libraries | .NET Core+ ❌ ```csharp public Task<int> ComputeHashAsync(byte[] data) => Task.Run(() => ComputeHash(data)); ``` ✅ ```csharp public int ComputeHash(byte[] data) { /* CPU-bound work */ } // Consumer decides: var hash = await Task.Run(() => lib.ComputeHash(data)); ``` **Impact: Eliminates unnecessary thread pool queue/dequeue overhead per call.** ### Don't Expose Sync Wrappers for Async Methods 🟡 **AVOID** creating sync wrappers that block on async implementations | .NET Core+ ❌ ```csharp public string GetData() => GetDataAsync().Result; ``` ✅ ```csharp public async Task<string> GetDataAsync() { /* ... */ } ``` **Impact: Prevents deadlocks and thread pool starvation from hidden sync-over-async blocking.** ### Use ValueTask for Hot Paths with Frequent Sync Completion 🟡 **DO** use `ValueTask<T>` on hot paths where sync completion is common | .NET Core 2.1+ ❌ ```csharp public async Task<int> ReadAsync(Memory<byte> buffer) { if (_bufferedCount > 0) return ReadFromBuffer(buffer.Span); return await ReadAsyncCore(buffer); } ``` ✅ ```csharp public ValueTask<int> ReadAsync(Memory<byte> buffer) { if (_bufferedCount > 0) return new ValueTask<int>(ReadFromBuffer(buffer.Span)); return new ValueTask<int>(ReadAsyncCore(buffer)); } ``` **Impact: Eliminates Task\<T\> allocation on synchronous completion — the struct stores results inline.** ### Use Channels for Producer/Consumer 🟡 **DO** use `System.Threading.Channels` for producer-consumer patterns | .NET Core 3.0+ ❌ ```csharp var queue = new BlockingCollection<WorkItem>(); var item = queue.Take(); ``` ✅ ```csharp var channel = Channel.CreateUnbounded<WorkItem>(); // Producer await channel.Writer.WriteAsync(item); // Consumer await foreach (var item in channel.Reader.ReadAllAsync()) Process(item); ``` **Impact: ~25% faster, ~95% fewer GC collections vs manual approaches.** ### Avoid False Sharing with Thread-Local State 🟡 **AVOID** adjacent mutable fields written by different threads | .NET 7+ ❌ ```csharp class SharedCounters { public long Counter1; public long Counter2; } ``` ✅ ```csharp [StructLayout(LayoutKind.Explicit, Size = 128)] struct PaddedCounter { [FieldOffset(0)] public long Value; } ``` **Impact: Eliminates cross-core cache invalidation — can improve multi-threaded throughput by 10x+.** ## Detection Scan recipes for async anti-patterns. Run these and report exact counts. ```bash # async void methods (correctness issue — crashes on exception) grep -rn --include='*.cs' 'async void' --exclude-dir=bin --exclude-dir=obj . | grep -v 'event' | wc -l ``` ### Patterns Requiring Manual Review - **Sync-over-async** (`.Result`, `.Wait()`): `.Result` matches any property named Result — needs type context to confirm it's `Task.Result` -
collections-and-linq.md 7.6 KB
# Collections & LINQ Patterns ### Use FrozenDictionary/FrozenSet for Read-Heavy Lookup Tables 🟡 **DO** use `FrozenDictionary`/`FrozenSet` for collections created once and read many times | .NET 8+ ❌ ```csharp private static readonly Dictionary<string, int> s_statusCodes = new() { ["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500 }; ``` ✅ ```csharp private static readonly FrozenDictionary<string, int> s_statusCodes = new Dictionary<string, int> { ["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500 }.ToFrozenDictionary(); ``` **Impact: ~50% faster lookups than Dictionary, ~14x faster than ImmutableDictionary.** ### Use Dictionary Alternate Lookup for Span-Based Keys 🟡 **DO** use `GetAlternateLookup<ReadOnlySpan<char>>()` to avoid string allocation on lookups | .NET 9+ ❌ ```csharp string key = headerLine.Substring(0, colonIndex); if (s_dict.TryGetValue(key, out int value)) { /* ... */ } ``` ✅ ```csharp var lookup = s_dict.GetAlternateLookup<ReadOnlySpan<char>>(); ReadOnlySpan<char> key = headerLine.AsSpan(0, colonIndex); if (lookup.TryGetValue(key, out int value)) { } ``` **Impact: Avoids string allocation per lookup — especially valuable in parser/protocol hot paths.** ### Use CollectionsMarshal.GetValueRefOrNullRef for Lookup-and-Update 🟡 **DO** use `CollectionsMarshal.GetValueRefOrAddDefault` for dictionary update patterns | .NET 6+ ❌ ```csharp _counts.TryGetValue(key, out int count); _counts[key] = count + 1; ``` ✅ ```csharp ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(_counts, key, out _); count++; ``` **Impact: ~48% faster for lookup-and-update patterns (95µs → 49µs).** ### Use Collection Expressions [] for Zero-Allocation Span Creation 🟡 **DO** use collection expressions for `Span<T>` targets | C# 12 / .NET 8+ ❌ ```csharp int[] values = new int[] { a, b, c, d }; ``` ✅ ```csharp Span<int> values = [a, b, c, d]; ReadOnlySpan<int> daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; ``` **Impact: Zero heap allocation for span-targeted collection expressions.** ### Use EnsureCapacity on List/Stack/Queue Before Bulk Adds 🟡 **DO** call `EnsureCapacity` before bulk insertions | .NET 6+ ❌ ```csharp var list = new List<int>(); for (int i = 0; i < 10000; i++) list.Add(i); ``` ✅ ```csharp var list = new List<int>(); list.EnsureCapacity(10000); for (int i = 0; i < 10000; i++) list.Add(i); ``` **Impact: Reduces reallocations and array copies during bulk operations.** ### Use TryGetNonEnumeratedCount for Pre-Sizing 🟡 **DO** use `TryGetNonEnumeratedCount` to pre-size destination collections | .NET 6+ ❌ ```csharp var results = new List<int>(); foreach (var item in source) results.Add(Transform(item)); ``` ✅ ```csharp var results = source.TryGetNonEnumeratedCount(out int count) ? new List<int>(count) : new List<int>(); foreach (var item in source) results.Add(Transform(item)); ``` **Impact: Avoids O(n) enumeration for counting; eliminates resizing allocations.** ### Hoist Static Data Out of Method Bodies 🟡 **AVOID** creating collections with static/deterministic data inside method bodies | .NET Core+ ❌ ```csharp public string Convert(long number) { var groupsMap = new Dictionary<long, Func<long, string>> { { 1_000_000_000, n => $"{Convert(n)} billion" }, { 1_000_000, n => $"{Convert(n)} million" }, { 1_000, n => $"{Convert(n)} thousand" }, }; } ``` ✅ ```csharp private static readonly FrozenDictionary<long, Func<long, string>> s_groupsMap = new Dictionary<long, Func<long, string>> { { 1_000_000_000, n => $"{Convert(n)} billion" }, { 1_000_000, n => $"{Convert(n)} million" }, { 1_000, n => $"{Convert(n)} thousand" }, }.ToFrozenDictionary(); public string Convert(long number) { // ... use s_groupsMap } ``` **Impact: Eliminates collection + internal storage + closure allocations per call. For a Dictionary with N entries, saves ~N+3 allocations per invocation.** ### Add Overloads to Avoid params Array Allocation 🟡 **DO** add 1- and 2-argument overloads for methods that accept `params T[]`, or use `params ReadOnlySpan<T>` on .NET 9+ | .NET Core+ ❌ ```csharp public static string Transform(this string input, params IStringTransformer[] transformers) => transformers.Aggregate(input, (current, t) => t.Transform(current)); "hello".Transform(To.TitleCase); ``` ✅ ```csharp // Option A: Explicit overloads for common arities public static string Transform(this string input, IStringTransformer transformer) => transformer.Transform(input); public static string Transform(this string input, IStringTransformer t1, IStringTransformer t2) => t2.Transform(t1.Transform(input)); public static string Transform(this string input, params IStringTransformer[] transformers) => transformers.Aggregate(input, (current, t) => t.Transform(current)); // Option B (.NET 9+ / C# 13): params ReadOnlySpan<T> — zero heap allocation public static string Transform(this string input, params ReadOnlySpan<IStringTransformer> transformers) { foreach (var t in transformers) input = t.Transform(input); return input; } ``` **Impact: Eliminates one array allocation per call for the common 1- and 2-argument cases. `params ReadOnlySpan<T>` eliminates it for all arities.** ## Detection Scan recipes for collection and LINQ anti-patterns. Run these and report exact counts. ```bash # Static Dictionary not using FrozenDictionary (read-only after init) grep -rn --include='*.cs' 'static readonly Dictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l # Static FrozenDictionary (already optimized — verify the inverse) grep -rn --include='*.cs' 'static readonly FrozenDictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l # Per-call List allocation (inside method bodies, not static/readonly fields) grep -rn --include='*.cs' 'new List<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l # Per-call Dictionary allocation (inside method bodies, not static/readonly fields) grep -rn --include='*.cs' 'new Dictionary<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l # StringComparer.CurrentCulture usage (almost always wrong in library code — use Ordinal) grep -rn --include='*.cs' 'StringComparer.CurrentCulture' --exclude-dir=bin --exclude-dir=obj . | wc -l # LINQ chains in extension/hot-path files (.Select, .Where, .Cast, .Take, .Aggregate) grep -rn --include='*.cs' -E '\.(Select|Where|Cast|Take|Aggregate)\(' --exclude-dir=bin --exclude-dir=obj . | wc -l ``` For the LINQ chain recipe: any hit in a file whose name ends in `Extensions.cs`, `Formatter.cs`, or implements a method called from a public extension method is a hot-path candidate. Inspect each hit in these files and flag LINQ chains that allocate delegates, enumerators, or intermediate collections on every call. Hits in localization converters or one-time initialization are lower priority. ### Patterns Requiring Manual Review - **ContainsKey + indexer double-lookup**: Requires verifying the same key is used in a subsequent indexer access — multi-line/multi-statement context - **LINQ on hot paths**: The LINQ chain recipe above catches call sites, but distinguishing hot-path from cold-path requires context. Prioritize hits in `*Extensions.cs` and `*Formatter.cs` files, which are typically called on every user invocation - **`new Dictionary/List<` in method bodies vs fields**: The grep heuristic (`grep -v 'static\|readonly'`) catches most cases but may include false positives from field initializers without `static`/`readonly` — spot-check flagged lines -
critical-patterns.md 6.6 KB
# Critical .NET Performance Anti-Patterns 17 patterns that cause deadlocks, order-of-magnitude regressions, or excessive allocations. ## Async / Tasks ### Never Block on Async (Sync-over-Async) 🔴 **AVOID** | .NET Core+ ❌ ```csharp public string GetData() => GetDataAsync().Result; ``` ✅ ```csharp public async Task<string> GetDataAsync() => await GetDataInternalAsync(); ``` **Impact: Deadlocks or thread pool starvation; wastes threads, destroys scalability.** ### Never Await a ValueTask Multiple Times 🔴 **AVOID** | .NET Core 2.1+ ❌ ```csharp ValueTask<int> vt = SomeMethodAsync(); int a = await vt; int b = await vt; ``` ✅ ```csharp int result = await SomeMethodAsync(); ``` **Impact: Undefined behavior — silent data corruption or exceptions.** ## Memory / Allocation ### Use Span\<T\> / AsSpan Instead of Substring for Slicing 🔴 **DO** | .NET Core 2.1+ ❌ ```csharp string sub = input.Substring(5, 10); ``` ✅ ```csharp ReadOnlySpan<char> sub = input.AsSpan(5, 10); ``` **Impact: Eliminates per-slice allocations; 2-4x faster via vectorization.** ### Use ArrayPool\<T\> for Temporary Buffers 🔴 **DO** | .NET Core+ ❌ ```csharp byte[] buf = new byte[4096]; ``` ✅ ```csharp byte[] buf = ArrayPool<byte>.Shared.Rent(4096); Process(buf); ArrayPool<byte>.Shared.Return(buf); ``` **Impact: Dramatically reduces GC pressure for buffer-heavy workloads.** ### Avoid stackalloc in Loops 🔴 **AVOID** | .NET 5+ ❌ ```csharp for (int i = 0; i < 10_000; i++) Span<byte> buf = stackalloc byte[1024]; ``` ✅ ```csharp Span<byte> buf = stackalloc byte[1024]; for (int i = 0; i < 10_000; i++) { Process(buf); } ``` **Impact: StackOverflowException — unrecoverable, no catch possible.** ### Avoid Boxing Value Types 🔴 **AVOID** | .NET 6+ ❌ ```csharp string s = string.Format("{0}.{1}", major, minor); ``` ✅ ```csharp string s = $"{major}.{minor}"; ``` **Impact: When replacing `string.Format` with C# 10+ interpolation, typical improvements are ~40% faster with significantly less allocation. Actual gains vary by call site.** ## Strings ### Use StringComparison.Ordinal for Non-Linguistic Comparisons 🔴 **DO** | .NET Core+ ❌ ```csharp bool found = text.IndexOf("Content-Type") >= 0; ``` ✅ ```csharp bool found = text.Contains("Content-Type", StringComparison.Ordinal); ``` **Impact: 2-3x faster; OrdinalIgnoreCase hash codes ~3.3x faster.** ### Use AsSpan Instead of Substring 🔴 **DO** | .NET Core 2.1+ ❌ ```csharp int val = int.Parse(str.Substring(5, 3)); ``` ✅ ```csharp int val = int.Parse(str.AsSpan(5, 3)); ``` **Impact: Eliminates one string allocation per parse operation.** ## Regular Expressions ### Use Source-Generated Regex [GeneratedRegex] 🔴 **ALWAYS** use `[GeneratedRegex]` for all static regex patterns | .NET 7+ ❌ ```csharp private static readonly Regex s_re = new(@"\w+@\w+\.\w+", RegexOptions.Compiled); ``` ✅ ```csharp [GeneratedRegex(@"\w+@\w+\.\w+")] private static partial Regex EmailRegex(); ``` **Impact: Always beneficial or neutral for static patterns — near-zero startup, better throughput, and required for AOT/trimming scenarios.** ### Avoid Nested Quantifiers (Catastrophic Backtracking) 🔴 **AVOID** | .NET Core+ ❌ ```csharp var r = new Regex(@"^(\w+)+$"); ``` ✅ ```csharp var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking); ``` **Impact: Can hang process indefinitely on crafted input.** ### Use TryGetValue Instead of ContainsKey + Indexer 🔴 **DO** | .NET Core+ ❌ ```csharp if (dict.ContainsKey(key)) Use(dict[key]); ``` ✅ ```csharp if (dict.TryGetValue(key, out var value)) Use(value); ``` **Impact: ~2x faster (50% reduction in lookup time).** ### Avoid LINQ in Hot Paths 🔴 **AVOID** | .NET Core+ ❌ ```csharp bool found = items.Any(x => x.Name == target); ``` ✅ ```csharp bool found = false; foreach (var item in items) if (item.Name == target) { found = true; break; } ``` **Impact: Eliminates 1-3 allocations per call; measurable in tight loops.** ### Don't Iterate IEnumerable Multiple Times 🔴 **AVOID** | .NET Core+ ❌ ```csharp foreach (Type t in types) { Validate(t); } _types = types.ToArray(); ``` ✅ ```csharp Type[] arr = types.ToArray(); foreach (Type t in arr) { Validate(t); } _types = arr; ``` **Impact: Halves enumeration cost; prevents bugs from re-executing deferred queries.** ## JSON Serialization ### Use System.Text.Json Source Generator 🔴 **DO** | .NET 6+ ❌ ```csharp string json = JsonSerializer.Serialize(post); ``` ✅ ```csharp [JsonSerializable(typeof(BlogPost))] internal partial class AppJsonCtx : JsonSerializerContext { } string json = JsonSerializer.Serialize(post, AppJsonCtx.Default.BlogPost); ``` **Impact: 37-44% faster; enables trimming and Native AOT.** ### Cache JsonSerializerOptions 🔴 **DO** | .NET 5+ ❌ ```csharp JsonSerializer.Serialize(obj, new JsonSerializerOptions()); ``` ✅ ```csharp private static readonly JsonSerializerOptions s_opts = new(); JsonSerializer.Serialize(obj, s_opts); ``` **Impact: Up to 592x slower without caching (.NET 6); always cache or use defaults.** ## Networking ### Reuse HttpClient Instances 🔴 **DO** | .NET Core 2.1+ ❌ ```csharp using var client = new HttpClient(); await client.GetStringAsync(url); ``` ✅ ```csharp private static readonly HttpClient s_http = new(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(5) }); await s_http.GetStringAsync(url); ``` **Impact: Prevents socket exhaustion; 6-12x faster concurrent HTTPS.** ## General ### Use SearchValues\<T\> for Repeated Set Searches 🔴 **DO** | .NET 8+ ❌ ```csharp int pos = text.IndexOfAny("ABCDEF".ToCharArray()); ``` ✅ ```csharp private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF"); int pos = text.AsSpan().IndexOfAny(s_hex); ``` **Impact: 2-10x faster for chars; 10-30x faster for multi-string (.NET 9+).** ## Detection Scan recipes for critical anti-patterns. Run these and report exact counts of issues found in each case. ```bash # .IndexOf(string) without StringComparison (culture-aware, 2-3x slower) grep -rn --include='*.cs' -E '\.IndexOf\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l # .Substring( calls (allocates new string — consider AsSpan) grep -rn --include='*.cs' '\.Substring(' --exclude-dir=bin --exclude-dir=obj . | wc -l # .StartsWith/.EndsWith without StringComparison (culture-aware, 2-3x slower) grep -rn --include='*.cs' -E '\.(StartsWith|EndsWith)\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l # .Contains(string) without StringComparison — NOTE: will also match collection .Contains() calls; filter to string receivers grep -rn --include='*.cs' -E '\.Contains\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l ``` -
io-and-serialization.md 3.6 KB
# I/O, Serialization & General Patterns ### Use HttpCompletionOption.ResponseHeadersRead for Streaming 🟡 **DO** use `ResponseHeadersRead` when downloading large responses | .NET Core 3.0+ ❌ ```csharp var response = await client.GetAsync(uri); ``` ✅ ```csharp using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead); using var stream = await response.Content.ReadAsStreamAsync(); await stream.CopyToAsync(destinationStream); ``` **Impact: ~2x faster for large downloads (10MB+), dramatically reduced memory usage.** ### Use Async FileStream Operations 🟡 **DO** use `FileStream` with `useAsync: true` for scalable file I/O | .NET 6+ ❌ ```csharp using var fs = new FileStream(path, FileMode.Open); ``` ✅ ```csharp await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); byte[] buffer = new byte[1024]; while (await fs.ReadAsync(buffer) != 0) { /* process */ } ``` **Impact: Up to 3x faster async reads; allocation reduced from megabytes to hundreds of bytes.** ### Use Memory\<byte\> Overloads for Stream.ReadAsync/WriteAsync 🟡 **DO** use `Memory<byte>`-based stream overloads instead of `byte[]` overloads | .NET 5+ ❌ ```csharp await stream.ReadAsync(buffer, 0, buffer.Length); await stream.WriteAsync(buffer, 0, buffer.Length); ``` ✅ ```csharp await stream.ReadAsync(buffer.AsMemory()); await stream.WriteAsync(buffer.AsMemory()); ``` **Impact: Eliminates ~72 KB allocation per 1,000 read/write pairs on NetworkStream.** ### Use Span-Based TryFormat for Number Formatting 🟡 **DO** use `TryFormat` to format numbers into `Span<char>` buffers | .NET Core 2.1+ ❌ ```csharp string formatted = value.ToString(); destination.Write(formatted); ``` ✅ ```csharp Span<char> buffer = stackalloc char[20]; if (value.TryFormat(buffer, out int charsWritten)) destination.Write(buffer[..charsWritten]); ``` **Impact: Int32.ToString() ~2x faster in .NET Core 2.1, Int32 parsing ~5x faster in .NET Core 3.0.** ### Use static readonly for Runtime Devirtualization 🟡 **DO** store implementations in `static readonly` fields for JIT devirtualization | .NET Core 3.0+ ❌ ```csharp private static Base s_impl = new DerivedImpl(); s_impl.Process(); ``` ✅ ```csharp private static readonly Base s_impl = new DerivedImpl(); s_impl.Process(); private static readonly bool s_feature = Environment.GetEnvironmentVariable("Feature") == "1"; ``` **Impact: Virtual call eliminated entirely — can be inlined to zero overhead. Dead code elimination in tier 1.** ### Avoid Explicit Static Constructors — Use Field Initializers 🟡 **AVOID** explicit `static` constructors when field initializers suffice | .NET Core 3.0+ ❌ ```csharp class Foo { static readonly int s_value; static Foo() { s_value = ComputeValue(); } } ``` ✅ ```csharp class Foo { static readonly int s_value = ComputeValue(); } ``` **Impact: Enables better JIT optimization and reduces potential lock overhead on static method access.** ## Detection Scan recipes for I/O and serialization anti-patterns. Run these and report exact counts. ```bash # new HttpClient() (socket exhaustion risk) grep -rn --include='*.cs' 'new HttpClient(' --exclude-dir=bin --exclude-dir=obj . | wc -l # new JsonSerializerOptions() not cached (592x slower in .NET 6) grep -rn --include='*.cs' 'new JsonSerializerOptions' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l ``` ### Patterns Requiring Manual Review - **`JsonSerializer.Serialize/Deserialize` without source-gen context**: Can't determine from grep if a context parameter is passed -
memory-and-strings.md 5.7 KB
# Memory & String Patterns ### Use ReadOnlySpan\<byte\> for Constant Byte Data 🟡 **DO** assign constant byte arrays to `ReadOnlySpan<byte>` | .NET 5+ ❌ ```csharp byte[] data = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F }; ``` ✅ ```csharp ReadOnlySpan<byte> data = [0x48, 0x65, 0x6C, 0x6C, 0x6F]; ReadOnlySpan<int> primes = [2, 3, 5, 7, 11, 13]; ``` **Impact: ~100x faster access than static byte[] field, zero allocation.** ### Use stackalloc for Small Temporary Buffers 🟡 **DO** use `stackalloc` for small, fixed-size temporary buffers | .NET Core+ ❌ ```csharp char[] buffer = new char[64]; guid.TryFormat(buffer, out int written); ``` ✅ ```csharp Span<char> buffer = stackalloc char[64]; guid.TryFormat(buffer, out int written); ``` **Impact: Zero heap allocation, no GC pressure, instant alloc/dealloc.** ### Use Span.TryWrite for Allocation-Free Interpolation 🟡 **DO** use `MemoryExtensions.TryWrite` to format into `Span<char>` buffers | .NET 6+ ❌ ```csharp string formatted = $"Date: {dt:R}"; destination.Write(formatted); ``` ✅ ```csharp Span<char> buffer = stackalloc char[64]; buffer.TryWrite($"Date: {dt:R}", out int charsWritten); ``` **Impact: Zero heap allocation for formatting operations.** ### Use Span.Split() for Zero-Allocation Splitting 🟡 **DO** use `MemoryExtensions.Split` for allocation-free string splitting | .NET 9+ ❌ ```csharp string[] parts = input.Split(','); ``` ✅ ```csharp foreach (Range range in input.AsSpan().Split(',')) { ReadOnlySpan<char> segment = input.AsSpan(range); } ``` **Impact: 208 bytes → 0 bytes per split, 2x faster.** ### Use UTF8 String Literals (u8 suffix) 🟡 **DO** use the `u8` suffix for compile-time UTF8 `ReadOnlySpan<byte>` | .NET 7+ ❌ ```csharp byte[] header = Encoding.UTF8.GetBytes("Content-Type"); ``` ✅ ```csharp ReadOnlySpan<byte> header = "Content-Type"u8; ``` **Impact: 17ns → 0.006ns — eliminates runtime transcoding entirely.** ### Use ReadOnlySpan\<char\> Pattern Matching with switch 🟡 **DO** use `switch` on `ReadOnlySpan<char>` for allocation-free string matching | C# 11+ ❌ ```csharp switch (attr.Value.Trim()) { case "preserve": /* ... */ break; } ``` ✅ ```csharp switch (attr.Value.AsSpan().Trim()) { case "preserve": return Preserve; case "default": return Default; } ``` **Impact: Eliminates string allocation from Trim() in switch-based dispatch.** ### Use params ReadOnlySpan\<T\> to Eliminate Array Allocations 🟡 **DO** add `params ReadOnlySpan<T>` overloads to library methods | C# 13 / .NET 9+ ❌ ```csharp public static void Log(params string[] messages) { /* ... */ } Log("Starting", "Processing", "Done"); ``` ✅ ```csharp public static void Log(params ReadOnlySpan<string> messages) { /* ... */ } Log("Starting", "Processing", "Done"); ``` **Impact: Eliminates params array allocation. E.g., Path.Join with 5+ segments saves 64 bytes per call.** ### Avoid Chained String-Returning Operations 🟡 **AVOID** chains of 3+ string-returning method calls that each allocate intermediates | .NET Core+ **Pattern 1: Chained .Replace() calls** ❌ ```csharp string result = input.Replace("a", "b").Replace("c", "d").Replace("e", "f"); ``` ✅ ```csharp var sb = new StringBuilder(input.Length); // single pass replacing all patterns ``` **Pattern 2: Chained Regex.Replace() calls** ❌ ```csharp public static string Underscore(this string input) => Regex3.Replace(Regex2.Replace(Regex1.Replace(input, "$1_$2"), "$1_$2"), "_").ToLower(); ``` ✅ ```csharp return string.Create(totalLength, state, (span, s) => { /* write directly */ }); ``` **Pattern 3: += string concatenation in loops** ❌ ```csharp string result = ""; foreach (var part in parts) result += separator + part; ``` ✅ ```csharp var sb = new StringBuilder(); foreach (var part in parts) sb.Append(separator).Append(part); return sb.ToString(); ``` **Impact: Eliminates N-1 intermediate string allocations per chain. For `+=` in loops, eliminates O(n²) total allocation.** ### Cache char.ToString() for Known Character Sets 🟡 **DO** cache `char.ToString()` results when the set of characters is small and known | .NET Core+ ❌ ```csharp return symbol.ToString(); foreach (var prefix in UnitPrefixes) input = input.Replace(prefix.Value.Name, prefix.Key.ToString()); ``` ✅ ```csharp private static readonly FrozenDictionary<char, string> s_charStrings = new Dictionary<char, string> { ['k'] = "k", ['M'] = "M", ['G'] = "G", }.ToFrozenDictionary(); return s_charStrings[symbol]; ``` **Impact: Eliminates one string allocation per char.ToString() call. Significant when called in loops or on hot paths.** ## Detection Scan recipes for memory and string anti-patterns. Run these and report exact counts. ```bash # .ToLower()/.ToUpper() without culture parameter (allocates + culture-sensitive) grep -rn --include='*.cs' -E '\.(ToLower|ToUpper)\(\)' --exclude-dir=bin --exclude-dir=obj . | wc -l # Chained .Replace( calls (3+ on one line — intermediate string allocations) grep -rn --include='*.cs' '\.Replace(.*\.Replace(.*\.Replace(' --exclude-dir=bin --exclude-dir=obj . | wc -l # params in method signatures (array allocation per call) grep -rn --include='*.cs' 'params ' --exclude-dir=bin --exclude-dir=obj . | wc -l # LINQ on strings — .All/.Any on IEnumerable<char> (replace with foreach loop) grep -rn --include='*.cs' -E '\.(All|Any)\(char\.' --exclude-dir=bin --exclude-dir=obj . | wc -l ``` ### Patterns Requiring Manual Review - **Boxing via string.Format**: Can't determine argument types from grep — needs type analysis - **`+=` string concatenation in loops**: `+=` matches all types (int, list, event, string) — needs type context to confirm string - **`char.ToString()`**: Requires knowing the variable type is `char` — not reliably greppable -
regex-patterns.md 3.2 KB
# Regex Patterns ### Choose the Right Regex Engine Mode 🟡 **DO** use `[GeneratedRegex]` for all static regex patterns, but never remove `NonBacktracking` if present | .NET 7+ ❌ ```csharp var r = new Regex(dynamicPattern, RegexOptions.Compiled); ``` ✅ ```csharp [GeneratedRegex("pattern")] private static partial Regex MyRegex(); var safe = new Regex(untrustedPattern, RegexOptions.NonBacktracking); var oneOff = new Regex("pattern"); ``` **Impact: Source generator is always beneficial for static patterns. NonBacktracking prevents O(2^N) worst case — never remove it if present.** ### Use IsMatch When You Only Need a Boolean Result 🟡 **DO** use `IsMatch` instead of `Match(...).Success` | .NET 7+ ❌ ```csharp bool found = Regex.Match(input, pattern).Success; ``` ✅ ```csharp bool found = Regex.IsMatch(input, pattern); ``` **Impact: Avoids Match object allocation; with NonBacktracking, ~3x faster by skipping capture computation.** ### Use Regex.Count/EnumerateMatches Instead of Matches 🟡 **DO** use `Count()` and `EnumerateMatches()` for allocation-free match processing | .NET 7+ ❌ ```csharp int count = 0; Match m = regex.Match(text); while (m.Success) { count++; m = m.NextMatch(); } ``` ✅ ```csharp int count = regex.Count(text); foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b")) { ReadOnlySpan<char> word = text.AsSpan(m.Index, m.Length); } ``` **Impact: ~3x faster than Match/NextMatch with NonBacktracking. Zero allocations for both Count and EnumerateMatches.** ### Use Span-Based Regex APIs for Allocation-Free Matching 🟡 **DO** use `ReadOnlySpan<char>` overloads for regex matching on spans | .NET 7+ ❌ ```csharp string sub = largeBuffer.Substring(start, length); bool found = Regex.IsMatch(sub, pattern); ``` ✅ ```csharp ReadOnlySpan<char> text = largeBuffer.AsSpan(start, length); foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b")) { ReadOnlySpan<char> word = text.Slice(m.Index, m.Length); } ``` **Impact: Eliminates string allocations when working with spans — particularly valuable in high-throughput parsing pipelines.** ## Detection Scan recipes for regex anti-patterns. Run these and report exact counts. ```bash # Compiled regex count (startup cost budget — compare ratio to GeneratedRegex) grep -rn --include='*.cs' 'RegexOptions.Compiled' --exclude-dir=bin --exclude-dir=obj . | wc -l # GeneratedRegex count (already optimized — verify the inverse) grep -rn --include='*.cs' 'GeneratedRegex' --exclude-dir=bin --exclude-dir=obj . | wc -l # Uncached new Regex() calls (construction cost per call) grep -rn --include='*.cs' 'new Regex(' --exclude-dir=bin --exclude-dir=obj . | wc -l ``` When `RegexOptions.Compiled` appears inside a class constructor or field initializer of an instantiated class (not a static singleton), count how many instances of that class are created at startup to determine total compiled regex budget. For example, if a `Rule` class compiles a regex in its constructor and 122 rules are registered, that is 122 compiled regexes at startup. ### Patterns Requiring Manual Review - **`new Regex(` uncached**: Field assignment may span multiple lines — grep on one line is unreliable. Verify that matched instances are stored in `static readonly` fields or `[GeneratedRegex]`. -
structural-patterns.md 1.6 KB
# Structural Patterns Patterns detected by the **absence** of a keyword or interface. These require codebase-wide counting scans, not single-file matching. ### Seal Classes for Devirtualization 🟡 **DO** seal all leaf classes (those not subclassed) | .NET Core 3.0+ Sealing lets the JIT devirtualize/inline virtual calls and use pointer comparison for type checks. Every non-abstract, non-static class that is not subclassed should be sealed. **Detection:** This is an absence pattern — scan for classes that are NOT sealed. ```bash # Count unsealed (non-abstract, non-static) classes grep -rn --include='*.cs' -E '^\s*((public|internal|private|protected|file)\s+)?(partial\s+)?class ' --exclude-dir=bin --exclude-dir=obj . | grep -v 'sealed' | grep -v 'abstract' | grep -v 'static' | wc -l # Count already-sealed classes (verify the inverse) grep -rn --include='*.cs' 'sealed class' --exclude-dir=bin --exclude-dir=obj . | wc -l ``` **Exclusions:** Do not seal classes that are subclassed elsewhere in the codebase. Identifying base classes requires manual review — grep for `: ClassName` patterns and cross-reference, but expect false positives from interface implementations and generic constraints. ❌ ```csharp internal class MyHandler : Base { public override int Run() => 42; } ``` ✅ ```csharp internal sealed class MyHandler : Base { public override int Run() => 42; } ``` **Impact: Virtual calls up to 500x faster; type checks ~25x faster. Severity scales with count.** **Scale-based severity:** - 1-10 unsealed leaf classes → ℹ️ Info - 11-50 unsealed leaf classes → 🟡 Moderate - 50+ unsealed leaf classes → 🟡 Moderate (elevated priority)
-
-
SKILL.md 12.4 KB
--- name: analyzing-dotnet-performance description: >- Scans .NET code for ~50 performance anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I/O with tiered severity classification. Use when analyzing .NET code for optimization opportunities, reviewing hot paths, or auditing allocation-heavy patterns. license: MIT --- # .NET Performance Patterns Scan C#/.NET code for performance anti-patterns and produce prioritized findings with concrete fixes. Patterns sourced from the official .NET performance blog series, distilled to customer-actionable guidance. ## When to Use - Reviewing C#/.NET code for performance optimization opportunities - Auditing hot paths for allocation-heavy or inefficient patterns - Systematic scan of a codebase for known anti-patterns before release - Second-opinion analysis after manual performance review ## When Not to Use - **Algorithmic complexity analysis** — this skill targets API usage patterns, not algorithm design - **Code not on a hot path** with no performance requirements — avoid premature optimization ## Inputs | Input | Required | Description | |-------|----------|-------------| | Source code | Yes | C# files, code blocks, or repository paths to scan | | Hot-path context | Recommended | Which code paths are performance-critical | | Target framework | Recommended | .NET version (some patterns require .NET 8+) | | Scan depth | Optional | `critical-only`, `standard` (default), or `comprehensive` | ## Workflow ### Step 1: Load Critical Reference Resolve bundled paths from the directory that contains this `SKILL.md`, not from the user's workspace. Load this reference file first: - `references/critical-patterns.md` If a direct read fails, list this skill's `references/` directory once and retry only when the listing shows the expected file. Do not use workspace file or text search to locate the skill installation. ### Step 2: Detect Code Signals and Select Topic Recipes Scan the code for signals that indicate which pattern categories to check. Use the `## Detection` section from the critical reference when available and the inline recipes in Step 3 for initial signal detection. After detecting signals, load only the topic-specific references selected by scan depth: - `critical-only`: No additional references (use only `critical-patterns.md`) - `standard` (default): Load references matching detected signals from this list: - `references/async-patterns.md` — async/Task/ValueTask signals - `references/memory-and-strings.md` — Span/Memory/string allocation signals - `references/regex-patterns.md` — Regex signals - `references/collections-and-linq.md` — Dictionary/List/LINQ signals - `references/io-and-serialization.md` — JsonSerializer/HttpClient/Stream signals - `references/structural-patterns.md` — always loaded (unsealed classes checked regardless) - `comprehensive`: Load all six topic-specific references above For coverage reporting, the selected references are `references/critical-patterns.md` plus only the topic-specific references selected above. If any selected reference remains unavailable after retry, use the inline recipes in Step 3 for the missing coverage. Include `Reference coverage: reduced; unavailable: <paths>; used inline recipes for missing references.` in the final report, with `<paths>` replaced by the missing relative paths of the selected references only. Use the `## Detection` sections from loaded reference files and the inline recipes in Step 3 for categories whose reference files are unavailable. | Signal in Code | Topic | |----------------|-------| | `async`, `await`, `Task`, `ValueTask` | Async patterns | | `Span<`, `Memory<`, `stackalloc`, `ArrayPool`, `string.Substring`, `.Replace(`, `.ToLower()`, `+=` in loops, `params` | Memory & strings | | `Regex`, `[GeneratedRegex]`, `Regex.Match`, `RegexOptions.Compiled` | Regex patterns | | `Dictionary<`, `List<`, `.ToList()`, `.Where(`, `.Select(`, LINQ methods, `static readonly Dictionary<` | Collections & LINQ | | `JsonSerializer`, `HttpClient`, `Stream`, `FileStream` | I/O & serialization | Always check structural patterns (unsealed classes) regardless of signals. **Scan depth controls scope:** - `critical-only`: Only critical patterns (deadlocks, >10x regressions) - `standard` (default): Critical + detected topic patterns - `comprehensive`: All pattern categories ### Step 3: Scan and Report **For files under 500 lines, read the entire file first** — you'll spot most patterns faster than running individual grep recipes. Use grep to confirm counts and catch patterns you might miss visually. For each relevant pattern category, run the detection recipes below. Report exact counts, not estimates. **Core scan recipes** (run these when reference files aren't available): ``` # Strings & memory grep -n '\.IndexOf(\"' FILE # Missing StringComparison grep -n '\.Substring(' FILE # Substring allocations grep -En '\.(StartsWith|EndsWith|Contains)\s*\(' FILE # Missing StringComparison grep -n '\.ToLower()\|\.ToUpper()' FILE # Culture-sensitive + allocation grep -n '\.Replace(' FILE # Chained Replace allocations grep -n 'params ' FILE # params array allocation # Collections & LINQ grep -n '\.Select\|\.Where\|\.OrderBy\|\.GroupBy' FILE # LINQ on hot path grep -n '\.All\|\.Any' FILE # LINQ on string/char grep -n 'new Dictionary<\|new List<' FILE # Per-call allocation grep -n 'static readonly Dictionary<' FILE # FrozenDictionary candidate # Regex grep -n 'RegexOptions.Compiled' FILE # Compiled regex budget grep -n 'new Regex(' FILE # Per-call regex grep -n 'GeneratedRegex' FILE # Positive: source-gen regex # Structural grep -n 'public class \|internal class ' FILE # Unsealed classes grep -n 'sealed class' FILE # Already sealed grep -n ': IEquatable' FILE # Positive: struct equality ``` **Rules:** - Run every relevant recipe for the detected pattern categories - **Emit a scan execution checklist** before classifying findings — list each recipe and the hit count - A result of **0 hits** is valid and valuable (confirms good practice) - If reference files were loaded, also run their `## Detection` recipes **Verify-the-Inverse Rule:** For absence patterns, always count both sides and report the ratio (e.g., "N of M classes are sealed"). The ratio determines severity — 0/185 is systematic, 12/15 is a consistency fix. ### Step 3b: 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 🟡 Moderate with the optimized file as evidence. ### Step 3c: Compound Allocation Check After running scan recipes, look for these multi-allocation patterns that single-line recipes miss: 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:** When a public method delegates to another method that itself allocates intermediates (e.g., A calls B which does 3 regex replaces, then A calls C), report the total chain cost as one finding. 3. **Compound `+=` with embedded allocating calls:** Lines like `result += $"...{Foo().ToLower()}"` are 2+ allocations (interpolation + ToLower + concatenation) — flag the compound cost, not just the `.ToLower()`. 4. **`string.Format` specificity:** Distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate the actionable sites. ### Step 4: Classify and Prioritize Findings Assign each finding a severity: | Severity | Criteria | Action | |----------|----------|--------| | 🔴 **Critical** | Deadlocks, crashes, security vulnerabilities, >10x regression | Must fix | | 🟡 **Moderate** | 2-10x improvement opportunity, best practice for hot paths | Should fix on hot paths | | ℹ️ **Info** | Pattern applies but code may not be on a hot path | Consider if profiling shows impact | **Prioritization rules:** 1. If the user identified hot-path code, elevate all findings in that code to their maximum severity 2. If hot-path context is unknown, report 🔴 Critical findings unconditionally; report 🟡 Moderate findings with a note: _"Impactful if this code is on a hot path"_ 3. Never suggest micro-optimizations on code that is clearly not performance-sensitive **Scale-based severity escalation:** When the same pattern appears across many instances, escalate severity: - 1-10 instances of the same anti-pattern → report at the pattern's base severity - 11-50 instances → escalate ℹ️ Info patterns to 🟡 Moderate - 50+ instances → escalate to 🟡 Moderate with elevated priority; flag as a codebase-wide systematic issue Always report exact counts (from scan recipes), not estimates or agent summaries. ### Step 5: Generate Findings **Keep findings compact.** Each finding is one short block — not an essay. Group by severity (🔴 → 🟡 → ℹ️), not by file. Format per finding: ``` #### ID. Title (N instances) **Impact:** one-line impact statement **Files:** file1.cs:L1, file2.cs:L2, ... (list locations, don't build tables) **Fix:** one-line description of the change (e.g., "Add `StringComparison.Ordinal` parameter") **Caveat:** only if non-obvious (version requirement, correctness risk) ``` **Rules for compact output:** - **No ❌/✅ code blocks** for trivial fixes (adding a keyword, parameter, or type change). A one-line fix description suffices. - **Only include code blocks** for non-obvious transformations (e.g., replacing a LINQ chain with a foreach loop, or hoisting a closure). - **File locations as inline comma-separated list**, not a table. Use `File.cs:L42` format. - **No explanatory prose** beyond the Impact line — the severity icon already conveys urgency. - **Merge related findings** that share the same fix (e.g., all `.ToLower()` calls go in one finding, not split by file). - **Positive findings** in a bullet list, not a table. One line per pattern: `✅ Pattern — evidence`. End with a summary table and disclaimer: ```markdown | Severity | Count | Top Issue | |----------|-------|-----------| | 🔴 Critical | N | ... | | 🟡 Moderate | N | ... | | ℹ️ Info | N | ... | > ⚠️ **Disclaimer:** These results are generated by an AI assistant and are non-deterministic. Findings may include false positives, miss real issues, or suggest changes that are incorrect for your specific context. Always verify recommendations with benchmarks and human review before applying changes to production code. ``` ## Validation Before delivering results, verify: - [ ] All critical patterns were checked (from reference files or inline recipes) - [ ] Topic-specific recipes run only when matching signals detected - [ ] Each finding includes a concrete code fix - [ ] Scan execution checklist is complete (all recipes run) - [ ] Summary table included at end ## Common Pitfalls | Pitfall | Correct Approach | |---------|-----------------| | Flagging every `Dictionary` as needing `FrozenDictionary` | Only flag if the dictionary is never mutated after construction | | Suggesting `Span<T>` in async methods | Use `Memory<T>` in async code; `Span<T>` only in sync hot paths | | Reporting LINQ outside hot paths | Only flag LINQ in identified hot paths or tight loops; LINQ is acceptable in code that runs infrequently. Since .NET 7, LINQ Min/Max/Sum/Average are vectorized — blanket bans on LINQ are misguided | | Suggesting `ConfigureAwait(false)` in app code | Only applicable in library code; not primarily a performance concern | | Recommending `ValueTask` everywhere | Only for hot paths with frequent synchronous completion | | Flagging `new HttpClient()` in DI services | Check if `IHttpClientFactory` is already in use | | Suggesting `[GeneratedRegex]` for dynamic patterns | Only flag when the pattern string is a compile-time literal | | Suggesting `CollectionsMarshal.AsSpan` broadly | Only for ultra-hot paths with benchmarked evidence; adds complexity and fragility | | Suggesting `unsafe` code for micro-optimizations | Avoid `unsafe` except where absolutely necessary — do not recommend it for micro-optimizations that don't matter. Safe alternatives like `Span<T>`, `stackalloc` in safe context, and `ArrayPool` cover the vast majority of performance needs |
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.