blob: e800171fc56dbfb67476c197e2fff021e0779423 [file] [log] [blame]
// Copyright (c) 2011 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 "chrome/browser/prerender/prerender_manager.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/metrics/histogram.h"
#include "base/time.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/prerender/prerender_contents.h"
#include "chrome/browser/prerender/prerender_final_status.h"
#include "chrome/browser/profiles/profile.h"
#include "content/browser/browser_thread.h"
#include "content/browser/renderer_host/render_view_host.h"
#include "content/browser/renderer_host/render_process_host.h"
#include "content/browser/renderer_host/resource_dispatcher_host.h"
#include "content/browser/tab_contents/render_view_host_manager.h"
#include "content/browser/tab_contents/tab_contents.h"
#include "content/common/notification_service.h"
#include "content/common/view_messages.h"
#include "googleurl/src/url_parse.h"
#include "googleurl/src/url_canon.h"
#include "googleurl/src/url_util.h"
namespace prerender {
// static
int PrerenderManager::prerenders_per_session_count_ = 0;
// static
base::TimeTicks PrerenderManager::last_prefetch_seen_time_;
// static
PrerenderManager::PrerenderManagerMode PrerenderManager::mode_ =
PRERENDER_MODE_ENABLED;
// static
PrerenderManager::PrerenderManagerMode PrerenderManager::GetMode() {
return mode_;
}
// static
void PrerenderManager::SetMode(PrerenderManagerMode mode) {
mode_ = mode;
}
// static
bool PrerenderManager::IsPrerenderingPossible() {
return
GetMode() == PRERENDER_MODE_ENABLED ||
GetMode() == PRERENDER_MODE_EXPERIMENT_PRERENDER_GROUP ||
GetMode() == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP;
}
// static
bool PrerenderManager::IsControlGroup() {
return GetMode() == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP;
}
// static
bool PrerenderManager::MaybeGetQueryStringBasedAliasURL(
const GURL& url, GURL* alias_url) {
DCHECK(alias_url);
url_parse::Parsed parsed;
url_parse::ParseStandardURL(url.spec().c_str(), url.spec().length(),
&parsed);
url_parse::Component query = parsed.query;
url_parse::Component key, value;
while (url_parse::ExtractQueryKeyValue(url.spec().c_str(), &query, &key,
&value)) {
if (key.len != 3 || strncmp(url.spec().c_str() + key.begin, "url", key.len))
continue;
// We found a url= query string component.
if (value.len < 1)
continue;
url_canon::RawCanonOutputW<1024> decoded_url;
url_util::DecodeURLEscapeSequences(url.spec().c_str() + value.begin,
value.len, &decoded_url);
GURL new_url(string16(decoded_url.data(), decoded_url.length()));
if (!new_url.is_empty() && new_url.is_valid()) {
*alias_url = new_url;
return true;
}
return false;
}
return false;
}
struct PrerenderManager::PrerenderContentsData {
PrerenderContents* contents_;
base::Time start_time_;
PrerenderContentsData(PrerenderContents* contents, base::Time start_time)
: contents_(contents),
start_time_(start_time) {
}
};
struct PrerenderManager::PendingContentsData {
PendingContentsData(const GURL& url, const std::vector<GURL>& alias_urls,
const GURL& referrer)
: url_(url), alias_urls_(alias_urls), referrer_(referrer) { }
~PendingContentsData() {}
GURL url_;
std::vector<GURL> alias_urls_;
GURL referrer_;
};
PrerenderManager::PrerenderManager(Profile* profile)
: rate_limit_enabled_(true),
enabled_(true),
profile_(profile),
max_prerender_age_(base::TimeDelta::FromSeconds(
kDefaultMaxPrerenderAgeSeconds)),
max_elements_(kDefaultMaxPrerenderElements),
prerender_contents_factory_(PrerenderContents::CreateFactory()),
last_prerender_start_time_(GetCurrentTimeTicks() -
base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs)) {
}
PrerenderManager::~PrerenderManager() {
while (!prerender_list_.empty()) {
PrerenderContentsData data = prerender_list_.front();
prerender_list_.pop_front();
data.contents_->set_final_status(FINAL_STATUS_MANAGER_SHUTDOWN);
delete data.contents_;
}
}
void PrerenderManager::SetPrerenderContentsFactory(
PrerenderContents::Factory* prerender_contents_factory) {
prerender_contents_factory_.reset(prerender_contents_factory);
}
bool PrerenderManager::AddPreload(const GURL& url,
const std::vector<GURL>& alias_urls,
const GURL& referrer) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
DeleteOldEntries();
if (FindEntry(url))
return false;
// Local copy, since we may have to add an additional entry to it.
std::vector<GURL> all_alias_urls = alias_urls;
GURL additional_alias_url;
if (IsControlGroup() &&
PrerenderManager::MaybeGetQueryStringBasedAliasURL(
url, &additional_alias_url))
all_alias_urls.push_back(additional_alias_url);
// Do not prerender if there are too many render processes, and we would
// have to use an existing one. We do not want prerendering to happen in
// a shared process, so that we can always reliably lower the CPU
// priority for prerendering.
// In single-process mode, ShouldTryToUseExistingProcessHost() always returns
// true, so that case needs to be explicitly checked for.
// TODO(tburkard): Figure out how to cancel prerendering in the opposite
// case, when a new tab is added to a process used for prerendering.
if (RenderProcessHost::ShouldTryToUseExistingProcessHost() &&
!RenderProcessHost::run_renderer_in_process()) {
// Only record the status if we are not in the control group.
if (!IsControlGroup())
RecordFinalStatus(FINAL_STATUS_TOO_MANY_PROCESSES);
return false;
}
// Check if enough time has passed since the last prerender.
if (!DoesRateLimitAllowPrerender()) {
// Cancel the prerender. We could add it to the pending prerender list but
// this doesn't make sense as the next prerender request will be triggered
// by a navigation and is unlikely to be the same site.
RecordFinalStatus(FINAL_STATUS_RATE_LIMIT_EXCEEDED);
return false;
}
// TODO(cbentzel): Move invalid checks here instead of PrerenderContents?
PrerenderContentsData data(CreatePrerenderContents(url, all_alias_urls,
referrer),
GetCurrentTime());
prerender_list_.push_back(data);
if (IsControlGroup()) {
data.contents_->set_final_status(FINAL_STATUS_CONTROL_GROUP);
} else {
last_prerender_start_time_ = GetCurrentTimeTicks();
data.contents_->StartPrerendering();
}
while (prerender_list_.size() > max_elements_) {
data = prerender_list_.front();
prerender_list_.pop_front();
data.contents_->set_final_status(FINAL_STATUS_EVICTED);
delete data.contents_;
}
StartSchedulingPeriodicCleanups();
return true;
}
void PrerenderManager::AddPendingPreload(
const std::pair<int,int>& child_route_id_pair,
const GURL& url,
const std::vector<GURL>& alias_urls,
const GURL& referrer) {
// Check if this is coming from a valid prerender rvh.
bool is_valid_prerender = false;
for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
it != prerender_list_.end(); ++it) {
PrerenderContents* pc = it->contents_;
int child_id;
int route_id;
bool has_child_id = pc->GetChildId(&child_id);
bool has_route_id = has_child_id && pc->GetRouteId(&route_id);
if (has_child_id && has_route_id &&
child_id == child_route_id_pair.first &&
route_id == child_route_id_pair.second) {
is_valid_prerender = true;
break;
}
}
// If not, we could check to see if the RenderViewHost specified by the
// child_route_id_pair exists and if so just start prerendering, as this
// suggests that the link was clicked, though this might prerender something
// that the user has already navigated away from. For now, we'll be
// conservative and skip the prerender which will mean some prerender requests
// from prerendered pages will be missed if the user navigates quickly.
if (!is_valid_prerender) {
RecordFinalStatus(FINAL_STATUS_PENDING_SKIPPED);
return;
}
PendingPrerenderList::iterator it =
pending_prerender_list_.find(child_route_id_pair);
if (it == pending_prerender_list_.end()) {
PendingPrerenderList::value_type el = std::make_pair(child_route_id_pair,
std::vector<PendingContentsData>());
it = pending_prerender_list_.insert(el).first;
}
it->second.push_back(PendingContentsData(url, alias_urls, referrer));
}
void PrerenderManager::DeleteOldEntries() {
while (!prerender_list_.empty()) {
PrerenderContentsData data = prerender_list_.front();
if (IsPrerenderElementFresh(data.start_time_))
return;
prerender_list_.pop_front();
data.contents_->set_final_status(FINAL_STATUS_TIMED_OUT);
delete data.contents_;
}
if (prerender_list_.empty())
StopSchedulingPeriodicCleanups();
}
PrerenderContents* PrerenderManager::GetEntry(const GURL& url) {
DeleteOldEntries();
for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
it != prerender_list_.end();
++it) {
PrerenderContents* pc = it->contents_;
if (pc->MatchesURL(url)) {
prerender_list_.erase(it);
return pc;
}
}
// Entry not found.
return NULL;
}
bool PrerenderManager::MaybeUsePreloadedPage(TabContents* tc, const GURL& url) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
scoped_ptr<PrerenderContents> pc(GetEntry(url));
if (pc.get() == NULL)
return false;
// If we are just in the control group (which can be detected by noticing
// that prerendering hasn't even started yet), record that this TC now would
// be showing a prerendered contents, but otherwise, don't do anything.
if (!pc->prerendering_has_started()) {
MarkTabContentsAsWouldBePrerendered(tc);
return false;
}
if (!pc->load_start_time().is_null())
RecordTimeUntilUsed(GetCurrentTimeTicks() - pc->load_start_time());
UMA_HISTOGRAM_COUNTS("Prerender.PrerendersPerSessionCount",
++prerenders_per_session_count_);
pc->set_final_status(FINAL_STATUS_USED);
int child_id;
int route_id;
CHECK(pc->GetChildId(&child_id));
CHECK(pc->GetRouteId(&route_id));
RenderViewHost* rvh = pc->render_view_host();
// RenderViewHosts in PrerenderContents start out hidden.
// Since we are actually using it now, restore it.
rvh->WasRestored();
pc->set_render_view_host(NULL);
rvh->Send(new ViewMsg_DisplayPrerenderedPage(rvh->routing_id()));
tc->SwapInRenderViewHost(rvh);
MarkTabContentsAsPrerendered(tc);
// See if we have any pending prerender requests for this routing id and start
// the preload if we do.
std::pair<int, int> child_route_pair = std::make_pair(child_id, route_id);
PendingPrerenderList::iterator pending_it =
pending_prerender_list_.find(child_route_pair);
if (pending_it != pending_prerender_list_.end()) {
for (std::vector<PendingContentsData>::iterator content_it =
pending_it->second.begin();
content_it != pending_it->second.end(); ++content_it) {
AddPreload(content_it->url_, content_it->alias_urls_,
content_it->referrer_);
}
pending_prerender_list_.erase(pending_it);
}
NotificationService::current()->Notify(
NotificationType::PRERENDER_CONTENTS_USED,
Source<std::pair<int, int> >(&child_route_pair),
NotificationService::NoDetails());
ViewHostMsg_FrameNavigate_Params* p = pc->navigate_params();
if (p != NULL)
tc->DidNavigate(rvh, *p);
string16 title = pc->title();
if (!title.empty())
tc->UpdateTitle(rvh, pc->page_id(), UTF16ToWideHack(title));
GURL icon_url = pc->icon_url();
if (!icon_url.is_empty()) {
std::vector<FaviconURL> urls;
urls.push_back(FaviconURL(icon_url, FaviconURL::FAVICON));
tc->favicon_helper().OnUpdateFaviconURL(pc->page_id(), urls);
}
if (pc->has_stopped_loading())
tc->DidStopLoading();
return true;
}
void PrerenderManager::RemoveEntry(PrerenderContents* entry) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
it != prerender_list_.end();
++it) {
if (it->contents_ == entry) {
RemovePendingPreload(entry);
prerender_list_.erase(it);
break;
}
}
DeleteOldEntries();
}
base::Time PrerenderManager::GetCurrentTime() const {
return base::Time::Now();
}
base::TimeTicks PrerenderManager::GetCurrentTimeTicks() const {
return base::TimeTicks::Now();
}
bool PrerenderManager::IsPrerenderElementFresh(const base::Time start) const {
base::Time now = GetCurrentTime();
return (now - start < max_prerender_age_);
}
PrerenderContents* PrerenderManager::CreatePrerenderContents(
const GURL& url,
const std::vector<GURL>& alias_urls,
const GURL& referrer) {
return prerender_contents_factory_->CreatePrerenderContents(
this, profile_, url, alias_urls, referrer);
}
// Helper macro for histograms.
#define RECORD_PLT(tag, perceived_page_load_time) { \
UMA_HISTOGRAM_CUSTOM_TIMES( \
base::FieldTrial::MakeName(std::string("Prerender.") + tag, \
"Prefetch"), \
perceived_page_load_time, \
base::TimeDelta::FromMilliseconds(10), \
base::TimeDelta::FromSeconds(60), \
100); \
}
// static
void PrerenderManager::RecordPerceivedPageLoadTime(
base::TimeDelta perceived_page_load_time,
TabContents* tab_contents) {
bool within_window = WithinWindow();
PrerenderManager* prerender_manager =
tab_contents->profile()->GetPrerenderManager();
if (!prerender_manager)
return;
if (!prerender_manager->is_enabled())
return;
RECORD_PLT("PerceivedPLT", perceived_page_load_time);
if (within_window)
RECORD_PLT("PerceivedPLTWindowed", perceived_page_load_time);
if (prerender_manager &&
((mode_ == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP &&
prerender_manager->WouldTabContentsBePrerendered(tab_contents)) ||
(mode_ == PRERENDER_MODE_EXPERIMENT_PRERENDER_GROUP &&
prerender_manager->IsTabContentsPrerendered(tab_contents)))) {
RECORD_PLT("PerceivedPLTMatched", perceived_page_load_time);
} else {
if (within_window)
RECORD_PLT("PerceivedPLTWindowNotMatched", perceived_page_load_time);
}
}
void PrerenderManager::RecordTimeUntilUsed(base::TimeDelta time_until_used) {
UMA_HISTOGRAM_CUSTOM_TIMES(
"Prerender.TimeUntilUsed",
time_until_used,
base::TimeDelta::FromMilliseconds(10),
base::TimeDelta::FromSeconds(kDefaultMaxPrerenderAgeSeconds),
50);
}
bool PrerenderManager::is_enabled() const {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
return enabled_;
}
void PrerenderManager::set_enabled(bool enabled) {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
enabled_ = enabled;
}
PrerenderContents* PrerenderManager::FindEntry(const GURL& url) {
for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
it != prerender_list_.end();
++it) {
if (it->contents_->MatchesURL(url))
return it->contents_;
}
// Entry not found.
return NULL;
}
PrerenderManager::PendingContentsData*
PrerenderManager::FindPendingEntry(const GURL& url) {
for (PendingPrerenderList::iterator map_it = pending_prerender_list_.begin();
map_it != pending_prerender_list_.end();
++map_it) {
for (std::vector<PendingContentsData>::iterator content_it =
map_it->second.begin();
content_it != map_it->second.end();
++content_it) {
if (content_it->url_ == url) {
return &(*content_it);
}
}
}
return NULL;
}
// static
void PrerenderManager::RecordPrefetchTagObserved() {
// Ensure that we are in the UI thread, and post to the UI thread if
// necessary.
if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
BrowserThread::PostTask(
BrowserThread::UI,
FROM_HERE,
NewRunnableFunction(
&PrerenderManager::RecordPrefetchTagObservedOnUIThread));
} else {
RecordPrefetchTagObservedOnUIThread();
}
}
// static
void PrerenderManager::RecordPrefetchTagObservedOnUIThread() {
// Once we get here, we have to be on the UI thread.
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
// If we observe multiple tags within the 30 second window, we will still
// reset the window to begin at the most recent occurrence, so that we will
// always be in a window in the 30 seconds from each occurrence.
last_prefetch_seen_time_ = base::TimeTicks::Now();
}
void PrerenderManager::RemovePendingPreload(PrerenderContents* entry) {
int child_id;
int route_id;
bool has_child_id = entry->GetChildId(&child_id);
bool has_route_id = has_child_id && entry->GetRouteId(&route_id);
// If the entry doesn't have a RenderViewHost then it didn't start
// prerendering and there shouldn't be any pending preloads to remove.
if (has_child_id && has_route_id) {
std::pair<int, int> child_route_pair = std::make_pair(child_id, route_id);
pending_prerender_list_.erase(child_route_pair);
}
}
// static
bool PrerenderManager::WithinWindow() {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
if (last_prefetch_seen_time_.is_null())
return false;
base::TimeDelta elapsed_time =
base::TimeTicks::Now() - last_prefetch_seen_time_;
return elapsed_time <= base::TimeDelta::FromSeconds(kWindowDurationSeconds);
}
bool PrerenderManager::DoesRateLimitAllowPrerender() const {
DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
base::TimeDelta elapsed_time =
GetCurrentTimeTicks() - last_prerender_start_time_;
UMA_HISTOGRAM_TIMES("Prerender.TimeBetweenPrerenderRequests",
elapsed_time);
if (!rate_limit_enabled_)
return true;
return elapsed_time >
base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs);
}
void PrerenderManager::StartSchedulingPeriodicCleanups() {
if (repeating_timer_.IsRunning())
return;
repeating_timer_.Start(
base::TimeDelta::FromMilliseconds(kPeriodicCleanupIntervalMs),
this,
&PrerenderManager::PeriodicCleanup);
}
void PrerenderManager::StopSchedulingPeriodicCleanups() {
repeating_timer_.Stop();
}
void PrerenderManager::PeriodicCleanup() {
DeleteOldEntries();
// Grab a copy of the current PrerenderContents pointers, so that we
// will not interfere with potential deletions of the list.
std::vector<PrerenderContents*> prerender_contents;
for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
it != prerender_list_.end();
++it) {
prerender_contents.push_back(it->contents_);
}
for (std::vector<PrerenderContents*>::iterator it =
prerender_contents.begin();
it != prerender_contents.end();
++it) {
(*it)->DestroyWhenUsingTooManyResources();
}
}
void PrerenderManager::MarkTabContentsAsPrerendered(TabContents* tc) {
prerendered_tc_set_.insert(tc);
}
void PrerenderManager::MarkTabContentsAsWouldBePrerendered(TabContents* tc) {
would_be_prerendered_tc_set_.insert(tc);
}
void PrerenderManager::MarkTabContentsAsNotPrerendered(TabContents* tc) {
prerendered_tc_set_.erase(tc);
would_be_prerendered_tc_set_.erase(tc);
}
bool PrerenderManager::IsTabContentsPrerendered(TabContents* tc) const {
return prerendered_tc_set_.count(tc) > 0;
}
bool PrerenderManager::WouldTabContentsBePrerendered(TabContents* tc) const {
return would_be_prerendered_tc_set_.count(tc) > 0;
}
} // namespace prerender