--- name: perf-optimization description: > Profile-driven performance optimization protocol. Use when profiling data (CPU, heap, trace) is available or when the user requests performance analysis. Covers methodology, pattern catalog, safety invariants, and when-to-stop heuristics. Language-specific tooling is in languages/*.md. --- # Performance Optimization Skill ## When to Use - User provides profiling data (pprof, flamegraph, py-spy, Chrome DevTools, Dart DevTools) - User asks to analyze or optimize performance of a specific component - A benchmark regression is detected - After deploying a new feature that touches a hot path ## Core Methodology ```mermaid graph LR P1[Profile] --> P2[Analyze] P2 --> P3[Prioritize] P3 --> P4[Optimize] P4 --> P5[Benchmark] P5 --> P6{Improvement?} P6 -->|Yes| P7[Verify & Ship] P6 -->|No| P3 ``` ### Step 1: Profile Collect profiling data using the language-appropriate tool. Load the relevant `languages/*.md` module for exact commands. **Output:** Raw profiling data (CPU profile, heap profile, or trace). ### Step 2: Analyze Read the profile. Focus on these principles (universal across all runtimes): 1. **Focus on `cum` (cumulative):** The total resources consumed by a function AND everything it called. This finds the expensive architectural flows. 2. **Contextualize `flat`:** Resources consumed by the function itself only. If a runtime function (GC, malloc, syscall) has high flat time, trace it UP the call chain to find the user-land code that triggered it. 3. **Ignore runtime noise:** Scheduler overhead (`runtime.mcall`, `runtime.systemstack`, GC workers) will always appear. Note if GC pressure is high, but don't try to "fix" the scheduler. 4. **Separate benchmark artifacts from production cost:** Test harness allocations (e.g., `httptest.NewRequest`, `ResponseRecorder`) inflate heap profiles but don't exist in production. **Output:** Structured analysis document in `docs/research_logs/{component}-perf-analysis.md`. ### Step 3: Prioritize Rank fixes by **impact/risk ratio**: | Priority | Criteria | |---|---| | Do first | Low risk, high impact (caching, pre-allocation, fast-reject) | | Do second | Medium risk, high impact (library swap, algorithm change) | | Do last | High risk, high impact (major refactor, custom implementation) | | Skip | Any risk, low impact (micro-optimization below noise floor) | **Rule:** If a fix requires more than 1 day AND saves < 20% on the hot path, defer it. ### Step 4: Optimize Implement one fix at a time. For each fix: 1. Write tests FIRST (TDD — Red → Green → Refactor) 2. Implement the fix 3. Run all existing tests to verify no regression 4. Benchmark immediately **Never batch multiple optimizations into one commit.** Each fix must be independently verifiable and revertable. ### Step 5: Benchmark Compare before/after with the **exact same benchmark configuration** (same `-benchtime`, same `-count`, same machine load). Report: - `ns/op` (latency) - `B/op` (memory per operation) - `allocs/op` (heap allocations per operation) ### Step 6: When to Stop **Stop optimizing when any of these are true:** - Remaining CPU is in hardware-optimized assembly (AES-NI, P-256, SIMD) — you cannot beat the hardware - Remaining allocations are from the language runtime itself (GC, goroutine stacks, HTTP server internals) - The fix requires a custom implementation of a well-audited library — the security/maintenance risk outweighs the perf gain - The measured improvement is < 5% and within benchmark noise --- ## Optimization Pattern Catalog These are generic, language-agnostic patterns. Apply them when the profiling data shows the corresponding symptom. ### Pattern: Result Caching **Symptom:** Same expensive computation repeated with identical inputs (crypto verification, JSON parsing, regex compilation). **Fix:** Cache results keyed by input hash. Use bounded LRU with TTL to prevent memory exhaustion. **Safety invariant:** When caching security-sensitive results (auth tokens, permission checks): - ALWAYS re-validate expiry/revocation on cache hit - ALWAYS bound cache size (DoS protection) - ALWAYS set TTL shorter than the security credential's validity period ### Pattern: Pre-allocation **Symptom:** High `allocs/op` from repeatedly constructing the same objects (option structs, config slices, header maps). **Fix:** Build the object once at init time, share it read-only across requests. Safe for concurrent use if the object is immutable after construction. ### Pattern: Fast-Reject / Short-Circuit **Symptom:** Expensive validation path runs even for clearly invalid inputs. **Fix:** Add a cheap structural pre-check before the expensive path. Examples: check string length before regex, count delimiters before parsing, check content-type before deserialization. ### Pattern: Library Swap **Symptom:** High allocation count or CPU in a third-party library's internal parsing/serialization. **Fix:** Replace with a library that uses lower-allocation strategies (manual scanners vs `encoding/json.Decoder`, zero-copy parsing, arena allocation). **Safety invariant:** When swapping security-critical libraries (JWT, TLS, crypto): - Explicitly restrict accepted algorithms (prevent algorithm confusion attacks) - Verify the replacement library is well-audited and actively maintained - Run the full existing test suite — no behavioral change allowed ### Pattern: Pooling **Symptom:** High GC pressure from many short-lived objects of the same type being allocated and discarded rapidly. **Fix:** Use an object pool (sync.Pool in Go, object pool in Java, arena in Rust) to reuse allocations. **Caveat:** Only effective when objects are uniform in size and have a clear acquire/release lifecycle. Misuse creates subtle bugs. ### Pattern: Batching **Symptom:** Many small I/O operations (DB queries, HTTP calls, file writes) dominating wall-clock time. **Fix:** Batch operations into fewer, larger calls. Examples: batch INSERT, pipeline Redis commands, buffer writes. ### Pattern: Artifact Partitioning by Change Frequency **Symptom:** Deploying a small change invalidates a large cached artifact (JS bundle, Docker image, compiled binary), forcing consumers to re-download/rebuild the entire thing. **Fix:** Partition build artifacts by change frequency so that stable layers survive volatile deploys: - **Stable layer**: dependencies, vendor libraries, base images — changes rarely - **Volatile layer**: application code — changes on every deploy **Examples across stacks:** - **JS/Bundler**: Vite `manualChunks` / Webpack `splitChunks` to isolate vendor libraries into separate chunks - **Docker**: multi-stage builds with `COPY go.mod` + `RUN go mod download` BEFORE `COPY . .` — dependency layer caches across builds - **Monorepo**: separate packages by change frequency so CI only rebuilds what changed **Safety invariant:** Total artifact size stays the same or slightly increases (chunk overhead). The benefit is on **repeat consumption** — stable layers serve from cache. **When NOT to apply:** One-shot artifacts with no caching benefit (single-use CI, ephemeral environments). ### Pattern: Dependency Discovery Parallelization **Symptom:** Sequential resource discovery creates waterfalls — each resource is discovered only after the previous one completes (download → parse → discover next → download → ...). **Fix:** Declare dependencies as early as possible so the system can fetch them in parallel: - Move resource declarations upstream (earlier in the boot/parse sequence) - Use explicit hints to bypass sequential discovery chains **Examples across stacks:** - **Browser**: `` to establish connections before CSS/JS requests them; move CSS `@import` to HTML `` for parallel discovery - **Go**: `go mod download` before build to prefetch modules - **DB**: connection pool warm-up at startup instead of on first query - **DNS**: `dns-prefetch` hints for domains the app will contact **Safety invariant:** Only pre-declare resources you WILL use. Unused preconnects/prefetches waste resources (TCP connections, DNS queries, module downloads). ### Pattern: Concurrent-Fetch Dedup **Symptom:** Network tab shows two identical API calls fired at the same time. Multiple UI components mount simultaneously and each independently calls the same fetch function. **Fix:** Add a loading-state guard (semaphore) at the store/service layer: ``` async function fetchData() { if (isLoading) return // ← drop duplicate in-flight request isLoading = true try { data = await api.getData() } finally { isLoading = false } } ``` **When to apply:** When the same data store is used by multiple co-mounted components (e.g., a navigation bar and a page view both calling `fetchProfile()` on mount). **Caveat:** This is a simple semaphore, not request dedup. If the data needs refreshing after the in-flight call completes, the caller should retry. For advanced use cases, consider a proper request dedup cache (e.g., TanStack Query's `staleTime`). --- ## Anti-Patterns (Things NOT to Do) 1. **Don't optimize runtime internals.** If `runtime.mallocgc` or `runtime.gcBgMarkWorker` is high, fix the USER CODE that triggers allocations — don't try to tune the GC directly. 2. **Don't replace battle-tested crypto with custom implementations.** The performance ceiling of ECDSA/RSA is in the math. Accept it. 3. **Don't optimize based on gut feeling.** Always profile first. Premature optimization is the root of all evil. 4. **Don't combine multiple optimizations into one commit.** If a combined commit causes a regression, you can't isolate which fix is at fault. 5. **Don't disable security features for performance.** Algorithm restriction, input validation, and expiry checks are non-negotiable. 6. **Don't profile without a stable baseline.** Run benchmarks with fixed parameters (`-benchtime`, `-count`, same machine load). Without a reproducible baseline, before/after comparisons are meaningless noise. --- ## Language Modules Load the relevant language module when working with a specific runtime: | Module | Use when | |---|---| | [Go](languages/go.md) | Go services, APIs, CLI tools | | [Rust](languages/rust.md) | Rust binaries, libraries | | [Python](languages/python.md) | Python services, CLI, data pipelines | | [Frontend](languages/frontend.md) | Web frontends (JS/TS bundle, rendering, network) | > **Contributing:** After completing a perf optimization session, extract generalizable patterns > from your `docs/research_logs/` findings into this catalog. Project-specific details stay in > the research log; reusable patterns belong here. ## Profiling Scripts Language-specific data extraction scripts live in `scripts/`: | Script | Purpose | |---|---| | [go-pprof.sh](scripts/go-pprof.sh) | Extract Go pprof CPU/heap profiles into agent-readable markdown | | [frontend-lighthouse.sh](scripts/frontend-lighthouse.sh) | Two modes: `lighthouse` (Core Web Vitals, needs Chrome) or `bundle` (Vite chunk analysis, always works) |