#!/usr/bin/env bash
#
# Dogfood — load test against a large repository (spec §11, M6).
#
#   > load test against a large repository (use the Linux kernel or the jj repo
#   > itself)
#
#   ./scripts/loadtest.sh [URL] [CONCURRENCY] [DURATION_SECONDS]
#
# What this measures, and why these three things:
#
#   * **Cold browse.** Tree and blob pages on a large repository, with the
#     highlight cache empty. This is the worst case the cache exists to remove,
#     and the number to compare against the warm run.
#   * **Warm browse.** The same pages again. If warm is not dramatically faster
#     than cold, the highlight cache is not working, which is the §8 performance
#     mistake spelled out by name.
#   * **The change list.** Spec §4 calls it "the hottest page in the product",
#     and it is the one that reads precomputed stack edges rather than walking
#     the commit graph — so a regression there is a regression in that decision.
#
# It drives the *public* HTTP surface with curl, so it measures what a user
# experiences rather than what a benchmark harness inside the process would.

set -euo pipefail

URL="${1:-https://dogfood.sh}"
CONCURRENCY="${2:-8}"
DURATION="${3:-20}"

c_green() { printf '\033[32m%s\033[0m\n' "$*"; }
c_dim() { printf '\033[2m%s\033[0m\n' "$*"; }
c_red() { printf '\033[31m%s\033[0m\n' "$*" >&2; }

command -v curl >/dev/null || { c_red "curl is required"; exit 1; }

# ─── pick a target ───────────────────────────────────────────────────────────

# The repository to hammer. Overridable, because "a large repository" is
# whatever the instance actually has — the point is that it is large, not that
# it is a particular one.
REPO_PATH="${DF_LOADTEST_REPO:-}"

# A file inside that repository with a grammar behind it, so the cold/warm
# comparison actually measures highlighting. A path that does not exist would
# 404 in microseconds and make the cache look infinitely fast.
BLOB_PATH="${DF_LOADTEST_BLOB:-/blob/main/README.md}"

if [[ -z "$REPO_PATH" ]]; then
    c_red "Set DF_LOADTEST_REPO to a repository path, e.g. DF_LOADTEST_REPO=/dogfood/linux"
    echo
    echo "Import one first:"
    echo "  jj git clone --colocate https://github.com/jj-vcs/jj /tmp/jj"
    echo "  cd /tmp/jj && jj git remote add dogfood $URL/<owner>/jj.git && jj git push --all"
    exit 1
fi

# ─── helpers ─────────────────────────────────────────────────────────────────

# One sample per line: seconds, then status.
#
# The newline is part of the same write. Several workers append to one file
# concurrently, and a write short enough to be atomic keeps their lines from
# interleaving — which is why this is not `curl … && echo`.
timed() {
    curl -s -o /dev/null -w '%{time_total} %{http_code}\n' --max-time 60 "$1"
}

# Drive one path with N workers for D seconds; print count, error count, and the
# p50/p95/p99 of the response times.
#
# Percentiles rather than a mean: spec §10 says to alert on push latency p99, and
# a mean hides exactly the tail that matters on a large repository.
hammer() {
    local label="$1" path="$2"
    local tmp
    tmp="$(mktemp)"

    local deadline=$((SECONDS + DURATION))
    for _ in $(seq 1 "$CONCURRENCY"); do
        (
            while [[ $SECONDS -lt $deadline ]]; do
                timed "$URL$path" >> "$tmp" || true
            done
        ) &
    done
    wait

    # Percentiles are computed over *successful* requests only. A 429 returns in
    # microseconds, so mixing them in would report a rate-limited run as the
    # fastest one — the opposite of the truth.
    local total ok throttled failed
    total="$(wc -l < "$tmp")"
    ok="$(awk '$2 == 200' "$tmp" | wc -l)"
    throttled="$(awk '$2 == 429' "$tmp" | wc -l)"
    failed=$((total - ok - throttled))

    # `asort` is a gawk extension the default Debian `mawk` does not have, so
    # sorting happens in sort(1) and awk only indexes.
    awk '$2 == 200 { print $1 * 1000 }' "$tmp" | sort -n | awk \
        -v label="$label" -v ok="$ok" -v throttled="$throttled" -v failed="$failed" '
        { t[NR] = $1 }
        END {
            n = NR
            if (n == 0) {
                printf "%-22s ok=0 throttled=%-5d failed=%-4d (no successful samples)\n",
                       label, throttled, failed
                exit
            }
            i50 = int(n * 0.50); if (i50 < 1) i50 = 1
            i95 = int(n * 0.95); if (i95 < 1) i95 = 1
            i99 = int(n * 0.99); if (i99 < 1) i99 = 1
            printf "%-22s ok=%-6d throttled=%-5d failed=%-4d p50=%7.1fms p95=%7.1fms p99=%7.1fms\n",
                   label, ok, throttled, failed, t[i50], t[i95], t[i99]
        }
    '

    rm -f "$tmp"
}

# ─── run ─────────────────────────────────────────────────────────────────────

echo "Dogfood load test"
echo "  target      $URL$REPO_PATH"
echo "  concurrency $CONCURRENCY"
echo "  duration    ${DURATION}s per phase"
echo

# Reachability first, so a typo in the URL fails in one second rather than after
# four phases of zeros.
code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$URL$REPO_PATH" || true)"
[[ "$code" == "200" ]] || { c_red "$URL$REPO_PATH returned $code — is it public?"; exit 1; }

c_dim "phase 1: cold browse (highlight cache may be empty)"
hammer "tree (cold)" "$REPO_PATH"
hammer "blob (cold)" "$REPO_PATH$BLOB_PATH"

c_dim "phase 2: warm browse (same pages, cache populated)"
hammer "tree (warm)" "$REPO_PATH"
hammer "blob (warm)" "$REPO_PATH$BLOB_PATH"

c_dim "phase 3: the hottest page (spec §4)"
hammer "change list" "$REPO_PATH/changes"
hammer "change list (all)" "$REPO_PATH/changes?state=all"

c_dim "phase 4: history and bookmarks"
hammer "log" "$REPO_PATH/log?limit=100"
hammer "bookmarks" "$REPO_PATH/bookmarks"

echo
c_green "done"
echo
echo "What to look for:"
echo "  · blob (warm) should be several times faster than blob (cold). If it is"
echo "    not, the highlight cache is not being hit — spec §8 calls that 'the"
echo "    easiest performance mistake to make here'."
echo "  · throttled > 0 is the rate limiter working as designed (spec §9): every"
echo "    request here comes from one address, so at concurrency ${CONCURRENCY} it will"
echo "    engage. Percentiles above are over successful requests only."
echo "  · failed > 0 is a real problem — a timeout or a 5xx."
echo "  · the change list should not degrade with repository size — it reads"
echo "    precomputed stack edges, never the commit graph (spec §4)."
