// Copyright (c) 2012 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 <algorithm> #include <functional> #include <string> #include <vector> #include "base/bind.h" #include "base/bind_helpers.h" #include "base/logging.h" #include "base/memory/weak_ptr.h" #include "base/metrics/histogram.h" #include "base/prefs/pref_service.h" #include "base/stl_util.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "base/timer/elapsed_timer.h" #include "base/values.h" #include "chrome/browser/browser_process.h" #include "chrome/browser/chrome_notification_types.h" #include "chrome/browser/common/cancelable_request.h" #include "chrome/browser/favicon/favicon_tab_helper.h" #include "chrome/browser/history/history_service_factory.h" #include "chrome/browser/net/chrome_cookie_notification_details.h" #include "chrome/browser/predictors/predictor_database.h" #include "chrome/browser/predictors/predictor_database_factory.h" #include "chrome/browser/prerender/prerender_condition.h" #include "chrome/browser/prerender/prerender_contents.h" #include "chrome/browser/prerender/prerender_field_trial.h" #include "chrome/browser/prerender/prerender_final_status.h" #include "chrome/browser/prerender/prerender_handle.h" #include "chrome/browser/prerender/prerender_histograms.h" #include "chrome/browser/prerender/prerender_history.h" #include "chrome/browser/prerender/prerender_local_predictor.h" #include "chrome/browser/prerender/prerender_manager_factory.h" #include "chrome/browser/prerender/prerender_tab_helper.h" #include "chrome/browser/prerender/prerender_tracker.h" #include "chrome/browser/prerender/prerender_util.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/search/search.h" #include "chrome/browser/tab_contents/tab_util.h" #include "chrome/browser/ui/browser_navigator.h" #include "chrome/browser/ui/tab_contents/core_tab_helper.h" #include "chrome/browser/ui/tab_contents/core_tab_helper_delegate.h" #include "chrome/common/chrome_switches.h" #include "chrome/common/pref_names.h" #include "chrome/common/prerender_messages.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/devtools_agent_host.h" #include "content/public/browser/navigation_controller.h" #include "content/public/browser/notification_service.h" #include "content/public/browser/notification_source.h" #include "content/public/browser/render_process_host.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/session_storage_namespace.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/browser/web_contents_view.h" #include "content/public/common/favicon_url.h" #include "content/public/common/url_constants.h" #include "extensions/common/constants.h" #include "net/url_request/url_request_context.h" #include "net/url_request/url_request_context_getter.h" using content::BrowserThread; using content::RenderViewHost; using content::SessionStorageNamespace; using content::WebContents; using predictors::LoggedInPredictorTable; namespace prerender { namespace { // Time interval at which periodic cleanups are performed. const int kPeriodicCleanupIntervalMs = 1000; // Valid HTTP methods for prerendering. const char* const kValidHttpMethods[] = { "GET", "HEAD", "OPTIONS", "POST", "TRACE", }; // Length of prerender history, for display in chrome://net-internals const int kHistoryLength = 100; // Timeout, in ms, for a session storage namespace merge. const int kSessionStorageNamespaceMergeTimeoutMs = 500; // Indicates whether a Prerender has been cancelled such that we need // a dummy replacement for the purpose of recording the correct PPLT for // the Match Complete case. // Traditionally, "Match" means that a prerendered page was actually visited & // the prerender was used. Our goal is to have "Match" cases line up in the // control group & the experiment group, so that we can make meaningful // comparisons of improvements. However, in the control group, since we don't // actually perform prerenders, many of the cancellation reasons cannot be // detected. Therefore, in the Prerender group, when we cancel for one of these // reasons, we keep track of a dummy Prerender representing what we would // have in the control group. If that dummy prerender in the prerender group // would then be swapped in (but isn't actually b/c it's a dummy), we record // this as a MatchComplete. This allows us to compare MatchComplete's // across Prerender & Control group which ideally should be lining up. // This ensures that there is no bias in terms of the page load times // of the pages forming the difference between the two sets. bool NeedMatchCompleteDummyForFinalStatus(FinalStatus final_status) { return final_status != FINAL_STATUS_USED && final_status != FINAL_STATUS_TIMED_OUT && final_status != FINAL_STATUS_MANAGER_SHUTDOWN && final_status != FINAL_STATUS_APP_TERMINATING && final_status != FINAL_STATUS_WINDOW_OPENER && final_status != FINAL_STATUS_CACHE_OR_HISTORY_CLEARED && final_status != FINAL_STATUS_CANCELLED && final_status != FINAL_STATUS_DEVTOOLS_ATTACHED && final_status != FINAL_STATUS_CROSS_SITE_NAVIGATION_PENDING && final_status != FINAL_STATUS_PAGE_BEING_CAPTURED && final_status != FINAL_STATUS_NAVIGATION_UNCOMMITTED; } void CheckIfCookiesExistForDomainResultOnUIThread( const net::CookieMonster::HasCookiesForETLDP1Callback& callback, bool cookies_exist) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); callback.Run(cookies_exist); } void CheckIfCookiesExistForDomainResultOnIOThread( const net::CookieMonster::HasCookiesForETLDP1Callback& callback, bool cookies_exist) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); BrowserThread::PostTask( BrowserThread::UI, FROM_HERE, base::Bind(&CheckIfCookiesExistForDomainResultOnUIThread, callback, cookies_exist)); } void CheckIfCookiesExistForDomainOnIOThread( net::URLRequestContextGetter* rq_context, const std::string& domain_key, const net::CookieMonster::HasCookiesForETLDP1Callback& callback) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::IO)); net::CookieStore* cookie_store = rq_context->GetURLRequestContext()->cookie_store(); cookie_store->GetCookieMonster()->HasCookiesForETLDP1Async( domain_key, base::Bind(&CheckIfCookiesExistForDomainResultOnIOThread, callback)); } } // namespace class PrerenderManager::OnCloseWebContentsDeleter : public content::WebContentsDelegate, public base::SupportsWeakPtr< PrerenderManager::OnCloseWebContentsDeleter> { public: OnCloseWebContentsDeleter(PrerenderManager* manager, WebContents* tab) : manager_(manager), tab_(tab), suppressed_dialog_(false) { tab_->SetDelegate(this); base::MessageLoop::current()->PostDelayedTask(FROM_HERE, base::Bind(&OnCloseWebContentsDeleter::ScheduleWebContentsForDeletion, AsWeakPtr(), true), base::TimeDelta::FromSeconds(kDeleteWithExtremePrejudiceSeconds)); } virtual void CloseContents(WebContents* source) OVERRIDE { DCHECK_EQ(tab_, source); ScheduleWebContentsForDeletion(false); } virtual void SwappedOut(WebContents* source) OVERRIDE { DCHECK_EQ(tab_, source); ScheduleWebContentsForDeletion(false); } virtual bool ShouldSuppressDialogs() OVERRIDE { // Use this as a proxy for getting statistics on how often we fail to honor // the beforeunload event. suppressed_dialog_ = true; return true; } private: static const int kDeleteWithExtremePrejudiceSeconds = 3; void ScheduleWebContentsForDeletion(bool timeout) { UMA_HISTOGRAM_BOOLEAN("Prerender.TabContentsDeleterTimeout", timeout); UMA_HISTOGRAM_BOOLEAN("Prerender.TabContentsDeleterSuppressedDialog", suppressed_dialog_); tab_->SetDelegate(NULL); manager_->ScheduleDeleteOldWebContents(tab_.release(), this); // |this| is deleted at this point. } PrerenderManager* manager_; scoped_ptr<WebContents> tab_; bool suppressed_dialog_; DISALLOW_COPY_AND_ASSIGN(OnCloseWebContentsDeleter); }; // static int PrerenderManager::prerenders_per_session_count_ = 0; // static PrerenderManager::PrerenderManagerMode PrerenderManager::mode_ = PRERENDER_MODE_ENABLED; struct PrerenderManager::NavigationRecord { NavigationRecord(const GURL& url, base::TimeTicks time) : url(url), time(time) { } GURL url; base::TimeTicks time; }; PrerenderManager::PrerenderedWebContentsData:: PrerenderedWebContentsData(Origin origin) : origin(origin) { } PrerenderManager::WouldBePrerenderedWebContentsData:: WouldBePrerenderedWebContentsData(Origin origin) : origin(origin), state(WAITING_FOR_PROVISIONAL_LOAD) { } PrerenderManager::PrerenderManager(Profile* profile, PrerenderTracker* prerender_tracker) : enabled_(profile && profile->GetPrefs() && profile->GetPrefs()->GetBoolean(prefs::kNetworkPredictionEnabled)), profile_(profile), prerender_tracker_(prerender_tracker), prerender_contents_factory_(PrerenderContents::CreateFactory()), last_prerender_start_time_(GetCurrentTimeTicks() - base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs)), prerender_history_(new PrerenderHistory(kHistoryLength)), histograms_(new PrerenderHistograms()) { // There are some assumptions that the PrerenderManager is on the UI thread. // Any other checks simply make sure that the PrerenderManager is accessed on // the same thread that it was created on. DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (IsLocalPredictorEnabled()) local_predictor_.reset(new PrerenderLocalPredictor(this)); if (IsLoggedInPredictorEnabled() && !profile_->IsOffTheRecord()) { predictors::PredictorDatabase* predictor_db = predictors::PredictorDatabaseFactory::GetForProfile(profile); if (predictor_db) { logged_in_predictor_table_ = predictor_db->logged_in_table(); scoped_ptr<LoggedInStateMap> new_state_map(new LoggedInStateMap); LoggedInStateMap* new_state_map_ptr = new_state_map.get(); BrowserThread::PostTaskAndReply( BrowserThread::DB, FROM_HERE, base::Bind(&LoggedInPredictorTable::GetAllData, logged_in_predictor_table_, new_state_map_ptr), base::Bind(&PrerenderManager::LoggedInPredictorDataReceived, AsWeakPtr(), base::Passed(&new_state_map))); } } // Certain experiments override our default config_ values. switch (PrerenderManager::GetMode()) { case PrerenderManager::PRERENDER_MODE_EXPERIMENT_MULTI_PRERENDER_GROUP: config_.max_link_concurrency = 4; config_.max_link_concurrency_per_launcher = 2; break; case PrerenderManager::PRERENDER_MODE_EXPERIMENT_15MIN_TTL_GROUP: config_.time_to_live = base::TimeDelta::FromMinutes(15); break; default: break; } notification_registrar_.Add( this, chrome::NOTIFICATION_COOKIE_CHANGED, content::NotificationService::AllBrowserContextsAndSources()); MediaCaptureDevicesDispatcher::GetInstance()->AddObserver(this); } PrerenderManager::~PrerenderManager() { MediaCaptureDevicesDispatcher::GetInstance()->RemoveObserver(this); // The earlier call to BrowserContextKeyedService::Shutdown() should have // emptied these vectors already. DCHECK(active_prerenders_.empty()); DCHECK(to_delete_prerenders_.empty()); } void PrerenderManager::Shutdown() { DestroyAllContents(FINAL_STATUS_MANAGER_SHUTDOWN); STLDeleteElements(&prerender_conditions_); on_close_web_contents_deleters_.clear(); // Must happen before |profile_| is set to NULL as // |local_predictor_| accesses it. if (local_predictor_) local_predictor_->Shutdown(); profile_ = NULL; DCHECK(active_prerenders_.empty()); } PrerenderHandle* PrerenderManager::AddPrerenderFromLinkRelPrerender( int process_id, int route_id, const GURL& url, const content::Referrer& referrer, const gfx::Size& size) { Origin origin = ORIGIN_LINK_REL_PRERENDER_CROSSDOMAIN; SessionStorageNamespace* session_storage_namespace = NULL; // Unit tests pass in a process_id == -1. if (process_id != -1) { RenderViewHost* source_render_view_host = RenderViewHost::FromID(process_id, route_id); if (!source_render_view_host) return NULL; WebContents* source_web_contents = WebContents::FromRenderViewHost(source_render_view_host); if (!source_web_contents) return NULL; if (source_web_contents->GetURL().host() == url.host()) origin = ORIGIN_LINK_REL_PRERENDER_SAMEDOMAIN; // TODO(ajwong): This does not correctly handle storage for isolated apps. session_storage_namespace = source_web_contents->GetController() .GetDefaultSessionStorageNamespace(); } // If the prerender request comes from a recently cancelled prerender that // |this| still owns, then abort the prerender. for (ScopedVector<PrerenderData>::iterator it = to_delete_prerenders_.begin(); it != to_delete_prerenders_.end(); ++it) { PrerenderContents* prerender_contents = (*it)->contents(); int contents_child_id; int contents_route_id; if (prerender_contents->GetChildId(&contents_child_id) && prerender_contents->GetRouteId(&contents_route_id)) { if (contents_child_id == process_id && contents_route_id == route_id) return NULL; } } if (PrerenderData* parent_prerender_data = FindPrerenderDataForChildAndRoute(process_id, route_id)) { // Instead of prerendering from inside of a running prerender, we will defer // this request until its launcher is made visible. if (PrerenderContents* contents = parent_prerender_data->contents()) { PrerenderHandle* prerender_handle = new PrerenderHandle(static_cast<PrerenderData*>(NULL)); scoped_ptr<PrerenderContents::PendingPrerenderInfo> pending_prerender_info(new PrerenderContents::PendingPrerenderInfo( prerender_handle->weak_ptr_factory_.GetWeakPtr(), origin, url, referrer, size)); contents->AddPendingPrerender(pending_prerender_info.Pass()); return prerender_handle; } } return AddPrerender(origin, process_id, url, referrer, size, session_storage_namespace); } PrerenderHandle* PrerenderManager::AddPrerenderFromOmnibox( const GURL& url, SessionStorageNamespace* session_storage_namespace, const gfx::Size& size) { if (!IsOmniboxEnabled(profile_)) return NULL; return AddPrerender(ORIGIN_OMNIBOX, -1, url, content::Referrer(), size, session_storage_namespace); } PrerenderHandle* PrerenderManager::AddPrerenderFromLocalPredictor( const GURL& url, SessionStorageNamespace* session_storage_namespace, const gfx::Size& size) { return AddPrerender(ORIGIN_LOCAL_PREDICTOR, -1, url, content::Referrer(), size, session_storage_namespace); } PrerenderHandle* PrerenderManager::AddPrerenderFromExternalRequest( const GURL& url, const content::Referrer& referrer, SessionStorageNamespace* session_storage_namespace, const gfx::Size& size) { return AddPrerender(ORIGIN_EXTERNAL_REQUEST, -1, url, referrer, size, session_storage_namespace); } PrerenderHandle* PrerenderManager::AddPrerenderForInstant( const GURL& url, content::SessionStorageNamespace* session_storage_namespace, const gfx::Size& size) { DCHECK(chrome::ShouldPrefetchSearchResults()); return AddPrerender(ORIGIN_INSTANT, -1, url, content::Referrer(), size, session_storage_namespace); } void PrerenderManager::DestroyPrerenderForRenderView( int process_id, int view_id, FinalStatus final_status) { DCHECK(CalledOnValidThread()); if (PrerenderData* prerender_data = FindPrerenderDataForChildAndRoute(process_id, view_id)) { prerender_data->contents()->Destroy(final_status); } } void PrerenderManager::CancelAllPrerenders() { DCHECK(CalledOnValidThread()); while (!active_prerenders_.empty()) { PrerenderContents* prerender_contents = active_prerenders_.front()->contents(); prerender_contents->Destroy(FINAL_STATUS_CANCELLED); } } bool PrerenderManager::MaybeUsePrerenderedPage(const GURL& url, chrome::NavigateParams* params) { DCHECK(CalledOnValidThread()); content::WebContents* web_contents = params->target_contents; DCHECK(!IsWebContentsPrerendering(web_contents, NULL)); // Don't prerender if the navigation involves some special parameters. if (params->uses_post || !params->extra_headers.empty()) return false; DeleteOldEntries(); to_delete_prerenders_.clear(); // First, try to find prerender data with the correct session storage // namespace. // TODO(ajwong): This doesn't handle isolated apps correctly. PrerenderData* prerender_data = FindPrerenderData( url, web_contents->GetController().GetDefaultSessionStorageNamespace()); // If this failed, we may still find a prerender for the same URL, but a // different session storage namespace. If we do, we might have to perform // a merge. if (!prerender_data) { prerender_data = FindPrerenderData(url, NULL); } else { RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_CANDIDATE_NAMESPACE_MATCHES); } if (!prerender_data) return false; RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_CANDIDATE); DCHECK(prerender_data->contents()); // If there is currently a merge pending for this prerender data, // or this webcontents, do not swap in, but give the merge a chance to // finish and swap into the intended target webcontents. if (prerender_data->pending_swap()) return false; RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_NO_MERGE_PENDING); SessionStorageNamespace* target_namespace = web_contents->GetController().GetDefaultSessionStorageNamespace(); SessionStorageNamespace* prerender_namespace = prerender_data->contents()->GetSessionStorageNamespace(); // Only when actually prerendering is session storage namespace merging an // issue. For the control group, it will be assumed that the merge succeeded. if (prerender_namespace && prerender_namespace != target_namespace && !prerender_namespace->IsAliasOf(target_namespace)) { if (!ShouldMergeSessionStorageNamespaces()) { RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_MERGING_DISABLED); return false; } RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_ISSUING_MERGE); prerender_data->set_pending_swap(new PendingSwap( this, web_contents, prerender_data, url, params->should_replace_current_entry)); prerender_data->pending_swap()->BeginSwap(); // Although this returns false, creating a PendingSwap registers with // PrerenderTracker to throttle MAIN_FRAME navigations while the swap is // pending. return false; } // No need to merge; swap synchronously. WebContents* new_web_contents = SwapInternal( url, web_contents, prerender_data, params->should_replace_current_entry); if (!new_web_contents) return false; // Record the new target_contents for the callers. params->target_contents = new_web_contents; return true; } WebContents* PrerenderManager::SwapInternal( const GURL& url, WebContents* web_contents, PrerenderData* prerender_data, bool should_replace_current_entry) { DCHECK(CalledOnValidThread()); DCHECK(!IsWebContentsPrerendering(web_contents, NULL)); // Only swap if the target WebContents has a CoreTabHelper delegate to swap // out of it. For a normal WebContents, this is if it is in a TabStripModel. CoreTabHelper* core_tab_helper = CoreTabHelper::FromWebContents(web_contents); if (!core_tab_helper || !core_tab_helper->delegate()) { RecordEvent(prerender_data->contents(), PRERENDER_EVENT_SWAPIN_NO_DELEGATE); return NULL; } if (IsNoSwapInExperiment(prerender_data->contents()->experiment_id())) return NULL; if (WebContents* new_web_contents = prerender_data->contents()->prerender_contents()) { if (web_contents == new_web_contents) return NULL; // Do not swap in to ourself. // We cannot swap in if there is no last committed entry, because we would // show a blank page under an existing entry from the current tab. Even if // there is a pending entry, it may not commit. // TODO(creis): If there is a pending navigation and no last committed // entry, we might be able to transfer the network request instead. if (!new_web_contents->GetController().CanPruneAllButLastCommitted()) { // Abort this prerender so it is not used later. http://crbug.com/292121 prerender_data->contents()->Destroy(FINAL_STATUS_NAVIGATION_UNCOMMITTED); return NULL; } } // Do not use the prerendered version if there is an opener object. if (web_contents->HasOpener()) { prerender_data->contents()->Destroy(FINAL_STATUS_WINDOW_OPENER); return NULL; } // Do not swap in the prerender if the current WebContents is being captured. if (web_contents->GetCapturerCount() > 0) { prerender_data->contents()->Destroy(FINAL_STATUS_PAGE_BEING_CAPTURED); return NULL; } // If we are just in the control group (which can be detected by noticing // that prerendering hasn't even started yet), record that |web_contents| now // would be showing a prerendered contents, but otherwise, don't do anything. if (!prerender_data->contents()->prerendering_has_started()) { MarkWebContentsAsWouldBePrerendered(web_contents, prerender_data->contents()->origin()); prerender_data->contents()->Destroy(FINAL_STATUS_WOULD_HAVE_BEEN_USED); return NULL; } // Don't use prerendered pages if debugger is attached to the tab. // See http://crbug.com/98541 if (content::DevToolsAgentHost::IsDebuggerAttached(web_contents)) { DestroyAndMarkMatchCompleteAsUsed(prerender_data->contents(), FINAL_STATUS_DEVTOOLS_ATTACHED); return NULL; } // If the prerendered page is in the middle of a cross-site navigation, // don't swap it in because there isn't a good way to merge histories. if (prerender_data->contents()->IsCrossSiteNavigationPending()) { DestroyAndMarkMatchCompleteAsUsed( prerender_data->contents(), FINAL_STATUS_CROSS_SITE_NAVIGATION_PENDING); return NULL; } // For bookkeeping purposes, we need to mark this WebContents to // reflect that it would have been prerendered. if (GetMode() == PRERENDER_MODE_EXPERIMENT_NO_USE_GROUP) { MarkWebContentsAsWouldBePrerendered(web_contents, prerender_data->contents()->origin()); prerender_data->contents()->Destroy(FINAL_STATUS_WOULD_HAVE_BEEN_USED); return NULL; } int child_id, route_id; CHECK(prerender_data->contents()->GetChildId(&child_id)); CHECK(prerender_data->contents()->GetRouteId(&route_id)); // Try to set the prerendered page as used, so any subsequent attempts to // cancel on other threads will fail. If this fails because the prerender // was already cancelled, possibly on another thread, fail. if (!prerender_tracker_->TryUse(child_id, route_id)) return NULL; // At this point, we've determined that we will use the prerender. if (prerender_data->pending_swap()) prerender_data->pending_swap()->set_swap_successful(true); ScopedVector<PrerenderData>::iterator to_erase = FindIteratorForPrerenderContents(prerender_data->contents()); DCHECK(active_prerenders_.end() != to_erase); DCHECK_EQ(prerender_data, *to_erase); scoped_ptr<PrerenderContents> prerender_contents(prerender_data->ReleaseContents()); active_prerenders_.erase(to_erase); if (!prerender_contents->load_start_time().is_null()) { histograms_->RecordTimeUntilUsed( prerender_contents->origin(), GetCurrentTimeTicks() - prerender_contents->load_start_time()); } histograms_->RecordPerSessionCount(prerender_contents->origin(), ++prerenders_per_session_count_); histograms_->RecordUsedPrerender(prerender_contents->origin()); prerender_contents->SetFinalStatus(FINAL_STATUS_USED); // Start pending prerender requests from the PrerenderContents, if there are // any. prerender_contents->PrepareForUse(); WebContents* new_web_contents = prerender_contents->ReleasePrerenderContents(); WebContents* old_web_contents = web_contents; DCHECK(new_web_contents); DCHECK(old_web_contents); MarkWebContentsAsPrerendered(new_web_contents, prerender_contents->origin()); // Merge the browsing history. new_web_contents->GetController().CopyStateFromAndPrune( &old_web_contents->GetController(), should_replace_current_entry); CoreTabHelper::FromWebContents(old_web_contents)->delegate()-> SwapTabContents(old_web_contents, new_web_contents); prerender_contents->CommitHistory(new_web_contents); GURL icon_url = prerender_contents->icon_url(); if (!icon_url.is_empty()) { #if defined(OS_ANDROID) // Do the delayed icon fetch since we didn't download // the favicon during prerendering on mobile devices. FaviconTabHelper * favicon_tap_helper = FaviconTabHelper::FromWebContents(new_web_contents); favicon_tap_helper->set_should_fetch_icons(true); favicon_tap_helper->FetchFavicon(icon_url); #endif // defined(OS_ANDROID) std::vector<content::FaviconURL> urls; urls.push_back(content::FaviconURL(icon_url, content::FaviconURL::FAVICON)); FaviconTabHelper::FromWebContents(new_web_contents)-> DidUpdateFaviconURL(prerender_contents->page_id(), urls); } // Update PPLT metrics: // If the tab has finished loading, record a PPLT of 0. // If the tab is still loading, reset its start time to the current time. PrerenderTabHelper* prerender_tab_helper = PrerenderTabHelper::FromWebContents(new_web_contents); DCHECK(prerender_tab_helper != NULL); prerender_tab_helper->PrerenderSwappedIn(); if (old_web_contents->NeedToFireBeforeUnload()) { // Schedule the delete to occur after the tab has run its unload handlers. // TODO(davidben): Honor the beforeunload event. http://crbug.com/304932 on_close_web_contents_deleters_.push_back( new OnCloseWebContentsDeleter(this, old_web_contents)); old_web_contents->GetRenderViewHost()-> FirePageBeforeUnload(false); } else { // No unload handler to run, so delete asap. ScheduleDeleteOldWebContents(old_web_contents, NULL); } // TODO(cbentzel): Should prerender_contents move to the pending delete // list, instead of deleting directly here? AddToHistory(prerender_contents.get()); RecordNavigation(url); return new_web_contents; } void PrerenderManager::MoveEntryToPendingDelete(PrerenderContents* entry, FinalStatus final_status) { DCHECK(CalledOnValidThread()); DCHECK(entry); ScopedVector<PrerenderData>::iterator it = FindIteratorForPrerenderContents(entry); DCHECK(it != active_prerenders_.end()); // If this PrerenderContents is being deleted due to a cancellation any time // after the prerender has started then we need to create a dummy replacement // for PPLT accounting purposes for the Match Complete group. This is the case // if the cancellation is for any reason that would not occur in the control // group case. if (entry->prerendering_has_started() && entry->match_complete_status() == PrerenderContents::MATCH_COMPLETE_DEFAULT && NeedMatchCompleteDummyForFinalStatus(final_status) && ActuallyPrerendering()) { // TODO(tburkard): I'd like to DCHECK that we are actually prerendering. // However, what if new conditions are added and // NeedMatchCompleteDummyForFinalStatus is not being updated. Not sure // what's the best thing to do here. For now, I will just check whether // we are actually prerendering. (*it)->MakeIntoMatchCompleteReplacement(); } else { to_delete_prerenders_.push_back(*it); active_prerenders_.weak_erase(it); } // Destroy the old WebContents relatively promptly to reduce resource usage. PostCleanupTask(); } // static void PrerenderManager::RecordPerceivedPageLoadTime( base::TimeDelta perceived_page_load_time, double fraction_plt_elapsed_at_swap_in, WebContents* web_contents, const GURL& url) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); PrerenderManager* prerender_manager = PrerenderManagerFactory::GetForProfile( Profile::FromBrowserContext(web_contents->GetBrowserContext())); if (!prerender_manager) return; if (!prerender_manager->IsEnabled()) return; Origin prerender_origin = ORIGIN_NONE; if (prerender_manager->IsWebContentsPrerendering(web_contents, &prerender_origin)) { prerender_manager->histograms_->RecordPageLoadTimeNotSwappedIn( prerender_origin, perceived_page_load_time, url); return; } bool was_prerender = prerender_manager->IsWebContentsPrerendered( web_contents, &prerender_origin); bool was_complete_prerender = was_prerender || prerender_manager->WouldWebContentsBePrerendered(web_contents, &prerender_origin); prerender_manager->histograms_->RecordPerceivedPageLoadTime( prerender_origin, perceived_page_load_time, was_prerender, was_complete_prerender, url); if (was_prerender) { prerender_manager->histograms_->RecordPercentLoadDoneAtSwapin( prerender_origin, fraction_plt_elapsed_at_swap_in); } if (prerender_manager->local_predictor_.get()) { prerender_manager->local_predictor_-> OnPLTEventForURL(url, perceived_page_load_time); } } void PrerenderManager::RecordFractionPixelsFinalAtSwapin( content::WebContents* web_contents, double fraction) { Origin origin = ORIGIN_NONE; bool is_prerendered = IsWebContentsPrerendered(web_contents, &origin); DCHECK(is_prerendered); histograms_->RecordFractionPixelsFinalAtSwapin(origin, fraction); } void PrerenderManager::set_enabled(bool enabled) { DCHECK(CalledOnValidThread()); enabled_ = enabled; } // static PrerenderManager::PrerenderManagerMode PrerenderManager::GetMode() { return mode_; } // static void PrerenderManager::SetMode(PrerenderManagerMode mode) { mode_ = mode; } // static const char* PrerenderManager::GetModeString() { switch (mode_) { case PRERENDER_MODE_DISABLED: return "_Disabled"; case PRERENDER_MODE_ENABLED: case PRERENDER_MODE_EXPERIMENT_PRERENDER_GROUP: return "_Enabled"; case PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP: return "_Control"; case PRERENDER_MODE_EXPERIMENT_MULTI_PRERENDER_GROUP: return "_Multi"; case PRERENDER_MODE_EXPERIMENT_15MIN_TTL_GROUP: return "_15MinTTL"; case PRERENDER_MODE_EXPERIMENT_NO_USE_GROUP: return "_NoUse"; case PRERENDER_MODE_MAX: default: NOTREACHED() << "Invalid PrerenderManager mode."; break; }; return ""; } // static bool PrerenderManager::IsPrerenderingPossible() { return GetMode() != PRERENDER_MODE_DISABLED; } // static bool PrerenderManager::ActuallyPrerendering() { return IsPrerenderingPossible() && !IsControlGroup(kNoExperiment); } // static bool PrerenderManager::IsControlGroup(uint8 experiment_id) { return GetMode() == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP || IsControlGroupExperiment(experiment_id); } // static bool PrerenderManager::IsNoUseGroup() { return GetMode() == PRERENDER_MODE_EXPERIMENT_NO_USE_GROUP; } bool PrerenderManager::IsWebContentsPrerendering( const WebContents* web_contents, Origin* origin) const { DCHECK(CalledOnValidThread()); if (PrerenderContents* prerender_contents = GetPrerenderContents(web_contents)) { if (origin) *origin = prerender_contents->origin(); return true; } // Also look through the pending-deletion list. for (ScopedVector<PrerenderData>::const_iterator it = to_delete_prerenders_.begin(); it != to_delete_prerenders_.end(); ++it) { if (PrerenderContents* prerender_contents = (*it)->contents()) { WebContents* prerender_web_contents = prerender_contents->prerender_contents(); if (prerender_web_contents == web_contents) { if (origin) *origin = prerender_contents->origin(); return true; } } } return false; } bool PrerenderManager::HasPrerenderedUrl( GURL url, content::WebContents* web_contents) const { content::SessionStorageNamespace* session_storage_namespace = web_contents-> GetController().GetDefaultSessionStorageNamespace(); for (ScopedVector<PrerenderData>::const_iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { PrerenderContents* prerender_contents = (*it)->contents(); if (prerender_contents->Matches(url, session_storage_namespace)) { return true; } } return false; } PrerenderContents* PrerenderManager::GetPrerenderContents( const content::WebContents* web_contents) const { DCHECK(CalledOnValidThread()); for (ScopedVector<PrerenderData>::const_iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { WebContents* prerender_web_contents = (*it)->contents()->prerender_contents(); if (prerender_web_contents == web_contents) { return (*it)->contents(); } } return NULL; } const std::vector<WebContents*> PrerenderManager::GetAllPrerenderingContents() const { DCHECK(CalledOnValidThread()); std::vector<WebContents*> result; for (ScopedVector<PrerenderData>::const_iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { if (WebContents* contents = (*it)->contents()->prerender_contents()) result.push_back(contents); } return result; } void PrerenderManager::MarkWebContentsAsPrerendered(WebContents* web_contents, Origin origin) { DCHECK(CalledOnValidThread()); prerendered_web_contents_data_.insert( base::hash_map<content::WebContents*, PrerenderedWebContentsData>::value_type( web_contents, PrerenderedWebContentsData(origin))); } void PrerenderManager::MarkWebContentsAsWouldBePrerendered( WebContents* web_contents, Origin origin) { DCHECK(CalledOnValidThread()); would_be_prerendered_map_.insert( base::hash_map<content::WebContents*, WouldBePrerenderedWebContentsData>::value_type( web_contents, WouldBePrerenderedWebContentsData(origin))); } void PrerenderManager::MarkWebContentsAsNotPrerendered( WebContents* web_contents) { DCHECK(CalledOnValidThread()); prerendered_web_contents_data_.erase(web_contents); base::hash_map<content::WebContents*, WouldBePrerenderedWebContentsData>:: iterator it = would_be_prerendered_map_.find(web_contents); if (it != would_be_prerendered_map_.end()) { if (it->second.state == WouldBePrerenderedWebContentsData::WAITING_FOR_PROVISIONAL_LOAD) { it->second.state = WouldBePrerenderedWebContentsData::SEEN_PROVISIONAL_LOAD; } else { would_be_prerendered_map_.erase(it); } } } bool PrerenderManager::IsWebContentsPrerendered( content::WebContents* web_contents, Origin* origin) const { DCHECK(CalledOnValidThread()); base::hash_map<content::WebContents*, PrerenderedWebContentsData>:: const_iterator it = prerendered_web_contents_data_.find(web_contents); if (it == prerendered_web_contents_data_.end()) return false; if (origin) *origin = it->second.origin; return true; } bool PrerenderManager::WouldWebContentsBePrerendered( WebContents* web_contents, Origin* origin) const { DCHECK(CalledOnValidThread()); base::hash_map<content::WebContents*, WouldBePrerenderedWebContentsData>:: const_iterator it = would_be_prerendered_map_.find(web_contents); if (it == would_be_prerendered_map_.end()) return false; if (origin) *origin = it->second.origin; return true; } bool PrerenderManager::HasRecentlyBeenNavigatedTo(Origin origin, const GURL& url) { DCHECK(CalledOnValidThread()); CleanUpOldNavigations(); std::list<NavigationRecord>::const_reverse_iterator end = navigations_.rend(); for (std::list<NavigationRecord>::const_reverse_iterator it = navigations_.rbegin(); it != end; ++it) { if (it->url == url) { base::TimeDelta delta = GetCurrentTimeTicks() - it->time; histograms_->RecordTimeSinceLastRecentVisit(origin, delta); return true; } } return false; } // static bool PrerenderManager::IsValidHttpMethod(const std::string& method) { // method has been canonicalized to upper case at this point so we can just // compare them. DCHECK_EQ(method, StringToUpperASCII(method)); for (size_t i = 0; i < arraysize(kValidHttpMethods); ++i) { if (method.compare(kValidHttpMethods[i]) == 0) return true; } return false; } // static bool PrerenderManager::DoesURLHaveValidScheme(const GURL& url) { return (url.SchemeIsHTTPOrHTTPS() || url.SchemeIs(extensions::kExtensionScheme) || url.SchemeIs("data")); } // static bool PrerenderManager::DoesSubresourceURLHaveValidScheme(const GURL& url) { return DoesURLHaveValidScheme(url) || url == GURL(content::kAboutBlankURL); } DictionaryValue* PrerenderManager::GetAsValue() const { DCHECK(CalledOnValidThread()); DictionaryValue* dict_value = new DictionaryValue(); dict_value->Set("history", prerender_history_->GetEntriesAsValue()); dict_value->Set("active", GetActivePrerendersAsValue()); dict_value->SetBoolean("enabled", enabled_); dict_value->SetBoolean("omnibox_enabled", IsOmniboxEnabled(profile_)); // If prerender is disabled via a flag this method is not even called. std::string enabled_note; if (IsControlGroup(kNoExperiment)) enabled_note += "(Control group: Not actually prerendering) "; if (IsNoUseGroup()) enabled_note += "(No-use group: Not swapping in prerendered pages) "; if (GetMode() == PRERENDER_MODE_EXPERIMENT_15MIN_TTL_GROUP) enabled_note += "(15 min TTL group: Extended prerender eviction to 15 mins) "; dict_value->SetString("enabled_note", enabled_note); return dict_value; } void PrerenderManager::ClearData(int clear_flags) { DCHECK_GE(clear_flags, 0); DCHECK_LT(clear_flags, CLEAR_MAX); if (clear_flags & CLEAR_PRERENDER_CONTENTS) DestroyAllContents(FINAL_STATUS_CACHE_OR_HISTORY_CLEARED); // This has to be second, since destroying prerenders can add to the history. if (clear_flags & CLEAR_PRERENDER_HISTORY) prerender_history_->Clear(); } void PrerenderManager::RecordFinalStatusWithMatchCompleteStatus( Origin origin, uint8 experiment_id, PrerenderContents::MatchCompleteStatus mc_status, FinalStatus final_status) const { histograms_->RecordFinalStatus(origin, experiment_id, mc_status, final_status); } void PrerenderManager::AddCondition(const PrerenderCondition* condition) { prerender_conditions_.push_back(condition); } void PrerenderManager::RecordNavigation(const GURL& url) { DCHECK(CalledOnValidThread()); navigations_.push_back(NavigationRecord(url, GetCurrentTimeTicks())); CleanUpOldNavigations(); } // protected struct PrerenderManager::PrerenderData::OrderByExpiryTime { bool operator()(const PrerenderData* a, const PrerenderData* b) const { return a->expiry_time() < b->expiry_time(); } }; PrerenderManager::PrerenderData::PrerenderData(PrerenderManager* manager, PrerenderContents* contents, base::TimeTicks expiry_time) : manager_(manager), contents_(contents), handle_count_(0), expiry_time_(expiry_time) { DCHECK_NE(static_cast<PrerenderContents*>(NULL), contents_); } PrerenderManager::PrerenderData::~PrerenderData() { } void PrerenderManager::PrerenderData::MakeIntoMatchCompleteReplacement() { DCHECK(contents_); contents_->set_match_complete_status( PrerenderContents::MATCH_COMPLETE_REPLACED); PrerenderData* to_delete = new PrerenderData(manager_, contents_.release(), expiry_time_); contents_.reset(to_delete->contents_->CreateMatchCompleteReplacement()); manager_->to_delete_prerenders_.push_back(to_delete); } void PrerenderManager::PrerenderData::OnHandleCreated(PrerenderHandle* handle) { DCHECK_NE(static_cast<PrerenderContents*>(NULL), contents_); ++handle_count_; contents_->AddObserver(handle); } void PrerenderManager::PrerenderData::OnHandleNavigatedAway( PrerenderHandle* handle) { DCHECK_LT(0, handle_count_); DCHECK_NE(static_cast<PrerenderContents*>(NULL), contents_); // We intentionally don't decrement the handle count here, so that the // prerender won't be canceled until it times out. manager_->SourceNavigatedAway(this); } void PrerenderManager::PrerenderData::OnHandleCanceled( PrerenderHandle* handle) { DCHECK_LT(0, handle_count_); DCHECK_NE(static_cast<PrerenderContents*>(NULL), contents_); if (--handle_count_ == 0) { // This will eventually remove this object from active_prerenders_. contents_->Destroy(FINAL_STATUS_CANCELLED); } } void PrerenderManager::PrerenderData::ClearPendingSwap() { pending_swap_.reset(NULL); } PrerenderContents* PrerenderManager::PrerenderData::ReleaseContents() { return contents_.release(); } PrerenderManager::PendingSwap::PendingSwap( PrerenderManager* manager, content::WebContents* target_contents, PrerenderData* prerender_data, const GURL& url, bool should_replace_current_entry) : content::WebContentsObserver(target_contents), manager_(manager), target_contents_(target_contents), prerender_data_(prerender_data), url_(url), should_replace_current_entry_(should_replace_current_entry), start_time_(base::TimeTicks::Now()), swap_successful_(false), weak_factory_(this) { RenderViewCreated(target_contents->GetRenderViewHost()); } PrerenderManager::PendingSwap::~PendingSwap() { for (size_t i = 0; i < rvh_ids_.size(); i++) { manager_->prerender_tracker()->RemovePrerenderPendingSwap( rvh_ids_[i], swap_successful_); } } void PrerenderManager::PendingSwap::BeginSwap() { SessionStorageNamespace* target_namespace = target_contents_->GetController().GetDefaultSessionStorageNamespace(); SessionStorageNamespace* prerender_namespace = prerender_data_->contents()->GetSessionStorageNamespace(); prerender_namespace->Merge( true, prerender_data_->contents()->child_id(), target_namespace, base::Bind(&PrerenderManager::PendingSwap::OnMergeCompleted, weak_factory_.GetWeakPtr())); merge_timeout_.Start( FROM_HERE, base::TimeDelta::FromMilliseconds( kSessionStorageNamespaceMergeTimeoutMs), this, &PrerenderManager::PendingSwap::OnMergeTimeout); } void PrerenderManager::PendingSwap::ProvisionalChangeToMainFrameUrl( const GURL& url, content::RenderViewHost* render_view_host) { // We must only cancel the pending swap if the |url| navigated to is not // the URL being attempted to be swapped in. That's because in the normal // flow, a ProvisionalChangeToMainFrameUrl will happen for the URL attempted // to be swapped in immediately after the pending swap has issued its merge. if (url != url_) prerender_data_->ClearPendingSwap(); } void PrerenderManager::PendingSwap::DidCommitProvisionalLoadForFrame( int64 frame_id, const base::string16& frame_unique_name, bool is_main_frame, const GURL& validated_url, content::PageTransition transition_type, content::RenderViewHost* render_view_host){ if (!is_main_frame) return; prerender_data_->ClearPendingSwap(); } void PrerenderManager::PendingSwap::RenderViewCreated( content::RenderViewHost* render_view_host) { // Record the RVH id in the tracker to install throttles on MAIN_FRAME // requests from that route. int child_id = render_view_host->GetProcess()->GetID(); int route_id = render_view_host->GetRoutingID(); PrerenderTracker::ChildRouteIdPair child_route_id_pair(child_id, route_id); rvh_ids_.push_back(child_route_id_pair); manager_->prerender_tracker()->AddPrerenderPendingSwap( child_route_id_pair, url_); } void PrerenderManager::PendingSwap::DidFailProvisionalLoad( int64 frame_id, const base::string16& frame_unique_name, bool is_main_frame, const GURL& validated_url, int error_code, const base::string16& error_description, content::RenderViewHost* render_view_host) { if (!is_main_frame) return; prerender_data_->ClearPendingSwap(); } void PrerenderManager::PendingSwap::WebContentsDestroyed( content::WebContents* web_contents) { prerender_data_->ClearPendingSwap(); } void PrerenderManager::PendingSwap::RecordEvent(PrerenderEvent event) const { manager_->RecordEvent(prerender_data_->contents(), event); } void PrerenderManager::PendingSwap::OnMergeCompleted( SessionStorageNamespace::MergeResult result) { UMA_HISTOGRAM_TIMES("Prerender.SessionStorageNamespaceMergeTime", base::TimeTicks::Now() - start_time_); RecordEvent(PRERENDER_EVENT_MERGE_RESULT_MERGE_DONE); // Log the exact merge result in a histogram. switch (result) { case SessionStorageNamespace::MERGE_RESULT_NAMESPACE_NOT_FOUND: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_NAMESPACE_NOT_FOUND); break; case SessionStorageNamespace::MERGE_RESULT_NAMESPACE_NOT_ALIAS: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_NAMESPACE_NOT_ALIAS); break; case SessionStorageNamespace::MERGE_RESULT_NOT_LOGGING: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_NOT_LOGGING); break; case SessionStorageNamespace::MERGE_RESULT_NO_TRANSACTIONS: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_NO_TRANSACTIONS); break; case SessionStorageNamespace::MERGE_RESULT_TOO_MANY_TRANSACTIONS: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_TOO_MANY_TRANSACTIONS); break; case SessionStorageNamespace::MERGE_RESULT_NOT_MERGEABLE: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_NOT_MERGEABLE); break; case SessionStorageNamespace::MERGE_RESULT_MERGEABLE: RecordEvent(PRERENDER_EVENT_MERGE_RESULT_RESULT_MERGEABLE); break; default: NOTREACHED(); } if (result != SessionStorageNamespace::MERGE_RESULT_MERGEABLE && result != SessionStorageNamespace::MERGE_RESULT_NO_TRANSACTIONS) { RecordEvent(PRERENDER_EVENT_MERGE_RESULT_MERGE_FAILED); prerender_data_->ClearPendingSwap(); return; } RecordEvent(PRERENDER_EVENT_MERGE_RESULT_SWAPPING_IN); // Note that SwapInternal, on success, will delete |prerender_data_| and // |this|. Pass in a new GURL object rather than a reference to |url_|. // // TODO(davidben): See about deleting PrerenderData asynchronously so this // behavior is more reasonable. WebContents* new_web_contents = manager_->SwapInternal(GURL(url_), target_contents_, prerender_data_, should_replace_current_entry_); if (!new_web_contents) { RecordEvent(PRERENDER_EVENT_MERGE_RESULT_SWAPIN_FAILED); prerender_data_->ClearPendingSwap(); } } void PrerenderManager::PendingSwap::OnMergeTimeout() { RecordEvent(PRERENDER_EVENT_MERGE_RESULT_TIMED_OUT); prerender_data_->ClearPendingSwap(); } void PrerenderManager::SetPrerenderContentsFactory( PrerenderContents::Factory* prerender_contents_factory) { DCHECK(CalledOnValidThread()); prerender_contents_factory_.reset(prerender_contents_factory); } void PrerenderManager::StartPendingPrerenders( const int process_id, ScopedVector<PrerenderContents::PendingPrerenderInfo>* pending_prerenders, content::SessionStorageNamespace* session_storage_namespace) { for (ScopedVector<PrerenderContents::PendingPrerenderInfo>::iterator it = pending_prerenders->begin(); it != pending_prerenders->end(); ++it) { PrerenderContents::PendingPrerenderInfo* info = *it; PrerenderHandle* existing_prerender_handle = info->weak_prerender_handle.get(); if (!existing_prerender_handle) continue; DCHECK(!existing_prerender_handle->IsPrerendering()); DCHECK(process_id == -1 || session_storage_namespace); scoped_ptr<PrerenderHandle> new_prerender_handle(AddPrerender( info->origin, process_id, info->url, info->referrer, info->size, session_storage_namespace)); if (new_prerender_handle) { // AddPrerender has returned a new prerender handle to us. We want to make // |existing_prerender_handle| active, so move the underlying // PrerenderData to our new handle. existing_prerender_handle->AdoptPrerenderDataFrom( new_prerender_handle.get()); continue; } } } void PrerenderManager::SourceNavigatedAway(PrerenderData* prerender_data) { // The expiry time of our prerender data will likely change because of // this navigation. This requires a resort of active_prerenders_. ScopedVector<PrerenderData>::iterator it = std::find(active_prerenders_.begin(), active_prerenders_.end(), prerender_data); if (it == active_prerenders_.end()) return; (*it)->set_expiry_time( std::min((*it)->expiry_time(), GetExpiryTimeForNavigatedAwayPrerender())); SortActivePrerenders(); } // private PrerenderHandle* PrerenderManager::AddPrerender( Origin origin, int process_id, const GURL& url_arg, const content::Referrer& referrer, const gfx::Size& size, SessionStorageNamespace* session_storage_namespace) { DCHECK(CalledOnValidThread()); if (!IsEnabled()) return NULL; if ((origin == ORIGIN_LINK_REL_PRERENDER_CROSSDOMAIN || origin == ORIGIN_LINK_REL_PRERENDER_SAMEDOMAIN) && IsGoogleSearchResultURL(referrer.url)) { origin = ORIGIN_GWS_PRERENDER; } GURL url = url_arg; GURL alias_url; uint8 experiment = GetQueryStringBasedExperiment(url_arg); if (IsControlGroup(experiment) && MaybeGetQueryStringBasedAliasURL(url, &alias_url)) { url = alias_url; } // From here on, we will record a FinalStatus so we need to register with the // histogram tracking. histograms_->RecordPrerender(origin, url_arg); if (PrerenderData* preexisting_prerender_data = FindPrerenderData(url, session_storage_namespace)) { RecordFinalStatus(origin, experiment, FINAL_STATUS_DUPLICATE); return new PrerenderHandle(preexisting_prerender_data); } // 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. // On Android we do reuse processes as we have a limited number of them and we // still want the benefits of prerendering even when several tabs are open. #if !defined(OS_ANDROID) if (content::RenderProcessHost::ShouldTryToUseExistingProcessHost( profile_, url) && !content::RenderProcessHost::run_renderer_in_process()) { RecordFinalStatus(origin, experiment, FINAL_STATUS_TOO_MANY_PROCESSES); return NULL; } #endif // Check if enough time has passed since the last prerender. if (!DoesRateLimitAllowPrerender(origin)) { // 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(origin, experiment, FINAL_STATUS_RATE_LIMIT_EXCEEDED); return NULL; } PrerenderContents* prerender_contents = CreatePrerenderContents( url, referrer, origin, experiment); DCHECK(prerender_contents); active_prerenders_.push_back( new PrerenderData(this, prerender_contents, GetExpiryTimeForNewPrerender(origin))); if (!prerender_contents->Init()) { DCHECK(active_prerenders_.end() == FindIteratorForPrerenderContents(prerender_contents)); return NULL; } histograms_->RecordPrerenderStarted(origin); DCHECK(!prerender_contents->prerendering_has_started()); PrerenderHandle* prerender_handle = new PrerenderHandle(active_prerenders_.back()); SortActivePrerenders(); last_prerender_start_time_ = GetCurrentTimeTicks(); gfx::Size contents_size = size.IsEmpty() ? config_.default_tab_bounds.size() : size; prerender_contents->StartPrerendering(process_id, contents_size, session_storage_namespace); DCHECK(IsControlGroup(experiment) || prerender_contents->prerendering_has_started()); if (GetMode() == PRERENDER_MODE_EXPERIMENT_MULTI_PRERENDER_GROUP) histograms_->RecordConcurrency(active_prerenders_.size()); // Query the history to see if the URL being prerendered has ever been // visited before. HistoryService* history_service = HistoryServiceFactory::GetForProfile( profile_, Profile::EXPLICIT_ACCESS); if (history_service) { history_service->QueryURL( url, false, &query_url_consumer_, base::Bind(&PrerenderManager::OnHistoryServiceDidQueryURL, base::Unretained(this), origin, experiment)); } StartSchedulingPeriodicCleanups(); return prerender_handle; } void PrerenderManager::StartSchedulingPeriodicCleanups() { DCHECK(CalledOnValidThread()); if (repeating_timer_.IsRunning()) return; repeating_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kPeriodicCleanupIntervalMs), this, &PrerenderManager::PeriodicCleanup); } void PrerenderManager::StopSchedulingPeriodicCleanups() { DCHECK(CalledOnValidThread()); repeating_timer_.Stop(); } void PrerenderManager::PeriodicCleanup() { DCHECK(CalledOnValidThread()); base::ElapsedTimer resource_timer; // 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(active_prerenders_.size()); std::transform(active_prerenders_.begin(), active_prerenders_.end(), prerender_contents.begin(), std::mem_fun(&PrerenderData::contents)); // And now check for prerenders using too much memory. std::for_each(prerender_contents.begin(), prerender_contents.end(), std::mem_fun( &PrerenderContents::DestroyWhenUsingTooManyResources)); // Measure how long the resource checks took. http://crbug.com/305419. UMA_HISTOGRAM_TIMES("Prerender.PeriodicCleanupResourceCheckTime", resource_timer.Elapsed()); base::ElapsedTimer cleanup_timer; // Perform deferred cleanup work. DeleteOldWebContents(); DeleteOldEntries(); if (active_prerenders_.empty()) StopSchedulingPeriodicCleanups(); to_delete_prerenders_.clear(); // Measure how long a the various cleanup tasks took. http://crbug.com/305419. UMA_HISTOGRAM_TIMES("Prerender.PeriodicCleanupDeleteContentsTime", cleanup_timer.Elapsed()); } void PrerenderManager::PostCleanupTask() { DCHECK(CalledOnValidThread()); base::MessageLoop::current()->PostTask( FROM_HERE, base::Bind(&PrerenderManager::PeriodicCleanup, AsWeakPtr())); } base::TimeTicks PrerenderManager::GetExpiryTimeForNewPrerender( Origin origin) const { base::TimeDelta ttl = config_.time_to_live; if (origin == ORIGIN_LOCAL_PREDICTOR) ttl = base::TimeDelta::FromSeconds(GetLocalPredictorTTLSeconds()); return GetCurrentTimeTicks() + ttl; } base::TimeTicks PrerenderManager::GetExpiryTimeForNavigatedAwayPrerender() const { return GetCurrentTimeTicks() + config_.abandon_time_to_live; } void PrerenderManager::DeleteOldEntries() { DCHECK(CalledOnValidThread()); while (!active_prerenders_.empty()) { PrerenderData* prerender_data = active_prerenders_.front(); DCHECK(prerender_data); DCHECK(prerender_data->contents()); if (prerender_data->expiry_time() > GetCurrentTimeTicks()) return; prerender_data->contents()->Destroy(FINAL_STATUS_TIMED_OUT); } } base::Time PrerenderManager::GetCurrentTime() const { return base::Time::Now(); } base::TimeTicks PrerenderManager::GetCurrentTimeTicks() const { return base::TimeTicks::Now(); } PrerenderContents* PrerenderManager::CreatePrerenderContents( const GURL& url, const content::Referrer& referrer, Origin origin, uint8 experiment_id) { DCHECK(CalledOnValidThread()); return prerender_contents_factory_->CreatePrerenderContents( this, profile_, url, referrer, origin, experiment_id); } void PrerenderManager::SortActivePrerenders() { std::sort(active_prerenders_.begin(), active_prerenders_.end(), PrerenderData::OrderByExpiryTime()); } PrerenderManager::PrerenderData* PrerenderManager::FindPrerenderData( const GURL& url, const SessionStorageNamespace* session_storage_namespace) { for (ScopedVector<PrerenderData>::iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { if ((*it)->contents()->Matches(url, session_storage_namespace)) return *it; } return NULL; } PrerenderManager::PrerenderData* PrerenderManager::FindPrerenderDataForChildAndRoute( const int child_id, const int route_id) { for (ScopedVector<PrerenderData>::iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { PrerenderContents* prerender_contents = (*it)->contents(); int contents_child_id; if (!prerender_contents->GetChildId(&contents_child_id)) continue; int contents_route_id; if (!prerender_contents->GetRouteId(&contents_route_id)) continue; if (contents_child_id == child_id && contents_route_id == route_id) return *it; } return NULL; } ScopedVector<PrerenderManager::PrerenderData>::iterator PrerenderManager::FindIteratorForPrerenderContents( PrerenderContents* prerender_contents) { for (ScopedVector<PrerenderData>::iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { if (prerender_contents == (*it)->contents()) return it; } return active_prerenders_.end(); } bool PrerenderManager::DoesRateLimitAllowPrerender(Origin origin) const { DCHECK(CalledOnValidThread()); base::TimeDelta elapsed_time = GetCurrentTimeTicks() - last_prerender_start_time_; histograms_->RecordTimeBetweenPrerenderRequests(origin, elapsed_time); if (!config_.rate_limit_enabled) return true; return elapsed_time >= base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs); } void PrerenderManager::DeleteOldWebContents() { while (!old_web_contents_list_.empty()) { WebContents* web_contents = old_web_contents_list_.front(); old_web_contents_list_.pop_front(); // TODO(dominich): should we use Instant Unload Handler here? delete web_contents; } } void PrerenderManager::CleanUpOldNavigations() { DCHECK(CalledOnValidThread()); // Cutoff. Navigations before this cutoff can be discarded. base::TimeTicks cutoff = GetCurrentTimeTicks() - base::TimeDelta::FromMilliseconds(kNavigationRecordWindowMs); while (!navigations_.empty()) { if (navigations_.front().time > cutoff) break; navigations_.pop_front(); } } void PrerenderManager::ScheduleDeleteOldWebContents( WebContents* tab, OnCloseWebContentsDeleter* deleter) { old_web_contents_list_.push_back(tab); PostCleanupTask(); if (deleter) { ScopedVector<OnCloseWebContentsDeleter>::iterator i = std::find( on_close_web_contents_deleters_.begin(), on_close_web_contents_deleters_.end(), deleter); DCHECK(i != on_close_web_contents_deleters_.end()); on_close_web_contents_deleters_.erase(i); } } void PrerenderManager::AddToHistory(PrerenderContents* contents) { PrerenderHistory::Entry entry(contents->prerender_url(), contents->final_status(), contents->origin(), base::Time::Now()); prerender_history_->AddEntry(entry); } Value* PrerenderManager::GetActivePrerendersAsValue() const { ListValue* list_value = new ListValue(); for (ScopedVector<PrerenderData>::const_iterator it = active_prerenders_.begin(); it != active_prerenders_.end(); ++it) { if (Value* prerender_value = (*it)->contents()->GetAsValue()) list_value->Append(prerender_value); } return list_value; } void PrerenderManager::DestroyAllContents(FinalStatus final_status) { DeleteOldWebContents(); while (!active_prerenders_.empty()) { PrerenderContents* contents = active_prerenders_.front()->contents(); contents->Destroy(final_status); } to_delete_prerenders_.clear(); } void PrerenderManager::DestroyAndMarkMatchCompleteAsUsed( PrerenderContents* prerender_contents, FinalStatus final_status) { prerender_contents->set_match_complete_status( PrerenderContents::MATCH_COMPLETE_REPLACED); histograms_->RecordFinalStatus(prerender_contents->origin(), prerender_contents->experiment_id(), PrerenderContents::MATCH_COMPLETE_REPLACEMENT, FINAL_STATUS_WOULD_HAVE_BEEN_USED); prerender_contents->Destroy(final_status); } void PrerenderManager::RecordFinalStatus(Origin origin, uint8 experiment_id, FinalStatus final_status) const { RecordFinalStatusWithMatchCompleteStatus( origin, experiment_id, PrerenderContents::MATCH_COMPLETE_DEFAULT, final_status); } bool PrerenderManager::IsEnabled() const { DCHECK(CalledOnValidThread()); if (!enabled_) return false; for (std::list<const PrerenderCondition*>::const_iterator it = prerender_conditions_.begin(); it != prerender_conditions_.end(); ++it) { const PrerenderCondition* condition = *it; if (!condition->CanPrerender()) return false; } return true; } PrerenderManager* FindPrerenderManagerUsingRenderProcessId( int render_process_id) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); content::RenderProcessHost* render_process_host = content::RenderProcessHost::FromID(render_process_id); // Each render process is guaranteed to only hold RenderViews owned by the // same BrowserContext. This is enforced by // RenderProcessHost::GetExistingProcessHost. if (!render_process_host || !render_process_host->GetBrowserContext()) return NULL; Profile* profile = Profile::FromBrowserContext( render_process_host->GetBrowserContext()); if (!profile) return NULL; return PrerenderManagerFactory::GetInstance()->GetForProfile(profile); } void PrerenderManager::Observe(int type, const content::NotificationSource& source, const content::NotificationDetails& details) { Profile* profile = content::Source<Profile>(source).ptr(); if (!profile || !profile_->IsSameProfile(profile) || profile->IsOffTheRecord()) { return; } DCHECK(type == chrome::NOTIFICATION_COOKIE_CHANGED); CookieChanged(content::Details<ChromeCookieDetails>(details).ptr()); } void PrerenderManager::OnCreatingAudioStream(int render_process_id, int render_view_id) { WebContents* tab = tab_util::GetWebContentsByID( render_process_id, render_view_id); if (!tab) return; if (!IsWebContentsPrerendering(tab, NULL)) return; prerender_tracker()->TryCancel( render_process_id, render_view_id, prerender::FINAL_STATUS_CREATING_AUDIO_STREAM); } void PrerenderManager::RecordLikelyLoginOnURL(const GURL& url) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!url.SchemeIsHTTPOrHTTPS()) return; if (logged_in_predictor_table_.get()) { BrowserThread::PostTask( BrowserThread::DB, FROM_HERE, base::Bind(&LoggedInPredictorTable::AddDomainFromURL, logged_in_predictor_table_, url)); } std::string key = LoggedInPredictorTable::GetKey(url); if (!logged_in_state_.get()) return; if (logged_in_state_->count(key)) return; (*logged_in_state_)[key] = base::Time::Now().ToInternalValue(); } void PrerenderManager::CheckIfLikelyLoggedInOnURL( const GURL& url, bool* lookup_result, bool* database_was_present, const base::Closure& result_cb) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!logged_in_predictor_table_.get()) { *database_was_present = false; *lookup_result = false; BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, result_cb); return; } BrowserThread::PostTaskAndReply( BrowserThread::DB, FROM_HERE, base::Bind(&LoggedInPredictorTable::HasUserLoggedIn, logged_in_predictor_table_, url, lookup_result, database_was_present), result_cb); } void PrerenderManager::CookieChanged(ChromeCookieDetails* details) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (!logged_in_predictor_table_.get()) return; // We only care when a cookie has been removed. if (!details->removed) return; std::string domain_key = LoggedInPredictorTable::GetKeyFromDomain(details->cookie->Domain()); // If we have no record of this domain as a potentially logged in domain, // nothing to do here. if (logged_in_state_.get() && logged_in_state_->count(domain_key) < 1) return; net::URLRequestContextGetter* rq_context = profile_->GetRequestContext(); if (!rq_context) return; BrowserThread::PostTask( BrowserThread::IO, FROM_HERE, base::Bind(&CheckIfCookiesExistForDomainOnIOThread, base::Unretained(rq_context), domain_key, base::Bind( &PrerenderManager::CookieChangedAnyCookiesLeftLookupResult, AsWeakPtr(), domain_key) )); } void PrerenderManager::CookieChangedAnyCookiesLeftLookupResult( const std::string& domain_key, bool cookies_exist) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); if (cookies_exist) return; if (logged_in_predictor_table_.get()) { BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, base::Bind(&LoggedInPredictorTable::DeleteDomain, logged_in_predictor_table_, domain_key)); } if (logged_in_state_.get()) logged_in_state_->erase(domain_key); } void PrerenderManager::LoggedInPredictorDataReceived( scoped_ptr<LoggedInStateMap> new_map) { DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI)); logged_in_state_.swap(new_map); } void PrerenderManager::RecordEvent(PrerenderContents* contents, PrerenderEvent event) const { if (!contents) histograms_->RecordEvent(ORIGIN_NONE, kNoExperiment, event); else histograms_->RecordEvent(contents->origin(), contents->experiment_id(), event); } // static void PrerenderManager::RecordCookieEvent(int process_id, int render_view_id, const GURL& url, const GURL& frame_url, PrerenderContents::CookieEvent event, const net::CookieList* cookie_list) { RenderViewHost* rvh = RenderViewHost::FromID(process_id, render_view_id); if (!rvh) return; WebContents* web_contents = WebContents::FromRenderViewHost(rvh); if (!web_contents) return; bool is_main_frame = true; PrerenderManager* prerender_manager = PrerenderManagerFactory::GetForProfile( Profile::FromBrowserContext(web_contents->GetBrowserContext())); if (!prerender_manager) return; PrerenderContents* prerender_contents = prerender_manager->GetPrerenderContents(web_contents); if (!prerender_contents) return; base::Time earliest_create_date; if (event == PrerenderContents::COOKIE_EVENT_SEND) { if (!cookie_list || cookie_list->empty()) return; for (size_t i = 0; i < cookie_list->size(); i++) { if (earliest_create_date.is_null() || (*cookie_list)[i].CreationDate() < earliest_create_date) { earliest_create_date = (*cookie_list)[i].CreationDate(); } } } prerender_contents->RecordCookieEvent(event, is_main_frame && url == frame_url, earliest_create_date); } void PrerenderManager::RecordCookieStatus(Origin origin, uint8 experiment_id, int cookie_status) const { histograms_->RecordCookieStatus(origin, experiment_id, cookie_status); } void PrerenderManager::OnHistoryServiceDidQueryURL( Origin origin, uint8 experiment_id, CancelableRequestProvider::Handle handle, bool success, const history::URLRow* url_row, history::VisitVector* visists) { histograms_->RecordPrerenderPageVisitedStatus(origin, experiment_id, success); } } // namespace prerender