;;; 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)