Files
sousa-gecko/toolkit/modules/tests/xpcshell/test_DeferredTask_gc.js
T
Rob Wu 7da0fc6819 Bug 2016940 - Automatically cancel DeferredTask when window unloads r=mconley
The _startIdleDispatch uses `ChromeUtils.idleDispatch`, which may keep
the DeferredTask instance alive longer than necessary, which in turn
prevents the task callback and its closure from being garbage collected.

This has resulted in observed window leaks at the end of test runs. To
prevent this issue from happening, we automatically detect whether the
callback belongs to a window, and if so, tie the (idle) timers to that
window, so that the callbacks are immediately released when that window
unloads.

We also clear the callback reference when `finalize()` is called, so
that the task callback and its closure are not kept alive for longer
than necessary (upon unload), even if the window is still alive.

Differential Revision: https://phabricator.services.mozilla.com/D284782
2026-03-02 22:00:00 +00:00

68 lines
2.1 KiB
JavaScript

/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
ChromeUtils.defineESModuleGetters(this, {
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
});
/**
* Checks that the "finalize" method allows the callback to be GC'd.
*/
add_task(async function test_finalize_after_arm() {
let count = 0;
let callback = () => {
++count;
};
let callbackWeakRef = Cu.getWeakReference(callback);
let deferredTask = new DeferredTask(
callback,
/* delayMs */ 600_000, // long timeout, we are not going to wait for it.
/* idleTimeoutMs */ undefined // no deadline.
);
// Clear callback reference, so that we are not the ones keeping it alive.
callback = null;
Cu.forceGC();
ok(callbackWeakRef.get(), "Callback not GC'd yet");
deferredTask.arm();
equal(count, 0, "callback not called yet");
await deferredTask.finalize();
equal(count, 1, "callback force called by finalize");
Cu.forceGC();
ok(!callbackWeakRef.get(), "Callback GC'd after finalize");
// Make sure to reference deferredTask so that there is still a strong
// reference to deferredTask, which prevents deferredTask from trivially
// being collected.
Assert.throws(
() => deferredTask.finalize(),
/The object has been already finalized/,
"finalize() cannot be called twice"
);
});
add_task(async function test_gc_without_arm_or_finalize() {
let count = 0;
let callback = () => {
++count;
};
let callbackWeakRef = Cu.getWeakReference(callback);
let deferredTask = new DeferredTask(
callback,
/* delayMs */ 600_000, // long timeout, we are not going to wait for it.
/* idleTimeoutMs */ undefined // no deadline.
);
// Clear callback reference, so that we are not the ones keeping it alive.
callback = null;
let deferredTaskWeakRef = Cu.getWeakReference(deferredTask);
deferredTask = null;
Cu.forceGC();
ok(!deferredTaskWeakRef.get(), "DeferredTask can be GC'd when not armed");
ok(!callbackWeakRef.get(), "Callback GC'd when DeferredTask is GC'd");
equal(count, 0, "callback was never called");
});