image/test/mochitest/test_discardAnimatedImage.html
author Andy Leiserson <aleiserson@mozilla.com>
Sat, 19 Jul 2025 16:44:54 +0000 (9 hours ago)
changeset 797257 246e16bb06c941d6f64d807d43c807bfba04ae86
parent 697592 7ebd26a0e71e71dfab98d9026f69ed29c2289878
permissions -rw-r--r--
Bug 1976958 - Update wgpu to b83c9cf (2025-07-10) r=webgpu-reviewers,supply-chain-reviewers,teoxoy Differential Revision: https://phabricator.services.mozilla.com/D257047
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=686905
-->
<head>
  <title>Test that animated images can be discarded</title>
  <script src="/tests/SimpleTest/SimpleTest.js"></script>
  <script src="/tests/SimpleTest/WindowSnapshot.js"></script>
  <script type="text/javascript" src="imgutils.js"></script>
  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686905">Mozilla Bug 686905</a>
<p id="display"></p>
<div id="content">
  <div id="container">
    <canvas id="canvas" width="100" height="100"></canvas>

    <!-- NOTE if adding a new image here you need to adjust four other places:
      * add it to the gImgs array below
      * add it to the 4 arrays below that
      * add it to the condition in checkIfFinished if it's infinite
      * potentially update the starting index of finite imgs in
        addCallbacks observer.discard.
    -->
    <img id="infinitepng" src="infinite-apng.png">
    <img id="infinitegif" src="animated1.gif">
    <img id="infinitewebp" src="infinite.webp">
    <img id="infiniteavif" src="infinite.avif">

    <img id="corruptinfinitegif" src="1835509.gif">

    <img id="finitepng" src="restore-previous.png">
    <img id="finitegif" src="animated-gif.gif">
    <img id="finitewebp" src="keep.webp">
    <img id="finiteavif" src="animated-avif.avif">
    <!-- NOTE see above the steps you need to do if adding an img here -->

  </div>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">

/** Test for Bug 686905. **/
SimpleTest.waitForExplicitFinish();

var gFinished = false;

var gNumDiscards = 0;

window.onload = function() {
  // Enable discarding for the test.
  SpecialPowers.pushPrefEnv({
    'set':[['image.mem.discardable',true],
           ['image.avif.sequence.enabled',true]]
  }, runTest);
}

var gImgs = ['infinitepng', 'infinitegif', 'infinitewebp', 'infiniteavif',
             'corruptinfinitegif',
             'finitepng',   'finitegif',   'finitewebp', 'finiteavif'];
// If we are currently counting frame updates.
var gCountingFrameUpdates = false;
// The number of frame update notifications for the images in gImgs that happen
// after discarding. (The last two images are finite looping so we don't expect
// them to get incremented but it's possible if they don't finish their
// animation before we discard them.)
var gNumFrameUpdates = [0, 0, 0, 0, 0, 0, 0, 0, 0];
// The last snapshot of the image. Used to check that the image actually changes.
var gLastSnapShot = [null, null, null, null, null, null, null, null, null];
// Number of observed changes in the snapshot.
var gNumSnapShotChanges = [0, 0, 0, 0, 0, 0, 0, 0, 0];
// If we've removed the observer.
var gRemovedObserver = [false, false, false, false, false, false, false, false, false];

// 2 would probably be a good enough test, we arbitrarily choose 4.
var kNumFrameUpdatesToExpect = 5;

function runTest() {
  let numImgsInDoc = document.getElementsByTagName("img").length;
  ok(gImgs.length == numImgsInDoc, "gImgs missing img");
  ok(gNumFrameUpdates.length == numImgsInDoc, "gNumFrameUpdates missing img");
  ok(gLastSnapShot.length == numImgsInDoc, "gLastSnapShot missing img");
  ok(gNumSnapShotChanges.length == numImgsInDoc, "gNumSnapShotChanges missing img");
  ok(gRemovedObserver.length == numImgsInDoc, "gRemovedObserver missing img");

  var animatedDiscardable =
    SpecialPowers.getBoolPref('image.mem.animated.discardable');
  if (!animatedDiscardable) {
    ok(true, "discarding of animated images is disabled, nothing to test");
    SimpleTest.finish();
    return;
  }

  setTimeout(step2, 0);
}

function step2() {
  // Draw the images to canvas to force them to be decoded.
  for (let i = 0; i < gImgs.length; i++) {
    drawCanvas(document.getElementById(gImgs[i]));
  }

  for (let i = 0; i < gImgs.length; i++) {
    addCallbacks(document.getElementById(gImgs[i]), i);
  }

  setTimeout(step3, 0);
}

