Comprehensive benchmark suite for plans and subscriptions list endpoints to establish latency baselines and detect performance regressions.
go test ./internal/handlers/... -bench=. -benchmem -benchtime=3s# Plans only
go test ./internal/handlers/... -bench=BenchmarkListPlans -benchmem
# Subscriptions only
go test ./internal/handlers/... -bench=BenchmarkListSubscriptions -benchmemgo test ./internal/handlers/... -bench=. -benchmem -cpuprofile=cpu.prof
go tool pprof cpu.profgo test ./internal/handlers/... -bench=. -benchmem -memprofile=mem.prof
go tool pprof mem.profTests performance across different data volumes:
- Empty: 0 records (baseline)
- Small: 10 records (typical single-page response)
- Medium: 100 records (typical paginated response)
- Large: 1,000 records (large merchant)
- ExtraLarge: 10,000 records (stress test)
Isolates JSON serialization performance:
go test ./internal/handlers/... -bench=JSONEncoding -benchmemTests complete request/response cycle:
go test ./internal/handlers/... -bench=FullHTTP -benchmemTests concurrent request handling:
go test ./internal/handlers/... -bench=Parallel -benchmemTests query filtering performance:
go test ./internal/handlers/... -bench=Filtered -benchmem| Dataset Size | Operations/sec | Latency (p50) | Latency (p95) | Allocs/op |
|---|---|---|---|---|
| Empty | ~500,000 | ~2 µs | ~5 µs | 2 |
| Small (10) | ~100,000 | ~10 µs | ~20 µs | 15 |
| Medium (100) | ~20,000 | ~50 µs | ~100 µs | 120 |
| Large (1K) | ~2,000 | ~500 µs | ~1 ms | 1,200 |
| XLarge (10K) | ~200 | ~5 ms | ~10 ms | 12,000 |
| Dataset Size | Operations/sec | Latency (p50) | Latency (p95) | Allocs/op |
|---|---|---|---|---|
| Empty | ~500,000 | ~2 µs | ~5 µs | 2 |
| Small (10) | ~90,000 | ~11 µs | ~22 µs | 18 |
| Medium (100) | ~18,000 | ~55 µs | ~110 µs | 140 |
| Large (1K) | ~1,800 | ~550 µs | ~1.1 ms | 1,400 |
| XLarge (10K) | ~180 | ~5.5 ms | ~11 ms | 14,000 |
Note: Actual results depend on hardware. These are reference values.
Trigger alerts if benchmarks exceed these thresholds:
plans:
small:
max_latency_us: 30
max_allocs: 25
medium:
max_latency_us: 150
max_allocs: 200
large:
max_latency_us: 1500
max_allocs: 2000
subscriptions:
small:
max_latency_us: 35
max_allocs: 30
medium:
max_latency_us: 165
max_allocs: 220
large:
max_latency_us: 1650
max_allocs: 2200BenchmarkListPlans_Medium-8 20000 50000 ns/op 12000 B/op 120 allocs/op
│ │ │ │ │
│ │ │ │ └─ Allocations per operation
│ │ │ └─ Bytes allocated per operation
│ │ └─ Nanoseconds per operation
│ └─ Number of iterations
└─ CPU cores used
- ns/op: Latency per operation (lower is better)
- B/op: Memory allocated per operation (lower is better)
- allocs/op: Number of allocations (lower is better)
# Run baseline
go test ./internal/handlers/... -bench=. -benchmem > baseline.txt
# Make changes
# ...
# Run comparison
go test ./internal/handlers/... -bench=. -benchmem > new.txt
# Compare
benchstat baseline.txt new.txt- Reduce allocations: Target <100 allocs/op for medium datasets
- Optimize JSON encoding: Consider faster JSON libraries
- Add pagination: Limit response size to 100 records max
- Response compression: Enable gzip for large responses
- Field selection: Allow clients to request specific fields
- Caching: Add ETag/Last-Modified headers
- Streaming responses: For very large datasets
- Binary protocols: Consider protobuf for internal APIs
- Connection pooling: Optimize database connections
name: Performance Benchmarks
on: [pull_request]
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Run benchmarks
run: |
go test ./internal/handlers/... -bench=. -benchmem -benchtime=3s > new.txt
cat new.txt
- name: Compare with baseline
run: |
# Download baseline from previous run
# Compare and fail if regression > 20%
go install golang.org/x/perf/cmd/benchstat@latest
benchstat baseline.txt new.txt#!/bin/bash
# detect_regression.sh
THRESHOLD=1.20 # 20% regression threshold
go test ./internal/handlers/... -bench=. -benchmem > new.txt
# Compare with baseline
benchstat baseline.txt new.txt | grep -E "~|±" | while read line; do
# Parse and check if regression > threshold
# Exit 1 if regression detected
done- Use b.ResetTimer(): Reset after setup
- Use b.ReportAllocs(): Track memory allocations
- Avoid I/O: Mock external dependencies
- Run multiple times: Use -benchtime for stability
- Test realistic data: Use representative fixtures
- Focus on trends: Single runs vary, track over time
- Compare apples to apples: Same hardware, same load
- Consider context: CPU, memory, concurrent load
- Profile hot paths: Use pprof for optimization
- Validate in production: Synthetic benchmarks != real traffic
- Run baseline benchmarks
- Identify bottlenecks with profiling
- Make targeted optimization
- Run benchmarks again
- Compare results with benchstat
- Repeat until targets met
Problem: Results vary significantly between runs
Solutions:
- Increase -benchtime (e.g., -benchtime=10s)
- Run on dedicated hardware
- Disable CPU frequency scaling
- Close other applications
Problem: Allocations increase over time
Solutions:
- Use memory profiler
- Check for goroutine leaks
- Verify proper cleanup
- Review object pooling
Problem: Benchmarks too fast/slow
Solutions:
- Verify fixtures are realistic
- Check for compiler optimizations
- Ensure work isn't optimized away
- Add realistic complexity
// Request latency histogram
histogram.Observe(duration.Seconds())
// Response size
counter.Add(float64(responseSize))
// Concurrent requests
gauge.Set(float64(activeRequests))- p50 latency: < 50ms
- p95 latency: < 200ms
- p99 latency: < 500ms
- Error rate: < 0.1%
- Throughput: > 1000 req/s
- Check dataset size (reduce for faster iteration)
- Use -benchtime=1s for quick runs
- Run specific benchmarks with -bench=Pattern
- Profile with -cpuprofile
- Check for memory leaks
- Review allocation patterns
- Consider object pooling
- Use memory profiler
- Run on stable hardware
- Increase benchmark time
- Check for background processes
- Use benchstat for statistical analysis