Skip to content

fix(jsoo): tail-recursive List.remove_assoc closes the add-all stack gap - #37

Merged
tiensonqin merged 1 commit into
mainfrom
devin/jsoo-stack-add-all
Oct 5, 2026
Merged

tiensonqin merged 1 commit into
mainfrom
devin/jsoo-stack-add-all

Conversation

@tiensonqin

Copy link
Copy Markdown
Contributor

Summary

add-all at BENCH_SIZE=10000 overflowed the default Node.js stack under js_of_ocaml (RangeError: Maximum call stack size exceeded, worked around with node --stack-size=8000 in docs/perf.md).

Root cause. Once per transact, ensure_current_tx_tempid (impl/transact.ml:82, called at impl/transact.ml:1908) runs List.remove_assoc "db/current-tx" (tempid_map_order tempids). The tempid order list carries one entry per transacted entity, so at size 10000 the walk is ~10,000 deep. The OCaml stdlib's remove_assoc is not tail-recursive — under jsoo that becomes ~10k JS frames plus a caml_compare/caml_compare_val frame pair per element (captured via the .js_error stashed by caml_wrap_exception). Measured on this machine: the crash reproduces at node --stack-size<=500 and survives near ~1000 — exactly the default-stack boundary.

Fix. Extend the existing Datascript_types.List shadow in type/datascript_types.ml — the same mechanism already used to replace Melange's recursive concat_map/concat/flatten — with a tail-recursive remove_assoc (first-occurrence removal, order preserved, = equality — identical stdlib semantics). Because every impl module opens Datascript_types, native, Melange, and jsoo all get the bounded-stack version; this also covers the other data-sized remove_assoc walk in impl/storage.ml (pending disk entries).

Measured (same machine, Node v24.9.0, OCaml 5.5.0, size 10000, warmup 200ms / sample 500ms / 5 samples, median ms):

benchmark before after
add-all jsoo 825 (default stack — survives on Node 24 by luck; crashes ≤ --stack-size=500) 764 / 923 (two runs; full suite also passes at --stack-size=200)
add-all native 245.36 245.42

The remaining ~3x jsoo/native gap matches the size-200/1000 ratios in docs/perf.md; the doc's stale --stack-size=8000 note is updated.

Verification. All 34 native test executables pass (incl. test_perf); test/js_smoke.bc.js and test/js_facade_runner.js against js/datascript_js.bc.js pass.

Link to Devin session: https://app.devin.ai/sessions/723787857ebe49cb8f0281b369d46433
Open in Devin Desktop: https://app.devin.ai/desktop/session/723787857ebe49cb8f0281b369d46433?variant=devin
Requested by: @tiensonqin

add-all at size 10000 overflowed the default Node stack under js_of_ocaml:
ensure_current_tx_tempid walks the tempid order list (one entry per
transacted entity) with the non-tail stdlib remove_assoc, producing ~10k
nested frames plus caml_compare calls. Extend the existing
Datascript_types.List shadow (already used for concat_map/concat/flatten)
with a tail-recursive remove_assoc so native, Melange, and jsoo all get
bounded-stack behavior. The full size-10000 bench suite now completes
under node --stack-size=200.
@devin-ai-integration

Copy link
Copy Markdown

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

@tiensonqin
tiensonqin merged commit 952c0be into main Oct 5, 2026
2 checks passed
@tiensonqin
tiensonqin deleted the devin/jsoo-stack-add-all branch October 5, 2026 07:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant