SwadhyAI Notes

All System Design notes

System Design Production Engineering

Performance Benchmarking Tools

A pull request “optimizes” JSON parsing. The author pastes a one-shot time run from a laptop on battery. Reviewers approve. Production P99 barely moves—because that was not a .

Benchmarking tools exist to make performance claims falsifiable: and harnesses, profilers and , and with a that respects the .

Honest measurement

1

Name the question

Function ops/sec? Handler P99? Encode MB/s?

2

Discard cold JIT and cache effects—or report them.

3

Enough iters; show error bars.

4

History + threshold, not hero demos.

What actually happens

A harness generates work (calls a function, hits an HTTP path, runs a query). Timers record latency or count completed ops for . After , steady-state samples feed a summary: mean, percentiles, and error. A profiler samples stacks to build a when you need the “why.”

Load tests ask “does the service meet an SLO under production-like intensity?” Benchmarks ask “is this implementation faster on this defined micro or macro workload?” Both measure performance; they answer different questions and use different tools.

How to choose a benchmarking approach

1. How big is the unit? Algorithm or codec → . Service surface or query plan → . Whole-system capacity → load/stress testing instead.

2. Do you need history? One-off laptop demos do not catch regressions next month. Prefer on stable hardware when the path is hot.

3. What decision will the number change? If it will not block a merge or guide a design choice, do not spend a week polishing the harness.

Simple rule of thumb

Never trust a performance claim without the workload definition, warmup policy, machine class, and a noise estimate. A single time ./app on a noisy laptop is storytelling, not measurement.

Tool landscape

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart TB
  Q[Performance question] --> M{Unit size?}
  M -->|function / codec| Micro[Micro harnesses]
  M -->|handler / query| Macro[Macro HTTP/RPC clients]
  M -->|fleet capacity| Load[Load / stress tests]
  Micro --> JMH[JMH / Criterion / testing.B / BenchmarkDotNet]
  Macro --> HTTP[wrk / hey / vegeta / k6 scenarios]
  JMH --> Stat[Scores + error estimates]
  HTTP --> Stat
  Stat --> Gate[Regression gate vs baseline]
  Gate -->|delta unexplained| Prof[Profiler + flame graph]
CategoryExamplesBest for
JVM microJMHAvoiding dead-code elimination & JIT lies
Systems languagesCriterion (Rust), Go testing.B, Google BenchmarkAllocator-aware micro benches with stats
.NETBenchmarkDotNetRich diagnostics and exportable reports
HTTP macrowrk, hey, vegeta, fortioHandler throughput/latency on a fixture
Scripted scenariosk6, Gatling (bench mode)Multi-step paths with thresholds
Profilingperf, async-profiler, pprof, py-spyExplaining CPU/wall regressions

Microbenchmarks without self-deception

Compilers delete unused work. JITs optimize the benchmark loop differently from production call sites. GC pauses land randomly. Mature micro harnesses force blackhole consumption, control forks/threads, and report error—use them instead of hand-rolled timers.

plaintext
JMH-style checklist (language-agnostic)
  [ ] Work result is consumed (no DCE)
  [ ] Warmup iterations discarded
  [ ] Measurement iterations ≥ noise needs
  [ ] Payload sizes match production distribution
  [ ] Allocation rate reported when GC-sensitive
  [ ] Score + error (or CI) published, not a single float
  [ ] Same JDK/runtime flags as production hot path

Anti-patterns
  - Printing inside the timed region
  - Timing only the first call (cold start as “proof”)
  - Comparing debug builds to optimized baselines
  - Claiming end-to-end wins from a 50ns micro delta

Macro HTTP/RPC benches

Tools like wrk and hey open many connections and report latency histograms and . They are excellent for comparing two builds of the same handler on the same box—and terrible as a substitute for a production-mix load test.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    participant Bench as Bench client
    participant SUT as Build under test
    participant Fix as Fixed fixture data
    participant Store as Result store

    Bench->>Bench: Warmup N seconds
    loop Measurement window
        Bench->>SUT: Concurrent requests
        SUT->>Fix: Deterministic reads/writes
        SUT-->>Bench: Status + latency
    end
    Bench->>Store: Histogram, RPS, build SHA
    Store->>Store: Compare to baseline + noise floor
    alt Regression beyond gate
        Store-->>Bench: Fail CI / open flamegraph task
    else Within noise
        Store-->>Bench: Pass
    end

Keep fixed

  • Machine class, CPU governor, background jobs
  • Payload fixture and concurrency
  • Runtime flags and dependency versions

Call out explicitly

  • Cold vs warm caches
  • Local loopback vs real NIC / TLS
  • Whether the DB is real, stubbed, or recorded

Worked example: gating a serializer change

plaintext
Hot path: encode FeedItem → JSON (p99 matters for edge fanout)

1) Micro (JMH / Criterion)
   workload: 1k items, production field set, UTF-8 names
   metric: ns/op and allocs/op
   gate: fail if ns/op worsens >3% beyond measured CV

