Skip to content

Instantly share code, notes, and snippets.

@mhofman
Created August 18, 2022 22:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mhofman/e4031aa4cd2375d0f151f62691724475 to your computer and use it in GitHub Desktop.
Save mhofman/e4031aa4cd2375d0f151f62691724475 to your computer and use it in GitHub Desktop.
Test cases for promise and resolver collection
// @ts-check
if (typeof console === "undefined") {
globalThis.console = {
log() {
print(...arguments);
},
};
}
let queueGCJob;
if (typeof setTimeout === "function") {
queueGCJob = function () {
return new Promise(function (resolve) {
setTimeout(resolve, 0);
});
};
} else if (typeof drainJobQueue === "function") {
queueGCJob = function () {
return new Promise(function (resolve) {
drainJobQueue();
resolve();
});
};
} else {
queueGCJob = function () {
return Promise.resolve();
};
}
const setupGC = async () => {
let gc = globalThis.gc || (typeof $262 !== "undefined" ? $262.gc : null);
if (!gc) {
try {
const [{ ["default"]: v8 }, { ["default"]: vm }] = await Promise.all([
import("v8"),
import("vm"),
]);
v8.setFlagsFromString("--expose_gc");
gc = vm.runInNewContext("gc");
v8.setFlagsFromString("--no-expose_gc");
} catch (err) {
gc = () => void Array.from({ length: 2 ** 24 }, () => Math.random());
}
}
globalThis.gc = gc;
};
/** @type {FinalizationRegistry<() => void>} */
const fr = new FinalizationRegistry((held) => {
held();
});
const forever = new Promise(() => {});
/**
* @template {any} [T=any]
* @typedef {object} PromiseKit
* @property {Promise<T>} promise
* @property {(value?: T | PromiseLike<T>) => void} resolve
* @property {(err?: any) => void} reject
*/
/**
* @template [T=any]
* @typedef {Pick<PromiseKit<T>, 'reject' | 'resolve'>} Deferred
*/
/**
* @template T
* @returns {PromiseKit<T>}
*/
const makePromiseKit = () => {
let resolve, reject;
const promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
// @ts-ignore
return { promise, resolve, reject };
};
/** @param {<T>(register: (val: T, name?: string, shouldCollect?: boolean) => T, done: Promise) => PromiseLike | void} testCase */
function test(testCase) {
let targets = [];
const register = (
val,
name = `value${targets.length + 1}`,
shouldCollect = true
) => {
const collected = makePromiseKit();
const targetInfo = {
name,
shouldCollect,
wasCollected: false,
onCollected: collected.promise,
};
targets.push(targetInfo);
collected.promise.then(() => {
targetInfo.wasCollected = true;
});
fr.register(val, collected.resolve);
return val;
};
const done = makePromiseKit();
return Promise.resolve(testCase(register, done.promise))
.then(() => {
const sentinelCollected = new Promise((resolve) => {
fr.register({}, resolve);
});
queueGCJob().then(gc);
return sentinelCollected;
})
.then(() => {})
.then(() => {
const result = Object.fromEntries(
targets.map(({ name, wasCollected, shouldCollect }) => [
name,
{ wasCollected, shouldCollect },
])
);
done.resolve();
return result;
});
}
const resolveWith = (value) => Promise.resolve().then(() => value);
const tests = {
// The promise and the resolver are dropped, and collected as expected
DropPromiseDropResolve: (register) => {
register(
new Promise((resolve) => {
register(resolve, "resolve");
}),
"promise"
);
// Test keeps nothing around
},
// // promise is collected by all engines
DropPromiseDropResolveWithReaction: (register) => {
register(
new Promise((resolve) => {
register(resolve, "resolve");
}),
"promise"
).then(() => {
console.log("never");
});
},
// The promise is kept but resolver dropped, resolver is collected (except SM)
KeepPromiseDropResolve: (register, done) => {
const promise = new Promise((resolve) => {
register(resolve, "resolve");
});
// Test keeps promise through the test
done.then(() => console.log("KeepPromiseDropResolve done:", promise));
},
// The resolver is kept but promise dropped. The promise could be collected.
// The promise has no reactions and is not observed by the program
KeepResolveDropPromiseNoReactions: (register, done) => {
let resolve;
register(
new Promise((r) => {
resolve = r;
}),
"promise"
);
// Test keeps the resolver around
done.then(() => resolve("foo"));
},
// The resolver is kept but promise dropped. The promise should not be collected.
// The promise has reactions, and while it will not observed by the program,
// Dropping it before settlement may be unexpected (and against our use case)
KeepResolveDropPromiseWithReactions: (register, done) => {
let resolve;
const printResult = (value) =>
console.log("KeepResolveDropPromiseWithReactions done:", value);
register(
(() => {
const p = new Promise((r) => {
resolve = r;
});
// add reaction without closing over "p"
p.then(printResult);
return p;
})(),
"promise",
// Should not collect
false
);
// Test keeps the resolver around
done.then(() => resolve("foo"));
},
// The resolver is kept but settled promise is dropped.
// The promise should be collected (only SM).
// The promise no longer has reactions (resolver is inert),
// and is not observed by the program.
KeepResolveDropSettledPromise: (register, done) =>
register(
new Promise((resolve) => {
done.then(resolve);
resolve(null);
}),
"promise"
),
// `candidate` promise is settled, it should be collected
// `raced` promise is settled, it should be collected
// `result` promise is settled, and is correctly collected
Race: (register) =>
(() => {
const candidate = Promise.resolve("done");
const raced = Promise.race([forever, candidate]);
const result = resolveWith(candidate);
register(raced, "candidate");
register(raced, "raced");
register(result, "result");
return Promise.allSettled([raced, result]);
})().then(([val1, val2]) => {
console.log("Race done:", JSON.stringify(val1), JSON.stringify(val2));
}),
};
setupGC().then(async () => {
Object.entries(tests).reduce(
(prev, [name, testCase]) =>
prev
.then(() => test(testCase))
.then((collected) =>
console.log(
name,
"collected:",
Object.entries(collected)
.map(
([name, { wasCollected, shouldCollect }]) =>
`${name}=${wasCollected}(${
shouldCollect !== wasCollected ? "un" : ""
}expected)`
)
.join(", ")
)
),
Promise.resolve()
);
});
#### JavaScriptCore
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected)
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected)
KeepPromiseDropResolve done: [object Promise]
KeepPromiseDropResolve collected: resolve=true(expected)
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected)
KeepResolveDropPromiseWithReactions collected: promise=false(expected)
KeepResolveDropSettledPromise collected: promise=false(unexpected)
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"}
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected)
foo: undefined
#### Moddable XS, V8
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected)
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected)
KeepPromiseDropResolve done: [object Promise]
KeepPromiseDropResolve collected: resolve=true(expected)
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected)
KeepResolveDropPromiseWithReactions done: foo
KeepResolveDropPromiseWithReactions collected: promise=false(expected)
KeepResolveDropSettledPromise collected: promise=false(unexpected)
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"}
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected)
#### SpiderMonkey
DropPromiseDropResolve collected: resolve=true(expected), promise=true(expected)
DropPromiseDropResolveWithReaction collected: resolve=true(expected), promise=true(expected)
KeepPromiseDropResolve done: [object Promise]
KeepPromiseDropResolve collected: resolve=false(unexpected)
KeepResolveDropPromiseNoReactions collected: promise=false(unexpected)
KeepResolveDropPromiseWithReactions done: foo
KeepResolveDropPromiseWithReactions collected: promise=false(expected)
KeepResolveDropSettledPromise collected: promise=true(expected)
Race done: {"status":"fulfilled","value":"done"} {"status":"fulfilled","value":"done"}
Race collected: candidate=false(unexpected), raced=false(unexpected), result=true(expected)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment