Skip to content

Commit e7b5b8c

Browse files
committed
WIP: another version
1 parent 9620735 commit e7b5b8c

File tree

6 files changed

+99
-33
lines changed

6 files changed

+99
-33
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ unittest:
1919
swift package --swift-sdk "$(SWIFT_SDK_ID)" \
2020
$(TRACING_ARGS) \
2121
--disable-sandbox \
22-
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc
22+
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc --verbose
2323

2424
.PHONY: regenerate_swiftpm_resources
2525
regenerate_swiftpm_resources:

Plugins/PackageToJS/Templates/runtime.mjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,13 +256,18 @@ class JSObjectSpace {
256256
this._refCounts[id]++;
257257
return id;
258258
}
259-
const newId = this._freeSlotStack.length > 0 ? this._freeSlotStack.pop() : this._values.length;
259+
const newId = this._freeSlotStack.length > 0
260+
? this._freeSlotStack.pop()
261+
: this._values.length;
260262
this._values[newId] = value;
261263
this._refCounts[newId] = 1;
262264
this._valueRefMap.set(value, newId);
263265
return newId;
264266
}
265267
retainByRef(ref) {
268+
if (this._refCounts[ref] === 0) {
269+
throw new ReferenceError("Attempted to retain invalid reference " + ref);
270+
}
266271
this._refCounts[ref]++;
267272
return ref;
268273
}

Runtime/bench/_version2.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { globalVariable } from "../src/find-global.js";
2+
import { ref } from "../src/types.js";
3+
4+
export class JSObjectSpace_v2 {
5+
private _entryByValue: Map<any, { id: number; rc: number }>;
6+
private _values: (any | undefined)[];
7+
private _refCounts: number[];
8+
private _nextRef: number;
9+
10+
constructor() {
11+
this._values = [];
12+
this._values[0] = undefined;
13+
this._values[1] = globalVariable;
14+
15+
this._entryByValue = new Map();
16+
this._entryByValue.set(globalVariable, { id: 1, rc: 1 });
17+
18+
this._refCounts = [];
19+
this._refCounts[0] = 0;
20+
this._refCounts[1] = 1;
21+
22+
// 0 is invalid, 1 is globalThis.
23+
this._nextRef = 2;
24+
}
25+
26+
retain(value: any) {
27+
const entry = this._entryByValue.get(value);
28+
if (entry) {
29+
entry.rc++;
30+
this._refCounts[entry.id]++;
31+
return entry.id;
32+
}
33+
34+
const id = this._nextRef++;
35+
this._values[id] = value;
36+
this._refCounts[id] = 1;
37+
this._entryByValue.set(value, { id, rc: 1 });
38+
return id;
39+
}
40+
41+
retainByRef(ref: ref) {
42+
return this.retain(this.getObject(ref));
43+
}
44+
45+
release(ref: ref) {
46+
const value = this._values[ref];
47+
const entry = this._entryByValue.get(value)!;
48+
entry.rc--;
49+
this._refCounts[ref]--;
50+
if (entry.rc != 0) return;
51+
52+
this._entryByValue.delete(value);
53+
// Keep IDs monotonic; clear slot and leave possible holes.
54+
this._values[ref] = undefined;
55+
this._refCounts[ref] = 0;
56+
}
57+
58+
getObject(ref: ref) {
59+
const value = this._values[ref];
60+
if (value === undefined) {
61+
throw new ReferenceError(
62+
"Attempted to read invalid reference " + ref,
63+
);
64+
}
65+
return value;
66+
}
67+
}

Runtime/bench/bench-runner.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import { JSObjectSpace } from "../src/object-heap.js";
77
import { JSObjectSpaceOriginal } from "./_original.js";
8+
import { JSObjectSpace_v2 } from "./_version2.js";
89

910
export interface HeapLike {
1011
retain(value: unknown): number;
@@ -94,7 +95,8 @@ function runBenchmark(
9495
function main() {
9596
const implementations: Array<{ name: string; Heap: new () => HeapLike }> = [
9697
{ name: "JSObjectSpaceOriginal", Heap: JSObjectSpaceOriginal },
97-
{ name: "JSObjectSpace (current)", Heap: JSObjectSpace },
98+
{ name: "JSObjectSpace_v2 (ref++, single map)", Heap: JSObjectSpace_v2 },
99+
{ name: "JSObjectSpace (current)", Heap: JSObjectSpace }
98100
];
99101

100102
console.log("JSObjectSpace benchmark");

Runtime/src/object-heap.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ export class JSObjectSpace {
4040
}
4141

4242
retainByRef(ref: ref) {
43+
if (this._refCounts[ref] === 0) {
44+
throw new ReferenceError(
45+
"Attempted to retain invalid reference " + ref,
46+
);
47+
}
48+
4349
this._refCounts[ref]++;
4450
return ref;
4551
}

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)