// Copyright 2017 The Chromium Authors. All rights reserved. // Use of this source code if governed by a BSD-style license that can be // found in LICENSE file. #include "base/numerics/safe_conversions.h" #include "base/test/scoped_feature_list.h" #include "base/time/time.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/public/common/features.h" #include "third_party/blink/public/platform/platform.h" #include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h" #include "third_party/blink/renderer/core/testing/sim/sim_request.h" #include "third_party/blink/renderer/core/testing/sim/sim_test.h" #include "third_party/blink/renderer/platform/scheduler/public/thread.h" #include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h" #include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/testing_platform_support_with_mock_scheduler.h" #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" using testing::AnyOf; using testing::ElementsAre; namespace blink { // When a page is backgrounded this is the absolute smallest amount of time // that can elapse between timer wake-ups. constexpr auto kDefaultThrottledWakeUpInterval = base::TimeDelta::FromSeconds(1); // This test suite relies on messages being posted to the console. In order to // be resilient against messages not posted by this specific test suite, a small // prefix is used to allowed filtering. constexpr char kTestConsoleMessagePrefix[] = "[ThrottlingTest]"; // A SimTest with mock time. class ThrottlingTestBase : public SimTest { public: ThrottlingTestBase() { platform_->SetAutoAdvanceNowToPendingTasks(false); // Align the time on a 1-minute interval, to simplify expectations. platform_->AdvanceClock( platform_->NowTicks().SnappedToNextTick( base::TimeTicks(), base::TimeDelta::FromMinutes(1)) - platform_->NowTicks()); } String BuildTimerConsoleMessage(String suffix = String()) { String message(kTestConsoleMessagePrefix); message = message + " Timer called"; if (!suffix.IsNull()) message + " " + suffix; return message; } // Returns a filtered copy of console messages where items not prefixed with // |kTestConsoleMessagePrefix| are removed. Vector FilteredConsoleMessages() { Vector result = ConsoleMessages(); result.erase( std::remove_if(result.begin(), result.end(), [](const String& element) { return !element.StartsWith(kTestConsoleMessagePrefix); }), result.end()); return result; } ScopedTestingPlatformSupport platform_; }; class DisableBackgroundThrottlingIsRespectedTest : public ThrottlingTestBase, private ScopedTimerThrottlingForBackgroundTabsForTest { public: DisableBackgroundThrottlingIsRespectedTest() : ScopedTimerThrottlingForBackgroundTabsForTest(false) {} }; TEST_F(DisableBackgroundThrottlingIsRespectedTest, DisableBackgroundThrottlingIsRespected) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete( String::Format("()", console_message.Utf8().c_str())); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); // Run delayed tasks for 1 second. All tasks should be completed // with throttling disabled. platform_->RunForPeriod(base::TimeDelta::FromSeconds(1)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(console_message, console_message, console_message, console_message, console_message)); } class BackgroundPageThrottlingTest : public ThrottlingTestBase {}; TEST_F(BackgroundPageThrottlingTest, TimersThrottledInBackgroundPage) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete( String::Format("()", console_message.Utf8().c_str())); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); // Make sure that we run no more than one task a second. platform_->RunForPeriod(base::TimeDelta::FromSeconds(3)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(console_message, console_message, console_message)); } // Verify the execution time of non-nested timers on a hidden page. // - setTimeout(..., 0) and setTimeout(..., -1) schedule their callback after // 1ms. The 1 ms delay exists for historical reasons crbug.com/402694. // - setTimeout(..., 5) schedules its callback at the next aligned time TEST_F(BackgroundPageThrottlingTest, WithoutNesting) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); String timeout_0_message = BuildTimerConsoleMessage("0"); String timeout_minus_1_message = BuildTimerConsoleMessage("-1"); String timeout_5_message = BuildTimerConsoleMessage("5"); main_resource.Complete(String::Format( "", timeout_0_message.Utf8().c_str(), timeout_minus_1_message.Utf8().c_str(), timeout_5_message.Utf8().c_str())); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1001)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(timeout_0_message, timeout_minus_1_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(998)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(timeout_0_message, timeout_minus_1_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(timeout_0_message, timeout_minus_1_message, timeout_5_message)); } // Verify that on a hidden page, a timer created with setTimeout(..., 0) is // throttled after 5 nesting levels. TEST_F(BackgroundPageThrottlingTest, NestedSetTimeoutZero) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete( String::Format("", console_message.Utf8().c_str())); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(1, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(2, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(3, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(4, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(995)); EXPECT_THAT(FilteredConsoleMessages(), Vector(4, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(5, console_message)); } // Verify that in a hidden page, a timer created with setInterval(..., 0) is // throttled after 5 nesting levels. TEST_F(BackgroundPageThrottlingTest, NestedSetIntervalZero) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete( String::Format("", console_message.Utf8().c_str())); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(1, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(2, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(3, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(4, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(995)); EXPECT_THAT(FilteredConsoleMessages(), Vector(4, console_message)); platform_->RunForPeriod(base::TimeDelta::FromMilliseconds(1)); EXPECT_THAT(FilteredConsoleMessages(), Vector(5, console_message)); } namespace { class IntensiveWakeUpThrottlingTest : public ThrottlingTestBase { public: IntensiveWakeUpThrottlingTest() { scoped_feature_list_.InitWithFeatures( {features::kIntensiveWakeUpThrottling}, // Disable freezing because it hides the effect of intensive throttling. {features::kStopInBackground}); } // Expect a console message every second, for |num_1hz_messages| seconds. // Then, expect a console messages every minute. void ExpectRepeatingTimerConsoleMessages(int num_1hz_messages) { for (int i = 0; i < num_1hz_messages; ++i) { ConsoleMessages().clear(); platform_->RunForPeriod(base::TimeDelta::FromSeconds(1)); EXPECT_EQ(FilteredConsoleMessages().size(), 1U); } constexpr int kNumIterations = 3; for (int i = 0; i < kNumIterations; ++i) { ConsoleMessages().clear(); platform_->RunForPeriod(base::TimeDelta::FromSeconds(30)); // Task shouldn't execute earlier than expected. EXPECT_EQ(FilteredConsoleMessages().size(), 0U); platform_->RunForPeriod(base::TimeDelta::FromSeconds(30)); EXPECT_EQ(FilteredConsoleMessages().size(), 1U); } } void TestNoIntensiveThrottlingOnTitleOrFaviconUpdate( const String& console_message) { // The page does not attempt to run onTimer in the first 5 minutes. platform_->RunForPeriod(base::TimeDelta::FromMinutes(5)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre()); // onTimer() communicates in background and re-posts itself. The background // communication inhibits intensive wake up throttling for 3 seconds, which // allows the re-posted task to run after |kDefaultThrottledWakeUpInterval|. constexpr int kNumIterations = 3; for (int i = 0; i < kNumIterations; ++i) { platform_->RunForPeriod(kDefaultThrottledWakeUpInterval); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre(console_message)); ConsoleMessages().clear(); } } private: base::test::ScopedFeatureList scoped_feature_list_; }; // Use to install a function that does not actually communicate with the user. constexpr char kCommunicationNop[] = ""; // Use to install a function that will communicate with the user via title // update. constexpr char kCommunicateThroughTitleScript[] = ""; // Use to install a function that will communicate with the user via favicon // update. constexpr char kCommunicateThroughFavisonScript[] = ""; // A script that schedules a timer task which logs to the console. The timer // task has a high nesting level and its timeout is not aligned on the intensive // wake up throttling interval. constexpr char kLongUnalignedTimerScriptTemplate[] = ""; // A time delta that matches the delay in the above script. constexpr base::TimeDelta kLongUnalignedTimerDelay = base::TimeDelta::FromSeconds(342); // Builds a page that waits 5 minutes and then creates a timer that reschedules // itself 50 times with 10 ms delay. The timer task logs |console_message| to // the console and invokes maybeCommunicateInBackground(). The caller must // provide the definition of maybeCommunicateInBackground() via // |communicate_script|. String BuildRepeatingTimerPage(const char* console_message, const char* communicate_script) { constexpr char kRepeatingTimerPageTemplate[] = "" "" " " "" "" "" "%s" // |communicate_script| inserted here "" ""; return String::Format(kRepeatingTimerPageTemplate, console_message, communicate_script); } } // namespace // Verify that a main frame timer that reposts itself with a 10 ms timeout runs // once every minute. TEST_F(IntensiveWakeUpThrottlingTest, MainFrameTimer_ShortTimeout) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); // Page does not communicate with the user. Normal intensive throttling // applies. main_resource.Complete(BuildRepeatingTimerPage( BuildTimerConsoleMessage().Utf8().c_str(), kCommunicationNop)); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); // No timer is scheduled in the 5 first minutes. platform_->RunForPeriod(base::TimeDelta::FromMinutes(5)); EXPECT_THAT(FilteredConsoleMessages(), ElementsAre()); // Expected execution: // // t = 5min 0s : afterFiveMinutes nesting=1 (low) // t = 5min 1s : onTimer nesting=2 (low) < // t = 5min 2s : onTimer nesting=3 (low) < 4 seconds at 1 Hz // t = 5min 3s : onTimer nesting=4 (low) < // t = 5min 4s : onTimer nesting=5 (high) ** < // t = 6min : onTimer nesting=6 (high) // t = 7min : onTimer nesting=7 (high) // ... // // ** In a main frame, a task with high nesting level is 1-second aligned // when no task with high nesting level ran in the last minute. ExpectRepeatingTimerConsoleMessages(4); } // Verify that a main frame timer that reposts itself with a 10 ms timeout runs // once every |kDefaultThrottledWakeUpInterval| after the first confirmed page // communication through title update. TEST_F(IntensiveWakeUpThrottlingTest, MainFrameTimer_ShortTimeout_TitleUpdate) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete(BuildRepeatingTimerPage( console_message.Utf8().c_str(), kCommunicateThroughTitleScript)); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); TestNoIntensiveThrottlingOnTitleOrFaviconUpdate(console_message); } // Verify that a main frame timer that reposts itself with a 10 ms timeout runs // once every |kDefaultThrottledWakeUpInterval| after the first confirmed page // communication through favicon update. TEST_F(IntensiveWakeUpThrottlingTest, MainFrameTimer_ShortTimeout_FaviconUpdate) { SimRequest main_resource("https://example.com/", "text/html"); LoadURL("https://example.com/"); const String console_message = BuildTimerConsoleMessage(); main_resource.Complete(BuildRepeatingTimerPage( console_message.Utf8().c_str(), kCommunicateThroughFavisonScript)); GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false); TestNoIntensiveThrottlingOnTitleOrFaviconUpdate(console_message); } // Verify that a same-origin subframe timer that reposts itself with a 10 ms // timeout runs once every minute. TEST_F(IntensiveWakeUpThrottlingTest, SameOriginSubFrameTimer_ShortTimeout) { SimRequest main_resource("https://example.com/", "text/html"); SimRequest subframe_resource("https://example.com/iframe.html", "text/html"); LoadURL("https://example.com/"); main_resource.Complete(R"(