Throttle requestAnimationFrame in cross-origin iframes to 30fps
authorsimon.fraser@apple.com <simon.fraser@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 7 Apr 2017 00:04:10 +0000 (00:04 +0000)
committersimon.fraser@apple.com <simon.fraser@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Fri, 7 Apr 2017 00:04:10 +0000 (00:04 +0000)
https://bugs.webkit.org/show_bug.cgi?id=170534

Reviewed by Dan Bates.

Source/WebCore:

Add a throttling reason to ScriptedAnimationController which is NonInteractedCrossOriginFrame,
set on cross-origin iframes whose documents have never seen a user interaction. It's cleared
as soon as an interaction on this frame or a child frame is detected.

Move the initialization of the LowPowerMode throttling reason to Document::requestAnimationFrame(),
since it's more appropriate to compute NonInteractedCrossOriginFrame here than down in ScriptedAnimationController,
and best to do both in the same place.

Tests: http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html

* dom/Document.cpp:
(WebCore::Document::requestAnimationFrame):
(WebCore::Document::updateLastHandledUserGestureTimestamp):
* dom/Document.h:
(WebCore::Document::hasHadUserInteraction):
* dom/ScriptedAnimationController.cpp:
(WebCore::ScriptedAnimationController::ScriptedAnimationController):
(WebCore::throttlingReasonToString):
(WebCore::ScriptedAnimationController::interval):
* dom/ScriptedAnimationController.h:
* loader/FrameLoader.cpp:
(WebCore::shouldAskForNavigationConfirmation):

LayoutTests:

* http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe-expected.txt: Added.
* http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html: Added.
* http/tests/frame-throttling/resources/requestAnimationFrame-frame.html: Added.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@215070 268f45cc-cd09-0410-ab3c-d52691b4dbfc

LayoutTests/ChangeLog
LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe-expected.txt [new file with mode: 0644]
LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html [new file with mode: 0644]
LayoutTests/http/tests/frame-throttling/resources/requestAnimationFrame-frame.html [new file with mode: 0644]
Source/WebCore/ChangeLog
Source/WebCore/dom/Document.cpp
Source/WebCore/dom/Document.h
Source/WebCore/dom/ScriptedAnimationController.cpp
Source/WebCore/dom/ScriptedAnimationController.h
Source/WebCore/loader/FrameLoader.cpp

index c1936ca..75f5020 100644 (file)
@@ -1,3 +1,14 @@
+2017-04-05  Simon Fraser  <simon.fraser@apple.com>
+
+        Throttle requestAnimationFrame in cross-origin iframes to 30fps
+        https://bugs.webkit.org/show_bug.cgi?id=170534
+
+        Reviewed by Dan Bates.
+
+        * http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe-expected.txt: Added.
+        * http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html: Added.
+        * http/tests/frame-throttling/resources/requestAnimationFrame-frame.html: Added.
+
 2017-04-06  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed, rolling out r215041.
