Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1#!/usr/bin/env bash
Matt W2#
Matt W3# Dogfood — load test against a large repository (spec §11, M6).
Matt W4#
Matt W5# > load test against a large repository (use the Linux kernel or the jj repo
Matt W6# > itself)
Matt W7#
Matt W8# ./scripts/loadtest.sh [URL] [CONCURRENCY] [DURATION_SECONDS]
Matt W9#
Matt W10# What this measures, and why these three things:
Matt W11#
Matt W12# * **Cold browse.** Tree and blob pages on a large repository, with the
Matt W13# highlight cache empty. This is the worst case the cache exists to remove,
Matt W14# and the number to compare against the warm run.
Matt W15# * **Warm browse.** The same pages again. If warm is not dramatically faster
Matt W16# than cold, the highlight cache is not working, which is the §8 performance
Matt W17# mistake spelled out by name.
Matt W18# * **The change list.** Spec §4 calls it "the hottest page in the product",
Matt W19# and it is the one that reads precomputed stack edges rather than walking
Matt W20# the commit graph — so a regression there is a regression in that decision.
Matt W21#
Matt W22# It drives the *public* HTTP surface with curl, so it measures what a user
Matt W23# experiences rather than what a benchmark harness inside the process would.
Matt W24
Matt W25set -euo pipefail
Matt W26
Matt W27URL="${1:-https://dogfood.sh}"
Matt W28CONCURRENCY="${2:-8}"
Matt W29DURATION="${3:-20}"
Matt W30
Matt W31c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
Matt W32c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }
Matt W33c_red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
Matt W34
Matt W35command -v curl >/dev/null || { c_red "curl is required"; exit 1; }
Matt W36
Matt W37# ─── pick a target ───────────────────────────────────────────────────────────
Matt W38
Matt W39# The repository to hammer. Overridable, because "a large repository" is
Matt W40# whatever the instance actually has — the point is that it is large, not that
Matt W41# it is a particular one.
Matt W42REPO_PATH="${DF_LOADTEST_REPO:-}"
Matt W43
Matt W44# A file inside that repository with a grammar behind it, so the cold/warm
Matt W45# comparison actually measures highlighting. A path that does not exist would
Matt W46# 404 in microseconds and make the cache look infinitely fast.
Matt W47BLOB_PATH="${DF_LOADTEST_BLOB:-/blob/main/README.md}"
Matt W48
Matt W49if [[ -z "$REPO_PATH" ]]; then
Matt W50 c_red "Set DF_LOADTEST_REPO to a repository path, e.g. DF_LOADTEST_REPO=/dogfood/linux"
Matt W51 echo
Matt W52 echo "Import one first:"
Matt W53 echo " jj git clone --colocate https://github.com/jj-vcs/jj /tmp/jj"
Matt W54 echo " cd /tmp/jj && jj git remote add dogfood $URL/<owner>/jj.git && jj git push --all"
Matt W55 exit 1
Matt W56fi
Matt W57
Matt W58# ─── helpers ─────────────────────────────────────────────────────────────────
Matt W59
Matt W60# One sample per line: seconds, then status.
Matt W61#
Matt W62# The newline is part of the same write. Several workers append to one file
Matt W63# concurrently, and a write short enough to be atomic keeps their lines from
Matt W64# interleaving — which is why this is not `curl … && echo`.
Matt W65timed() {
Matt W66 curl -s -o /dev/null -w '%{time_total} %{http_code}\n' --max-time 60 "$1"
Matt W67}
Matt W68
Matt W69# Drive one path with N workers for D seconds; print count, error count, and the
Matt W70# p50/p95/p99 of the response times.
Matt W71#
Matt W72# Percentiles rather than a mean: spec §10 says to alert on push latency p99, and
Matt W73# a mean hides exactly the tail that matters on a large repository.
Matt W74hammer() {
Matt W75 local label="$1" path="$2"
Matt W76 local tmp
Matt W77 tmp="$(mktemp)"
Matt W78
Matt W79 local deadline=$((SECONDS + DURATION))
Matt W80 for _ in $(seq 1 "$CONCURRENCY"); do
Matt W81 (
Matt W82 while [[ $SECONDS -lt $deadline ]]; do
Matt W83 timed "$URL$path" >> "$tmp" || true
Matt W84 done
Matt W85 ) &
Matt W86 done
Matt W87 wait
Matt W88
Matt W89 # Percentiles are computed over *successful* requests only. A 429 returns in
Matt W90 # microseconds, so mixing them in would report a rate-limited run as the
Matt W91 # fastest one — the opposite of the truth.
Matt W92 local total ok throttled failed
Matt W93 total="$(wc -l < "$tmp")"
Matt W94 ok="$(awk '$2 == 200' "$tmp" | wc -l)"
Matt W95 throttled="$(awk '$2 == 429' "$tmp" | wc -l)"
Matt W96 failed=$((total - ok - throttled))
Matt W97
Matt W98 # `asort` is a gawk extension the default Debian `mawk` does not have, so
Matt W99 # sorting happens in sort(1) and awk only indexes.
Matt W100 awk '$2 == 200 { print $1 * 1000 }' "$tmp" | sort -n | awk \
Matt W101 -v label="$label" -v ok="$ok" -v throttled="$throttled" -v failed="$failed" '
Matt W102 { t[NR] = $1 }
Matt W103 END {
Matt W104 n = NR
Matt W105 if (n == 0) {
Matt W106 printf "%-22s ok=0 throttled=%-5d failed=%-4d (no successful samples)\n",
Matt W107 label, throttled, failed
Matt W108 exit
Matt W109 }
Matt W110 i50 = int(n * 0.50); if (i50 < 1) i50 = 1
Matt W111 i95 = int(n * 0.95); if (i95 < 1) i95 = 1
Matt W112 i99 = int(n * 0.99); if (i99 < 1) i99 = 1
Matt W113 printf "%-22s ok=%-6d throttled=%-5d failed=%-4d p50=%7.1fms p95=%7.1fms p99=%7.1fms\n",
Matt W114 label, ok, throttled, failed, t[i50], t[i95], t[i99]
Matt W115 }
Matt W116 '
Matt W117
Matt W118 rm -f "$tmp"
Matt W119}
Matt W120
Matt W121# ─── run ─────────────────────────────────────────────────────────────────────
Matt W122
Matt W123echo "Dogfood load test"
Matt W124echo " target $URL$REPO_PATH"
Matt W125echo " concurrency $CONCURRENCY"
Matt W126echo " duration ${DURATION}s per phase"
Matt W127echo
Matt W128
Matt W129# Reachability first, so a typo in the URL fails in one second rather than after
Matt W130# four phases of zeros.
Matt W131code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$URL$REPO_PATH" || true)"
Matt W132[[ "$code" == "200" ]] || { c_red "$URL$REPO_PATH returned $code — is it public?"; exit 1; }
Matt W133
Matt W134c_dim "phase 1: cold browse (highlight cache may be empty)"
Matt W135hammer "tree (cold)" "$REPO_PATH"
Matt W136hammer "blob (cold)" "$REPO_PATH$BLOB_PATH"
Matt W137
Matt W138c_dim "phase 2: warm browse (same pages, cache populated)"
Matt W139hammer "tree (warm)" "$REPO_PATH"
Matt W140hammer "blob (warm)" "$REPO_PATH$BLOB_PATH"
Matt W141
Matt W142c_dim "phase 3: the hottest page (spec §4)"
Matt W143hammer "change list" "$REPO_PATH/changes"
Matt W144hammer "change list (all)" "$REPO_PATH/changes?state=all"
Matt W145
Matt W146c_dim "phase 4: history and bookmarks"
Matt W147hammer "log" "$REPO_PATH/log?limit=100"
Matt W148hammer "bookmarks" "$REPO_PATH/bookmarks"
Matt W149
Matt W150echo
Matt W151c_green "done"
Matt W152echo
Matt W153echo "What to look for:"
Matt W154echo " · blob (warm) should be several times faster than blob (cold). If it is"
Matt W155echo " not, the highlight cache is not being hit — spec §8 calls that 'the"
Matt W156echo " easiest performance mistake to make here'."
Matt W157echo " · throttled > 0 is the rate limiter working as designed (spec §9): every"
Matt W158echo " request here comes from one address, so at concurrency ${CONCURRENCY} it will"
Matt W159echo " engage. Percentiles above are over successful requests only."
Matt W160echo " · failed > 0 is a real problem — a timeout or a 5xx."
Matt W161echo " · the change list should not degrade with repository size — it reads"
Matt W162echo " precomputed stack edges, never the commit graph (spec §4)."

162 lines · Shell