Title: [279800] trunk
Revision
279800
Author
rn...@webkit.org
Date
2021-07-09 18:03:03 -0700 (Fri, 09 Jul 2021)

Log Message

ResizeObserver / IntersectionObserver memory leak on detached & out of reference elements
https://bugs.webkit.org/show_bug.cgi?id=227194
<rdar://problem/79839851>

Reviewed by Chris Dumez.

Source/WebCore:

The memory leak was caused by ResizeObserver and IntersectionObserver keeping their respective
JS wrapper objects alive so long as there are some observed elements by having pending
activity as ActiveDOMObjects. This is not the right GC model for these JS wrapper objects
since there is no obvious end to these activities. So long as the observed nodes are alive,
ResizeObserver and IntersectionObserver may get new observation entries queued later.

To address this issue, this patch reworks the way ResizeObserver and IntersectionObserver keep
their JS wrappers alive. Namely, they're no longer ActiveDOMObjects. Instead, their JS wrappers
would use their respective observed nodes as opaque roots. i.e. so long as any of the observed
nodes are kept alive by GC, then its observer's JS wrapper is kept alive.

ResizeObserver had an additional bug that every observed node was kept using GCReachableRef,
which obviously leaked every observed node until the observations were explicitly cleared.
This patch makes only the target elements of the active observations (i.e. there are entries
to be delivered to the observer callback) are kept alive with GCReachableRef as done with
IntersectionObserver and MutationObserver.

Finally, this patch fixes the bug that IntersectionObserver wasn't keeping its root node alive.
We even had a test which was testing this erroneously behavior. The test has been rewritten to
expect the new behavior.

Tests: intersection-observer/intersection-observer-should-not-leak-observed-nodes.html
       intersection-observer/root-element-deleted.html
       resize-observer/resize-observer-should-not-leak-observed-nodes.html

* bindings/js/JSIntersectionObserverCustom.cpp:
(WebCore::JSIntersectionObserver::visitAdditionalChildren): Add the root node as an opaque root.
This has an effect of keeping the root node alive so long as IntersectionObserver is alive.
(WebCore::JSIntersectionObserverOwner::isReachableFromOpaqueRoots): Added.
* bindings/js/JSResizeObserverCustom.cpp:
(WebCore::JSResizeObserverOwner::isReachableFromOpaqueRoots): Added.
* dom/Document.cpp:
(WebCore::Document::updateIntersectionObservations):
* html/LazyLoadFrameObserver.cpp:
(WebCore::LazyLoadFrameObserver::isObserved const):
* html/LazyLoadImageObserver.cpp:
(WebCore::LazyLoadImageObserver::isObserved const):
* page/IntersectionObserver.cpp:
(WebCore::IntersectionObserver::create):
(WebCore::IntersectionObserver::IntersectionObserver):
(WebCore::IntersectionObserver::~IntersectionObserver):
(WebCore::IntersectionObserver::isObserving const): Added.
(WebCore::IntersectionObserver::observe):
(WebCore::IntersectionObserver::virtualHasPendingActivity const): Deleted.
(WebCore::IntersectionObserver::activeDOMObjectName const): Deleted.
(WebCore::IntersectionObserver::stop): Deleted.
(WebCore::IntersectionObserver::isReachableFromOpaqueRoots const): Added.
* page/IntersectionObserver.h:
(WebCore::IntersectionObserver::root const):
(WebCore::IntersectionObserver::observationTargets const):
(WebCore::IntersectionObserver::hasObservationTargets const):
* page/IntersectionObserver.idl:
* page/ResizeObservation.cpp:
(WebCore::ResizeObservation::create):
(WebCore::ResizeObservation::ResizeObservation):
(WebCore::ResizeObservation::targetElementDepth const):
* page/ResizeObservation.h:
(WebCore::ResizeObservation::target const):
(WebCore::ResizeObservation): Renamed m_pendingTargets to m_activeObservationTargets to reflect
the new semantics.
* page/ResizeObserver.cpp:
(WebCore::ResizeObserver::ResizeObserver):
(WebCore::ResizeObserver::observe): Don't store the new observation target with GCReachableRef as
that would result in an immediate leak of this element.
(WebCore::ResizeObserver::gatherObservations): Now that we have an active observation (i.e. there
is an entry to be delivered to the JS callback), store the target element using GCReachableRef.
This keeps the JS wrapper of this target element alive between now and when the JS callback is
notified of this element's resize. Without this GCReachableRef, JS wrapper of the element can be
erroneously collected when there is no JS reference to the element and the element is no longer in
any live document. Note that this GC behavior is already tested by existing tests. This patch
simply delays the use of GCReachableRef from when ResizeObserver started observing this element
to when we created an active observation for the element; this is because GCReachableRef like
a pending activity of ActiveDOMObject is only appropriate when there is a definite end to it.
(WebCore::ResizeObserver::isReachableFromOpaqueRoots const): Added.
(WebCore::ResizeObserver::removeAllTargets):
(WebCore::ResizeObserver::removeObservation):
(WebCore::ResizeObserver::virtualHasPendingActivity const): Deleted.
(WebCore::ResizeObserver::activeDOMObjectName const): Deleted.
(WebCore::ResizeObserver::stop): Deleted.
* page/ResizeObserver.h:
* page/ResizeObserver.idl:

LayoutTests:

Added regression tests for leaking nodes with IntersectionObserver and ResizeObsever.

Also rewrote intersection-observer/root-element-deleted.html since the test was previously asserting
that IntersectionObserver.root becomes null if it was no longer in the document, which is wrong.

* intersection-observer/intersection-observer-should-not-leak-observed-nodes-expected.txt: Added.
* intersection-observer/intersection-observer-should-not-leak-observed-nodes.html: Added.
* intersection-observer/root-element-deleted-expected.txt:
* intersection-observer/root-element-deleted.html:
* resize-observer/resize-observer-should-not-leak-observed-nodes-expected.txt: Added.
* resize-observer/resize-observer-should-not-leak-observed-nodes.html: Added.

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (279799 => 279800)


--- trunk/LayoutTests/ChangeLog	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/LayoutTests/ChangeLog	2021-07-10 01:03:03 UTC (rev 279800)
@@ -1,3 +1,23 @@
+2021-07-09  Ryosuke Niwa  <rn...@webkit.org>
+
+        ResizeObserver / IntersectionObserver memory leak on detached & out of reference elements
+        https://bugs.webkit.org/show_bug.cgi?id=227194
+        <rdar://problem/79839851>
+
+        Reviewed by Chris Dumez.
+
+        Added regression tests for leaking nodes with IntersectionObserver and ResizeObsever.
+
+        Also rewrote intersection-observer/root-element-deleted.html since the test was previously asserting
+        that IntersectionObserver.root becomes null if it was no longer in the document, which is wrong.
+
+        * intersection-observer/intersection-observer-should-not-leak-observed-nodes-expected.txt: Added.
+        * intersection-observer/intersection-observer-should-not-leak-observed-nodes.html: Added.
+        * intersection-observer/root-element-deleted-expected.txt:
+        * intersection-observer/root-element-deleted.html:
+        * resize-observer/resize-observer-should-not-leak-observed-nodes-expected.txt: Added.
+        * resize-observer/resize-observer-should-not-leak-observed-nodes.html: Added.
+
 2021-07-09  Ryan Haddad  <ryanhad...@apple.com>
 
         REGRESSION: [ macOS wk2 ] inspector/canvas/create-context-bitmaprenderer.html is flaky failing

Added: trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes-expected.txt (0 => 279800)


--- trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes-expected.txt	2021-07-10 01:03:03 UTC (rev 279800)
@@ -0,0 +1,4 @@
+This tests observing an element with an IntersectionObserver and removing the element from the document.
+The element should become eligible for GC at some point.
+
+PASS

Added: trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes.html (0 => 279800)


--- trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes.html	                        (rev 0)
+++ trunk/LayoutTests/intersection-observer/intersection-observer-should-not-leak-observed-nodes.html	2021-07-10 01:03:03 UTC (rev 279800)
@@ -0,0 +1,51 @@
+<!DOCTYPE html><!-- webkit-test-runner [ IntersectionObserverEnabled=true ] -->
+<html>
+<body>
+<pre id="log">This tests observing an element with an IntersectionObserver and removing the element from the document.
+The element should become eligible for GC at some point.
+
+</pre>
+<script src=""
+<script>
+
+let initialNodeCount;
+function runTest()
+{
+    if (!window.testRunner || !window.internals) {
+        log.textContent += 'FAIL - This test requires internals'; 
+        return;
+    }
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    initialNodeCount = internals.referencingNodeCount(document);
+
+    const intersectionObservers = [];
+    for (let i = 0; i < 100; ++i)
+        intersectionObservers.push(createIntersectionObserver());
+
+    requestAnimationFrame(() => {
+        setTimeout(() => {
+            gc();
+            log.textContent += internals.referencingNodeCount(document) < initialNodeCount + intersectionObservers.length * 0.8 ? 'PASS' : 'FAIL - Less than 20% of nodes were collected.'
+            testRunner.notifyDone();
+        }, 0);
+    });
+}
+
+function createIntersectionObserver()
+{
+    const element = document.createElement('div');
+    const intersectionObserver = new IntersectionObserver(() => { }).observe(element);
+    document.body.appendChild(element);
+    setTimeout(() => {
+        element.remove();
+    }, 0);
+    return intersectionObserver;
+}
+
+_onload_ = runTest;
+
+</script>
+</body>
+</html>

Modified: trunk/LayoutTests/intersection-observer/root-element-deleted-expected.txt (279799 => 279800)


--- trunk/LayoutTests/intersection-observer/root-element-deleted-expected.txt	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/LayoutTests/intersection-observer/root-element-deleted-expected.txt	2021-07-10 01:03:03 UTC (rev 279800)
@@ -1,3 +1,25 @@
+This tests removing the root node of IntersectionObserver. The root node should be eligible for GC.
 
-PASS IntersectionObserver doesn't keep unreachable root alive
+Test 1: PASS
+Test 2: PASS
+Test 3: PASS
+Test 4: PASS
+Test 5: PASS
+Test 6: PASS
+Test 7: PASS
+Test 8: PASS
+Test 9: PASS
+Test 10: PASS
+Test 11: PASS
+Test 12: PASS
+Test 13: PASS
+Test 14: PASS
+Test 15: PASS
+Test 16: PASS
+Test 17: PASS
+Test 18: PASS
+Test 19: PASS
+Test 20: PASS
+Node count: PASS
+Intersection observer count: PASS
 

Modified: trunk/LayoutTests/intersection-observer/root-element-deleted.html (279799 => 279800)


--- trunk/LayoutTests/intersection-observer/root-element-deleted.html	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/LayoutTests/intersection-observer/root-element-deleted.html	2021-07-10 01:03:03 UTC (rev 279800)
@@ -1,44 +1,95 @@
 <!DOCTYPE html><!-- webkit-test-runner [ IntersectionObserverEnabled=true ] -->
 <head>
-<script src=""
-<script src=""
+<script src=""
 <body>
+<pre id="log">This tests removing the root node of IntersectionObserver. The root node should be eligible for GC.
 
-<div id="root" style="position:absolute">
-    <div id="target" style="width: 100px; height: 100px; background-color: green"></div>
-</div>
+</pre>
+<script>
 
-<script>
-    function observerShouldBeRemoved()
-    {
-        return new Promise(function(resolve) {
-            handle = setInterval(function() {
-                GCController.collect();
-                if (internals && internals.numberOfIntersectionObservers(document) == 0) {
-                    clearInterval(handle);
-                    resolve();
-                }
-            }, 10);
-        });
+function createRoot(rootId)
+{
+    const container = document.createElement('div');
+    container.innerHTML = `<div id="${rootId}" class="root" style="position:absolute"><div class="target" style="width: 100px; height: 100px; background-color: green"></div></div>`;
+    const root = container.getElementsByClassName('root')[0];
+    root.remove();
+    return root;
+}
+
+let initialNodeCount = 0;
+window._onload_ = () => {
+    if (!window.testRunner || !window.internals) {
+        log.textContent += 'FAIL - This test requires internals';
+        return;
     }
+    if (internals.numberOfIntersectionObservers(document)) {
+        log.textContent += 'FAIL - Initial intersection observer count is not zero';
+        return;
+    }
+    initialNodeCount = internals.referencingNodeCount(document);
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+    setTimeout(() => testRunner.notifyDone(), 5000);
+    runTest(1);
+}
 
-    async_test(function(t) {
-        var root = document.getElementById('root');
-        var observer = new IntersectionObserver(function() {}, { root: document.getElementById('root') });;
-        var target = document.getElementById('target');
-        assert_equals(observer.root, root, 'Observer starts out with non-null root');
-        observer.observe(target);
-        assert_equals(internals.numberOfIntersectionObservers(document), 1);
-        root.parentNode.removeChild(root);
-        root = null;
-        target = null;
-        requestAnimationFrame(function() {
-            observer.takeRecords();
-            observerShouldBeRemoved().then(t.step_func_done(function() {
-                assert_equals(observer.root, null, 'Observer has null root after root element is destroyed');
-            }));
-        });
-    }, "IntersectionObserver doesn't keep unreachable root alive");
+const totalTestCount = 20;
+function runTest(testNumber) {
+    log.textContent += `Test ${testNumber}: `;
+    const rootId = 'root-' + testNumber;
+    let root = createRoot(rootId);
+    root.alive = true;
+    document.body.appendChild(root);
+
+    let observer = new IntersectionObserver(() => { }, { root });
+    let target = root.getElementsByClassName('target')[0];
+    if (observer.root != root)
+        return testFailed(testNumber, 'observer.root != root after construction');
+    observer.observe(target);
+
+    root.remove();
+    root = null;
+    target = null;
+    requestAnimationFrame(function () {
+        observer.takeRecords();
+        gc();
+
+        if (!observer.root)
+            return testFailed(testNumber, 'observer.root is null');
+        if (observer.root.id != rootId)
+            return testFailed(testNumber, `observer.root.id (${observer.root.id}) != rootId (${rootId}) after running rAF and GC`);
+        if (!observer.root.alive)
+            return testFailed(testNumber, 'observer.alive is false');
+
+        observer = null;
+        gc();
+
+        log.textContent += 'PASS\n';
+
+        if (testNumber < totalTestCount)
+            setTimeout(() => runTest(testNumber + 1), 0);
+        else
+            setTimeout(() => finish(testNumber), 0);
+    });
+}
+
+function testFailed(testNumber, error)
+{
+    log.textContent += `FAIL: ${error}\n`;
+    finish(testNumber);
+}
+
+function finish(finishedTestCount)
+{
+    log.textContent += 'Node count: ';
+    log.textContent += (internals.referencingNodeCount(document) < initialNodeCount + 2 * finishedTestCount * 0.8) ? 'PASS' : 'FAIL - Less than 20% of the root nodes were collected';
+    log.textContent += '\n';
+    log.textContent += 'Intersection observer count: ';
+    log.textContent += (internals.numberOfIntersectionObservers(document) < finishedTestCount * 0.8) ? 'PASS' : 'FAIL - Less than 20% of the intersection observers were collected';
+    log.textContent += '\n';
+    testRunner.notifyDone();
+}
+
 </script>
 </body>
 </html>

Added: trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes-expected.txt (0 => 279800)


--- trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes-expected.txt	                        (rev 0)
+++ trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes-expected.txt	2021-07-10 01:03:03 UTC (rev 279800)
@@ -0,0 +1,4 @@
+This tests observing an element with an ResizeObserver and removing the element from the document.
+The element should become eligible for GC at some point.
+
+PASS

Added: trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes.html (0 => 279800)


--- trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes.html	                        (rev 0)
+++ trunk/LayoutTests/resize-observer/resize-observer-should-not-leak-observed-nodes.html	2021-07-10 01:03:03 UTC (rev 279800)
@@ -0,0 +1,47 @@
+<!DOCTYPE html><!-- webkit-test-runner [ ResizeObserverEnabled=true ] -->
+<html>
+<body>
+<pre id="log">This tests observing an element with an ResizeObserver and removing the element from the document.
+The element should become eligible for GC at some point.
+
+</pre>
+<script src=""
+<script>
+
+let initialNodeCount;
+function runTest()
+{
+    if (!window.testRunner || !window.internals) {
+        log.textContent += 'FAIL - This test requires internals'; 
+        return;
+    }
+    testRunner.dumpAsText();
+    testRunner.waitUntilDone();
+
+    initialNodeCount = internals.referencingNodeCount(document);
+
+    const resizeObserverCount = 100;
+    for (let i = 0; i < resizeObserverCount; ++i)
+        createResizeObserver();
+
+    requestAnimationFrame(() => {
+        gc();
+        setTimeout(() => {
+            gc();
+            log.textContent += internals.referencingNodeCount(document) < initialNodeCount + resizeObserverCount * 0.95 ? 'PASS' : 'FAIL - Less than 20% of nodes were collected.'
+            testRunner.notifyDone();
+        }, 0);
+    });
+}
+
+function createResizeObserver()
+{
+    const element = document.createElement('div');
+    const resizeObserver = new ResizeObserver(() => { }).observe(element);
+}
+
+_onload_ = runTest;
+
+</script>
+</body>
+</html>

Modified: trunk/Source/WebCore/ChangeLog (279799 => 279800)


--- trunk/Source/WebCore/ChangeLog	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/ChangeLog	2021-07-10 01:03:03 UTC (rev 279800)
@@ -1,3 +1,93 @@
+2021-07-09  Ryosuke Niwa  <rn...@webkit.org>
+
+        ResizeObserver / IntersectionObserver memory leak on detached & out of reference elements
+        https://bugs.webkit.org/show_bug.cgi?id=227194
+        <rdar://problem/79839851>
+
+        Reviewed by Chris Dumez.
+
+        The memory leak was caused by ResizeObserver and IntersectionObserver keeping their respective
+        JS wrapper objects alive so long as there are some observed elements by having pending
+        activity as ActiveDOMObjects. This is not the right GC model for these JS wrapper objects
+        since there is no obvious end to these activities. So long as the observed nodes are alive,
+        ResizeObserver and IntersectionObserver may get new observation entries queued later.
+
+        To address this issue, this patch reworks the way ResizeObserver and IntersectionObserver keep
+        their JS wrappers alive. Namely, they're no longer ActiveDOMObjects. Instead, their JS wrappers
+        would use their respective observed nodes as opaque roots. i.e. so long as any of the observed
+        nodes are kept alive by GC, then its observer's JS wrapper is kept alive.
+
+        ResizeObserver had an additional bug that every observed node was kept using GCReachableRef,
+        which obviously leaked every observed node until the observations were explicitly cleared.
+        This patch makes only the target elements of the active observations (i.e. there are entries
+        to be delivered to the observer callback) are kept alive with GCReachableRef as done with
+        IntersectionObserver and MutationObserver.
+
+        Finally, this patch fixes the bug that IntersectionObserver wasn't keeping its root node alive.
+        We even had a test which was testing this erroneously behavior. The test has been rewritten to
+        expect the new behavior.
+
+        Tests: intersection-observer/intersection-observer-should-not-leak-observed-nodes.html
+               intersection-observer/root-element-deleted.html
+               resize-observer/resize-observer-should-not-leak-observed-nodes.html
+
+        * bindings/js/JSIntersectionObserverCustom.cpp:
+        (WebCore::JSIntersectionObserver::visitAdditionalChildren): Add the root node as an opaque root.
+        This has an effect of keeping the root node alive so long as IntersectionObserver is alive.
+        (WebCore::JSIntersectionObserverOwner::isReachableFromOpaqueRoots): Added.
+        * bindings/js/JSResizeObserverCustom.cpp:
+        (WebCore::JSResizeObserverOwner::isReachableFromOpaqueRoots): Added.
+        * dom/Document.cpp:
+        (WebCore::Document::updateIntersectionObservations):
+        * html/LazyLoadFrameObserver.cpp:
+        (WebCore::LazyLoadFrameObserver::isObserved const):
+        * html/LazyLoadImageObserver.cpp:
+        (WebCore::LazyLoadImageObserver::isObserved const):
+        * page/IntersectionObserver.cpp:
+        (WebCore::IntersectionObserver::create):
+        (WebCore::IntersectionObserver::IntersectionObserver):
+        (WebCore::IntersectionObserver::~IntersectionObserver):
+        (WebCore::IntersectionObserver::isObserving const): Added.
+        (WebCore::IntersectionObserver::observe):
+        (WebCore::IntersectionObserver::virtualHasPendingActivity const): Deleted.
+        (WebCore::IntersectionObserver::activeDOMObjectName const): Deleted.
+        (WebCore::IntersectionObserver::stop): Deleted.
+        (WebCore::IntersectionObserver::isReachableFromOpaqueRoots const): Added.
+        * page/IntersectionObserver.h:
+        (WebCore::IntersectionObserver::root const):
+        (WebCore::IntersectionObserver::observationTargets const):
+        (WebCore::IntersectionObserver::hasObservationTargets const):
+        * page/IntersectionObserver.idl:
+        * page/ResizeObservation.cpp:
+        (WebCore::ResizeObservation::create):
+        (WebCore::ResizeObservation::ResizeObservation):
+        (WebCore::ResizeObservation::targetElementDepth const):
+        * page/ResizeObservation.h:
+        (WebCore::ResizeObservation::target const):
+        (WebCore::ResizeObservation): Renamed m_pendingTargets to m_activeObservationTargets to reflect
+        the new semantics.
+        * page/ResizeObserver.cpp:
+        (WebCore::ResizeObserver::ResizeObserver):
+        (WebCore::ResizeObserver::observe): Don't store the new observation target with GCReachableRef as
+        that would result in an immediate leak of this element.
+        (WebCore::ResizeObserver::gatherObservations): Now that we have an active observation (i.e. there
+        is an entry to be delivered to the JS callback), store the target element using GCReachableRef.
+        This keeps the JS wrapper of this target element alive between now and when the JS callback is
+        notified of this element's resize. Without this GCReachableRef, JS wrapper of the element can be
+        erroneously collected when there is no JS reference to the element and the element is no longer in
+        any live document. Note that this GC behavior is already tested by existing tests. This patch
+        simply delays the use of GCReachableRef from when ResizeObserver started observing this element
+        to when we created an active observation for the element; this is because GCReachableRef like
+        a pending activity of ActiveDOMObject is only appropriate when there is a definite end to it.
+        (WebCore::ResizeObserver::isReachableFromOpaqueRoots const): Added.
+        (WebCore::ResizeObserver::removeAllTargets):
+        (WebCore::ResizeObserver::removeObservation):
+        (WebCore::ResizeObserver::virtualHasPendingActivity const): Deleted.
+        (WebCore::ResizeObserver::activeDOMObjectName const): Deleted.
+        (WebCore::ResizeObserver::stop): Deleted.
+        * page/ResizeObserver.h:
+        * page/ResizeObserver.idl:
+
 2021-07-09  Aditya Keerthi  <akeer...@apple.com>
 
         [iOS] Increase contrast for textfields, checkboxes, and radio buttons

Modified: trunk/Source/WebCore/bindings/js/JSIntersectionObserverCustom.cpp (279799 => 279800)


--- trunk/Source/WebCore/bindings/js/JSIntersectionObserverCustom.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/bindings/js/JSIntersectionObserverCustom.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -27,6 +27,7 @@
 #include "config.h"
 #include "JSIntersectionObserver.h"
 
+#include "JSNodeCustom.h"
 #include <_javascript_Core/JSCInlines.h>
 
 namespace WebCore {
@@ -36,8 +37,19 @@
 {
     if (auto* callback = wrapped().callbackConcurrently())
         callback->visitJSFunction(visitor);
+    visitor.addOpaqueRoot(root(wrapped().root()));
 }
 
 DEFINE_VISIT_ADDITIONAL_CHILDREN(JSIntersectionObserver);
 
+bool JSIntersectionObserverOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void*, JSC::AbstractSlotVisitor& visitor, const char**reason)
+{
+    if (jsCast<JSIntersectionObserver*>(handle.slot()->asCell())->wrapped().isReachableFromOpaqueRoots(visitor)) {
+        if (UNLIKELY(reason))
+            *reason = "Reachable from observed nodes";
+        return true;
+    }
+    return false;
+}
+
 } // namespace WebCore

Modified: trunk/Source/WebCore/bindings/js/JSResizeObserverCustom.cpp (279799 => 279800)


--- trunk/Source/WebCore/bindings/js/JSResizeObserverCustom.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/bindings/js/JSResizeObserverCustom.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -41,4 +41,14 @@
 
 DEFINE_VISIT_ADDITIONAL_CHILDREN(JSResizeObserver);
 
+bool JSResizeObserverOwner::isReachableFromOpaqueRoots(JSC::Handle<JSC::Unknown> handle, void*, JSC::AbstractSlotVisitor& visitor, const char**reason)
+{
+    if (jsCast<JSResizeObserver*>(handle.slot()->asCell())->wrapped().isReachableFromOpaqueRoots(visitor)) {
+        if (UNLIKELY(reason))
+            *reason = "Reachable from observed nodes";
+        return true;
+    }
+    return false;
+}
+
 } // namespace WebCore

Modified: trunk/Source/WebCore/dom/Document.cpp (279799 => 279800)


--- trunk/Source/WebCore/dom/Document.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/dom/Document.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -7912,7 +7912,7 @@
         auto timestamp = observer->nowTimestamp();
         if (!timestamp)
             continue;
-        for (Element* target : observer->observationTargets()) {
+        for (auto& target : observer->observationTargets()) {
             auto& targetRegistrations = target->intersectionObserverDataIfExists()->registrations;
             auto index = targetRegistrations.findMatching([observer](auto& registration) {
                 return registration.observer.get() == observer;
@@ -7969,7 +7969,7 @@
                     { targetBoundingClientRect.x(), targetBoundingClientRect.y(), targetBoundingClientRect.width(), targetBoundingClientRect.height() },
                     { clientIntersectionRect.x(), clientIntersectionRect.y(), clientIntersectionRect.width(), clientIntersectionRect.height() },
                     intersectionRatio,
-                    target,
+                    target.get(),
                     thresholdIndex > 0,
                 }));
                 needNotify = true;

Modified: trunk/Source/WebCore/html/LazyLoadFrameObserver.cpp (279799 => 279800)


--- trunk/Source/WebCore/html/LazyLoadFrameObserver.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/html/LazyLoadFrameObserver.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -103,7 +103,7 @@
 
 bool LazyLoadFrameObserver::isObserved(Element& element) const
 {
-    return m_observer && m_observer->observationTargets().contains(&element);
+    return m_observer && m_observer->isObserving(element);
 }
 
 }