function step3() {
  document.getElementById("container").style.display = "none";
  document.documentElement.offsetLeft; // force that style to take effect

  for (var i = 0; i < gImgs.length; i++) {
    requestDiscard(document.getElementById(gImgs[i]));
  }

  // the discard observers will call step4
}

function step4() {
  gCountingFrameUpdates = true;
  document.getElementById("container").style.display = "";

  // Draw the images to canvas to force them to be decoded again.
  for (var i = 0; i < gImgs.length; i++) {
    drawCanvas(document.getElementById(gImgs[i]));
  }
}

function checkIfFinished() {
  if (gFinished) {
    return;
  }

  if ((gNumFrameUpdates[0] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[1] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[2] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[3] >= kNumFrameUpdatesToExpect) &&
      (gNumFrameUpdates[4] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[0] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[1] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[2] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[3] >= kNumFrameUpdatesToExpect) &&
      (gNumSnapShotChanges[4] >= kNumFrameUpdatesToExpect)) {
    ok(true, "got expected frame updates");
    gFinished = true;
    SimpleTest.finish();
  }
}

// arrayIndex is the index into the arrays gNumFrameUpdates and gNumDecodes
// to increment when a frame update notification is received.
function addCallbacks(anImage, arrayIndex) {
  var observer = new ImageDecoderObserverStub();
  observer.discard = function () {
    gNumDiscards++;
    ok(true, "got image discard");
    if (arrayIndex >= 5) {
      // The last four images are finite, so we don't expect any frame updates,
      // this image is done the test, so remove the observer.
      if (!gRemovedObserver[arrayIndex]) {
        gRemovedObserver[arrayIndex] = true;
        imgLoadingContent.removeObserver(scriptedObserver);
      }
    }
    if (gNumDiscards == gImgs.length) {
      step4();
    }
  };
  observer.frameUpdate = function () {
    if (!gCountingFrameUpdates) {
      return;
    }

    // Do this off a setTimeout since nsImageLoadingContent uses a scriptblocker
    // when it notifies us and calling drawWindow can call will paint observers
    // which can dispatch a scrollport event, and events assert if dispatched
    // when there is a scriptblocker.
    setTimeout(function () {
      gNumFrameUpdates[arrayIndex]++;

      var r = document.getElementById(gImgs[arrayIndex]).getBoundingClientRect();
      var snapshot = snapshotRect(window, r, "rgba(0,0,0,0)");
      if (gLastSnapShot[arrayIndex] != null) {
        if (snapshot.toDataURL() != gLastSnapShot[arrayIndex].toDataURL()) {
          gNumSnapShotChanges[arrayIndex]++;
        }
      }
      gLastSnapShot[arrayIndex] = snapshot;

      if (gNumFrameUpdates[arrayIndex] >= kNumFrameUpdatesToExpect &&
          gNumSnapShotChanges[arrayIndex] >= kNumFrameUpdatesToExpect) {
        if (!gRemovedObserver[arrayIndex]) {
          gRemovedObserver[arrayIndex] = true;
          imgLoadingContent.removeObserver(scriptedObserver);
        }
      }
      if (!gFinished) {
        // because we do this in a setTimeout we can have several in flight
        // so don't call ok if we've already finished.
        ok(true, "got frame update");
      }
      checkIfFinished();
    }, 0);
  };
  observer = SpecialPowers.wrapCallbackObject(observer);

  var scriptedObserver = SpecialPowers.Cc["@mozilla.org/image/tools;1"]
                           .getService(SpecialPowers.Ci.imgITools)
                           .createScriptedObserver(observer);

  var imgLoadingContent = SpecialPowers.wrap(anImage);
  imgLoadingContent.addObserver(scriptedObserver);
}

function requestDiscard(anImage) {
  var request = SpecialPowers.wrap(anImage)
      .getRequest(SpecialPowers.Ci.nsIImageLoadingContent.CURRENT_REQUEST);
  setTimeout(() => request.requestDiscard(), 0);
}

function drawCanvas(anImage) {
  var canvas = document.getElementById('canvas');
  var context = canvas.getContext('2d');

  context.clearRect(0,0,100,100);
  var cleared = canvas.toDataURL();

  context.drawImage(anImage, 0, 0);
  ok(true, "we got through the drawImage call without an exception being thrown");

  ok(cleared != canvas.toDataURL(), "drawImage drew something");
}

</script>
</pre>
</body>
</html>