[autoresearch] baseline: system prompt quality tests (5/5 passing)

This commit is contained in:
evandrodevbr committed 2026-04-11 05:27:36 -03:00
1 parent 05cc036f6d
commit 60b0101717
142 files changed
+18589 -9947

No files matched your search

+227
View File
@@ -0,0 +1,227 @@
---
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**: `<link rel="preconnect">` to establish connections before CSS/JS requests them; move CSS `@import` to HTML `<link>` 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) |
@@ -0,0 +1,347 @@
# Frontend Performance Profiling — Vue 3 + Vite + PWA
## Stack Context
| Layer | Tool | Version |
|---|---|---|
| Framework | Vue 3 (SFC + Composition API) | ^3.5 |
| State | Pinia | ^3.0 |
| Router | Vue Router | ^5.0 |
| Bundler | Vite | ^7.3 |
| CSS | Tailwind CSS (Vite plugin) | ^4.2 |
| PWA | vite-plugin-pwa (Workbox) | ^1.2 |
| Backend client | Supabase JS | ^2.99 |
| Build | vue-tsc + vite build | — |
| Tests | Vitest + jsdom | ^4.1 |
---
## Profiling Toolchain
### Build-Time Analysis
| Tool | Purpose | Command |
|---|---|---|
| Vite bundle visualizer | Chunk sizes, tree-shaking effectiveness | `npx vite-bundle-visualizer` (generates `stats.html`) |
| `vite build --report` | Build timing and chunk summary | `npx vite build 2>&1` |
| `source-map-explorer` | Trace bytes to source files | `npx source-map-explorer dist/assets/*.js` |
| `bundlephobia` | Check dependency size before adding | `npx bundlephobia <package-name>` |
**Quick bundle size check (no extra dependency needed):**
```bash
# Build and report chunk sizes
cd apps/frontend && npx vite build 2>&1 | grep -E "(dist/|\.js|\.css)" | sort -t'│' -k2 -rn
```
**Detailed analysis with rollup-plugin-visualizer:**
```bash
# Install temporarily and run
cd apps/frontend && npx vite-bundle-visualizer
# Opens stats.html in browser — shows treemap of every module
```
### Runtime Analysis (Chrome DevTools)
| Tab | What it measures | When to use |
|---|---|---|
| **Performance** | Frame rate, scripting, rendering, painting | Janky animations, slow interactions |
| **Lighthouse** | LCP, FID/INP, CLS, TTI, bundle size | Overall page health score |
| **Network** | Waterfall, asset sizes, cache status | Slow loads, unnecessary requests |
| **Memory** | Heap snapshots, allocation timeline | Memory leaks, growing detached DOM |
| **Coverage** | Used vs unused JS/CSS bytes | Dead code, over-imported libraries |
### Core Web Vitals
| Metric | Target | What it measures |
|---|---|---|
| **LCP** (Largest Contentful Paint) | < 2.5s | How fast the main content appears |
| **INP** (Interaction to Next Paint) | < 200ms | How fast interactions respond |
| **CLS** (Cumulative Layout Shift) | < 0.1 | How much the layout shifts during load |
| **TTFB** (Time to First Byte) | < 800ms | Server response time |
| **FCP** (First Contentful Paint) | < 1.8s | How fast anything renders |
### Lighthouse CLI (Headless — for Scripted Analysis)
```bash
# Run Lighthouse from CLI (no browser GUI needed)
npx lighthouse http://localhost:4173 \
--output=json \
--output-path=./lighthouse-report.json \
--chrome-flags="--headless --no-sandbox" \
--only-categories=performance \
--preset=desktop
# For mobile (default):
npx lighthouse http://localhost:4173 \
--output=json \
--output-path=./lighthouse-report.json \
--chrome-flags="--headless --no-sandbox" \
--only-categories=performance
```
**Agent-readable extraction:**
```bash
# Extract key metrics from Lighthouse JSON
cat lighthouse-report.json | node -e "
const r = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const a = r.audits;
console.log('| Metric | Score | Value |');
console.log('|---|---|---|');
['first-contentful-paint','largest-contentful-paint','total-blocking-time',
'cumulative-layout-shift','speed-index','interactive'].forEach(k => {
if(a[k]) console.log('| ' + a[k].title + ' | ' + (a[k].score*100) + ' | ' + a[k].displayValue + ' |');
});
"
```
---
## Vue/Vite-Specific Optimization Patterns
### Pattern: Vendor Chunk Splitting (Vite)
**Symptom:** Every deploy invalidates the entire JS bundle cache, including stable vendor libs (Vue, Supabase JS). See agnostic principle: *Artifact Partitioning by Change Frequency* in SKILL.md.
**Fix:** Add `manualChunks` to `vite.config.ts`:
```typescript
build: {
rollupOptions: {
output: {
manualChunks: {
'vendor-vue': ['vue', 'vue-router', 'pinia'],
'vendor-supabase': ['@supabase/supabase-js'],
},
},
},
},
```
**Group by change frequency:**
- `vendor-vue`: framework runtime — changes only on major upgrades
- `vendor-supabase`: SDK/client — changes infrequently
- App code (auto): changes every deploy
**Verification:** After `vite build`, check `dist/assets/` for `vendor-vue-*.js` and `vendor-supabase-*.js` chunks.
### Pattern: Resource Hint Pipeline (HTML)
**Symptom:** Fonts load late because browser discovers them only after parsing CSS. See agnostic principle: *Dependency Discovery Parallelization* in SKILL.md.
**Fix:** Replace CSS `@import` with HTML `<link>` tags and add `preconnect` hints:
```html
<!-- In index.html <head> — BEFORE the CSS link -->
<link rel="preconnect" href="https://fonts.googleapis.com" crossorigin>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=...&display=swap">
```
Then remove the `@import url(...)` from your CSS file.
**Safety:** Only `preconnect` to domains used on every page load. For runtime-determined domains (e.g., Supabase URL from env vars), skip the hint — hardcoding would break across environments.
### Pattern: Route-Level Code Splitting
**Symptom:** Lighthouse reports a large initial JS bundle with low Coverage score. All routes are in a single chunk.
**Fix:** Use dynamic imports for route components in Vue Router:
```typescript
// ❌ Eager import — everything in one chunk
import SiraatView from '@/views/SiraatView.vue'
// ✅ Lazy route — separate chunk, loaded on navigation
const SiraatView = () => import('@/views/SiraatView.vue')
```
**Verification:** After build, check `dist/assets/` — each lazy route should produce a separate `.js` chunk.
### Pattern: Component Lazy Loading
**Symptom:** Heavy components (charts, editors, modals) are loaded even when not visible.
**Fix:** Use `defineAsyncComponent` for below-the-fold or conditionally-rendered components:
```typescript
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent(() =>
import('@/features/analytics/HeavyChart.vue')
)
```
### Pattern: Pinia Store Hydration
**Symptom:** Multiple API calls for the same data on page load. Network waterfall shows redundant fetches.
**Fix:** Deduplicate fetches in Pinia stores with loading-state guards:
```typescript
const store = defineStore('lesson', () => {
const data = ref<Lesson | null>(null)
const loading = ref(false)
async function fetch(id: string) {
if (loading.value) return // ← prevent double-fetch
if (data.value?.id === id) return // ← already have it
loading.value = true
try {
data.value = await api.getLesson(id)
} finally {
loading.value = false
}
}
return { data, loading, fetch }
})
```
### Pattern: Image Optimization
**Symptom:** Lighthouse flags large images (LCP degradation). Network shows uncompressed PNGs.
**Fix checklist:**
1. Use `<img loading="lazy">` for below-the-fold images
2. Use WebP/AVIF formats (Vite handles this with plugins)
3. Specify explicit `width` and `height` attributes (prevents CLS)
4. Use `<picture>` with `srcset` for responsive images
### Pattern: Watchers / Computed Overuse
**Symptom:** DevTools Performance tab shows repeated "Scripting" blocks during reactivity updates.
**Fix:**
- Prefer `computed` over `watch` — computed values are lazy and cached
- Use `watchEffect` only when side effects are truly needed
- Use `shallowRef` / `shallowReactive` for large objects that don't need deep reactivity
```typescript
// ❌ Deep reactive on a 1000-item array — Vue tracks every nested property
const items = ref<Item[]>([])
// ✅ Shallow — Vue only tracks the array reference, not individual items
const items = shallowRef<Item[]>([])
```
### Pattern: Virtual Scrolling for Long Lists
**Symptom:** Rendering > 100 DOM nodes causes jank. DevTools shows long "Rendering" and "Painting" phases.
**Fix:** Use virtual scrolling (only render visible items):
```bash
npm install @tanstack/vue-virtual
```
### Pattern: PWA Cache Strategy Audit
**Symptom:** Repeat visits are slow despite service worker. Network tab shows cache misses.
**Audit checklist:**
1. Verify `navigator.serviceWorker.controller` is active (DevTools → Application → Service Workers)
2. Check `Cache Storage` in DevTools — are the expected caches populated?
3. Verify `runtimeCaching` patterns in `vite.config.ts` match actual API URL patterns
4. Check `networkTimeoutSeconds` — too high means slow fallback to cache
### Pattern: Tailwind CSS Purging
**Symptom:** Large CSS bundle despite using Tailwind. Build output shows > 50KB CSS.
**Fix:** Tailwind v4 (your version) uses automatic content detection. Verify:
1. No `@import` of the full Tailwind CSS build in production
2. The `@tailwindcss/vite` plugin is used (it is — confirmed in your config)
3. No dynamic class names that prevent tree-shaking (e.g., `class={`text-${color}-500`}`)
---
## Vue/Vite-Specific Anti-Patterns
1. **Don't eagerly import all routes.** This defeats Vite's code splitting. Always use dynamic `() => import()` for route components.
2. **Don't use `v-if` + `v-for` on the same element.** `v-if` has higher priority in Vue 3 — use a `<template>` wrapper.
3. **Don't deep-watch large arrays.** Use `shallowRef` or `watch(() => arr.length)` to avoid O(n) reactivity tracking.
4. **Don't ignore `key` in `v-for`.** Missing keys force Vue to re-render entire lists instead of patching individual items.
5. **Don't inline large SVGs when they could be `<img>` references.** Inline SVGs increase HTML parse time and bundle size.
6. **Don't load Supabase JS in every component.** Import the singleton client from `supabaseClient.ts` — avoid creating multiple GoTrue instances.
7. **Don't use plain functions for reactive template expressions.** If `v-if="isVisible()"` calls a function, Vue re-executes it on every render cycle. Use `computed` instead — Vue caches the result and only re-runs when dependencies change.
---
## Irreducible Floors in Vue/Vite/Browser
| Cost | What it is | Why it's irreducible |
|---|---|---|
| Vue runtime | Reactivity system, virtual DOM diffing | Framework overhead — ~30KB min gzipped |
| Supabase JS | GoTrue + PostgREST client | Auth + API client — ~40KB min gzipped |
| TLS handshake | HTTPS connection setup | Network latency — mitigated by HTTP/2, preconnect |
| Font loading | WOFF2 download + layout shift | Use `font-display: swap` and preload critical fonts |
| Service Worker registration | SW parse + install on first visit | One-time cost per SW update |
| DNS resolution | Supabase API domain lookup | Use `<link rel="dns-prefetch">` |
---
## Data Extraction Scripts
Use the scripts in `scripts/` for automated profiling:
```bash
# Always-works bundle analysis (no Chrome needed) — start here
scripts/frontend-lighthouse.sh bundle ./apps/frontend
# Lighthouse Core Web Vitals (needs Chrome/Chromium)
scripts/frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
# With custom timeout (default: 120s)
LIGHTHOUSE_TIMEOUT=60 scripts/frontend-lighthouse.sh lighthouse http://localhost:4173
```
> **Agent tip:** If Lighthouse hangs (common in headless/CI environments), the script
> auto-kills after the timeout and suggests bundle mode. Always start with `bundle` mode
> since it works everywhere and provides the most actionable data (chunk sizes, vendor
> splitting status, oversized chunk flags).
## Recommended Profiling Workflow
1. **Bundle analysis first:** Run `scripts/frontend-lighthouse.sh bundle ./apps/frontend` to check chunk sizes
2. **Lighthouse desktop + mobile:** Run `scripts/frontend-lighthouse.sh lighthouse <url> desktop` and default (mobile) — compare scores
3. **Runtime profiling:** Use Chrome DevTools Performance tab to record user flows (navigation, exercise completion)
4. **Memory:** Take heap snapshots before and after a user flow — diff for leaks (detached DOM, event listeners)
5. **Coverage:** Run Coverage tool — identify dead JS/CSS shipped to the client
---
## Post-Optimization Clean Sweep Checklist
After applying the main fixes, run this checklist to catch leftover issues.
This was codified from the FATH frontend session (2026-03-23).
### Template Reactivity
- [ ] All `v-if` / `v-show` conditions use `computed` refs, not plain function calls
- [ ] `v-for` lists use stable `:key` values (not array index)
- [ ] No plain functions in template expressions that could be `computed`
### Component Loading
- [ ] All routes use `() => import()` (lazy routes)
- [ ] Modals, drawers, and sheets use `defineAsyncComponent` (they are never visible on initial render)
- [ ] Heavy sub-components within lazy routes use `defineAsyncComponent` if they are conditionally rendered
### Store Health
- [ ] Stores used by multiple co-mounted views have a concurrent-fetch guard (`if (isLoading) return`)
- [ ] No `deep: true` watchers on large arrays — use `shallowRef` + explicit trigger
- [ ] Computed getters are used instead of methods for derived store state
### Bundle Health
- [ ] Vendor libraries are isolated into stable chunks (`manualChunks`)
- [ ] CSS `@import` statements for external stylesheets (fonts, etc.) are replaced with HTML `<link>` tags
- [ ] `<link rel="preconnect">` hints exist for all statically-known third-party origins
- [ ] Supabase JS is imported only once from a singleton module, not in individual components
### Build Verification
- [ ] `npx vite build` — clean output, no warnings
- [ ] `npx vitest run` — all tests pass
- [ ] `npx vue-tsc -b` — no type errors
- [ ] Build output diff — no unexpected chunk size regressions
@@ -0,0 +1,133 @@
# Go Performance Profiling
## Toolchain
| Tool | Purpose | Command |
|---|---|---|
| `go tool pprof` | CPU / heap / block / mutex profiles | `go tool pprof -text -cum -nodecount=30 profile.pb.gz` |
| `go test -bench` | Micro-benchmarks | `go test -bench=BenchmarkX -benchtime=3s -benchmem -count=3` |
| `go test -cpuprofile` | Capture CPU profile during benchmarks | `go test -bench=BenchmarkX -cpuprofile=cpu.prof` |
| `go test -memprofile` | Capture heap profile during benchmarks | `go test -bench=BenchmarkX -memprofile=mem.prof` |
| `go build -gcflags="-m"` | Escape analysis (what allocates on heap) | `go build -gcflags="-m" ./path/to/pkg 2>&1 \| grep "escapes to heap"` |
| `go test -trace` | Execution trace (goroutine scheduling) | `go test -bench=BenchmarkX -trace=trace.out` |
| `benchstat` | Statistical comparison of benchmarks | `benchstat old.txt new.txt` |
## Data Extraction Script
Use `scripts/go-pprof.sh` to extract profiles into agent-readable markdown.
```bash
# CPU profile from a .prof file
.agents/skills/perf-optimization/scripts/go-pprof.sh cpu path/to/cpu.prof
# Heap profile from a live endpoint
.agents/skills/perf-optimization/scripts/go-pprof.sh heap http://localhost:6060/debug/pprof/heap
# CPU profile from a benchmark (generates and analyzes in one step)
.agents/skills/perf-optimization/scripts/go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth
```
## Go-Specific Optimization Patterns
### Escape Analysis
Before optimizing allocations, run escape analysis to understand WHY values escape to heap:
```bash
go build -gcflags="-m" ./internal/platform/server/ 2>&1 | grep "escapes to heap"
```
Common escape reasons:
- Returning a pointer to a local variable
- Storing a value in an interface (boxing)
- Closure captures
- Slice/map growth beyond initial capacity
### `sync.Pool` for Hot-Path Allocations
**When to use:** The profiler shows a specific type allocated/freed thousands of times per second on the same hot path.
**When NOT to use:** Objects that are long-lived, vary wildly in size, or have cleanup requirements.
```go
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
defer func() { buf.Reset(); bufPool.Put(buf) }()
// use buf...
}
```
### Pre-sized Slices and Maps
**Symptom:** `runtime.growslice` or `runtime.mapassign` in the profile — the runtime is repeatedly growing backing arrays.
```go
// ❌ Grows 3 times: cap 0 → 1 → 2 → 4
items := []Item{}
for _, raw := range data { items = append(items, parse(raw)) }
// ✅ One allocation
items := make([]Item, 0, len(data))
for _, raw := range data { items = append(items, parse(raw)) }
```
### `strings.Builder` Over `fmt.Sprintf` in Loops
**Symptom:** `fmt.Sprintf` or `runtime.convTstring` in allocation-heavy loops.
```go
var b strings.Builder
b.Grow(estimatedLen)
for _, part := range parts { b.WriteString(part) }
result := b.String()
```
### Benchmark Best Practices
```go
func BenchmarkX(b *testing.B) {
// Setup OUTSIDE the loop — don't measure init cost
fixture := buildFixture(b)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := doWork(fixture)
// Prevent compiler from eliminating the call
_ = result
}
}
```
**Statistical validity:** Always run with `-count=5` minimum and compare with `benchstat`:
```bash
go test -bench=BenchmarkX -benchmem -count=5 > old.txt
# ... make changes ...
go test -bench=BenchmarkX -benchmem -count=5 > new.txt
benchstat old.txt new.txt
```
## Go-Specific Anti-Patterns
1. **Don't use `sync.Pool` for everything.** It's only effective for truly hot paths with uniform objects. Misuse adds complexity without measurable gain.
2. **Don't fight the GC.** Go's GC is tuned for latency. If throughput matters more, tune `GOGC` before rewriting code.
3. **Don't use `unsafe` for performance.** The compiler's escape analysis and inlining are usually sufficient. `unsafe` breaks memory safety guarantees.
4. **Don't ignore `-race` when benchmarking.** Always verify correctness with `-race` before measuring performance without it.
## Irreducible Floors in Go
These are performance costs you cannot eliminate — recognize them and stop:
| Function | What it is | Why it's irreducible |
|---|---|---|
| `p256MulInternal` | P-256 scalar multiplication (ECDSA) | Hardware-optimized assembly |
| `runtime.mallocgc` | Go memory allocator | Symptom, not cause — fix the caller |
| `runtime.gcBgMarkWorker` | GC mark phase | Proportional to allocation rate — fix allocs |
| `runtime.futex` / `runtime.usleep` | OS synchronization | Scheduling overhead |
| `syscall.Syscall` | Kernel calls (I/O) | Irreducible for actual I/O ops |
| `crypto/aes` | AES encryption | Hardware AES-NI, cannot improve |
@@ -0,0 +1,12 @@
# Python Performance Profiling
> **Stub:** This module will be populated when Python profiling is needed in a project.
> Follow the pattern established in [go.md](go.md).
## Expected Content
- Toolchain: `py-spy`, `cProfile`, `memory_profiler`, `line_profiler`, `scalene`
- Patterns: `__slots__`, generator expressions, `collections.deque`, `numpy` vectorization
- Benchmarking: `timeit`, `pytest-benchmark`
- Irreducible floors: GIL contention, interpreter overhead, C extension boundaries
- Script: `scripts/python-pyspy.sh`
@@ -0,0 +1,12 @@
# Rust Performance Profiling
> **Stub:** This module will be populated when Rust profiling is needed in a project.
> Follow the pattern established in [go.md](go.md).
## Expected Content
- Toolchain: `perf`, `flamegraph`, `criterion`, `cargo-flamegraph`, `dhat`
- Patterns: Arena allocation, `#[inline]`, zero-copy parsing, `Box` vs stack
- Benchmarking: `criterion` setup, statistical comparison
- Irreducible floors: syscall overhead, TLS handshake, memory-mapped I/O
- Script: `scripts/rust-flamegraph.sh`
@@ -0,0 +1,374 @@
#!/usr/bin/env bash
# frontend-lighthouse.sh — Robust frontend performance profiling for AI agents.
#
# Two modes:
# 1. `lighthouse` — Run headless Lighthouse with timeout and binary discovery
# 2. `bundle` — Analyze Vite build output (always works, no Chrome needed)
#
# Usage:
# frontend-lighthouse.sh lighthouse <url> [preset]
# frontend-lighthouse.sh bundle <frontend-dir>
#
# Arguments:
# lighthouse mode:
# url — The URL to audit (e.g., http://localhost:4173)
# preset — "desktop" or "mobile" (default: mobile)
#
# bundle mode:
# frontend-dir — Path to the frontend project root (must have vite.config.*)
#
# Examples:
# frontend-lighthouse.sh lighthouse http://localhost:4173 desktop
# frontend-lighthouse.sh bundle ./apps/frontend
# frontend-lighthouse.sh bundle /home/user/project/apps/frontend
#
# Prerequisites:
# lighthouse mode: Node.js + Chrome/Chromium (or globally installed lighthouse)
# bundle mode: Node.js + Vite project (no Chrome needed)
#
# Robustness features (learned from real-world agent sessions):
# - Binary discovery: checks PATH, npx, ~/.npm-global/bin, common install locations
# - Timeout: kills Lighthouse after LIGHTHOUSE_TIMEOUT seconds (default: 120)
# - Fallback: if Lighthouse fails/hangs, suggests running bundle mode instead
# - Error reporting disabled: prevents interactive consent prompts
# - Bundle mode: always works, extracts chunk sizes from vite build output
#
# Output: Structured markdown tables suitable for LLM consumption.
set -euo pipefail
MODE="${1:-}"
LIGHTHOUSE_TIMEOUT="${LIGHTHOUSE_TIMEOUT:-120}"
# ─── Binary Discovery ──────────────────────────────────────────────────────────
# Lighthouse can be installed globally via npm, in ~/.npm-global, or via npx.
# Try multiple locations to find a working binary.
find_lighthouse() {
# 1. Check PATH
if command -v lighthouse &>/dev/null; then
echo "lighthouse"
return
fi
# 2. Check common global install locations
local candidates=(
"$HOME/.npm-global/bin/lighthouse"
"/usr/local/bin/lighthouse"
"/usr/bin/lighthouse"
)
for candidate in "${candidates[@]}"; do
if [[ -x "$candidate" ]]; then
echo "$candidate"
return
fi
done
# 3. Fall back to npx (will auto-install if needed)
echo "npx --yes lighthouse"
}
# ─── Lighthouse Mode ───────────────────────────────────────────────────────────
run_lighthouse() {
local url="$1"
local preset="${2:-mobile}"
local lh_cmd
lh_cmd=$(find_lighthouse)
local preset_flag=""
if [[ "$preset" == "desktop" ]]; then
preset_flag="--preset=desktop"
fi
local tmpfile
tmpfile=$(mktemp /tmp/lighthouse-XXXXXX.json)
trap 'rm -f "$tmpfile"' EXIT
echo "# Lighthouse Report: ${url} (${preset})"
echo ""
echo "Binary: ${lh_cmd}"
echo "Timeout: ${LIGHTHOUSE_TIMEOUT}s"
echo ""
# Run Lighthouse with timeout to prevent infinite hangs in headless environments.
# --enable-error-reporting=false prevents interactive consent prompts.
# --no-sandbox is required in containerized/CI environments.
local lh_pid
local lh_exit=0
$lh_cmd "$url" \
--output=json \
--output-path="$tmpfile" \
--chrome-flags="--headless --no-sandbox --disable-gpu --disable-dev-shm-usage" \
--only-categories=performance \
$preset_flag \
--enable-error-reporting=false \
--quiet 2>/dev/null &
lh_pid=$!
# Wait with timeout — kill if Lighthouse hangs (common in headless/CI)
local elapsed=0
while kill -0 "$lh_pid" 2>/dev/null; do
if (( elapsed >= LIGHTHOUSE_TIMEOUT )); then
kill -9 "$lh_pid" 2>/dev/null || true
wait "$lh_pid" 2>/dev/null || true
echo "⚠️ Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT}s."
echo ""
echo "This usually means Chrome/Chromium is not available or the headless"
echo "environment doesn't support it. Try bundle mode instead:"
echo ""
echo " $0 bundle <frontend-dir>"
echo ""
return 1
fi
sleep 2
elapsed=$((elapsed + 2))
done
wait "$lh_pid" || lh_exit=$?
if [[ $lh_exit -ne 0 ]]; then
echo "⚠️ Lighthouse exited with code ${lh_exit}."
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
return 1
fi
if [[ ! -s "$tmpfile" ]]; then
echo "⚠️ Lighthouse produced no output."
echo "Try bundle mode instead: $0 bundle <frontend-dir>"
return 1
fi
# Extract metrics into structured markdown
node -e "
const fs = require('fs');
const r = JSON.parse(fs.readFileSync('$tmpfile', 'utf8'));
const a = r.audits;
const score = Math.round((r.categories.performance.score || 0) * 100);
console.log('## Performance Score: ' + score + '/100');
console.log('');
console.log('## Core Web Vitals');
console.log('');
console.log('| Metric | Value | Score |');
console.log('|---|---|---|');
const vitals = [
'first-contentful-paint',
'largest-contentful-paint',
'total-blocking-time',
'cumulative-layout-shift',
'speed-index',
'interactive',
];
vitals.forEach(k => {
if (a[k]) {
const s = a[k].score !== null ? Math.round(a[k].score * 100) : 'N/A';
console.log('| ' + a[k].title + ' | ' + (a[k].displayValue || 'N/A') + ' | ' + s + ' |');
}
});
console.log('');
console.log('## Opportunities');
console.log('');
const opportunities = Object.values(a)
.filter(audit => audit.details && audit.details.type === 'opportunity' && audit.details.overallSavingsMs > 0)
.sort((x, y) => (y.details.overallSavingsMs || 0) - (x.details.overallSavingsMs || 0));
if (opportunities.length > 0) {
console.log('| Opportunity | Potential Savings |');
console.log('|---|---|');
opportunities.forEach(o => {
console.log('| ' + o.title + ' | ' + Math.round(o.details.overallSavingsMs) + ' ms |');
});
} else {
console.log('No significant opportunities found.');
}
console.log('');
console.log('## Diagnostics');
console.log('');
const diagnostics = Object.values(a)
.filter(audit => audit.details && audit.details.type === 'table' && audit.score !== null && audit.score < 0.9)
.slice(0, 10);
if (diagnostics.length > 0) {
console.log('| Diagnostic | Score |');
console.log('|---|---|');
diagnostics.forEach(d => {
console.log('| ' + d.title + ' | ' + Math.round(d.score * 100) + ' |');
});
} else {
console.log('All diagnostics passed.');
}
"
}
# ─── Bundle Analysis Mode ──────────────────────────────────────────────────────
# Always works — no Chrome needed. Extracts chunk sizes from vite build output.
# This was the most useful data source during the FATH frontend session: it
# revealed the 297KB monolithic index chunk and guided all 6 fixes.
run_bundle() {
local frontend_dir="$1"
if [[ ! -d "$frontend_dir" ]]; then
echo "Error: directory '$frontend_dir' does not exist."
exit 1
fi
# Verify it's a Vite project
if ! ls "$frontend_dir"/vite.config.* &>/dev/null; then
echo "Error: no vite.config.* found in '$frontend_dir'. Is this a Vite project?"
exit 1
fi
echo "# Bundle Analysis: ${frontend_dir}"
echo ""
echo "Running \`vite build\`..."
echo ""
local build_output
build_output=$(cd "$frontend_dir" && npx vite build 2>&1)
local build_exit=$?
if [[ $build_exit -ne 0 ]]; then
echo "⚠️ Build failed (exit code $build_exit):"
echo ""
echo '```'
echo "$build_output"
echo '```'
return 1
fi
# Parse build output into structured tables
node -e "
const output = \`$build_output\`;
const lines = output.split('\n');
const jsChunks = [];
const cssChunks = [];
// Parse Vite build output lines like:
// dist/assets/index-BExof8y3.js 20.04 kB │ gzip: 6.90 kB
for (const line of lines) {
const match = line.match(/dist\/assets\/(.+?)\s+([\d.]+)\s*kB\s*│\s*gzip:\s*([\d.]+)\s*kB/);
if (match) {
const [, name, rawKB, gzipKB] = match;
const entry = { name, raw: parseFloat(rawKB), gzip: parseFloat(gzipKB) };
if (name.endsWith('.js')) jsChunks.push(entry);
else if (name.endsWith('.css')) cssChunks.push(entry);
}
}
// Sort by gzip size descending
jsChunks.sort((a, b) => b.gzip - a.gzip);
cssChunks.sort((a, b) => b.gzip - a.gzip);
const totalJsRaw = jsChunks.reduce((s, c) => s + c.raw, 0);
const totalJsGzip = jsChunks.reduce((s, c) => s + c.gzip, 0);
const totalCssRaw = cssChunks.reduce((s, c) => s + c.raw, 0);
const totalCssGzip = cssChunks.reduce((s, c) => s + c.gzip, 0);
console.log('## JS Chunks (sorted by gzip size)');
console.log('');
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
console.log('|---|---|---|');
jsChunks.forEach(c => {
// Clean chunk name: remove hash suffix for readability
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.js$/, '.js');
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
});
console.log('| **Total JS** | **' + totalJsRaw.toFixed(2) + '** | **' + totalJsGzip.toFixed(2) + '** |');
if (cssChunks.length > 0) {
console.log('');
console.log('## CSS Chunks');
console.log('');
console.log('| Chunk | Raw (KB) | Gzip (KB) |');
console.log('|---|---|---|');
cssChunks.forEach(c => {
const clean = c.name.replace(/-[A-Za-z0-9_-]{8}\\.css$/, '.css');
console.log('| ' + clean + ' | ' + c.raw.toFixed(2) + ' | ' + c.gzip.toFixed(2) + ' |');
});
console.log('| **Total CSS** | **' + totalCssRaw.toFixed(2) + '** | **' + totalCssGzip.toFixed(2) + '** |');
}
console.log('');
console.log('## Summary');
console.log('');
console.log('| Metric | Value |');
console.log('|---|---|');
console.log('| JS chunks | ' + jsChunks.length + ' |');
console.log('| CSS chunks | ' + cssChunks.length + ' |');
console.log('| Total JS (gzip) | ' + totalJsGzip.toFixed(2) + ' KB |');
console.log('| Total CSS (gzip) | ' + totalCssGzip.toFixed(2) + ' KB |');
console.log('| Largest JS chunk | ' + (jsChunks[0]?.name || 'N/A') + ' (' + (jsChunks[0]?.gzip.toFixed(2) || '0') + ' KB gzip) |');
// Flag potential issues
console.log('');
console.log('## Flags');
console.log('');
const bigChunks = jsChunks.filter(c => c.gzip > 50);
if (bigChunks.length > 0) {
console.log('⚠️ **Large chunks (>50 KB gzip):**');
bigChunks.forEach(c => {
console.log(' - ' + c.name + ': ' + c.gzip.toFixed(2) + ' KB gzip');
});
console.log('');
console.log('Consider splitting vendor libraries into separate chunks (see: Artifact Partitioning pattern in SKILL.md).');
} else {
console.log('✅ No oversized chunks detected.');
}
const vendorChunks = jsChunks.filter(c => c.name.startsWith('vendor-'));
if (vendorChunks.length > 0) {
console.log('');
console.log('✅ Vendor chunk splitting detected: ' + vendorChunks.map(c => c.name).join(', '));
} else if (jsChunks.some(c => c.gzip > 40)) {
console.log('');
console.log('⚠️ No vendor chunk splitting detected. The largest chunk may contain framework + app code bundled together.');
}
"
}
# ─── Main ──────────────────────────────────────────────────────────────────────
case "${MODE}" in
lighthouse|lh)
URL="${2:-}"
PRESET="${3:-mobile}"
if [[ -z "$URL" ]]; then
echo "Usage: $0 lighthouse <url> [desktop|mobile]"
echo " $0 bundle <frontend-dir>"
exit 1
fi
run_lighthouse "$URL" "$PRESET"
;;
bundle|build)
DIR="${2:-}"
if [[ -z "$DIR" ]]; then
echo "Usage: $0 bundle <frontend-dir>"
echo " $0 lighthouse <url> [desktop|mobile]"
exit 1
fi
run_bundle "$DIR"
;;
*)
echo "Frontend Performance Profiling Script"
echo ""
echo "Usage:"
echo " $0 lighthouse <url> [desktop|mobile] — Run Lighthouse (needs Chrome)"
echo " $0 bundle <frontend-dir> — Analyze Vite build chunks (no Chrome)"
echo ""
echo "Examples:"
echo " $0 lighthouse http://localhost:4173 desktop"
echo " $0 bundle ./apps/frontend"
echo ""
echo "Environment:"
echo " LIGHTHOUSE_TIMEOUT=120 — Seconds before killing a hung Lighthouse (default: 120)"
exit 1
;;
esac
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# go-pprof.sh — Extract Go pprof profiles into agent-readable markdown.
#
# Usage:
# go-pprof.sh cpu <profile-path-or-url> [top-n]
# go-pprof.sh heap <profile-path-or-url> [top-n]
# go-pprof.sh bench <package-path> <bench-regex> [top-n]
#
# Examples:
# go-pprof.sh cpu cpu.prof
# go-pprof.sh heap http://localhost:6060/debug/pprof/heap
# go-pprof.sh bench ./internal/platform/server/... BenchmarkRequireAuth 40
#
# Output: Structured markdown suitable for LLM consumption.
set -euo pipefail
PROFILE_TYPE="${1:-}"
SOURCE="${2:-}"
TOP_N="${3:-30}"
usage() {
echo "Usage:"
echo " $0 cpu <profile-path-or-url> [top-n]"
echo " $0 heap <profile-path-or-url> [top-n]"
echo " $0 bench <package-path> <bench-regex> [top-n]"
exit 1
}
if [[ -z "$PROFILE_TYPE" || -z "$SOURCE" ]]; then
usage
fi
extract_pprof() {
local profile_file="$1"
local label="$2"
local top="$3"
echo ""
echo "--- PPROF ${label} PROFILE (Top ${top} by Cumulative) ---"
echo ""
echo '```'
go tool pprof -text -cum -nodecount="$top" "$profile_file" 2>/dev/null \
| grep -v "^Showing nodes" \
| sed '/^$/d'
echo '```'
echo ""
}
case "$PROFILE_TYPE" in
cpu|heap|block|mutex)
extract_pprof "$SOURCE" "$(echo "$PROFILE_TYPE" | tr '[:lower:]' '[:upper:]')" "$TOP_N"
;;
bench)
BENCH_REGEX="${3:-}"
TOP_N="${4:-30}"
if [[ -z "$BENCH_REGEX" ]]; then
echo "Error: bench mode requires <package-path> and <bench-regex>"
usage
fi
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
echo "# Benchmark Profile: ${BENCH_REGEX}"
echo ""
# Run benchmark and capture results + profiles
echo "## Benchmark Results"
echo ""
echo '```'
go test "$SOURCE" \
-bench="$BENCH_REGEX" \
-benchtime=2s \
-benchmem \
-count=1 \
-run='^$' \
-cpuprofile="$TMPDIR/cpu.prof" \
-memprofile="$TMPDIR/mem.prof" \
-timeout=120s 2>&1
echo '```'
# Extract CPU profile
if [[ -f "$TMPDIR/cpu.prof" ]]; then
# Find the test binary (go test leaves it next to the profile)
TEST_BIN=$(find "$TMPDIR" -name '*.test' 2>/dev/null | head -1 || true)
if [[ -n "$TEST_BIN" ]]; then
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
else
extract_pprof "$TMPDIR/cpu.prof" "CPU" "$TOP_N"
fi
fi
# Extract heap profile
if [[ -f "$TMPDIR/mem.prof" ]]; then
echo ""
echo "--- PPROF HEAP PROFILE (Top ${TOP_N} by Cumulative, alloc_space) ---"
echo ""
echo '```'
go tool pprof -text -cum -nodecount="$TOP_N" -alloc_space "$TMPDIR/mem.prof" 2>/dev/null \
| grep -v "^Showing nodes" \
| sed '/^$/d'
echo '```'
echo ""
fi
;;
*)
echo "Error: unknown profile type '$PROFILE_TYPE'"
usage
;;
esac