diff --git a/package.json b/package.json index cc88add..5ab20dc 100644 --- a/package.json +++ b/package.json @@ -24,8 +24,10 @@ }, "scripts": { "build": "tsc && vite build", + "prepare": "vite build", "test": "vitest", - "lint": "eslint ." + "lint": "eslint .", + "format": "prettier --write \"./src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"" }, "peerDependencies": { "quickjs-emscripten": "*" diff --git a/src/index.ts b/src/index.ts index b9ae857..f28434c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -51,6 +51,8 @@ export type Options = { compat?: boolean; /** Experimental: use QuickJSContextEx, which wraps existing QuickJSContext. */ experimentalContextEx?: boolean; + /** Globally enable syncing mode. Default is true. If returns false, note that the handle cannot be synchronized between the host and the QuickJS even if arena.sync is used. */ + syncEnabled?: boolean; }; /** @@ -230,7 +232,7 @@ export class Arena { mode: true | "json" | undefined, ): Wrapped | 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 => { @@ -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 => { - return this._register(t, h, undefined, true)?.[0]; + return this._register(t, h, undefined, this._options?.syncEnabled ?? true)?.[0]; }; _unmarshalFind = (h: QuickJSHandle): unknown => { @@ -333,6 +337,7 @@ export class Arena { this._marshal, this._syncMode, this._options?.isWrappable, + this._options?.syncEnabled ?? true, ); } @@ -354,6 +359,7 @@ export class Arena { this._unmarshal, this._syncMode, this._options?.isHandleWrappable, + this._options?.syncEnabled ?? true, ); } diff --git a/src/memory.test.ts b/src/memory.test.ts new file mode 100644 index 0000000..907f6cb --- /dev/null +++ b/src/memory.test.ts @@ -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>("globalThis.test.check()"); + arena.executePendingJobs(); + const data = await promise; + expect(data).not.toBeNull(); + + for (let i = 0; i < 100; i++) { + const promise = arena.evalCode>("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(); + }); +}); diff --git a/src/wrapper.ts b/src/wrapper.ts index 948757c..4e68547 100644 --- a/src/wrapper.ts +++ b/src/wrapper.ts @@ -15,6 +15,7 @@ export function wrap( marshal: (target: any) => [QuickJSHandle, boolean], syncMode?: (target: T) => SyncMode | undefined, wrappable?: (target: unknown) => boolean, + syncEnabled = true, ): Wrapped | undefined { // promise and date cannot be wrapped if ( @@ -22,10 +23,17 @@ export function wrap( 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; + } const rec = new Proxy(target as any, { get(obj, key) { @@ -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 | 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, false]; + } const getSyncMode = (h: QuickJSHandle) => { const res = syncMode?.(unmarshal(h));