blob: 3bff32db4e91d0ea4d46de3ecc3e07d10829b22d [file] [log] [blame]
// 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/strings/stringprintf.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);
class DisableBackgroundThrottlingIsRespectedTest
: public SimTest,
private ScopedTimerThrottlingForBackgroundTabsForTest {
public:
DisableBackgroundThrottlingIsRespectedTest()
: ScopedTimerThrottlingForBackgroundTabsForTest(false) {}
void SetUp() override { SimTest::SetUp(); }
};
TEST_F(DisableBackgroundThrottlingIsRespectedTest,
DisableBackgroundThrottlingIsRespected) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(
"(<script>"
" function f(repetitions) {"
" if (repetitions == 0) return;"
" console.log('called f');"
" setTimeout(f, 10, repetitions - 1);"
" }"
" f(5);"
"</script>)");
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
// Run delayed tasks for 1 second. All tasks should be completed
// with throttling disabled.
test::RunDelayedTasks(base::TimeDelta::FromSeconds(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called f", "called f", "called f",
"called f", "called f"));
}
class BackgroundPageThrottlingTest : public SimTest {};
TEST_F(BackgroundPageThrottlingTest, BackgroundPagesAreThrottled) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(
"(<script>"
" function f(repetitions) {"
" if (repetitions == 0) return;"
" console.log('called f');"
" setTimeout(f, 10, repetitions - 1);"
" }"
" setTimeout(f, 10, 50);"
"</script>)");
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
// Make sure that we run no more than one task a second.
test::RunDelayedTasks(base::TimeDelta::FromMilliseconds(3000));
EXPECT_THAT(
ConsoleMessages(),
AnyOf(ElementsAre("called f", "called f", "called f"),
ElementsAre("called f", "called f", "called f", "called f")));
}
class IntensiveWakeUpThrottlingTest : public SimTest {
public:
IntensiveWakeUpThrottlingTest() {
scoped_feature_list_.InitWithFeatures(
{features::kIntensiveWakeUpThrottling},
// Disable freezing because it hides the effect of intensive throttling.
{features::kStopInBackground});
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());
}
void TestNoIntensiveThrotlingOnTitleOrFaviconUpdate() {
// The page does not attempt to run onTimer in the first 5 minutes.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(5));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
// At 5 minutes, a timer fires to run the afterFiveMinutes() function.
// This function does not communicate in the background, so the intensive
// throttling policy applies and onTimer() can only run after 1 minute.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
ConsoleMessages().clear();
// Beyond this point intensive background throttling will not apply anymore
// since the page is communicating in the background from onTimer().
constexpr auto kTimeUntilNextCheck = base::TimeDelta::FromSeconds(30);
platform_->RunForPeriod(kTimeUntilNextCheck);
// Tasks are not throttled beyond the default background throttling behavior
// nor do they get to run more often.
Vector<String> expected_ouput(
kTimeUntilNextCheck / kDefaultThrottledWakeUpInterval,
"called onTimer");
EXPECT_THAT(ConsoleMessages(), expected_ouput);
}
ScopedTestingPlatformSupport<TestingPlatformSupportWithMockScheduler>
platform_;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
namespace {
// Use to install a function that does not actually communicate with the user.
constexpr char kCommunicationNop[] =
"<script>"
" function maybeCommunicateInBackground() { "
" return; "
" }"
"</script>";
// Use to install a function that will communicate with the user via title
// update.
constexpr char kCommunicateThroughTitleScript[] =
"<script>"
" function maybeCommunicateInBackground() { "
" document.title += \"A\";"
" }"
"</script>";
// Use to install a function that will communicate with the user via favicon
// update.
constexpr char kCommunicateThroughFavisonScript[] =
"<script>"
" function maybeCommunicateInBackground() { "
" document.querySelector(\"link[rel*='icon']\").href = \"favicon.ico\";"
" }"
"</script>";
// A script that schedules a timer with a long delay that is not aligned on the
// intensive throttling wake up interval.
constexpr char kLongUnalignedTimerScript[] =
"<script>"
" function onTimer() {"
" console.log('called onTimer');"
" }"
" setTimeout(onTimer, 342 * 1000);"
"</script>";
// A time delta that matches the delay in the above script.
constexpr base::TimeDelta kLongUnalignedTimerDelay =
base::TimeDelta::FromSeconds(342);
// Use to build a web-page ready to test intensive javascript throttling.
// The page will differ in its definition of the maybeCommunicateInBackground()
// function which has to be defined in a script passed in |communicate_script|.
String BuildRepeatingTimerPage(const char* communicate_script) {
// A template for a page that waits 5 minutes on load then creates a timer
// that reschedules itself 50 times with 10 ms delay. Contains the minimimal
// page structure to simulate background communication with the user via title
// or favicon update. Needs to be augmented with a definition for
// maybeCommunicateInBackground;
constexpr char kRepeatingTimerPageTemplate[] =
"<html>"
"<head>"
" <link rel='icon' href='http://www.foobar.com/favicon.ico'>"
"</head>"
"<body>"
"<script>"
" function onTimer(repetitions) {"
" if (repetitions == 0) return;"
" console.log('called onTimer');"
" maybeCommunicateInBackground();"
" setTimeout(onTimer, 10, repetitions - 1);"
" }"
" function afterFiveMinutes() {"
" setTimeout(onTimer, 10, 50);"
" }"
" setTimeout(afterFiveMinutes, 5 * 60 * 1000);"
"</script>"
"%s" // maybeCommunicateInBackground definition inserted here.
"</body>"
"</html>";
std::string page =
base::StringPrintf(kRepeatingTimerPageTemplate, communicate_script);
return {page.data(), page.size()};
}
} // 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(kCommunicationNop));
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
// No timer is scheduled in the 5 first minutes.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(5));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
// After that, intensive throttling starts and there should be 1 wake up per
// minute.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
// No tasks execute early.
platform_->RunForPeriod(base::TimeDelta::FromSeconds(30));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
// A minute after the last timer.
platform_->RunForPeriod(base::TimeDelta::FromSeconds(30));
EXPECT_THAT(ConsoleMessages(),
ElementsAre("called onTimer", "called onTimer"));
}
// 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/");
main_resource.Complete(
BuildRepeatingTimerPage(kCommunicateThroughTitleScript));
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
TestNoIntensiveThrotlingOnTitleOrFaviconUpdate();
}
// 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/");
main_resource.Complete(
BuildRepeatingTimerPage(kCommunicateThroughFavisonScript));
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
TestNoIntensiveThrotlingOnTitleOrFaviconUpdate();
}
// 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"(<iframe src="https://example.com/iframe.html" />)");
// Run tasks to let the main frame request the iframe resource. It is not
// possible to complete the iframe resource request before that.
platform_->RunUntilIdle();
subframe_resource.Complete(BuildRepeatingTimerPage(kCommunicationNop));
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
// No timer is scheduled in the 5 first minutes.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(5));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
// After that, intensive throttling starts and there should be 1 wake up per
// minute.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(),
ElementsAre("called onTimer", "called onTimer"));
}
// Verify that a cross-origin subframe timer that reposts itself with a 10 ms
// timeout runs once every minute.
TEST_F(IntensiveWakeUpThrottlingTest, CrossOriginSubFrameTimer_ShortTimeout) {
SimRequest main_resource("https://example.com/", "text/html");
SimRequest subframe_resource("https://cross-origin.example.com/iframe.html",
"text/html");
LoadURL("https://example.com/");
main_resource.Complete(
R"(<iframe src="https://cross-origin.example.com/iframe.html" />)");
// Run tasks to let the main frame request the iframe resource. It is not
// possible to complete the iframe resource request before that.
platform_->RunUntilIdle();
subframe_resource.Complete(BuildRepeatingTimerPage(kCommunicationNop));
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
// No timer is scheduled in the 5 first minutes.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(5));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
// After that, intensive throttling starts and there should be 1 wake up per
// minute.
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
platform_->RunForPeriod(base::TimeDelta::FromMinutes(1));
EXPECT_THAT(ConsoleMessages(),
ElementsAre("called onTimer", "called onTimer"));
}
// Verify that a main frame timer with a long timeout runs at the desired run
// time when there is no other recent timer wake up.
TEST_F(IntensiveWakeUpThrottlingTest, MainFrameTimer_LongUnalignedTimeout) {
SimRequest main_resource("https://example.com/", "text/html");
LoadURL("https://example.com/");
main_resource.Complete(kLongUnalignedTimerScript);
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
platform_->RunForPeriod(kLongUnalignedTimerDelay -
base::TimeDelta::FromSeconds(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
platform_->RunForPeriod(base::TimeDelta::FromSeconds(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
}
// Verify that a same-origin subframe timer with a long timeout runs at the
// desired run time when there is no other recent timer wake up.
TEST_F(IntensiveWakeUpThrottlingTest,
SameOriginSubFrameTimer_LongUnalignedTimeout) {
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"(<iframe src="https://example.com/iframe.html" />)");
// Run tasks to let the main frame request the iframe resource. It is not
// possible to complete the iframe resource request before that.
platform_->RunUntilIdle();
subframe_resource.Complete(kLongUnalignedTimerScript);
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
platform_->RunForPeriod(kLongUnalignedTimerDelay -
base::TimeDelta::FromSeconds(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
platform_->RunForPeriod(base::TimeDelta::FromSeconds(1));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
}
// Verify that a cross-origin subframe timer with a long timeout runs at an
// aligned time, even when there is no other recent timer wake up (in a
// same-origin frame, it would have run at the desired time).
TEST_F(IntensiveWakeUpThrottlingTest,
CrossOriginSubFrameTimer_LongUnalignedTimeout) {
SimRequest main_resource("https://example.com/", "text/html");
SimRequest subframe_resource("https://cross-origin.example.com/iframe.html",
"text/html");
LoadURL("https://example.com/");
main_resource.Complete(
R"(<iframe src="https://cross-origin.example.com/iframe.html" />)");
// Run tasks to let the main frame request the iframe resource. It is not
// possible to complete the iframe resource request before that.
platform_->RunUntilIdle();
subframe_resource.Complete(kLongUnalignedTimerScript);
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
platform_->RunForPeriod(base::TimeDelta::FromSeconds(342));
EXPECT_THAT(ConsoleMessages(), ElementsAre());
// Fast-forward to the next aligned time.
platform_->RunForPeriod(base::TimeDelta::FromSeconds(18));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
}
// Verify that if both the main frame and a cross-origin frame schedule a timer
// with a long unaligned delay, the main frame timer runs at the desired time
// (because there was no recent same-origin wake up) while the cross-origin
// timer runs at an aligned time.
TEST_F(IntensiveWakeUpThrottlingTest,
MainFrameAndCrossOriginSubFrameTimer_LongUnalignedTimeout) {
SimRequest main_resource("https://example.com/", "text/html");
SimRequest subframe_resource("https://cross-origin.example.com/iframe.html",
"text/html");
LoadURL("https://example.com/");
main_resource.Complete(
WTF::String(kLongUnalignedTimerScript) +
"<iframe src=\"https://cross-origin.example.com/iframe.html\" />");
// Run tasks to let the main frame request the iframe resource. It is not
// possible to complete the iframe resource request before that.
platform_->RunUntilIdle();
subframe_resource.Complete(kLongUnalignedTimerScript);
GetDocument().GetPage()->GetPageScheduler()->SetPageVisible(false);
platform_->RunForPeriod(base::TimeDelta::FromSeconds(342));
EXPECT_THAT(ConsoleMessages(), ElementsAre("called onTimer"));
// Fast-forward to the next aligned time.
platform_->RunForPeriod(base::TimeDelta::FromSeconds(18));
EXPECT_THAT(ConsoleMessages(),
ElementsAre("called onTimer", "called onTimer"));
}
} // namespace blink