// 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