blob: bfd7d3a768e292007568eb458e2c815879528da6 [file] [log] [blame]
// Copyright 2020 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 "components/performance_manager/public/graph/policies/tab_loading_frame_navigation_policy.h"
#include "base/bind.h"
#include "base/task/post_task.h"
#include "base/task/task_traits.h"
#include "components/performance_manager/graph/page_node_impl.h"
#include "components/performance_manager/public/performance_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "url/gurl.h"
namespace performance_manager {
namespace policies {
namespace {
bool CanThrottleUrlScheme(const GURL& url) {
return url.SchemeIs("http") || url.SchemeIs("https");
}
} // namespace
TabLoadingFrameNavigationPolicy::TabLoadingFrameNavigationPolicy(
StopThrottlingCallback stop_throttling_callback)
: stop_throttling_callback_(stop_throttling_callback) {
DCHECK(!stop_throttling_callback.is_null());
}
TabLoadingFrameNavigationPolicy::~TabLoadingFrameNavigationPolicy() {
// All timers and timeouts should have been canceled, as no page nodes
// should be actively tracked.
DCHECK(timeouts_.empty());
DCHECK(!timeout_timer_.IsRunning());
DCHECK_EQ(base::TimeTicks::Min(), scheduled_timer_);
}
// static
bool TabLoadingFrameNavigationPolicy::ShouldThrottleWebContents(
content::WebContents* contents) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Don't throttle unless http or https.
bool throttled = true;
const GURL& url = contents->GetLastCommittedURL();
if (!CanThrottleUrlScheme(url))
throttled = false;
// Post a notification to the graph. Even if we're not throttling the
// notification is sent, just in case the WebContents was previously throttled
// and is being reused for a new navigation. This is racy, as the policy
// object can be destroyed while this message is in flight. This is resolved
// on the PM sequence, with the message only being dispatched if the policy
// object exists in the graph.
PerformanceManager::CallOnGraph(
FROM_HERE,
base::BindOnce(&SetPageNodeThrottled,
PerformanceManager::GetPageNodeForWebContents(contents),
throttled));
return throttled;
}
// static
bool TabLoadingFrameNavigationPolicy::ShouldThrottleNavigation(
content::NavigationHandle* handle) {
DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
// Don't throttle unless http or https.
const GURL& url = handle->GetURL();
if (!CanThrottleUrlScheme(url))
return false;
// Never throttle the main frame.
if (handle->IsInMainFrame())
return false;
// Never throttle frames that are navigating to the same eTLD+1 as the main
// frame.
auto* contents = handle->GetWebContents();
if (net::registry_controlled_domains::SameDomainOrHost(
url, contents->GetLastCommittedURL(),
net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES)) {
return false;
}
// Throttle any child-frame navigations to a different eTLD+1.
return true;
}
void TabLoadingFrameNavigationPolicy::OnBeforePageNodeRemoved(
const PageNode* page_node) {
// There's no public graph accessor. We could cache this in OnPassedToGraph,
// but it's reachable this way.
DCHECK(IsRegistered(page_node->GetGraph()));
MaybeErasePageTimeout(page_node);
}
void TabLoadingFrameNavigationPolicy::OnFirstContentfulPaint(
const FrameNode* frame_node,
base::TimeDelta time_since_navigation_start) {
// There's no public graph accessor. We could cache this in OnPassedToGraph,
// but it's reachable this way.
DCHECK(IsRegistered(frame_node->GetGraph()));
// We're only interested in current main-frame FCP notifications.
if (!frame_node->IsMainFrame() || !frame_node->IsCurrent())
return;
// Wait for another FCP time period, waiting a minimum of 1 second.
double fcp_ms = time_since_navigation_start.InMillisecondsF();
double delta_ms = std::max(1000.0, fcp_ms);
MaybeUpdatePageTimeout(frame_node->GetPageNode(),
base::TimeDelta::FromMillisecondsD(delta_ms));
}
void TabLoadingFrameNavigationPolicy::OnPassedToGraph(Graph* graph) {
DCHECK(NothingRegistered(graph));
graph->AddFrameNodeObserver(this);
graph->AddPageNodeObserver(this);
graph->RegisterObject(this);
}
void TabLoadingFrameNavigationPolicy::OnTakenFromGraph(Graph* graph) {
DCHECK(IsRegistered(graph));
graph->UnregisterObject(this);
graph->RemovePageNodeObserver(this);
graph->RemoveFrameNodeObserver(this);
}
// static
void TabLoadingFrameNavigationPolicy::SetPageNodeThrottled(
base::WeakPtr<const PageNode> page_node,
bool throttled,
Graph* graph) {
auto* self = GetFromGraph(graph);
if (!self || !page_node)
return;
self->SetPageNodeThrottledImpl(page_node.get(), throttled);
}
void TabLoadingFrameNavigationPolicy::SetPageNodeThrottledImpl(
const PageNode* page_node,
bool throttled) {
// There's no public graph accessor. We could cache this in OnPassedToGraph,
// but it's reachable this way.
DCHECK(IsRegistered(page_node->GetGraph()));
// It's possible for WebContents to be reused if a main-frame renavigates.
// On the UI thread a new scheduler object is created in that case, which will
// cause a timeout entry to be (temporarily) orphaned here. So first cleanup
// existing timeouts.
for (size_t i = 0; i < timeouts_.size(); ++i) {
if (timeouts_[i].page_node == page_node) {
timeouts_.erase(i);
break;
}
}
if (!throttled) {
MaybeUpdateTimeoutTimer();
return;
}
// Create a brand new timeout for the page.
// TODO(chrisha): Make this configurable via Finch experiment.
CreatePageTimeout(page_node, timeout_);
}
void TabLoadingFrameNavigationPolicy::CreatePageTimeout(
const PageNode* page_node,
base::TimeDelta timeout) {
#if DCHECK_IS_ON()
// Sanity check that no timeout entry already exists for this page.
for (const auto& timeout : timeouts_) {
DCHECK_NE(timeout.page_node, page_node);
}
#endif
base::TimeTicks when = base::TimeTicks::Now() + timeout;
timeouts_.insert(Timeout{page_node, when});
MaybeUpdateTimeoutTimer();
}
void TabLoadingFrameNavigationPolicy::MaybeUpdatePageTimeout(
const PageNode* page_node,
base::TimeDelta timeout) {
// Find or create an entry for the given |page_node|.
size_t i = 0;
for (; i < timeouts_.size(); ++i) {
if (timeouts_[i].page_node == page_node)
break;
}
if (i == timeouts_.size())
return;
// Update the entry if need be.
base::TimeTicks when = base::TimeTicks::Now() + timeout;
if (when < timeouts_[i].timeout) {
timeouts_.Replace(i, Timeout{page_node, when});
MaybeUpdateTimeoutTimer();
}
}
void TabLoadingFrameNavigationPolicy::MaybeErasePageTimeout(
const PageNode* page_node) {
for (size_t i = 0; i < timeouts_.size(); ++i) {
if (timeouts_[i].page_node == page_node) {
timeouts_.erase(i);
MaybeUpdateTimeoutTimer();
return;
}
}
}
void TabLoadingFrameNavigationPolicy::MaybeUpdateTimeoutTimer() {
if (timeouts_.empty()) {
timeout_timer_.Stop();
scheduled_timer_ = base::TimeTicks::Min();
return;
}
if (timeout_timer_.IsRunning()) {
// If the timer is already set to the right time then it doesn't need
// updating.
if (scheduled_timer_ == timeouts_.top().timeout)
return;
// Otherwise the timer needs rescheduling.
scheduled_timer_ = base::TimeTicks::Min();
timeout_timer_.Stop();
}
// If the next timeout should already have fired, do so synchronously.
base::TimeTicks now = base::TimeTicks::Now();
base::TimeTicks when = timeouts_.top().timeout;
if (when <= now) {
StopThrottlingExpiredPages();
// Early return if there are no remaining timeouts.
if (timeouts_.empty())
return;
}
// Restart the timer for the next event.
DCHECK(!timeouts_.empty());
when = timeouts_.top().timeout;
DCHECK_LT(now, when);
timeout_timer_.Start(
FROM_HERE, when - now,
base::BindOnce(&TabLoadingFrameNavigationPolicy::OnTimeout,
base::Unretained(this)));
scheduled_timer_ = when;
}
void TabLoadingFrameNavigationPolicy::OnTimeout() {
DCHECK(!timeouts_.empty());
timeout_timer_.Stop();
scheduled_timer_ = base::TimeTicks::Min();
StopThrottlingExpiredPages();
MaybeUpdateTimeoutTimer();
}
void TabLoadingFrameNavigationPolicy::StopThrottlingExpiredPages() {
DCHECK(!timeouts_.empty());
// Send notifications for all expired throttles.
auto now = base::TimeTicks::Now();
while (timeouts_.size() && now >= timeouts_.top().timeout) {
// There's no public graph accessor. We could cache this in OnPassedToGraph,
// but it's reachable this way.
const PageNode* page_node = timeouts_.top().page_node;
DCHECK(IsRegistered(page_node->GetGraph()));
timeouts_.pop();
// Post a task to the UI thread to notify the mechanism to stop throttling
// the contents.
base::PostTask(
FROM_HERE,
{content::BrowserThread::UI, base::TaskPriority::USER_VISIBLE},
base::BindOnce(
[](StopThrottlingCallback stop_throttling_callback,
const WebContentsProxy& proxy) {
auto* contents = proxy.Get();
if (contents)
stop_throttling_callback.Run(contents);
},
stop_throttling_callback_, page_node->GetContentsProxy()));
}
}
} // namespace policies
} // namespace performance_manager