lumbda/tests/bench-gc-http.sh
russell@unturf.com f7352b51b0 rename: uncommonlisp -> lumbda throughout the repo
Historical internal name "uncommonlisp" retired in favor of the
public name "lumbda" ahead of lumbda.com going live. Scope of
this commit:

Source files renamed:
  uncommonlisp.py                     -> lumbda.py
  asm/uncommonlisp.s                  -> asm/lumbda.s
  c/uncommonlisp.h                    -> c/lumbda.h
  whitepaper/uncommonlisp-whitepaper  -> whitepaper/lumbda-whitepaper (.rst + .pdf)

Binaries renamed (tracked ones; c/ was always gitignored):
  asm/uncommonlisp, asm/uncommonlisp-gc, asm/uncommonlisp.o,
  asm/uncommonlisp-gc.o                -> asm/lumbda(-gc)(.o)
  c/.gitignore                          -> ignores lumbda

Internal string updates (sed pass ordered longest-first):
  asm/uncommonlisp -> asm/lumbda
  c/uncommonlisp   -> c/lumbda
  uncommonlisp.py  -> lumbda.py
  UNCOMMONLISP_BIN -> LUMBDA_BIN (asm/test.sh env var)
  "uncommonlisp> " -> "lumbda> " (asm REPL prompt baked into binary)
  UNCOMMONLISP     -> LUMBDA (macros, comments)
  uncommonlisp     -> lumbda (prose)

Binary portal magic updated:
  "ULPORTAL" -> "LUMBDAB1"   # "Lumbda Binary v1"
Old portal files are not backward-compatible — this is a deliberate
break since it's the rename moment. S-expression portals already
carry their own ";; lumbda-portal v1" header and remain cleanly
versioned.

WHITEPAPER.pdf / WHITEPAPER.rst symlinks repointed to the renamed
files. Makefile's whitepaper target targets lumbda-whitepaper.pdf.

Not changed (intentional, separate phases):
  - Filesystem directory /home/fox/git/uncommonlisp itself
    (fox renames locally and the gitlab repo URL in a follow-up)
  - tests.py hardcoded cwd=/home/fox/git/uncommonlisp
    (matches the current on-disk location; will flip when the
    directory rename ships)
  - Git history (immutable; old commits still say uncommonlisp,
    which is correct — that's what they were)

Verified:
  137 asm no-GC + 137 asm GC + 571 Python + 83 C + 189 shared
  functional tests all pass under the new names.
  bench-gc-http (2000 req): all 4 cells behave as expected
  (cells 1/2 flat, 3 leaks, 4 bounded at 1 chunk).
  Python REPL, C REPL, asm REPL all start cleanly.
2026-04-19 10:20:11 -04:00

104 lines
4.3 KiB
Bash
Executable file

#!/bin/bash
# bench-gc-http.sh — validate the asm naive GC under sustained HTTP
# load. Starts each server, hits it with N requests, samples peak
# RSS. Expected: server with GC holds memory flat under the
# no-arena workload; server without GC leaks monotonically.
#
# Four configs compared:
# 1. asm no-GC + http-server.lsp (bounded via heap-snapshot)
# 2. asm GC + http-server.lsp (bounded via snapshot + GC backstop)
# 3. asm no-GC + http-server-noarena.lsp (leaks — baseline failure)
# 4. asm GC + http-server-noarena.lsp (bounded via GC alone)
set -u
cd "$(dirname "$0")/.."
REQUESTS=${REQUESTS:-5000}
CONCURRENCY=${CONCURRENCY:-8}
# Kill stragglers on exit. The noarena + no-GC case can balloon
# to gigabytes if the test somehow overshoots REQUESTS; ulimit -v
# on each spawn caps blast radius.
declare -a SPAWNED=()
cleanup() {
local pid
for pid in "${SPAWNED[@]}"; do kill -9 "$pid" 2>/dev/null; done
sleep 0.2
pkill -9 -u "$USER" -f 'examples/http-server' 2>/dev/null || true
}
trap cleanup EXIT INT TERM
make -s -C asm all
printf "\n═══════════════════════════════════════════════════════\n"
printf "asm GC vs no-GC under sustained HTTP load\n"
printf " %s requests, concurrency %s, 1 KB body per request\n" "$REQUESTS" "$CONCURRENCY"
printf "═══════════════════════════════════════════════════════\n"
run_case() {
local label="$1" bin="$2" srv="$3" vcap="$4"
# Fresh port per case so we don't collide with a lingering socket.
local pid peak=0 rss time_s=0 t0 t1
printf "\n── %s ──\n" "$label"
( ulimit -v "$vcap" && exec "$bin" < "$srv" >/dev/null 2>&1 ) &
pid=$!
SPAWNED+=("$pid")
# Wait for port.
for _ in $(seq 1 30); do
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:8080/" 2>/dev/null | grep -q 200; then
break
fi
sleep 0.1
done
if ! kill -0 "$pid" 2>/dev/null; then
echo " SERVER FAILED TO START (ulimit -v $vcap may be too tight)"
return 1
fi
# Baseline RSS
local rss0
rss0=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0)
printf " baseline_rss_kb=%s\n" "$rss0"
# Drive traffic; sample RSS every 200ms.
t0=$(date +%s.%N)
( seq 1 "$REQUESTS" | xargs -P "$CONCURRENCY" -I_ \
curl -s -o /dev/null "http://localhost:8080/bench" ) &
local curl_pid=$!
while kill -0 "$curl_pid" 2>/dev/null; do
if kill -0 "$pid" 2>/dev/null; then
rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0)
[ "$rss" -gt "$peak" ] && peak=$rss
else
echo " SERVER DIED MID-RUN (likely OOM at ulimit -v $vcap)"
kill "$curl_pid" 2>/dev/null
break
fi
sleep 0.2
done
wait "$curl_pid" 2>/dev/null || true
t1=$(date +%s.%N)
# Final RSS right before we kill the server.
local final_rss=0
if kill -0 "$pid" 2>/dev/null; then
final_rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0)
fi
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null || true
time_s=$(python3 -c "print(f'{float(\"$t1\")-float(\"$t0\"):.2f}')")
local rps
rps=$(python3 -c "print(f'{$REQUESTS / ($time_s if $time_s>0 else 1):.0f}')")
printf " time_s=%s req/s=%s peak_rss_kb=%s final_rss_kb=%s growth_kb=%s\n" \
"$time_s" "$rps" "$peak" "$final_rss" "$((final_rss - rss0))"
}
# vcap = ulimit -v in KB. 131072 = 128 MB — enough for one GC chunk
# and the process; tight enough that a leaker will OOM before the
# kernel eats all RAM. Widen if the no-GC noarena case needs to run
# to completion rather than OOM'd.
VCAP=${VCAP:-524288}
run_case "asm no-GC + snapshot loop" "./asm/lumbda" "examples/http-server.lsp" "$VCAP" || true
run_case "asm GC + snapshot loop" "./asm/lumbda-gc" "examples/http-server.lsp" "$VCAP" || true
run_case "asm no-GC + no snapshot" "./asm/lumbda" "examples/http-server-noarena.lsp" "$VCAP" || true
run_case "asm GC + no snapshot" "./asm/lumbda-gc" "examples/http-server-noarena.lsp" "$VCAP" || true
printf "\n"