lumbda/examples/http-client-bench.lsp
russell@unturf.com b8d6afdeb3 heap-snapshot + native HTTP client + MOAD-0001 cleanup
Three wins in one commit.

1) heap-snapshot / heap-restore (asm arena primitive)
   asm has no GC. Long-running servers leaked ~64 MB per heap growth.
   Two new builtins let a programmer capture r15 and later rewind to
   it, recycling intermediate allocations in O(1) memory.
   Python + C get no-op versions so portable .lsp code can call them
   unconditionally.

   examples/http-server.lsp now takes a snapshot at top level and
   rewinds after every request. Measured asm RSS: 88 KB initial,
   100 KB after 100 requests, 100 KB after 1100 requests — flat.
   Prior behavior was +64 MB per few thousand requests.

2) examples/http-client-bench.lsp — native HTTP load generator
   Uses only the six tcp-* primitives + current-time-ms. Runs
   identically in all three impls. Eliminates curl's ~2 ms/req
   fork+exec overhead, so real server throughput shows up:

     Python server ← Python client   2403 rps
     C      server ← C      client   2439 rps
     asm    server ← asm    client   2994 rps
     asm    server ← C      client   2500 rps

   The earlier curl-based bench was clamped near 400 rps by the
   client; the actual servers handle 6–7× that.

3) MOAD-0001 cleanup
   - c/builtins.c bi_string_replace: strncmp-at-every-position
     (hand-rolled, sedimentary) → strstr (libc-tuned, typically
     Boyer-Moore-Horspool). O(N*k) → O(N + matches*k).
   - uncommonlisp.py _tokenize_lines: per-token src.count('\n', 0, pos)
     → precompute line_starts once, bisect_right per token.
     O(N*M) → O(M + N log M).

Also adds current-time-ms to all three impls so benchmarks can
time themselves without relying on the Python/C float `current-time`
(asm has no floats). Seconds-since-epoch tagged as a 61-bit int.

Test counts unchanged: 571 py + 132 asm + 189 shared + 83 c = 975.
All green via make test-all.
2026-04-16 20:49:32 -04:00

66 lines
2.1 KiB
Text

;;; http-client-bench.lsp — sequential HTTP load generator in Scheme
;;;
;;; Makes N requests to 127.0.0.1:PORT and reports elapsed wall time
;;; + requests/sec. Uses only the six tcp-* primitives, so it runs
;;; identically in Python, C, and asm.
;;;
;;; The earlier tests/web-benchmark.sh used curl — each curl fork+exec
;;; costs ~2 ms, swamping actual server work. This client keeps every
;;; request in-process: that irreducible cost disappears, so the real
;;; server throughput shows up.
;;;
;;; Usage:
;;; python3 uncommonlisp.py --fast examples/http-client-bench.lsp
;;; ./c/uncommonlisp examples/http-client-bench.lsp
;;; ./asm/uncommonlisp < examples/http-client-bench.lsp
;;;
;;; Override N or PORT by pre-setting *n-requests* / *port* before load.
(define *host* "127.0.0.1")
(define *port* 8080)
(define *n-requests* 500)
(define *path* "/bench")
(define *request*
(string-append
"GET " *path* " HTTP/1.0\r\n"
"Host: " *host* "\r\n"
"Connection: close\r\n\r\n"))
;;; Pass snap as arg so heap-restore can rewind per-request
;;; allocations on asm without invalidating the client loop's
;;; closure env.
(define (one-request)
(let ((sock (tcp-connect *host* *port*)))
(if sock
(begin
(tcp-send sock *request*)
(let ((resp (tcp-recv sock 8192)))
(tcp-close sock)
(if (and resp (> (string-length resp) 0)) 1 0)))
0)))
(define (client-loop n ok snap)
(if (= n 0)
ok
(let ((got (one-request)))
(heap-restore snap)
(client-loop (- n 1) (+ ok got) snap))))
(define t0 (current-time-ms))
(define ok (client-loop *n-requests* 0 (heap-snapshot)))
(define t1 (current-time-ms))
(define elapsed-ms (- t1 t0))
(define rps
(if (> elapsed-ms 0)
(quotient (* *n-requests* 1000) elapsed-ms)
0))
(display "target : http://") (display *host*) (display ":") (display *port*)
(display *path*) (newline)
(display "requests : ") (display *n-requests*) (newline)
(display "ok : ") (display ok) (newline)
(display "elapsed : ") (display elapsed-ms) (display " ms") (newline)
(display "rps : ") (display rps) (newline)