Skip to content

Commit

Permalink
Memory consumption/leak with img out of viewport and lazy loading
Browse files Browse the repository at this point in the history
https://bugs.webkit.org/show_bug.cgi?id=263521

Reviewed by Chris Dumez.

This change fixes the problem with dangling of dynamically created (in JS)
HTMLImageElement when it is detached from the document before loading the resource
starts. It happened when img element was created (dynamically) with lazy loading
and the element was outside the viewport (the loading of resource is deferred until
the img element becomes visible). If the element was removed from document it
becomes dangling element and will never be deleted by GC.

* Source/WebCore/loader/ImageLoader.cpp:
(WebCore::ImageLoader::hasPendingActivity const):

To avoid leaking of the dynamically created element, the pending activity of
the element should check has the load of the resource actually started.
Similar check is done in case of static HTMLImageElement in
ImageLoader::updatedHasPendingEvent.

* Source/WebCore/loader/ImageLoader.h:
(WebCore::ImageLoader::hasPendingActivity const): Deleted.

Moved implementation to cpp file.

 * LayoutTests/fast/dom/dynamic-image-with-lazy-loading-leak-expected.txt: Added.
 * LayoutTests/fast/dom/dynamic-image-with-lazy-loading-leak.html: Added.

Canonical link: https://commits.webkit.org/270745@main
  • Loading branch information
pgorszkowski-igalia authored and Ahmad Saleem committed Nov 15, 2023
1 parent c093fd4 commit 78e4577
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Tests that lazy image loading doesn't cause leaks when 'img' element is created and removed dynamically.

On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".


PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS internals.isElementAlive(imgElementId) is true
PASS The img element didn't leak.
PASS successfullyParsed is true

TEST COMPLETE

64 changes: 64 additions & 0 deletions LayoutTests/fast/dom/dynamic-image-with-lazy-loading-leak.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<body>
<script src="../../resources/js-test.js"></script>
<div style="height: 2000px"></div>
<div id="below-viewport-img-container" style="height: 1000px"></div>
<script>
description("Tests that lazy image loading doesn't cause leaks when 'img' element is created and removed dynamically.");
jsTestIsAsync = true;

checkCount = 0;

const imgCount = 10;
imgs = [];
const image_path = 'image.png';
imgElementIds = [];

function runTest()
{
handle = setInterval(() => {
gc();
for (let imgElementId of imgElementIds) {
if (!internals.isElementAlive(imgElementId)) {
clearInterval(handle);
testPassed("The img element didn't leak.");
finishJSTest();
return;
}
}
checkCount++;
if (checkCount > 1000) {
clearInterval(handle);
testFailed("The img elements leaked.");
finishJSTest();
}
}, 10);
}

onload = () => {
for (let i = 0; i < imgCount; i++) {
img = document.getElementById("lazy-loaded-img-"+i);
imgElementId = internals.elementIdentifier(img);
shouldBeTrue("internals.isElementAlive(imgElementId)");
imgElementIds.push(imgElementId);
document.getElementById('below-viewport-img-container').removeChild(img);
img = null;
}
imgs = [];
setTimeout(runTest, 0);
};

for (let i = 0; i < imgCount; i++) {
img = new Image();
img.id = "lazy-loaded-img-"+i;
img.loading = 'lazy';
img.src = image_path;
document.getElementById('below-viewport-img-container').append(img);
imgs.push(img);
img = null;
}

</script>
</body>
</html>
8 changes: 8 additions & 0 deletions Source/WebCore/loader/ImageLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,14 @@ void ImageLoader::timerFired()
m_protectedElement = nullptr;
}

bool ImageLoader::hasPendingActivity() const
{
// Because of lazy image loading, an image's load may be deferred indefinitely. To avoid leaking the element, we only
// protect it once the load has actually started.
bool imageWillBeLoadedLater = m_image && !m_image->isLoading() && m_image->stillNeedsLoad();
return (m_hasPendingLoadEvent && !imageWillBeLoadedLater) || m_hasPendingErrorEvent;
}

void ImageLoader::dispatchPendingEvent(ImageEventSender* eventSender, const AtomString& eventType)
{
ASSERT_UNUSED(eventSender, eventSender == &loadEventSender());
Expand Down
2 changes: 1 addition & 1 deletion Source/WebCore/loader/ImageLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ImageLoader : public CachedImageClient {

// FIXME: Delete this code. beforeload event no longer exists.
bool hasPendingBeforeLoadEvent() const { return m_hasPendingBeforeLoadEvent; }
bool hasPendingActivity() const { return m_hasPendingLoadEvent || m_hasPendingErrorEvent; }
bool hasPendingActivity() const;

void dispatchPendingEvent(ImageEventSender*, const AtomString& eventType);

Expand Down

0 comments on commit 78e4577

Please sign in to comment.