-
Notifications
You must be signed in to change notification settings - Fork 126
Description
I'm using QuickJS to sandbox code on the server. When dealing with large objects being passed back from a function (if there are both a promise-based functions and a normal ones) at a certain size of object, I start getting list_empty assertions. With a smaller number of objects, let's say 6,500 instead of 7,000, the debug version reports no problems whatsoever and it works perfectly.
quickjs-emscripten 0.31.0.
Source that fails (I apologize for it not being shorter, but this is a very fiddly thing to reproduce):
const { newQuickJSWASMModule, DEBUG_SYNC, Scope } = require("quickjs-emscripten")
async function main() {
const memoryLimit = 256 * 1024 * 1024
const maxStackSize = 1024 * 1024
// Create QuickJS module and context
// const QuickJS = await newQuickJSWASMModule()
const QuickJS = await newQuickJSWASMModule(DEBUG_SYNC)
const runtime = QuickJS.newRuntime()
runtime.setMemoryLimit(memoryLimit)
runtime.setMaxStackSize(maxStackSize)
const scope = new Scope()
const context = scope.manage(runtime.newContext())
// Function that simulates reading data asynchronously
const readFileHandle = context.newFunction("readFile", () => {
const promise = context.newPromise()
setTimeout(() => {
const content = "Sdfsdfsdfsdf"
promise.resolve(scope.manage(context.newString(content || "")))
}, 100)
return scope.manage(promise.handle)
})
readFileHandle.consume(handle => context.setProp(context.global, "readFile", handle))
// ========================================================================
// Expose parseit (sync function)
// ========================================================================
const parseitHandle = context.newFunction("parseit", () => {
const data2 = []
// CHANGING THIS TO 6500 CAUSES THE TEST TO PASS!!
for (let i = 0; i < 7000; i++) {
data2.push({
field1: 'IEQ 08-DH5NBT',
field2: 'SAHI - WP Inventory',
field3: 'IEQ 08',
field4: 'M/B_MORONDAVA_CU MORONDAVA_FKT TSIMAHAVAOKELY_Puits non protégé Secteur I 6',
field5: 'Final',
field6: '847347003',
field7: 'Secteur I 6',
field8: '847346923',
field12: 'Menabe',
field13: 'Morondava',
field14: 'Morondava',
field15: '2025-09-11',
field16: 'Periodic / sectoral monitoring',
})
}
const evalResult = context.evalCode(`(${JSON.stringify(data2)})`)
if (evalResult.error) {
evalResult.error.dispose()
throw new Error("Failed to create data")
}
return evalResult.value
})
context.setProp(context.global, "parseit", parseitHandle)
parseitHandle.dispose()
// ========================================================================
// Execute the code
// ========================================================================
const code = `
await readFile()
parseit()
`
const wrappedCode = `(async () => { ${code} })()`
const evalResult = context.evalCode(wrappedCode)
if (evalResult.error) {
console.error("Eval error:", context.dump(evalResult.error))
evalResult.error.dispose()
context.dispose()
runtime.dispose()
return
}
const promiseHandle = evalResult.value
// Wait for the promise to settle
let promiseState = context.getPromiseState(promiseHandle)
while (promiseState.type === "pending") {
const result = runtime.executePendingJobs()
result.dispose()
await new Promise(r => setTimeout(r, 1))
promiseState = context.getPromiseState(promiseHandle)
}
// Drain any remaining pending jobs
while (runtime.hasPendingJob()) {
const result = runtime.executePendingJobs()
result.dispose()
}
// Get the result
if (promiseState.type === "fulfilled") {
const result = context.dump(promiseState.value)
promiseState.value.dispose()
} else {
console.error("Promise rejected:", context.dump(promiseState.error))
promiseState.error.dispose()
}
// Dispose the main promise
promiseHandle.dispose()
scope.dispose()
runtime.dispose()
}
main().catch(console.error)If you change it to 6500 items instead of 7000, it works fine and shows no memory leak. However, at 7000, it shows the following in debug mode:
Object leaks:
ADDRESS REFS SHRF PROTO CLASS PROPS
0x2b03d40 1 [js_context]
Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: ../../vendor/quickjs/quickjs.c,1998,JS_FreeRuntime)
RuntimeError: Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at: ../../vendor/quickjs/quickjs.c,1998,JS_FreeRuntime)
at abort (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:693:11)
at ___assert_fail (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:1485:7)
at wasm://wasm/01944492:wasm-function[164]:0xc05b
at wasm://wasm/01944492:wasm-function[60]:0x46f8
at file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:728:12
at ccall (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:4843:17)
at QuickJSFFI.QTS_FreeRuntime (file:///home/clayton/dev/mWater/monorepo/node_modules/.pnpm/@jitl+quickjs-wasmfile-debug-sync@0.31.0/node_modules/@jitl/quickjs-wasmfile-debug-sync/dist/emscripten-module.mjs:4859:27)
at _Lifetime.disposer (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:6:949)
at _Lifetime.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:2:2198)
at _Scope.dispose (/home/clayton/dev/mWater/monorepo/node_modules/.pnpm/quickjs-emscripten-core@0.31.0/node_modules/quickjs-emscripten-core/dist/index.js:4:1321)
The code I'm writing is to help with importing large datasets, and so the fact that this dies unexpectedly with bigger data structures is a blocker. So if anyone has an idea on how to get around this, it would be much appreciated!