diff --git a/Plugins/PackageToJS/Templates/runtime.d.ts b/Plugins/PackageToJS/Templates/runtime.d.ts index 353db3894..d1ef0eaf5 100644 --- a/Plugins/PackageToJS/Templates/runtime.d.ts +++ b/Plugins/PackageToJS/Templates/runtime.d.ts @@ -2,14 +2,16 @@ type ref = number; type pointer = number; declare class JSObjectSpace { - private _heapValueById; - private _heapEntryByValue; - private _heapNextKey; + private _slotByValue; + private _values; + private _stateBySlot; + private _freeSlotStack; constructor(); retain(value: any): number; - retainByRef(ref: ref): number; - release(ref: ref): void; - getObject(ref: ref): any; + retainByRef(reference: ref): number; + release(reference: ref): void; + getObject(reference: ref): any; + private _getValidatedSlotState; } /** diff --git a/Plugins/PackageToJS/Templates/runtime.mjs b/Plugins/PackageToJS/Templates/runtime.mjs index d79275476..4585004d6 100644 --- a/Plugins/PackageToJS/Templates/runtime.mjs +++ b/Plugins/PackageToJS/Templates/runtime.mjs @@ -238,44 +238,90 @@ function deserializeError(error) { const globalVariable = globalThis; +const SLOT_BITS = 22; +const SLOT_MASK = (1 << SLOT_BITS) - 1; +const GEN_MASK = (1 << (32 - SLOT_BITS)) - 1; class JSObjectSpace { constructor() { - this._heapValueById = new Map(); - this._heapValueById.set(1, globalVariable); - this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 }); - // Note: 0 is preserved for invalid references, 1 is preserved for globalThis - this._heapNextKey = 2; + this._slotByValue = new Map(); + this._values = []; + this._stateBySlot = []; + this._freeSlotStack = []; + this._values[0] = undefined; + this._values[1] = globalVariable; + this._slotByValue.set(globalVariable, 1); + this._stateBySlot[1] = 1; // gen=0, rc=1 } retain(value) { - const entry = this._heapEntryByValue.get(value); - if (entry) { - entry.rc++; - return entry.id; - } - const id = this._heapNextKey++; - this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - return id; - } - retainByRef(ref) { - return this.retain(this.getObject(ref)); - } - release(ref) { - const value = this._heapValueById.get(ref); - const entry = this._heapEntryByValue.get(value); - entry.rc--; - if (entry.rc != 0) + const slot = this._slotByValue.get(value); + if (slot !== undefined) { + const state = this._stateBySlot[slot]; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError(`Reference count overflow at slot ${slot}`); + } + this._stateBySlot[slot] = nextState; + return ((nextState & ~SLOT_MASK) | slot) >>> 0; + } + let newSlot; + let state; + if (this._freeSlotStack.length > 0) { + newSlot = this._freeSlotStack.pop(); + const gen = this._stateBySlot[newSlot] >>> SLOT_BITS; + state = ((gen << SLOT_BITS) | 1) >>> 0; + } + else { + newSlot = this._values.length; + if (newSlot > SLOT_MASK) { + throw new RangeError(`Reference slot overflow: ${newSlot} exceeds ${SLOT_MASK}`); + } + state = 1; + } + this._stateBySlot[newSlot] = state; + this._values[newSlot] = value; + this._slotByValue.set(value, newSlot); + return ((state & ~SLOT_MASK) | newSlot) >>> 0; + } + retainByRef(reference) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError(`Reference count overflow at slot ${slot}`); + } + this._stateBySlot[slot] = nextState; + return reference; + } + release(reference) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + if ((state & SLOT_MASK) > 1) { + this._stateBySlot[slot] = (state - 1) >>> 0; return; - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } - getObject(ref) { - const value = this._heapValueById.get(ref); - if (value === undefined) { - throw new ReferenceError("Attempted to read invalid reference " + ref); } - return value; + this._slotByValue.delete(this._values[slot]); + this._values[slot] = undefined; + const nextGen = ((state >>> SLOT_BITS) + 1) & GEN_MASK; + this._stateBySlot[slot] = (nextGen << SLOT_BITS) >>> 0; + this._freeSlotStack.push(slot); + } + getObject(reference) { + this._getValidatedSlotState(reference); + return this._values[reference & SLOT_MASK]; + } + // Returns the packed state for the slot, after validating the reference. + _getValidatedSlotState(reference) { + const slot = reference & SLOT_MASK; + if (slot === 0) + throw new ReferenceError("Attempted to use invalid reference " + reference); + const state = this._stateBySlot[slot]; + if (state === undefined || (state & SLOT_MASK) === 0) { + throw new ReferenceError("Attempted to use invalid reference " + reference); + } + if ((state >>> SLOT_BITS) !== (reference >>> SLOT_BITS)) { + throw new ReferenceError("Attempted to use stale reference " + reference); + } + return state; } } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index ba9cf8021..fa8363413 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -1,59 +1,113 @@ import { globalVariable } from "./find-global.js"; import { ref } from "./types.js"; -type SwiftRuntimeHeapEntry = { - id: number; - rc: number; -}; +const SLOT_BITS = 22; +const SLOT_MASK = (1 << SLOT_BITS) - 1; +const GEN_MASK = (1 << (32 - SLOT_BITS)) - 1; + export class JSObjectSpace { - private _heapValueById: Map; - private _heapEntryByValue: Map; - private _heapNextKey: number; + private _slotByValue: Map; + private _values: (any | undefined)[]; + private _stateBySlot: number[]; + private _freeSlotStack: number[]; constructor() { - this._heapValueById = new Map(); - this._heapValueById.set(1, globalVariable); - - this._heapEntryByValue = new Map(); - this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 }); + this._slotByValue = new Map(); + this._values = []; + this._stateBySlot = []; + this._freeSlotStack = []; - // Note: 0 is preserved for invalid references, 1 is preserved for globalThis - this._heapNextKey = 2; + this._values[0] = undefined; + this._values[1] = globalVariable; + this._slotByValue.set(globalVariable, 1); + this._stateBySlot[1] = 1; // gen=0, rc=1 } retain(value: any) { - const entry = this._heapEntryByValue.get(value); - if (entry) { - entry.rc++; - return entry.id; + const slot = this._slotByValue.get(value); + if (slot !== undefined) { + const state = this._stateBySlot[slot]!; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError( + `Reference count overflow at slot ${slot}`, + ); + } + this._stateBySlot[slot] = nextState; + return ((nextState & ~SLOT_MASK) | slot) >>> 0; + } + + let newSlot: number; + let state: number; + if (this._freeSlotStack.length > 0) { + newSlot = this._freeSlotStack.pop()!; + const gen = this._stateBySlot[newSlot]! >>> SLOT_BITS; + state = ((gen << SLOT_BITS) | 1) >>> 0; + } else { + newSlot = this._values.length; + if (newSlot > SLOT_MASK) { + throw new RangeError( + `Reference slot overflow: ${newSlot} exceeds ${SLOT_MASK}`, + ); + } + state = 1; } - const id = this._heapNextKey++; - this._heapValueById.set(id, value); - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - return id; + + this._stateBySlot[newSlot] = state; + this._values[newSlot] = value; + this._slotByValue.set(value, newSlot); + return ((state & ~SLOT_MASK) | newSlot) >>> 0; } - retainByRef(ref: ref) { - return this.retain(this.getObject(ref)); + retainByRef(reference: ref) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + const nextState = (state + 1) >>> 0; + if ((nextState & SLOT_MASK) === 0) { + throw new RangeError(`Reference count overflow at slot ${slot}`); + } + this._stateBySlot[slot] = nextState; + return reference; } - release(ref: ref) { - const value = this._heapValueById.get(ref); - const entry = this._heapEntryByValue.get(value)!; - entry.rc--; - if (entry.rc != 0) return; + release(reference: ref) { + const state = this._getValidatedSlotState(reference); + const slot = reference & SLOT_MASK; + if ((state & SLOT_MASK) > 1) { + this._stateBySlot[slot] = (state - 1) >>> 0; + return; + } + + this._slotByValue.delete(this._values[slot]); + this._values[slot] = undefined; + const nextGen = ((state >>> SLOT_BITS) + 1) & GEN_MASK; + this._stateBySlot[slot] = (nextGen << SLOT_BITS) >>> 0; + this._freeSlotStack.push(slot); + } - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); + getObject(reference: ref) { + this._getValidatedSlotState(reference); + return this._values[reference & SLOT_MASK]; } - getObject(ref: ref) { - const value = this._heapValueById.get(ref); - if (value === undefined) { + // Returns the packed state for the slot, after validating the reference. + private _getValidatedSlotState(reference: ref): number { + const slot = reference & SLOT_MASK; + if (slot === 0) + throw new ReferenceError( + "Attempted to use invalid reference " + reference, + ); + const state = this._stateBySlot[slot]; + if (state === undefined || (state & SLOT_MASK) === 0) { + throw new ReferenceError( + "Attempted to use invalid reference " + reference, + ); + } + if (state >>> SLOT_BITS !== reference >>> SLOT_BITS) { throw new ReferenceError( - "Attempted to read invalid reference " + ref, + "Attempted to use stale reference " + reference, ); } - return value; + return state; } } diff --git a/Tests/JavaScriptKitTests/JSClosureTests.swift b/Tests/JavaScriptKitTests/JSClosureTests.swift index 3d609a9b9..e278656d8 100644 --- a/Tests/JavaScriptKitTests/JSClosureTests.swift +++ b/Tests/JavaScriptKitTests/JSClosureTests.swift @@ -92,52 +92,38 @@ class JSClosureTests: XCTestCase { throw XCTSkip("Missing --expose-gc flag") } - // Step 1: Create many JSClosure instances + // Step 1: Create many source closures and keep only JS references alive. + // These closures must remain callable even after heavy finalizer churn. let obj = JSObject() - var closurePointers: Set = [] let numberOfSourceClosures = 10_000 do { var closures: [JSClosure] = [] for i in 0.. maxClosurePointer { - break + let numberOfProbeClosures = 50_000 + for i in 0..