2) Macro (hey/wrk against /v1/feed on lab box)
   concurrency 64, 60s measure after 20s warmup
   metric: P99 latency and MB/s encode observed via metrics
   gate: P99 +5% vs baseline SHA on same hardware class

3) If gate fails
   capture CPU flame graph; check for unexpected copies
   or charset conversions introduced by the “optimization”

4) Continuous
   nightly job on dedicated runner stores history
   PR job runs fast subset; nightly runs full suite

Noise floor

~2.5% CV

Measured from 20 baseline repeats on the lab host.

PR gate

+3% micro

Above noise; intentional breaks need an owner comment.

Explain tool

flame graph

Opened only when the number moves—not on every PR.

Continuous benchmarking and gates

turns a one-off demo into a trend line. Store build SHA, hardware label, workload id, and raw histograms. A compares against a baseline branch or trailing median, not against the author’s laptop.

Shared CI runners are noisy: noisy neighbors, thermal throttling, and virtualization steal. Either dedicate bench hardware, use multiple repeats with robust statistics, or treat CI benches as coarse smoke and keep golden numbers on a lab machine.

  • Separate “warn” and “fail” thresholds to reduce alarm fatigue.
  • Allow documented intentional regressions (clarity, safety) with a linked ADR.
  • Re-baseline after expected large changes (major runtime upgrade) with a recorded ceremony.

From problem to mechanism

Problem. Performance PRs thrash: some help, some hurt, nobody can tell until users complain.

Naive system. Ad-hoc time commands, no warmup policy, no stored history, profilers only after SEV.

Break it. A “faster” hash ships; micro looks good because the benchmark skipped realistic key lengths; macro P99 on the gateway rises 12% from extra allocations under concurrency.

Mechanism. Define workloads, adopt a real harness, measure , gate on micro+macro where it matters, and use to explain failures.

Failure timeline: the laptop win

t0  Author times encode on laptop: "2× faster"
t1  PR merges; no JMH/Criterion, no CI history
t2  Edge fanout P99 rises 8% (alloc churn under concurrency)
t3  On-call bisects; flame graph shows unexpected copies
t4  Revert; add micro bench + macro gate on lab runner
t5  Re-land with proven ns/op win and flat handler P99

Still fails when…

  • Gates run on noisy multi-tenant CI without repeats.
  • Micro wins are treated as proof of user-visible latency wins.
  • Workloads omit production payload shapes and skew.

Trade-offs

  • Dedicated bench hardware is accurate and costly; shared CI is cheap and noisy.
  • Strict gates protect latency and slow delivery when false positives fire.
  • Deep profiling on every PR is expensive; profile on regression, not always.

Benchmark vs load test vs profiler

ActivityQuestionTypical tools
BenchmarkIs this build faster on workload W?JMH, Criterion, wrk, hey
Load / stressDo we meet SLO at intensity N?k6, Gatling, Locust
ProfileWhere did the time go?perf, pprof, async-profiler

Choosing and explaining performance benchmarking

A complete explanation connects the mechanism to a requirement. Describe what improves when you introduce it, which new failure cases become possible, and how you would know that the trade-off is worthwhile.

Explain the core idea

A benchmark is a controlled comparison: fixed workload, warmup, enough samples, and a noise estimate—not a one-shot laptop timing.

Use microbenches for algorithms and codecs; macro benches for handlers; load tests for fleet SLOs—do not substitute one for another.

Continuous benchmarking plus a regression gate turns performance into a reviewed property of the codebase.

When it belongs

Hot paths change serializers, codecs, caches, or query plans.

Teams argue about “faster” without shared numbers.

Latency regressions keep shipping through code review.

How it fails

Dead-code elimination and missing warmup invent fake speedups.

Noisy CI gates create alert fatigue or get ignored.

Micro wins hide macro regressions under concurrency and allocation pressure.

Trade-offs to weigh

Lab hardware accuracy versus CI convenience.

Strict gates protect users and can block intentional trade-offs without process.

Broader workloads transfer better and cost more to maintain.

Common misunderstandings

Publishing a single float with false precision.

Profiling instead of measuring—or measuring without ever explaining.

Using wrk RPS on localhost as proof the service is production-ready.

Wrap-up

Exercises

  1. Pick the tool. You need to compare two hash functions for a cache key. Micro or macro? Which harness properties matter most?
  2. Set a gate. Noise floor is ±3%. Propose warn and fail thresholds for a P99 macro bench, and an escape hatch for intentional regressions.
  3. Explain a miss. Micro ns/op improved 20%; production P99 worsened. List three hypotheses and which tool validates each.

Topic-specific references

  • JMH — the reference JVM microbenchmark harness and its pitfalls documentation.
  • Brendan Gregg: Flame Graphs — interpreting CPU time after a regression is detected.
  • Criterion.rs — statistical microbenchmarking practices portable as ideas to other languages.

Key takeaway

Benchmarking tools make performance claims falsifiable. Choose micro vs macro for the question, respect warmup and noise, gate on history, and reach for flame graphs to explain—not to replace—the measurement.