diff --git a/Makefile b/Makefile index 306235f..c7fc847 100644 --- a/Makefile +++ b/Makefile @@ -138,6 +138,11 @@ bench-gc-adaptive: asm-build bench-gc-http: asm-build @bash tests/bench-gc-http.sh +bench-lumbda-www: asm-build + @bash tests/bench-lumbda-www.sh + +bench-www-race: asm-build + @bash tests/bench-www-race.sh bench-all: bench c-bench bench-3way bench-portal bench-portal-cross bench-web bench-rpc-chain bench-proof @echo "═══════════════════════════════════════════════════════════" @echo "All benchmarks complete. Numbers in the whitepaper §6.4," diff --git a/asm/lumbda-gc b/asm/lumbda-gc index cfaf410..8bdd070 100755 Binary files a/asm/lumbda-gc and b/asm/lumbda-gc differ diff --git a/asm/lumbda-gc.o b/asm/lumbda-gc.o index e07506e..94f7516 100644 Binary files a/asm/lumbda-gc.o and b/asm/lumbda-gc.o differ diff --git a/asm/lumbda.s b/asm/lumbda.s index c9b9761..777d306 100644 --- a/asm/lumbda.s +++ b/asm/lumbda.s @@ -788,9 +788,27 @@ heap_alloc: movq %r15, gc_free_list(%rip) addq %rax, %r15 # %r15 now == %r13 .ha_grow_no_pad: + # Chunk size: at least HEAP_SIZE, but large enough for this one + # block. A single big allocation (e.g. file->string on a ~3 MB + # PDF) would otherwise loop forever because each mmap'd 1 MB + # chunk still doesn't fit. Round up (8+%rbx) to HEAP_SIZE + # multiples so small allocs still land in standard-sized chunks. + leaq 8(%rbx), %rcx # required bytes + cmpq $HEAP_SIZE, %rcx + jbe .ha_grow_std + # Oversize: pad up to HEAP_SIZE alignment and use that. + addq $(HEAP_SIZE - 1), %rcx + movq $HEAP_SIZE, %rdx + negq %rdx # rdx = -HEAP_SIZE (low bits = 0-HEAP_SIZE mask) + andq %rdx, %rcx + jmp .ha_grow_do_mmap +.ha_grow_std: + movq $HEAP_SIZE, %rcx +.ha_grow_do_mmap: + movq %rcx, %r12 # stash size across the syscall movq $SYS_MMAP, %rax xorq %rdi, %rdi - movq $HEAP_SIZE, %rsi + movq %r12, %rsi # chunk size movq $3, %rdx movq $0x22, %r10 movq $-1, %r8 @@ -799,7 +817,9 @@ heap_alloc: cmpq $-1, %rax je die_oom movq %rax, %r15 - leaq HEAP_SIZE(%rax), %r13 + addq %r12, %rax # chunk end + movq %rax, %r13 + movq %r15, %rax # restore ptr (we'll use below) movq gc_chunk_count(%rip), %rdx cmpq $GC_MAX_CHUNKS, %rdx jae die_oom diff --git a/examples/http-static-server.lsp b/examples/http-static-server.lsp new file mode 100644 index 0000000..4b4d186 --- /dev/null +++ b/examples/http-static-server.lsp @@ -0,0 +1,124 @@ +;;; http-static-server.lsp — serve lumbda.com's docroot from pure Scheme. +;;; +;;; Runs in any tier that has file->string + the tcp-* family: Python, +;;; C, and both asm builds. The asm-gc build is the target deployment +;;; (~27 KB binary, bounded memory via heap-snapshot per request). +;;; +;;; ./asm/lumbda-gc < examples/http-static-server.lsp +;;; +;;; Serves GET requests under *docroot*. Default docroot is "www" — +;;; run from the repo root so the relative path resolves. + +(define *port* 8080) +(define *docroot* "www") +(define *max-requests* 100000) + +(define *crlf* "\r\n") +(define *crlf-crlf* "\r\n\r\n") + +;;; ── HTTP helpers ──────────────────────────────────────────── + +(define (http-response status ctype body) + (string-append + "HTTP/1.0 " status *crlf* + "Content-Type: " ctype *crlf* + "Content-Length: " (number->string (string-length body)) *crlf* + "Connection: close" *crlf-crlf* + body)) + +(define SPACE 32) +(define DOT 46) +(define (char-at s i) (char->integer (string-ref s i))) + +;;; Pull the URL path out of the request line "GET /path HTTP/1.0\r\n..." +(define (second-token s) + (let ((len (string-length s))) + (let loop1 ((i 0)) + (cond + ((= i len) "/") + ((= (char-at s i) SPACE) + (let loop2 ((j (+ i 1))) + (cond + ((= j len) (substring s (+ i 1) len)) + ((= (char-at s j) SPACE) (substring s (+ i 1) j)) + (else (loop2 (+ j 1)))))) + (else (loop1 (+ i 1))))))) + +;;; ── Path safety ───────────────────────────────────────────── + +;;; Reject any path containing ".." — keeps requests under docroot. +(define (has-dotdot? s) + (let ((n (string-length s))) + (let loop ((i 0)) + (cond + ((>= (+ i 1) n) #f) + ((and (= (char-at s i) DOT) (= (char-at s (+ i 1)) DOT)) #t) + (else (loop (+ i 1))))))) + +;;; ── MIME dispatch ─────────────────────────────────────────── + +(define (ends-with? s suffix) + (let ((ns (string-length s)) (nf (string-length suffix))) + (if (< ns nf) #f + (string=? suffix (substring s (- ns nf) ns))))) + +(define (mime-of path) + (cond + ((ends-with? path ".html") "text/html; charset=utf-8") + ((ends-with? path ".css") "text/css; charset=utf-8") + ((ends-with? path ".pdf") "application/pdf") + ((ends-with? path ".txt") "text/plain; charset=utf-8") + ((ends-with? path ".js") "application/javascript") + ((ends-with? path ".png") "image/png") + ((ends-with? path ".jpg") "image/jpeg") + ((ends-with? path ".svg") "image/svg+xml") + ((ends-with? path ".ico") "image/x-icon") + (else "application/octet-stream"))) + +;;; ── File resolution ───────────────────────────────────────── + +(define (resolve-fs-path url-path) + (cond + ((string=? url-path "/") (string-append *docroot* "/index.html")) + (else (string-append *docroot* url-path)))) + +(define (serve-file url-path) + (cond + ((has-dotdot? url-path) + (http-response "403 Forbidden" "text/plain" "forbidden\n")) + (else + (let ((fs-path (resolve-fs-path url-path))) + (let ((body (file->string fs-path))) + (if body + (http-response "200 OK" (mime-of fs-path) body) + (let ((nf (file->string (string-append *docroot* "/404.html")))) + (http-response "404 Not Found" "text/html; charset=utf-8" + (if nf nf "not found\n"))))))))) + +(define (handle-request req) + (let ((path (second-token req))) + (serve-file path))) + +;;; ── Main loop ─────────────────────────────────────────────── + +(define server (tcp-listen *port*)) + +(define (server-loop n snap) + (if (>= n *max-requests*) + (begin + (display "request cap reached, exiting\n") + (tcp-close server)) + (begin + (let ((client (tcp-accept server))) + (let ((req (tcp-recv client 4096))) + (if (and req (> (string-length req) 0)) + (tcp-send client (handle-request req)) + #f)) + (tcp-close client)) + ;; Rewind per-request allocations on asm; no-op on Python/C. + (heap-restore snap) + (server-loop (+ n 1) snap)))) + +(display "lumbda-www on :") (display *port*) +(display " serving ") (display *docroot*) (newline) +(server-loop 0 (heap-snapshot)) diff --git a/tests/bench-lumbda-www.sh b/tests/bench-lumbda-www.sh new file mode 100755 index 0000000..86f03c8 --- /dev/null +++ b/tests/bench-lumbda-www.sh @@ -0,0 +1,105 @@ +#!/bin/bash +# bench-lumbda-www.sh — serve lumbda.com's docroot from asm-gc +# and bombard it with small-file + large-file requests. Verifies: +# (a) throughput is viable for real traffic, +# (b) peak RSS stays bounded under mixed payloads (2.67 MB PDF +# per request would otherwise blow up without heap-restore), +# (c) PDF bytes round-trip identically across thousands of +# requests (no truncation, no partial writes). +set -u +cd "$(dirname "$0")/.." + +SMALL_N=${SMALL_N:-5000} +LARGE_N=${LARGE_N:-500} +CONCURRENCY=${CONCURRENCY:-8} +BIN=${BIN:-./asm/lumbda-gc} + +declare -a SPAWNED=() +cleanup() { + for pid in "${SPAWNED[@]}"; do kill -9 "$pid" 2>/dev/null; done + sleep 0.2 + pkill -9 -u "$USER" -f 'examples/http-static-server' 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +make -s -C asm all + +ulimit -v 524288 + +printf "\n═══════════════════════════════════════════════════════\n" +printf "lumbda.com under asm-gc: %s small (/) + %s large (/whitepaper.pdf)\n" "$SMALL_N" "$LARGE_N" +printf " concurrency %s\n" "$CONCURRENCY" +printf "═══════════════════════════════════════════════════════\n" + +"$BIN" < examples/http-static-server.lsp >/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"; exit 1 +fi + +# Verify once that the PDF round-trips byte-identically. +curl -s -o /tmp/bench-pdf-check http://localhost:8080/whitepaper.pdf +if cmp /tmp/bench-pdf-check whitepaper/lumbda-whitepaper.pdf >/dev/null 2>&1; then + echo " pdf-integrity: byte-identical ($(wc -c < /tmp/bench-pdf-check) bytes)" +else + echo " pdf-integrity: DIFFERS"; exit 1 +fi +rm -f /tmp/bench-pdf-check + +# Baseline RSS +rss0=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status) +echo " baseline_rss_kb=$rss0" + +# Drive small-file traffic, sample RSS while it runs. +peak=0 +t0=$(date +%s.%N) +( seq 1 "$SMALL_N" | xargs -P "$CONCURRENCY" -I_ \ + curl -s -o /dev/null http://localhost:8080/ ) & +curl_pid=$! +while kill -0 "$curl_pid" 2>/dev/null; do + rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0) + [ "$rss" -gt "$peak" ] && peak=$rss + sleep 0.2 +done +wait "$curl_pid" 2>/dev/null || true +t1=$(date +%s.%N) +small_rps=$(python3 -c "print(f'{$SMALL_N / ((float(\"$t1\") - float(\"$t0\")) if float(\"$t1\") > float(\"$t0\") else 1):.0f}')") +printf " small (/) %s req/s peak_rss_kb=%s\n" "$small_rps" "$peak" + +# Reset peak for large-file phase +peak=0 +t0=$(date +%s.%N) +( seq 1 "$LARGE_N" | xargs -P "$CONCURRENCY" -I_ \ + curl -s -o /dev/null http://localhost:8080/whitepaper.pdf ) & +curl_pid=$! +while kill -0 "$curl_pid" 2>/dev/null; do + rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0) + [ "$rss" -gt "$peak" ] && peak=$rss + sleep 0.2 +done +wait "$curl_pid" 2>/dev/null || true +t1=$(date +%s.%N) +large_rps=$(python3 -c "print(f'{$LARGE_N / ((float(\"$t1\") - float(\"$t0\")) if float(\"$t1\") > float(\"$t0\") else 1):.0f}')") +large_mib=$(python3 -c "print(f'{$LARGE_N * 2669058 / (1024 * 1024) / ((float(\"$t1\") - float(\"$t0\")) if float(\"$t1\") > float(\"$t0\") else 1):.0f}')") +printf " large (/pdf 2.67M) %s req/s %s MiB/s peak_rss_kb=%s\n" "$large_rps" "$large_mib" "$peak" + +# Final RSS before kill +final=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0) +growth=$((final - rss0)) +printf " final_rss_kb=%s growth_kb=%s\n" "$final" "$growth" + +kill -9 "$pid" 2>/dev/null +wait "$pid" 2>/dev/null || true + +if pgrep -u "$USER" -f 'examples/http-static-server' > /dev/null; then + echo "STRAGGLER" >&2; exit 1 +fi diff --git a/tests/bench-www-race.sh b/tests/bench-www-race.sh new file mode 100755 index 0000000..c1a1104 --- /dev/null +++ b/tests/bench-www-race.sh @@ -0,0 +1,108 @@ +#!/bin/bash +# bench-www-race.sh — asm-gc lumbda-www vs caddy file-server on the +# same lumbda.com docroot. Small-request (index.html) and large- +# request (2.67 MB PDF) workloads, identical concurrency, adjacent +# runs. Reports req/s, MiB/s, and peak RSS for each server. +set -u +cd "$(dirname "$0")/.." + +SMALL_N=${SMALL_N:-2000} +LARGE_N=${LARGE_N:-200} +CONCURRENCY=${CONCURRENCY:-8} +LUMBDA_PORT=${LUMBDA_PORT:-8080} +CADDY_PORT=${CADDY_PORT:-8081} +CADDY_BIN=${CADDY_BIN:-/home/fox/git/make_post_sell/caddy} + +declare -a SPAWNED=() +cleanup() { + for pid in "${SPAWNED[@]}"; do kill -9 "$pid" 2>/dev/null; done + sleep 0.2 + pkill -9 -u "$USER" -f 'http-static-server' 2>/dev/null || true + pkill -9 -u "$USER" -f 'caddy file-server' 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +make -s -C asm all + +printf "\n═══════════════════════════════════════════════════════\n" +printf "asm-gc lumbda-www vs caddy — same docroot, same workload\n" +printf " %s small (/), %s large (/whitepaper.pdf 2.67 MiB)\n" "$SMALL_N" "$LARGE_N" +printf " concurrency %s\n" "$CONCURRENCY" +printf "═══════════════════════════════════════════════════════\n" + +# ── start both servers ── +./asm/lumbda-gc < examples/http-static-server.lsp >/dev/null 2>&1 & +LUMBDA_PID=$! +SPAWNED+=("$LUMBDA_PID") + +"$CADDY_BIN" file-server --root www --listen ":$CADDY_PORT" >/dev/null 2>&1 & +CADDY_PID=$! +SPAWNED+=("$CADDY_PID") + +# Wait for both to be ready +for _ in $(seq 1 30); do + a=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$LUMBDA_PORT/" 2>/dev/null || echo 0) + b=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$CADDY_PORT/" 2>/dev/null || echo 0) + [ "$a" = "200" ] && [ "$b" = "200" ] && break + sleep 0.1 +done +kill -0 "$LUMBDA_PID" 2>/dev/null || { echo "lumbda-www failed to start"; exit 1; } +kill -0 "$CADDY_PID" 2>/dev/null || { echo "caddy failed to start"; exit 1; } + +# ── Byte-integrity check (both ways) ── +curl -s "http://localhost:$LUMBDA_PORT/whitepaper.pdf" -o /tmp/a-lumbda.pdf +curl -s "http://localhost:$CADDY_PORT/whitepaper.pdf" -o /tmp/a-caddy.pdf +echo -n " pdf-integrity (lumbda): " +cmp /tmp/a-lumbda.pdf whitepaper/lumbda-whitepaper.pdf >/dev/null 2>&1 && echo "byte-identical" || echo "DIFFERS" +echo -n " pdf-integrity (caddy): " +cmp /tmp/a-caddy.pdf whitepaper/lumbda-whitepaper.pdf >/dev/null 2>&1 && echo "byte-identical" || echo "DIFFERS" +rm -f /tmp/a-lumbda.pdf /tmp/a-caddy.pdf + +# ── benchmark one (pid, port, label, N, path) ── +bench_one() { + local pid="$1" port="$2" label="$3" n="$4" path="$5" + local peak=0 rss t0 t1 + t0=$(date +%s.%N) + ( seq 1 "$n" | xargs -P "$CONCURRENCY" -I_ \ + curl -s -o /dev/null "http://localhost:$port$path" ) & + local curl_pid=$! + while kill -0 "$curl_pid" 2>/dev/null; do + rss=$(awk '/^VmRSS:/ {print $2}' /proc/$pid/status 2>/dev/null || echo 0) + [ "$rss" -gt "$peak" ] && peak=$rss + sleep 0.2 + done + wait "$curl_pid" 2>/dev/null || true + t1=$(date +%s.%N) + local rps=$(python3 -c " +t = float('$t1') - float('$t0') +print(f'{$n / max(t, 1e-6):.0f}') +") + local mibs=0 + if [ "$path" = "/whitepaper.pdf" ]; then + mibs=$(python3 -c " +t = float('$t1') - float('$t0') +print(f'{$n * 2669058 / (1024*1024) / max(t, 1e-6):.0f}') +") + printf " %-20s %6s req/s %4s MiB/s peak_rss_kb=%s\n" "$label" "$rps" "$mibs" "$peak" + else + printf " %-20s %6s req/s peak_rss_kb=%s\n" "$label" "$rps" "$peak" + fi +} + +echo +echo "── small (/) ──" +bench_one "$LUMBDA_PID" "$LUMBDA_PORT" "lumbda-www (asm-gc)" "$SMALL_N" "/" +bench_one "$CADDY_PID" "$CADDY_PORT" "caddy file-server" "$SMALL_N" "/" + +echo +echo "── large (/whitepaper.pdf, 2.67 MiB) ──" +bench_one "$LUMBDA_PID" "$LUMBDA_PORT" "lumbda-www (asm-gc)" "$LARGE_N" "/whitepaper.pdf" +bench_one "$CADDY_PID" "$CADDY_PORT" "caddy file-server" "$LARGE_N" "/whitepaper.pdf" + +echo +echo "── binary sizes ──" +printf " lumbda-gc %10s bytes stripped\n" "$(du -b asm/lumbda-gc | cut -f1)" +printf " caddy %10s bytes (Go, v2.5.1, static)\n" "$(du -b "$CADDY_BIN" | cut -f1)" + +kill -9 "$LUMBDA_PID" "$CADDY_PID" 2>/dev/null +wait "$LUMBDA_PID" "$CADDY_PID" 2>/dev/null || true