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
7 changes: 4 additions & 3 deletions Plugins/PackageToJS/Templates/runtime.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ type ref = number;
type pointer = number;

declare class JSObjectSpace {
private _heapValueById;
private _heapEntryByValue;
private _heapNextKey;
private _valueRefMap;
private _values;
private _refCounts;
private _freeSlotStack;
constructor();
retain(value: any): number;
retainByRef(ref: ref): number;
Expand Down
54 changes: 32 additions & 22 deletions Plugins/PackageToJS/Templates/runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -240,38 +240,48 @@ const globalVariable = globalThis;

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._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) {
const entry = this._heapEntryByValue.get(value);
if (entry) {
entry.rc++;
return entry.id;
const id = this._valueRefMap.get(value);
if (id !== undefined) {
this._refCounts[id]++;
return id;
}
const id = this._heapNextKey++;
this._heapValueById.set(id, value);
this._heapEntryByValue.set(value, { id: id, rc: 1 });
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) {
return this.retain(this.getObject(ref));
this._refCounts[ref]++;
return ref;
}
release(ref) {
const value = this._heapValueById.get(ref);
const entry = this._heapEntryByValue.get(value);
entry.rc--;
if (entry.rc != 0)
if (--this._refCounts[ref] !== 0)
return;
this._heapEntryByValue.delete(value);
this._heapValueById.delete(ref);
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) {
const value = this._heapValueById.get(ref);
const value = this._values[ref];
if (value === undefined) {
throw new ReferenceError("Attempted to read invalid reference " + ref);
}
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;
}
}
131 changes: 131 additions & 0 deletions Runtime/bench/bench-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Benchmark runner for JSObjectSpace implementations.
* Run with: npm run bench (builds via rollup.bench.mjs, then node bench/dist/bench.mjs)
*/

import { JSObjectSpace } from "../src/object-heap.js";
import { JSObjectSpaceOriginal } from "./_original.js";

export interface HeapLike {
retain(value: unknown): number;
release(ref: number): void;
getObject(ref: number): unknown;
}

const ITERATIONS = 5;
const HEAVY_OPS = 200_000;
const FILL_LEVELS = [1_000, 10_000, 50_000] as const;
const MIXED_OPS_PER_LEVEL = 100_000;

function median(numbers: number[]): number {
const sorted = [...numbers].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 !== 0
? sorted[mid]!
: (sorted[mid - 1]! + sorted[mid]!) / 2;
}

function runHeavyRetain(Heap: new () => HeapLike): number {
const times: number[] = [];
for (let iter = 0; iter < ITERATIONS; iter++) {
const heap = new Heap();
const start = performance.now();
for (let i = 0; i < HEAVY_OPS; i++) {
heap.retain({ __i: i });
}
times.push(performance.now() - start);
}
return median(times);
}

function runHeavyRelease(Heap: new () => HeapLike): number {
const times: number[] = [];
for (let iter = 0; iter < ITERATIONS; iter++) {
const heap = new Heap();
const refs: number[] = [];
for (let i = 0; i < HEAVY_OPS; i++) {
refs.push(heap.retain({ __i: i }));
}
const start = performance.now();
for (let i = 0; i < HEAVY_OPS; i++) {
heap.release(refs[i]!);
}
times.push(performance.now() - start);
}
return median(times);
}

function runMixedFillLevel(Heap: new () => HeapLike, fillLevel: number): number {
const times: number[] = [];
for (let iter = 0; iter < ITERATIONS; iter++) {
const heap = new Heap();
const refs: number[] = [];
for (let i = 0; i < fillLevel; i++) {
refs.push(heap.retain({ __i: i }));
}
let nextId = fillLevel;
const start = performance.now();
for (let i = 0; i < MIXED_OPS_PER_LEVEL; i++) {
const idx = i % fillLevel;
heap.release(refs[idx]!);
refs[idx] = heap.retain({ __i: nextId++ });
}
times.push(performance.now() - start);
}
return median(times);
}

function runBenchmark(
name: string,
Heap: new () => HeapLike,
): { name: string; heavyRetain: number; heavyRelease: number; mixed: Record<string, number> } {
return {
name,
heavyRetain: runHeavyRetain(Heap),
heavyRelease: runHeavyRelease(Heap),
mixed: {
"1k": runMixedFillLevel(Heap, 1_000),
"10k": runMixedFillLevel(Heap, 10_000),
"50k": runMixedFillLevel(Heap, 50_000),
},
};
}

