Title: [291141] trunk
Revision
291141
Author
cdu...@apple.com
Date
2022-03-10 18:04:19 -0800 (Thu, 10 Mar 2022)

Log Message

Main document is leaking on haaretz.co.il due to lazy image loading
https://bugs.webkit.org/show_bug.cgi?id=237660
<rdar://problem/90035071>

Reviewed by Geoffrey Garen.

Source/WebCore:

When an HTML image uses `loading=lazy`, ImageLoader::updateFromElement() may get
called twice. If the image is outside the viewport, the first time ImageLoader::updateFromElement()
is called, we'll request a CachedImage but we'll set m_lazyImageLoadState to LazyImageLoadState::Deferred
and not ask the CachedImage to load. Then, later on, if the HTML image approaches the viewport,
ImageLoader::loadDeferredImage() will get called. It will set m_lazyImageLoadState to LazyImageLoadState::LoadImmediately
and call updateFromElement() again. This time however, updateFromElement() will actually ask the CachedImage
to load.

The issue was that the first time ImageLoader::updateFromElement(), we would set m_lazyImageLoadState to Deferred
and not ask the CachedImage to load but still set m_hasPendingLoadEvent to true. This is problematic if the CachedImage
is not already loaded since no was was started and thus no load event is coming (and no load event may ever come if the
image never approaches the viewport). When updatedHasPendingEvent() is called, it will protect the HTMLImageElement if
m_hasPendingLoadEvent is true, to make sure the image element stays alive long enough for us to dispatch the load event.
With lazy loading, this meant that we would protect the HTMLImageElement right away and undefinitely since no load event
may ever come. This meant that when navigating away from a page with images that are lazily loaded (and not loaded yet),
we would leak the HTMLImageElements (and ImageLoaders), which in turn would keep the Document alive too.

To address the issue, we now make sure that m_hasPendingLoadEvent is no longer set to true when updateFromElement()
is called but the CachedImage is not already loaded and not loading (lazy loading case). When updateFromElement() gets
called the second time (when the lazily loaded image approaches the viewport), we make sure that the m_hasPendingLoadEvent
flag gets set to true then.

Test: fast/dom/lazy-image-loading-document-leak.html

* loader/ImageLoader.cpp:
(WebCore::ImageLoader::ImageLoader):
(WebCore::ImageLoader::updateFromElement):
(WebCore::ImageLoader::didUpdateCachedImage):
(WebCore::ImageLoader::didStartLoading):
* loader/ImageLoader.h:
* loader/cache/CachedImage.cpp:
(WebCore::CachedImage::load):
* loader/cache/CachedImageClient.h:
(WebCore::CachedImageClient::didStartLoading):

LayoutTests:

Add layout test coverage.

* fast/dom/lazy-image-loading-document-leak-expected.txt: Added.
* fast/dom/lazy-image-loading-document-leak.html: Added.
* fast/dom/resources/lazy-image-loading-document-leak-popup.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (291140 => 291141)


--- trunk/LayoutTests/ChangeLog	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/LayoutTests/ChangeLog	2022-03-11 02:04:19 UTC (rev 291141)
@@ -1,3 +1,17 @@
+2022-03-10  Chris Dumez  <cdu...@apple.com>
+
+        Main document is leaking on haaretz.co.il due to lazy image loading
+        https://bugs.webkit.org/show_bug.cgi?id=237660
+        <rdar://problem/90035071>
+
+        Reviewed by Geoffrey Garen.
+
+        Add layout test coverage.
+
+        * fast/dom/lazy-image-loading-document-leak-expected.txt: Added.
+        * fast/dom/lazy-image-loading-document-leak.html: Added.
+        * fast/dom/resources/lazy-image-loading-document-leak-popup.html: Added.
+
 2022-03-10  Matteo Flores  <matteo_flo...@apple.com>
 
         [ iOS Mac ] imported/w3c/web-platform-tests/service-workers/service-worker/partitioned-service-worker-matchAll.tentative.https.html is a flaky text failure on all queues

Added: trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak-expected.txt (0 => 291141)


--- trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak-expected.txt	2022-03-11 02:04:19 UTC (rev 291141)
@@ -0,0 +1,11 @@
+Tests that lazy image loading doesn't cause Document leaks.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS internals.isDocumentAlive(frameDocumentID) is true
+PASS The popup document didn't leak.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+

Added: trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak.html (0 => 291141)


--- trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak.html	                        (rev 0)
+++ trunk/LayoutTests/fast/dom/lazy-image-loading-document-leak.html	2022-03-11 02:04:19 UTC (rev 291141)
@@ -0,0 +1,45 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src=""
+<script>
+description("Tests that lazy image loading doesn't cause Document leaks.");
+jsTestIsAsync = true;
+
+frameDocumentID = 0;
+checkCount = 0;
+popup = null;
+
+function runTest()
+{
+    popup.close();
+    popup = null;
+
+    handle = setInterval(() => {
+        gc();
+        if (!internals.isDocumentAlive(frameDocumentID)) {
+            clearInterval(handle);
+            testPassed("The popup document didn't leak.");
+            finishJSTest();
+        }
+        checkCount++;
+        if (checkCount > 500) {
+            clearInterval(handle);
+            testFailed("The popup document leaked.");
+            finishJSTest();
+        }
+    }, 10);
+}
+
+_onload_ = () => {
+    popup = window.open("resources/lazy-image-loading-document-leak-popup.html");
+    popup._onload_ = () => {
+        popup._onload_ = null;
+        frameDocumentID = internals.documentIdentifier(popup.document);
+        shouldBeTrue("internals.isDocumentAlive(frameDocumentID)");
+        setTimeout(runTest, 0);
+    }
+};
+</script>
+</body>
+</html>

Added: trunk/LayoutTests/fast/dom/resources/lazy-image-loading-document-leak-popup.html (0 => 291141)


--- trunk/LayoutTests/fast/dom/resources/lazy-image-loading-document-leak-popup.html	                        (rev 0)
+++ trunk/LayoutTests/fast/dom/resources/lazy-image-loading-document-leak-popup.html	2022-03-11 02:04:19 UTC (rev 291141)
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<body>
+<div style="height: 3000px">
+<div style="height: 2000px">Test lazy image loading</div>
+<div style="height: 1000px">
+<img loading="lazy" src=""
+<img loading="lazy" src=""
+</div>
+</div>
+</body>
+</html>

Modified: trunk/Source/WebCore/ChangeLog (291140 => 291141)


--- trunk/Source/WebCore/ChangeLog	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/Source/WebCore/ChangeLog	2022-03-11 02:04:19 UTC (rev 291141)
@@ -1,3 +1,46 @@
+2022-03-10  Chris Dumez  <cdu...@apple.com>
+
+        Main document is leaking on haaretz.co.il due to lazy image loading
+        https://bugs.webkit.org/show_bug.cgi?id=237660
+        <rdar://problem/90035071>
+
+        Reviewed by Geoffrey Garen.
+
+        When an HTML image uses `loading=lazy`, ImageLoader::updateFromElement() may get
+        called twice. If the image is outside the viewport, the first time ImageLoader::updateFromElement()
+        is called, we'll request a CachedImage but we'll set m_lazyImageLoadState to LazyImageLoadState::Deferred
+        and not ask the CachedImage to load. Then, later on, if the HTML image approaches the viewport,
+        ImageLoader::loadDeferredImage() will get called. It will set m_lazyImageLoadState to LazyImageLoadState::LoadImmediately
+        and call updateFromElement() again. This time however, updateFromElement() will actually ask the CachedImage
+        to load.
+
+        The issue was that the first time ImageLoader::updateFromElement(), we would set m_lazyImageLoadState to Deferred
+        and not ask the CachedImage to load but still set m_hasPendingLoadEvent to true. This is problematic if the CachedImage
+        is not already loaded since no was was started and thus no load event is coming (and no load event may ever come if the
+        image never approaches the viewport). When updatedHasPendingEvent() is called, it will protect the HTMLImageElement if
+        m_hasPendingLoadEvent is true, to make sure the image element stays alive long enough for us to dispatch the load event.
+        With lazy loading, this meant that we would protect the HTMLImageElement right away and undefinitely since no load event
+        may ever come. This meant that when navigating away from a page with images that are lazily loaded (and not loaded yet),
+        we would leak the HTMLImageElements (and ImageLoaders), which in turn would keep the Document alive too.
+
+        To address the issue, we now make sure that m_hasPendingLoadEvent is no longer set to true when updateFromElement()
+        is called but the CachedImage is not already loaded and not loading (lazy loading case). When updateFromElement() gets
+        called the second time (when the lazily loaded image approaches the viewport), we make sure that the m_hasPendingLoadEvent
+        flag gets set to true then.
+
+        Test: fast/dom/lazy-image-loading-document-leak.html
+
+        * loader/ImageLoader.cpp:
+        (WebCore::ImageLoader::ImageLoader):
+        (WebCore::ImageLoader::updateFromElement):
+        (WebCore::ImageLoader::didUpdateCachedImage):
+        (WebCore::ImageLoader::didStartLoading):
+        * loader/ImageLoader.h:
+        * loader/cache/CachedImage.cpp:
+        (WebCore::CachedImage::load):
+        * loader/cache/CachedImageClient.h:
+        (WebCore::CachedImageClient::didStartLoading):
+
 2022-03-10  Michael Saboff  <msab...@apple.com>
 
         Catalyst _javascript_Core, WebCore, WebKitLegacy, and WebKit shouldn't be copied to the Secondary Path

Modified: trunk/Source/WebCore/loader/ImageLoader.cpp (291140 => 291141)


--- trunk/Source/WebCore/loader/ImageLoader.cpp	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/Source/WebCore/loader/ImageLoader.cpp	2022-03-11 02:04:19 UTC (rev 291141)
@@ -47,6 +47,7 @@
 #include "RenderSVGImage.h"
 #include "Settings.h"
 #include <wtf/NeverDestroyed.h>
+#include <wtf/Scope.h>
 
 #if ENABLE(VIDEO)
 #include "RenderVideo.h"
@@ -106,6 +107,7 @@
     , m_imageComplete(true)
     , m_loadManually(false)
     , m_elementIsProtected(false)
+    , m_inUpdateFromElement(false)
 {
 }
 
@@ -177,6 +179,11 @@
     if (!m_failedLoadURL.isEmpty() && attr == m_failedLoadURL)
         return;
 
+    m_inUpdateFromElement = true;
+    auto inUpdateFromElementScope = makeScopeExit([this] {
+        m_inUpdateFromElement = false;
+    });
+
     // Do not load any image if the 'src' attribute is missing or if it is
     // an empty string.
     CachedResourceHandle<CachedImage> newImage = nullptr;
@@ -238,8 +245,16 @@
         errorEventSender().dispatchEventSoon(*this);
     }
 
