mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-29 07:04:09 +08:00
Find weak collection entries through hashed indexes (#2004)
This commit is contained in:
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/7742-weak-collections-many-keys.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
+173
-33
@@ -16274,26 +16274,176 @@ typedef struct ScrWeakEntry {
|
||||
unsigned kind;
|
||||
ScrDyn *value;
|
||||
ScrWeakMap *owner;
|
||||
struct ScrWeakEntry *next, *all_next;
|
||||
/* The owner's entries, and every collection's entries for the same key
|
||||
* pointer (whatever their kind): both doubly linked, so an entry leaves
|
||||
* either list in constant time. */
|
||||
struct ScrWeakEntry *next, *prev, *key_next, *key_prev;
|
||||
} ScrWeakEntry;
|
||||
|
||||
struct ScrWeakMap { size_t rc; ScrWeakEntry *entries; bool is_set; };
|
||||
/* Weak observers borrow key identities. Values are traced strong edges;
|
||||
* value-to-key cycles require explicit deletion or container disposal. */
|
||||
#define SCR_WEAK_BUCKETS 1024
|
||||
static SCR_TL ScrWeakEntry *scr_weak_entries[SCR_WEAK_BUCKETS];
|
||||
static size_t scr_weak_bucket(void *key) {
|
||||
uintptr_t bits = (uintptr_t)key;
|
||||
return ((bits >> 4) ^ (bits >> 14)) & (SCR_WEAK_BUCKETS - 1);
|
||||
* value-to-key cycles require explicit deletion or container disposal.
|
||||
*
|
||||
* Two open-addressing indexes per thread find entries in expected constant
|
||||
* time: scr_weak_index by (collection, key, kind), for get/set/has/delete,
|
||||
* and scr_weak_keys by key pointer, the head of that key's entries, for a
|
||||
* disposal, which ends the key's entries in every collection. Both use
|
||||
* linear probing with backward-shift deletion, grow at half load, and are
|
||||
* freed when their last entry goes. */
|
||||
static SCR_TL ScrWeakEntry **scr_weak_index;
|
||||
static SCR_TL size_t scr_weak_index_mask, scr_weak_index_used;
|
||||
typedef struct { void *key; ScrWeakEntry *head; } ScrWeakKeySlot;
|
||||
static SCR_TL ScrWeakKeySlot *scr_weak_keys;
|
||||
static SCR_TL size_t scr_weak_keys_mask, scr_weak_keys_used;
|
||||
|
||||
/* Mixed in 64 bits on every target: uintptr_t is 32 bits on wasm32, where
|
||||
* these shifts would exceed its width. */
|
||||
static inline size_t scr_weak_mix(uint64_t bits) {
|
||||
bits ^= bits >> 33;
|
||||
bits *= UINT64_C(0xff51afd7ed558ccd);
|
||||
bits ^= bits >> 29;
|
||||
return (size_t)bits;
|
||||
}
|
||||
static inline size_t scr_weak_entry_hash(const ScrWeakMap *owner, const void *key, unsigned kind) {
|
||||
return scr_weak_mix((uint64_t)(uintptr_t)key ^ ((uint64_t)(uintptr_t)owner * UINT64_C(0x9E3779B97F4A7C15)) ^
|
||||
((uint64_t)kind << 56));
|
||||
}
|
||||
static inline size_t scr_weak_key_hash(const void *key) { return scr_weak_mix((uint64_t)(uintptr_t)key); }
|
||||
|
||||
static void scr_weak_index_put(ScrWeakEntry *entry) {
|
||||
size_t j = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & scr_weak_index_mask;
|
||||
while (scr_weak_index[j]) j = (j + 1) & scr_weak_index_mask;
|
||||
scr_weak_index[j] = entry;
|
||||
}
|
||||
static void scr_weak_index_reserve(void) {
|
||||
if (scr_weak_index && (scr_weak_index_used + 1) * 2 <= scr_weak_index_mask + 1) return;
|
||||
ScrWeakEntry **old = scr_weak_index;
|
||||
size_t n = old ? scr_weak_index_mask + 1 : 0, cap = n ? n * 2 : 64;
|
||||
scr_weak_index = calloc(cap, sizeof *scr_weak_index);
|
||||
if (!scr_weak_index) scr_json_oom();
|
||||
scr_weak_index_mask = cap - 1;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
if (old[i]) scr_weak_index_put(old[i]);
|
||||
free(old);
|
||||
}
|
||||
static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) {
|
||||
if (!scr_weak_index) return NULL;
|
||||
for (size_t j = scr_weak_entry_hash(map, ptr, kind) & scr_weak_index_mask;; j = (j + 1) & scr_weak_index_mask) {
|
||||
ScrWeakEntry *entry = scr_weak_index[j];
|
||||
if (!entry) return NULL;
|
||||
if (entry->owner == map && entry->key == ptr && entry->kind == kind) return entry;
|
||||
}
|
||||
}
|
||||
static void scr_weak_index_delete(ScrWeakEntry *entry) {
|
||||
size_t mask = scr_weak_index_mask;
|
||||
size_t i = scr_weak_entry_hash(entry->owner, entry->key, entry->kind) & mask;
|
||||
while (scr_weak_index[i] != entry) i = (i + 1) & mask;
|
||||
if (--scr_weak_index_used == 0) {
|
||||
/* Empty: give the table back, so a thread that exits leaves none. */
|
||||
free(scr_weak_index);
|
||||
scr_weak_index = NULL;
|
||||
return;
|
||||
}
|
||||
for (size_t k = i;;) {
|
||||
scr_weak_index[i] = NULL;
|
||||
for (;;) {
|
||||
k = (k + 1) & mask;
|
||||
ScrWeakEntry *e = scr_weak_index[k];
|
||||
if (!e) return;
|
||||
size_t home = scr_weak_entry_hash(e->owner, e->key, e->kind) & mask;
|
||||
/* e stays unless its home lies cyclically outside (i, k]. */
|
||||
if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue;
|
||||
scr_weak_index[i] = e;
|
||||
i = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static ScrWeakKeySlot *scr_weak_key_slot(const void *key) {
|
||||
if (!scr_weak_keys) return NULL;
|
||||
for (size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask;; j = (j + 1) & scr_weak_keys_mask) {
|
||||
if (scr_weak_keys[j].key == key) return &scr_weak_keys[j];
|
||||
if (!scr_weak_keys[j].key) return NULL;
|
||||
}
|
||||
}
|
||||
static ScrWeakKeySlot *scr_weak_key_slot_put(void *key) {
|
||||
if (!scr_weak_keys || (scr_weak_keys_used + 1) * 2 > scr_weak_keys_mask + 1) {
|
||||
ScrWeakKeySlot *old = scr_weak_keys;
|
||||
size_t n = old ? scr_weak_keys_mask + 1 : 0, cap = n ? n * 2 : 64;
|
||||
scr_weak_keys = calloc(cap, sizeof *scr_weak_keys);
|
||||
if (!scr_weak_keys) scr_json_oom();
|
||||
scr_weak_keys_mask = cap - 1;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!old[i].key) continue;
|
||||
size_t j = scr_weak_key_hash(old[i].key) & scr_weak_keys_mask;
|
||||
while (scr_weak_keys[j].key) j = (j + 1) & scr_weak_keys_mask;
|
||||
scr_weak_keys[j] = old[i];
|
||||
}
|
||||
free(old);
|
||||
}
|
||||
size_t j = scr_weak_key_hash(key) & scr_weak_keys_mask;
|
||||
while (scr_weak_keys[j].key && scr_weak_keys[j].key != key) j = (j + 1) & scr_weak_keys_mask;
|
||||
if (!scr_weak_keys[j].key) {
|
||||
scr_weak_keys[j].key = key;
|
||||
scr_weak_keys[j].head = NULL;
|
||||
scr_weak_keys_used++;
|
||||
}
|
||||
return &scr_weak_keys[j];
|
||||
}
|
||||
static void scr_weak_key_slot_delete(ScrWeakKeySlot *slot) {
|
||||
size_t mask = scr_weak_keys_mask;
|
||||
size_t i = (size_t)(slot - scr_weak_keys);
|
||||
if (--scr_weak_keys_used == 0) {
|
||||
free(scr_weak_keys);
|
||||
scr_weak_keys = NULL;
|
||||
return;
|
||||
}
|
||||
for (size_t k = i;;) {
|
||||
scr_weak_keys[i].key = NULL;
|
||||
scr_weak_keys[i].head = NULL;
|
||||
for (;;) {
|
||||
k = (k + 1) & mask;
|
||||
if (!scr_weak_keys[k].key) return;
|
||||
size_t home = scr_weak_key_hash(scr_weak_keys[k].key) & mask;
|
||||
if (i <= k ? (i < home && home <= k) : (i < home || home <= k)) continue;
|
||||
scr_weak_keys[i] = scr_weak_keys[k];
|
||||
i = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Indexes a new entry (its key, kind and owner set) under its key. */
|
||||
static void scr_weak_link(ScrWeakEntry *entry) {
|
||||
scr_weak_index_reserve();
|
||||
scr_weak_index_put(entry);
|
||||
scr_weak_index_used++;
|
||||
ScrWeakKeySlot *slot = scr_weak_key_slot_put(entry->key);
|
||||
entry->key_prev = NULL;
|
||||
entry->key_next = slot->head;
|
||||
if (slot->head) slot->head->key_prev = entry;
|
||||
slot->head = entry;
|
||||
}
|
||||
|
||||
/* Takes an entry out of both indexes (it stays in its owner's list). */
|
||||
static void scr_weak_unlink(ScrWeakEntry *entry) {
|
||||
scr_weak_index_delete(entry);
|
||||
if (entry->key_next) entry->key_next->key_prev = entry->key_prev;
|
||||
if (entry->key_prev) entry->key_prev->key_next = entry->key_next;
|
||||
else {
|
||||
ScrWeakKeySlot *slot = scr_weak_key_slot(entry->key);
|
||||
if (entry->key_next) slot->head = entry->key_next;
|
||||
else scr_weak_key_slot_delete(slot);
|
||||
}
|
||||
}
|
||||
|
||||
static void scr_weak_remove_impl(ScrWeakEntry *entry, bool collected) {
|
||||
ScrWeakEntry **slot = &entry->owner->entries;
|
||||
while (*slot != entry) slot = &(*slot)->next;
|
||||
*slot = entry->next;
|
||||
slot = &scr_weak_entries[scr_weak_bucket(entry->key)];
|
||||
while (*slot != entry) slot = &(*slot)->all_next;
|
||||
*slot = entry->all_next;
|
||||
ScrWeakMap *owner = entry->owner;
|
||||
if (entry->next) entry->next->prev = entry->prev;
|
||||
if (entry->prev) entry->prev->next = entry->next;
|
||||
else owner->entries = entry->next;
|
||||
scr_weak_unlink(entry);
|
||||
ScrDyn *value = entry->value;
|
||||
free(entry);
|
||||
/* Unlink before release: releasing a value can dispose another key. */
|
||||
@@ -16317,10 +16467,9 @@ static void scr_weak_map_gcfree(void *ptr) {
|
||||
|
||||
static void scr_weak_disposed(void *key) {
|
||||
for (;;) {
|
||||
ScrWeakEntry *entry = scr_weak_entries[scr_weak_bucket(key)];
|
||||
while (entry && entry->key != key) entry = entry->all_next;
|
||||
if (!entry) return;
|
||||
scr_weak_remove(entry);
|
||||
ScrWeakKeySlot *slot = scr_weak_key_slot(key);
|
||||
if (!slot) return;
|
||||
scr_weak_remove(slot->head);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16378,12 +16527,6 @@ static int scr_weak_key(const ScrDyn *key, void **ptr, unsigned *kind) {
|
||||
}
|
||||
}
|
||||
|
||||
static ScrWeakEntry *scr_weak_find(ScrWeakMap *map, void *ptr, unsigned kind) {
|
||||
for (ScrWeakEntry *entry = map->entries; entry; entry = entry->next)
|
||||
if (entry->key == ptr && entry->kind == kind) return entry;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) {
|
||||
void *ptr = NULL;
|
||||
unsigned kind = 0;
|
||||
@@ -16412,10 +16555,9 @@ static bool scr_weak_set(ScrWeakMap *map, ScrDyn *key, ScrDyn *value) {
|
||||
entry->value = scr_dyn_retain(value);
|
||||
entry->owner = map;
|
||||
entry->next = map->entries;
|
||||
if (map->entries) map->entries->prev = entry;
|
||||
map->entries = entry;
|
||||
size_t bucket = scr_weak_bucket(ptr);
|
||||
entry->all_next = scr_weak_entries[bucket];
|
||||
scr_weak_entries[bucket] = entry;
|
||||
scr_weak_link(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -17652,16 +17794,14 @@ static bool scr_dyn_decl_rekey(ScrDyn *node, void *ptr) {
|
||||
bool moved = false;
|
||||
if (!scr_weak_observing) return false;
|
||||
for (;;) {
|
||||
ScrWeakEntry **slot = &scr_weak_entries[scr_weak_bucket(node)];
|
||||
while (*slot && !((*slot)->key == (void *)node && (*slot)->kind == 0)) slot = &(*slot)->all_next;
|
||||
ScrWeakEntry *entry = *slot;
|
||||
ScrWeakKeySlot *slot = scr_weak_key_slot(node);
|
||||
ScrWeakEntry *entry = slot ? slot->head : NULL;
|
||||
while (entry && entry->kind != 0) entry = entry->key_next;
|
||||
if (!entry) return moved;
|
||||
*slot = entry->all_next;
|
||||
scr_weak_unlink(entry);
|
||||
entry->key = ptr;
|
||||
entry->kind = 32;
|
||||
ScrWeakEntry **head = &scr_weak_entries[scr_weak_bucket(ptr)];
|
||||
entry->all_next = *head;
|
||||
*head = entry;
|
||||
scr_weak_link(entry);
|
||||
moved = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { execFile } from "node:child_process";
|
||||
import { execFile, execFileSync } from "node:child_process";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
@@ -84,3 +84,32 @@ test("json runtime: parser, dyn, dynCheck failure path, stringify buffer", async
|
||||
].join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
function zigOnPath(): boolean {
|
||||
try {
|
||||
execFileSync("zig", ["version"], { stdio: "ignore" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 32-bit uintptr_t: the weak-collection hashes mix in 64 bits, so no shift
|
||||
// exceeds the width of its operand on wasm32.
|
||||
test.skipIf(!zigOnPath())("json runtime builds for wasm32 without oversized shifts", async () => {
|
||||
await execFileAsync("zig", [
|
||||
"cc",
|
||||
"-target",
|
||||
"wasm32-wasi",
|
||||
"-std=c11",
|
||||
"-O2",
|
||||
"-D_GNU_SOURCE",
|
||||
"-D_WASI_EMULATED_SIGNAL",
|
||||
"-D_WASI_EMULATED_PROCESS_CLOCKS",
|
||||
"-Werror=shift-count-overflow",
|
||||
"-c",
|
||||
"-o",
|
||||
join(testDir, "build", "scr_json.wasm32.o"),
|
||||
join(testDir, "../src/scr_json.c"),
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Weak collections with many keys: entries found among tens of thousands
|
||||
// in WeakMaps and WeakSets that share keys, deleted and replaced, and gone
|
||||
// with their keys, so blocks a new key reuses are not found under it.
|
||||
// Keys in reference cycles die by the cycle collector.
|
||||
class Key {
|
||||
readonly id: number;
|
||||
peer: Key | null = null;
|
||||
constructor(id: number) {
|
||||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
const n = 40000;
|
||||
let keys: Key[] = [];
|
||||
for (let i = 0; i < n; i++) keys.push(new Key(i));
|
||||
const values = new WeakMap<Key, number>();
|
||||
const labels = new WeakMap<Key, string>();
|
||||
const marked = new WeakSet<Key>();
|
||||
for (const k of keys) {
|
||||
values.set(k, k.id * 2);
|
||||
if (k.id % 3 === 0) labels.set(k, "k" + k.id);
|
||||
if (k.id % 2 === 0) marked.add(k);
|
||||
}
|
||||
let sum = 0;
|
||||
let labelled = 0;
|
||||
let counted = 0;
|
||||
for (const k of keys) {
|
||||
sum += values.get(k) ?? 0;
|
||||
if (labels.has(k)) labelled++;
|
||||
if (marked.has(k)) counted++;
|
||||
}
|
||||
console.log("filled", sum, labelled, counted);
|
||||
|
||||
// Deleting every fourth key from one collection leaves the others alone.
|
||||
let deleted = 0;
|
||||
for (const k of keys) if (k.id % 4 === 0 && values.delete(k)) deleted++;
|
||||
let remaining = 0;
|
||||
for (const k of keys) if (values.has(k)) remaining++;
|
||||
console.log("deleted", deleted, remaining, values.delete(keys[0]!), marked.has(keys[0]!), labels.get(keys[3]!));
|
||||
|
||||
// Overwriting keeps one entry per key.
|
||||
for (const k of keys) if (k.id % 5 === 0) values.set(k, -1);
|
||||
let negative = 0;
|
||||
for (const k of keys) if (values.get(k) === -1) negative++;
|
||||
console.log("overwritten", negative);
|
||||
|
||||
// Dropping the keys ends their entries; new keys are not found.
|
||||
keys = [];
|
||||
const fresh: Key[] = [];
|
||||
for (let i = 0; i < n; i++) fresh.push(new Key(n + i));
|
||||
let stale = 0;
|
||||
for (const k of fresh) if (values.has(k) || labels.has(k) || marked.has(k)) stale++;
|
||||
console.log("fresh", fresh.length, stale);
|
||||
|
||||
// Keys that reach each other die together through the cycle collector.
|
||||
for (let round = 0; round < 3; round++) {
|
||||
let ring: Key[] = [];
|
||||
for (let i = 0; i < 1000; i++) ring.push(new Key(i));
|
||||
for (let i = 0; i < ring.length; i++) ring[i]!.peer = ring[(i + 1) % ring.length]!;
|
||||
for (const k of ring) {
|
||||
values.set(k, round);
|
||||
marked.add(k);
|
||||
}
|
||||
ring = [];
|
||||
}
|
||||
const after: Key[] = [];
|
||||
for (let i = 0; i < 3000; i++) after.push(new Key(i));
|
||||
let found = 0;
|
||||
for (const k of after) if (values.has(k) || marked.has(k)) found++;
|
||||
console.log("cycles", found);
|
||||
@@ -0,0 +1 @@
|
||||
{ "plain": 2500, "san": 6000 }
|
||||
@@ -313,6 +313,9 @@ describe.skipIf(!zigOnPath())("wasm32-wasi differential", () => {
|
||||
// ThreadSlots blocks and pages hold 32-bit pointers.
|
||||
"5727-thread-slots-basics.ts",
|
||||
"5729-thread-slots-values.ts",
|
||||
// Weak-collection hashes mix 32-bit pointers in 64 bits.
|
||||
"4591-weak-keys-union-records.ts",
|
||||
"7742-weak-collections-many-keys.ts",
|
||||
])("the LLVM target runs %s byte-identically to Node", async (fixture) => {
|
||||
const entry = join(repoRoot, "tests/corpus", fixture);
|
||||
const outDir = await mkdtemp("/tmp/scriptc-wasi-");
|
||||
|
||||
Reference in New Issue
Block a user