lantern-system: ship fs.read-dir over the bulk lane (drop the O(n²) json-encode)
fs.read-dir returned #{ entries: (N stat dicts) total: }, which the bridge json-ENCODES on the normal resolve path — and that per-char encode is O(n²) (the same cost fs.read-file already dodges via the bulk lane). Measured on a 600-file folder: json-encoding the result was ~359ms of a ~607ms listing, and it gets quadratically worse on larger folders.
Build the listing's TSV on the host and ship it as base64 over the BULK lane instead (the #{ bulk-b64: meta: } shape fs.read-file uses), bypassing the json-encode entirely. The wire format is exactly what (slate providers lantern) parse-dir-tsv already consumes: "<name>t<type>t<size>" per line, names %-escaped for % / TAB / NEWLINE, with the "capped" marker row baked in when the listing is truncated. The TSV is built in one O(n) string-join (not incremental string-append) and names escape via a char-list pass, so the encode is O(n).
Measured result: 600-file listing 607ms -> 288ms (~53%), and the O(n²) tail is gone so large folders stay responsive. Tests updated to decode the bulk TSV (lantern-system 28 passed, lantern 39 passed).
lantern-system/src/lantern-system/fs.sgl | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
lantern-system/test/test-lantern-system.sgl | 52 +++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 101 insertions(+), 15 deletions(-)lantern-system/src/lantern-system/fs.sglmodified
mtime: (dict-ref st mtime: 0) mode: (dict-ref st mode: 0))) ;; ---- read-dir TSV encoding (the BULK lane, O(n)) ------------------------- ;; fs.read-dir used to return #{ entries: (600 stat dicts) total: } — which the ;; bridge json-ENCODES on the normal resolve path, and that encode is O(n²) ;; (per-char string-ref; the same cost fs.read-file dodged via the bulk lane). ;; Measured ~359ms of a 607ms 600-file listing. So build the listing's TSV on ;; the host and ship it as base64 over the BULK lane instead — no json-encode, ;; less IPC, and it removes the O(n²) blow-up on large folders. The wire format ;; is exactly what (slate providers lantern) parse-dir-tsv already consumes: ;; "<name>\t<type>\t<size>" per line, names %-escaped for % / TAB / NEWLINE. ;; Escape a name for the TSV: % / TAB / NEWLINE -> %25 / %09 / %0A (so the ;; name never breaks the tab/newline framing; parse-dir-tsv's decode-name ;; reverses it). Built as a char list right-to-left then list->string — O(name), ;; no incremental string-append, only (sigil core) primitives. (define (tsv-escape-name nm) (let loop ((i (- (string-length nm) 1)) (acc '())) (if (< i 0) (list->string acc) (let ((c (string-ref nm i))) (loop (- i 1) (cond ((char=? c #\%) (cons #\% (cons #\2 (cons #\5 acc)))) ((char=? c #\tab) (cons #\% (cons #\0 (cons #\9 acc)))) ((char=? c #\newline) (cons #\% (cons #\0 (cons #\A acc)))) (else (cons c acc)))))))) ;; type: is a symbol (regular/directory/…); parse-dir-tsv only distinguishes ;; "directory", but keep the full lowering for fidelity with stat->json. (define (tsv-type t) (cond ((symbol? t) (symbol->string t)) ((string? t) t) (else "regular"))) (define (stat->tsv-row st) (string-append (tsv-escape-name (dict-ref st name: "")) "\t" (tsv-type (dict-ref st type: #f)) "\t" (number->string (dict-ref st size: 0)))) ;; A visible "capped" marker row when the listing was truncated (limit:), so ;; the missing entries are shown rather than silently dropped (mirrors the old ;; JS entriesTsv marker). (define (tsv-cap-marker shown total) (string-append "⋯ +" (number->string (- total shown)) " more (listing capped at " (number->string shown) ")\tregular\t0")) ;; Build the whole listing's TSV in ONE join (O(n) — NOT incremental ;; string-append), then base64 it for the bulk lane. (define (entries->tsv-b64 sorted total) (let* ((rows (map stat->tsv-row sorted)) (rows+ (if (> total (length sorted)) (append rows (list (tsv-cap-marker (length sorted) total))) rows)) (tsv (string-join rows+ "\n"))) ;; base64-encode's ->bytes UTF-8-encodes a string, so pass the TSV string ;; directly (no (sigil io) string->utf8 dependency here). (base64-encode tsv))) ;; Build the fs command set bound to grants table `g`. (define (fs-commands g) (list (let ((st (guard (e (#t #f)) (fs-stat g (path-join path (car ns)))))) (loop (cdr ns) (if st (cons st acc) acc)))))) (entries (map stat->json (fs-sort-entries stats)))) (dict entries: entries total: total))))) (sorted (fs-sort-entries stats))) ;; BULK lane: ship the listing's TSV as base64 (no json-encode of ;; the entries — that was ~59% of the cost and O(n²)). Delivered ;; via #{ bulk-b64: meta: }, the same shape fs.read-file uses. (dict bulk-b64: (entries->tsv-b64 sorted total) meta: (dict total: total)))))) ;; fs.stat #{ path: } -> a stat dict. (lantern-command "fs.stat"lantern-system/test/test-lantern-system.sglmodified
;; and nothing got registered for a would-be handle (assert-equal (servable-lookup reg "f1") #f)));; fs.read-dir now ships the listing's TSV over the BULK lane (bulk-b64) to skip;; the O(n²) json-encode. Decode it back to rows — the test-side mirror of;; (slate providers lantern) parse-dir-tsv — so these tests assert on the same;; entries the folder view sees.(define (read-dir-rows r) (let ((tsv (utf8->string (base64-decode (dict-ref r bulk-b64: ""))))) (if (string=? tsv "") '() (map (lambda (line) (let ((f (string-split line "\t"))) (dict name: (car f) type: (cadr f) size: (or (string->number (caddr f)) 0)))) (string-split tsv "\n")))))(define (read-dir-total r) (dict-ref (dict-ref r meta: #{}) total: 0));; The "⋯ +N more (listing capped)" marker row (name starts with U+22EF).(define (cap-marker-row? e) (let ((nm (dict-ref e name: ""))) (and (> (string-length nm) 0) (= (char->integer (string-ref nm 0)) #x22EF))))(test "fs.read-dir lists entries with kinds" (let* ((fx (fs-fixture)) (rd (handler-for (dict-ref fx cmds:) "fs.read-dir"))) (let* ((r (rd (dict path: (dict-ref fx dir:)) #f)) (entries (dict-ref r entries: '())) (entries (read-dir-rows r)) (names (map (lambda (e) (dict-ref e name: #f)) entries))) (assert-true (member "a.txt" names)) (assert-true (member "sub" names)) (write-file-string (path-join dir ".gitignore") "i") (ensure-directory (path-join dir ".git")) (let* ((r (rd (dict path: dir) #f)) (names (map (lambda (e) (dict-ref e name: #f)) (dict-ref r entries: '())))) (names (map (lambda (e) (dict-ref e name: #f)) (read-dir-rows r)))) ;; hidden dirs, dirs, hidden files, files — alphabetical within each tier (assert-equal names (list ".git" "sub" ".gitignore" "a.txt" "README.md"))))) (let* ((fx (fs-fixture)) (rd (handler-for (dict-ref fx cmds:) "fs.read-dir"))) ;; the fixture dir has 2 top-level entries (a.txt + sub/) (let ((r (rd (dict path: (dict-ref fx dir:) limit: 1) #f))) (assert-equal (length (dict-ref r entries: '())) 1) (assert-equal (dict-ref r total: 0) 2)) ;; no limit: everything, total matches (let ((r (rd (dict path: (dict-ref fx dir:)) #f))) (assert-equal (length (dict-ref r entries: '())) 2) (assert-equal (dict-ref r total: 0) 2)))) (let* ((r (rd (dict path: (dict-ref fx dir:) limit: 1) #f)) (rows (read-dir-rows r))) ;; total is the FULL count (in meta:), regardless of the cap (assert-equal (read-dir-total r) 2) ;; 1 real kept entry, plus a visible "capped" marker row (assert-equal (length (filter (lambda (e) (not (cap-marker-row? e))) rows)) 1) (assert-equal (length (filter cap-marker-row? rows)) 1)) ;; no limit: everything, total matches, no marker (let* ((r (rd (dict path: (dict-ref fx dir:)) #f)) (rows (read-dir-rows r))) (assert-equal (length rows) 2) (assert-equal (read-dir-total r) 2) (assert-equal (length (filter cap-marker-row? rows)) 0))))(test "fs.read-file returns the file bytes on the bulk lane (utf8 meta)" (let* ((fx (fs-fixture)) (write-file-string (path-join dir "a.txt") "hello A") (dict dir: dir g: g bridge: (sys-bridge g))))(test "bridge: fs.read-dir resolves with entries JSON"(test "bridge: fs.read-dir resolves on the bulk lane (TSV base64, no entries JSON)" (reset-evals!) (let* ((fx (fs-bridge-fixture)) (bridge (dict-ref fx bridge:))) (string-append "{\"id\":1,\"cmd\":\"fs.read-dir\",\"args\":{\"path\":\"" (dict-ref fx dir:) "\"}}")) (let ((js (last-eval))) (assert-true (string-contains? js "window.lantern._resolve(1,")) (assert-true (string-contains? js "entries")) (assert-true (string-contains? js "a.txt"))))) ;; the listing crosses on the BULK lane now (base64 TSV via _resolveBulk), ;; NOT as an entries JSON body — that per-entry json-encode was the O(n²) cost (assert-true (string-contains? js "window.lantern._resolveBulk(1,")) (assert-true (string-contains? js "\"total\":")) ; the full count rides in meta (assert-false (string-contains? js "entries")))))(test "bridge: fs.read-file resolves on the bulk lane (raw base64, no JSON body)" (reset-evals!)