| name | perf-profiler |
| description | Profile and optimize application performance. Use when diagnosing slow code, measuring CPU/memory usage, generating flame graphs, benchmarking functions, load testing APIs, finding memory leaks, or optimizing database queries. |
| metadata | {"clawdbot":{"emoji":"⚡","requires":{"anyBins":["node","python3","go","curl","ab"]},"os":["linux","darwin","win32"]}} |
Performance Profiler
Measure, profile, and optimize application performance. Covers CPU profiling, memory analysis, flame graphs, benchmarking, load testing, and language-specific optimization patterns.
When to Use
- Diagnosing why an application or function is slow
- Measuring CPU and memory usage
- Generating flame graphs to visualize hot paths
- Benchmarking functions or endpoints
- Load testing APIs before deployment
- Finding and fixing memory leaks
- Optimizing database query performance
- Comparing performance before and after changes
Quick Timing
Command-line timing
time my-command --flag
for i in $(seq 1 10); do
/usr/bin/time -f "%e" my-command 2>&1
done | awk '{sum+=$1; sumsq+=$1*$1; count++} END {
avg=sum/count;
stddev=sqrt(sumsq/count - avg*avg);
printf "runs=%d avg=%.3fs stddev=%.3fs\n", count, avg, stddev
}'
hyperfine 'command-a' 'command-b'
hyperfine --warmup 3 --runs 20 'my-command'
hyperfine --export-json results.json 'old-version' 'new-version'
Inline timing (any language)
console.time('operation');
await doExpensiveThing();
console.timeEnd('operation');
const start = performance.now();
await doExpensiveThing();
const elapsed = performance.now() - start;
console.log(`Elapsed: ${elapsed.toFixed(2)}ms`);
import time
start = time.perf_counter()
do_expensive_thing()
elapsed = time.perf_counter() - start
print(f"Elapsed: {elapsed:.4f}s")
from contextlib import contextmanager
@contextmanager
def timer(label=""):
start = time.perf_counter()
yield
elapsed = time.perf_counter() - start
print(f"{label}: {elapsed:.4f}s")
with timer("data processing"):
process_data()
start := time.Now()
doExpensiveThing()
fmt.Printf("Elapsed: %v\n", time.Since(start))
Node.js Profiling
CPU profiling with V8 inspector
node --cpu-prof app.js
node --cpu-prof --cpu-prof-interval=100 app.js
node --inspect app.js
Heap snapshots (memory)
node --heap-prof app.js
node -e "
const v8 = require('v8');
const fs = require('fs');
// Take snapshot
const snapshotStream = v8.writeHeapSnapshot();
console.log('Heap snapshot written to:', snapshotStream);
"
Memory usage monitoring
setInterval(() => {
const usage = process.memoryUsage();
console.log({
rss: `${(usage.rss / 1024 / 1024).toFixed(1)}MB`,
heapUsed: `${(usage.heapUsed / 1024 / 1024).toFixed(1)}MB`,
heapTotal: `${(usage.heapTotal / 1024 / 1024).toFixed(1)}MB`,
external: `${(usage.external / 1024 / 1024).toFixed(1)}MB`,
});
}, 5000);
let lastHeap = 0;
setInterval(() => {
const heap = process.memoryUsage().heapUsed;
const delta = heap - lastHeap;
if (delta > 1024 * 1024) {
console.warn(`Heap grew by ${(delta / 1024 / 1024).toFixed(1)}MB`);
}
lastHeap = heap;
}, 10000);
Node.js benchmarking
function benchmark(name, fn, iterations = 10000) {
for (let i = 0; i < 100; i++) fn();
const start = performance.now();
for (let i = 0; i < iterations; i++) fn();
const elapsed = performance.now() - start;
console.log(`${name}: ${(elapsed / iterations).toFixed(4)}ms/op (${iterations} iterations in ${elapsed.toFixed(1)}ms)`);
}
benchmark('JSON.parse', () => JSON.parse('{"key":"value","num":42}'));
benchmark('regex match', () => /^\d{4}-\d{2}-\d{2}$/.test('2026-02-03'));
Python Profiling
cProfile (built-in CPU profiler)
python3 -m cProfile -s cumulative my_script.py
python3 -m cProfile -o profile.prof my_script.py
python3 -c "
import pstats
stats = pstats.Stats('profile.prof')
stats.sort_stats('cumulative')
stats.print_stats(20)
"
python3 -c "
import cProfile
from my_module import expensive_function
cProfile.run('expensive_function()', sort='cumulative')
"
line_profiler (line-by-line)
pip install line_profiler
kernprof -l -v my_script.py
from line_profiler import LineProfiler
def process_data(data):
result = []
for item in data:
transformed = transform(item)
if validate(transformed):
result.append(transformed)
return result
profiler = LineProfiler()
profiler.add_function(process_data)
profiler.enable()
process_data(large_dataset)
profiler.disable()
profiler.print_stats()
Memory profiling (Python)
pip install memory_profiler
python3 -m memory_profiler my_script.py
from memory_profiler import profile
@profile
def load_data():
data = []
for i in range(1000000):
data.append({'id': i, 'value': f'item_{i}'})
return data
import tracemalloc
tracemalloc.start()
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
Python benchmarking
import timeit
result = timeit.timeit('sorted(range(1000))', number=10000)
print(f"sorted: {result:.4f}s for 10000 iterations")
setup = "data = list(range(10000))"
t1 = timeit.timeit('list(filter(lambda x: x % 2 == 0, data))', setup=setup, number=1000)
t2 = timeit.timeit('[x for x in data if x % 2 == 0]', setup=setup, number=1000)
print(f"filter: {t1:.4f}s | listcomp: {t2:.4f}s | speedup: {t1/t2:.2f}x")
Go Profiling
Built-in pprof
import (
"net/http"
_ "net/http/pprof"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
}
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
go tool pprof http://localhost:6060/debug/pprof/heap
go tool pprof http://localhost:6060/debug/pprof/goroutine
Go benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(42, 58)
}
}
func BenchmarkSort1000(b *testing.B) {
data := make([]int, 1000)
for i := range data {
data[i] = rand.Intn(1000)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Ints(append([]int{}, data...))
}
}
go test -bench=. -benchmem ./...
go test -bench=. -count=5 ./... > old.txt
go test -bench=. -count=5 ./... > new.txt
go install golang.org/x/perf/cmd/benchstat@latest
benchstat old.txt new.txt
Flame Graphs
Generate flame graphs
npx 0x app.js
npx clinic flame -- node app.js
npx clinic doctor -- node app.js
npx clinic bubbleprof -- node app.js
pip install py-spy
py-spy record -o flame.svg -- python3 my_script.py
py-spy record -o flame.svg --pid 12345
go tool pprof -http=:8080 http://localhost:6060/debug/pprof/profile?seconds=30
perf record -g -p PID -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > flame.svg
Reading flame graphs
Key concepts:
- X-axis: NOT time. It's alphabetical sort of stack frames. Width = % of samples.
- Y-axis: Stack depth. Top = leaf function (where CPU time is spent).
- Wide bars at the top = hot functions (optimize these first).
- Narrow tall stacks = deep call chains (may indicate excessive abstraction).
What to look for:
1. Wide plateaus at the top → function that dominates CPU time
2. Multiple paths converging to one function → shared bottleneck
3. GC/runtime frames taking significant width → memory pressure
4. Unexpected functions appearing wide → performance bug
Load Testing
curl-based quick test
curl -o /dev/null -s -w "HTTP %{http_code} | Total: %{time_total}s | TTFB: %{time_starttransfer}s | Connect: %{time_connect}s\n" https://api.example.com/endpoint
for i in $(seq 1 20); do
curl -o /dev/null -s -w "%{time_total}\n" https://api.example.com/endpoint
done | awk '{sum+=$1; count++; if($1>max)max=$1} END {printf "avg=%.3fs max=%.3fs n=%d\n", sum/count, max, count}'
Apache Bench (ab)
ab -n 100 -c 10 http://localhost:3000/api/endpoint
ab -n 100 -c 10 -p data.json -T application/json http://localhost:3000/api/endpoint
wrk (modern load testing)
wrk -t4 -c100 -d10s http://localhost:3000/api/endpoint
wrk -t4 -c100 -d10s -s post.lua http://localhost:3000/api/endpoint
wrk.method = "POST"
wrk.body = '{"key": "value"}'
wrk.headers["Content-Type"] = "application/json"
request = function()
local id = math.random(1, 10000)
local path = "/api/users/" .. id
return wrk.format("GET", path)
end
Autocannon (Node.js load testing)
npx autocannon -c 100 -d 10 http://localhost:3000/api/endpoint
npx autocannon -c 100 -d 10 -m POST -b '{"key":"value"}' -H 'Content-Type=application/json' http://localhost:3000/api/endpoint
Database Query Performance
EXPLAIN analysis
psql -c "EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM orders WHERE user_id = 123;"
mysql -e "EXPLAIN SELECT * FROM orders WHERE user_id = 123;" mydb
sqlite3 mydb.sqlite "EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 123;"
Slow query detection
psql -c "
SELECT schemaname, relname, seq_scan, seq_tup_read,
idx_scan, idx_tup_fetch,
seq_tup_read / GREATEST(seq_scan, 1) AS avg_rows_per_scan
FROM pg_stat_user_tables
WHERE seq_scan > 100 AND seq_tup_read / GREATEST(seq_scan, 1) > 1000
ORDER BY seq_tup_read DESC
LIMIT 10;
"
Memory Leak Detection Patterns
Node.js
const v8 = require('v8');
function checkMemory() {
const heap = v8.getHeapStatistics();
const usage = process.memoryUsage();
return {
heapUsedMB: (usage.heapUsed / 1024 / 1024).toFixed(1),
heapTotalMB: (usage.heapTotal / 1024 / 1024).toFixed(1),
rssMB: (usage.rss / 1024 / 1024).toFixed(1),
externalMB: (usage.external / 1024 / 1024).toFixed(1),
arrayBuffersMB: (usage.arrayBuffers / 1024 / 1024).toFixed(1),
};
}
let baseline = process.memoryUsage().heapUsed;
setInterval(() => {
const current = process.memoryUsage().heapUsed;
const growthMB = (current - baseline) / / ;
(growthMB > ) {
.();
.(());
}
}, );
Common leak patterns
Node.js:
- Event listeners not removed (emitter.on without emitter.off)
- Closures capturing large objects in long-lived scopes
- Global caches without eviction (Map/Set that only grows)
- Unresolved promises accumulating
Python:
- Circular references (use weakref for caches)
- Global lists/dicts that grow unbounded
- File handles not closed (use context managers)
- C extension objects not properly freed
Go:
- Goroutine leaks (goroutine started, never returns)
- Forgotten channel listeners
- Unclosed HTTP response bodies
- Global maps that grow forever
Performance Comparison Script
#!/bin/bash
CMD="${1:?Usage: perf-compare.sh <command> [runs]}"
RUNS="${2:-10}"
echo "Benchmarking: $CMD"
echo "Runs: $RUNS"
echo ""
times=()
for i in $(seq 1 "$RUNS"); do
start=$(date +%s%N)
eval "$CMD" > /dev/null 2>&1
end=$(date +%s%N)
elapsed=$(echo "scale=3; ($end - $start) / 1000000" | bc)
times+=("$elapsed")
printf " Run %2d: %sms\n" "$i" "$elapsed"
done
echo ""
printf '%s\n' "${times[@]}" | awk '{
sum += $1
sumsq += $1 * $1
if (NR == 1 || $1 < min) min = $1
if (NR == 1 || $1 > max) max = $1
count++
} END {
avg = sum / count
stddev = sqrt(sumsq/count - avg*avg)
printf "Results: avg=%.1fms min=%.1fms max=%.1fms stddev=%.1fms (n=%d)\n", avg, min, max, stddev, count
}'
Tips
- Profile before optimizing. Guessing where bottlenecks are is wrong more often than right. Measure first.
- Optimize the hot path. Flame graphs show you exactly which functions consume the most time. A 10% improvement in a function that takes 80% of CPU time is worth more than a 50% improvement in one that takes 2%.
- Memory and CPU are different problems. A memory leak can exist in fast code. A CPU bottleneck can exist in code with stable memory. Profile both independently.
- Benchmark under realistic conditions. Microbenchmarks (empty loops, single-function timing) can be misleading due to JIT optimization, caching, and branch prediction. Use realistic data and workloads.
- p99 matters more than average. An API with 50ms average but 2s p99 has a tail latency problem. Always look at percentiles, not just averages.
- Load test before shipping.
ab, wrk, or autocannon for 60 seconds at expected peak traffic reveals problems that unit tests never will.
- GC pauses are real. In Node.js, Python, Go, and Java, garbage collection can cause latency spikes. If flame graphs show significant GC time, reduce allocation pressure (reuse objects, use object pools, avoid unnecessary copies).
- Database queries are usually the bottleneck. Before optimizing application code, run
EXPLAIN on your slowest queries. An index can turn a 2-second query into 2ms.