Benchmark handoff: redis-thp (for Gemini)
Benchmark handoff: redis-thp (for Gemini)
You are building the harness behind the post “The p99.9 That Only Spiked While We Were Scaling Up”
(collections/_posts/2026-07-20-the-p999-that-only-spiked-while-scaling.md). The post is drafted with
placeholder tokens [[BENCH:*]]; produce the real measurements that replace them, plus the checked-in
evidence, following the conventions already used by benchmarks/pg-stats/ and benchmarks/cache-aside/.
Read benchmarks/pg-stats/README.md first and match its structure, honesty rules, and file layout.
0. ENVIRONMENT CAVEAT — read before anything else
Transparent Huge Pages is a host kernel setting, not a per-container one. A container shares the
host’s THP state via /sys/kernel/mm/transparent_hugepage/enabled. To run the THP-on vs THP-off arms of
this benchmark you must be able to write that file as root on the host kernel.
- Run this on a Linux host you control: a bare-metal Linux box, a cloud VM, or a local Linux VM.
- macOS Docker Desktop will not work for the THP toggle — Redis runs inside the LinuxKit VM whose THP state you can’t reliably set. If you’re on the user’s Mac, either provision a Linux VM (multipass/lima/a cloud instance) or STOP and report that the THP arms can’t be measured here.
- This limitation is not a bug to work around — it is literally the subject of the post (the standard fix needs host access most readers don’t have). If you can only measure the diskless-replication arm and not the THP toggle, that is still a partial, honest result: capture what you can, mark the THP arms as not-run in the README, and say why.
Record in run_metadata.csv: kernel version, the THP enabled and defrag values seen, whether the
host was writable, and the allocator (mem_allocator from INFO — must be jemalloc).
1. The claim you must reproduce (or honestly fail to)
A write-served Redis primary, when a fresh replica pulls a full sync, shows a p99.9 latency spike that p50/p99 hide, and the spike is caused by copy-on-write page faults amplified by THP, not by the fork itself. Specifically:
- Steady state: p50/p99/p99.9 all low and close.
- During a replica full sync with THP=always + disk sync (
repl-diskless-sync no): p99.9 spikes hard (target the ~hundreds-of-ms shape), while p50/p99 barely move. latest_fork_usecis small — the fork is cheap. The spike is post-fork COW, not fork./proc/<redis-pid>/smapsshowsAnonHugePagesdominating RSS (THP is backing the heap).- THP=never + disk sync: the same sync’s p99.9 drops sharply — direct fix.
- THP=always +
repl-diskless-sync yes: p99.9 is softened (shorter fork-child window) without any host change — the fix available when you don’t own the kernel.
If steady vs sync doesn’t diverge in the p99.9, or THP=never doesn’t fix it, preserve that under
results/attempts/ and say so, exactly like pg-stats kept the shapes that didn’t trip the planner.
Workload must be write-heavy on the primary during the sync. COW faults are only paid when the parent writes to shared pages. A read-only load will not reproduce the spike — that’s a good negative control to keep under attempts/.
2. Deliverables (match pg-stats layout)
benchmarks/redis-thp/
├── docker-compose.yml # digest-pinned redis primary + replica; --save "" --appendonly no
├── benchmark.py # loader + steady write load + latency sampler + sync trigger + self-verify
├── requirements.txt # pin redis-py (cache-aside style)
├── README.md # pg-stats shape: env caveat, run steps, results desc, honesty
└── results/
├── latency_percentiles.csv # one row per scenario: p50/p99/p99.9 for steady and during-sync
├── latency_timeline.csv # per-sample latency over time, with the scale-out marker timestamp
├── fork_and_mem.csv # latest_fork_usec, AnonHugePages, sync duration, per scenario
├── run_metadata.csv # machine, kernel, THP state, redis+jemalloc versions, dataset shape
├── latency_doctor_thp_on.txt # raw LATENCY DOCTOR + LATENCY HISTORY output, untouched
├── smaps_thp_on.txt # raw /proc/<pid>/smaps_rollup (or grep AnonHugePages), untouched
├── smaps_thp_off.txt
└── attempts/ # non-reproducing shapes (read-only load, tiny dataset, etc.)
└── <shape-name>/ ...
Scenario matrix (run each; one primary reused, replica re-attached per arm)
| Arm | THP (host) | repl-diskless-sync | Expectation | |—|—|—|—| | A | always | no | p99.9 spike (the bug) | | B | never | no | p99.9 flat (textbook fix) | | C | always | yes | p99.9 softened (managed-cluster fix) | | control | always | no | read-only load → no spike (keep under attempts/) |
benchmark.py requirements
- Deterministic: fixed dataset size, fixed value size, seeded RNG, fixed write rate.
- Load a dataset big enough that the forked child lives long enough to accumulate a visible COW storm (size it up until arm A reproduces; record the size that worked, and keep undersized attempts).
- Drive a steady write workload against the primary and sample round-trip latency continuously into
latency_timeline.csv; compute p50/p99/p99.9 over the steady window and over the sync window. - Trigger the full sync by attaching the replica (
REPLICAOF/REPLICAOF NO ONEto force a fresh FULLRESYNC), and mark the timestamp in the timeline. - Capture per arm:
latest_fork_usec(INFO stats),LATENCY DOCTOR+LATENCY HISTORY fork,AnonHugePagesfrom/proc/<pid>/smaps_rollup, and the full-sync duration. - Toggle THP on the host between arms A/C and B via the script (needs root; if not root, skip B and mark
it). Reset
LATENCY RESETand warm up before each captured window, like pg-stats’ warm-up. - Self-verify and exit non-zero (after writing evidence) if arm A’s sync p99.9 is not materially above its steady p99.9, or if arm B doesn’t flatten it. A story that didn’t happen locally doesn’t ship.
3. Token map — what fills each [[BENCH:*]] in the post
Replace tokens in collections/_posts/2026-07-20-the-p999-that-only-spiked-while-scaling.md from these.
| Token | Source |
|---|---|
[[BENCH:redis_version]] |
run_metadata redis_version |
[[BENCH:kernel]] |
run_metadata kernel (uname -r) |
[[BENCH:dataset_keys]] |
run_metadata key count that reproduced arm A |
[[BENCH:dataset_bytes]] |
dataset footprint (used_memory_human after load) |
[[BENCH:ops_rate]] |
steady write rate driven (ops/sec) |
[[BENCH:p50_steady]] / [[BENCH:p99_steady]] / [[BENCH:p999_steady]] |
latency_percentiles, arm A steady window |
[[BENCH:p50_sync]] / [[BENCH:p99_sync]] / [[BENCH:p999_sync_thp_on]] |
latency_percentiles, arm A sync window |
[[BENCH:fork_ms]] |
latest_fork_usec (arm A) converted to ms — show it’s small |
[[BENCH:latency_doctor]] |
latency_doctor_thp_on.txt (paste the relevant lines verbatim) |
[[BENCH:anon_hugepages]] |
AnonHugePages line from smaps_thp_on.txt |
[[BENCH:p999_sync_thp_off]] |
latency_percentiles, arm B sync window p99.9 |
[[BENCH:p999_diskless]] |
latency_percentiles, arm C sync window p99.9 |
[[BENCH:sync_duration_thp_on]] |
full-sync duration, arm C (diskless) — the shorter window |
[[BENCH:sync_duration_thp_off]] |
full-sync duration, arm B (kept for completeness) |
[[BENCH:failed_shape_note]] |
one honest sentence on what didn’t reproduce (from attempts/), matching pg-stats’ “first 5 million rows behaved perfectly.” If everything reproduced first try, say that. |
Figures. Two <figure class="cache-bench"> blocks carry placeholder geometry:
- Timeline SVG: recompute
p50andp999polylinepointsfromlatency_timeline.csv(x → 90..600, y so peak→30, zero→210). p50 flat and low; p999 jumps at the marker and decays over the sync window. - The three-arm bar figure: set each
--value:NN%from the real p99.9 of each arm relative to arm A (arm A = 100%).
When done: remove the <!-- DRAFT ... --> comment, delete the published: false line, and confirm
grep -n 'BENCH:' <post> is empty.
4. Honesty rules (inherited from pg-stats)
- Numbers from one Linux host you control. README + post both say the mechanism transfers, the ms don’t.
- Keep non-reproducing shapes (read-only control, undersized dataset) under
results/attempts/. - Raw
LATENCY DOCTOR,LATENCY HISTORY, andsmapscaptured untouched. - Digest-pin the redis image. Record kernel, THP state, allocator, versions in run_metadata.
- The self-verification gate must fail the run if arm A didn’t spike or arm B didn’t fix it.
- If the environment can’t toggle THP (section 0), that’s a real outcome — report the partial result, never fake the THP arms.
5. Run it (Gemini invocation)
From the repo root, on a Linux host you control:
gemini "Read benchmarks/redis-thp/HANDOFF.md, heed section 0's environment caveat, and build the full
harness it specifies: docker-compose.yml, benchmark.py, requirements.txt, README.md, and the results/
tree. Run arms A/B/C plus the read-only control, capture real measurements into results/, then fill
every [[BENCH:*]] token in collections/_posts/2026-07-20-the-p999-that-only-spiked-while-scaling.md
using the token map in section 3, recompute both figure geometries from the CSVs, and remove the DRAFT
comment and the published:false line. Preserve non-reproducing shapes under results/attempts/. Do not
fabricate any number — if the host can't toggle THP or a value wasn't measured, say so and mark that arm
not-run rather than inventing it."
Hand back: the populated post, the benchmarks/redis-thp/ tree, and a one-paragraph note on what
reproduced, what didn’t, and whether the THP toggle was available in your environment.