Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ unittest:
swift package --swift-sdk "$(SWIFT_SDK_ID)" \
$(TRACING_ARGS) \
--disable-sandbox \
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc
js test --prelude ./Tests/prelude.mjs -Xnode --expose-gc --verbose

.PHONY: regenerate_swiftpm_resources
regenerate_swiftpm_resources:
Expand Down
14 changes: 8 additions & 6 deletions Plugins/PackageToJS/Templates/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
110 changes: 78 additions & 32 deletions Plugins/PackageToJS/Templates/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down
1 change: 1 addition & 0 deletions Runtime/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/lib
/bench/dist
/node_modules
61 changes: 61 additions & 0 deletions Runtime/bench/_original.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { globalVariable } from "../src/find-global.js";
import { ref } from "../src/types.js";

type SwiftRuntimeHeapEntry = {
id: number;
rc: number;
};

/** Original implementation kept for benchmark comparison. Same API as JSObjectSpace. */
export class JSObjectSpaceOriginal {
private _heapValueById: Map<number, any>;
private _heapEntryByValue: Map<any, SwiftRuntimeHeapEntry>;
private _heapNextKey: number;

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;
}

retain(value: any) {
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: ref) {
return this.retain(this.getObject(ref));
}

release(ref: ref) {
const value = this._heapValueById.get(ref);
const entry = this._heapEntryByValue.get(value)!;
entry.rc--;
if (entry.rc != 0) return;

this._heapEntryByValue.delete(value);
this._heapValueById.delete(ref);
}

getObject(ref: ref) {
const value = this._heapValueById.get(ref);
if (value === undefined) {
throw new ReferenceError(
"Attempted to read invalid reference " + ref,
);
}
return value;
}
}
76 changes: 76 additions & 0 deletions Runtime/bench/_version1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { globalVariable } from "../src/find-global.js";
import { ref } from "../src/types.js";

export class JSObjectSpace_v1 {
private _valueRefMap: Map<any, number>;
private _values: (any | undefined)[];
private _refCounts: number[];
private _freeSlotStack: number[];

constructor() {
this._values = [];
this._values[0] = undefined;
this._values[1] = globalVariable;

this._valueRefMap = new Map();
this._valueRefMap.set(globalVariable, 1);

this._refCounts = [];
this._refCounts[0] = 0;
this._refCounts[1] = 1;

this._freeSlotStack = [];
}

retain(value: any) {
const id = this._valueRefMap.get(value);
if (id !== undefined) {
this._refCounts[id]++;
return id;
}

const newId =
this._freeSlotStack.length > 0
? this._freeSlotStack.pop()!
: this._values.length;
this._values[newId] = value;
this._refCounts[newId] = 1;
this._valueRefMap.set(value, newId);
return newId;
}

retainByRef(ref: ref) {
if (this._refCounts[ref] === 0) {
throw new ReferenceError(
"Attempted to retain invalid reference " + ref,
);
}

this._refCounts[ref]++;
return ref;
}

release(ref: ref) {
if (--this._refCounts[ref] !== 0) return;

const value = this._values[ref];
this._valueRefMap.delete(value);
if (ref === this._values.length - 1) {
this._values.length = ref;
this._refCounts.length = ref;
} else {
this._values[ref] = undefined;
this._freeSlotStack.push(ref);
}
}

getObject(ref: ref) {
const value = this._values[ref];
if (value === undefined) {
throw new ReferenceError(
"Attempted to read invalid reference " + ref,
);
}
return value;
}
}
75 changes: 75 additions & 0 deletions Runtime/bench/_version2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { globalVariable } from "../src/find-global.js";
import { ref } from "../src/types.js";

export class JSObjectSpace_v2 {
private _idByValue: Map<any, number>;
private _valueById: Record<number, any>;
private _refCountById: Record<number, number | undefined>;
private _nextRef: number;

constructor() {
this._idByValue = new Map();
this._idByValue.set(globalVariable, 1);
this._valueById = Object.create(null);
this._refCountById = Object.create(null);
this._valueById[1] = globalVariable;
this._refCountById[1] = 1;

// 0 is invalid, 1 is globalThis.
this._nextRef = 2;
}

retain(value: any) {
const id = this._idByValue.get(value);
if (id !== undefined) {
this._refCountById[id]!++;
return id;
}

const newId = this._nextRef++;
this._valueById[newId] = value;
this._refCountById[newId] = 1;
this._idByValue.set(value, newId);
return newId;
}

retainByRef(ref: ref) {
const rc = this._refCountById[ref];
if (rc === undefined) {
throw new ReferenceError(
"Attempted to retain invalid reference " + ref,
);
}
this._refCountById[ref] = rc + 1;
return ref;
}

release(ref: ref) {
const rc = this._refCountById[ref];
if (rc === undefined) {
throw new ReferenceError(
"Attempted to release invalid reference " + ref,
);
}
const next = rc - 1;
if (next !== 0) {
this._refCountById[ref] = next;
return;
}

const value = this._valueById[ref];
this._idByValue.delete(value);
delete this._valueById[ref];
delete this._refCountById[ref];
}

getObject(ref: ref) {
const rc = this._refCountById[ref];
if (rc === undefined) {
throw new ReferenceError(
"Attempted to read invalid reference " + ref,
);
}
return this._valueById[ref];
}
}
Loading