lumbda-www cached: in-memory hash-table cache, 1.64x PDF throughput
examples/http-static-server-cached.lsp — same HTTP server but every
preloaded URL's full HTTP/1.0 response (headers + body) is composed
once at startup and stored in a hash-table, so the per-request
handler is a single hash-table-ref/default. No file->string, no
string-append, no MIME lookup in the hot path.
Config knobs:
*docroot* filesystem root (default "www")
*cache-max* soft cap on cached entries (default 100)
*preload-paths* list of URL paths to pre-fetch at startup
Preload list for lumbda.com: "/", "/style.css", "/whitepaper.pdf",
"/robots.txt", "/404.html" — anything not in the list returns the
cached 404 response (no disk hit). Cache lives pre-snapshot so
heap-restore never reclaims it; RSS stays at the cache size
forever.
Race vs caddy on this laptop (2000 small / 200 large, concurrency 8):
small req/s PDF req/s PDF MiB/s peak RSS
lumbda-www uncached 690 195 495 15.5 MB
lumbda-www cached 686 319 811 7.2 MB
caddy file-server 688 478 1,217 38.9 MB
Cache wins on the PDF: 1.64x faster than uncached, RSS DROPS from
15.5 MB to 7.2 MB because the cached path allocates nothing per
request (all allocation happened pre-snapshot). On small files
already-hot paths mean the cache is a wash — 690 vs 686 is noise.
Caddy still wins 1.5x on the PDF via sendfile(2) zero-copy; we
allocate the 2.67 MB response once at startup and tcp-send it.
Closing the gap further would take a sendfile asm primitive —
separate project. For a minimal static site serving its own
whitepaper, the cached 27 KB asm binary is viable: 319 req/s
and 811 MiB/s with 5x less memory than caddy.
tests/bench-www-race.sh updated to run all three side-by-side
(uncached + cached + caddy) at three ports. Cached server's port
is patched via sed at the entry point so the two lumbda variants
don't collide. PDF byte-integrity checked on all three paths.
This commit is contained in:
parent
dd961d2133
commit
ed90adc451
2 changed files with 206 additions and 25 deletions
157
examples/http-static-server-cached.lsp
Normal file
157
examples/http-static-server-cached.lsp
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
;;; http-static-server-cached.lsp — lumbda.com's docroot served
|
||||
;;; from an in-memory response cache. Every preloaded path's full
|
||||
;;; HTTP/1.0 response (headers + body) is composed once at startup
|
||||
;;; and stored in a hash-table, so the per-request handler is a
|
||||
;;; single hash-table-ref. No file->string, no string-append, no
|
||||
;;; MIME lookup in the hot path.
|
||||
;;;
|
||||
;;; Configuration:
|
||||
;;; *docroot* filesystem root containing the files
|
||||
;;; *cache-max* soft cap on cached entries (default 100)
|
||||
;;; *preload-paths* list of URL paths to pre-fetch at startup
|
||||
;;;
|
||||
;;; Add a file: drop it in *docroot*, add its URL path to
|
||||
;;; *preload-paths*, restart. A server restart is deliberate
|
||||
;;; — cache lives forever across requests.
|
||||
;;;
|
||||
;;; Runs in any tier (Python/C/asm/asm-gc); target deployment is
|
||||
;;; asm-gc (27 KB stripped, bounded RSS).
|
||||
|
||||
(define *port* 8080)
|
||||
(define *docroot* "www")
|
||||
(define *cache-max* 100)
|
||||
(define *max-requests* 1000000)
|
||||
|
||||
(define *crlf* "\r\n")
|
||||
(define *crlf-crlf* "\r\n\r\n")
|
||||
|
||||
;;; ── HTTP / MIME helpers (startup-time only) ─────────────────
|
||||
|
||||
(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 ".svg") "image/svg+xml")
|
||||
((ends-with? path ".png") "image/png")
|
||||
((ends-with? path ".jpg") "image/jpeg")
|
||||
((ends-with? path ".ico") "image/x-icon")
|
||||
(else "application/octet-stream")))
|
||||
|
||||
(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 (url-to-fs url-path)
|
||||
(cond
|
||||
((string=? url-path "/") (string-append *docroot* "/index.html"))
|
||||
(else (string-append *docroot* url-path))))
|
||||
|
||||
;;; ── Cache (hash-table: url-path → full HTTP response) ───────
|
||||
|
||||
(define *cache* (make-hash-table))
|
||||
|
||||
(define (cache-count) (hash-table-size *cache*))
|
||||
|
||||
(define (cache-add! url-path)
|
||||
(if (>= (cache-count) *cache-max*)
|
||||
(begin
|
||||
(display "cache-add!: skipped (cap reached): ") (display url-path) (newline)
|
||||
'capped)
|
||||
(let ((fs-path (url-to-fs url-path)))
|
||||
(let ((body (file->string fs-path)))
|
||||
(cond
|
||||
(body
|
||||
(hash-table-set! *cache* url-path
|
||||
(http-response "200 OK" (mime-of fs-path) body))
|
||||
'cached)
|
||||
(else
|
||||
(display "cache-add!: missing file: ") (display fs-path) (newline)
|
||||
'missing))))))
|
||||
|
||||
;;; Paths to preload. Anything not in this list returns 404.
|
||||
(define *preload-paths*
|
||||
(list "/"
|
||||
"/style.css"
|
||||
"/whitepaper.pdf"
|
||||
"/robots.txt"
|
||||
"/404.html"))
|
||||
|
||||
;;; ── 404 / 403 (built once) ──────────────────────────────────
|
||||
|
||||
(define *resp-404*
|
||||
(let ((body (file->string (string-append *docroot* "/404.html"))))
|
||||
(if body
|
||||
(http-response "404 Not Found" "text/html; charset=utf-8" body)
|
||||
(http-response "404 Not Found" "text/plain" "not found\n"))))
|
||||
|
||||
(define *resp-403*
|
||||
(http-response "403 Forbidden" "text/plain" "forbidden\n"))
|
||||
|
||||
;;; Preload.
|
||||
(define (preload-all paths)
|
||||
(cond
|
||||
((null? paths) 'done)
|
||||
(else
|
||||
(cache-add! (car paths))
|
||||
(preload-all (cdr paths)))))
|
||||
|
||||
(preload-all *preload-paths*)
|
||||
|
||||
;;; ── Per-request handler ────────────────────────────────────
|
||||
|
||||
(define SPACE 32)
|
||||
(define (char-at s i) (char->integer (string-ref s i)))
|
||||
|
||||
(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)))))))
|
||||
|
||||
(define (handle-request req)
|
||||
(let ((path (second-token req)))
|
||||
(hash-table-ref/default *cache* path *resp-404*)))
|
||||
|
||||
;;; ── 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))
|
||||
(heap-restore snap)
|
||||
(server-loop (+ n 1) snap))))
|
||||
|
||||
(display "lumbda-www cached on :") (display *port*)
|
||||
(display " serving ") (display *docroot*)
|
||||
(display " (") (display (cache-count)) (display "/") (display *cache-max*)
|
||||
(display " cached)") (newline)
|
||||
(server-loop 0 (heap-snapshot))
|
||||
|
|
@ -10,6 +10,7 @@ SMALL_N=${SMALL_N:-2000}
|
|||
LARGE_N=${LARGE_N:-200}
|
||||
CONCURRENCY=${CONCURRENCY:-8}
|
||||
LUMBDA_PORT=${LUMBDA_PORT:-8080}
|
||||
LUMBDA_CACHED_PORT=${LUMBDA_CACHED_PORT:-8082}
|
||||
CADDY_PORT=${CADDY_PORT:-8081}
|
||||
CADDY_BIN=${CADDY_BIN:-/home/fox/git/make_post_sell/caddy}
|
||||
|
||||
|
|
@ -25,38 +26,56 @@ 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 "asm-gc lumbda-www (uncached + cached) vs caddy\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 ──
|
||||
# ── start three servers ──
|
||||
|
||||
# 1. lumbda-www uncached (reads file per request)
|
||||
./asm/lumbda-gc < examples/http-static-server.lsp >/dev/null 2>&1 &
|
||||
LUMBDA_PID=$!
|
||||
SPAWNED+=("$LUMBDA_PID")
|
||||
|
||||
# 2. lumbda-www cached (in-memory hash-table of pre-built responses).
|
||||
# Patch *port* to avoid colliding with the uncached server.
|
||||
sed "s|(define \*port\* 8080)|(define *port* $LUMBDA_CACHED_PORT)|" \
|
||||
examples/http-static-server-cached.lsp \
|
||||
| ./asm/lumbda-gc >/dev/null 2>&1 &
|
||||
LUMBDA_CACHED_PID=$!
|
||||
SPAWNED+=("$LUMBDA_CACHED_PID")
|
||||
|
||||
# 3. caddy file-server
|
||||
"$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
|
||||
# Wait for all three to be ready.
|
||||
for _ in $(seq 1 40); 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
|
||||
b=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$LUMBDA_CACHED_PORT/" 2>/dev/null || echo 0)
|
||||
c=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$CADDY_PORT/" 2>/dev/null || echo 0)
|
||||
[ "$a" = "200" ] && [ "$b" = "200" ] && [ "$c" = "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; }
|
||||
kill -0 "$LUMBDA_PID" 2>/dev/null || { echo "lumbda-www (uncached) failed to start"; exit 1; }
|
||||
kill -0 "$LUMBDA_CACHED_PID" 2>/dev/null || { echo "lumbda-www (cached) 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
|
||||
# ── Byte-integrity checks ──
|
||||
curl -s "http://localhost:$LUMBDA_PORT/whitepaper.pdf" -o /tmp/a-lumbda.pdf
|
||||
curl -s "http://localhost:$LUMBDA_CACHED_PORT/whitepaper.pdf" -o /tmp/a-cached.pdf
|
||||
curl -s "http://localhost:$CADDY_PORT/whitepaper.pdf" -o /tmp/a-caddy.pdf
|
||||
for f in /tmp/a-lumbda.pdf /tmp/a-cached.pdf /tmp/a-caddy.pdf; do
|
||||
label=$(basename "$f" .pdf | sed 's/^a-//')
|
||||
if cmp "$f" whitepaper/lumbda-whitepaper.pdf >/dev/null 2>&1; then
|
||||
printf " pdf-integrity (%-7s): byte-identical\n" "$label"
|
||||
else
|
||||
printf " pdf-integrity (%-7s): DIFFERS\n" "$label"
|
||||
fi
|
||||
done
|
||||
rm -f /tmp/a-lumbda.pdf /tmp/a-cached.pdf /tmp/a-caddy.pdf
|
||||
|
||||
# ── benchmark one (pid, port, label, N, path) ──
|
||||
bench_one() {
|
||||
|
|
@ -91,18 +110,23 @@ print(f'{$n * 2669058 / (1024*1024) / max(t, 1e-6):.0f}')
|
|||
|
||||
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" "/"
|
||||
bench_one "$LUMBDA_PID" "$LUMBDA_PORT" "lumbda-www uncached" "$SMALL_N" "/"
|
||||
bench_one "$LUMBDA_CACHED_PID" "$LUMBDA_CACHED_PORT" "lumbda-www cached" "$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"
|
||||
bench_one "$LUMBDA_PID" "$LUMBDA_PORT" "lumbda-www uncached" "$LARGE_N" "/whitepaper.pdf"
|
||||
bench_one "$LUMBDA_CACHED_PID" "$LUMBDA_CACHED_PORT" "lumbda-www cached" "$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)"
|
||||
echo "── binary sizes (stripped) ──"
|
||||
# Strip to temp for fair comparison — asm binaries ship stripped in prod.
|
||||
cp asm/lumbda-gc /tmp/lumbda-gc-s && strip /tmp/lumbda-gc-s
|
||||
printf " lumbda-gc %10s bytes\n" "$(du -b /tmp/lumbda-gc-s | cut -f1)"
|
||||
printf " caddy %10s bytes (Go, v2.5.1, already stripped)\n" "$(du -b "$CADDY_BIN" | cut -f1)"
|
||||
rm -f /tmp/lumbda-gc-s
|
||||
|
||||
kill -9 "$LUMBDA_PID" "$CADDY_PID" 2>/dev/null
|
||||
wait "$LUMBDA_PID" "$CADDY_PID" 2>/dev/null || true
|
||||
kill -9 "$LUMBDA_PID" "$LUMBDA_CACHED_PID" "$CADDY_PID" 2>/dev/null
|
||||
wait "$LUMBDA_PID" "$LUMBDA_CACHED_PID" "$CADDY_PID" 2>/dev/null || true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue