#!/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"