Commit 01d665cb authored by Francois Doray's avatar Francois Doray Committed by Chromium LUCI CQ

[PM] Mark a page as loading as soon as "DidStartLoading" is invoked.

Today, PageNode::IsLoading() transitions to "true" when a response is
received for a main frame different-document navigation
(WebContentsObserver::DidReceiveResponse()). It transitions to "false"
when the load has stopped (WebContentsObserver::DidStopLoading()) and
the page has been idling for a short timeout. This implies that
PageNode::IsLoading() is "false" between when a navigation starts and
when the initial response is received.

We want to build a policy that preempts BEST_EFFORT tasks when any
page is loading. If we use PageNode::IsLoading() to build that policy,
BEST_EFFORT tasks will still be able to execute between when a
navigation starts and when the initial response is received. This is
undesirable, because BEST_EFFORT tasks could still interfere with
navigation.

This CL solves the issue by replacing PageNode::IsLoading() with
PageNode::GetLoadingState(). This method returns an enum:

- kLoadingNotStarted: No top-level document has started loading yet.
- kLoading: A different top-level document is loading. This correspond
to WebContents::IsLoadingToDifferentDocument().
- kLoadedBusy: A different top-level document finished loading, but the
page did not  reach CPU and network quiescence since then.
- kLoadedIdle: The page reached CPU and network quiescence after loading
the current top-level document, or the load failed.

In a different CL, we will preempt BEST_EFFORT tasks when any page
is kLoading or kLoadedBusy.

