Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"scripts": {
"build": "tsc && vite build",
"test": "vitest",
"lint": "eslint ."
"lint": "eslint .",
"format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\""
},
"peerDependencies": {
"quickjs-emscripten": "*"
Expand Down
12 changes: 9 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export type Options = {
compat?: boolean;
/** Experimental: use QuickJSContextEx, which wraps existing QuickJSContext. */
experimentalContextEx?: boolean;
/** Globally enable syncing mode, If returns false, If returns false, note that the handle cannot be synchronized between the host and the QuickJS even if arena.sync is used. */
syncEnabled?: boolean;
};

/**
Expand Down Expand Up @@ -230,7 +232,7 @@ export class Arena {
mode: true | "json" | undefined,
): Wrapped<QuickJSHandle> | undefined => {
if (mode === "json") return;
return this._register(t, handleFrom(h), this._map)?.[1];
return this._register(t, handleFrom(h), this._map, this._options?.syncEnabled)?.[1];
};

_marshalPreApply = (target: Function, that: unknown, args: unknown[]): void => {
Expand Down Expand Up @@ -261,11 +263,13 @@ export class Arena {
custom: this._options?.customMarshaller,
});

return [handle, !this._map.hasHandle(handle)];
const syncEnabled = this._options?.syncEnabled ?? true;

return [handle, !syncEnabled || !this._map.hasHandle(handle)];
};

_preUnmarshal = (t: any, h: QuickJSHandle): Wrapped<any> => {
return this._register(t, h, undefined, true)?.[0];
return this._register(t, h, undefined, this._options?.syncEnabled ?? true)?.[0];
};

_unmarshalFind = (h: QuickJSHandle): unknown => {
Expand Down Expand Up @@ -333,6 +337,7 @@ export class Arena {
this._marshal,
this._syncMode,
this._options?.isWrappable,
this._options?.syncEnabled ?? true,
);
}

Expand All @@ -354,6 +359,7 @@ export class Arena {
this._unmarshal,
this._syncMode,
this._options?.isHandleWrappable,
this._options?.syncEnabled ?? true,
);
}

Expand Down
108 changes: 108 additions & 0 deletions src/memory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { getQuickJS } from "quickjs-emscripten";
import { describe, expect, test } from "vitest";

import { Arena } from ".";

describe("memory", () => {
test("memory leak", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, {
isMarshalable: true,
registeredObjects: [],
syncEnabled: false,
});

const getMemory = () => {
const handle = ctx.runtime.computeMemoryUsage();
const mem = ctx.dump(handle);
handle.dispose();
return mem;
};

arena.expose({
fnFromHost: () => {
return {
id: "some id",
data: Math.random(),
};
},
});

arena.evalCode(`globalThis.test = {
check: () => {
return fnFromHost();
}
}`);

const memoryBefore = getMemory().memory_used_size as number;
const data = arena.evalCode("globalThis.test.check()");
expect(data).not.toBeNull();

for (let i = 0; i < 100; i++) {
const data = arena.evalCode("globalThis.test.check()");
expect(data).not.toBeNull();
}

const memoryAfter = getMemory().memory_used_size as number;

console.log("Allocation increased %d", memoryAfter - memoryBefore);
expect((memoryAfter - memoryBefore) / 1024).toBe(0);

arena.dispose();
ctx.dispose();
});

test("memory leak promise", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, {
isMarshalable: true,
registeredObjects: [],
syncEnabled: false,
});

const getMemory = () => {
const handle = ctx.runtime.computeMemoryUsage();
const mem = ctx.dump(handle);
handle.dispose();
return mem;
};

arena.expose({
fnFromHost: () => {
return {
id: "some id",
data: Math.random(),
};
},
});

arena.evalCode(`globalThis.test = {
check: async () => {
const hostData = await fnFromHost();
return hostData;
}
}`);

const memoryBefore = getMemory().memory_used_size as number;

const promise = arena.evalCode<Promise<any>>("globalThis.test.check()");
arena.executePendingJobs();
const data = await promise;
expect(data).not.toBeNull();

for (let i = 0; i < 100; i++) {
const promise = arena.evalCode<Promise<any>>("globalThis.test.check()");
arena.executePendingJobs();
const data = await promise;
expect(data).not.toBeNull();
}

const memoryAfter = getMemory().memory_used_size as number;

console.log("Allocation increased %d", memoryAfter - memoryBefore);
expect((memoryAfter - memoryBefore) / 1024).toBe(0);

arena.dispose();
ctx.dispose();
});
});
21 changes: 18 additions & 3 deletions src/wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,25 @@ export function wrap<T = any>(
marshal: (target: any) => [QuickJSHandle, boolean],
syncMode?: (target: T) => SyncMode | undefined,
wrappable?: (target: unknown) => boolean,
syncEnabled = true,
): Wrapped<T> | undefined {
// promise and date cannot be wrapped
if (
!isObject(target) ||
target instanceof Promise ||
target instanceof Date ||
(wrappable && !wrappable(target))
)
) {
return undefined;
}

if (isWrapped(target, proxyKeySymbol)) return target;
if (isWrapped(target, proxyKeySymbol)) {
return target;
}

if (!syncEnabled) {
return target as Wrapped<T>;
}

const rec = new Proxy(target as any, {
get(obj, key) {
Expand Down Expand Up @@ -80,11 +88,18 @@ export function wrapHandle(
unmarshal: (handle: QuickJSHandle) => any,
syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
syncEnabled = true,
): [Wrapped<QuickJSHandle> | undefined, boolean] {
if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
return [undefined, false];

if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) {
return [handle, false];
}

if (!syncEnabled) {
return [handle as Wrapped<QuickJSHandle>, false];
}

const getSyncMode = (h: QuickJSHandle) => {
const res = syncMode?.(unmarshal(h));
Expand Down