[iOS] Throttle requestAnimationFrame to 30fps in low power mode
authorcdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Feb 2017 21:26:27 +0000 (21:26 +0000)
committercdumez@apple.com <cdumez@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Tue, 28 Feb 2017 21:26:27 +0000 (21:26 +0000)
https://bugs.webkit.org/show_bug.cgi?id=168837
<rdar://problem/30700929>

Reviewed by Simon Fraser.

Source/WebCore:

Throttle requestAnimationFrame to 30fps in low power mode on iOS to save battery.

ScriptedAnimationController now maintains an OptionSet of throttling reasons.
Throttling reasons for now are: OutsideViewport, VisuallyIdle, and LowPowerMode.
The requestAnimationFrame interval is then determined based on those throttling
reasons:
- OutsideViewport or VisuallyIdle: 10 seconds (very aggressive throttling)
- LowPowerMode: 30fps
- No reasons: 60fps

The Page now keeps track of low power mode state using a LowPowerModeNotifier.
Whenever low power mode changes, it updates the throttling reasons in all the
documents' ScriptedAnimationControllers in the frame tree.

Tests: fast/animation/request-animation-frame-throttling-detached-iframe.html
       fast/animation/request-animation-frame-throttling-lowPowerMode.html

* dom/Document.cpp:
(WebCore::Document::requestAnimationFrame):
* dom/Document.h:
* dom/ScriptedAnimationController.cpp:
(WebCore::ScriptedAnimationController::ScriptedAnimationController):
(WebCore::throttlingReasonToString):
(WebCore::throttlingReasonsToString):
(WebCore::ScriptedAnimationController::addThrottlingReason):
(WebCore::ScriptedAnimationController::removeThrottlingReason):
(WebCore::ScriptedAnimationController::isThrottled):
(WebCore::ScriptedAnimationController::interval):
(WebCore::ScriptedAnimationController::page):
(WebCore::ScriptedAnimationController::scheduleAnimation):
* dom/ScriptedAnimationController.h:
(WebCore::ScriptedAnimationController::create):
* page/FrameView.cpp:
(WebCore::FrameView::updateScriptedAnimationsAndTimersThrottlingState):
* page/Page.cpp:
(WebCore::Page::Page):
(WebCore::Page::isLowPowerModeEnabled):
(WebCore::Page::setLowPowerModeEnabledOverrideForTesting):
(WebCore::updateScriptedAnimationsThrottlingReason):
(WebCore::Page::setIsVisuallyIdleInternal):
(WebCore::Page::handleLowModePowerChange):
* page/Page.h:
* testing/Internals.cpp:
(WebCore::Internals::resetToConsistentState):
(WebCore::Internals::requestAnimationFrameInterval):
(WebCore::Internals::setLowPowerModeEnabled):
* testing/Internals.h:
* testing/Internals.idl:

Source/WTF:

Add support for operator -= on WTF::OptionSet for convenience:
    set -= Enum::A;
looks much better than:
    set = set - Enum::A;

* wtf/OptionSet.h:
(WTF::OptionSet::operator-=):

Tools:

Add unit test for -= operator on WTF::OptionSet.

* TestWebKitAPI/Tests/WTF/OptionSet.cpp:
(TestWebKitAPI::TEST):

LayoutTests:

Add layout test coverage.

* fast/animation/request-animation-frame-throttling-detached-iframe-expected.txt: Added.
* fast/animation/request-animation-frame-throttling-detached-iframe.html: Added.
* fast/animation/request-animation-frame-throttling-lowPowerMode-expected.txt: Added.
* fast/animation/request-animation-frame-throttling-lowPowerMode.html: Added.
* fast/animation/resources/frame-with-animation.html: Added.

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

21 files changed:
LayoutTests/ChangeLog
LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe-expected.txt [new file with mode: 0644]
LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe.html [new file with mode: 0644]
LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode-expected.txt [new file with mode: 0644]
LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode.html [new file with mode: 0644]
LayoutTests/fast/animation/resources/frame-with-animation.html [new file with mode: 0644]
Source/WTF/ChangeLog
Source/WTF/wtf/OptionSet.h
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/page/FrameView.cpp
Source/WebCore/page/Page.cpp
Source/WebCore/page/Page.h
Source/WebCore/testing/Internals.cpp
Source/WebCore/testing/Internals.h
Source/WebCore/testing/Internals.idl
Tools/ChangeLog
Tools/TestWebKitAPI/Tests/WTF/OptionSet.cpp