Modified: trunk/Source/WebCore/html/LazyLoadImageObserver.cpp (279799 => 279800)


--- trunk/Source/WebCore/html/LazyLoadImageObserver.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/html/LazyLoadImageObserver.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -95,7 +95,7 @@
 
 bool LazyLoadImageObserver::isObserved(Element& element) const
 {
-    return m_observer && m_observer->observationTargets().contains(&element);
+    return m_observer && m_observer->isObserving(element);
 }
 
 }

Modified: trunk/Source/WebCore/page/IntersectionObserver.cpp (279799 => 279800)


--- trunk/Source/WebCore/page/IntersectionObserver.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/IntersectionObserver.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -37,6 +37,7 @@
 #include "IntersectionObserverCallback.h"
 #include "IntersectionObserverEntry.h"
 #include "Performance.h"
+#include <_javascript_Core/AbstractSlotVisitorInlines.h>
 #include <wtf/Vector.h>
 
 namespace WebCore {
@@ -86,7 +87,7 @@
 
 ExceptionOr<Ref<IntersectionObserver>> IntersectionObserver::create(Document& document, Ref<IntersectionObserverCallback>&& callback, IntersectionObserver::Init&& init)
 {
-    ContainerNode* root = nullptr;
+    RefPtr<ContainerNode> root;
     if (init.root) {
         WTF::switchOn(*init.root, [&root] (RefPtr<Element> element) {
             root = element.get();
@@ -112,38 +113,34 @@
             return Exception { RangeError, "Failed to construct 'IntersectionObserver': all thresholds must lie in the range [0.0, 1.0]." };
     }
 
-    return adoptRef(*new IntersectionObserver(document, WTFMove(callback), root, rootMarginOrException.releaseReturnValue(), WTFMove(thresholds)));
+    return adoptRef(*new IntersectionObserver(document, WTFMove(callback), root.get(), rootMarginOrException.releaseReturnValue(), WTFMove(thresholds)));
 }
 
 IntersectionObserver::IntersectionObserver(Document& document, Ref<IntersectionObserverCallback>&& callback, ContainerNode* root, LengthBox&& parsedRootMargin, Vector<double>&& thresholds)
-    : ActiveDOMObject(callback->scriptExecutionContext())
-    , m_root(root)
+    : m_root(makeWeakPtr(root))
     , m_rootMargin(WTFMove(parsedRootMargin))
     , m_thresholds(WTFMove(thresholds))
     , m_callback(WTFMove(callback))
 {
-    if (is<Document>(m_root)) {
-        auto& observerData = downcast<Document>(*m_root).ensureIntersectionObserverData();
+    if (is<Document>(root)) {
+        auto& observerData = downcast<Document>(*root).ensureIntersectionObserverData();
         observerData.observers.append(makeWeakPtr(this));
-    } else if (m_root) {
-        ASSERT(is<Element>(m_root));
-        auto& observerData = downcast<Element>(*m_root).ensureIntersectionObserverData();
+    } else if (root) {
+        auto& observerData = downcast<Element>(*root).ensureIntersectionObserverData();
         observerData.observers.append(makeWeakPtr(this));
     } else if (auto* frame = document.frame())
         m_implicitRootDocument = makeWeakPtr(frame->mainFrame().document());
 
     std::sort(m_thresholds.begin(), m_thresholds.end());
-    suspendIfNeeded();
 }
 
 IntersectionObserver::~IntersectionObserver()
 {
-    if (is<Document>(m_root)) {
-        downcast<Document>(*m_root).intersectionObserverDataIfExists()->observers.removeFirst(this);
-    } else if (m_root) {
-        ASSERT(is<Element>(m_root));
-        downcast<Element>(*m_root).intersectionObserverDataIfExists()->observers.removeFirst(this);
-    }
+    RefPtr root = m_root.get();
+    if (is<Document>(root))
+        downcast<Document>(*root).intersectionObserverDataIfExists()->observers.removeFirst(this);
+    else if (root)
+        downcast<Element>(*root).intersectionObserverDataIfExists()->observers.removeFirst(this);
     disconnect();
 }
 
@@ -157,14 +154,22 @@
     return stringBuilder.toString();
 }
 
+bool IntersectionObserver::isObserving(const Element& element) const
+{
+    return m_observationTargets.findMatching([&](auto& target) {
+        return target.get() == &element;
+    }) != notFound;
+}
+
 void IntersectionObserver::observe(Element& target)
 {
-    if (!trackingDocument() || !m_callback || m_observationTargets.contains(&target))
+    if (!trackingDocument() || !m_callback || isObserving(target))
         return;
 
     target.ensureIntersectionObserverData().registrations.append({ makeWeakPtr(this), std::nullopt });
     bool hadObservationTargets = hasObservationTargets();
-    m_observationTargets.append(&target);
+    m_observationTargets.append(makeWeakPtr(target));
+
     auto* document = trackingDocument();
     if (!hadObservationTargets)
         document->addIntersectionObserver(*this);
@@ -223,7 +228,7 @@
 
 void IntersectionObserver::removeAllTargets()
 {
-    for (auto* target : m_observationTargets) {
+    for (auto& target : m_observationTargets) {
         bool removed = removeTargetRegistration(*target);
         ASSERT_UNUSED(removed, removed);
     }
@@ -279,24 +284,15 @@
     InspectorInstrumentation::didFireObserverCallback(*context);
 }
 
-bool IntersectionObserver::virtualHasPendingActivity() const
+bool IntersectionObserver::isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor& visitor) const
 {
-    return (hasObservationTargets() && trackingDocument()) || !m_queuedEntries.isEmpty();
+    for (auto& target : m_observationTargets) {
+        if (auto* element = target.get(); element && visitor.containsOpaqueRoot(element->opaqueRoot()))
+            return true;
+    }
+    return false;
 }
 
-const char* IntersectionObserver::activeDOMObjectName() const
-{
-    return "IntersectionObserver";
-}
-
-void IntersectionObserver::stop()
-{
-    disconnect();
-    m_callback = nullptr;
-    m_queuedEntries.clear();
-    m_pendingTargets.clear();
-}
-
 } // namespace WebCore
 
 #endif // ENABLE(INTERSECTION_OBSERVER)

Modified: trunk/Source/WebCore/page/IntersectionObserver.h (279799 => 279800)


--- trunk/Source/WebCore/page/IntersectionObserver.h	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/IntersectionObserver.h	2021-07-10 01:03:03 UTC (rev 279800)
@@ -27,7 +27,6 @@
 
 #if ENABLE(INTERSECTION_OBSERVER)
 
-#include "ActiveDOMObject.h"
 #include "GCReachableRef.h"
 #include "IntersectionObserverCallback.h"
 #include "IntersectionObserverEntry.h"
@@ -37,6 +36,12 @@
 #include <wtf/WeakPtr.h>
 #include <wtf/text/WTFString.h>
 
+namespace JSC {
+
+class AbstractSlotVisitor;
+
+}
+
 namespace WebCore {
 
 class Document;
@@ -60,7 +65,7 @@
     Vector<IntersectionObserverRegistration> registrations;
 };
 
-class IntersectionObserver : public RefCounted<IntersectionObserver>, public ActiveDOMObject, public CanMakeWeakPtr<IntersectionObserver> {
+class IntersectionObserver : public RefCounted<IntersectionObserver>, public CanMakeWeakPtr<IntersectionObserver> {
 public:
     struct Init {
         std::optional<Variant<RefPtr<Element>, RefPtr<Document>>> root;
@@ -74,11 +79,13 @@
 
     Document* trackingDocument() const { return m_root ? &m_root->document() : m_implicitRootDocument.get(); }
 
-    ContainerNode* root() const { return m_root; }
+    ContainerNode* root() const { return m_root.get(); }
     String rootMargin() const;
     const LengthBox& rootMarginBox() const { return m_rootMargin; }
     const Vector<double>& thresholds() const { return m_thresholds; }
-    const Vector<Element*> observationTargets() const { return m_observationTargets; }
+    const Vector<WeakPtr<Element>>& observationTargets() const { return m_observationTargets; }
+    bool hasObservationTargets() const { return m_observationTargets.size(); }
+    bool isObserving(const Element&) const;
 
     void observe(Element&);
     void unobserve(Element&);
@@ -91,7 +98,6 @@
     TakenRecords takeRecords();
 
     void targetDestroyed(Element&);
-    bool hasObservationTargets() const { return m_observationTargets.size(); }
     void rootDestroyed();
 
     std::optional<ReducedResolutionSeconds> nowTimestamp() const;
@@ -100,24 +106,20 @@
     void notify();
 
     IntersectionObserverCallback* callbackConcurrently() { return m_callback.get(); }
+    bool isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor&) const;
 
 private:
     IntersectionObserver(Document&, Ref<IntersectionObserverCallback>&&, ContainerNode* root, LengthBox&& parsedRootMargin, Vector<double>&& thresholds);
 
-    // ActiveDOMObject.
-    bool virtualHasPendingActivity() const override;
-    const char* activeDOMObjectName() const override;
-    void stop() override;
-
     bool removeTargetRegistration(Element&);
     void removeAllTargets();
 
     WeakPtr<Document> m_implicitRootDocument;
-    ContainerNode* m_root;
+    WeakPtr<ContainerNode> m_root;
     LengthBox m_rootMargin;
     Vector<double> m_thresholds;
     RefPtr<IntersectionObserverCallback> m_callback;
-    Vector<Element*> m_observationTargets;
+    Vector<WeakPtr<Element>> m_observationTargets;
     Vector<GCReachableRef<Element>> m_pendingTargets;
     Vector<Ref<IntersectionObserverEntry>> m_queuedEntries;
 };

Modified: trunk/Source/WebCore/page/IntersectionObserver.idl (279799 => 279800)


--- trunk/Source/WebCore/page/IntersectionObserver.idl	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/IntersectionObserver.idl	2021-07-10 01:03:03 UTC (rev 279800)
@@ -26,11 +26,12 @@
 // https://wicg.github.io/IntersectionObserver/
 
 [
-    ActiveDOMObject,
     Conditional=INTERSECTION_OBSERVER,
     EnabledBySetting=IntersectionObserver,
     Exposed=Window,
     JSCustomMarkFunction,
+    CustomIsReachable,
+    ImplementationLacksVTable
 ] interface IntersectionObserver {
     [CallWith=Document] constructor(IntersectionObserverCallback callback, optional IntersectionObserverInit options);
 

Modified: trunk/Source/WebCore/page/ResizeObservation.cpp (279799 => 279800)


--- trunk/Source/WebCore/page/ResizeObservation.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/ResizeObservation.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -34,13 +34,13 @@
 
 namespace WebCore {
 
-Ref<ResizeObservation> ResizeObservation::create(Element* target)
+Ref<ResizeObservation> ResizeObservation::create(Element& target)
 {
     return adoptRef(*new ResizeObservation(target));
 }
 
-ResizeObservation::ResizeObservation(Element* element)
-    : m_target(element)
+ResizeObservation::ResizeObservation(Element& element)
+    : m_target { makeWeakPtr(element) }
 {
 }
 
@@ -92,7 +92,7 @@
 size_t ResizeObservation::targetElementDepth() const
 {
     unsigned depth = 0;
-    for (Element* ownerElement  = m_target; ownerElement; ownerElement = ownerElement->document().ownerElement()) {
+    for (Element* ownerElement = m_target.get(); ownerElement; ownerElement = ownerElement->document().ownerElement()) {
         for (Element* parent = ownerElement; parent; parent = parent->parentElement())
             ++depth;
     }

Modified: trunk/Source/WebCore/page/ResizeObservation.h (279799 => 279800)


--- trunk/Source/WebCore/page/ResizeObservation.h	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/ResizeObservation.h	2021-07-10 01:03:03 UTC (rev 279800)
@@ -30,6 +30,7 @@
 #include "LayoutSize.h"
 
 #include <wtf/RefCounted.h>
+#include <wtf/WeakPtr.h>
 
 namespace WebCore {
 
@@ -38,7 +39,7 @@
 class ResizeObservation : public RefCounted<ResizeObservation> {
     WTF_MAKE_FAST_ALLOCATED;
 public:
-    static Ref<ResizeObservation> create(Element* target);
+    static Ref<ResizeObservation> create(Element& target);
 
     ~ResizeObservation();
 
@@ -48,13 +49,13 @@
     FloatRect computeContentRect() const;
 
     bool elementSizeChanged(LayoutSize&) const;
-    Element* target() const { return m_target; }
+    Element* target() const { return m_target.get(); }
     size_t targetElementDepth() const;
 
 private:
-    ResizeObservation(Element* target);
+    ResizeObservation(Element& target);
 
-    Element* m_target;
+    WeakPtr<Element> m_target;
     LayoutSize m_lastObservationSize;
 };
 

Modified: trunk/Source/WebCore/page/ResizeObserver.cpp (279799 => 279800)


--- trunk/Source/WebCore/page/ResizeObserver.cpp	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/ResizeObserver.cpp	2021-07-10 01:03:03 UTC (rev 279800)
@@ -31,6 +31,7 @@
 #include "Element.h"
 #include "InspectorInstrumentation.h"
 #include "ResizeObserverEntry.h"
+#include <_javascript_Core/AbstractSlotVisitorInlines.h>
 
 namespace WebCore {
 
@@ -40,11 +41,9 @@
 }
 
 ResizeObserver::ResizeObserver(Document& document, Ref<ResizeObserverCallback>&& callback)
-    : ActiveDOMObject(callback->scriptExecutionContext())
-    , m_document(makeWeakPtr(document))
+    : m_document(makeWeakPtr(document))
     , m_callback(WTFMove(callback))
 {
-    suspendIfNeeded();
 }
 
 ResizeObserver::~ResizeObserver()
@@ -69,8 +68,7 @@
     auto& observerData = target.ensureResizeObserverData();
     observerData.observers.append(makeWeakPtr(this));
 
-    m_observations.append(ResizeObservation::create(&target));
-    m_pendingTargets.append(target);
+    m_observations.append(ResizeObservation::create(target));
 
     if (m_document) {
         m_document->addResizeObserver(*this);
@@ -107,6 +105,7 @@
             if (depth > deeperThan) {
                 observation->updateObservationSize(currentSize);
                 m_activeObservations.append(observation.get());
+                m_activeObservationTargets.append(*observation->target());
                 minObservedDepth = std::min(depth, minObservedDepth);
             } else
                 m_hasSkippedObservations = true;
@@ -133,6 +132,15 @@
     InspectorInstrumentation::didFireObserverCallback(*context);
 }
 
+bool ResizeObserver::isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor& visitor) const
+{
+    for (auto& observation : m_observations) {
+        if (auto* target = observation->target(); target && visitor.containsOpaqueRoot(target->opaqueRoot()))
+            return true;
+    }
+    return false;
+}
+
 bool ResizeObserver::removeTarget(Element& target)
 {
     auto* observerData = target.resizeObserverData();
@@ -149,7 +157,7 @@
         bool removed = removeTarget(*observation->target());
         ASSERT_UNUSED(removed, removed);
     }
-    m_pendingTargets.clear();
+    m_activeObservationTargets.clear();
     m_activeObservations.clear();
     m_observations.clear();
 }
@@ -156,7 +164,7 @@
 
 bool ResizeObserver::removeObservation(const Element& target)
 {
-    m_pendingTargets.removeFirstMatching([&target](auto& pendingTarget) {
+    m_activeObservationTargets.removeFirstMatching([&target](auto& pendingTarget) {
         return pendingTarget.ptr() == &target;
     });
 
@@ -169,22 +177,6 @@
     });
 }
 
-bool ResizeObserver::virtualHasPendingActivity() const
-{
-    return (hasObservations() && m_document) || !m_activeObservations.isEmpty();
-}
-
-const char* ResizeObserver::activeDOMObjectName() const
-{
-    return "ResizeObserver";
-}
-
-void ResizeObserver::stop()
-{
-    disconnect();
-    m_callback = nullptr;
-}
-
 } // namespace WebCore
 
 #endif // ENABLE(RESIZE_OBSERVER)

Modified: trunk/Source/WebCore/page/ResizeObserver.h (279799 => 279800)


--- trunk/Source/WebCore/page/ResizeObserver.h	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/ResizeObserver.h	2021-07-10 01:03:03 UTC (rev 279800)
@@ -27,13 +27,19 @@
 
 #if ENABLE(RESIZE_OBSERVER)
 
-#include "ActiveDOMObject.h"
 #include "GCReachableRef.h"
 #include "ResizeObservation.h"
 #include "ResizeObserverCallback.h"
+#include <wtf/Lock.h>
 #include <wtf/RefCounted.h>
 #include <wtf/WeakPtr.h>
 
+namespace JSC {
+
+class AbstractSlotVisitor;
+
+}
+
 namespace WebCore {
 
 class Document;
@@ -44,8 +50,7 @@
     Vector<WeakPtr<ResizeObserver>> observers;
 };
 
-class ResizeObserver : public RefCounted<ResizeObserver>, public ActiveDOMObject, public CanMakeWeakPtr<ResizeObserver> {
-    WTF_MAKE_FAST_ALLOCATED;
+class ResizeObserver : public RefCounted<ResizeObserver>, public CanMakeWeakPtr<ResizeObserver> {
 public:
     static Ref<ResizeObserver> create(Document&, Ref<ResizeObserverCallback>&&);
     ~ResizeObserver();
@@ -65,15 +70,11 @@
     void setHasSkippedObservations(bool skipped) { m_hasSkippedObservations = skipped; }
 
     ResizeObserverCallback* callbackConcurrently() { return m_callback.get(); }
+    bool isReachableFromOpaqueRoots(JSC::AbstractSlotVisitor&) const;
 
 private:
     ResizeObserver(Document&, Ref<ResizeObserverCallback>&&);
 
-    // ActiveDOMObject.
-    bool virtualHasPendingActivity() const override;
-    const char* activeDOMObjectName() const override;
-    void stop() override;
-
     bool removeTarget(Element&);
     void removeAllTargets();
     bool removeObservation(const Element&);
@@ -83,7 +84,7 @@
     Vector<Ref<ResizeObservation>> m_observations;
 
     Vector<Ref<ResizeObservation>> m_activeObservations;
-    Vector<GCReachableRef<Element>> m_pendingTargets;
+    Vector<GCReachableRef<Element>> m_activeObservationTargets;
     bool m_hasSkippedObservations { false };
 };
 

Modified: trunk/Source/WebCore/page/ResizeObserver.idl (279799 => 279800)


--- trunk/Source/WebCore/page/ResizeObserver.idl	2021-07-09 23:00:28 UTC (rev 279799)
+++ trunk/Source/WebCore/page/ResizeObserver.idl	2021-07-10 01:03:03 UTC (rev 279800)
@@ -26,11 +26,12 @@
 // https://wicg.github.io/ResizeObserver/
 
 [
-    ActiveDOMObject,
     Conditional=RESIZE_OBSERVER,
     EnabledBySetting=ResizeObserver,
     Exposed=Window,
     JSCustomMarkFunction,
+    CustomIsReachable,
+    ImplementationLacksVTable
 ] interface ResizeObserver {
     [CallWith=Document] constructor(ResizeObserverCallback callback);
 
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to