script/loadtest [duration [clients]], or make loadtest, is a benchmark that script/check does not run. It builds the image and vegeta from a pinned commit, then for each scenario starts a new pixad container and a new cmd/loadtest-origin container, an upstream host that answers every path with one generated JPEG: a cached image (hit), a new source image per request (miss), and each new source image asked for by all clients at once (herd). It prints vegeta's report, pixad's peak resident memory and the requests the origin got. The containers share a network in 203.0.113.0/24, as pixad refuses private and local upstream addresses. README.md says how to run and read it; TODO.md records a small baseline. Model: opus-5-5
163 lines
5.1 KiB
Bash
Executable File
163 lines
5.1 KiB
Bash
Executable File
#!/bin/sh
|
|
# script/loadtest: measure pixad's throughput, latency and peak memory.
|
|
#
|
|
# script/loadtest [duration [clients]] (defaults: 10s and 4)
|
|
#
|
|
# A benchmark, not a check: script/check does not run it. It needs Docker
|
|
# and Go. It builds the image with script/docker and builds vegeta, the
|
|
# load tool, from a pinned commit. Each scenario then gets a new pixad
|
|
# container and a new origin container (cmd/loadtest-origin, which answers
|
|
# every path with the same JPEG), and vegeta sends requests for <duration>
|
|
# from <clients> clients at once, each asking for an image resized to
|
|
# 400x300 WebP:
|
|
#
|
|
# hit the same image every time, put in the cache first
|
|
# miss a new source image every time
|
|
# herd each new source image once per client in a row, so that all
|
|
# clients ask for it at the same time
|
|
#
|
|
# For each, it prints vegeta's report, pixad's peak resident memory and
|
|
# how many requests reached the origin. README.md says how to read them.
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
|
|
# vegeta v12.13.0, 2026-10-04
|
|
VEGETA_COMMIT=4b240c3089fa4aa10816542d64a74294d974211f
|
|
|
|
# pixad refuses upstream hosts with private or local addresses, so the
|
|
# containers share a network in 203.0.113.0/24, a range set aside for
|
|
# documentation (RFC 5737) that pixad does not refuse and that is never
|
|
# routed on the internet.
|
|
SUBNET=203.0.113.0/24
|
|
|
|
main() {
|
|
duration="${1:-10s}"
|
|
clients="${2:-4}"
|
|
case "$clients" in
|
|
*[!0-9]* | 0)
|
|
echo "usage: script/loadtest [duration [clients]]" >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
|
|
cd "$ROOT"
|
|
run="pixa-loadtest-$$"
|
|
tmp="$(mktemp -d)"
|
|
trap cleanup EXIT
|
|
trap 'exit 1' HUP INT TERM
|
|
|
|
"$SCRIPT_DIR/docker"
|
|
# The image's ID, so a build elsewhere that moves the tag does not
|
|
# change what a later scenario starts.
|
|
image="$(docker image inspect --format '{{.Id}}' \
|
|
"$("$SCRIPT_DIR/projectname")")"
|
|
|
|
GOBIN="$tmp" go install "github.com/tsenart/vegeta/v12@$VEGETA_COMMIT"
|
|
# The origin runs in a container, so it is built for the Docker host.
|
|
CGO_ENABLED=0 GOOS=linux \
|
|
GOARCH="$(docker version --format '{{.Server.Arch}}')" \
|
|
go build -o "$tmp/loadtest-origin" ./cmd/loadtest-origin
|
|
|
|
docker network create --subnet "$SUBNET" "$run" >/dev/null
|
|
|
|
start_containers
|
|
# Put the image the hit scenario asks for in the cache.
|
|
docker exec "$run-pixad" wget -q -O /dev/null \
|
|
"http://localhost:8080/v1/image/origin/hit.jpg/400x300.webp"
|
|
attack hit hit_targets
|
|
stop_containers
|
|
|
|
start_containers
|
|
attack miss miss_targets
|
|
stop_containers
|
|
|
|
start_containers
|
|
attack herd herd_targets
|
|
stop_containers
|
|
}
|
|
|
|
# start_containers starts a new origin and a new pixad, and waits up to 30
|
|
# seconds for pixad's health check to pass.
|
|
start_containers() {
|
|
docker run -d --name "$run-origin" \
|
|
--network "$run" --network-alias origin \
|
|
-v "$tmp/loadtest-origin:/usr/local/bin/loadtest-origin:ro" \
|
|
--entrypoint /usr/local/bin/loadtest-origin "$image" >/dev/null
|
|
docker run -d --name "$run-pixad" \
|
|
--network "$run" -p 127.0.0.1::8080 --health-interval=1s \
|
|
-e PIXA_SIGNING_KEY="$(head -c 32 /dev/urandom | base64)" \
|
|
-e PIXA_ALLOWLIST_HOSTS=origin -e PIXA_ALLOW_HTTP=true \
|
|
"$image" >/dev/null
|
|
|
|
waited=0
|
|
until [ "$(docker inspect --format '{{.State.Health.Status}}' \
|
|
"$run-pixad")" = healthy ]; do
|
|
if [ "$waited" -ge 30 ]; then
|
|
echo "loadtest: pixad not healthy after 30 seconds; its log:" >&2
|
|
docker logs "$run-pixad" >&2
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
waited=$((waited + 1))
|
|
done
|
|
pixa="http://$(docker port "$run-pixad" 8080/tcp)"
|
|
}
|
|
|
|
stop_containers() {
|
|
docker rm -f "$run-pixad" "$run-origin" >/dev/null
|
|
}
|
|
|
|
# attack <scenario> <targets>: send the requests <targets> prints and
|
|
# report on them.
|
|
attack() {
|
|
echo
|
|
echo "== $1: $clients clients for $duration"
|
|
"$2" | "$tmp/vegeta" attack -lazy -rate 0 -workers "$clients" \
|
|
-max-workers "$clients" -duration "$duration" -max-body 0 |
|
|
"$tmp/vegeta" report
|
|
# pixad is process 1 in its container: the entrypoint execs it.
|
|
echo "pixad peak memory (VmHWM):" \
|
|
"$(docker exec "$run-pixad" awk '/^VmHWM:/ { print $2, $3 }' \
|
|
/proc/1/status)"
|
|
echo "requests to the origin:" \
|
|
"$(docker logs "$run-origin" 2>&1 | grep -c ' request ')"
|
|
}
|
|
|
|
# The targets functions print vegeta targets until vegeta stops reading.
|
|
|
|
hit_targets() {
|
|
while :; do
|
|
echo "GET $pixa/v1/image/origin/hit.jpg/400x300.webp"
|
|
done
|
|
}
|
|
|
|
miss_targets() {
|
|
i=0
|
|
while :; do
|
|
i=$((i + 1))
|
|
echo "GET $pixa/v1/image/origin/miss/$i.jpg/400x300.webp"
|
|
done
|
|
}
|
|
|
|
herd_targets() {
|
|
i=0
|
|
while :; do
|
|
i=$((i + 1))
|
|
n=0
|
|
while [ "$n" -lt "$clients" ]; do
|
|
n=$((n + 1))
|
|
echo "GET $pixa/v1/image/origin/herd/$i.jpg/400x300.webp"
|
|
done
|
|
done
|
|
}
|
|
|
|
cleanup() {
|
|
docker rm -f "$run-pixad" "$run-origin" >/dev/null 2>&1 || :
|
|
docker network rm "$run" >/dev/null 2>&1 || :
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
main "$@"
|