diff --git a/LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe-expected.txt b/LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe-expected.txt
new file mode 100644 (file)
index 0000000..a93e803
--- /dev/null
@@ -0,0 +1,22 @@
+Tests that requestAnimationFrame is throttled in subframes that are cross-origin, and not in same-origin frames
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Received message: frameload
+Received message: frameload
+Checking that requestAnimationFrame is throttled in cross origin frame
+Received message: throttled[cross]: true
+Received message: throttled[same]: false
+PASS throttledState['cross'] is "true"
+PASS throttledState['same'] is "false"
+Interacted with cross-origin frame
+Interacted with same-origin frame
+Received message: throttled[cross]: false
+Received message: throttled[same]: false
+PASS throttledState['cross'] is "false"
+PASS throttledState['same'] is "false"
+PASS successfullyParsed is true
+
+TEST COMPLETE
diff --git a/LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html b/LayoutTests/http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html
new file mode 100644 (file)
index 0000000..996cd4e
--- /dev/null
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <style>
+        iframe {
+            height: 200px;
+            width: 400px;
+        }
+    </style>
+    <script src="/js-test-resources/js-test-pre.js"></script>
+    <script src="/js-test-resources/ui-helper.js"></script>
+    
+    <script>
+    description("Tests that requestAnimationFrame is throttled in subframes that are cross-origin, and not in same-origin frames");
+    window.jsTestIsAsync = true;
+    
+    var crossOriginFrame;
+    var sameOriginFrame
+
+    var throttledState = {
+        'cross' : undefined,
+        'same' : undefined,
+    }
+
+    var messageHandler;
+    var messagesReceived = 0;
+
+    function interactWithSubframes()
+    {
+        UIHelper.activateAt(crossOriginFrame.offsetLeft + 5, crossOriginFrame.offsetTop + 5).then(function() {
+            debug("Interacted with cross-origin frame");
+            UIHelper.activateAt(sameOriginFrame.offsetLeft + 5, sameOriginFrame.offsetTop + 5).then(function() {
+                debug("Interacted with same-origin frame");
+                messageHandler = checkUnthrottledAfterInteraction;
+                messagesReceived = 0;
+                crossOriginFrame.contentWindow.postMessage("report-throttle-cross", "*");
+                sameOriginFrame.contentWindow.postMessage("report-throttle-same", "*");
+            });
+        });
+    }
+
+    function runTest()
+    {
+        crossOriginFrame = document.getElementById("cross-origin-frame");
+        sameOriginFrame = document.getElementById("same-origin-frame");
+
+        debug("Checking that requestAnimationFrame is throttled in cross origin frame");
+        
+        messageHandler = checkInitiallyThrottled;
+        messagesReceived = 0;
+        crossOriginFrame.contentWindow.postMessage("report-throttle-cross", "*");
+        sameOriginFrame.contentWindow.postMessage("report-throttle-same", "*");
+    }
+
+    function checkInitiallyThrottled()
+    {
+        shouldBeEqualToString("throttledState['cross']", "true");
+        shouldBeEqualToString("throttledState['same']", "false");
+        interactWithSubframes();
+    }
+
+    function checkUnthrottledAfterInteraction()
+    {
+        shouldBeEqualToString("throttledState['cross']", "false");
+        shouldBeEqualToString("throttledState['same']", "false");
+        finishJSTest();
+    }
+
+    window.onmessage = function(message)
+    {
+        debug("Received message: " + message.data);
+        if (message.data === "frameload") {
+            if (++messagesReceived == 2)
+                runTest();
+            return;
+        }
+
+        var re = /throttled\[(\w+)\]: (true|false)/;
+        var match = re.exec(message.data);
+        if (match) {
+            frameID = match[1];
+            throttledState[frameID] = match[2];
+            if (++messagesReceived == 2)
+                messageHandler();
+            return;
+        }
+        
+        debug("Failed to handle message " + message.data);
+    }
+    </script>
+</head>
+<body>
+    <iframe id="cross-origin-frame" src="http://localhost:8000/frame-throttling/resources/requestAnimationFrame-frame.html?dontcacheme"></iframe>
+    <iframe id="same-origin-frame" src="resources/requestAnimationFrame-frame.html?dontcacheme"></iframe>
+
+    <script src="/js-test-resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/http/tests/frame-throttling/resources/requestAnimationFrame-frame.html b/LayoutTests/http/tests/frame-throttling/resources/requestAnimationFrame-frame.html
new file mode 100644 (file)
index 0000000..df78626
--- /dev/null
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+    <script>
+        var i = 0;
+        function step()
+        {
+            i++;
+            requestAnimationFrame(step);
+        }
+        step();
+
+        window.onmessage = function(message)
+        {
+            var re = /report-throttle-(cross|same)/;
+            var match = re.exec(message.data);
+
+            if (match) {
+                var frameId = match[1];
+                if (window.internals)
+                    parent.window.postMessage("throttled[" + frameId + "]: " + internals.isRequestAnimationFrameThrottled(), "*");
+            }
+        }
+
+        window.addEventListener("load", function() {
+            parent.window.postMessage("frameload", "*");
+        }, false);
+    </script>
+</head>
+<body>
+    Child frame.
+</body>
+</html>
index 483c036..e641e56 100644 (file)
@@ -1,5 +1,35 @@
 2017-04-05  Simon Fraser  <simon.fraser@apple.com>
 
+        Throttle requestAnimationFrame in cross-origin iframes to 30fps
+        https://bugs.webkit.org/show_bug.cgi?id=170534
+
+        Reviewed by Dan Bates.
+
+        Add a throttling reason to ScriptedAnimationController which is NonInteractedCrossOriginFrame,
+        set on cross-origin iframes whose documents have never seen a user interaction. It's cleared
+        as soon as an interaction on this frame or a child frame is detected.
+
+        Move the initialization of the LowPowerMode throttling reason to Document::requestAnimationFrame(),
+        since it's more appropriate to compute NonInteractedCrossOriginFrame here than down in ScriptedAnimationController,
+        and best to do both in the same place.
+
+        Tests: http/tests/frame-throttling/raf-throttle-in-cross-origin-subframe.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::requestAnimationFrame):
+        (WebCore::Document::updateLastHandledUserGestureTimestamp):
+        * dom/Document.h:
+        (WebCore::Document::hasHadUserInteraction):
+        * dom/ScriptedAnimationController.cpp:
+        (WebCore::ScriptedAnimationController::ScriptedAnimationController):
+        (WebCore::throttlingReasonToString):
+        (WebCore::ScriptedAnimationController::interval):
+        * dom/ScriptedAnimationController.h:
+        * loader/FrameLoader.cpp:
+        (WebCore::shouldAskForNavigationConfirmation):
+
+2017-04-05  Simon Fraser  <simon.fraser@apple.com>
+
         Use the Accelerate framework to optimize FEColorMatrix operations
         https://bugs.webkit.org/show_bug.cgi?id=170518
 
index 2965abe..381fab1 100644 (file)
@@ -6011,6 +6011,12 @@ int Document::requestAnimationFrame(Ref<RequestAnimationFrameCallback>&& callbac
         if (!page() || page()->scriptedAnimationsSuspended())
             m_scriptedAnimationController->suspend();
 
+        if (page() && page()->isLowPowerModeEnabled())
+            m_scriptedAnimationController->addThrottlingReason(ScriptedAnimationController::ThrottlingReason::LowPowerMode);
+
+        if (!topOrigin().canAccess(securityOrigin()) && !hasHadUserInteraction())
+            m_scriptedAnimationController->addThrottlingReason(ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame);
+
         if (settings().shouldDispatchRequestAnimationFrameEvents()) {
             if (!page())
                 dispatchEvent(Event::create("raf-no-page", false, false));
@@ -6315,6 +6321,11 @@ Document::RegionFixedPair Document::absoluteRegionForEventTargets(const EventTar
 void Document::updateLastHandledUserGestureTimestamp(MonotonicTime time)
 {
     m_lastHandledUserGestureTimestamp = time;
+
+    if (static_cast<bool>(time) && m_scriptedAnimationController) {
+        // It's OK to always remove NonInteractedCrossOriginFrame even if this frame isn't cross-origin.
+        m_scriptedAnimationController->removeThrottlingReason(ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame);
+    }
     
     if (HTMLFrameOwnerElement* element = ownerElement())
         element->document().updateLastHandledUserGestureTimestamp(time);
index 0465bcc..ebb4d6b 100644 (file)
@@ -1144,6 +1144,7 @@ public:
     void didRemoveWheelEventHandler(Node&, EventHandlerRemoval = EventHandlerRemoval::One);
 
     MonotonicTime lastHandledUserGestureTimestamp() const { return m_lastHandledUserGestureTimestamp; }
+    bool hasHadUserInteraction() const { return static_cast<bool>(m_lastHandledUserGestureTimestamp); }
     void updateLastHandledUserGestureTimestamp(MonotonicTime);
 
     // Used for testing. Count handlers in the main document, and one per frame which contains handlers.
index 964b184..54dd34f 100644 (file)
@@ -64,10 +64,6 @@ ScriptedAnimationController::ScriptedAnimationController(Document& document, Pla
 #endif
 {
     windowScreenDidChange(displayID);
-
-    auto* page = document.page();
-    if (page && page->isLowPowerModeEnabled())
-        addThrottlingReason(ThrottlingReason::LowPowerMode);
 }
 
 ScriptedAnimationController::~ScriptedAnimationController()
@@ -110,6 +106,8 @@ static const char* throttlingReasonToString(ScriptedAnimationController::Throttl
         return "OutsideViewport";
     case ScriptedAnimationController::ThrottlingReason::LowPowerMode:
         return "LowPowerMode";
+    case ScriptedAnimationController::ThrottlingReason::NonInteractedCrossOriginFrame:
+        return "NonInteractiveCrossOriginFrame";
     }
 }
 
@@ -268,8 +266,13 @@ Seconds ScriptedAnimationController::interval() const
 #if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
     if (m_throttlingReasons.contains(ThrottlingReason::VisuallyIdle) || m_throttlingReasons.contains(ThrottlingReason::OutsideViewport))
         return aggressiveThrottlingAnimationInterval;
+
     if (m_throttlingReasons.contains(ThrottlingReason::LowPowerMode))
         return halfSpeedThrottlingAnimationInterval;
+
+    if (m_throttlingReasons.contains(ThrottlingReason::NonInteractedCrossOriginFrame))
+        return halfSpeedThrottlingAnimationInterval;
+
     ASSERT(m_throttlingReasons.isEmpty());
 #endif
     return fullSpeedAnimationInterval;
index ce91daf..92eef93 100644 (file)
@@ -73,9 +73,10 @@ public:
     void resume();
 
     enum class ThrottlingReason {
-        VisuallyIdle    = 1 << 0,
-        OutsideViewport = 1 << 1,
-        LowPowerMode    = 1 << 2,
+        VisuallyIdle                    = 1 << 0,
+        OutsideViewport                 = 1 << 1,
+        LowPowerMode                    = 1 << 2,
+        NonInteractedCrossOriginFrame   = 1 << 3,
     };
     void addThrottlingReason(ThrottlingReason);
     void removeThrottlingReason(ThrottlingReason);
index 2288580..23f1b4f 100644 (file)
@@ -3036,7 +3036,7 @@ void FrameLoader::dispatchUnloadEvents(UnloadEventPolicy unloadEventPolicy)
 
 static bool shouldAskForNavigationConfirmation(Document& document, const BeforeUnloadEvent& event)
 {
-    bool userDidInteractWithPage = static_cast<bool>(document.topDocument().lastHandledUserGestureTimestamp());
+    bool userDidInteractWithPage = document.topDocument().hasHadUserInteraction();
     // Web pages can request we ask for confirmation before navigating by:
     // - Cancelling the BeforeUnloadEvent (modern way)
     // - Setting the returnValue attribute on the BeforeUnloadEvent to a non-empty string.