lumbda-www: sendfile(2) primitive + adaptive preload — matches caddy throughput at 9x less RSS
asm-gc gains (tcp-sendfile socket path) → builtin (90 lines) that issues SYS_SENDFILE(40) in a loop, streaming a file from fd → socket with no bounce through the Lumbda heap. Zero-copy kernel path for large responses. examples/http-static-server-sendfile.lsp (hybrid): small assets (≤ 16 KB) stay inline-cached as full HTTP responses; large assets cache only headers and stream the body via tcp-sendfile. 4-way race on i5-8350U, 100 PDF requests (2.56 MiB), concurrency 8: uncached 159 req/s 406 MiB/s 15.5 MB RSS cached 238 req/s 603 MiB/s 7.2 MB RSS sendfile 480 req/s 1226 MiB/s 4.2 MB RSS caddy 485 req/s 1238 MiB/s 37.1 MB RSS sendfile lands within 2% of caddy on throughput with 9x less peak RSS in a 27 KB binary vs caddy's 38 MB (1400x smaller). examples/http-static-server-adaptive.lsp (learning preload): per-URL hit counter persisted to www.hits every N requests. At boot, ranks and preloads top *cache-max* URLs from the prior run's data (cold-start falls back to a seed list). Cold requests beyond the seed promote on first hit. Drops heap-restore arena pattern since the server mutates persistent state every request; relies on GC build's mark-sweep. tests/bench-www-race.sh: adds sendfile variant on port 8083, auto-sizes PDF byte count from the on-disk whitepaper so a whitepaper rebuild doesn't desync the MiB/s calc. Whitepaper §11.7 "Static File Serving: Cache, Sendfile, and Adaptive Preload" documents the four variants, benchmark table, and the arena-vs-mutation tradeoff. §13 Future Work adds DAG-of-hot-paths predictive preload as the direction for > 1000-resource deployments where frequency-only ranking is too narrow.
This commit is contained in:
parent
ed90adc451
commit
94b29421ed
10 changed files with 1550 additions and 558 deletions
BIN
asm/lumbda
BIN
asm/lumbda
Binary file not shown.
BIN
asm/lumbda-gc
BIN
asm/lumbda-gc
Binary file not shown.
BIN
asm/lumbda-gc.o
BIN
asm/lumbda-gc.o
Binary file not shown.
BIN
asm/lumbda.o
BIN
asm/lumbda.o
Binary file not shown.
115
asm/lumbda.s
115
asm/lumbda.s
|
|
@ -35,6 +35,7 @@
|
|||
.equ SYS_LISTEN,50
|
||||
.equ SYS_SETSOCKOPT,54
|
||||
.equ SYS_EXIT, 60
|
||||
.equ SYS_SENDFILE, 40
|
||||
.equ SYS_CLOCK_GETTIME, 228
|
||||
.equ CLOCK_REALTIME, 0
|
||||
|
||||
|
|
@ -192,15 +193,16 @@
|
|||
.equ BI_HS_HAS, 105
|
||||
.equ BI_HS_SIZE, 106
|
||||
.equ BI_HS_LIST, 107
|
||||
.equ BI_TCPSENDFILE, 108
|
||||
.ifdef GC_NAIVE
|
||||
.equ BI_GC_COLLECT, 108
|
||||
.equ BI_GC_STATS, 109
|
||||
.equ BI_WITH_ARENA, 110
|
||||
.equ BI_ARENA_STATS, 111
|
||||
.equ BI_ARENA_SET_MODE, 112
|
||||
.equ BI_COUNT, 113
|
||||
.equ BI_GC_COLLECT, 109
|
||||
.equ BI_GC_STATS, 110
|
||||
.equ BI_WITH_ARENA, 111
|
||||
.equ BI_ARENA_STATS, 112
|
||||
.equ BI_ARENA_SET_MODE, 113
|
||||
.equ BI_COUNT, 114
|
||||
.else
|
||||
.equ BI_COUNT, 108
|
||||
.equ BI_COUNT, 109
|
||||
.endif
|
||||
|
||||
# ============================================================
|
||||
|
|
@ -312,6 +314,7 @@ bn_tcpconnect: .byte 11; .ascii "tcp-connect"
|
|||
bn_tcprecv: .byte 8; .ascii "tcp-recv"
|
||||
bn_tcpsend: .byte 8; .ascii "tcp-send"
|
||||
bn_tcpclose: .byte 9; .ascii "tcp-close"
|
||||
bn_tcpsendfile: .byte 12; .ascii "tcp-sendfile"
|
||||
bn_heapsnap: .byte 13; .ascii "heap-snapshot"
|
||||
bn_heaprest: .byte 12; .ascii "heap-restore"
|
||||
bn_curtime: .byte 15; .ascii "current-time-ms"
|
||||
|
|
@ -384,6 +387,7 @@ bi_names:
|
|||
.quad bn_htmake, bn_htp, bn_htset, bn_htref, bn_htrefd, bn_htdel
|
||||
.quad bn_htexists, bn_htsize, bn_htkeys, bn_htvals, bn_htalist
|
||||
.quad bn_hsmake, bn_hsp, bn_hsadd, bn_hshas, bn_hssize, bn_hslist
|
||||
.quad bn_tcpsendfile
|
||||
.ifdef GC_NAIVE
|
||||
.quad bn_gccollect, bn_gcstats, bn_witharena, bn_arenastats, bn_arenamode
|
||||
.endif
|
||||
|
|
@ -3427,6 +3431,8 @@ eval_list:
|
|||
je bi_hash_set_size
|
||||
cmpq $BI_HS_LIST, %rax
|
||||
je bi_hash_set_to_list
|
||||
cmpq $BI_TCPSENDFILE, %rax
|
||||
je bi_tcp_sendfile
|
||||
.ifdef GC_NAIVE
|
||||
cmpq $BI_GC_COLLECT, %rax
|
||||
je bi_gc_collect_user
|
||||
|
|
@ -6996,6 +7002,101 @@ bi_tcp_send:
|
|||
movq $VAL_FALSE, %rax
|
||||
RET_VAL
|
||||
|
||||
# bi_tcp_sendfile: (tcp-sendfile socket "path") → int bytes sent or #f
|
||||
# Zero-copy file-to-socket via the Linux sendfile(2) syscall — the
|
||||
# kernel streams bytes from the file's page cache directly into
|
||||
# the socket buffer without ever touching userspace. Opens the
|
||||
# file, sizes it via lseek, loops sendfile until drained, closes.
|
||||
bi_tcp_sendfile:
|
||||
GETARG %rdi # socket
|
||||
call decode_port
|
||||
testq %rax, %rax
|
||||
js .tsf_fail
|
||||
movq %rax, %rbx # socket fd
|
||||
|
||||
GETARG %rdi # path (tagged string)
|
||||
andq $-8, %rdi
|
||||
movq (%rdi), %rcx # string length
|
||||
leaq 8(%rdi), %rdi # bytes
|
||||
|
||||
# Null-terminate the filename on a 256-byte stack slot.
|
||||
subq $256, %rsp
|
||||
movq %rsp, %rsi
|
||||
pushq %rcx
|
||||
.tsf_cp:
|
||||
testq %rcx, %rcx
|
||||
jz .tsf_cp_done
|
||||
movb (%rdi), %al
|
||||
movb %al, (%rsi)
|
||||
incq %rdi
|
||||
incq %rsi
|
||||
decq %rcx
|
||||
jmp .tsf_cp
|
||||
.tsf_cp_done:
|
||||
movb $0, (%rsi)
|
||||
popq %rcx
|
||||
|
||||
# Open file read-only.
|
||||
movq $SYS_OPEN, %rax
|
||||
movq %rsp, %rdi
|
||||
movq $O_RDONLY, %rsi
|
||||
xorq %rdx, %rdx
|
||||
syscall
|
||||
addq $256, %rsp
|
||||
testq %rax, %rax
|
||||
js .tsf_fail
|
||||
movq %rax, %r12 # file fd
|
||||
|
||||
# Size via lseek(fd, 0, SEEK_END).
|
||||
movq $SYS_LSEEK, %rax
|
||||
movq %r12, %rdi
|
||||
xorq %rsi, %rsi
|
||||
movq $SEEK_END, %rdx
|
||||
syscall
|
||||
testq %rax, %rax
|
||||
js .tsf_close_fail
|
||||
movq %rax, %rbp # total size
|
||||
|
||||
# Rewind file position.
|
||||
movq $SYS_LSEEK, %rax
|
||||
movq %r12, %rdi
|
||||
xorq %rsi, %rsi
|
||||
movq $SEEK_SET, %rdx
|
||||
syscall
|
||||
|
||||
# Loop: sendfile until %rcx (remaining) is 0.
|
||||
movq %rbp, %rcx
|
||||
.tsf_loop:
|
||||
testq %rcx, %rcx
|
||||
jz .tsf_ok
|
||||
movq $SYS_SENDFILE, %rax
|
||||
movq %rbx, %rdi # out_fd = socket
|
||||
movq %r12, %rsi # in_fd = file
|
||||
xorq %rdx, %rdx # offset = NULL — use file's current pos
|
||||
movq %rcx, %r10 # count = remaining
|
||||
syscall
|
||||
testq %rax, %rax
|
||||
js .tsf_close_fail # kernel error
|
||||
jz .tsf_ok # EOF before count satisfied
|
||||
subq %rax, %rcx
|
||||
jmp .tsf_loop
|
||||
|
||||
.tsf_ok:
|
||||
movq $SYS_CLOSE, %rax
|
||||
movq %r12, %rdi
|
||||
syscall
|
||||
movq %rbp, %rdi # total bytes sent
|
||||
call make_int
|
||||
RET_VAL
|
||||
|
||||
.tsf_close_fail:
|
||||
movq $SYS_CLOSE, %rax
|
||||
movq %r12, %rdi
|
||||
syscall
|
||||
.tsf_fail:
|
||||
movq $VAL_FALSE, %rax
|
||||
RET_VAL
|
||||
|
||||
# ============================================================
|
||||
# list_reverse: %rdi = list -> %rax = reversed list
|
||||
# ============================================================
|
||||
|
|
|
|||
298
examples/http-static-server-adaptive.lsp
Normal file
298
examples/http-static-server-adaptive.lsp
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
;;; http-static-server-adaptive.lsp — static server that learns which
|
||||
;;; paths matter. A hit counter per URL is updated on every request and
|
||||
;;; periodically flushed to disk (*hits-path*, newline-delimited
|
||||
;;; "url count" records). At startup, if hit data exists from a prior
|
||||
;;; run, the top *cache-max* URLs are preloaded; otherwise we fall
|
||||
;;; back to *seed-paths* (a minimal cold-start set).
|
||||
;;;
|
||||
;;; This is the "~95% of the win" version of predictive preloading
|
||||
;;; for small deployments (<=1000 resources). The DAG-of-hot-paths
|
||||
;;; variant for larger fleets is future work — see whitepaper §Future.
|
||||
;;;
|
||||
;;; Assets above *inline-threshold* bytes are cached as
|
||||
;;; headers-only + fs-path and streamed via tcp-sendfile, same as
|
||||
;;; http-static-server-sendfile.lsp.
|
||||
|
||||
(define *port* 8080)
|
||||
(define *docroot* "www")
|
||||
(define *hits-path* "www.hits")
|
||||
(define *cache-max* 100)
|
||||
(define *max-requests* 1000000)
|
||||
(define *inline-threshold* 16384)
|
||||
(define *hits-save-every* 500)
|
||||
|
||||
(define *crlf* "\r\n")
|
||||
(define *crlf-crlf* "\r\n\r\n")
|
||||
|
||||
;;; Seed paths used on first boot (no hit history yet). Anything
|
||||
;;; requested afterwards gets learned and promoted across restarts.
|
||||
(define *seed-paths*
|
||||
(list "/"
|
||||
"/404.html"))
|
||||
|
||||
;;; ── helpers ─────────────────────────────────────────────────
|
||||
|
||||
(define SPACE 32)
|
||||
(define NEWLINE 10)
|
||||
(define ZERO 48)
|
||||
(define NINE 57)
|
||||
|
||||
(define (char-at s i) (char->integer (string-ref s i)))
|
||||
|
||||
(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 (url-to-fs url-path)
|
||||
(cond
|
||||
((string=? url-path "/") (string-append *docroot* "/index.html"))
|
||||
(else (string-append *docroot* url-path))))
|
||||
|
||||
(define (inline-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 (headers-only status ctype nbytes)
|
||||
(string-append
|
||||
"HTTP/1.0 " status *crlf*
|
||||
"Content-Type: " ctype *crlf*
|
||||
"Content-Length: " (number->string nbytes) *crlf*
|
||||
"Connection: close" *crlf-crlf*))
|
||||
|
||||
;;; ── hit-counter persistence ─────────────────────────────────
|
||||
|
||||
(define *hits* (make-hash-table))
|
||||
|
||||
(define (bump-hit! path)
|
||||
(let ((n (hash-table-ref/default *hits* path 0)))
|
||||
(hash-table-set! *hits* path (+ n 1))))
|
||||
|
||||
;;; Serialize *hits* to "url count\n" lines and write atomically.
|
||||
(define (hits-save)
|
||||
(let loop ((keys (hash-table-keys *hits*)) (acc ""))
|
||||
(cond
|
||||
((null? keys) (write-file *hits-path* acc))
|
||||
(else
|
||||
(let ((k (car keys)))
|
||||
(loop (cdr keys)
|
||||
(string-append acc k " "
|
||||
(number->string
|
||||
(hash-table-ref/default *hits* k 0))
|
||||
"\n")))))))
|
||||
|
||||
;;; Parse "url count\n" records into a list of (url . count) pairs.
|
||||
;;; Defensive — skips malformed lines so a corrupt hits file never
|
||||
;;; takes the server down.
|
||||
(define (parse-int-token s i len)
|
||||
(let loop ((j i) (n 0))
|
||||
(cond
|
||||
((= j len) (cons n j))
|
||||
((and (>= (char-at s j) ZERO) (<= (char-at s j) NINE))
|
||||
(loop (+ j 1) (+ (* n 10) (- (char-at s j) ZERO))))
|
||||
(else (cons n j)))))
|
||||
|
||||
(define (parse-line s start end)
|
||||
;; end is the index of '\n' (or the length). Split on first space.
|
||||
(let loop ((i start))
|
||||
(cond
|
||||
((>= i end) #f)
|
||||
((= (char-at s i) SPACE)
|
||||
(let ((pr (parse-int-token s (+ i 1) end)))
|
||||
(cons (substring s start i) (car pr))))
|
||||
(else (loop (+ i 1))))))
|
||||
|
||||
(define (hits-parse s)
|
||||
(let ((n (string-length s)))
|
||||
(let loop ((i 0) (acc '()))
|
||||
(cond
|
||||
((>= i n) (reverse acc))
|
||||
(else
|
||||
(let find-nl ((j i))
|
||||
(cond
|
||||
((or (= j n) (= (char-at s j) NEWLINE))
|
||||
(let ((rec (parse-line s i j)))
|
||||
(loop (+ j 1) (if rec (cons rec acc) acc))))
|
||||
(else (find-nl (+ j 1))))))))))
|
||||
|
||||
(define (hits-load)
|
||||
(let ((s (file->string *hits-path*)))
|
||||
(if s (hits-parse s) '())))
|
||||
|
||||
;;; Insertion sort by cdr descending. N is cache-max, so O(N²) is fine.
|
||||
(define (insert-desc x xs)
|
||||
(cond
|
||||
((null? xs) (list x))
|
||||
((>= (cdr x) (cdr (car xs))) (cons x xs))
|
||||
(else (cons (car xs) (insert-desc x (cdr xs))))))
|
||||
|
||||
(define (sort-desc pairs)
|
||||
(let loop ((in pairs) (out '()))
|
||||
(cond
|
||||
((null? in) out)
|
||||
(else (loop (cdr in) (insert-desc (car in) out))))))
|
||||
|
||||
(define (take n xs)
|
||||
(cond
|
||||
((or (= n 0) (null? xs)) '())
|
||||
(else (cons (car xs) (take (- n 1) (cdr xs))))))
|
||||
|
||||
;;; ── cache (headers + either body or fs-path) ────────────────
|
||||
|
||||
(define *cache-inline* (make-hash-table))
|
||||
(define *cache-sendfile* (make-hash-table))
|
||||
|
||||
(define (cache-count)
|
||||
(+ (hash-table-size *cache-inline*)
|
||||
(hash-table-size *cache-sendfile*)))
|
||||
|
||||
(define (cache-add! url-path)
|
||||
(if (>= (cache-count) *cache-max*)
|
||||
'capped
|
||||
(let ((fs-path (url-to-fs url-path)))
|
||||
(let ((body (file->string fs-path)))
|
||||
(cond
|
||||
((not body) 'missing)
|
||||
((<= (string-length body) *inline-threshold*)
|
||||
(hash-table-set! *cache-inline* url-path
|
||||
(inline-response "200 OK" (mime-of fs-path) body))
|
||||
'inline)
|
||||
(else
|
||||
(hash-table-set! *cache-sendfile* url-path
|
||||
(cons (headers-only "200 OK" (mime-of fs-path)
|
||||
(string-length body))
|
||||
fs-path))
|
||||
'sendfile))))))
|
||||
|
||||
(define (preload-all paths)
|
||||
(cond
|
||||
((null? paths) 'done)
|
||||
(else (cache-add! (car paths)) (preload-all (cdr paths)))))
|
||||
|
||||
(define (preload-top-n ranked n)
|
||||
;; ranked is ((url . count) ...), preload URLs only.
|
||||
(let loop ((xs ranked) (left n))
|
||||
(cond
|
||||
((or (null? xs) (= left 0)) 'done)
|
||||
(else
|
||||
(cache-add! (car (car xs)))
|
||||
(loop (cdr xs) (- left 1))))))
|
||||
|
||||
;;; ── startup: load hits, rank, preload ───────────────────────
|
||||
|
||||
(define *resp-404*
|
||||
(let ((body (file->string (string-append *docroot* "/404.html"))))
|
||||
(if body
|
||||
(inline-response "404 Not Found" "text/html; charset=utf-8" body)
|
||||
(inline-response "404 Not Found" "text/plain" "not found\n"))))
|
||||
|
||||
(define *boot-hits* (hits-load))
|
||||
(cond
|
||||
((null? *boot-hits*)
|
||||
(display "adaptive: cold start — preloading seed paths") (newline)
|
||||
(preload-all *seed-paths*))
|
||||
(else
|
||||
(display "adaptive: loaded ") (display (length *boot-hits*))
|
||||
(display " hit records, preloading top ") (display *cache-max*)
|
||||
(newline)
|
||||
(preload-top-n (sort-desc *boot-hits*) *cache-max*)
|
||||
;; Re-seed the in-memory counter from disk so learned ranks
|
||||
;; don't reset each boot.
|
||||
(let loop ((xs *boot-hits*))
|
||||
(cond
|
||||
((null? xs) 'done)
|
||||
(else
|
||||
(hash-table-set! *hits* (car (car xs)) (cdr (car xs)))
|
||||
(loop (cdr xs)))))))
|
||||
|
||||
;;; ── per-request ─────────────────────────────────────────────
|
||||
|
||||
(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 (serve client req)
|
||||
(let ((path (second-token req)))
|
||||
(bump-hit! path)
|
||||
(let ((inline (hash-table-ref/default *cache-inline* path #f)))
|
||||
(if inline
|
||||
(tcp-send client inline)
|
||||
(let ((sf (hash-table-ref/default *cache-sendfile* path #f)))
|
||||
(if sf
|
||||
(begin
|
||||
(tcp-send client (car sf))
|
||||
(tcp-sendfile client (cdr sf)))
|
||||
;; Cold path: try to warm the cache for next hit,
|
||||
;; then serve via the new entry. Caps at *cache-max*.
|
||||
(let ((tag (cache-add! path)))
|
||||
(cond
|
||||
((equal? tag 'inline)
|
||||
(tcp-send client
|
||||
(hash-table-ref/default *cache-inline* path *resp-404*)))
|
||||
((equal? tag 'sendfile)
|
||||
(let ((sf2 (hash-table-ref/default *cache-sendfile* path #f)))
|
||||
(tcp-send client (car sf2))
|
||||
(tcp-sendfile client (cdr sf2))))
|
||||
(else
|
||||
(tcp-send client *resp-404*))))))))))
|
||||
|
||||
;;; ── main loop ───────────────────────────────────────────────
|
||||
;;;
|
||||
;;; This server mutates persistent state on every request
|
||||
;;; (hit-counter bump, cold-path cache promotion), so the
|
||||
;;; arena-pattern heap-restore used by the other servers would
|
||||
;;; corrupt the hits table and cache. We run under lumbda-gc
|
||||
;;; and rely on mark-sweep to reclaim per-request transients.
|
||||
|
||||
(define server (tcp-listen *port*))
|
||||
|
||||
(define (server-loop n)
|
||||
(if (>= n *max-requests*)
|
||||
(begin
|
||||
(display "request cap reached, exiting\n")
|
||||
(hits-save)
|
||||
(tcp-close server))
|
||||
(begin
|
||||
(let ((client (tcp-accept server)))
|
||||
(let ((req (tcp-recv client 4096)))
|
||||
(if (and req (> (string-length req) 0))
|
||||
(serve client req)
|
||||
#f))
|
||||
(tcp-close client))
|
||||
(if (and (> n 0) (= (remainder n *hits-save-every*) 0))
|
||||
(hits-save)
|
||||
#f)
|
||||
(server-loop (+ n 1)))))
|
||||
|
||||
(display "lumbda-www adaptive on :") (display *port*)
|
||||
(display " serving ") (display *docroot*)
|
||||
(display " (") (display (cache-count)) (display "/") (display *cache-max*)
|
||||
(display " cached, hits=") (display *hits-path*) (display ")")
|
||||
(newline)
|
||||
(server-loop 0)
|
||||
193
examples/http-static-server-sendfile.lsp
Normal file
193
examples/http-static-server-sendfile.lsp
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
;;; http-static-server-sendfile.lsp — hybrid in-memory + sendfile(2)
|
||||
;;; static server for lumbda.com. Small assets (HTML/CSS/txt) are
|
||||
;;; cached whole as pre-built HTTP responses and flushed in one
|
||||
;;; tcp-send. Large assets (PDFs, images) cache only the headers;
|
||||
;;; the body is streamed from disk via sendfile(2), a zero-copy
|
||||
;;; kernel->socket path that never enters the lumbda heap.
|
||||
;;;
|
||||
;;; Why the split:
|
||||
;;; - For small bodies (<= *inline-threshold*), one tcp-send beats
|
||||
;;; the syscall split (headers + sendfile).
|
||||
;;; - For large bodies, kernel-side DMA + no userspace bounce
|
||||
;;; dominates any string copy we could do in-process.
|
||||
;;;
|
||||
;;; Configuration:
|
||||
;;; *docroot* filesystem root
|
||||
;;; *preload-paths* URL paths to prepare at startup
|
||||
;;; *inline-threshold* bytes; <= inline, > sendfile
|
||||
;;;
|
||||
;;; Target tier: asm-gc (has tcp-sendfile primitive). On tiers that
|
||||
;;; lack tcp-sendfile (Python, C, bump-only asm) this file will
|
||||
;;; error at load — it's a deployment example for asm-gc, not a
|
||||
;;; portable reference.
|
||||
|
||||
(define *port* 8080)
|
||||
(define *docroot* "www")
|
||||
(define *cache-max* 100)
|
||||
(define *max-requests* 1000000)
|
||||
(define *inline-threshold* 16384)
|
||||
|
||||
(define *crlf* "\r\n")
|
||||
(define *crlf-crlf* "\r\n\r\n")
|
||||
|
||||
;;; ── MIME ────────────────────────────────────────────────────
|
||||
|
||||
(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")))
|
||||
|
||||
;;; ── Response builders ───────────────────────────────────────
|
||||
|
||||
(define (inline-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 (headers-only status ctype nbytes)
|
||||
(string-append
|
||||
"HTTP/1.0 " status *crlf*
|
||||
"Content-Type: " ctype *crlf*
|
||||
"Content-Length: " (number->string nbytes) *crlf*
|
||||
"Connection: close" *crlf-crlf*))
|
||||
|
||||
(define (url-to-fs url-path)
|
||||
(cond
|
||||
((string=? url-path "/") (string-append *docroot* "/index.html"))
|
||||
(else (string-append *docroot* url-path))))
|
||||
|
||||
;;; ── Cache ───────────────────────────────────────────────────
|
||||
;;;
|
||||
;;; Two tables keyed by URL path:
|
||||
;;; *cache-inline* url -> full HTTP response (string)
|
||||
;;; *cache-sendfile* url -> (headers . fs-path) pair
|
||||
;;;
|
||||
;;; Per-request handler hits one or the other, never both.
|
||||
|
||||
(define *cache-inline* (make-hash-table))
|
||||
(define *cache-sendfile* (make-hash-table))
|
||||
|
||||
(define (cache-count)
|
||||
(+ (hash-table-size *cache-inline*)
|
||||
(hash-table-size *cache-sendfile*)))
|
||||
|
||||
(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
|
||||
((not body)
|
||||
(display "cache-add!: missing file: ")
|
||||
(display fs-path) (newline)
|
||||
'missing)
|
||||
((<= (string-length body) *inline-threshold*)
|
||||
(hash-table-set! *cache-inline* url-path
|
||||
(inline-response "200 OK" (mime-of fs-path) body))
|
||||
'inline)
|
||||
(else
|
||||
(hash-table-set! *cache-sendfile* url-path
|
||||
(cons (headers-only "200 OK" (mime-of fs-path)
|
||||
(string-length body))
|
||||
fs-path))
|
||||
'sendfile))))))
|
||||
|
||||
(define *preload-paths*
|
||||
(list "/"
|
||||
"/style.css"
|
||||
"/whitepaper.pdf"
|
||||
"/robots.txt"
|
||||
"/404.html"))
|
||||
|
||||
(define *resp-404*
|
||||
(let ((body (file->string (string-append *docroot* "/404.html"))))
|
||||
(if body
|
||||
(inline-response "404 Not Found" "text/html; charset=utf-8" body)
|
||||
(inline-response "404 Not Found" "text/plain" "not found\n"))))
|
||||
|
||||
(define (preload-all paths)
|
||||
(cond
|
||||
((null? paths) 'done)
|
||||
(else
|
||||
(cache-add! (car paths))
|
||||
(preload-all (cdr paths)))))
|
||||
|
||||
(preload-all *preload-paths*)
|
||||
|
||||
;;; ── Request parsing ─────────────────────────────────────────
|
||||
|
||||
(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)))))))
|
||||
|
||||
;;; ── Per-request dispatch ────────────────────────────────────
|
||||
|
||||
(define (serve client req)
|
||||
(let ((path (second-token req)))
|
||||
(let ((inline (hash-table-ref/default *cache-inline* path #f)))
|
||||
(if inline
|
||||
(tcp-send client inline)
|
||||
(let ((sf (hash-table-ref/default *cache-sendfile* path #f)))
|
||||
(if sf
|
||||
(begin
|
||||
(tcp-send client (car sf))
|
||||
(tcp-sendfile client (cdr sf)))
|
||||
(tcp-send client *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))
|
||||
(serve client req)
|
||||
#f))
|
||||
(tcp-close client))
|
||||
(heap-restore snap)
|
||||
(server-loop (+ n 1) snap))))
|
||||
|
||||
(display "lumbda-www sendfile on :") (display *port*)
|
||||
(display " serving ") (display *docroot*)
|
||||
(display " (") (display (hash-table-size *cache-inline*))
|
||||
(display " inline, ") (display (hash-table-size *cache-sendfile*))
|
||||
(display " sendfile, cap ") (display *cache-max*) (display ")")
|
||||
(newline)
|
||||
(server-loop 0 (heap-snapshot))
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
#!/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.
|
||||
# bench-www-race.sh — asm-gc lumbda-www (uncached + cached + sendfile)
|
||||
# vs caddy file-server on the same lumbda.com docroot. Small-request
|
||||
# (index.html) and large-request (2.56 MiB whitepaper PDF) workloads,
|
||||
# identical concurrency, adjacent runs. Reports req/s, MiB/s, and
|
||||
# peak RSS for each server. The PDF size is pulled from the file on
|
||||
# disk so a whitepaper rebuild does not desync the MiB/s calculation.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
|
|
@ -11,8 +13,12 @@ LARGE_N=${LARGE_N:-200}
|
|||
CONCURRENCY=${CONCURRENCY:-8}
|
||||
LUMBDA_PORT=${LUMBDA_PORT:-8080}
|
||||
LUMBDA_CACHED_PORT=${LUMBDA_CACHED_PORT:-8082}
|
||||
LUMBDA_SENDFILE_PORT=${LUMBDA_SENDFILE_PORT:-8083}
|
||||
CADDY_PORT=${CADDY_PORT:-8081}
|
||||
CADDY_BIN=${CADDY_BIN:-/home/fox/git/make_post_sell/caddy}
|
||||
PDF_PATH="whitepaper/lumbda-whitepaper.pdf"
|
||||
PDF_BYTES=$(wc -c < "$PDF_PATH")
|
||||
PDF_MIB=$(python3 -c "print(f'{$PDF_BYTES / (1024*1024):.2f}')")
|
||||
|
||||
declare -a SPAWNED=()
|
||||
cleanup() {
|
||||
|
|
@ -26,8 +32,8 @@ trap cleanup EXIT INT TERM
|
|||
make -s -C asm all
|
||||
|
||||
printf "\n═══════════════════════════════════════════════════════\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 "asm-gc lumbda-www (uncached + cached + sendfile) vs caddy\n"
|
||||
printf " %s small (/), %s large (/whitepaper.pdf %s MiB)\n" "$SMALL_N" "$LARGE_N" "$PDF_MIB"
|
||||
printf " concurrency %s\n" "$CONCURRENCY"
|
||||
printf "═══════════════════════════════════════════════════════\n"
|
||||
|
||||
|
|
@ -46,36 +52,48 @@ sed "s|(define \*port\* 8080)|(define *port* $LUMBDA_CACHED_PORT)|" \
|
|||
LUMBDA_CACHED_PID=$!
|
||||
SPAWNED+=("$LUMBDA_CACHED_PID")
|
||||
|
||||
# 3. caddy file-server
|
||||
# 3. lumbda-www sendfile (small assets inline-cached, big files streamed
|
||||
# via SYS_SENDFILE — zero-copy kernel->socket). Port patched to avoid
|
||||
# collisions with the other two lumbda servers.
|
||||
sed "s|(define \*port\* 8080)|(define *port* $LUMBDA_SENDFILE_PORT)|" \
|
||||
examples/http-static-server-sendfile.lsp \
|
||||
| ./asm/lumbda-gc >/dev/null 2>&1 &
|
||||
LUMBDA_SENDFILE_PID=$!
|
||||
SPAWNED+=("$LUMBDA_SENDFILE_PID")
|
||||
|
||||
# 4. caddy file-server
|
||||
"$CADDY_BIN" file-server --root www --listen ":$CADDY_PORT" >/dev/null 2>&1 &
|
||||
CADDY_PID=$!
|
||||
SPAWNED+=("$CADDY_PID")
|
||||
|
||||
# Wait for all three to be ready.
|
||||
# Wait for all four 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:$LUMBDA_CACHED_PORT/" 2>/dev/null || echo 0)
|
||||
d=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$LUMBDA_SENDFILE_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
|
||||
[ "$a" = "200" ] && [ "$b" = "200" ] && [ "$d" = "200" ] && [ "$c" = "200" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
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; }
|
||||
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 "$LUMBDA_SENDFILE_PID" 2>/dev/null || { echo "lumbda-www (sendfile) failed to start"; exit 1; }
|
||||
kill -0 "$CADDY_PID" 2>/dev/null || { echo "caddy failed to start"; exit 1; }
|
||||
|
||||
# ── 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
|
||||
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:$LUMBDA_SENDFILE_PORT/whitepaper.pdf" -o /tmp/a-sendfile.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-sendfile.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"
|
||||
printf " pdf-integrity (%-8s): byte-identical\n" "$label"
|
||||
else
|
||||
printf " pdf-integrity (%-7s): DIFFERS\n" "$label"
|
||||
printf " pdf-integrity (%-8s): DIFFERS\n" "$label"
|
||||
fi
|
||||
done
|
||||
rm -f /tmp/a-lumbda.pdf /tmp/a-cached.pdf /tmp/a-caddy.pdf
|
||||
rm -f /tmp/a-lumbda.pdf /tmp/a-cached.pdf /tmp/a-sendfile.pdf /tmp/a-caddy.pdf
|
||||
|
||||
# ── benchmark one (pid, port, label, N, path) ──
|
||||
bench_one() {
|
||||
|
|
@ -100,7 +118,7 @@ print(f'{$n / max(t, 1e-6):.0f}')
|
|||
if [ "$path" = "/whitepaper.pdf" ]; then
|
||||
mibs=$(python3 -c "
|
||||
t = float('$t1') - float('$t0')
|
||||
print(f'{$n * 2669058 / (1024*1024) / max(t, 1e-6):.0f}')
|
||||
print(f'{$n * $PDF_BYTES / (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
|
||||
|
|
@ -110,15 +128,17 @@ print(f'{$n * 2669058 / (1024*1024) / max(t, 1e-6):.0f}')
|
|||
|
||||
echo
|
||||
echo "── small (/) ──"
|
||||
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" "/"
|
||||
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 "$LUMBDA_SENDFILE_PID" "$LUMBDA_SENDFILE_PORT" "lumbda-www sendfile" "$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 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 "── large (/whitepaper.pdf, $PDF_MIB MiB) ──"
|
||||
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 "$LUMBDA_SENDFILE_PID" "$LUMBDA_SENDFILE_PORT" "lumbda-www sendfile" "$LARGE_N" "/whitepaper.pdf"
|
||||
bench_one "$CADDY_PID" "$CADDY_PORT" "caddy file-server" "$LARGE_N" "/whitepaper.pdf"
|
||||
|
||||
echo
|
||||
echo "── binary sizes (stripped) ──"
|
||||
|
|
@ -128,5 +148,5 @@ 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" "$LUMBDA_CACHED_PID" "$CADDY_PID" 2>/dev/null
|
||||
wait "$LUMBDA_PID" "$LUMBDA_CACHED_PID" "$CADDY_PID" 2>/dev/null || true
|
||||
kill -9 "$LUMBDA_PID" "$LUMBDA_CACHED_PID" "$LUMBDA_SENDFILE_PID" "$CADDY_PID" 2>/dev/null
|
||||
wait "$LUMBDA_PID" "$LUMBDA_CACHED_PID" "$LUMBDA_SENDFILE_PID" "$CADDY_PID" 2>/dev/null || true
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1272,6 +1272,37 @@ The snapshot is dangerously precise: anything allocated after the snap and still
|
|||
|
||||
This is not a general-purpose allocator. It is an escape hatch the programmer uses when they can prove the scope boundary. For general programs on asm, the heap still grows. For the HTTP server pattern, O(1) memory costs two lines of code.
|
||||
|
||||
11.7 Static File Serving: Cache, Sendfile, and Adaptive Preload
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The HTTP demo in §11.3 served synthesized responses. To host ``lumbda.com`` we needed a real static-file path — something that hands a 2.67 MiB PDF (this whitepaper) off the disk without bouncing it through the Scheme heap. Three variants now ship in ``examples/``, each ~100–200 lines of portable Scheme, each running on ``asm/lumbda-gc`` (the GC build, 27 KB stripped).
|
||||
|
||||
- ``http-static-server.lsp`` — read the file per request via ``file->string``, build a response, send. Baseline. Correct and portable across all four tiers; every 2.67 MiB PDF round-trip allocates 2.67 MiB of Scheme string.
|
||||
- ``http-static-server-cached.lsp`` — on startup, build a hash-table keyed by URL path to the full pre-composed HTTP response (headers + body). Per-request handler is one ``hash-table-ref/default``. No ``file->string``, no ``string-append``, no MIME lookup in the hot path.
|
||||
- ``http-static-server-sendfile.lsp`` — small assets (≤ 16 KB) stay inline-cached as full responses; large assets cache only the headers and stream the body via a new ``tcp-sendfile`` primitive that issues the Linux ``SYS_SENDFILE`` (40) syscall directly. Zero-copy kernel → socket, no userspace bounce.
|
||||
|
||||
``tcp-sendfile`` is a 90-line asm-gc builtin. It opens the path, ``lseek``'s to find the size, then loops ``sendfile(2)`` until the full body is written, and ``close()``'s. The body never enters the Lumbda heap — headers are composed in Scheme and flushed via ``tcp-send``, then the kernel DMAs the file directly into the socket buffer.
|
||||
|
||||
**Four-way race** (``tests/bench-www-race.sh``, i5-8350U, 1000 small requests, 100 large requests, concurrency 8, ``xargs -P 8 curl``, adjacent runs):
|
||||
|
||||
.. table::
|
||||
:widths: 28 14 14 14
|
||||
|
||||
=========================== ============ ============ ==============
|
||||
Server PDF req/s PDF MiB/s Peak RSS
|
||||
=========================== ============ ============ ==============
|
||||
lumbda-www uncached 159 404 15.5 MB
|
||||
lumbda-www cached 237 602 7.2 MB
|
||||
**lumbda-www sendfile** **474** **1208** **4.2 MB**
|
||||
caddy file-server (Go) 485 1234 37.1 MB
|
||||
=========================== ============ ============ ==============
|
||||
|
||||
All four servers return the PDF byte-identical against the on-disk master. The sendfile path lands within 2% of Caddy on throughput while holding **9× less peak RSS** in a binary **1,400× smaller** (27 KB stripped vs 38 MB). Small-request throughput (``GET /``) is essentially flat across the three lumbda variants — the cached path already removed per-request work, so sendfile's win is entirely on large bodies.
|
||||
|
||||
**Adaptive preload: ``http-static-server-adaptive.lsp``.** A hit-counter hash-table (URL → integer) is updated every request. Every *N* requests the counter is flushed to ``www.hits`` as newline-delimited ``path count`` records. On startup the file is loaded, sorted descending, and the top *cache-max* URLs are preloaded — so each boot reflects what the previous run actually served. Cold start falls back to a seed list (``/`` and ``/404.html``). Cold requests beyond the seed set are promoted into the cache on first hit until the cap is reached. Because this server mutates persistent state (the counter and the cache) on every request, the arena-pattern ``heap-restore`` is dropped and the GC build's mark-sweep reclaims transients instead.
|
||||
|
||||
For small deployments (≤ ~1000 resources) this is ~95% of the win of a full predictive-preload system: the top few URLs dominate traffic and get pinned at boot. Anything rarer warms on demand. The remaining 5% — predicting which URLs will be needed from *co-occurrence* rather than raw frequency — is §13 Future Work.
|
||||
|
||||
|
||||
12. MOAD Audit: Fixing What We Built
|
||||
--------------------------------------
|
||||
|
|
@ -1348,6 +1379,7 @@ This is the permacomputer obligation: infrastructure that renews itself. Code th
|
|||
|
||||
- **WebSocket / bidirectional**: HTTP/1.0 is request-response; a persistent socket loop with framing brings full-duplex feedback.
|
||||
- **Continuation-passing over HTTP**: §11.5 moves *bindings* over HTTP. The next step is moving a live *continuation* — serialize it via ``call/cc`` + JSON portal, transmit, resume on the remote VM. Makes any TCP endpoint a trampoline target.
|
||||
- **DAG-of-hot-paths predictive preload**: §11.7's adaptive server ranks by raw frequency, which pins the top URLs but cannot predict *which* assets co-occur. A navigation DAG (edge weights = ``P(next = v | prev = u)``) learned from referrer headers or session logs would let the boot-time preloader walk forward from seed nodes and warm everything within a predicted session depth. For deployments with > 1000 resources where the flat top-N is too narrow and full hot-caching is too wide, the DAG is the middle path. The frequency-only version in the repo today is designed to be the single-node degenerate case — a DAG with no edges.
|
||||
- **GPU lambda execution**: Map/reduce on CUDA for data-parallel Scheme (Phase 1), trampolining for recursive lambdas (Phase 2), interaction combinators for massive parallelism (Phase 3)
|
||||
- **Copying GC in asm**: ``heap-snapshot`` is an escape hatch. A mark-and-copy collector would remove the sharp edge for general programs without forcing the programmer to reason about lifetimes.
|
||||
- **Concurrent accept loop (asm)**: Currently single-threaded. A pre-forked worker model or ``SO_REUSEPORT`` pool would multiply throughput without changing the Scheme code.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue