Created
May 25, 2024 01:57
-
-
Save mhofman/7fbfd71048148b97089993bb4463b465 to your computer and use it in GitHub Desktop.
test cases to show how different engines retain values in scope
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//// Preamble to normalize environments \\\\ | |
if (typeof console === 'undefined') { | |
globalThis.console = { | |
log() { | |
print(...arguments); | |
}, | |
}; | |
} | |
const enqueueHostTask = | |
typeof setImmediate === 'function' | |
? setImmediate | |
: typeof setTimeout === 'function' | |
? callback => setTimeout(callback, 0) | |
: callback => { | |
if (typeof drainJobQueue === 'function') { | |
drainJobQueue(); | |
} | |
Promise.resolve().then(callback); | |
}; | |
const hostTask = () => new Promise(resolve => enqueueHostTask(resolve)); | |
let engineGc; | |
const setupGC = async () => { | |
engineGc = globalThis.gc || (typeof $262 !== 'undefined' ? $262.gc : null); | |
if (!engineGc) { | |
try { | |
const [{ ['default']: v8 }, { ['default']: vm }] = await Promise.all([ | |
import('v8'), | |
import('vm'), | |
]); | |
v8.setFlagsFromString('--expose_gc'); | |
engineGc = vm.runInNewContext('gc'); | |
v8.setFlagsFromString('--no-expose_gc'); | |
} catch (err) { | |
engineGc = () => | |
void Array.from({ length: 2 ** 24 }, () => Math.random()); | |
} | |
} | |
try { | |
globalThis.gc = engineGc; | |
} catch (e) {} | |
}; | |
// Some GC tools | |
const fr = new FinalizationRegistry(held => { | |
held(); | |
}); | |
const whenCollected = target => | |
new Promise(resolve => fr.register(target, resolve)); | |
const isRetained = async whenCollectedPromise => { | |
let retained = true; | |
whenCollectedPromise.then(() => { | |
retained = false; | |
}); | |
await whenCollected({}); | |
// in case our sentinel trigged before the target | |
await Promise.resolve(); | |
return retained; | |
}; | |
// Test cases | |
const tests = { | |
'ignore-scope': async ({ log }) => { | |
const doStuff = async () => { | |
await null; | |
log('stuff', 'ignored'); | |
}; | |
await doStuff(); | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
'retaining-scope': async ({ large, log }) => { | |
const doStuff = async () => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(); | |
const bar = arg => { | |
log('bar', arg, large.length); | |
}; | |
return { bar }; | |
}, | |
'single-scope': async ({ large, log }) => { | |
const doStuff = async () => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(); | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
'split-scope': async ({ large: outerLarge, log }) => { | |
const doStuff = async large => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(outerLarge); | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
'nulling-single-scope': async ({ large, log }) => { | |
const doStuff = async () => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(); | |
large = null; | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
'nulling-single-scope-options': async options => { | |
let { large } = options; | |
const { log } = options; | |
const doStuff = async () => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(); | |
large = null; | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
'nulling-single-scope-nulling-options': async options => { | |
let { large } = options; | |
const { log } = options; | |
options = null; | |
const doStuff = async () => { | |
const { length } = large; | |
await null; | |
log('stuff', length); | |
}; | |
await doStuff(); | |
large = null; | |
const bar = arg => { | |
log('bar', arg); | |
}; | |
return { bar }; | |
}, | |
}; | |
const makeTestKit = async (maker, name) => { | |
const large = new Uint8Array(10 * 1024 * 1024); | |
const largeCollected = whenCollected(large); | |
const foo = await maker({ | |
large, | |
log: () => {}, // console.log.bind(console, name) | |
}); | |
return { foo, largeCollected }; | |
}; | |
void setupGC() | |
.then(async () => { | |
// Check that all GC tools work as expected | |
const sentinelCollected = whenCollected({}); | |
// v8 is unable to collect the trigger object unless gc is in different "stack" | |
await null; | |
const sentinelIsRetained = isRetained(sentinelCollected); | |
engineGc(); | |
if (await sentinelIsRetained) { | |
throw Error(`GC didn't find our test sentinel`); | |
} | |
console.log('start'); | |
}) | |
.then(async () => { | |
for (const [name, maker] of Object.entries(tests)) { | |
const { foo, largeCollected } = await makeTestKit(maker, name); | |
const largeIsRetained = isRetained(largeCollected); | |
engineGc(); | |
foo.bar(42); | |
console.log( | |
name, | |
'large', | |
(await largeIsRetained) ? 'retained' : 'collected', | |
); | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#### JavaScriptCore | |
start | |
ignore-scope large collected | |
retaining-scope large retained | |
single-scope large retained | |
split-scope large retained | |
nulling-single-scope large collected | |
nulling-single-scope-options large retained | |
nulling-single-scope-nulling-options large collected | |
#### Moddable XS | |
start | |
ignore-scope large collected | |
retaining-scope large retained | |
single-scope large collected | |
split-scope large collected | |
nulling-single-scope large collected | |
nulling-single-scope-options large collected | |
nulling-single-scope-nulling-options large collected | |
#### SpiderMonkey | |
start | |
ignore-scope large collected | |
retaining-scope large retained | |
single-scope large retained | |
split-scope large collected | |
nulling-single-scope large collected | |
nulling-single-scope-options large retained | |
nulling-single-scope-nulling-options large collected | |
#### V8 | |
start | |
ignore-scope large collected | |
retaining-scope large retained | |
single-scope large retained | |
split-scope large collected | |
nulling-single-scope large collected | |
nulling-single-scope-options large collected | |
nulling-single-scope-nulling-options large collected |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment