chromium/third_party/blink/web_tests/wpt_internal/dom/abort/resources/abort-signal-any-memory-tests.js

// Global state that should be prevented from being garbage collected.
let gController;
let gController2;
let gSignals = [];

function abortSignalAnyMemoryTests(signalInterface, controllerInterface) {
  const suffix = `(using ${signalInterface.name} and ${controllerInterface.name})`;

  // Use promise tests so tests are not interleaved (to prevent global state
  // from getting clobbered).
  promise_test(async t => {
    let wr1;
    let wr2;

    (function() {
      let controller1 = new controllerInterface();
      let controller2 = new controllerInterface();

      gSignals.push(controller1.signal);
      gSignals.push(controller2.signal);

      signal = signalInterface.any(gSignals);
      gSignals.push(signal);

      wr1 = new WeakRef(controller1);
      wr2 = new WeakRef(controller2);
      controller1 = null;
      controller2 = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'controller1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'controller2 should be GCed');
  }, `Controllers can be GCed when their signals are being followed ${suffix}`);

  promise_test(async t => {
    let wr1;
    let wr2;
    let wr3;

    (function() {
      let controller1 = new controllerInterface();
      let controller2 = new controllerInterface();
      let signal = signalInterface.any([controller1.signal, controller2.signal]);

      wr1 = new WeakRef(controller1);
      wr2 = new WeakRef(controller2);
      wr3 = new WeakRef(signal);

      controller1 = null;
      controller2 = null;
      signal = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'controller1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'controller2 should be GCed');
    assert_equals(wr3.deref(), undefined, 'signal should be GCed');
  }, `Signals can be GCed when all abort sources have been GCed ${suffix}`);

  promise_test(async t => {
    let wr1;
    let wr2;

    gController = new controllerInterface();

    (function() {
      let signal1 = signalInterface.any([gController.signal]);
      let signal2 = signalInterface.any([signal1]);

      wr1 = new WeakRef(signal1);
      wr2 = new WeakRef(signal2);

      signal1 = null;
      signal2 = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'signal1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'signal2 should be GCed');
  }, `Signals can be GCed when they have no references or event listeners ${suffix}`);

  promise_test(async t => {
    let wr1;
    let wr2;

    gController = new controllerInterface();

    (function() {
      let signal1 = signalInterface.any([gController.signal]);
      signal1.addEventListener('event', () => {});

      let signal2 = signalInterface.any([signal1]);
      signal2.addEventListener('event', () => {});

      wr1 = new WeakRef(signal1);
      wr2 = new WeakRef(signal2);

      signal1 = null;
      signal2 = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'signal1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'signal2 should be GCed');
  }, `Signals can be GCed when they have no references or relevant event listeners ${suffix}`);

  promise_test(async t => {
    let wr1;
    let wr2;

    gController = new controllerInterface();

    (function() {
      let signal1 = signalInterface.any([gController.signal]);
      let signal2 = signalInterface.any([signal1]);

      wr1 = new WeakRef(signal1);
      wr2 = new WeakRef(signal2);

      const abortCallback1 = () => {};
      const abortCallback2 = () => {};

      signal1.addEventListener('abort', abortCallback1);
      signal1.addEventListener('abort', abortCallback2);

      signal2.addEventListener('abort', abortCallback1);
      signal2.addEventListener('abort', abortCallback2);

      signal1.removeEventListener('abort', abortCallback1);
      signal1.removeEventListener('abort', abortCallback2);

      signal2.removeEventListener('abort', abortCallback1);
      signal2.removeEventListener('abort', abortCallback2);

      signal1 = null;
      signal2 = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'signal1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'signal2 should be GCed');
  }, `Signals can be GCed when all abort event listeners have been removed ${suffix}`);

  promise_test(async t => {
    let fired = false;

    gController = new controllerInterface();

    (function() {
      let signal = signalInterface.any([gController.signal]);
      signal.onabort = t.step_func((e) => {
        fired = true;
        assert_true(e.target.aborted);
      });

      signal = null;
    })();

    await runAsyncGC();

    gController.abort();
    assert_true(fired, 'signal should not be GCed before being aborted');
  }, `Signals are not GCed before being aborted by a controller when they have abort event listeners ${suffix}`);

  promise_test(async t => {
    let fired = false;
    let wr;

    gController = new controllerInterface();

    (function() {
      let signal = signalInterface.any([AbortSignal.timeout(20)]);
      signal.onabort = t.step_func(() => {
        fired = true;
      });
      wr = new WeakRef(signal);
      signal = null;
    })();

    await runAsyncGC();
    await t.step_wait(() => fired, 'The abort listener should run before the signal is GCed', 500, 20);
    await runAsyncGC();
    assert_equals(wr.deref(), undefined, 'signal should be GCed');
  }, `Composite signals are not GCed before being aborted by timeout when they have abort event listeners ${suffix}`);

  promise_test(async t => {
    let fired = false;
    let wr1;
    let wr2;

    gController = new controllerInterface();

    (function() {
      // `tempCompositeSignal` can be GCed after this function because it is
      // only used to construct `compositeSignal`.
      let tempCompositeSignal = signalInterface.any([gController.signal]);
      wr1 = new WeakRef(tempCompositeSignal);

      let compositeSignal = signalInterface.any([tempCompositeSignal]);
      compositeSignal.onabort = t.step_func(() => {
        fired = true;
      });
      wr2 = new WeakRef(compositeSignal);

      tempCompositeSignal = null;
      compositeSignal = null;
    })();

    await runAsyncGC();

    assert_equals(wr1.deref(), undefined, 'tempCompositeSignal should be GCed');
    assert_not_equals(wr2.deref(), undefined, 'compositeSignal shound not be GCed yet');
    assert_false(fired, 'The abort listener should not run before tempCompositeSignal is GCed');

    gController.abort();

    await runAsyncGC();

    assert_equals(wr2.deref(), undefined, 'compositeSignal should be GCed');
    assert_true(fired, 'The abort listener should run before compositeSignal is GCed');
  }, `Temporary composite signals used for constructing other composite signals can be GCed ${suffix}`);

  promise_test(async t => {
    let fired = false;
    let wr1;
    let wr2;
    let wr3;
    let wr4;
    let wr5;

    gController = new controllerInterface();
    gController2 = new controllerInterface();

    (function() {
      // These signals should be GCed after this function runs, before the
      // timeout aborts `testSignal`.
      let signal1 = signalInterface.any([gController.signal]);
      let signal2 = signalInterface.any([gController2.signal]);
      let signal3 = signalInterface.any([signal1, signal2]);

      wr1 = new WeakRef(signal1);
      wr2 = new WeakRef(signal2);
      wr3 = new WeakRef(signal3);

      let timeoutSignal = AbortSignal.timeout(20);
      // This and `timeoutSignal` must remain alive until the timeout fires.
      let testSignal = signalInterface.any([signal3, timeoutSignal]);
      testSignal.onabort = t.step_func(() => {
        fired = true;
      });

      wr4 = new WeakRef(timeoutSignal);
      wr5 = new WeakRef(testSignal);

      signal1 = null;
      signal2 = null;
      signal3 = null;
      timeoutSignal = null;
      testSignal = null;
    })();

    // Running GC async in high priority tasks should complete before the timeout.
    await runAsyncGC({priority: 'user-blocking'});
    assert_false(fired, 'GC should complete before the timeout fires');
    assert_equals(wr1.deref(), undefined, 'signal1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'signal2 should be GCed');
    assert_equals(wr3.deref(), undefined, 'signal3 should be GCed');
    assert_not_equals(wr4.deref(), undefined, 'timeoutSignal should not be GCed before the timeout');
    assert_not_equals(wr5.deref(), undefined, 'testSignal should not be GCed before the timeout');

    await t.step_wait(() => fired, 'The abort listener should run before the signal is GCed', 500, 20);

    await runAsyncGC();
    assert_equals(wr4.deref(), undefined, 'timeoutSignal should be GCed');
    assert_equals(wr5.deref(), undefined, 'testSignal should be GCed');
  }, `Nested and intermediate composite signals can be GCed when expected ${suffix}`);

  promise_test(async t => {
    let wr1;
    let wr2;

    (function() {
      let signal1 = signalInterface.any([]);
      signal1.addEventListener('abort', () => {});
      // For plain AbortSignals, this should not be a no-op. For TaskSignals,
      // this will test the settling logic.
      signal1.addEventListener('prioritychange', () => {});
      wr1 = new WeakRef(signal1);

      let controller = new controllerInterface();
      let signal2 = signalInterface.any([controller.signal]);
      signal2.addEventListener('abort', () => {});
      signal2.addEventListener('prioritychange', () => {});
      wr2 = new WeakRef(signal2);

      signal1 = null;
      signal2 = null;
      controller = null;
    })();

    await runAsyncGC();
    assert_equals(wr1.deref(), undefined, 'signal1 should be GCed');
    assert_equals(wr2.deref(), undefined, 'signal2 should be GCed');
  }, `Settled composite signals with event listeners can be GCed ${suffix}`);
}