88e86b3faccd
Rebuild benchmarks as a clean single-root reposi
2 months ago
| 1 | #!/usr/bin/env bash
|
| 2 | set -euo pipefail
|
| 3 |
|
| 4 | # Provision an EC2 box, run the Oak benchmarks on it, download results/,
|
| 5 | # then destroy everything it created (instance, security group, key pair).
|
| 6 | #
|
| 7 | # Usage:
|
| 8 | # scripts/aws_bench.sh [options] [-- <extra bench.py args>]
|
| 9 | #
|
| 10 | # Options:
|
| 11 | # --profile {micro,smoke,standard,large} Benchmark profile (default: smoke)
|
| 12 | # --instance-type TYPE EC2 instance type (default: c6id.2xlarge β has
|
| 13 | # local NVMe, used as TMPDIR so fixture I/O hits
|
| 14 | # instance-store SSD, not EBS)
|
| 15 | # --region REGION AWS region (default: AWS CLI configured region)
|
| 16 | # --disk-gb N Root EBS volume size in GB (default: 60)
|
| 17 | # --timeout-mins N Max minutes to wait for the benchmark (default: 360)
|
| 18 | # --keep Do not destroy the instance afterwards (prints SSH cmd)
|
| 19 | # -- ... Everything after -- is passed through to bench.py
|
| 20 | #
|
| 21 | # Examples:
|
| 22 | # scripts/aws_bench.sh # smoke run
|
| 23 | # scripts/aws_bench.sh --profile standard
|
| 24 | # scripts/aws_bench.sh --profile large -- --skip-diff
|
| 25 | #
|
| 26 | # Requires: aws CLI (authenticated β run `aws login` first), ssh, rsync, curl.
|
| 27 | # Results land in results/aws/<run-id>/ locally.
|
| 28 |
|
| 29 | SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
| 30 | BENCH_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
| 31 |
|
| 32 | PROFILE=smoke
|
| 33 | INSTANCE_TYPE=c6id.2xlarge
|
| 34 | REGION="$(aws configure get region 2>/dev/null || true)"
|
| 35 | DISK_GB=60
|
| 36 | TIMEOUT_MINS=360
|
| 37 | KEEP=0
|
| 38 | BENCH_EXTRA_ARGS=()
|
| 39 |
|
| 40 | while [ $# -gt 0 ]; do
|
| 41 | case "$1" in
|
| 42 | --profile) PROFILE="$2"; shift 2 ;;
|
| 43 | --instance-type) INSTANCE_TYPE="$2"; shift 2 ;;
|
| 44 | --region) REGION="$2"; shift 2 ;;
|
| 45 | --disk-gb) DISK_GB="$2"; shift 2 ;;
|
| 46 | --timeout-mins) TIMEOUT_MINS="$2"; shift 2 ;;
|
| 47 | --keep) KEEP=1; shift ;;
|
| 48 | --) shift; BENCH_EXTRA_ARGS=("$@"); break ;;
|
| 49 | -h|--help) sed -n '4,30p' "$0"; exit 0 ;;
|
| 50 | *) echo "unknown option: $1" >&2; exit 2 ;;
|
| 51 | esac
|
| 52 | done
|
| 53 |
|
| 54 | [ -n "$REGION" ] || { echo "no AWS region configured; pass --region" >&2; exit 2; }
|
| 55 |
|
| 56 | RUN_ID="oak-bench-$(date +%Y%m%d-%H%M%S)"
|
| 57 | STATE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/${RUN_ID}.XXXX")"
|
| 58 | PEM="$STATE_DIR/key.pem"
|
| 59 | SSH_OPTS=(-i "$PEM"
|
| 60 | -o StrictHostKeyChecking=accept-new
|
| 61 | -o UserKnownHostsFile="$STATE_DIR/known_hosts"
|
| 62 | -o ConnectTimeout=10
|
| 63 | -o ServerAliveInterval=15
|
| 64 | -o ServerAliveCountMax=8)
|
| 65 | SSH_USER=ubuntu
|
| 66 |
|
| 67 | log() { printf '[aws-bench] %s\n' "$*" >&2; }
|
| 68 |
|
| 69 | aws_ec2() { aws ec2 --region "$REGION" "$@"; }
|
| 70 |
|
| 71 | INSTANCE_ID=""
|
| 72 | SG_ID=""
|
| 73 | KEY_CREATED=0
|
| 74 |
|
| 75 | cleanup() {
|
| 76 | local rc=$?
|
| 77 | set +e
|
| 78 | if [ "$KEEP" = 1 ] && [ -n "$INSTANCE_ID" ]; then
|
| 79 | log "--keep set: leaving $INSTANCE_ID running"
|
| 80 | log "ssh: ssh -i $PEM $SSH_USER@${PUBLIC_IP:-<ip>}"
|
| 81 | log "destroy later with:"
|
| 82 | log " aws ec2 --region $REGION terminate-instances --instance-ids $INSTANCE_ID"
|
| 83 | log " aws ec2 --region $REGION delete-security-group --group-id $SG_ID"
|
| 84 | log " aws ec2 --region $REGION delete-key-pair --key-name $RUN_ID"
|
| 85 | exit "$rc"
|
| 86 | fi
|
| 87 | if [ -n "$INSTANCE_ID" ]; then
|
| 88 | log "terminating $INSTANCE_ID"
|
| 89 | aws_ec2 terminate-instances --instance-ids "$INSTANCE_ID" >/dev/null
|
| 90 | aws_ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"
|
| 91 | fi
|
| 92 | if [ -n "$SG_ID" ]; then
|
| 93 | # SG deletion can lag the instance's ENI teardown; retry briefly.
|
| 94 | for _ in 1 2 3 4 5 6; do
|
| 95 | aws_ec2 delete-security-group --group-id "$SG_ID" 2>/dev/null && break
|
| 96 | sleep 10
|
| 97 | done
|
| 98 | fi
|
| 99 | [ "$KEY_CREATED" = 1 ] && aws_ec2 delete-key-pair --key-name "$RUN_ID" >/dev/null
|
| 100 | rm -rf "$STATE_DIR"
|
| 101 | exit "$rc"
|
| 102 | }
|
| 103 | trap cleanup EXIT
|
| 104 |
|
| 105 | # --- Preflight -------------------------------------------------------------
|
| 106 | aws sts get-caller-identity >/dev/null 2>&1 || {
|
| 107 | echo "AWS credentials missing/expired β run: aws login" >&2; exit 2; }
|
| 108 |
|
| 109 | log "run id: $RUN_ID region: $REGION type: $INSTANCE_TYPE profile: $PROFILE"
|
| 110 |
|
| 111 | AMI_ID="$(aws --region "$REGION" ssm get-parameter \
|
| 112 | --name /aws/service/canonical/ubuntu/server/24.04/stable/current/amd64/hvm/ebs-gp3/ami-id \
|
| 113 | --query Parameter.Value --output text)"
|
| 114 | log "ubuntu 24.04 ami: $AMI_ID"
|
| 115 |
|
| 116 | MY_IP="$(curl -fsS https://checkip.amazonaws.com)"
|
| 117 |
|
| 118 | # --- Provision -------------------------------------------------------------
|
| 119 | aws_ec2 create-key-pair --key-name "$RUN_ID" --key-type ed25519 \
|
| 120 | --query KeyMaterial --output text > "$PEM"
|
| 121 | chmod 600 "$PEM"
|
| 122 | KEY_CREATED=1
|
| 123 |
|
| 124 | VPC_ID="$(aws_ec2 describe-vpcs --filters Name=isDefault,Values=true \
|
| 125 | --query 'Vpcs[0].VpcId' --output text)"
|
| 126 | [ "$VPC_ID" != "None" ] || { echo "no default VPC in $REGION" >&2; exit 2; }
|
| 127 |
|
| 128 | SG_ID="$(aws_ec2 create-security-group --group-name "$RUN_ID" \
|
| 129 | --description "ephemeral oak benchmark runner" --vpc-id "$VPC_ID" \
|
| 130 | --query GroupId --output text)"
|
| 131 | aws_ec2 authorize-security-group-ingress --group-id "$SG_ID" \
|
| 132 | --protocol tcp --port 22 --cidr "${MY_IP}/32" >/dev/null
|
| 133 |
|
| 134 | INSTANCE_ID="$(aws_ec2 run-instances \
|
| 135 | --image-id "$AMI_ID" \
|
| 136 | --instance-type "$INSTANCE_TYPE" \
|
| 137 | --key-name "$RUN_ID" \
|
| 138 | --security-group-ids "$SG_ID" \
|
| 139 | --block-device-mappings "[{\"DeviceName\":\"/dev/sda1\",\"Ebs\":{\"VolumeSize\":$DISK_GB,\"VolumeType\":\"gp3\",\"DeleteOnTermination\":true}}]" \
|
| 140 | --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=$RUN_ID},{Key=purpose,Value=oak-benchmarks}]" \
|
| 141 | --query 'Instances[0].InstanceId' --output text)"
|
| 142 | log "instance: $INSTANCE_ID (waiting for running state)"
|
| 143 |
|
| 144 | aws_ec2 wait instance-running --instance-ids "$INSTANCE_ID"
|
| 145 | PUBLIC_IP="$(aws_ec2 describe-instances --instance-ids "$INSTANCE_ID" \
|
| 146 | --query 'Reservations[0].Instances[0].PublicIpAddress' --output text)"
|
| 147 | [ "$PUBLIC_IP" != "None" ] || { echo "instance has no public IP" >&2; exit 1; }
|
| 148 | log "public ip: $PUBLIC_IP (waiting for SSH)"
|
| 149 |
|
| 150 | for i in $(seq 1 30); do
|
| 151 | ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" true 2>/dev/null && break
|
| 152 | [ "$i" = 30 ] && { echo "SSH never came up" >&2; exit 1; }
|
| 153 | sleep 10
|
| 154 | done
|
| 155 |
|
| 156 | # --- Upload + setup --------------------------------------------------------
|
| 157 | log "syncing benchmarks repo to instance"
|
| 158 | rsync -az -e "ssh ${SSH_OPTS[*]}" \
|
| 159 | --exclude results/ --exclude workdirs/ --exclude worktrees/ --exclude runs/ \
|
| 160 | --exclude fixtures/ --exclude __pycache__/ --exclude '.git/' --exclude '.oak/' \
|
| 161 | "$BENCH_ROOT/" "$SSH_USER@$PUBLIC_IP:benchmarks/"
|
| 162 |
|
| 163 | log "installing dependencies (git, python3, oak)"
|
| 164 | ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" 'bash -s' <<'SETUP'
|
| 165 | set -euo pipefail
|
| 166 | sudo apt-get update -qq
|
| 167 | sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
|
| 168 | git python3 rsync curl ca-certificates >/dev/null
|
| 169 |
|
| 170 | # Use instance-store NVMe (if this type has one) as the benchmark temp dir,
|
| 171 | # so fixture generation hits local SSD rather than the EBS root volume.
|
| 172 | ROOT_DISK="$(lsblk -no PKNAME "$(findmnt -no SOURCE /)" | head -1)"
|
| 173 | for dev in /dev/nvme*n1; do
|
| 174 | [ -b "$dev" ] || continue
|
| 175 | [ "$(basename "$dev")" = "$ROOT_DISK" ] && continue
|
| 176 | if [ -z "$(lsblk -no MOUNTPOINTS "$dev" | tr -d '[:space:]')" ]; then
|
| 177 | sudo mkfs.ext4 -q -F "$dev"
|
| 178 | sudo mkdir -p /mnt/bench
|
| 179 | sudo mount "$dev" /mnt/bench
|
| 180 | sudo chown "$USER" /mnt/bench
|
| 181 | echo "[setup] instance-store NVMe mounted at /mnt/bench"
|
| 182 | break
|
| 183 | fi
|
| 184 | done
|
| 185 |
|
| 186 | curl -fsSL oak.space/install | sh
|
| 187 | export PATH="$HOME/.oak/bin:$HOME/.local/bin:/usr/local/bin:$PATH"
|
| 188 | echo "[setup] git: $(git --version)"
|
| 189 | echo "[setup] python: $(python3 --version)"
|
| 190 | echo "[setup] oak: $(command -v oak) ($(oak --version 2>&1 | head -1))"
|
| 191 | SETUP
|
| 192 |
|
| 193 | # --- Run benchmark (detached, so an SSH drop doesn't kill a long run) -------
|
| 194 | EXTRA_ARGS_STR=""
|
| 195 | [ ${#BENCH_EXTRA_ARGS[@]} -gt 0 ] && EXTRA_ARGS_STR="$(printf ' %q' "${BENCH_EXTRA_ARGS[@]}")"
|
| 196 |
|
| 197 | log "starting bench.py --profile $PROFILE${EXTRA_ARGS_STR}"
|
| 198 | ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" 'bash -s' <<RUN
|
| 199 | set -euo pipefail
|
| 200 | cat > run_bench.sh <<'INNER'
|
| 201 | #!/usr/bin/env bash
|
| 202 | set -uo pipefail
|
| 203 | export PATH="\$HOME/.oak/bin:\$HOME/.local/bin:/usr/local/bin:\$PATH"
|
| 204 | if mountpoint -q /mnt/bench; then
|
| 205 | export TMPDIR=/mnt/bench/tmp
|
| 206 | mkdir -p "\$TMPDIR"
|
| 207 | fi
|
| 208 | cd "\$HOME/benchmarks"
|
| 209 | python3 scripts/bench.py --profile $PROFILE${EXTRA_ARGS_STR}
|
| 210 | echo \$? > "\$HOME/bench.exit"
|
| 211 | INNER
|
| 212 | chmod +x run_bench.sh
|
| 213 | rm -f bench.exit bench.log
|
| 214 | setsid nohup ./run_bench.sh > bench.log 2>&1 < /dev/null &
|
| 215 | RUN
|
| 216 |
|
| 217 | # Poll until the exit file appears, streaming new log output as it arrives.
|
| 218 | DEADLINE=$(( $(date +%s) + TIMEOUT_MINS * 60 ))
|
| 219 | LOG_OFFSET=0
|
| 220 | BENCH_EXIT=""
|
| 221 | while :; do
|
| 222 | sleep 20
|
| 223 | CHUNK="$(ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" \
|
| 224 | "tail -c +$((LOG_OFFSET + 1)) bench.log 2>/dev/null; \
|
| 225 | [ -f bench.exit ] && printf '\n__BENCH_EXIT__%s' \"\$(cat bench.exit)\"" \
|
| 226 | 2>/dev/null)" || { log "ssh poll failed; retrying"; continue; }
|
| 227 | if [[ "$CHUNK" == *__BENCH_EXIT__* ]]; then
|
| 228 | BENCH_EXIT="${CHUNK##*__BENCH_EXIT__}"
|
| 229 | CHUNK="${CHUNK%$'\n'__BENCH_EXIT__*}"
|
| 230 | fi
|
| 231 | if [ -n "$CHUNK" ]; then
|
| 232 | printf '%s\n' "$CHUNK"
|
| 233 | LOG_OFFSET=$((LOG_OFFSET + $(printf '%s' "$CHUNK" | wc -c) + 1))
|
| 234 | fi
|
| 235 | [ -n "$BENCH_EXIT" ] && break
|
| 236 | if [ "$(date +%s)" -ge "$DEADLINE" ]; then
|
| 237 | echo "benchmark exceeded ${TIMEOUT_MINS}m timeout" >&2
|
| 238 | exit 1
|
| 239 | fi
|
| 240 | done
|
| 241 | log "bench.py exited with code $BENCH_EXIT"
|
| 242 |
|
| 243 | # --- Download results ------------------------------------------------------
|
| 244 | DEST="$BENCH_ROOT/results/aws/$RUN_ID"
|
| 245 | mkdir -p "$DEST"
|
| 246 | rsync -az -e "ssh ${SSH_OPTS[*]}" \
|
| 247 | "$SSH_USER@$PUBLIC_IP:benchmarks/results/" "$DEST/" 2>/dev/null || true
|
| 248 | ssh "${SSH_OPTS[@]}" "$SSH_USER@$PUBLIC_IP" "cat bench.log" > "$DEST/bench.log" || true
|
| 249 |
|
| 250 | log "results downloaded to results/aws/$RUN_ID/"
|
| 251 | if [ -f "$DEST/latest.summary.md" ]; then
|
| 252 | echo
|
| 253 | cat "$DEST/latest.summary.md"
|
| 254 | fi
|
| 255 |
|
| 256 | exit "$BENCH_EXIT"
|