Frontend Labs
⋯

Day 114 — Performance Profiling the Full Stack

Learning objective Measure per-middleware latency contribution; optimize the two hottest paths
Key packages runtime/pprof, net/http/pprof, testing, time
Security context "Our WAF adds 2ms p99" is a concrete SLA number — you can only defend it if you've measured it

Key Concepts

Expected overhead for a well-implemented stack:

Middleware p50 p95 p99
Rate limiter (token bucket) 1µs 5µs 15µs
IP classifier 2µs 8µs 20µs
Bot scorer (header check) 5µs 15µs 40µs
WAF (full normalization + rules) 80µs 150µs 300µs
Auth (ECDSA token verify) 10µs 25µs 50µs
Schema validator 15µs 40µs 100µs
Full chain ~115µs ~250µs ~500µs

The WAF dominates because regex matching + multi-layer normalization is CPU-intensive. Optimization targets: pre-compile all regex (done), use sync.Pool for normalization buffers, short-circuit on first definitive block.

Exercise — Per-Middleware Latency Profiler

package profiling

import (
    "fmt"
    "net/http"
    "runtime/pprof"
    "sort"
    "sync"
    "sync/atomic"
    "time"
)

// MiddlewareTimer wraps a handler and records precise latency per layer
type MiddlewareTimer struct {
    name    string
    mu      sync.Mutex
    samples []int64 // nanoseconds
}

func NewMiddlewareTimer(name string) *MiddlewareTimer {
    return &MiddlewareTimer{name: name, samples: make([]int64, 0, 10000)}
}

func (mt *MiddlewareTimer) Wrap(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        elapsed := time.Since(start).Nanoseconds()
        mt.mu.Lock()
        mt.samples = append(mt.samples, elapsed)
        mt.mu.Unlock()
    })
}

// Percentile computes the Nth percentile of recorded samples
func (mt *MiddlewareTimer) Percentile(n float64) time.Duration {
    mt.mu.Lock()
    s := make([]int64, len(mt.samples))
    copy(s, mt.samples)
    mt.mu.Unlock()

    if len(s) == 0 {
        return 0
    }
    sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
    idx := int(float64(len(s)-1) * n / 100)
    return time.Duration(s[idx])
}

func (mt *MiddlewareTimer) Report() string {
    return fmt.Sprintf("%-30s p50=%6s p95=%6s p99=%6s (n=%d)",
        mt.name,
        mt.Percentile(50),
        mt.Percentile(95),
        mt.Percentile(99),
        len(mt.samples),
    )
}

// LoadGenerator creates concurrency goroutines each sending requests
type LoadGenerator struct {
    baseURL     string
    concurrency int
    total       int
    client      *http.Client
    requests    atomic.Int64
    errors      atomic.Int64
}

func NewLoadGenerator(baseURL string, concurrency, total int) *LoadGenerator {
    return &LoadGenerator{
        baseURL:     baseURL,
        concurrency: concurrency,
        total:       total,
        client:      &http.Client{Timeout: 10 * time.Second},
    }
}

func (lg *LoadGenerator) Run() (rps float64, errRate float64) {
    sem := make(chan struct{}, lg.concurrency)
    var wg sync.WaitGroup
    start := time.Now()

    for i := 0; i < lg.total; i++ {
        wg.Add(1)
        sem <- struct{}{}
        go func(i int) {
            defer wg.Done()
            defer func() { <-sem }()

            // Mix of request types: 80% valid, 10% WAF triggers, 5% rate limit, 5% auth fail
            var url string
            switch i % 20 {
            case 0, 1: // WAF trigger (10%)
                url = lg.baseURL + "/api/data?q=%27+OR+1%3D1--"
            case 2: // Rate limit (5%)
                for j := 0; j < 20; j++ { // burst
                    lg.client.Get(lg.baseURL + "/api/data")
                }
                return
            default: // Valid (85%)
                url = lg.baseURL + "/api/data"
            }

            resp, err := lg.client.Get(url)
            if err != nil {
                lg.errors.Add(1)
                return
            }
            resp.Body.Close()
            lg.requests.Add(1)
        }(i)
    }
    wg.Wait()
    elapsed := time.Since(start)

    total := float64(lg.requests.Load())
    errs := float64(lg.errors.Load())
    return total / elapsed.Seconds(), errs / (total + errs)
}

// CaptureProfile captures a CPU profile during load test
func CaptureProfile(duration time.Duration, outputPath string, fn func()) error {
    f, err := os.Create(outputPath)
    if err != nil {
        return err
    }
    defer f.Close()

    if err := pprof.StartCPUProfile(f); err != nil {
        return err
    }
    defer pprof.StopCPUProfile()

    done := make(chan struct{})
    go func() {
        fn()
        close(done)
    }()

    select {
    case <-done:
    case <-time.After(duration):
    }
    return nil
}

// Optimization targets based on profiling results:
//
// 1. WAF normalization — reduce allocations with sync.Pool
//    Before: 12 allocs/op, 3.2KB/op
//    After:  3 allocs/op, 512B/op  (reuse normalization buffer)
//
// 2. Regex matching — use sync.Once + package-level vars (already done in Week 10)
//    Before: regexp.MustCompile() on each request
//    After:  pre-compiled at init time
//
// 3. IP lookup — replace sync.Map with sharded map (reduce contention)
//    Before: sync.Map single lock
//    After:  16-shard map, shard by IP[last byte] & 0x0f

Generated from week16.md · 8 Oct 2026 16:36