Skip to content

Commit 9a8bf21

Browse files
committed
build(benchmark): add bare-metal script with benchmark-mode, preload, and Bun tests
1 parent 944bd10 commit 9a8bf21

2 files changed

Lines changed: 340 additions & 1 deletion

File tree

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: build run test bench lint precompress clean release install commit bump changelog benchmark benchmark-keep benchmark-down
1+
.PHONY: build run test bench lint precompress clean release install commit bump changelog benchmark benchmark-keep benchmark-down benchmark-baremetal
22

33
# Binary output path and name
44
BIN := bin/static-web
@@ -83,3 +83,7 @@ benchmark-keep:
8383
## benchmark-down: tear down any running benchmark containers
8484
benchmark-down:
8585
docker compose -f benchmark/docker-compose.benchmark.yml down --remove-orphans
86+
87+
## benchmark-baremetal: run bare-metal benchmark (static-web production vs Bun, no Docker)
88+
benchmark-baremetal:
89+
@bash benchmark/baremetal.sh

benchmark/baremetal.sh

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
#!/usr/bin/env bash
2+
# =============================================================================
3+
# baremetal.sh — Bare-metal benchmark: static-web vs Bun
4+
#
5+
# Builds static-web from source, benchmarks three configurations on the same
6+
# port one at a time, then prints a ranked comparison. No Docker.
7+
#
8+
# Configurations tested:
9+
# 1. static-web --benchmark-mode (minimal handler, zero overhead)
10+
# 2. static-web --preload --gc-percent 400 (production optimised)
11+
# 3. Bun native static HTML server
12+
#
13+
# Usage:
14+
# ./benchmark/baremetal.sh [OPTIONS]
15+
#
16+
# Options:
17+
# -c <int> Connections (default: 50)
18+
# -n <int> Total requests (default: 100000)
19+
# -d <int> Duration seconds — overrides -n when set
20+
# -p <int> Port to use (default: 8080)
21+
# -r <dir> Root directory (default: ./public)
22+
# -h Show this help
23+
#
24+
# Requirements:
25+
# - go (to build static-web)
26+
# - bun (https://bun.sh)
27+
# - bombardier (https://github.com/codesenberg/bombardier)
28+
# =============================================================================
29+
set -euo pipefail
30+
31+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
33+
RESULTS_DIR="${SCRIPT_DIR}/results"
34+
35+
# ---------- defaults ---------------------------------------------------------
36+
CONNECTIONS=50
37+
REQUESTS=100000
38+
DURATION=""
39+
PORT=8080
40+
ROOT_DIR="./public"
41+
WARMUP_REQUESTS=50000
42+
SETTLE_SECONDS=3 # pause between server start and warmup
43+
44+
# ---------- colours ----------------------------------------------------------
45+
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
46+
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
47+
48+
# ---------- arg parse --------------------------------------------------------
49+
usage() {
50+
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,2\}//'
51+
exit 0
52+
}
53+
54+
while getopts "c:n:d:p:r:h" opt; do
55+
case $opt in
56+
c) CONNECTIONS="$OPTARG" ;;
57+
n) REQUESTS="$OPTARG" ;;
58+
d) DURATION="$OPTARG" ;;
59+
p) PORT="$OPTARG" ;;
60+
r) ROOT_DIR="$OPTARG" ;;
61+
h) usage ;;
62+
*) echo "Unknown option -$OPTARG"; exit 1 ;;
63+
esac
64+
done
65+
66+
# ---------- dependency checks ------------------------------------------------
67+
check_deps() {
68+
local missing=""
69+
command -v go >/dev/null 2>&1 || missing="$missing go"
70+
command -v bun >/dev/null 2>&1 || missing="$missing bun"
71+
command -v bombardier >/dev/null 2>&1 || missing="$missing bombardier"
72+
73+
if [ -n "$missing" ]; then
74+
echo -e "${RED}Missing dependencies:${missing}${RESET}"
75+
echo ""
76+
echo "Install bombardier: brew install bombardier"
77+
echo "Install bun: curl -fsSL https://bun.sh/install | bash"
78+
exit 1
79+
fi
80+
}
81+
82+
# ---------- helpers ----------------------------------------------------------
83+
BIN="/tmp/static-web-baremetal-$$"
84+
SERVER_PID=""
85+
86+
cleanup() {
87+
if [ -n "$SERVER_PID" ] && kill -0 "$SERVER_PID" 2>/dev/null; then
88+
kill "$SERVER_PID" 2>/dev/null
89+
wait "$SERVER_PID" 2>/dev/null || true
90+
fi
91+
rm -f "$BIN"
92+
}
93+
trap cleanup EXIT
94+
95+
wait_for_port() {
96+
local port=$1 max=15 i=0
97+
while ! curl -sf -o /dev/null "http://localhost:${port}/" 2>/dev/null; do
98+
sleep 0.5
99+
i=$((i + 1))
100+
if [ "$i" -ge "$max" ]; then
101+
echo -e " ${RED}TIMEOUT${RESET}"
102+
return 1
103+
fi
104+
done
105+
}
106+
107+
kill_on_port() {
108+
local pids
109+
pids=$(lsof -ti :"$1" 2>/dev/null || true)
110+
if [ -n "$pids" ]; then
111+
echo "$pids" | xargs kill -9 2>/dev/null || true
112+
sleep 1
113+
fi
114+
}
115+
116+
run_bombardier() {
117+
local url=$1
118+
if [ -n "$DURATION" ]; then
119+
bombardier -c "$CONNECTIONS" -d "${DURATION}s" -l --print r "$url" 2>/dev/null
120+
else
121+
bombardier -c "$CONNECTIONS" -n "$REQUESTS" -l --print r "$url" 2>/dev/null
122+
fi
123+
}
124+
125+
parse_rps() { awk '/Reqs\/sec/{print $2; exit}'; }
126+
parse_p50() { awk '/50\%/{print $2; exit}'; }
127+
parse_p99() { awk '/99\%/{print $2; exit}'; }
128+
parse_tp() { awk '/Throughput/{print $2; exit}'; }
129+
130+
# ---------- main -------------------------------------------------------------
131+
main() {
132+
check_deps
133+
134+
mkdir -p "$RESULTS_DIR"
135+
136+
# Resolve root to absolute path
137+
local abs_root
138+
abs_root="$(cd "$PROJECT_ROOT" && cd "$ROOT_DIR" 2>/dev/null && pwd)" || {
139+
echo -e "${RED}Root directory not found: ${ROOT_DIR}${RESET}"
140+
exit 1
141+
}
142+
143+
echo ""
144+
echo -e "${BOLD}╔════════════════════════════════════════════════════════════════════╗${RESET}"
145+
echo -e "${BOLD}║ Bare-Metal Benchmark: static-web vs Bun ║${RESET}"
146+
echo -e "${BOLD}╚════════════════════════════════════════════════════════════════════╝${RESET}"
147+
echo ""
148+
149+
if [ -n "$DURATION" ]; then
150+
echo -e " ${CYAN}Mode: duration ${DURATION}s${RESET}"
151+
else
152+
echo -e " ${CYAN}Mode: ${REQUESTS} requests${RESET}"
153+
fi
154+
echo -e " ${CYAN}Connections: ${CONNECTIONS}${RESET}"
155+
echo -e " ${CYAN}Warmup: ${WARMUP_REQUESTS} requests${RESET}"
156+
echo -e " ${CYAN}Port: ${PORT}${RESET}"
157+
echo -e " ${CYAN}Root: ${abs_root}${RESET}"
158+
echo -e " ${CYAN}Tool: $(bombardier --version 2>&1 | head -1)${RESET}"
159+
echo -e " ${CYAN}Go: $(go version | awk '{print $3}')${RESET}"
160+
echo -e " ${CYAN}Bun: $(bun --version)${RESET}"
161+
echo -e " ${CYAN}Date: $(date -u '+%Y-%m-%d %H:%M:%S UTC')${RESET}"
162+
echo -e " ${CYAN}OS/Arch: $(uname -s)/$(uname -m)${RESET}"
163+
echo ""
164+
165+
# ---- build static-web ----------------------------------------------------
166+
echo -e "${BOLD}→ Building static-web...${RESET}"
167+
(cd "$PROJECT_ROOT" && go build -ldflags="-s -w" -o "$BIN" ./cmd/static-web)
168+
echo -e " ${GREEN}Built: ${BIN}${RESET}"
169+
echo ""
170+
171+
# Make sure port is free
172+
kill_on_port "$PORT"
173+
174+
local URL="http://localhost:${PORT}/index.html"
175+
176+
# Result arrays (indexed: 0=benchmark-mode, 1=preload, 2=bun)
177+
local -a NAMES RPS_ARR P50_ARR P99_ARR TP_ARR
178+
NAMES=("static-web (benchmark-mode)" "static-web (preload+gc400)" "Bun")
179+
180+
# ======================================================================
181+
# Test 1: static-web --benchmark-mode (minimal handler)
182+
# ======================================================================
183+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
184+
echo -e "${BOLD} [ static-web — benchmark mode (minimal handler) ]${RESET}"
185+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
186+
187+
"$BIN" --benchmark-mode --port "$PORT" "$abs_root" &
188+
SERVER_PID=$!
189+
sleep "$SETTLE_SECONDS"
190+
wait_for_port "$PORT"
191+
echo -e " ${GREEN}Server ready (PID ${SERVER_PID})${RESET}"
192+
193+
echo -e " ${DIM}Warming up (${WARMUP_REQUESTS} requests)...${RESET}"
194+
bombardier -c "$CONNECTIONS" -n "$WARMUP_REQUESTS" --print i "$URL" >/dev/null 2>&1
195+
echo -e " ${DIM}Settle (${SETTLE_SECONDS}s)...${RESET}"
196+
sleep "$SETTLE_SECONDS"
197+
198+
echo -e " ${CYAN}Benchmarking...${RESET}"
199+
local raw
200+
raw=$(run_bombardier "$URL" | tee "${RESULTS_DIR}/baremetal-static-web-benchmark.txt")
201+
echo ""
202+
203+
RPS_ARR[0]=$(echo "$raw" | parse_rps)
204+
P50_ARR[0]=$(echo "$raw" | parse_p50)
205+
P99_ARR[0]=$(echo "$raw" | parse_p99)
206+
TP_ARR[0]=$(echo "$raw" | parse_tp)
207+
208+
kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null || true
209+
SERVER_PID=""
210+
sleep 1
211+
kill_on_port "$PORT"
212+
213+
# ======================================================================
214+
# Test 2: static-web --preload --gc-percent 400 (production mode)
215+
# ======================================================================
216+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
217+
echo -e "${BOLD} [ static-web — production: --preload --gc-percent 400 ]${RESET}"
218+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
219+
220+
"$BIN" --quiet --no-compress --preload --gc-percent 400 --port "$PORT" "$abs_root" &
221+
SERVER_PID=$!
222+
sleep "$SETTLE_SECONDS"
223+
wait_for_port "$PORT"
224+
echo -e " ${GREEN}Server ready (PID ${SERVER_PID})${RESET}"
225+
226+
echo -e " ${DIM}Warming up (${WARMUP_REQUESTS} requests)...${RESET}"
227+
bombardier -c "$CONNECTIONS" -n "$WARMUP_REQUESTS" --print i "$URL" >/dev/null 2>&1
228+
echo -e " ${DIM}Settle (${SETTLE_SECONDS}s)...${RESET}"
229+
sleep "$SETTLE_SECONDS"
230+
231+
echo -e " ${CYAN}Benchmarking...${RESET}"
232+
raw=$(run_bombardier "$URL" | tee "${RESULTS_DIR}/baremetal-static-web-preload.txt")
233+
echo ""
234+
235+
RPS_ARR[1]=$(echo "$raw" | parse_rps)
236+
P50_ARR[1]=$(echo "$raw" | parse_p50)
237+
P99_ARR[1]=$(echo "$raw" | parse_p99)
238+
TP_ARR[1]=$(echo "$raw" | parse_tp)
239+
240+
kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null || true
241+
SERVER_PID=""
242+
sleep 1
243+
kill_on_port "$PORT"
244+
245+
# ======================================================================
246+
# Test 3: Bun static serve
247+
# ======================================================================
248+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
249+
echo -e "${BOLD} [ Bun — native static HTML server ]${RESET}"
250+
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
251+
252+
(cd "$abs_root" && bun --port "$PORT" ./index.html) &
253+
SERVER_PID=$!
254+
sleep "$SETTLE_SECONDS"
255+
wait_for_port "$PORT"
256+
echo -e " ${GREEN}Server ready (PID ${SERVER_PID})${RESET}"
257+
258+
echo -e " ${DIM}Warming up (${WARMUP_REQUESTS} requests)...${RESET}"
259+
bombardier -c "$CONNECTIONS" -n "$WARMUP_REQUESTS" --print i "$URL" >/dev/null 2>&1
260+
echo -e " ${DIM}Settle (${SETTLE_SECONDS}s)...${RESET}"
261+
sleep "$SETTLE_SECONDS"
262+
263+
echo -e " ${CYAN}Benchmarking...${RESET}"
264+
raw=$(run_bombardier "$URL" | tee "${RESULTS_DIR}/baremetal-bun.txt")
265+
echo ""
266+
267+
RPS_ARR[2]=$(echo "$raw" | parse_rps)
268+
P50_ARR[2]=$(echo "$raw" | parse_p50)
269+
P99_ARR[2]=$(echo "$raw" | parse_p99)
270+
TP_ARR[2]=$(echo "$raw" | parse_tp)
271+
272+
kill "$SERVER_PID" 2>/dev/null; wait "$SERVER_PID" 2>/dev/null || true
273+
SERVER_PID=""
274+
275+
# ======================================================================
276+
# Rank results (descending by RPS — insertion sort, 3 elements)
277+
# ======================================================================
278+
local -a SORTED_IDX=(0 1 2)
279+
local n=3 i=1
280+
while [ $i -lt $n ]; do
281+
local key_idx=${SORTED_IDX[$i]}
282+
local key_rps=${RPS_ARR[$key_idx]}
283+
local j=$((i - 1))
284+
while [ $j -ge 0 ]; do
285+
local cmp_idx=${SORTED_IDX[$j]}
286+
local cmp_rps=${RPS_ARR[$cmp_idx]}
287+
if awk "BEGIN{exit !($cmp_rps < $key_rps)}" 2>/dev/null; then
288+
SORTED_IDX[$((j + 1))]=${SORTED_IDX[$j]}
289+
j=$((j - 1))
290+
else
291+
break
292+
fi
293+
done
294+
SORTED_IDX[$((j + 1))]=$key_idx
295+
i=$((i + 1))
296+
done
297+
298+
# ======================================================================
299+
# Print results table
300+
# ======================================================================
301+
echo ""
302+
echo -e "${BOLD}╔════════════════════════════════════════════════════════════════════╗${RESET}"
303+
echo -e "${BOLD}║ Bare-Metal Results ║${RESET}"
304+
echo -e "${BOLD}╠════════════════════════════════════════════════════════════════════╣${RESET}"
305+
printf "${BOLD}║ %-4s %-30s %10s %8s %8s ║${RESET}\n" \
306+
"#" "Server" "Req/sec" "p50" "p99"
307+
echo -e "${BOLD}╠════════════════════════════════════════════════════════════════════╣${RESET}"
308+
309+
local rank=1
310+
for idx in "${SORTED_IDX[@]}"; do
311+
local colour medal
312+
if [ "$rank" -eq 1 ]; then
313+
colour="$GREEN"; medal="1st"
314+
elif [ "$rank" -eq 2 ]; then
315+
colour="$YELLOW"; medal="2nd"
316+
else
317+
colour="$RESET"; medal="3rd"
318+
fi
319+
320+
printf "${colour}║ %-4s %-30s %10s %8s %8s ║${RESET}\n" \
321+
"$medal" "${NAMES[$idx]}" "${RPS_ARR[$idx]}" "${P50_ARR[$idx]}" "${P99_ARR[$idx]}"
322+
rank=$((rank + 1))
323+
done
324+
325+
echo -e "${BOLD}╚════════════════════════════════════════════════════════════════════╝${RESET}"
326+
echo ""
327+
echo -e " ${DIM}Throughput:${RESET}"
328+
echo -e " ${DIM} benchmark-mode ${TP_ARR[0]}${RESET}"
329+
echo -e " ${DIM} preload+gc400 ${TP_ARR[1]}${RESET}"
330+
echo -e " ${DIM} Bun ${TP_ARR[2]}${RESET}"
331+
echo -e " ${DIM}Results saved to: ${RESULTS_DIR}/baremetal-*.txt${RESET}"
332+
echo ""
333+
}
334+
335+
main "$@"

0 commit comments

Comments
 (0)