Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f1f290b

Browse files
authored
faster JSObjectSpace (JS runtime retain / release) (#676)
1 parent 5e96639 commit f1f290b

4 files changed

Lines changed: 192 additions & 102 deletions

File tree

Plugins/PackageToJS/Templates/runtime.d.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@ type ref = number;
22
type pointer = number;
33

44
declare class JSObjectSpace {
5-
private _heapValueById;
6-
private _heapEntryByValue;
7-
private _heapNextKey;
5+
private _slotByValue;
6+
private _values;
7+
private _stateBySlot;
8+
private _freeSlotStack;
89
constructor();
910
retain(value: any): number;
10-
retainByRef(ref: ref): number;
11-
release(ref: ref): void;
12-
getObject(ref: ref): any;
11+
retainByRef(reference: ref): number;
12+
release(reference: ref): void;
13+
getObject(reference: ref): any;
14+
private _getValidatedSlotState;
1315
}
1416

1517
/**

Plugins/PackageToJS/Templates/runtime.mjs

Lines changed: 78 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -241,44 +241,91 @@ function deserializeError(error) {
241241

242242
const globalVariable = globalThis;
243243

244+
const SLOT_BITS = 24;
245+
const SLOT_MASK = (1 << SLOT_BITS) - 1;
246+
const GEN_MASK = (1 << (32 - SLOT_BITS)) - 1;
244247
class JSObjectSpace {
245248
constructor() {
246-
this._heapValueById = new Map();
247-
this._heapValueById.set(1, globalVariable);
248-
this._heapEntryByValue = new Map();
249-
this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 });
249+
this._slotByValue = new Map();
250+
this._values = [];
251+
this._stateBySlot = [];
252+
this._freeSlotStack = [];
250253
// Note: 0 is preserved for invalid references, 1 is preserved for globalThis
251-
this._heapNextKey = 2;
254+
this._values[0] = undefined;
255+
this._values[1] = globalVariable;
256+
this._slotByValue.set(globalVariable, 1);
257+
this._stateBySlot[1] = 1; // gen=0, rc=1
252258
}
253259
retain(value) {
254-
const entry = this._heapEntryByValue.get(value);
255-
if (entry) {
256-
entry.rc++;
257-
return entry.id;
258-
}
259-
const id = this._heapNextKey++;
260-
this._heapValueById.set(id, value);
261-
this._heapEntryByValue.set(value, { id: id, rc: 1 });
262-
return id;
263-
}
264-
retainByRef(ref) {
265-
return this.retain(this.getObject(ref));
266-
}
267-
release(ref) {
268-
const value = this._heapValueById.get(ref);
269-
const entry = this._heapEntryByValue.get(value);
270-
entry.rc--;
271-
if (entry.rc != 0)
260+
const slot = this._slotByValue.get(value);
261+
if (slot !== undefined) {
262+
const state = this._stateBySlot[slot];
263+
const nextState = (state + 1) >>> 0;
264+
if ((nextState & SLOT_MASK) === 0) {
265+
throw new RangeError(`Reference count overflow at slot ${slot}`);
266+
}
267+
this._stateBySlot[slot] = nextState;
268+
return ((nextState & ~SLOT_MASK) | slot) >>> 0;
269+
}
270+
let newSlot;
271+
let state;
272+
if (this._freeSlotStack.length > 0) {
273+
newSlot = this._freeSlotStack.pop();
274+
const gen = this._stateBySlot[newSlot] >>> SLOT_BITS;
275+
state = ((gen << SLOT_BITS) | 1) >>> 0;
276+
}
277+
else {
278+
newSlot = this._values.length;
279+
if (newSlot > SLOT_MASK) {
280+
throw new RangeError(`Reference slot overflow: ${newSlot} exceeds ${SLOT_MASK}`);
281+
}
282+
state = 1;
283+
}
284+
this._stateBySlot[newSlot] = state;
285+
this._values[newSlot] = value;
286+
this._slotByValue.set(value, newSlot);
287+
return ((state & ~SLOT_MASK) | newSlot) >>> 0;
288+
}
289+
retainByRef(reference) {
290+
const state = this._getValidatedSlotState(reference);
291+
const slot = reference & SLOT_MASK;
292+
const nextState = (state + 1) >>> 0;
293+
if ((nextState & SLOT_MASK) === 0) {
294+
throw new RangeError(`Reference count overflow at slot ${slot}`);
295+
}
296+
this._stateBySlot[slot] = nextState;
297+
return reference;
298+
}
299+
release(reference) {
300+
const state = this._getValidatedSlotState(reference);
301+
const slot = reference & SLOT_MASK;
302+
if ((state & SLOT_MASK) > 1) {
303+
this._stateBySlot[slot] = (state - 1) >>> 0;
272304
return;
273-
this._heapEntryByValue.delete(value);
274-
this._heapValueById.delete(ref);
275-
}
276-
getObject(ref) {
277-
const value = this._heapValueById.get(ref);
278-
if (value === undefined) {
279-
throw new ReferenceError("Attempted to read invalid reference " + ref);
280305
}
281-
return value;
306+
this._slotByValue.delete(this._values[slot]);
307+
this._values[slot] = undefined;
308+
const nextGen = ((state >>> SLOT_BITS) + 1) & GEN_MASK;
309+
this._stateBySlot[slot] = (nextGen << SLOT_BITS) >>> 0;
310+
this._freeSlotStack.push(slot);
311+
}
312+
getObject(reference) {
313+
this._getValidatedSlotState(reference);
314+
return this._values[reference & SLOT_MASK];
315+
}
316+
// Returns the packed state for the slot, after validating the reference.
317+
_getValidatedSlotState(reference) {
318+
const slot = reference & SLOT_MASK;
319+
if (slot === 0)
320+
throw new ReferenceError(`Attempted to use invalid reference ${reference}`);
321+
const state = this._stateBySlot[slot];
322+
if (state === undefined || (state & SLOT_MASK) === 0) {
323+
throw new ReferenceError(`Attempted to use invalid reference ${reference}`);
324+
}
325+
if (state >>> SLOT_BITS !== reference >>> SLOT_BITS) {
326+
throw new ReferenceError(`Attempted to use stale reference ${reference}`);
327+
}
328+
return state;
282329
}
283330
}
284331

Runtime/src/object-heap.ts

Lines changed: 90 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,114 @@
11
import { globalVariable } from "./find-global.js";
22
import { ref } from "./types.js";
33

4-
type SwiftRuntimeHeapEntry = {
5-
id: number;
6-
rc: number;
7-
};
4+
const SLOT_BITS = 24;
5+
const SLOT_MASK = (1 << SLOT_BITS) - 1;
6+
const GEN_MASK = (1 << (32 - SLOT_BITS)) - 1;
7+
88
export class JSObjectSpace {
9-
private _heapValueById: Map<number, any>;
10-
private _heapEntryByValue: Map<any, SwiftRuntimeHeapEntry>;
11-
private _heapNextKey: number;
9+
private _slotByValue: Map<any, number>;
10+
private _values: (any | undefined)[];
11+
private _stateBySlot: number[];
12+
private _freeSlotStack: number[];
1213

1314
constructor() {
14-
this._heapValueById = new Map();
15-
this._heapValueById.set(1, globalVariable);
16-
17-
this._heapEntryByValue = new Map();
18-
this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 });
15+
this._slotByValue = new Map();
16+
this._values = [];
17+
this._stateBySlot = [];
18+
this._freeSlotStack = [];
1919

2020
// Note: 0 is preserved for invalid references, 1 is preserved for globalThis
21-
this._heapNextKey = 2;
21+
this._values[0] = undefined;
22+
this._values[1] = globalVariable;
23+
this._slotByValue.set(globalVariable, 1);
24+
this._stateBySlot[1] = 1; // gen=0, rc=1
2225
}
2326

2427
retain(value: any) {
25-
const entry = this._heapEntryByValue.get(value);
26-
if (entry) {
27-
entry.rc++;
28-
return entry.id;
28+
const slot = this._slotByValue.get(value);
29+
if (slot !== undefined) {
30+
const state = this._stateBySlot[slot]!;
31+
const nextState = (state + 1) >>> 0;
32+
if ((nextState & SLOT_MASK) === 0) {
33+
throw new RangeError(
34+
`Reference count overflow at slot ${slot}`,
35+
);
36+
}
37+
this._stateBySlot[slot] = nextState;
38+
return ((nextState & ~SLOT_MASK) | slot) >>> 0;
39+
}
40+
41+
let newSlot: number;
42+
let state: number;
43+
if (this._freeSlotStack.length > 0) {
44+
newSlot = this._freeSlotStack.pop()!;
45+
const gen = this._stateBySlot[newSlot]! >>> SLOT_BITS;
46+
state = ((gen << SLOT_BITS) | 1) >>> 0;
47+
} else {
48+
newSlot = this._values.length;
49+
if (newSlot > SLOT_MASK) {
50+
throw new RangeError(
51+
`Reference slot overflow: ${newSlot} exceeds ${SLOT_MASK}`,
52+
);
53+
}
54+
state = 1;
2955
}
30-
const id = this._heapNextKey++;
31-
this._heapValueById.set(id, value);
32-
this._heapEntryByValue.set(value, { id: id, rc: 1 });
33-
return id;
56+
57+
this._stateBySlot[newSlot] = state;
58+
this._values[newSlot] = value;
59+
this._slotByValue.set(value, newSlot);
60+
return ((state & ~SLOT_MASK) | newSlot) >>> 0;
3461
}
3562

36-
retainByRef(ref: ref) {
37-
return this.retain(this.getObject(ref));
63+
retainByRef(reference: ref) {
64+
const state = this._getValidatedSlotState(reference);
65+
const slot = reference & SLOT_MASK;
66+
const nextState = (state + 1) >>> 0;
67+
if ((nextState & SLOT_MASK) === 0) {
68+
throw new RangeError(`Reference count overflow at slot ${slot}`);
69+
}
70+
this._stateBySlot[slot] = nextState;
71+
return reference;
3872
}
3973

40-
release(ref: ref) {
41-
const value = this._heapValueById.get(ref);
42-
const entry = this._heapEntryByValue.get(value)!;
43-
entry.rc--;
44-
if (entry.rc != 0) return;
74+
release(reference: ref) {
75+
const state = this._getValidatedSlotState(reference);
76+
const slot = reference & SLOT_MASK;
77+
if ((state & SLOT_MASK) > 1) {
78+
this._stateBySlot[slot] = (state - 1) >>> 0;
79+
return;
80+
}
81+
82+
this._slotByValue.delete(this._values[slot]);
83+
this._values[slot] = undefined;
84+
const nextGen = ((state >>> SLOT_BITS) + 1) & GEN_MASK;
85+
this._stateBySlot[slot] = (nextGen << SLOT_BITS) >>> 0;
86+
this._freeSlotStack.push(slot);
87+
}
4588

46-
this._heapEntryByValue.delete(value);
47-
this._heapValueById.delete(ref);
89+
getObject(reference: ref) {
90+
this._getValidatedSlotState(reference);
91+
return this._values[reference & SLOT_MASK];
4892
}
4993

50-
getObject(ref: ref) {
51-
const value = this._heapValueById.get(ref);
52-
if (value === undefined) {
94+
// Returns the packed state for the slot, after validating the reference.
95+
private _getValidatedSlotState(reference: ref): number {
96+
const slot = reference & SLOT_MASK;
97+
if (slot === 0)
98+
throw new ReferenceError(
99+
`Attempted to use invalid reference ${reference}`,
100+
);
101+
const state = this._stateBySlot[slot];
102+
if (state === undefined || (state & SLOT_MASK) === 0) {
103+
throw new ReferenceError(
104+
`Attempted to use invalid reference ${reference}`,
105+
);
106+
}
107+
if (state >>> SLOT_BITS !== reference >>> SLOT_BITS) {
53108
throw new ReferenceError(
54-
"Attempted to read invalid reference " + ref,
109+
`Attempted to use stale reference ${reference}`,
55110
);
56111
}
57-
return value;
112+
return state;
58113
}
59114
}

Tests/JavaScriptKitTests/JSClosureTests.swift

Lines changed: 16 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -92,62 +92,48 @@ class JSClosureTests: XCTestCase {
9292
throw XCTSkip("Missing --expose-gc flag")
9393
}
9494

95-
// Step 1: Create many JSClosure instances
95+
// Step 1: Create many source closures and keep only JS references alive.
96+
// These closures must remain callable even after heavy finalizer churn.
9697
let obj = JSObject()
97-
var closurePointers: Set<UInt32> = []
9898
let numberOfSourceClosures = 10_000
9999

100100
do {
101101
var closures: [JSClosure] = []
102102
for i in 0..<numberOfSourceClosures {
103-
let closure = JSClosure { _ in .undefined }
103+
let closure = JSClosure { _ in .number(Double(i)) }
104104
obj["c\(i)"] = closure.jsValue
105105
closures.append(closure)
106-
// Store
107-
closurePointers.insert(UInt32(UInt(bitPattern: Unmanaged.passUnretained(closure).toOpaque())))
108-
109-
// To avoid all JSClosures having a common address diffs, randomly allocate a new object.
110-
if Bool.random() {
111-
_ = JSObject()
112-
}
113-
}
114-
}
115-
116-
// Step 2: Create many JSObject to make JSObject.id close to Swift heap object address
117-
let minClosurePointer = closurePointers.min() ?? 0
118-
let maxClosurePointer = closurePointers.max() ?? 0
119-
while true {
120-
let obj = JSObject()
121-
if minClosurePointer == obj.id {
122-
break
123106
}
124107
}
125108

126-
// Step 3: Create JSClosure instances and find the one with JSClosure.id == &closurePointers[x]
109+
// Step 2: Create many temporary objects/closures to stress ID reuse and finalizer paths.
110+
// Under the optimized object heap, IDs are aggressively reused, so this should exercise
111+
// the same misdeallocation surface without relying on monotonic ID growth.
127112
do {
128-
while true {
129-
let c = JSClosure { _ in .undefined }
130-
if closurePointers.contains(c.id) || c.id > maxClosurePointer {
131-
break
113+
let numberOfProbeClosures = 50_000
114+
for i in 0..<numberOfProbeClosures {
115+
let tempClosure = JSClosure { _ in .number(Double(i)) }
116+
if i % 3 == 0 {
117+
let tempObject = JSObject()
118+
tempObject["probe"] = tempClosure.jsValue
132119
}
133-
// To avoid all JSClosures having a common JSObject.id diffs, randomly allocate a new JS object.
134-
if Bool.random() {
120+
if i % 7 == 0 {
135121
_ = JSObject()
136122
}
137123
}
138124
}
139125

140-
// Step 4: Trigger garbage collection to call the finalizer of the conflicting JSClosure instance
126+
// Step 3: Trigger garbage collection to run finalizers for temporary closures.
141127
for _ in 0..<100 {
142128
gc()
143129
// Tick the event loop to allow the garbage collector to run finalizers
144130
// registered by FinalizationRegistry.
145131
try await Task.sleep(for: .milliseconds(0))
146132
}
147133

148-
// Step 5: Verify that the JSClosure instances are still alive and can be called
134+
// Step 4: Verify source closures are still alive and correct.
149135
for i in 0..<numberOfSourceClosures {
150-
_ = obj["c\(i)"].function!()
136+
XCTAssertEqual(obj["c\(i)"].function!(), .number(Double(i)))
151137
}
152138
}
153139
}

0 commit comments

Comments
 (0)