Performance Profiling
Intro
Intuition about performance is unreliable — always profile before
optimizing. Work the cycle: identify the symptom, measure the
baseline, profile the right resource, optimize the top bottleneck,
verify the improvement. One change at a time.
Overview
Performance analysis cycle
Always work the full cycle: Identify -> Measure -> Profile ->
Optimize -> Verify.
- Identify the concern: slow response, high memory, excessive
CPU, I/O stalls
- Measure the baseline with concrete numbers (latency p50/p95/p99,
throughput, RSS)
- Profile with the right tool for the resource type — see
references/profiling-tools.md
- Optimize the top bottleneck only; do not scatter-shot
- Verify with the same measurement from step 2
Never skip measurement. Without a number you cannot tell whether the
change helped or hurt.
CPU profiling
Identify what the application spends CPU time on:
- Use a sampling profiler (not instrumenting) to minimize
overhead
- Collect a representative workload, not a trivial test case
- Generate a flame graph for visual analysis
- Look for wide stacks (hot functions), deep stacks (excess
abstraction), and unexpected functions (regex compilation,
serialization, logging)
Track CPU time per operation, not wall-clock (which includes I/O
waits).
Memory profiling
Identify allocation pressure and leaks:
- Track peak RSS and allocation rate, not just current usage
- For leaks, take heap snapshots at intervals and diff them
- Look for unbounded collections, caches without eviction, event
listener accumulation, and reference cycles in GC languages
- Distinguish a leak (unbounded growth) from high-but-stable usage,
which may be acceptable
I/O profiling
Identify disk, network, or database bottlenecks:
- Compare wall-clock time with CPU time — a large gap is I/O wait
- For databases: enable slow query logging, look for N+1, missing
indexes
- For network: check connection pooling, DNS, TLS handshake overhead
- For disk: check synchronous writes, excessive fsync, unnecessary
file ops
Quick checklist before profiling
- What is the user-visible symptom? (slow page, high bill, OOM)
- What resource is constrained? (CPU, memory, disk, network, DB)
- Constant or intermittent? (steady-state vs spike-triggered)
- What changed recently? Regressions usually correlate with
recent deploys
- What is the acceptable target? (p99 < 200 ms, RSS < 512 MB —
you need a goal to know when you are done)
Gotchas
Agent-specific failure modes — provider-neutral pause-and-self-check items:
- Optimizing before establishing a baseline measurement. Without a concrete before number (p50/p95/p99, RSS, throughput), there is no way to tell whether the change helped, hurt, or made no difference. Establish the baseline first — then optimize — then verify with the same measurement.
- Profiling under trivial or synthetic load. A profiler run against a tight loop with no I/O tells you nothing about production behavior. Collect a workload representative of real traffic — replay production logs, replicate actual access patterns, or drive realistic concurrency before reading the profiler output.
- Chasing secondary bottlenecks before exhausting the primary one. After a profiling pass, fix the top bottleneck first and re-profile. Fixing the second-biggest bottleneck while the primary one still saturates the resource often produces no measurable improvement.
- Reporting wall-clock time when the constrained resource is CPU. A function that takes 200ms wall-clock while waiting for a database query uses almost no CPU. Profiling the wrong resource leads to wrong conclusions. Distinguish CPU time, wall-clock, I/O wait, and memory allocation — they require different tools and different fixes.
- Declaring success after a microbenchmark without re-running under realistic load. Verifying a change with a tight loop under no concurrency and then shipping it is not a verification. Re-run the same realistic workload used for the baseline before declaring the goal met.
- Fixing a performance regression without adding a regression test. A regression fixed once will recur unless a benchmark or load-test encodes the boundary. After every performance fix, add a test that would catch reversion to the slow path.
- Running high-rate sampling profilers against production. Profilers at 100% sampling rate or instrumenting every function call introduce measurable overhead that can itself degrade the system under investigation. Use a low-overhead sampling approach in production; reserve high-rate profiling for staging.
Full reference
Profiling tools by language
references/profiling-tools.md has the full table. Quick picks:
- Python:
py-spy (sampling CPU, attach to running process,
flame graphs); memray (allocations, native + Python frames);
scalene (line-level CPU + memory + GPU); cProfile for built-in
function-level
- Node.js:
clinic.js (Doctor / Bubbleprof / Flame); 0x (flame
graphs); Chrome DevTools for heap snapshots
- Rust:
cargo flamegraph (CPU); criterion (statistical
benchmarks); dhat (heap); perf for hardware counters
- Go:
pprof for CPU/heap/goroutine; trace for scheduler and
GC events; benchstat for statistical comparison
- JVM: JFR (Flight Recorder) — built-in and production-safe;
async-profiler for sampling CPU/alloc/lock with no safepoint
bias; jmap + MAT for heap dumps
Benchmarking and regression detection
- Isolate the code under test — benchmark the function, not the
setup
- Run enough iterations for statistical significance (mean, stddev,
percentiles)
- Control external factors: disable turbo boost, pin CPU frequency,
close other processes
- For regression detection, store baseline results in version
control and compare on each run
# Rust criterion
cargo bench -- --save-baseline main
# After changes:
cargo bench -- --baseline main
Reading flame graphs
- X-axis is alphabetical (not time); width = proportion of
samples
- Y-axis is stack depth, read bottom-up (caller -> callee)
- Wide boxes at the top are hot leaf functions — optimize first
- Wide boxes at the bottom are hot callers — consider algorithmic
changes
- Look for unexpected frames: GC pauses, lock contention, allocator
overhead
- Use differential flame graphs (red/blue) to compare before/after
Common patterns: a single tall tower means one deep call chain
dominates (look for unnecessary abstraction); a wide plateau at the
top means a leaf function consuming most CPU; many thin towers with
the same top function is a memoization candidate; GC/malloc frames
indicate allocation pressure; lock/mutex frames mean contention;
syscall frames (read, write, poll) mean the workload is I/O
bound.
Common optimization patterns
After profiling reveals the bottleneck, apply the right fix:
- Algorithmic — O(n²) -> O(n log n). Always check this first.
- Batching — combine many small operations into fewer large ones
- Caching — reuse computed results, but measure the hit rate
- Pooling — reuse expensive resources (connections, threads,
buffers)
- Lazy evaluation — defer work until actually needed
- Data layout — struct-of-arrays vs array-of-structs for cache
friendliness
General tips
- Always profile a realistic workload, not a microbenchmark
- Profile in an environment close to production — same data
size, same concurrency
- Sampling profilers (
py-spy, async-profiler, perf) are
preferred over instrumenting profilers in production
- Collect profiles for at least 30 seconds for statistical
significance
- Compare before and after with the same workload to validate
improvements
1---2name: performance-profiling3description: Performance analysis and profiling for CPU, memory, and I/O — flame graphs, benchmarks, regression detection. Use when identifying performance bottlenecks, profiling CPU/memory/I/O, interpreting flame graphs, setting up benchmarking or regression detection, or optimizing a slow function or endpoint.4---56# Performance Profiling78## Intro910Intuition about performance is unreliable — always profile before11optimizing. Work the cycle: identify the symptom, measure the12baseline, profile the right resource, optimize the top bottleneck,13verify the improvement. One change at a time.1415## Overview1617### Performance analysis cycle1819Always work the full cycle: **Identify -> Measure -> Profile ->20Optimize -> Verify**.21221. **Identify** the concern: slow response, high memory, excessive23 CPU, I/O stalls242. **Measure** the baseline with concrete numbers (latency p50/p95/p99,25 throughput, RSS)263. **Profile** with the right tool for the resource type — see27 `references/profiling-tools.md`284. **Optimize** the top bottleneck only; do not scatter-shot295. **Verify** with the same measurement from step 23031Never skip measurement. Without a number you cannot tell whether the32change helped or hurt.3334### CPU profiling3536Identify what the application spends CPU time on:37381. Use a **sampling** profiler (not instrumenting) to minimize39 overhead402. Collect a representative workload, not a trivial test case413. Generate a flame graph for visual analysis424. Look for **wide stacks** (hot functions), **deep stacks** (excess43 abstraction), and unexpected functions (regex compilation,44 serialization, logging)4546Track **CPU time** per operation, not wall-clock (which includes I/O47waits).4849### Memory profiling5051Identify allocation pressure and leaks:52531. Track **peak RSS** and **allocation rate**, not just current usage542. For leaks, take heap snapshots at intervals and diff them553. Look for unbounded collections, caches without eviction, event56 listener accumulation, and reference cycles in GC languages574. Distinguish a leak (unbounded growth) from high-but-stable usage,58 which may be acceptable5960### I/O profiling6162Identify disk, network, or database bottlenecks:63641. Compare wall-clock time with CPU time — a large gap is I/O wait652. For databases: enable slow query logging, look for N+1, missing66 indexes673. For network: check connection pooling, DNS, TLS handshake overhead684. For disk: check synchronous writes, excessive fsync, unnecessary69 file ops7071### Quick checklist before profiling72731. What is the **user-visible symptom**? (slow page, high bill, OOM)742. What **resource** is constrained? (CPU, memory, disk, network, DB)753. **Constant or intermittent**? (steady-state vs spike-triggered)764. **What changed recently?** Regressions usually correlate with77 recent deploys785. What is the **acceptable target**? (p99 < 200 ms, RSS < 512 MB —79 you need a goal to know when you are done)8081## Gotchas8283Agent-specific failure modes — provider-neutral pause-and-self-check items:8485- **Optimizing before establishing a baseline measurement.** Without a concrete before number (p50/p95/p99, RSS, throughput), there is no way to tell whether the change helped, hurt, or made no difference. Establish the baseline first — then optimize — then verify with the same measurement.86- **Profiling under trivial or synthetic load.** A profiler run against a tight loop with no I/O tells you nothing about production behavior. Collect a workload representative of real traffic — replay production logs, replicate actual access patterns, or drive realistic concurrency before reading the profiler output.87- **Chasing secondary bottlenecks before exhausting the primary one.** After a profiling pass, fix the top bottleneck first and re-profile. Fixing the second-biggest bottleneck while the primary one still saturates the resource often produces no measurable improvement.88- **Reporting wall-clock time when the constrained resource is CPU.** A function that takes 200ms wall-clock while waiting for a database query uses almost no CPU. Profiling the wrong resource leads to wrong conclusions. Distinguish CPU time, wall-clock, I/O wait, and memory allocation — they require different tools and different fixes.89- **Declaring success after a microbenchmark without re-running under realistic load.** Verifying a change with a tight loop under no concurrency and then shipping it is not a verification. Re-run the same realistic workload used for the baseline before declaring the goal met.90- **Fixing a performance regression without adding a regression test.** A regression fixed once will recur unless a benchmark or load-test encodes the boundary. After every performance fix, add a test that would catch reversion to the slow path.91- **Running high-rate sampling profilers against production.** Profilers at 100% sampling rate or instrumenting every function call introduce measurable overhead that can itself degrade the system under investigation. Use a low-overhead sampling approach in production; reserve high-rate profiling for staging.9293## Full reference9495### Profiling tools by language9697`references/profiling-tools.md` has the full table. Quick picks:9899- **Python**: `py-spy` (sampling CPU, attach to running process,100 flame graphs); `memray` (allocations, native + Python frames);101 `scalene` (line-level CPU + memory + GPU); `cProfile` for built-in102 function-level103- **Node.js**: `clinic.js` (Doctor / Bubbleprof / Flame); `0x` (flame104 graphs); Chrome DevTools for heap snapshots105- **Rust**: `cargo flamegraph` (CPU); `criterion` (statistical106 benchmarks); `dhat` (heap); `perf` for hardware counters107- **Go**: `pprof` for CPU/heap/goroutine; `trace` for scheduler and108 GC events; `benchstat` for statistical comparison109- **JVM**: JFR (Flight Recorder) — built-in and production-safe;110 `async-profiler` for sampling CPU/alloc/lock with no safepoint111 bias; `jmap` + MAT for heap dumps112113### Benchmarking and regression detection1141151. Isolate the code under test — benchmark the function, not the116 setup1172. Run enough iterations for statistical significance (mean, stddev,118 percentiles)1193. Control external factors: disable turbo boost, pin CPU frequency,120 close other processes1214. For regression detection, store baseline results in version122 control and compare on each run123124```bash125# Rust criterion126cargo bench -- --save-baseline main127# After changes:128cargo bench -- --baseline main129```130131### Reading flame graphs132133- **X-axis** is alphabetical (not time); width = proportion of134 samples135- **Y-axis** is stack depth, read bottom-up (caller -> callee)136- **Wide boxes at the top** are hot leaf functions — optimize first137- **Wide boxes at the bottom** are hot callers — consider algorithmic138 changes139- Look for unexpected frames: GC pauses, lock contention, allocator140 overhead141- Use **differential** flame graphs (red/blue) to compare before/after142143Common patterns: a single tall tower means one deep call chain144dominates (look for unnecessary abstraction); a wide plateau at the145top means a leaf function consuming most CPU; many thin towers with146the same top function is a memoization candidate; GC/malloc frames147indicate allocation pressure; lock/mutex frames mean contention;148syscall frames (`read`, `write`, `poll`) mean the workload is I/O149bound.150151### Common optimization patterns152153After profiling reveals the bottleneck, apply the right fix:154155- **Algorithmic** — O(n²) -> O(n log n). Always check this first.156- **Batching** — combine many small operations into fewer large ones157- **Caching** — reuse computed results, but measure the hit rate158- **Pooling** — reuse expensive resources (connections, threads,159 buffers)160- **Lazy evaluation** — defer work until actually needed161- **Data layout** — struct-of-arrays vs array-of-structs for cache162 friendliness163164### General tips165166- Always profile a **realistic workload**, not a microbenchmark167- Profile in an environment **close to production** — same data168 size, same concurrency169- Sampling profilers (`py-spy`, `async-profiler`, `perf`) are170 preferred over instrumenting profilers in production171- Collect profiles for **at least 30 seconds** for statistical172 significance173- Compare **before and after** with the same workload to validate174 improvements