index 059f0f6..cc9aaee 100644 (file)
@@ -1,3 +1,19 @@
+2017-02-28  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Throttle requestAnimationFrame to 30fps in low power mode
+        https://bugs.webkit.org/show_bug.cgi?id=168837
+        <rdar://problem/30700929>
+
+        Reviewed by Simon Fraser.
+
+        Add layout test coverage.
+
+        * fast/animation/request-animation-frame-throttling-detached-iframe-expected.txt: Added.
+        * fast/animation/request-animation-frame-throttling-detached-iframe.html: Added.
+        * fast/animation/request-animation-frame-throttling-lowPowerMode-expected.txt: Added.
+        * fast/animation/request-animation-frame-throttling-lowPowerMode.html: Added.
+        * fast/animation/resources/frame-with-animation.html: Added.
+
 2017-02-28  Myles C. Maxfield  <mmaxfield@apple.com>
 
         [macOS] Migrate off of CTFontCreateForCSS
diff --git a/LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe-expected.txt b/LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe-expected.txt
new file mode 100644 (file)
index 0000000..7286488
--- /dev/null
@@ -0,0 +1,31 @@
+Test that requestAnimationFrame gets the right throttling in an iframe when inserted into a document.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS internals.isRequestAnimationFrameThrottled() is false
+PASS internals.requestAnimationFrameInterval is 0.015
+PASS frame.contentWindow.internals.isRequestAnimationFrameThrottled() is false
+PASS frame.contentWindow.internals.requestAnimationFrameInterval is 0.015
+internals.setLowPowerModeEnabled(true);
+PASS internals.isRequestAnimationFrameThrottled() is true
+PASS internals.requestAnimationFrameInterval is 0.030
+PASS frame.contentWindow.internals.isRequestAnimationFrameThrottled() is true
+PASS frame.contentWindow.internals.requestAnimationFrameInterval is 0.030
+frame.remove()
+document.body.appendChild(frame)
+PASS internals.isRequestAnimationFrameThrottled() is true
+PASS internals.requestAnimationFrameInterval is 0.030
+PASS frame.contentWindow.internals.isRequestAnimationFrameThrottled() is true
+PASS frame.contentWindow.internals.requestAnimationFrameInterval is 0.030
+frame.remove()
+internals.setLowPowerModeEnabled(false);
+PASS internals.isRequestAnimationFrameThrottled() is false
+PASS internals.requestAnimationFrameInterval is 0.015
+document.body.appendChild(frame)
+PASS frame.contentWindow.internals.isRequestAnimationFrameThrottled() is false
+PASS frame.contentWindow.internals.requestAnimationFrameInterval is 0.015
+PASS successfullyParsed is true
+
+TEST COMPLETE
diff --git a/LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe.html b/LayoutTests/fast/animation/request-animation-frame-throttling-detached-iframe.html
new file mode 100644 (file)
index 0000000..74280c3
--- /dev/null
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test-pre.js"></script>
+<script>
+description("Test that requestAnimationFrame gets the right throttling in an iframe when inserted into a document.");
+jsTestIsAsync = true;
+
+let i = 0;
+function doWork()
+{
+    i++;
+    requestAnimationFrame(doWork);
+}
+
+requestAnimationFrame(doWork);
+
+const frame = document.createElement("iframe");
+frame.src = "resources/frame-with-animation.html";
+frame.onload = function() {
+    shouldBeFalse("internals.isRequestAnimationFrameThrottled()");
+    shouldBe("internals.requestAnimationFrameInterval", "0.015");
+    shouldBeFalse("frame.contentWindow.internals.isRequestAnimationFrameThrottled()");
+    shouldBe("frame.contentWindow.internals.requestAnimationFrameInterval", "0.015");
+
+    evalAndLog("internals.setLowPowerModeEnabled(true);");
+    shouldBeTrue("internals.isRequestAnimationFrameThrottled()");
+    shouldBe("internals.requestAnimationFrameInterval", "0.030");
+    shouldBeTrue("frame.contentWindow.internals.isRequestAnimationFrameThrottled()");
+    shouldBe("frame.contentWindow.internals.requestAnimationFrameInterval", "0.030");
+    evalAndLog("frame.remove()");
+
+    evalAndLog("document.body.appendChild(frame)");
+    frame.onload = function() {
+        shouldBeTrue("internals.isRequestAnimationFrameThrottled()");
+        shouldBe("internals.requestAnimationFrameInterval", "0.030");
+        shouldBeTrue("frame.contentWindow.internals.isRequestAnimationFrameThrottled()");
+        shouldBe("frame.contentWindow.internals.requestAnimationFrameInterval", "0.030");
+
+        evalAndLog("frame.remove()");
+        evalAndLog("internals.setLowPowerModeEnabled(false);");
+        shouldBeFalse("internals.isRequestAnimationFrameThrottled()");
+        shouldBe("internals.requestAnimationFrameInterval", "0.015");
+
+        evalAndLog("document.body.appendChild(frame)");
+        frame.onload = function() {
+            shouldBeFalse("frame.contentWindow.internals.isRequestAnimationFrameThrottled()");
+            shouldBe("frame.contentWindow.internals.requestAnimationFrameInterval", "0.015");
+            finishJSTest();
+        }
+    }
+};
+document.body.appendChild(frame);
+</script>
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode-expected.txt b/LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode-expected.txt
new file mode 100644 (file)
index 0000000..9833ffb
--- /dev/null
@@ -0,0 +1,26 @@
+Test that requestAnimationFrame gets throttled in low power mode.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+PASS internals.isRequestAnimationFrameThrottled() is false
+PASS internals.requestAnimationFrameInterval is Infinity
+rAFHandle = requestAnimationFrame(doWork);
+PASS internals.isRequestAnimationFrameThrottled() is false
+PASS internals.requestAnimationFrameInterval is 0.015
+internals.setLowPowerModeEnabled(true);
+PASS internals.isRequestAnimationFrameThrottled() is true
+PASS internals.requestAnimationFrameInterval is 0.030
+cancelAnimationFrame(rAFHandle);
+PASS internals.isRequestAnimationFrameThrottled() is true
+PASS internals.requestAnimationFrameInterval is 0.030
+rAFHandle = requestAnimationFrame(doWork);
+PASS internals.isRequestAnimationFrameThrottled() is true
+PASS internals.requestAnimationFrameInterval is 0.030
+internals.setLowPowerModeEnabled(false);
+PASS internals.isRequestAnimationFrameThrottled() is false
+PASS internals.requestAnimationFrameInterval is 0.015
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode.html b/LayoutTests/fast/animation/request-animation-frame-throttling-lowPowerMode.html
new file mode 100644 (file)
index 0000000..dbae09f
--- /dev/null
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+<body>
+<script src="../../resources/js-test-pre.js"></script>
+<script>
+description("Test that requestAnimationFrame gets throttled in low power mode.");
+
+let rAFHandle;
+let i = 0;
+function doWork()
+{
+    i++;
+    rAFHandle = requestAnimationFrame(doWork);
+}
+
+shouldBeFalse("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "Infinity");
+evalAndLog("rAFHandle = requestAnimationFrame(doWork);");
+shouldBeFalse("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "0.015");
+evalAndLog("internals.setLowPowerModeEnabled(true);");
+shouldBeTrue("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "0.030");
+evalAndLog("cancelAnimationFrame(rAFHandle);");
+shouldBeTrue("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "0.030");
+evalAndLog("rAFHandle = requestAnimationFrame(doWork);");
+shouldBeTrue("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "0.030");
+evalAndLog("internals.setLowPowerModeEnabled(false);");
+shouldBeFalse("internals.isRequestAnimationFrameThrottled()");
+shouldBe("internals.requestAnimationFrameInterval", "0.015");
+</script>
+<script src="../../resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/LayoutTests/fast/animation/resources/frame-with-animation.html b/LayoutTests/fast/animation/resources/frame-with-animation.html
new file mode 100644 (file)
index 0000000..eeb796e
--- /dev/null
@@ -0,0 +1,10 @@
+<script>
+let i = 0;
+function doWork()
+{
+    i++;
+    requestAnimationFrame(doWork);
+}
+
+requestAnimationFrame(doWork);
+</script>
index 15fdf01..a24cc67 100644 (file)
@@ -1,3 +1,19 @@
+2017-02-28  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Throttle requestAnimationFrame to 30fps in low power mode
+        https://bugs.webkit.org/show_bug.cgi?id=168837
+        <rdar://problem/30700929>
+
+        Reviewed by Simon Fraser.
+
+        Add support for operator -= on WTF::OptionSet for convenience:
+            set -= Enum::A;
+        looks much better than:
+            set = set - Enum::A;
+
+        * wtf/OptionSet.h:
+        (WTF::OptionSet::operator-=):
+
 2017-02-28  Michael Saboff  <msaboff@apple.com>
 
         Add ability to configure JSC options from a file
index e839f42..f3a8c00 100644 (file)
@@ -125,6 +125,13 @@ public:
         return lhs;
     }
 
+    friend OptionSet& operator-=(OptionSet& lhs, OptionSet rhs)
+    {
+        lhs.m_storage &= ~rhs.m_storage;
+
+        return lhs;
+    }
+
     constexpr friend OptionSet operator-(OptionSet lhs, OptionSet rhs)
     {
         return OptionSet::fromRaw(lhs.m_storage & ~rhs.m_storage);
index f0f1f59..f099ac1 100644 (file)
@@ -1,3 +1,60 @@
+2017-02-28  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Throttle requestAnimationFrame to 30fps in low power mode
+        https://bugs.webkit.org/show_bug.cgi?id=168837
+        <rdar://problem/30700929>
+
+        Reviewed by Simon Fraser.
+
+        Throttle requestAnimationFrame to 30fps in low power mode on iOS to save battery.
+
+        ScriptedAnimationController now maintains an OptionSet of throttling reasons.
+        Throttling reasons for now are: OutsideViewport, VisuallyIdle, and LowPowerMode.
+        The requestAnimationFrame interval is then determined based on those throttling
+        reasons:
+        - OutsideViewport or VisuallyIdle: 10 seconds (very aggressive throttling)
+        - LowPowerMode: 30fps
+        - No reasons: 60fps
+
+        The Page now keeps track of low power mode state using a LowPowerModeNotifier.
+        Whenever low power mode changes, it updates the throttling reasons in all the
+        documents' ScriptedAnimationControllers in the frame tree.
+
+        Tests: fast/animation/request-animation-frame-throttling-detached-iframe.html
+               fast/animation/request-animation-frame-throttling-lowPowerMode.html
+
+        * dom/Document.cpp:
+        (WebCore::Document::requestAnimationFrame):
+        * dom/Document.h:
+        * dom/ScriptedAnimationController.cpp:
+        (WebCore::ScriptedAnimationController::ScriptedAnimationController):
+        (WebCore::throttlingReasonToString):
+        (WebCore::throttlingReasonsToString):
+        (WebCore::ScriptedAnimationController::addThrottlingReason):
+        (WebCore::ScriptedAnimationController::removeThrottlingReason):
+        (WebCore::ScriptedAnimationController::isThrottled):
+        (WebCore::ScriptedAnimationController::interval):
+        (WebCore::ScriptedAnimationController::page):
+        (WebCore::ScriptedAnimationController::scheduleAnimation):
+        * dom/ScriptedAnimationController.h:
+        (WebCore::ScriptedAnimationController::create):
+        * page/FrameView.cpp:
+        (WebCore::FrameView::updateScriptedAnimationsAndTimersThrottlingState):
+        * page/Page.cpp:
+        (WebCore::Page::Page):
+        (WebCore::Page::isLowPowerModeEnabled):
+        (WebCore::Page::setLowPowerModeEnabledOverrideForTesting):
+        (WebCore::updateScriptedAnimationsThrottlingReason):
+        (WebCore::Page::setIsVisuallyIdleInternal):
+        (WebCore::Page::handleLowModePowerChange):
+        * page/Page.h:
+        * testing/Internals.cpp:
+        (WebCore::Internals::resetToConsistentState):
+        (WebCore::Internals::requestAnimationFrameInterval):
+        (WebCore::Internals::setLowPowerModeEnabled):
+        * testing/Internals.h:
+        * testing/Internals.idl:
+
 2017-02-28  Youenn Fablet  <youenn@apple.com>
 
         [WebRTC] Limit libwebrtc logging in Debug build
index df8b6f7..b1e2f4a 100644 (file)
@@ -5481,12 +5481,6 @@ void Document::resumeScriptedAnimationControllerCallbacks()
         m_scriptedAnimationController->resume();
 }
 
-void Document::scriptedAnimationControllerSetThrottled(bool isThrottled)
-{
-    if (m_scriptedAnimationController)
-        m_scriptedAnimationController->setThrottled(isThrottled);
-}
-
 void Document::windowScreenDidChange(PlatformDisplayID displayID)
 {
     if (m_scriptedAnimationController)
@@ -6058,9 +6052,9 @@ int Document::requestAnimationFrame(Ref<RequestAnimationFrameCallback>&& callbac
 {
     if (!m_scriptedAnimationController) {
 #if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
-        m_scriptedAnimationController = ScriptedAnimationController::create(this, page() ? page()->chrome().displayID() : 0);
+        m_scriptedAnimationController = ScriptedAnimationController::create(*this, page() ? page()->chrome().displayID() : 0);
 #else
-        m_scriptedAnimationController = ScriptedAnimationController::create(this, 0);
+        m_scriptedAnimationController = ScriptedAnimationController::create(*this, 0);
 #endif
         // It's possible that the Page may have suspended scripted animations before
         // we were created. We need to make sure that we don't start up the animation
index 99ffcdb..61de319 100644 (file)
@@ -958,7 +958,6 @@ public:
     ScriptedAnimationController* scriptedAnimationController() { return m_scriptedAnimationController.get(); }
     void suspendScriptedAnimationControllerCallbacks();
     void resumeScriptedAnimationControllerCallbacks();
-    void scriptedAnimationControllerSetThrottled(bool);
     
     void windowScreenDidChange(PlatformDisplayID);
 
index b94ebdf..605df6e 100644 (file)
 #include "Settings.h"
 #include <wtf/Ref.h>
 #include <wtf/SystemTracing.h>
+#include <wtf/text/StringBuilder.h>
 
 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
 #include <algorithm>
 #include <wtf/CurrentTime.h>
 
 // Allow a little more than 60fps to make sure we can at least hit that frame rate.
-#define MinimumAnimationInterval 0.015
-#define MinimumThrottledAnimationInterval 10
+static const Seconds fullSpeedAnimationInterval { 0.015 };
+// Allow a little more than 30fps to make sure we can at least hit that frame rate.
+static const Seconds halfSpeedThrottlingAnimationInterval { 0.030 };
+static const Seconds aggressiveThrottlingAnimationInterval { 10 };
 #endif
 
+#define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(page() && page()->isAlwaysOnLoggingAllowed(), PerformanceLogging, "%p - ScriptedAnimationController::" fmt, this, ##__VA_ARGS__)
+
 namespace WebCore {
 
-ScriptedAnimationController::ScriptedAnimationController(Document* document, PlatformDisplayID displayID)
-    : m_document(document)
+ScriptedAnimationController::ScriptedAnimationController(Document& document, PlatformDisplayID displayID)
+    : m_document(&document)
 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
     , m_animationTimer(*this, &ScriptedAnimationController::animationTimerFired)
 #endif
 {
     windowScreenDidChange(displayID);
+
+    auto* page = document.page();
+    if (page && page->isLowPowerModeEnabled())
+        addThrottlingReason(ThrottlingReason::LowPowerMode);
 }
 
 ScriptedAnimationController::~ScriptedAnimationController()
@@ -86,28 +95,78 @@ void ScriptedAnimationController::resume()
         scheduleAnimation();
 }
 
-void ScriptedAnimationController::setThrottled(bool isThrottled)
+#if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR) && !RELEASE_LOG_DISABLED
+
+static const char* throttlingReasonToString(ScriptedAnimationController::ThrottlingReason reason)
+{
+    switch (reason) {
+    case ScriptedAnimationController::ThrottlingReason::VisuallyIdle:
+        return "VisuallyIdle";
+    case ScriptedAnimationController::ThrottlingReason::OutsideViewport:
+        return "OutsideViewport";
+    case ScriptedAnimationController::ThrottlingReason::LowPowerMode:
+        return "LowPowerMode";
+    }
+}
+
+static String throttlingReasonsToString(OptionSet<ScriptedAnimationController::ThrottlingReason> reasons)
+{
+    if (reasons.isEmpty())
+        return ASCIILiteral("[Unthrottled]");
+
+    StringBuilder builder;
+    for (auto reason : reasons) {
+        if (!builder.isEmpty())
+            builder.append('|');
+        builder.append(throttlingReasonToString(reason));
+    }
+    return builder.toString();
+}
+
+#endif
+
+void ScriptedAnimationController::addThrottlingReason(ThrottlingReason reason)
 {
 #if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
-    if (m_isThrottled == isThrottled)
+    if (m_throttlingReasons.contains(reason))
         return;
 
-    LOG(Animations, "%p - Setting RequestAnimationFrame throttling state to %d in frame %p (isMainFrame: %d)", this, isThrottled, m_document->frame(), m_document->frame() ? m_document->frame()->isMainFrame() : 0);
+    m_throttlingReasons |= reason;
+
+    RELEASE_LOG_IF_ALLOWED("addThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
 
-    m_isThrottled = isThrottled;
     if (m_animationTimer.isActive()) {
         m_animationTimer.stop();
         scheduleAnimation();
     }
 #else
-    UNUSED_PARAM(isThrottled);
+    UNUSED_PARAM(reason);
+#endif
+}
+
+void ScriptedAnimationController::removeThrottlingReason(ThrottlingReason reason)
+{
+#if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
+    if (!m_throttlingReasons.contains(reason))
+        return;
+
+    m_throttlingReasons -= reason;
+
+    RELEASE_LOG_IF_ALLOWED("removeThrottlingReason(%s) -> %s", throttlingReasonToString(reason), throttlingReasonsToString(m_throttlingReasons).utf8().data());
+
+    if (m_animationTimer.isActive()) {
+        m_animationTimer.stop();
+        scheduleAnimation();
+    }
+#else
+    UNUSED_PARAM(reason);
 #endif
 }
 
 bool ScriptedAnimationController::isThrottled() const
 {
 #if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
-    return m_isThrottled;
+    return !m_throttlingReasons.isEmpty();
 #else
     return false;
 #endif
@@ -192,6 +251,23 @@ void ScriptedAnimationController::windowScreenDidChange(PlatformDisplayID displa
 #endif
 }
 
+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;
+    ASSERT(m_throttlingReasons.isEmpty());
+#endif
+    return fullSpeedAnimationInterval;
+}
+
+Page* ScriptedAnimationController::page() const
+{
+    return m_document ? m_document->page() : nullptr;
+}
+
 void ScriptedAnimationController::scheduleAnimation()
 {
     if (!requestAnimationFrameEnabled())
@@ -199,7 +275,7 @@ void ScriptedAnimationController::scheduleAnimation()
 
 #if USE(REQUEST_ANIMATION_FRAME_TIMER)
 #if USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
-    if (!m_isUsingTimer && !m_isThrottled) {
+    if (!m_isUsingTimer && !isThrottled()) {
         if (DisplayRefreshMonitorManager::sharedManager().scheduleAnimation(*this))
             return;
 
@@ -209,13 +285,8 @@ void ScriptedAnimationController::scheduleAnimation()
     if (m_animationTimer.isActive())
         return;
 
-    double animationInterval = MinimumAnimationInterval;
-#if USE(REQUEST_ANIMATION_FRAME_TIMER) && USE(REQUEST_ANIMATION_FRAME_DISPLAY_MONITOR)
-    if (m_isThrottled)
-        animationInterval = MinimumThrottledAnimationInterval;
-#endif
-
-    double scheduleDelay = std::max<double>(animationInterval - (m_document->domWindow()->nowTimestamp() - m_lastAnimationFrameTimestamp), 0);
+    Seconds animationInterval = interval();
+    double scheduleDelay = std::max<double>(animationInterval.value() - (m_document->domWindow()->nowTimestamp() - m_lastAnimationFrameTimestamp), 0);
     m_animationTimer.startOneShot(scheduleDelay);
 #else
     if (FrameView* frameView = m_document->view())
index 012c258..875a366 100644 (file)
@@ -27,6 +27,7 @@
 
 #include "DOMTimeStamp.h"
 #include "PlatformScreen.h"
+#include <wtf/OptionSet.h>
 #include <wtf/RefCounted.h>
 #include <wtf/RefPtr.h>
 #include <wtf/Vector.h>
@@ -44,6 +45,7 @@
 namespace WebCore {
 
 class Document;
+class Page;
 class RequestAnimationFrameCallback;
 
 class ScriptedAnimationController : public RefCounted<ScriptedAnimationController>
@@ -52,7 +54,7 @@ class ScriptedAnimationController : public RefCounted<ScriptedAnimationControlle
 #endif
 {
 public:
-    static Ref<ScriptedAnimationController> create(Document* document, PlatformDisplayID displayID)
+    static Ref<ScriptedAnimationController> create(Document& document, PlatformDisplayID displayID)
     {
         return adoptRef(*new ScriptedAnimationController(document, displayID));
     }
@@ -68,13 +70,23 @@ public:
 
     void suspend();
     void resume();
-    void setThrottled(bool);
+
+    enum class ThrottlingReason {
+        VisuallyIdle    = 1 << 0,
+        OutsideViewport = 1 << 1,
+        LowPowerMode    = 1 << 2,
+    };
+    void addThrottlingReason(ThrottlingReason);
+    void removeThrottlingReason(ThrottlingReason);
     WEBCORE_EXPORT bool isThrottled() const;
+    WEBCORE_EXPORT Seconds interval() const;
 
     void windowScreenDidChange(PlatformDisplayID);
 
 private:
-    ScriptedAnimationController(Document*, PlatformDisplayID);
+    ScriptedAnimationController(Document&, PlatformDisplayID);
+
+    Page* page() const;
 
     typedef Vector<RefPtr<RequestAnimationFrameCallback>> CallbackList;
     CallbackList m_callbacks;
@@ -97,7 +109,8 @@ private:
     RefPtr<DisplayRefreshMonitor> createDisplayRefreshMonitor(PlatformDisplayID) const override;
 
     bool m_isUsingTimer { false };
-    bool m_isThrottled { false };
+
+    OptionSet<ThrottlingReason> m_throttlingReasons;
 #endif
 };
 
index d150805..3c9cea9 100644 (file)
@@ -2563,8 +2563,12 @@ void FrameView::updateScriptedAnimationsAndTimersThrottlingState(const IntRect&
     // We don't throttle zero-size or display:none frames because those are usually utility frames.
     bool shouldThrottle = visibleRect.isEmpty() && !m_size.isEmpty() && frame().ownerRenderer();
 
-    if (auto* scriptedAnimationController = document->scriptedAnimationController())
-        scriptedAnimationController->setThrottled(shouldThrottle);
+    if (auto* scriptedAnimationController = document->scriptedAnimationController()) {
+        if (shouldThrottle)
+            scriptedAnimationController->addThrottlingReason(ScriptedAnimationController::ThrottlingReason::OutsideViewport);
+        else
+            scriptedAnimationController->removeThrottlingReason(ScriptedAnimationController::ThrottlingReason::OutsideViewport);
+    }
 
     document->setTimerThrottlingEnabled(shouldThrottle);
 }
index 5670447..131b098 100644 (file)
@@ -57,6 +57,7 @@
 #include "InspectorInstrumentation.h"
 #include "LibWebRTCProvider.h"
 #include "Logging.h"
+#include "LowPowerModeNotifier.h"
 #include "MainFrame.h"
 #include "MediaCanStartListener.h"
 #include "Navigator.h"
@@ -84,6 +85,7 @@
 #include "SVGDocumentExtensions.h"
 #include "SchemeRegistry.h"
 #include "ScriptController.h"
+#include "ScriptedAnimationController.h"
 #include "ScrollingCoordinator.h"
 #include "Settings.h"
 #include "SharedBuffer.h"
@@ -263,6 +265,7 @@ Page::Page(PageConfiguration&& pageConfiguration)
     , m_isClosing(false)
     , m_isUtilityPage(isUtilityPageChromeClient(chrome().client()))
     , m_performanceMonitor(isUtilityPage() ? nullptr : std::make_unique<PerformanceMonitor>(*this))
+    , m_lowPowerModeNotifier(std::make_unique<LowPowerModeNotifier>([this](bool isLowPowerModeEnabled) { handleLowModePowerChange(isLowPowerModeEnabled); }))
 {
     updateTimerThrottlingState();
 
@@ -954,6 +957,20 @@ bool Page::isOnlyNonUtilityPage() const
     return !isUtilityPage() && nonUtilityPageCount == 1;
 }
 
+bool Page::isLowPowerModeEnabled() const
+{
+    if (m_lowPowerModeEnabledOverrideForTesting)
+        return m_lowPowerModeEnabledOverrideForTesting.value();
+
+    return m_lowPowerModeNotifier->isLowPowerModeEnabled();
+}
+
+void Page::setLowPowerModeEnabledOverrideForTesting(std::optional<bool> isEnabled)
+{
+    m_lowPowerModeEnabledOverrideForTesting = isEnabled;
+    handleLowModePowerChange(m_lowPowerModeEnabledOverrideForTesting.value_or(false));
+}
+
 void Page::setTopContentInset(float contentInset)
 {
     if (m_topContentInset == contentInset)
@@ -1094,14 +1111,34 @@ void Page::resumeScriptedAnimations()
     }
 }
 
-void Page::setIsVisuallyIdleInternal(bool isVisuallyIdle)
+enum class ThrottlingReasonOperation { Add, Remove };
+static void updateScriptedAnimationsThrottlingReason(Page& page, ThrottlingReasonOperation operation, ScriptedAnimationController::ThrottlingReason reason)
 {
-    for (Frame* frame = &mainFrame(); frame; frame = frame->tree().traverseNext()) {
-        if (frame->document())
-            frame->document()->scriptedAnimationControllerSetThrottled(isVisuallyIdle);
+    for (Frame* frame = &page.mainFrame(); frame; frame = frame->tree().traverseNext()) {
+        auto* document = frame->document();
+        if (!document)
+            continue;
+        auto* scriptedAnimationController = document->scriptedAnimationController();
+        if (!scriptedAnimationController)
+            continue;
+
+        if (operation == ThrottlingReasonOperation::Add)
+            scriptedAnimationController->addThrottlingReason(reason);
+        else
+            scriptedAnimationController->removeThrottlingReason(reason);
     }
 }
 
+void Page::setIsVisuallyIdleInternal(bool isVisuallyIdle)
+{
+    updateScriptedAnimationsThrottlingReason(*this, isVisuallyIdle ? ThrottlingReasonOperation::Add : ThrottlingReasonOperation::Remove, ScriptedAnimationController::ThrottlingReason::VisuallyIdle);
+}
+
+void Page::handleLowModePowerChange(bool isLowPowerModeEnabled)
+{
+    updateScriptedAnimationsThrottlingReason(*this, isLowPowerModeEnabled ? ThrottlingReasonOperation::Add : ThrottlingReasonOperation::Remove, ScriptedAnimationController::ThrottlingReason::LowPowerMode);
+}
+
 void Page::userStyleSheetLocationChanged()
 {
     // FIXME: Eventually we will move to a model of just being handed the sheet
index 73ee269..938a096 100644 (file)
@@ -99,6 +99,7 @@ class UserInputBridge;
 class InspectorClient;
 class InspectorController;
 class LibWebRTCProvider;
+class LowPowerModeNotifier;
 class MainFrame;
 class MediaCanStartListener;
 class MediaPlaybackTarget;
@@ -576,6 +577,9 @@ public:
     WEBCORE_EXPORT bool hasSelectionAtPosition(const FloatPoint&) const;
 #endif
 
+    bool isLowPowerModeEnabled() const;
+    WEBCORE_EXPORT void setLowPowerModeEnabledOverrideForTesting(std::optional<bool>);
+
 private:
     WEBCORE_EXPORT void initGroup();
 
@@ -598,6 +602,8 @@ private:
 
     Vector<Ref<PluginViewBase>> pluginViews();
 
+    void handleLowModePowerChange(bool);
+
     enum class TimerThrottlingState { Disabled, Enabled, EnabledIncreasing };
     void hiddenPageDOMTimerThrottlingStateChanged();
     void setTimerThrottlingState(TimerThrottlingState);
@@ -780,6 +786,8 @@ private:
     std::optional<EventThrottlingBehavior> m_eventThrottlingBehaviorOverride;
 
     std::unique_ptr<PerformanceMonitor> m_performanceMonitor;
+    std::unique_ptr<LowPowerModeNotifier> m_lowPowerModeNotifier;
+    std::optional<bool> m_lowPowerModeEnabledOverrideForTesting;
 
     bool m_isRunningUserScripts { false };
 };
index d69b2c9..21e74f3 100644 (file)
@@ -436,6 +436,7 @@ void Internals::resetToConsistentState(Page& page)
 #endif
 
     page.setShowAllPlugins(false);
+    page.setLowPowerModeEnabledOverrideForTesting(std::nullopt);
 
 #if USE(QUICK_LOOK)
     MockQuickLookHandleClient::singleton().setPassword("");
@@ -1019,6 +1020,14 @@ bool Internals::isRequestAnimationFrameThrottled() const
     return scriptedAnimationController->isThrottled();
 }
 
+double Internals::requestAnimationFrameInterval() const
+{
+    auto* scriptedAnimationController = contextDocument()->scriptedAnimationController();
+    if (!scriptedAnimationController)
+        return INFINITY;
+    return scriptedAnimationController->interval().value();
+}
+
 bool Internals::areTimersThrottled() const
 {
     return contextDocument()->isTimerThrottlingEnabled();
@@ -1290,6 +1299,19 @@ void Internals::invalidateFontCache()
     FontCache::singleton().invalidate();
 }
 
+ExceptionOr<void> Internals::setLowPowerModeEnabled(bool isEnabled)
+{
+    auto* document = contextDocument();
+    if (!document)
+        return Exception { INVALID_ACCESS_ERR };
+    auto* page = document->page();
+    if (!page)
+        return Exception { INVALID_ACCESS_ERR };
+
+    page->setLowPowerModeEnabledOverrideForTesting(isEnabled);
+    return { };
+}
+
 ExceptionOr<void> Internals::setScrollViewPosition(int x, int y)
 {
     Document* document = contextDocument();
index 473a344..3dc45f6 100644 (file)
@@ -130,6 +130,7 @@ public:
     // DOMTimers throttling testing.
     ExceptionOr<bool> isTimerThrottled(int timeoutId);
     bool isRequestAnimationFrameThrottled() const;
+    double requestAnimationFrameInterval() const;
     bool areTimersThrottled() const;
 
     enum EventThrottlingBehavior { Responsive, Unresponsive };
@@ -174,6 +175,7 @@ public:
     ExceptionOr<void> setMarkedTextMatchesAreHighlighted(bool);
 
     void invalidateFontCache();
+    ExceptionOr<void> setLowPowerModeEnabled(bool);
 
     ExceptionOr<void> setScrollViewPosition(int x, int y);
     
index 8bbb9de..1024842 100644 (file)
@@ -350,6 +350,9 @@ enum EventThrottlingBehavior {
     boolean isRequestAnimationFrameThrottled();
     boolean areTimersThrottled();
 
+    [MayThrowException] void setLowPowerModeEnabled(boolean enabled);
+    readonly attribute double requestAnimationFrameInterval;
+
     // Override the behavior of WebPage::eventThrottlingDelay(), which only affects iOS.
     attribute EventThrottlingBehavior? eventThrottlingBehaviorOverride;
 
index 3c61229..5fa8936 100644 (file)
@@ -1,3 +1,16 @@
+2017-02-28  Chris Dumez  <cdumez@apple.com>
+
+        [iOS] Throttle requestAnimationFrame to 30fps in low power mode
+        https://bugs.webkit.org/show_bug.cgi?id=168837
+        <rdar://problem/30700929>
+
+        Reviewed by Simon Fraser.
+
+        Add unit test for -= operator on WTF::OptionSet.
+
+        * TestWebKitAPI/Tests/WTF/OptionSet.cpp:
+        (TestWebKitAPI::TEST):
+
 2017-02-28  Jonathan Bedard  <jbedard@apple.com>
 
         webkitpy: Regular expression for parsing simctl device information is wrong for iPad Pro
index f017383..7551794 100644 (file)
@@ -86,6 +86,18 @@ TEST(WTF_OptionSet, Minus)
     EXPECT_TRUE((set - set).isEmpty());
 }
 
+TEST(WTF_OptionSet, MinusEqual)
+{
+    OptionSet<ExampleFlags> set { ExampleFlags::A, ExampleFlags::B, ExampleFlags::C };
+
+    EXPECT_TRUE(((set -= ExampleFlags::A) == OptionSet<ExampleFlags> { ExampleFlags::B, ExampleFlags::C }));
+    EXPECT_TRUE((set == OptionSet<ExampleFlags> { ExampleFlags::B, ExampleFlags::C }));
+    EXPECT_TRUE(((set -= ExampleFlags::D) == OptionSet<ExampleFlags> { ExampleFlags::B, ExampleFlags::C }));
+    EXPECT_TRUE((set == OptionSet<ExampleFlags> { ExampleFlags::B, ExampleFlags::C }));
+    EXPECT_TRUE((set -= set).isEmpty());
+    EXPECT_TRUE(set.isEmpty());
+}
+
 TEST(WTF_OptionSet, ContainsTwoFlags)
 {
     OptionSet<ExampleFlags> set { ExampleFlags::A, ExampleFlags::B };