Bug: 887407
Change-Id: Ie88a56457309aa86221f1ea31f7ceda731547b07
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2300400
Commit-Queue: François Doray <fdoray@chromium.org>
Reviewed-by: default avatarJoe Mason <joenotcharles@chromium.org>
Cr-Commit-Position: refs/heads/master@{#832480}
parent 1acc268a
......@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/process/process.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "chrome/browser/ui/browser.h"
......@@ -11,30 +12,39 @@
#include "components/performance_manager/performance_manager_impl.h"
#include "components/performance_manager/public/performance_manager.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace performance_manager {
namespace {
// A class that waits for the IsLoading() property of a PageNode to transition
// to a desired value. Generates an error if the first IsLoading() transitions
// is not for the observed PageNode. Ignores IsLoading() transitions after the
// first one.
class PageIsLoadingObserver : public PageNode::ObserverDefaultImpl,
public GraphOwnedDefaultImpl {
using ::testing::AnyOf;
using ::testing::ElementsAre;
// A class that waits for the GetLoadingState() property of a PageNode to
// transition to LoadingState::kLoadedIdle. Collects all intermediate states
// observed in-between. Generates an error if transitions are observed for
// another PageNode than |page_node|.
class PageLoadingStateObserver : public PageNode::ObserverDefaultImpl,
public GraphOwnedDefaultImpl {
public:
PageIsLoadingObserver(base::WeakPtr<PageNode> page_node,
bool desired_is_loading)
: page_node_(page_node), desired_is_loading_(desired_is_loading) {
PageLoadingStateObserver(base::WeakPtr<PageNode> page_node,
bool exit_if_already_loaded_idle)
: page_node_(page_node) {
DCHECK(PerformanceManagerImpl::IsAvailable());
PerformanceManagerImpl::CallOnGraphImpl(
FROM_HERE,
base::BindLambdaForTesting([&](performance_manager::GraphImpl* graph) {
// |exit_if_already_loaded_idle| is captured by copy because the lambda
// can be executed after the constructor returns.
base::BindLambdaForTesting([&, exit_if_already_loaded_idle](
performance_manager::GraphImpl* graph) {
EXPECT_TRUE(page_node_);
if (page_node_->IsLoading() == desired_is_loading_) {
run_loop_.Quit();
if (exit_if_already_loaded_idle &&
page_node_->GetLoadingState() ==
PageNode::LoadingState::kLoadedIdle) {
QuitRunLoop();
} else {
graph_ = graph;
graph_->AddPageNodeObserver(this);
......@@ -42,32 +52,49 @@ class PageIsLoadingObserver : public PageNode::ObserverDefaultImpl,
}));
}
~PageIsLoadingObserver() override = default;
~PageLoadingStateObserver() override = default;
void Wait() {
// The RunLoop is quit when |page_node_->IsLoading()| becomes equal to
// |desired_is_loading_|.
run_loop_.Run();
}
// Returns loading states observed before reaching LoadingState::kLoadedIdle.
// Can only be accessed safely after Wait() has returned.
const std::vector<PageNode::LoadingState>& observed_loading_states() const {
// If |graph_| is not nullptr, the RunLoop wasn't quit and accessing
// |observed_loading_states_| would be racy.
DCHECK(!graph_);
return observed_loading_states_;
}
private:
void QuitRunLoop() {
graph_ = nullptr;
run_loop_.Quit();
}
// PageNodeObserver:
void OnIsLoadingChanged(const PageNode* page_node) override {
void OnLoadingStateChanged(const PageNode* page_node) override {
EXPECT_EQ(page_node_.get(), page_node);
EXPECT_EQ(page_node->IsLoading(), desired_is_loading_);
graph_->RemovePageNodeObserver(this);
run_loop_.Quit();
if (page_node->GetLoadingState() == PageNode::LoadingState::kLoadedIdle) {
graph_->RemovePageNodeObserver(this);
QuitRunLoop();
return;
}
observed_loading_states_.push_back(page_node->GetLoadingState());
}
// This RunLoop is quit when |page_node_->IsLoading()| is equal to
// |desired_is_loading_|.
// This RunLoop is quit when |page_node_->GetLoadingState()| is equal to
// LoadingState::kLoadedIdle.
base::RunLoop run_loop_;
// The watched PageNode.
const base::WeakPtr<PageNode> page_node_;
// Desired value for |page_node_->IsLoading()|.
const bool desired_is_loading_;
// Observed states before reaching kLoadedIdle.
std::vector<PageNode::LoadingState> observed_loading_states_;
// Set when registering |this| as a PageNodeObserver. Used to unregister.
GraphImpl* graph_ = nullptr;
......@@ -82,35 +109,46 @@ class PageLoadTrackerDecoratorTest : public InProcessBrowserTest {
};
// Integration test verifying that everything is hooked up in Chrome to update
// PageNode::IsLoading() is updated on navigation. See
// PageNode::GetLoadingState() is updated on navigation. See
// PageLoadTrackerDecoratorTest for low level unit tests.
IN_PROC_BROWSER_TEST_F(PageLoadTrackerDecoratorTest, PageNodeIsLoading) {
IN_PROC_BROWSER_TEST_F(PageLoadTrackerDecoratorTest, PageNodeLoadingState) {
ASSERT_TRUE(embedded_test_server()->Start());
base::WeakPtr<PageNode> page_node =
PerformanceManager::GetPageNodeForWebContents(
browser()->tab_strip_model()->GetActiveWebContents());
// Wait until IsLoading() is false (the initial navigation may or may not be
// ongoing).
PageIsLoadingObserver observer1(page_node, false);
observer1.Wait();
// Wait until GetLoadingState() is LoadingState::kLoadedIdle (the initial
// navigation may or may not be ongoing).
{
PageLoadingStateObserver observer(page_node,
/* exit_if_already_loaded_idle=*/true);
observer.Wait();
}
// Create an Observer that will observe IsLoading() becoming true when the
// navigation below starts.
PageIsLoadingObserver observer2(page_node, true);
// Create an Observer that will observe GetLoadingState() becoming
// LoadingState::kLoadedIdle after the navigation below starts.
PageLoadingStateObserver observer(page_node,
/* exit_if_already_loaded_idle=*/false);
// Navigate.
browser()->OpenURL(content::OpenURLParams(
embedded_test_server()->GetURL("/empty.html"), content::Referrer(),
WindowOpenDisposition::CURRENT_TAB, ui::PAGE_TRANSITION_TYPED, false));
// Wait until IsLoading() is true.
observer2.Wait();
// Wait until IsLoading() is false.
PageIsLoadingObserver observer3(page_node, false);
observer3.Wait();
// Wait until GetLoadingState() transitions to LoadingState::kLoadedIdle.
observer.Wait();
// States observed before reaching LoadingState::kLoadedIdle must follow one
// of the two expected sequenced (state can go through |kLoadingTimedOut| or
// not).
EXPECT_THAT(observer.observed_loading_states(),
AnyOf(ElementsAre(PageNode::LoadingState::kLoading,
PageNode::LoadingState::kLoadedBusy),
ElementsAre(PageNode::LoadingState::kLoading,
PageNode::LoadingState::kLoadingTimedOut,
PageNode::LoadingState::kLoading,
PageNode::LoadingState::kLoadedBusy)));
}
} // namespace performance_manager
......@@ -98,27 +98,50 @@ void BackgroundTabLoadingPolicy::OnTakenFromGraph(Graph* graph) {
graph->RemovePageNodeObserver(this);
}
void BackgroundTabLoadingPolicy::OnIsLoadingChanged(const PageNode* page_node) {
if (!page_node->IsLoading()) {
// Once the PageNode finishes loading, stop tracking it within this policy.
RemovePageNode(page_node);
void BackgroundTabLoadingPolicy::OnLoadingStateChanged(
const PageNode* page_node) {
switch (page_node->GetLoadingState()) {
// Loading is complete or stalled.
case PageNode::LoadingState::kLoadingNotStarted:
case PageNode::LoadingState::kLoadedIdle:
case PageNode::LoadingState::kLoadingTimedOut:
// Since there is a free loading slot, load more tab if needed.
MaybeLoadSomeTabs();
return;
}
// The PageNode started loading, either because of this policy or because of
// external factors (e.g. user-initiated). In either case, remove the PageNode
// from the set of PageNodes for which a load needs to be initiated and from
// the set of PageNodes for which a load has been initiated but hasn't
// started.
ErasePageNodeToLoadData(page_node);
base::Erase(page_nodes_load_initiated_, page_node);
{
// Stop tracking the page within this policy.
RemovePageNode(page_node);
// Since there might be a free loading slot, attempt to load more tabs.
MaybeLoadSomeTabs();
return;
}
// Loading starts.
case PageNode::LoadingState::kLoading: {
// The PageNode started loading because of this policy or because of
// external factors (e.g. user-initiated). In either case, remove the
// PageNode from the set of PageNodes for which a load needs to be
// initiated and from the set of PageNodes for which a load has been
// initiated but hasn't started.
ErasePageNodeToLoadData(page_node);
base::Erase(page_nodes_load_initiated_, page_node);
// Keep track of all PageNodes that are loading, even when the load isn't
// initiated by this policy.
DCHECK(!base::Contains(page_nodes_loading_, page_node));
page_nodes_loading_.push_back(page_node);
return;
}
// Keep track of all PageNodes that are loading, even when the load isn't
// initiated by this policy.
DCHECK(!base::Contains(page_nodes_loading_, page_node));
page_nodes_loading_.push_back(page_node);
// Loading is progressing.
case PageNode::LoadingState::kLoadedBusy: {
// This PageNode should have been added to |page_nodes_loading_| when it
// transitioned to |kLoading|.
DCHECK(base::Contains(page_nodes_loading_, page_node));
return;
}
}
}
void BackgroundTabLoadingPolicy::OnBeforePageNodeRemoved(
......@@ -187,7 +210,7 @@ base::Value BackgroundTabLoadingPolicy::DescribePageNodeData(
const PageNode* node) const {
base::Value dict(base::Value::Type::DICTIONARY);
if (base::Contains(page_nodes_load_initiated_, node)) {
// Transient state between InitiateLoad() and OnIsLoadingChanged(),
// Transient state between InitiateLoad() and OnLoadingStateChanged(),
// shouldn't be sticking around for long.
dict.SetBoolKey("page_load_initiated", true);
}
......
......@@ -43,7 +43,7 @@ class BackgroundTabLoadingPolicy : public GraphOwned,
void OnTakenFromGraph(Graph* graph) override;
// PageNodeObserver implementation:
void OnIsLoadingChanged(const PageNode* page_node) override;
void OnLoadingStateChanged(const PageNode* page_node) override;
void OnBeforePageNodeRemoved(const PageNode* page_node) override;
// Schedules the PageNodes in |page_nodes| to be loaded when appropriate.
......
......@@ -117,14 +117,14 @@ TEST_F(BackgroundTabLoadingPolicyTest, AllLoadingSlotsUsed) {
testing::Mock::VerifyAndClear(loader());
// Simulate load start of a PageNode that initiated load.
page_node_impl->SetIsLoading(true);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
// The policy should allow one more PageNode to load after a PageNode finishes
// loading.
EXPECT_CALL(*loader(), LoadPageNode(raw_page_nodes[2]));
// Simulate load finish of a PageNode.
page_node_impl->SetIsLoading(false);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
}
TEST_F(BackgroundTabLoadingPolicyTest, ShouldLoad_MaxTabsToRestore) {
......@@ -256,28 +256,28 @@ TEST_F(BackgroundTabLoadingPolicyTest, ScoreAndScheduleTabLoad) {
PageNodeImpl* page_node_impl = page_nodes[1].get();
// Simulate load start of a PageNode that initiated load.
page_node_impl->SetIsLoading(true);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
// The policy should allow one more PageNode to load after a PageNode finishes
// loading.
EXPECT_CALL(*loader(), LoadPageNode(raw_page_nodes[0]));
// Simulate load finish of a PageNode.
page_node_impl->SetIsLoading(false);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
testing::Mock::VerifyAndClear(loader());
page_node_impl = page_nodes[0].get();
// Simulate load start of a PageNode that initiated load.
page_node_impl->SetIsLoading(true);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
// The policy should allow one more PageNode to load after a PageNode finishes
// loading.
EXPECT_CALL(*loader(), LoadPageNode(raw_page_nodes[2]));
// Simulate load finish of a PageNode.
page_node_impl->SetIsLoading(false);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
}
TEST_F(BackgroundTabLoadingPolicyTest, OnMemoryPressure) {
......@@ -314,11 +314,11 @@ TEST_F(BackgroundTabLoadingPolicyTest, OnMemoryPressure) {
PageNodeImpl* page_node_impl = page_nodes[0].get();
// Simulate load start of a PageNode that initiated load.
page_node_impl->SetIsLoading(true);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
// Simulate load finish of a PageNode and expect the policy to not start
// another load.
page_node_impl->SetIsLoading(false);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
}
} // namespace policies
......
......@@ -243,8 +243,10 @@ uint64_t UserspaceSwapPolicy::GetSwapDeviceFreeSpaceBytes() {
GetSwapDeviceFreeSpaceBytes();
}
bool UserspaceSwapPolicy::IsPageNodeLoading(const PageNode* page_node) {
return page_node->IsLoading();
bool UserspaceSwapPolicy::IsPageNodeLoadingOrBusy(const PageNode* page_node) {
const PageNode::LoadingState loading_state = page_node->GetLoadingState();
return loading_state == PageNode::LoadingState::kLoading ||
loading_state == PageNode::LoadingState::kLoadedBusy;
}
bool UserspaceSwapPolicy::IsPageNodeAudible(const PageNode* page_node) {
......@@ -290,7 +292,7 @@ bool UserspaceSwapPolicy::IsEligibleToSwap(const ProcessNode* process_node,
// it.
if (page_node) {
// If we're loading, audible, or visible we will not swap.
if (IsPageNodeLoading(page_node) || IsPageNodeVisible(page_node) ||
if (IsPageNodeLoadingOrBusy(page_node) || IsPageNodeVisible(page_node) ||
IsPageNodeAudible(page_node)) {
return false;
}
......
......@@ -67,7 +67,7 @@ class UserspaceSwapPolicy : public GraphOwned,
const ProcessNode* process_node);
virtual bool IsPageNodeVisible(const PageNode* page_node);
virtual bool IsPageNodeAudible(const PageNode* page_node);
virtual bool IsPageNodeLoading(const PageNode* page_node);
virtual bool IsPageNodeLoadingOrBusy(const PageNode* page_node);
virtual base::TimeDelta GetTimeSinceLastVisibilityChange(
const PageNode* page_node);
......
......@@ -35,10 +35,10 @@ TabManager::ResourceCoordinatorSignalObserver::
TabManager::ResourceCoordinatorSignalObserver::
~ResourceCoordinatorSignalObserver() = default;
void TabManager::ResourceCoordinatorSignalObserver::OnIsLoadingChanged(
void TabManager::ResourceCoordinatorSignalObserver::OnLoadingStateChanged(
const PageNode* page_node) {
// Forward the notification over to the UI thread when the page stops loading.
if (!page_node->IsLoading()) {
if (page_node->GetLoadingState() == PageNode::LoadingState::kLoadedIdle) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(&OnPageStoppedLoadingOnUi,
page_node->GetContentsProxy()));
......
......@@ -34,7 +34,7 @@ class TabManager::ResourceCoordinatorSignalObserver
// PageNode::ObserverDefaultImpl:
// This function run on the performance manager sequence.
void OnIsLoadingChanged(const PageNode* page_node) override;
void OnLoadingStateChanged(const PageNode* page_node) override;
// GraphOwned implementation:
void OnPassedToGraph(Graph* graph) override;
......
......@@ -124,7 +124,7 @@ class DiscardsGraphDumpImpl : public discards::mojom::GraphDump,
void OnIsAudibleChanged(
const performance_manager::PageNode* page_node) override {}
// Ignored.
void OnIsLoadingChanged(
void OnLoadingStateChanged(
const performance_manager::PageNode* page_node) override {}
// Ignored.
void OnUkmSourceIdChanged(
......
......@@ -50,12 +50,19 @@ class PageLoadTrackerDecorator : public FrameNode::ObserverDefaultImpl,
// Invoked by PageLoadTrackerDecoratorHelper when corresponding
// WebContentsObserver methods are invoked, and the WebContents is loading to
// a different document.
static void DidStartLoading(PageNodeImpl* page_node);
static void DidReceiveResponse(PageNodeImpl* page_node);
static void DidStopLoading(PageNodeImpl* page_node);
protected:
friend class PageLoadTrackerDecoratorTest;
// The amount of time after which a page transitions from
// kLoadingWaitingForResponse to kLoadingWaitingForResponseTimedOut if it
// hasn't received a response.
static constexpr base::TimeDelta kWaitingForResponseTimeout =
base::TimeDelta::FromSeconds(5);
// The amount of time a page has to be idle post-loading in order for it to be
// considered loaded and idle. This is used in UpdateLoadIdleState
// transitions.
......@@ -88,6 +95,13 @@ class PageLoadTrackerDecorator : public FrameNode::ObserverDefaultImpl,
void UpdateLoadIdleStateProcess(ProcessNodeImpl* process_node);
static void UpdateLoadIdleStatePage(PageNodeImpl* page_node);
// Schedules a call to UpdateLoadIdleStatePage() for |page_node| after
// |delayed_run_time| - |now| has elapsed.
static void ScheduleDelayedUpdateLoadIdleStatePage(
PageNodeImpl* page_node,
base::TimeTicks now,
base::TimeTicks delayed_run_time);
// Helper function for transitioning to the final state.
static void TransitionToLoadedAndIdle(PageNodeImpl* page_node);
......@@ -99,25 +113,32 @@ class PageLoadTrackerDecorator : public FrameNode::ObserverDefaultImpl,
class PageLoadTrackerDecorator::Data {
public:
// The state transitions associated with a load. In general a page transitions
// through these states from top to bottom.
// The state transitions associated with a load. This is more granular than
// the publicly exposed PageNode::LoadingState, to provide the required
// details to implement state transitions.
enum class LoadIdleState {
// The initial state. Can only transition to kLoading from here.
kLoadingNotStarted,
// Loading started, but no data arrived yet. Can transition to
// kLoadingDidReceiveResponse, kLoadingWaitingForResponseTimedOut or
// kLoadedAndIdle from here.
kLoadingWaitingForResponse,
// Loading started and a timeout has elapsed, but no data arrived yet. Can
// transition to kLoadingDidReceiveResponse or kLoadedAndIdle from here.
kLoadingWaitingForResponseTimedOut,
// Incoming data has started to arrive for a load. Almost idle signals are
// ignored in this state. Can transition to kLoadedNotIdling and
// kLoadedAndIdling from here.
kLoading,
kLoadingDidReceiveResponse,
// Loading has completed, but the page has not started idling. Can only
// transition to kLoadedAndIdling from here.
kLoadedNotIdling,
// Loading has completed, and the page is idling. Can transition to
// kLoadedNotIdling or kLoadedAndIdle from here.
kLoadedAndIdling,
// Loading has completed and the page has been idling for sufficiently long.
// This is the final state. Once this state has been reached a signal will
// be emitted and no further state transitions will be tracked. Committing a
// new non-same document navigation can start the cycle over again.
// Loading has completed and the page has been idling for sufficiently long
// or encountered an error. This is the final state. Once this state has
// been reached a signal will be emitted and no further state transitions
// will be tracked. Committing a new non-same document navigation can start
// the cycle over again.
kLoadedAndIdle
};
......@@ -132,9 +153,19 @@ class PageLoadTrackerDecorator::Data {
// Returns the LoadIdleState for the page.
LoadIdleState load_idle_state() const { return load_idle_state_; }
// Whether there is an ongoing different-document load, i.e. DidStartLoading()
// was invoked but not DidStopLoading().
bool is_loading_ = false;
// Whether there is an ongoing different-document load for which data started
// arriving.
bool loading_received_response_ = false;
// arriving, i.e. both DidStartLoading() and DidReceiveResponse() were
// invoked but not DidStopLoading().
bool received_response_ = false;
// Marks the point in time when the state transitioned to
// kLoadingWaitingForResponse. This is used as the basis for the
// kWaitingForResponseTimeout.
base::TimeTicks loading_started_;
// Marks the point in time when the DidStopLoading signal was received,
// transitioning to kLoadedAndNotIdling or kLoadedAndIdling. This is used as
......@@ -142,18 +173,16 @@ class PageLoadTrackerDecorator::Data {
base::TimeTicks loading_stopped_;
// Marks the point in time when the last transition to kLoadedAndIdling
// occurred. Used for gating the transition to kLoadedAndIdle.
// occurred. This is used as the basis for the kLoadedAndIdlingTimeout.
base::TimeTicks idling_started_;
// A one-shot timer used for transitioning between kLoadedAndIdling and
// kLoadedAndIdle.
base::OneShotTimer idling_timer_;
// A one-shot timer used to transition state after a timeout.
base::OneShotTimer timer_;
private:
// Initially at kLoadingNotStarted. Transitions through the states via calls
// to UpdateLoadIdleState. Is reset to kLoadingNotStarted when a non-same
// document navigation is committed.
LoadIdleState load_idle_state_ = LoadIdleState::kLoadingNotStarted;
// Initially at kLoadingWaitingForResponse when a load starts. Transitions
// through the states via calls to UpdateLoadIdleState.
LoadIdleState load_idle_state_ = LoadIdleState::kLoadingWaitingForResponse;
};
} // namespace performance_manager
......
......@@ -55,11 +55,9 @@ class PageLoadTrackerDecoratorHelper::WebContentsObserver
}
outer_->first_web_contents_observer_ = this;
if (web_contents->IsLoadingToDifferentDocument() &&
!web_contents->IsWaitingForResponse()) {
// Simulate receiving the missed DidReceiveResponse() notification.
DidReceiveResponse();
}
// |web_contents| must not be loading when it starts being tracked by this
// observer. Otherwise, loading state wouldn't be tracked correctly.
DCHECK(!web_contents->IsLoadingToDifferentDocument());
}
WebContentsObserver(const WebContentsObserver&) = delete;
......@@ -70,6 +68,20 @@ class PageLoadTrackerDecoratorHelper::WebContentsObserver
}
// content::WebContentsObserver:
void DidStartLoading() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
DCHECK(web_contents()->IsLoading());
DCHECK_EQ(loading_state_, LoadingState::kNotLoading);
// Only observe top-level navigation to a different document.
if (!web_contents()->IsLoadingToDifferentDocument())
return;
loading_state_ = LoadingState::kLoadingWaitingForResponse;
NotifyPageLoadTrackerDecoratorOnPMSequence(
web_contents(), &PageLoadTrackerDecorator::DidStartLoading);
}
void DidReceiveResponse() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
......@@ -78,20 +90,22 @@ class PageLoadTrackerDecoratorHelper::WebContentsObserver
return;
DCHECK(web_contents()->IsLoading());
#if DCHECK_IS_ON()
DCHECK(!did_receive_response_);
did_receive_response_ = true;
#endif
DCHECK_EQ(loading_state_, LoadingState::kLoadingWaitingForResponse);
loading_state_ = LoadingState::kLoadingDidReceiveResponse;
NotifyPageLoadTrackerDecoratorOnPMSequence(
web_contents(), &PageLoadTrackerDecorator::DidReceiveResponse);
}
void DidStopLoading() override {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
#if DCHECK_IS_ON()
did_receive_response_ = false;
#endif
// The state can be |kNotLoading| if this isn't a top-level navigation to a
// different document.
if (loading_state_ == LoadingState::kNotLoading)
return;
loading_state_ = LoadingState::kNotLoading;
NotifyPageLoadTrackerDecoratorOnPMSequence(
web_contents(), &PageLoadTrackerDecorator::DidStopLoading);
}
......@@ -126,11 +140,23 @@ class PageLoadTrackerDecoratorHelper::WebContentsObserver
WebContentsObserver* prev_;
WebContentsObserver* next_;
#if DCHECK_IS_ON()
// Used to verify the invariant that DidReceiveResponse() cannot be called
// twice in a row without a DidStopLoading() in between.
bool did_receive_response_ = false;
#endif
enum class LoadingState {
// Initial state.
// DidStartLoading(): Transition to kLoadingWaitingForResponse.
// DidReceiveResponse(): Invalid from this state.
// DidStopLoading(): Invalid from this state.
kNotLoading,
// DidStartLoading(): Invalid from this state.
// DidReceiveResponse(): Transition to kLoadingDidReceiveResponse.
// DidStopLoading(): Transition to kNotLoading.
kLoadingWaitingForResponse,
// DidStartLoading(): Invalid from this state.
// DidReceiveResponse(): Invalid from this state.
// DidStopLoading(): Transition to kNotLoading.
kLoadingDidReceiveResponse,
};
LoadingState loading_state_ = LoadingState::kNotLoading;
SEQUENCE_CHECKER(sequence_checker_);
};
......
......@@ -38,6 +38,18 @@ class SiteDataAccess {
namespace {
bool IsLoadedIdle(PageNode::LoadingState loading_state) {
switch (loading_state) {
case PageNode::LoadingState::kLoadingNotStarted:
case PageNode::LoadingState::kLoadedBusy:
case PageNode::LoadingState::kLoading:
case PageNode::LoadingState::kLoadingTimedOut:
return false;
case PageNode::LoadingState::kLoadedIdle:
return true;
}
}
// NodeAttachedData used to adorn every page node with a SiteDataWriter.
class SiteDataNodeData : public NodeAttachedDataImpl<SiteDataNodeData>,
public SiteDataRecorder::Data {
......@@ -62,7 +74,7 @@ class SiteDataNodeData : public NodeAttachedDataImpl<SiteDataNodeData>,
// Functions called whenever one of the tracked properties changes.
void OnMainFrameUrlChanged(const GURL& url, bool page_is_visible);
void OnIsLoadingChanged(bool is_loading);
void OnIsLoadedIdleChanged(bool is_loaded_idle);
void OnIsVisibleChanged(bool is_visible);
void OnIsAudibleChanged(bool audible);
void OnTitleUpdated();
......@@ -111,9 +123,9 @@ class SiteDataNodeData : public NodeAttachedDataImpl<SiteDataNodeData>,
// The PageNode that owns this object.
const PageNodeImpl* page_node_ = nullptr;
// The time at which this tab switched to the loaded state, null if this tab
// is not currently loaded.
base::TimeTicks loaded_time_;
// The time at which this tab switched to LoadingState::kLoadedIdle, null if
// this tab is not currently in that state.
base::TimeTicks loaded_idle_time_;
std::unique_ptr<SiteDataWriter> writer_;
std::unique_ptr<SiteDataReader> reader_;
......@@ -140,22 +152,26 @@ void SiteDataNodeData::OnMainFrameUrlChanged(const GURL& url,
writer_ = data_cache_->GetWriterForOrigin(origin);
reader_ = data_cache_->GetReaderForOrigin(origin);
// The writer is assumed to be in an unloaded state by default, set the proper
// loading state if necessary.
if (!page_node_->is_loading())
OnIsLoadingChanged(false);
// The writer is assumed not to be LoadingState::kLoadedIdle at this point.
// Make adjustments if it is LoadingState::kLoadedIdle.
if (IsLoadedIdle(page_node_->loading_state()))
OnIsLoadedIdleChanged(true);
DCHECK_EQ(IsLoadedIdle(page_node_->loading_state()),
!loaded_idle_time_.is_null());
}
void SiteDataNodeData::OnIsLoadingChanged(bool is_loading) {
void SiteDataNodeData::OnIsLoadedIdleChanged(bool is_loaded_idle) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (!writer_)
return;
if (is_loading && !loaded_time_.is_null()) {
if (!is_loaded_idle && !loaded_idle_time_.is_null()) {
writer_->NotifySiteUnloaded(GetPageNodeVisibility());
loaded_time_ = base::TimeTicks();
} else if (!is_loading) {
loaded_idle_time_ = base::TimeTicks();
} else if (is_loaded_idle) {
writer_->NotifySiteLoaded(GetPageNodeVisibility());
loaded_time_ = base::TimeTicks::Now();
loaded_idle_time_ = base::TimeTicks::Now();
}
}
......@@ -164,9 +180,9 @@ void SiteDataNodeData::OnIsVisibleChanged(bool is_visible) {
if (!writer_)
return;
if (is_visible) {
writer_->NotifySiteForegrounded(!page_node_->is_loading());
writer_->NotifySiteForegrounded(IsLoadedIdle(page_node_->loading_state()));
} else {
writer_->NotifySiteBackgrounded(!page_node_->is_loading());
writer_->NotifySiteBackgrounded(IsLoadedIdle(page_node_->loading_state()));
}
}
......@@ -196,9 +212,10 @@ void SiteDataNodeData::OnFaviconUpdated() {
void SiteDataNodeData::Reset() {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
if (writer_ && !loaded_time_.is_null() && !page_node_->is_loading()) {
if (writer_ && !loaded_idle_time_.is_null() &&
IsLoadedIdle(page_node_->loading_state())) {
writer_->NotifySiteUnloaded(GetPageNodeVisibility());
loaded_time_ = base::TimeTicks();
loaded_idle_time_ = base::TimeTicks();
}
writer_.reset();
reader_.reset();
......@@ -211,7 +228,7 @@ bool SiteDataNodeData::ShouldIgnoreFeatureUsageEvent(FeatureType feature_type) {
return true;
// Ignore all features happening before the website gets fully loaded.
if (page_node_->is_loading())
if (!IsLoadedIdle(page_node_->loading_state()))
return true;
// Ignore events if the tab is not in background.
......@@ -220,8 +237,8 @@ bool SiteDataNodeData::ShouldIgnoreFeatureUsageEvent(FeatureType feature_type) {
if (feature_type == FeatureType::kTitleChange ||
feature_type == FeatureType::kFaviconChange) {
DCHECK(!loaded_time_.is_null());
if (base::TimeTicks::Now() - loaded_time_ <
DCHECK(!loaded_idle_time_.is_null());
if (base::TimeTicks::Now() - loaded_idle_time_ <
kTitleOrFaviconChangePostLoadGracePeriod) {
return true;
}
......@@ -292,10 +309,10 @@ void SiteDataRecorder::OnMainFrameUrlChanged(const PageNode* page_node) {
page_node->IsVisible());
}
void SiteDataRecorder::OnIsLoadingChanged(const PageNode* page_node) {
void SiteDataRecorder::OnLoadingStateChanged(const PageNode* page_node) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
auto* data = GetSiteDataNodeDataFromPageNode(page_node);
data->OnIsLoadingChanged(page_node->IsLoading());
data->OnIsLoadedIdleChanged(IsLoadedIdle(page_node->GetLoadingState()));
}
void SiteDataRecorder::OnIsVisibleChanged(const PageNode* page_node) {
......
......@@ -157,7 +157,7 @@ class SiteDataRecorderTest : public PerformanceManagerTestHarness {
auto* page_node_impl = PageNodeImpl::FromNode(page_node.get());
page_node_impl->SetIsAudible(false);
page_node_impl->SetIsVisible(false);
page_node_impl->SetIsLoading(true);
page_node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
}));
}
......@@ -242,7 +242,7 @@ TEST_F(SiteDataRecorderTest, FeatureEventsGetForwardedWhenInBackground) {
node_impl = PageNodeImpl::FromNode(page_node.get());
EXPECT_CALL(*mock_writer, NotifySiteLoaded(TabVisibility::kBackground));
node_impl->SetIsLoading(false);
node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
::testing::Mock::VerifyAndClear(mock_writer);
EXPECT_CALL(*mock_writer, NotifySiteForegrounded(true));
......@@ -373,11 +373,11 @@ TEST_F(SiteDataRecorderTest, LoadEvent) {
// Test that the load/unload events get forwarded to the writer.
EXPECT_CALL(*mock_writer, NotifySiteLoaded(TabVisibility::kBackground));
node_impl->SetIsLoading(false);
node_impl->SetLoadingState(PageNode::LoadingState::kLoadedIdle);
::testing::Mock::VerifyAndClear(mock_writer);
EXPECT_CALL(*mock_writer, NotifySiteUnloaded(TabVisibility::kBackground));
node_impl->SetIsLoading(true);
node_impl->SetLoadingState(PageNode::LoadingState::kLoading);
::testing::Mock::VerifyAndClear(mock_writer);
}));
}
......
......@@ -23,6 +23,23 @@ const char* PageNode::ToString(PageNode::OpenedType opened_type) {
NOTREACHED();
}
// static
const char* PageNode::ToString(PageNode::LoadingState loading_state) {
switch (loading_state) {
case LoadingState::kLoadingNotStarted:
return "kLoadingNotStated";
case LoadingState::kLoading:
return "kLoading";
case LoadingState::kLoadingTimedOut:
return "kLoadingTimedOut";
case LoadingState::kLoadedBusy:
return "kLoadedBusy";
case LoadingState::kLoadedIdle:
return "kLoadedIdle";
}
NOTREACHED();
}
PageNode::PageNode() = default;
PageNode::~PageNode() = default;
......
......@@ -68,8 +68,8 @@ void PageNodeImpl::RemoveFrame(base::PassKey<FrameNodeImpl>,
}
}
void PageNodeImpl::SetIsLoading(bool is_loading) {
is_loading_.SetAndMaybeNotify(this, is_loading);
void PageNodeImpl::SetLoadingState(LoadingState loading_state) {
loading_state_.SetAndMaybeNotify(this, loading_state);
}
void PageNodeImpl::SetIsVisible(bool is_visible) {
......@@ -176,9 +176,9 @@ bool PageNodeImpl::is_audible() const {
return is_audible_.value();
}
bool PageNodeImpl::is_loading() const {
PageNode::LoadingState PageNodeImpl::loading_state() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return is_loading_.value();
return loading_state_.value();
}
ukm::SourceId PageNodeImpl::ukm_source_id() const {
......@@ -358,9 +358,9 @@ bool PageNodeImpl::IsAudible() const {
return is_audible();
}
bool PageNodeImpl::IsLoading() const {
PageNode::LoadingState PageNodeImpl::GetLoadingState() const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return is_loading();
return loading_state();
}
ukm::SourceId PageNodeImpl::GetUkmSourceID() const {
......
......@@ -57,7 +57,7 @@ class PageNodeImpl
void SetIsVisible(bool is_visible);
void SetIsAudible(bool is_audible);
void SetIsLoading(bool is_loading);
void SetLoadingState(LoadingState loading_state);
void SetUkmSourceId(ukm::SourceId ukm_source_id);
void OnFaviconUpdated();
void OnTitleUpdated();
......@@ -87,7 +87,7 @@ class PageNodeImpl
OpenedType opened_type() const;
bool is_visible() const;
bool is_audible() const;
bool is_loading() const;
LoadingState loading_state() const;
ukm::SourceId ukm_source_id() const;
LifecycleState lifecycle_state() const;
bool is_holding_weblock() const;
......@@ -180,7 +180,7 @@ class PageNodeImpl
bool IsVisible() const override;
base::TimeDelta GetTimeSinceLastVisibilityChange() const override;
bool IsAudible() const override;
bool IsLoading() const override;
LoadingState GetLoadingState() const override;
ukm::SourceId GetUkmSourceID() const override;
LifecycleState GetLifecycleState() const override;
bool IsHoldingWebLock() const override;
......@@ -272,9 +272,10 @@ class PageNodeImpl
is_audible_{false};
// The loading state. This is driven by instrumentation in the browser
// process.
ObservedProperty::NotifiesOnlyOnChanges<bool,
&PageNodeObserver::OnIsLoadingChanged>
is_loading_{false};
ObservedProperty::NotifiesOnlyOnChanges<
LoadingState,
&PageNodeObserver::OnLoadingStateChanged>
loading_state_{LoadingState::kLoadingNotStarted};
// The UKM source ID associated with the URL of the main frame of this page.
ObservedProperty::NotifiesOnlyOnChanges<
ukm::SourceId,
......
......@@ -60,7 +60,9 @@ base::Value PageNodeImplDescriber::DescribePageNodeData(
page_node_impl->browser_context_id_);
result.SetBoolKey("is_visible", page_node_impl->is_visible_.value());
result.SetBoolKey("is_audible", page_node_impl->is_audible_.value());
result.SetBoolKey("is_loading", page_node_impl->is_loading_.value());
result.SetStringKey(
"loading_state",
PageNode::ToString(page_node_impl->loading_state_.value()));
result.SetStringKey(
"ukm_source_id",
base::NumberToString(page_node_impl->ukm_source_id_.value()));
......
......@@ -164,24 +164,26 @@ TEST_F(PageNodeImplTest, BrowserContextID) {
EXPECT_EQ(public_page_node->GetBrowserContextID(), kTestBrowserContextId);
}
TEST_F(PageNodeImplTest, IsLoading) {
TEST_F(PageNodeImplTest, LoadingState) {
MockSinglePageInSingleProcessGraph mock_graph(graph());
auto* page_node = mock_graph.page.get();
// This should be initialized to false.
EXPECT_FALSE(page_node->is_loading());
// This should start at kLoadingNotStarted.
EXPECT_EQ(PageNode::LoadingState::kLoadingNotStarted,
page_node->loading_state());
// Set to false and the property should stay false.
page_node->SetIsLoading(false);
EXPECT_FALSE(page_node->is_loading());
// Set to kLoadingNotStarted and the property should stay kLoadingNotStarted.
page_node->SetLoadingState(PageNode::LoadingState::kLoadingNotStarted);
EXPECT_EQ(PageNode::LoadingState::kLoadingNotStarted,
page_node->loading_state());
// Set to true and the property should read true.
page_node->SetIsLoading(true);
EXPECT_TRUE(page_node->is_loading());
// Set to kLoading and the property should switch to kLoading.
page_node->SetLoadingState(PageNode::LoadingState::kLoading);
EXPECT_EQ(PageNode::LoadingState::kLoading, page_node->loading_state());
// Set to false and the property should read false again.
page_node->SetIsLoading(false);
EXPECT_FALSE(page_node->is_loading());
// Set to kLoading again and the property should stay kLoading.
page_node->SetLoadingState(PageNode::LoadingState::kLoading);
EXPECT_EQ(PageNode::LoadingState::kLoading, page_node->loading_state());
}
TEST_F(PageNodeImplTest, HadFormInteractions) {
......@@ -228,7 +230,7 @@ class LenientMockObserver : public PageNodeImpl::Observer {
void(const PageNode*, const FrameNode*, OpenedType));
MOCK_METHOD1(OnIsVisibleChanged, void(const PageNode*));
MOCK_METHOD1(OnIsAudibleChanged, void(const PageNode*));
MOCK_METHOD1(OnIsLoadingChanged, void(const PageNode*));
MOCK_METHOD1(OnLoadingStateChanged, void(const PageNode*));
MOCK_METHOD1(OnUkmSourceIdChanged, void(const PageNode*));
MOCK_METHOD1(OnPageLifecycleStateChanged, void(const PageNode*));
MOCK_METHOD1(OnPageIsHoldingWebLockChanged, void(const PageNode*));
......@@ -284,9 +286,9 @@ TEST_F(PageNodeImplTest, ObserverWorks) {
page_node->SetIsAudible(true);
EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());
EXPECT_CALL(obs, OnIsLoadingChanged(_))
EXPECT_CALL(obs, OnLoadingStateChanged(_))
.WillOnce(Invoke(&obs, &MockObserver::SetNotifiedPageNode));
page_node->SetIsLoading(true);
page_node->SetLoadingState(PageNode::LoadingState::kLoading);
EXPECT_EQ(raw_page_node, obs.TakeNotifiedPageNode());
EXPECT_CALL(obs, OnUkmSourceIdChanged(_))
......@@ -349,7 +351,7 @@ TEST_F(PageNodeImplTest, PublicInterface) {
public_page_node->GetBrowserContextID());
EXPECT_EQ(page_node->is_visible(), public_page_node->IsVisible());
EXPECT_EQ(page_node->is_audible(), public_page_node->IsAudible());
EXPECT_EQ(page_node->is_loading(), public_page_node->IsLoading());
EXPECT_EQ(page_node->loading_state(), public_page_node->GetLoadingState());
EXPECT_EQ(page_node->ukm_source_id(), public_page_node->GetUkmSourceID());
EXPECT_EQ(page_node->lifecycle_state(),
public_page_node->GetLifecycleState());
......
......@@ -37,7 +37,7 @@ class SiteDataRecorder : public GraphOwned,
void OnPageNodeAdded(const PageNode* page_node) override;
void OnBeforePageNodeRemoved(const PageNode* page_node) override;
void OnMainFrameUrlChanged(const PageNode* page_node) override;
void OnIsLoadingChanged(const PageNode* page_node) override;
void OnLoadingStateChanged(const PageNode* page_node) override;
void OnIsVisibleChanged(const PageNode* page_node) override;
void OnIsAudibleChanged(const PageNode* page_node) override;
void OnTitleUpdated(const PageNode* page_node) override;
......
......@@ -51,6 +51,30 @@ class PageNode : public Node {
// Returns a string for a PageNode::OpenedType enumeration.
static const char* ToString(PageNode::OpenedType opened_type);
// Loading state of a page.
enum class LoadingState {
// No top-level document has started loading yet.
kLoadingNotStarted,
// A different top-level document is loading. The load started less than 5
// seconds ago or the initial response was received.
kLoading,
// A different top-level document is loading. The load started more than 5
// seconds ago and no response was received yet. Note: The state will
// transition back to |kLoading| if a response is received.
kLoadingTimedOut,
// A different top-level document finished loading, but the page did not
// reach CPU and network quiescence since then. Note: A page is considered
// to have reached CPU and network quiescence after 1 minute, even if the
// CPU and network are still busy - see page_load_tracker_decorator.h.
kLoadedBusy,
// The page reached CPU and network quiescence after loading the current
// top-level document, or the load failed.
kLoadedIdle,
};
// Returns a string for a PageNode::LoadingState enumeration.
static const char* ToString(PageNode::LoadingState loading_state);
PageNode();
~PageNode() override;
......@@ -77,13 +101,8 @@ class PageNode : public Node {
// See PageNodeObserver::OnIsAudibleChanged.
virtual bool IsAudible() const = 0;
// Returns true if this page is currently loading, false otherwise. The page
// starts loading when incoming data starts arriving for a top-level load to a
// different document. It stops loading when it reaches an "almost idle"
// state, based on CPU and network quiescence, or after an absolute timeout.
// Note: This is different from WebContents::IsLoading(). See
// PageNodeObserver::OnIsLoadingChanged.
virtual bool IsLoading() const = 0;
// Returns the page's loading state.
virtual LoadingState GetLoadingState() const = 0;
// Returns the UKM source ID associated with the URL of the main frame of
// this page.
......@@ -194,8 +213,8 @@ class PageNodeObserver {
// Invoked when the IsAudible property changes.
virtual void OnIsAudibleChanged(const PageNode* page_node) = 0;
// Invoked when the IsLoading property changes.
virtual void OnIsLoadingChanged(const PageNode* page_node) = 0;
// Invoked when the GetLoadingState property changes.
virtual void OnLoadingStateChanged(const PageNode* page_node) = 0;
// Invoked when the UkmSourceId property changes.
virtual void OnUkmSourceIdChanged(const PageNode* page_node) = 0;
......@@ -254,7 +273,7 @@ class PageNode::ObserverDefaultImpl : public PageNodeObserver {
OpenedType previous_opened_type) override {}
void OnIsVisibleChanged(const PageNode* page_node) override {}
void OnIsAudibleChanged(const PageNode* page_node) override {}
void OnIsLoadingChanged(const PageNode* page_node) override {}
void OnLoadingStateChanged(const PageNode* page_node) override {}
void OnUkmSourceIdChanged(const PageNode* page_node) override {}
void OnPageLifecycleStateChanged(const PageNode* page_node) override {}
void OnPageIsHoldingWebLockChanged(const PageNode* page_node) override {}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment