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.
66 lines
2.1 KiB
Text
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)
|