image/test/mochitest/test_discardFramesAnimatedImage.html
author Andy Leiserson <aleiserson@mozilla.com>
Sat, 19 Jul 2025 16:44:54 +0000 (10 hours ago)
changeset 797257 246e16bb06c941d6f64d807d43c807bfba04ae86
parent 490610 3be18873536a3f403c9a0cf30f06bc939103a787
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=523950
-->
<head>
  <title>Test that animated images can discard frames and redecode</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=523950">Mozilla Bug 523950</a>
<p id="display"></p>
<div id="content">
  <div id="container">
    <canvas id="canvas" width="100" height="100"></canvas>
    <img id="rainbow.gif"/>
  </div>
</div>
<pre id="test">
<script class="testbody" type="text/javascript">

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

var gFinished = false;

var gNumOnloads = 0;

var gNumDiscards = 0;

window.onload = function() {
  // Enable minimal frame discard thresholds for the test.
  SpecialPowers.pushPrefEnv({
    'set':[['image.animated.decode-on-demand.threshold-kb',0],
           ['image.animated.decode-on-demand.batch-size',1],
           ['image.mem.discardable',true],
           ['image.mem.animated.discardable',true]]
  }, runTest);
}

var gImgs = ['rainbow.gif'];
// 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];
// The last snapshot of the image. Used to check that the image actually changes.
var gLastSnapShot = [null];
// Number of observed changes in the snapshot.
var gNumSnapShotChanges = [0];
// If we've removed the observer.
var gRemovedObserver = [false];

// rainbow.gif has 9 frames, so we choose arbitrarily 22 to include two loops.
var kNumFrameUpdatesToExpect = 22;

function runTest() {
  var thresholdKb =
    SpecialPowers.getIntPref('image.animated.decode-on-demand.threshold-kb');
  var batchSize =
    SpecialPowers.getIntPref('image.animated.decode-on-demand.batch-size');
  var discardable =
    SpecialPowers.getBoolPref('image.mem.discardable');
  var animDiscardable =
    SpecialPowers.getBoolPref('image.mem.animated.discardable');
  if (thresholdKb > 0 || batchSize > 1 || !discardable || !animDiscardable) {
    ok(true, "discarding frames of animated images is disabled, nothing to test");
    SimpleTest.finish();
    return;
  }

  setTimeout(step2, 0);
}

function step2() {
  // Only set the src after setting the pref.
  for (var i = 0; i < gImgs.length; i++) {
    var elm = document.getElementById(gImgs[i]);
    elm.src = gImgs[i];
    elm.onload = checkIfLoaded;
  }
}

function step3() {
  // 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(step4, 0);
}

function step4() {
  ok(true, "now accepting frame updates");
  gCountingFrameUpdates = true;
}

function step5() {
  ok(true, "discarding images");

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

  // Reset our state to let us do it all again after discarding.
  resetState();

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

  // the discard observers will call step6
}

function step6() {
  // Repeat the cycle now that we discarded everything.
  ok(gNumDiscards >= gImgs.length, "discard complete, restarting animations");
  document.getElementById("container").style.display = "";

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

  setTimeout(step4, 0);
}

function checkIfLoaded() {
  ++gNumOnloads;
  if (gNumOnloads != gImgs.length) {
    return;
  }

  ok(true, "got onloads");
  setTimeout(step3, 0);
}

function resetState() {
  gFinished = false;
  gCountingFrameUpdates = false;
  for (let i = 0; i < gNumFrameUpdates.length; ++i) {
    gNumFrameUpdates[i] = 0;
  }
  for (let i = 0; i < gNumSnapShotChanges.length; ++i) {
    gNumSnapShotChanges[i] = 0;
  }
  for (let i = 0; i < gLastSnapShot.length; ++i) {
    gLastSnapShot[i] = null;
  }
}

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

  for (var i = 0; i < gNumFrameUpdates.length; ++i) {
    if (gNumFrameUpdates[i] < kNumFrameUpdatesToExpect ||
        gNumSnapShotChanges[i] < kNumFrameUpdatesToExpect) {
      return;
    }
  }

  ok(true, "got expected frame updates");
  gFinished = true;

  if (gNumDiscards == 0) {
    // If we haven't discarded any complete images, we should do so, and
    // verify the animation again.
    setTimeout(step5, 0);
    return;
  }

  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 (gNumDiscards == gImgs.length) {
      step6();
    }
  };
  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 &&
	  gNumDiscards >= gImgs.length) {
        if (!gRemovedObserver[arrayIndex]) {
	  ok(true, "removing observer for " + 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>