+    didUpdateCachedImage(relevantMutation, WTFMove(newImage));
+}
+
+void ImageLoader::didUpdateCachedImage(RelevantMutation relevantMutation, CachedResourceHandle<CachedImage>&& newImage)
+{
+    auto& document = element().document();
+
     CachedImage* oldImage = m_image.get();
-    if (newImage != oldImage || relevantMutation == RelevantMutation::Yes) {
+    bool loadHasNowLazilyStarted = oldImage == newImage && !m_hasPendingLoadEvent && newImage && newImage->isLoading();
+    if (newImage != oldImage || relevantMutation == RelevantMutation::Yes || loadHasNowLazilyStarted) {
         if (m_hasPendingBeforeLoadEvent) {
             beforeLoadEventSender().cancelEvent(*this);
             m_hasPendingBeforeLoadEvent = false;
@@ -258,9 +273,12 @@
             m_hasPendingErrorEvent = false;
         }
 
+        // Due to lazy image loading and the loadsImagesAutomatically setting, it is possible that we did not actually start the image load.
+        bool imageWillBeLoadedLater = newImage && !newImage->isLoading() && newImage->stillNeedsLoad();
+
         m_image = newImage;
-        m_hasPendingBeforeLoadEvent = !document.isImageDocument() && newImage;
-        m_hasPendingLoadEvent = newImage;
+        m_hasPendingBeforeLoadEvent = !document.isImageDocument() && newImage && !imageWillBeLoadedLater;
+        m_hasPendingLoadEvent = newImage && !imageWillBeLoadedLater;
         m_imageComplete = !newImage;
 
         if (newImage) {
@@ -269,8 +287,12 @@
             else
                 updateRenderer();
 
-            if (m_lazyImageLoadState == LazyImageLoadState::Deferred)
-                LazyLoadImageObserver::observe(element());
+            if (m_lazyImageLoadState == LazyImageLoadState::Deferred) {
+                if (imageWillBeLoadedLater)
+                    LazyLoadImageObserver::observe(element());
+                else // No need to set up a LazyLoadImageObserver if the image is already loaded (e.g. was already cached).
+                    m_lazyImageLoadState = LazyImageLoadState::None;
+            }
 
             // If newImage is cached, addClient() will result in the load event
             // being queued to fire. Ensure this happens after beforeload is
@@ -577,6 +599,13 @@
     updateFromElement(RelevantMutation::No);
 }
 
+void ImageLoader::didStartLoading()
+{
+    if (m_inUpdateFromElement)
+        return;
+
+    didUpdateCachedImage(RelevantMutation::No, CachedResourceHandle { m_image });
+}
 void ImageLoader::resetLazyImageLoading(Document& document)
 {
     if (isDeferred())

Modified: trunk/Source/WebCore/loader/ImageLoader.h (291140 => 291141)


--- trunk/Source/WebCore/loader/ImageLoader.h	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/Source/WebCore/loader/ImageLoader.h	2022-03-11 02:04:19 UTC (rev 291141)
@@ -40,7 +40,7 @@
 template<typename T> class EventSender;
 using ImageEventSender = EventSender<ImageLoader>;
 
-enum class RelevantMutation : bool { Yes, No };
+enum class RelevantMutation : bool { No, Yes };
 
 class ImageLoader : public CachedImageClient, public CanMakeWeakPtr<ImageLoader> {
     WTF_MAKE_FAST_ALLOCATED;
@@ -89,6 +89,7 @@
 protected:
     explicit ImageLoader(Element&);
     void notifyFinished(CachedResource&, const NetworkLoadMetrics&) override;
+    void didStartLoading() override;
 
 private:
     void resetLazyImageLoading(Document&);
@@ -98,6 +99,7 @@
     virtual String sourceURI(const AtomString&) const = 0;
 
     void updatedHasPendingEvent();
+    void didUpdateCachedImage(RelevantMutation, CachedResourceHandle<CachedImage>&&);
 
     void dispatchPendingBeforeLoadEvent();
     void dispatchPendingLoadEvent();
@@ -130,6 +132,7 @@
     bool m_imageComplete : 1;
     bool m_loadManually : 1;
     bool m_elementIsProtected : 1;
+    bool m_inUpdateFromElement : 1;
     LazyImageLoadState m_lazyImageLoadState { LazyImageLoadState::None };
 };
 

Modified: trunk/Source/WebCore/loader/cache/CachedImage.cpp (291140 => 291141)


--- trunk/Source/WebCore/loader/cache/CachedImage.cpp	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/Source/WebCore/loader/cache/CachedImage.cpp	2022-03-11 02:04:19 UTC (rev 291141)
@@ -99,9 +99,12 @@
 void CachedImage::load(CachedResourceLoader& loader)
 {
     m_skippingRevalidationDocument = loader.document();
-    if (loader.shouldPerformImageLoad(url()))
+    if (loader.shouldPerformImageLoad(url())) {
         CachedResource::load(loader);
-    else
+        CachedResourceClientWalker<CachedImageClient> walker(m_clients);
+        while (auto* client = walker.next())
+            client->didStartLoading();
+    } else
         setLoading(false);
 }
 

Modified: trunk/Source/WebCore/loader/cache/CachedImageClient.h (291140 => 291141)


--- trunk/Source/WebCore/loader/cache/CachedImageClient.h	2022-03-11 01:47:40 UTC (rev 291140)
+++ trunk/Source/WebCore/loader/cache/CachedImageClient.h	2022-03-11 02:04:19 UTC (rev 291141)
@@ -49,6 +49,7 @@
     virtual VisibleInViewportState imageFrameAvailable(CachedImage& image, ImageAnimatingState, const IntRect* changeRect) { imageChanged(&image, changeRect); return VisibleInViewportState::No; }
     virtual VisibleInViewportState imageVisibleInViewport(const Document&) const { return VisibleInViewportState::No; }
 
+    virtual void didStartLoading() { }
     virtual void didRemoveCachedImageClient(CachedImage&) { }
 
     virtual void scheduleRenderingUpdateForImage(CachedImage&) { }
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to