function main() {
const implementations: Array<{ name: string; Heap: new () => HeapLike }> = [
{ name: "JSObjectSpaceOriginal", Heap: JSObjectSpaceOriginal },
{ name: "JSObjectSpace (current)", Heap: JSObjectSpace },
];

console.log("JSObjectSpace benchmark");
console.log("======================\n");
console.log(
`Heavy retain: ${HEAVY_OPS} ops, Heavy release: ${HEAVY_OPS} ops`,
);
console.log(
`Mixed: ${MIXED_OPS_PER_LEVEL} ops per fill level (${FILL_LEVELS.join(", ")})`,
);
console.log(`Median of ${ITERATIONS} runs per scenario.\n`);

const results: Array<ReturnType<typeof runBenchmark>> = [];
for (const { name, Heap } of implementations) {
console.log(`Running ${name}...`);
runBenchmark(name, Heap);
results.push(runBenchmark(name, Heap));
}

console.log("\nResults (median ms):\n");
const pad = Math.max(...results.map((r) => r.name.length));
for (const r of results) {
console.log(
`${r.name.padEnd(pad)} retain: ${r.heavyRetain.toFixed(2)}ms release: ${r.heavyRelease.toFixed(2)}ms mixed(1k): ${r.mixed["1k"].toFixed(2)}ms mixed(10k): ${r.mixed["10k"].toFixed(2)}ms mixed(50k): ${r.mixed["50k"].toFixed(2)}ms`,
);
}

const total = (r: (typeof results)[0]) =>
r.heavyRetain + r.heavyRelease + r.mixed["1k"] + r.mixed["10k"] + r.mixed["50k"];
const best = results.reduce((a, b) => (total(a) <= total(b) ? a : b));
console.log(`\nFastest overall (sum of medians): ${best.name}`);
}

main();
11 changes: 11 additions & 0 deletions Runtime/rollup.bench.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typescript from "@rollup/plugin-typescript";

/** @type {import('rollup').RollupOptions} */
export default {
input: "bench/bench-runner.ts",
output: {
file: "bench/dist/bench.mjs",
format: "esm",
},
plugins: [typescript({ tsconfig: "tsconfig.bench.json" })],
};
69 changes: 40 additions & 29 deletions Runtime/src/object-heap.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
import { globalVariable } from "./find-global.js";
import { ref } from "./types.js";

type SwiftRuntimeHeapEntry = {
id: number;
rc: number;
};
export class JSObjectSpace {
private _heapValueById: Map<number, any>;
private _heapEntryByValue: Map<any, SwiftRuntimeHeapEntry>;
private _heapNextKey: number;
private _valueRefMap: Map<any, number>;
private _values: (any | undefined)[];
private _refCounts: number[];
private _freeSlotStack: number[];

constructor() {
this._heapValueById = new Map();
this._heapValueById.set(1, globalVariable);
this._values = [];
this._values[0] = undefined;
this._values[1] = globalVariable;

this._heapEntryByValue = new Map();
this._heapEntryByValue.set(globalVariable, { id: 1, rc: 1 });
this._valueRefMap = new Map();
this._valueRefMap.set(globalVariable, 1);

// Note: 0 is preserved for invalid references, 1 is preserved for globalThis
this._heapNextKey = 2;
this._refCounts = [];
this._refCounts[0] = 0;
this._refCounts[1] = 1;

this._freeSlotStack = [];
}

retain(value: any) {
const entry = this._heapEntryByValue.get(value);
if (entry) {
entry.rc++;
return entry.id;
const id = this._valueRefMap.get(value);
if (id !== undefined) {
this._refCounts[id]++;
return id;
}
const id = this._heapNextKey++;
this._heapValueById.set(id, value);
this._heapEntryByValue.set(value, { id: id, rc: 1 });
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) {
return this.retain(this.getObject(ref));
this._refCounts[ref]++;
return ref;
}

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

this._heapEntryByValue.delete(value);
this._heapValueById.delete(ref);
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._heapValueById.get(ref);
const value = this._values[ref];
if (value === undefined) {
throw new ReferenceError(
"Attempted to read invalid reference " + ref,
Expand Down
5 changes: 5 additions & 0 deletions Runtime/tsconfig.bench.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "./tsconfig.json",
"compilerOptions": { "rootDir": "." },
"include": ["src/**/*", "bench/**/*"]
}
Loading
Loading