Skip to content

Instantly share code, notes, and snippets.

@mhofman
Created May 25, 2024 01:57
Show Gist options
  • Save mhofman/7fbfd71048148b97089993bb4463b465 to your computer and use it in GitHub Desktop.
Save mhofman/7fbfd71048148b97089993bb4463b465 to your computer and use it in GitHub Desktop.
test cases to show how different engines retain values in scope
//// 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',
);
}
});
#### 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