Files
spring-boot-demo/docker-images/scripts/measure.sh
T
asmhatreandClaude Opus 5 644da9e65e Add docker-images: one Spring Boot 4 service packaged nine ways
Companion code for "Dockerizing Spring Boot 4: Layered Jars, Buildpacks,
Distroless and Image Size Benchmarks". Fat jar on JDK and JRE, layered jar
on Debian, Alpine and distroless, jlink, the JDK 25 AOT cache, Paketo
buildpacks and Jib, each measured for size on disk and pushed, rebuild
delta, startup, user and shell. Also PID 1 and signal handling, the jdeps
module gap, AOT cache mismatches and buildpacks memory calculation.
Transcripts in docs/output/, regenerated by scripts/run-all.sh.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01C3TETMrqVUWeFkNtz3Jbo3
2026-09-11 17:11:46 +00:00

79 lines
4.1 KiB
Bash

#!/usr/bin/env bash
# Measure every variant: size on disk, size over the wire, layers, user, shell, packages,
# startup to readiness, and memory at idle. -> docs/output/image-matrix.txt
# Needs a registry at $REGISTRY (crane registry serve --address localhost:5000 is enough).
set -uo pipefail
source "$(dirname "$0")/env.sh"
RUNS="${RUNS:-3}"
PORT=18090
compressed() { # image in registry -> "bytes layers"
crane manifest "$1" | python3 -c 'import json,sys
m=json.load(sys.stdin); L=m["layers"]
print(sum(l["size"] for l in L)+m["config"]["size"], len(L))'
}
packages() { # count OS packages from the image filesystem, without running anything in it
local cid; cid=$(docker create "$1" /x 2>/dev/null)
docker export "$cid" > /tmp/img-fs.tar; docker rm "$cid" > /dev/null
local n=""
if tar -tf /tmp/img-fs.tar var/lib/dpkg/status > /dev/null 2>&1; then
n=$(tar -xOf /tmp/img-fs.tar var/lib/dpkg/status | grep -c '^Package:')
n="$n (dpkg)"
elif [ "$(tar -tf /tmp/img-fs.tar | grep -c '^var/lib/dpkg/status.d/.')" -gt 0 ]; then
# distroless and Paketo "tiny" keep one status file per package (plus .md5sums) in status.d/
n=$(tar -tf /tmp/img-fs.tar | grep -E '^var/lib/dpkg/status.d/[^/]+$' | grep -vc '\.md5sums$')
n="$n (status.d)"
elif tar -tf /tmp/img-fs.tar lib/apk/db/installed > /dev/null 2>&1; then
n=$(tar -xOf /tmp/img-fs.tar lib/apk/db/installed | grep -c '^P:')
n="$n (apk)"
else n="?"; fi
rm -f /tmp/img-fs.tar; echo "$n"
}
startup() { # image -> "ms_to_ready startedIn rss"
local name="measure-$$" t0 t1
docker rm -f "$name" > /dev/null 2>&1
t0=$(date +%s%N)
docker run -d --name "$name" -p "$PORT:8080" "$1" > /dev/null
for _ in $(seq 1 600); do
if [ "$(curl -s -o /dev/null -w '%{http_code}' localhost:$PORT/actuator/health/readiness)" = 200 ]; then break; fi
sleep 0.05
done
t1=$(date +%s%N)
for _ in 1 2 3 4 5; do curl -s -o /dev/null localhost:$PORT/api/items; done
sleep 2
local rss started
rss=$(docker stats --no-stream --format '{{.MemUsage}}' "$name" | awk '{print $1}')
started=$(docker logs "$name" 2>&1 | grep -o 'Started ImagesApplication in [0-9.]* seconds' | awk '{print $4}')
docker rm -f "$name" > /dev/null
echo "$(( (t1 - t0) / 1000000 )) ${started:-?} $rss"
}
{
echo "# Spring Boot 4.1.1 application (webmvc + actuator + validation + prometheus), fat jar $(du -h "$MODULE_DIR/target/app.jar" | cut -f1)"
echo "# Docker Engine $(docker version --format '{{.Server.Version}}'), classic overlay2 store. $RUNS startup runs each, median shown."
echo "# 'on disk' = docker image inspect .Size (uncompressed). 'pushed' = layer blobs + config in the registry (compressed)."
echo
printf '%-20s %9s %9s %7s %-9s %-6s %-16s %9s %9s %9s %s\n' variant "on disk" pushed layers user shell packages "ready ms" "Started" "RSS" "JVM"
for v in "${VARIANTS[@]}"; do
img="sbd/docker-images:$v"
docker tag "$img" "$REPO:$v" && docker push -q "$REPO:$v" > /dev/null
disk=$(docker image inspect -f '{{.Size}}' "$img")
read -r wire layers < <(compressed "$REPO:$v")
user=$(docker image inspect -f '{{.Config.User}}' "$img"); user="${user:-root}"
if docker run --rm --entrypoint /bin/sh "$img" -c true > /dev/null 2>&1; then shell=yes; else shell=no; fi
pkgs=$(packages "$img")
samples=()
for _ in $(seq 1 "$RUNS"); do samples+=("$(startup "$img")"); done
median=$(printf '%s\n' "${samples[@]}" | sort -n | sed -n "$(( (RUNS + 1) / 2 ))p")
read -r ready started rss <<< "$median"
name="measure-$$"; docker run -d --name "$name" -p "$PORT:8080" "$img" > /dev/null
for _ in $(seq 1 600); do curl -s -o /dev/null localhost:$PORT/api/runtime && break; sleep 0.05; done
jvm=$(curl -s localhost:$PORT/api/runtime | python3 -c 'import json,sys; r=json.load(sys.stdin); print(r["java.vendor"].split()[0], r["java.version"])')
docker rm -f "$name" > /dev/null
printf '%-20s %8sM %8sM %7s %-9s %-6s %-16s %9s %9s %9s %s\n' "$v" \
"$(( disk / 1000000 ))" "$(( wire / 1000000 ))" "$layers" "$user" "$shell" "$pkgs" "$ready" "${started}s" "$rss" "$jvm"
done
} | tee "$OUT/image-matrix.txt"