blob: d32b99bb69b3ba95ac55a330cc814b8cbea0213e [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/display_lock/display_lock_budget.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_context.h"
#include "third_party/blink/renderer/core/display_lock/strict_yielding_display_lock_budget.h"
#include "third_party/blink/renderer/core/display_lock/unyielding_display_lock_budget.h"
#include "third_party/blink/renderer/core/display_lock/yielding_display_lock_budget.h"
#include "third_party/blink/renderer/core/html_names.h"
#include "third_party/blink/renderer/core/testing/core_unit_test_helper.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/testing/wtf/scoped_mock_clock.h"
namespace blink {
class DisplayLockBudgetTest : public RenderingTest {
public:
void SetUp() override {
RenderingTest::SetUp();
features_backup_.emplace();
RuntimeEnabledFeatures::SetDisplayLockingEnabled(true);
}
void TearDown() override {
if (features_backup_) {
features_backup_->Restore();
features_backup_.reset();
}
}
double GetBudgetMs(const YieldingDisplayLockBudget& budget) const {
return budget.GetCurrentBudgetMs();
}
private:
base::Optional<RuntimeEnabledFeatures::Backup> features_backup_;
};
TEST_F(DisplayLockBudgetTest, UnyieldingBudget) {
// Note that we're not testing the display lock here, just the budget so we
// can do minimal work to ensure we have a context, ignoring containment and
// other requirements.
SetBodyInnerHTML(R"HTML(
<div id="container"></div>
)HTML");
auto* element = GetDocument().getElementById("container");
{
auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame());
ScriptState::Scope scope(script_state);
element->getDisplayLockForBindings()->acquire(script_state, nullptr);
}
ASSERT_TRUE(element->GetDisplayLockContext());
UnyieldingDisplayLockBudget budget(element->GetDisplayLockContext());
// Since the lifecycle is clean, we don't actually need any updates.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
// Dirtying the element will cause us to do updates.
element->GetLayoutObject()->SetNeedsLayout("");
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
// Check everything twice since it shouldn't matter how many times we ask the
// unyielding budget, the results should always be the same.
for (int i = 0; i < 2; ++i) {
budget.WillStartLifecycleUpdate();
// Note that although we only dirtied layout, all phases "should" complete,
// since the budget should never be responsible for blocking phases for any
// reason other than we're out of budget.
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
budget.DidPerformPhase(DisplayLockBudget::Phase::kLayout);
budget.DidPerformPhase(DisplayLockBudget::Phase::kPrePaint);
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
}
}
TEST_F(DisplayLockBudgetTest, StrictYieldingBudget) {
// Note that we're not testing the display lock here, just the budget so we
// can do minimal work to ensure we have a context, ignoring containment and
// other requirements.
SetBodyInnerHTML(R"HTML(
<div id="container"></div>
)HTML");
auto* element = GetDocument().getElementById("container");
{
auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame());
ScriptState::Scope scope(script_state);
element->getDisplayLockForBindings()->acquire(script_state, nullptr);
}
ASSERT_TRUE(element->GetDisplayLockContext());
StrictYieldingDisplayLockBudget budget(element->GetDisplayLockContext());
// Since the lifecycle is clean, we don't actually need any updates.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
// Dirtying the element will cause us to do updates.
element->GetLayoutObject()->SetNeedsLayout("");
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
{
budget.WillStartLifecycleUpdate();
// Initially all of the phase checks should return true, since we don't know
// which phase the system wants to process next.
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Not doing anything should ensure that we schedule another animation by
// returning true here.
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
}
{
budget.WillStartLifecycleUpdate();
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Once we perform a phase, its check should remain true, but the rest
// will be false for this cycle.
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_FALSE(
budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// We would need at least one more run to finish everything.
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
}
{
budget.WillStartLifecycleUpdate();
// Run the previous block again, now everything will always return true
// since the phase we complete here (style) has already been completed
// before, and we are open to complete a new phase.
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Since we already befored style before, no new phase has been processed
// and all phases are allowed to finish.
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// We would need at least one more run to finish everything.
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
}
{
budget.WillStartLifecycleUpdate();
// On the next run, the checks for phases completed before should always
// return true, and as before since we haven't completed a new phase, the
// remainder of the phases should return true for now.
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// This check is the same as in the previous block, but is here to verify
// that going through NeedsLifecycleUpdates() and then
// WillStartLifecycleUpdate() again doesn't change the fact that we should
// still perform all of the phases at this point.
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Let's say layout was clean and we jumped and did prepaint instead, now
// every phase before and including prepaint should be true, the rest are
// locked from completing.
budget.DidPerformPhase(DisplayLockBudget::Phase::kPrePaint);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Note that since we processed everything, we no longer need lifecycle
// updates.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
}
{
// Do one more run to ensure everything is still returning true.
budget.WillStartLifecycleUpdate();
// On the last run, we'll complete all phases. Since there is only one
// remaining phase we haven't done, all of the checks should always return
// true (it's either an old phase or a first uncompleted phase).
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
budget.DidPerformPhase(DisplayLockBudget::Phase::kLayout);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
budget.DidPerformPhase(DisplayLockBudget::Phase::kPrePaint);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Since we completed everything, we should now be returning false here (no
// more updates needed).
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
}
}
TEST_F(DisplayLockBudgetTest,
StrictYieldingBudgetOnlyNeedsUpdatesForDirtyPhases) {
// Note that we're not testing the display lock here, just the budget so we
// can do minimal work to ensure we have a context, ignoring containment and
// other requirements.
SetBodyInnerHTML(R"HTML(
<div id="container"></div>
)HTML");
auto* element = GetDocument().getElementById("container");
{
auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame());
ScriptState::Scope scope(script_state);
element->getDisplayLockForBindings()->acquire(script_state, nullptr);
}
ASSERT_TRUE(element->GetDisplayLockContext());
StrictYieldingDisplayLockBudget budget(element->GetDisplayLockContext());
// Since the lifecycle is clean, we don't actually need any updates.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
// Dirtying the element will cause us to do updates.
element->GetLayoutObject()->SetNeedsLayout("");
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
// Cleaning the lifecycle phases makes the budget not want any more updates.
UpdateAllLifecyclePhasesForTest();
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
element->GetLayoutObject()->SetNeedsLayout("");
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
budget.WillStartLifecycleUpdate();
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
budget.DidPerformPhase(DisplayLockBudget::Phase::kLayout);
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
budget.WillStartLifecycleUpdate();
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
budget.DidPerformPhase(DisplayLockBudget::Phase::kPrePaint);
// Note that since the layout was indicated as done (from the budget
// perspective), it will no longer need updates even though the true layout is
// dirty. This is because it will no longer block layout from synchronously
// completing whenever necessary.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
}
TEST_F(DisplayLockBudgetTest, YieldingBudget) {
// Note that we're not testing the display lock here, just the budget so we
// can do minimal work to ensure we have a context, ignoring containment and
// other requirements.
SetBodyInnerHTML(R"HTML(
<div id="container"></div>
)HTML");
auto* element = GetDocument().getElementById("container");
{
auto* script_state = ToScriptStateForMainWorld(GetDocument().GetFrame());
ScriptState::Scope scope(script_state);
element->getDisplayLockForBindings()->acquire(script_state, nullptr);
}
ASSERT_TRUE(element->GetDisplayLockContext());
YieldingDisplayLockBudget budget(element->GetDisplayLockContext());
WTF::ScopedMockClock clock;
// Since the lifecycle is clean, we don't actually need any updates.
EXPECT_FALSE(budget.NeedsLifecycleUpdates());
// Dirtying the element will cause us to do updates.
element->GetLayoutObject()->SetNeedsLayout("");
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
budget.WillStartLifecycleUpdate();
// Initially all of the phase checks should return true, since we don't know
// which phase the system wants to process next.
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Not doing anything should ensure that we schedule another animation by
// returning true here.
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
// Advancing the clock a bit will make us still want to the phases.
clock.Advance(TimeDelta::FromMillisecondsD(GetBudgetMs(budget) / 2));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// However, once we're out of budget, we will not do anything.
clock.Advance(TimeDelta::FromMillisecondsD(GetBudgetMs(budget)));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Starting a new lifecycle will reset the budget.
budget.WillStartLifecycleUpdate();
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Performing a phase still keeps the rest of the phases available for work
// since we haven't advanced the clock.
budget.DidPerformPhase(DisplayLockBudget::Phase::kStyle);
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Now that we're out of budget, phases performed previously should remain
// true.
clock.Advance(TimeDelta::FromMillisecondsD(GetBudgetMs(budget) * 2));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Sanity check here: the element still needs layout.
EXPECT_TRUE(budget.NeedsLifecycleUpdates());
// Resetting the budget, and advnacing again should yield the same results as
// before.
budget.WillStartLifecycleUpdate();
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
clock.Advance(TimeDelta::FromMillisecondsD(GetBudgetMs(budget) * 2));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_FALSE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
// Eventually the budget becomes essentially infinite.
for (int i = 0; i < 60; ++i)
budget.WillStartLifecycleUpdate();
EXPECT_GT(GetBudgetMs(budget), 1e6);
for (int i = 0; i < 60; ++i) {
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kStyle));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kLayout));
EXPECT_TRUE(budget.ShouldPerformPhase(DisplayLockBudget::Phase::kPrePaint));
clock.Advance(TimeDelta::FromMillisecondsD(10000));
}
}
} // namespace blink