Jump to…
snowinitial commitqoxwzsukwmkx1mo
1#!/usr/bin/env bash
2#
3# Dogfood — load test against a large repository (spec §11, M6).
4#
5# > load test against a large repository (use the Linux kernel or the jj repo
6# > itself)
7#
8# ./scripts/loadtest.sh [URL] [CONCURRENCY] [DURATION_SECONDS]
9#
10# What this measures, and why these three things:
11#
12# * **Cold browse.** Tree and blob pages on a large repository, with the
13# highlight cache empty. This is the worst case the cache exists to remove,
14# and the number to compare against the warm run.
15# * **Warm browse.** The same pages again. If warm is not dramatically faster
16# than cold, the highlight cache is not working, which is the §8 performance
17# mistake spelled out by name.
18# * **The change list.** Spec §4 calls it "the hottest page in the product",
19# and it is the one that reads precomputed stack edges rather than walking
20# the commit graph — so a regression there is a regression in that decision.
21#
22# It drives the *public* HTTP surface with curl, so it measures what a user
23# experiences rather than what a benchmark harness inside the process would.
24
25set -euo pipefail
26
27URL="${1:-https://dogfood.sh}"
28CONCURRENCY="${2:-8}"
29DURATION="${3:-20}"
30
31c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
32c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }
33c_red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }
34
35command -v curl >/dev/null || { c_red "curl is required"; exit 1; }
36
37# ─── pick a target ───────────────────────────────────────────────────────────
38
39# The repository to hammer. Overridable, because "a large repository" is
40# whatever the instance actually has — the point is that it is large, not that
41# it is a particular one.
42REPO_PATH="${DF_LOADTEST_REPO:-}"
43
44# A file inside that repository with a grammar behind it, so the cold/warm
45# comparison actually measures highlighting. A path that does not exist would
46# 404 in microseconds and make the cache look infinitely fast.
47BLOB_PATH="${DF_LOADTEST_BLOB:-/blob/main/README.md}"
48
49if [[ -z "$REPO_PATH" ]]; then
50 c_red "Set DF_LOADTEST_REPO to a repository path, e.g. DF_LOADTEST_REPO=/dogfood/linux"
51 echo
52 echo "Import one first:"
53 echo " jj git clone --colocate https://github.com/jj-vcs/jj /tmp/jj"
54 echo " cd /tmp/jj && jj git remote add dogfood $URL/<owner>/jj.git && jj git push --all"
55 exit 1
56fi
57
58# ─── helpers ─────────────────────────────────────────────────────────────────
59
60# One sample per line: seconds, then status.
61#
62# The newline is part of the same write. Several workers append to one file
63# concurrently, and a write short enough to be atomic keeps their lines from
64# interleaving — which is why this is not `curl … && echo`.
65timed() {
66 curl -s -o /dev/null -w '%{time_total} %{http_code}\n' --max-time 60 "$1"
67}
68
69# Drive one path with N workers for D seconds; print count, error count, and the
70# p50/p95/p99 of the response times.
71#
72# Percentiles rather than a mean: spec §10 says to alert on push latency p99, and
73# a mean hides exactly the tail that matters on a large repository.
74hammer() {
75 local label="$1" path="$2"
76 local tmp
77 tmp="$(mktemp)"
78
79 local deadline=$((SECONDS + DURATION))
80 for _ in $(seq 1 "$CONCURRENCY"); do
81 (
82 while [[ $SECONDS -lt $deadline ]]; do
83 timed "$URL$path" >> "$tmp" || true
84 done
85 ) &
86 done
87 wait
88
89 # Percentiles are computed over *successful* requests only. A 429 returns in
90 # microseconds, so mixing them in would report a rate-limited run as the
91 # fastest one — the opposite of the truth.
92 local total ok throttled failed
93 total="$(wc -l < "$tmp")"
94 ok="$(awk '$2 == 200' "$tmp" | wc -l)"
95 throttled="$(awk '$2 == 429' "$tmp" | wc -l)"
96 failed=$((total - ok - throttled))
97
98 # `asort` is a gawk extension the default Debian `mawk` does not have, so
99 # sorting happens in sort(1) and awk only indexes.
100 awk '$2 == 200 { print $1 * 1000 }' "$tmp" | sort -n | awk \
101 -v label="$label" -v ok="$ok" -v throttled="$throttled" -v failed="$failed" '
102 { t[NR] = $1 }
103 END {
104 n = NR
105 if (n == 0) {
106 printf "%-22s ok=0 throttled=%-5d failed=%-4d (no successful samples)\n",
107 label, throttled, failed
108 exit
109 }
110 i50 = int(n * 0.50); if (i50 < 1) i50 = 1
111 i95 = int(n * 0.95); if (i95 < 1) i95 = 1
112 i99 = int(n * 0.99); if (i99 < 1) i99 = 1
113 printf "%-22s ok=%-6d throttled=%-5d failed=%-4d p50=%7.1fms p95=%7.1fms p99=%7.1fms\n",
114 label, ok, throttled, failed, t[i50], t[i95], t[i99]
115 }
116 '
117
118 rm -f "$tmp"
119}
120
121# ─── run ─────────────────────────────────────────────────────────────────────
122
123echo "Dogfood load test"
124echo " target $URL$REPO_PATH"
125echo " concurrency $CONCURRENCY"
126echo " duration ${DURATION}s per phase"
127echo
128
129# Reachability first, so a typo in the URL fails in one second rather than after
130# four phases of zeros.
131code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$URL$REPO_PATH" || true)"
132[[ "$code" == "200" ]] || { c_red "$URL$REPO_PATH returned $code — is it public?"; exit 1; }
133
134c_dim "phase 1: cold browse (highlight cache may be empty)"
135hammer "tree (cold)" "$REPO_PATH"
136hammer "blob (cold)" "$REPO_PATH$BLOB_PATH"
137
138c_dim "phase 2: warm browse (same pages, cache populated)"
139hammer "tree (warm)" "$REPO_PATH"
140hammer "blob (warm)" "$REPO_PATH$BLOB_PATH"
141
142c_dim "phase 3: the hottest page (spec §4)"
143hammer "change list" "$REPO_PATH/changes"
144hammer "change list (all)" "$REPO_PATH/changes?state=all"
145
146c_dim "phase 4: history and bookmarks"
147hammer "log" "$REPO_PATH/log?limit=100"
148hammer "bookmarks" "$REPO_PATH/bookmarks"
149
150echo
151c_green "done"
152echo
153echo "What to look for:"
154echo " · blob (warm) should be several times faster than blob (cold). If it is"
155echo " not, the highlight cache is not being hit — spec §8 calls that 'the"
156echo " easiest performance mistake to make here'."
157echo " · throttled > 0 is the rate limiter working as designed (spec §9): every"
158echo " request here comes from one address, so at concurrency ${CONCURRENCY} it will"
159echo " engage. Percentiles above are over successful requests only."
160echo " · failed > 0 is a real problem — a timeout or a 5xx."
161echo " · the change list should not degrade with repository size — it reads"
162echo " precomputed stack edges, never the commit graph (spec §4)."

162 lines · Shell