From 58eb082d3fbcdd7d1d68028acb690204c8fcfc6e Mon Sep 17 00:00:00 2001
From: Yoav Weiss <yoavweiss@chromium.org>
Date: Thu, 28 Oct 2021 10:28:31 +0000
Subject: [PATCH] Bug 1736586 [wpt PR 31314] - [LCP] Add animated image
 support, a=testonly
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Automatic update from web-platform-tests
[LCP] Add animated image support

This CL adds support for better handling of animated images in LCP:
* A new attribute is exposing the first animated frame's paint time
(behind a flag).
* `startTime` is not changed.
* The PageLoadMetrics reported for LCP are set to that first frame paint
time for animated images (behind another flag).
* Entries are not emitted until the image is loaded.

Relevant spec issue:
https://github.com/WICG/largest-contentful-paint/issues/83

Change-Id: I6bb01eacb4f200f9c032ffcfcd9a1a41126a7773
Bug: 1260953
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3226157
Commit-Queue: Yoav Weiss <yoavweiss@chromium.org>
Reviewed-by: Nicolás Peña Moreno <npm@chromium.org>
Cr-Commit-Position: refs/heads/main@{#935133}

--

wpt-commits: 8c690c8980d38fc3c01d8d9a284367401dec4fb3
wpt-pr: 31314
---
 .../web-platform/tests/images/anim-tao.png    | Bin 0 -> 460 bytes
 .../tests/images/anim-tao.png.headers         |   2 ++
 .../tests/images/webp-animated.webp           | Bin 0 -> 340 bytes
 .../observe-animated-image-gif.tentative.html |  27 +++++++++++++++
 ...observe-animated-image-webp.tentative.html |  27 +++++++++++++++
 .../observe-animated-image.tentative.html     |  29 ++++++++++++++++
 ...cross-origin-animated-image.tentative.html |  30 ++++++++++++++++
 ...s-origin-tao-animated-image.tentative.html |  30 ++++++++++++++++
 .../observe-non-animated-image.tentative.html |  27 +++++++++++++++
 .../largest-contentful-paint-helpers.js       |  32 ++++++++++++++++++
 10 files changed, 204 insertions(+)
 create mode 100644 testing/web-platform/tests/images/anim-tao.png
 create mode 100644 testing/web-platform/tests/images/anim-tao.png.headers
 create mode 100644 testing/web-platform/tests/images/webp-animated.webp
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
 create mode 100644 testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html

diff --git a/testing/web-platform/tests/images/anim-tao.png b/testing/web-platform/tests/images/anim-tao.png
new file mode 100644
index 0000000000000000000000000000000000000000..925e2efc9a97ade490f04d57271adc10b2fe69b6
GIT binary patch
literal 460
zcmeAS@N?(olHy`uVBq!ia0vp^DL`z*!3HE(nbz$CQXGlNAwEEw35Xd!_f9SVQc`IU
zF^~{g1Bd|zjLa_>8Ci1NIDx$Bo-U3d6?5L6GvoytbHL!w?6zvIV;iqHq?@gO(Ktbl
z<K;jQ2N+p&UTaSlw4MTVBFrO<KsJg;)CE8uxnZWB2$Y$Smf{!!q?v%8xww#*!9d{1
zhTHWo-%Ics81zrMTUPR>oS|)rq)@U)kD_7Q5eZr|_$v>-*5=KZ^ai?#!PC{xWt~$(
F698Hck68c!

literal 0
HcmV?d00001

diff --git a/testing/web-platform/tests/images/anim-tao.png.headers b/testing/web-platform/tests/images/anim-tao.png.headers
new file mode 100644
index 00000000000000..0230e176e44567
--- /dev/null
+++ b/testing/web-platform/tests/images/anim-tao.png.headers
@@ -0,0 +1,2 @@
+Timing-Allow-Origin: *
+
diff --git a/testing/web-platform/tests/images/webp-animated.webp b/testing/web-platform/tests/images/webp-animated.webp
new file mode 100644
index 0000000000000000000000000000000000000000..35a8dfcf34d58060ec70f0a2a04e0d58e357e084
GIT binary patch
literal 340
zcmWIYbaV4zWMBw)bqWXzu!!JdU|<jeVjwNUz~JcT>B|P>F);l9590Z{MS*!B0;1vt
zGXn!qpN|tzM4yX+ok3hd%;(wvmN~~P`X2vb?_+b_xHcr*I-p6!<-_fEjddq~hN*SD
z*x#_=1f%tZIXZErQ(4dTt19a~HWL-+;f2`%bPy{81JGsx28J&{4%`Md1{R<V0v<te
zDRqqwlRhaKbF)mE!9KI{A9KB^Mpw>q&&Br)8)v=KKh9&++1c^WpP^yW)$4OZRe!M0
zE0}Gar~tAd2Iy9xp+E$*fuDf^<_8y$AJ`5sgKbEX!)8O4^vRv~{y8UIh<UpG{MAD?
aB7%SYIjwr!7V~kw^RNGW`0(Mgmze?U@@4@5

literal 0
HcmV?d00001

diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
new file mode 100644
index 00000000000000..a2c0d7975abe34
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-gif.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 136 is the size of the animated GIF up until the first frame.
+      // The trickle pipe delays the response after the first frame by 1 second.
+      const url = window.location.origin +
+                  `/images/anim-gr.gif?pipe=trickle(136:d${delay_pipe_value})`;
+      const entry = await load_and_observe(url);
+      // anim-gr.gif is 100 by 50.
+      const size = 100 * 50;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
new file mode 100644
index 00000000000000..de59d5c5f78c68
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image-webp.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 142 is the size of the animated WebP up until the first frame.
+      // The trickle pipe delays the response after the first frame by 1 second.
+      const url = window.location.origin +
+        `/images/webp-animated.webp?pipe=trickle(142:d${delay_pipe_value})`;
+      const entry = await load_and_observe(url);
+      // webp-animated.webp is 11 by 29.
+      const size = 11 * 29;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html
new file mode 100644
index 00000000000000..cf7d262b0f842a
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-animated-image.tentative.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 262 is the size of the animated PNG up until the first frame,
+      // including the chunk that starts the second frame (indicating that
+      // the first frame data is done).
+      // The trickle pipe delays the response after the first frame by 1 second.
+      const url = window.location.origin +
+                  `/images/anim-gr.png?pipe=trickle(262:d${delay_pipe_value})`;
+      const entry = await load_and_observe(url);
+      // anim-gr.png is 100 by 50.
+      const size = 100 * 50;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
new file mode 100644
index 00000000000000..993883c607b8f7
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+  <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 262 is the size of the animated PNG up until the first frame,
+      // including the chunk that starts the second frame (indicating that
+      //the first frame data is done).
+      const {REMOTE_ORIGIN} = get_host_info();
+      const url = REMOTE_ORIGIN +
+                  '/images/anim-gr.png?pipe=trickle(262:d1)';
+      const entry = await load_and_observe(url);
+      // anim-gr.png is 100 by 50.
+      const size = 100 * 50;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["renderTimeIs0", "animated-zero"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
new file mode 100644
index 00000000000000..137dde66383f77
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-cross-origin-tao-animated-image.tentative.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+  <script src="/common/get-host-info.sub.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 262 is the size of the animated PNG up until the first frame,
+      // including the chunk that starts the second frame (indicating that
+      //the first frame data is done).
+      const {REMOTE_ORIGIN} = get_host_info();
+      const url = REMOTE_ORIGIN +
+                  '/images/anim-tao.png?pipe=trickle(262:d1)';
+      const entry = await load_and_observe(url);
+      // anim-gr.png is 100 by 50.
+      const size = 100 * 50;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["animated"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
new file mode 100644
index 00000000000000..6bbc0958b1deb2
--- /dev/null
+++ b/testing/web-platform/tests/largest-contentful-paint/animated/observe-non-animated-image.tentative.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset=utf-8>
+  <title>Largest Contentful Paint: observe image.</title>
+  <script src="/resources/testharness.js"></script>
+  <script src="/resources/testharnessreport.js"></script>
+  <script src="../resources/largest-contentful-paint-helpers.js"></script>
+</head>
+<body>
+  <script>
+    promise_test(async () => {
+      assert_implements(window.LargestContentfulPaint,
+                        "LargestContentfulPaint is not implemented");
+      const beforeLoad = performance.now();
+      // 262 is the size of the animated PNG up until the first frame,
+      // including the chunk that starts the second frame (indicating that
+      //the first frame data is done).
+      const url = window.location.origin + '/images/blue.png';
+      const entry = await load_and_observe(url);
+      // blue.png is 133 by 106.
+      const size = 133 * 106;
+      checkImage(entry, url, 'image_id', size, beforeLoad, ["animated-zero"]);
+    }, "Same origin animated image is observable and has a first frame.");
+  </script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
index e12ece0a7561cb..5012faf3b1be33 100644
--- a/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
+++ b/testing/web-platform/tests/largest-contentful-paint/resources/largest-contentful-paint-helpers.js
@@ -1,3 +1,6 @@
+const image_delay = 1000;
+const delay_pipe_value = image_delay / 1000;
+
 // Receives an image LargestContentfulPaint |entry| and checks |entry|'s attribute values.
 // The |timeLowerBound| parameter is a lower bound on the loadTime value of the entry.
 // The |options| parameter may contain some string values specifying the following:
@@ -33,4 +36,33 @@ function checkImage(entry, expectedUrl, expectedID, expectedSize, timeLowerBound
   } else {
     assert_equals(entry.size, expectedSize);
   }
+  if (options.includes('animated')) {
+    assert_greater_than(entry.loadTime, entry.firstAnimatedFrameTime,
+      'firstAnimatedFrameTime should be smaller than loadTime');
+    assert_greater_than(entry.renderTime, entry.firstAnimatedFrameTime,
+      'firstAnimatedFrameTime should be smaller than renderTime');
+    assert_less_than(entry.firstAnimatedFrameTime, image_delay,
+      'firstAnimatedFrameTime should be smaller than the delay applied to the second frame');
+    assert_greater_than(entry.firstAnimatedFrameTime, 0,
+      'firstAnimatedFrameTime should be larger than 0');
+  }
+  if (options.includes('animated-zero')) {
+    assert_equals(entry.firstAnimatedFrameTime, 0, 'firstAnimatedFrameTime should be 0');
+  }
 }
+
+const load_and_observe = url => {
+  return new Promise(resolve => {
+    (new PerformanceObserver(entryList => {
+      for (let entry of entryList.getEntries()) {
+        if (entry.url == url) {
+          resolve(entryList.getEntries()[0]);
+        }
+      }
+    })).observe({type: 'largest-contentful-paint', buffered: true});
+    const img = new Image();
+    img.id = 'image_id';
+    img.src = url;
+    document.body.appendChild(img);
+  });
+};