Skip to content

Find weak collection entries through hashed indexes - #1702

Closed
ejc3 wants to merge 2 commits into
vercel-labs:mainfrom
ejc3:weak-collections-hashed-lookup
Closed

ejc3 wants to merge 2 commits into
vercel-labs:mainfrom
ejc3:weak-collections-hashed-lookup

Conversation

@ejc3

@ejc3 ejc3 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Fixes #1619.

WeakMap and WeakSet found a key by walking every entry of the collection (scr_weak_find over map->entries in packages/runtime/src/scr_json.c), and scr_weak_remove_impl walked the list again to unlink an entry, so filling a collection with n keys took O(n²) time. 200,000 object keys took 64.6 s on Linux arm64 where Node takes 0.12 s.

  • Each collection keeps its entries in a hashed index (buckets, a power-of-two length) that doubles when its entries outnumber its buckets. get, set, has and delete look up and unlink through it in expected constant time, and tracing, release and collection walk its buckets.
  • The thread-wide index of all entries, which key disposal searches, starts from the same static 1,024 buckets, moves to a heap table that doubles as entries grow, and returns to the static table when the last entry goes, so a thread that ends with no weak entries frees it.
  • Key disposal returns at once when no weak entry exists.
  • Both indexes hash the pointer with a multiplicative mix (scr_weak_hash), which spreads keys over any table size; the old (bits >> 4) ^ (bits >> 14) was fitted to a fixed 1,024-bucket table.

Tests:

  • Corpus 1619 covers both indexes growing past their first sizes, overwrites, deletes and re-adds, keys shared by two maps and a set, a map dropped while its keys live, keys dropped while maps live, and fresh keys (which may reuse a dropped key's memory) not being found.
  • tests/harness/weak-collection-scaling.test.ts runs the same work with 25,000 and 200,000 keys and bounds the ratio of the two times at 32. With this change it measures 11x to 14x, in both lanes; on main the 200,000-key run is stopped at its 60 s timeout. It follows closure-environment-scaling.test.ts.

Testing

On main at 1ee5c84. Environment: Linux arm64 (glibc), Node 24.22.0, pnpm 11.1.3, LLVM 22.1.8, Zig 0.16.0, with the llvm-linux-arm64-gnu and runtime-linux-arm64-gnu native artifacts built locally from this tree. I have no Vercel Sandbox credentials, so these are the local fallback lanes from CONTRIBUTING.md. The host was shared with other test runs.

  • The issue's program (200,000 keys) runs in 0.06 s with this branch, against 0.11 s for Node and 64.6 s for 0.4.0. Its output matches Node's.
  • Red on main: weak-collection-scaling.test.ts fails at its 60 s timeout on the 200,000-key run. With this branch it passes, plain and with SCRIPTC_SAN=1. Corpus 1619 passes in differential.test.ts and llvm-differential.test.ts, plain and sanitized.
  • SCRIPTC_TEST_WORKERS=4 pnpm test: 262 failed, 12620 passed, 126 skipped; main gives 263 failed, 12616 passed on this host, one of them this branch's scaling test, which I had copied there to see it fail. No test fails on this branch only, and library-contract.test.ts's identity-hash case failed on main only. The shared failures: those that target wasm32-wasi (wasi-differential, three, static-spatial, wasm-library, library-dispatch), since no wasm32-wasi runtime pack is built here; 40 in native-toolchain.test.ts, most with ENOENT on scratch cache directories; the 600 s timeout of coverage.test.ts's all-static check; the size bound in runtime-tree-shaking.test.ts; and one ASan case in library-mode.test.ts.
  • SCRIPTC_SAN=1: a full local lane with four workers would have taken more than a day here, so I ran it in two parts on this head and on main. The corpus files (differential.test.ts and llvm-differential.test.ts, in 12 SCRIPTC_TEST_SHARD shards): 7016 passed, 0 failed (main: 7014 passed, 0 failed). Everything else (--exclude those two, 12 workers): 266 failed, 5571 passed, 155 skipped; main gives 266 failed, 5570 passed, and the failing tests are the same: the ones above, plus three ASan cases in library-multi.test.ts and one in self-hosting-native-driver.test.ts.
  • pnpm lint, pnpm format:check and pnpm test:ts7 --baselines-only pass.
  • Not run: the Sandbox gate, macOS, Windows, and other targets' runtime packs (only linux-arm64-gnu was rebuilt).

ejc3 added 2 commits October 21, 2026 10:12
Corpus 1619 pins WeakMap and WeakSet behavior with thousands of keys; the scaling test bounds how the time of the same work grows from 25,000 to 200,000 keys (vercel-labs#1619). The scaling test fails on main: each lookup scans every entry, and the 200,000-key run is stopped at 60 seconds.
WeakMap and WeakSet found a key by walking every entry of the collection,
so filling one with n keys took O(n^2) time: 200,000 keys ran for about a
minute where Node takes a tenth of a second (vercel-labs#1619).

- Each collection keeps its entries in a hashed index that doubles when
  its entries outnumber its buckets; get, set, has and delete look up and
  unlink through it.
- The thread-wide index of all entries, which key disposal searches, grows
  the same way from its static 1,024 buckets, and returns to them when the
  last entry goes.
- Key disposal returns at once when no weak entry exists.
- Corpus 1619 covers growth, overwrites, deletes, keys shared by several
  collections, a dropped collection, dropped keys and fresh keys after
  them; a harness test bounds how the time grows from 25,000 to 200,000
  keys.
@vercel

vercel Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

@ejc3 is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@ejc3

ejc3 commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Main has moved since the Testing section's 1ee5c84. The two commits apply cleanly to 7c235ce, and there the scaling test, native-weak-collections.test.ts and the weak corpus programs in differential.test.ts and llvm-differential.test.ts pass (plain lane, 33 tests). I have not rerun the full lanes on 7c235ce.

@ctate ctate closed this in #2004 Oct 24, 2026
jlucaso1 pushed a commit to jlucaso1/scriptc that referenced this pull request Oct 24, 2026
WeakMap and WeakSet kept each collection's entries in one list and found
a key by walking it, and every collection's entries for a key in 1,024
shared buckets. Filling a collection and reading it back took quadratic
time: 40,000 keys took 6.7 s against Node's 0.2 s, and 200,000 keys did
not finish.

Each thread now indexes its entries twice, in open-addressing tables with
linear probing and backward-shift deletion: by collection, key and kind,
for get, set, has and delete, and by key pointer, for a key's disposal,
which ends the key's entries in every collection. A collection's entries
and a key's entries are doubly linked, so an entry leaves both in
constant time. The tables grow at half load and are freed when their last
entry goes, so an exiting worker leaves none. 200,000 keys now take
0.24 s (Node 0.30 s).

Closes vercel-labs#1702

Co-authored-by: ejc3 <1694850+ejc3@users.noreply.github.com>
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.

WeakMap and WeakSet operations scan every entry, so filling one is quadratic

1 participant