1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/prerender/prerender_local_predictor.h"
12 #include "base/metrics/histogram.h"
13 #include "base/timer.h"
14 #include "chrome/browser/prerender/prerender_histograms.h"
15 #include "chrome/browser/prerender/prerender_manager.h"
16 #include "chrome/browser/profiles/profile.h"
17 #include "chrome/browser/history/history.h"
18 #include "chrome/browser/history/history_database.h"
19 #include "chrome/browser/history/history_service_factory.h"
20 #include "content/public/browser/browser_thread.h"
21 #include "content/public/common/page_transition_types.h"
23 using content::BrowserThread
;
24 using content::PageTransition
;
31 // Task to lookup the URL for a given URLID.
32 class GetURLForURLIDTask
: public HistoryDBTask
{
34 GetURLForURLIDTask(URLID url_id
, base::Callback
<void(const GURL
&)> callback
)
38 start_time_(base::Time::Now()) {
41 virtual bool RunOnDBThread(history::HistoryBackend
* backend
,
42 history::HistoryDatabase
* db
) OVERRIDE
{
43 history::URLRow url_row
;
44 success_
= db
->GetURLRow(url_id_
, &url_row
);
50 virtual void DoneRunOnMainThread() OVERRIDE
{
53 UMA_HISTOGRAM_CUSTOM_TIMES("Prerender.LocalPredictorURLLookupTime",
54 base::Time::Now() - start_time_
,
55 base::TimeDelta::FromMilliseconds(10),
56 base::TimeDelta::FromSeconds(10),
62 virtual ~GetURLForURLIDTask() {}
66 base::Callback
<void(const GURL
&)> callback_
;
67 base::Time start_time_
;
69 DISALLOW_COPY_AND_ASSIGN(GetURLForURLIDTask
);
72 // Task to load history from the visit database on startup.
73 class GetVisitHistoryTask
: public HistoryDBTask
{
75 GetVisitHistoryTask(PrerenderLocalPredictor
* local_predictor
,
77 : local_predictor_(local_predictor
),
78 max_visits_(max_visits
),
79 visit_history_(new std::vector
<history::BriefVisitInfo
>) {
82 virtual bool RunOnDBThread(history::HistoryBackend
* backend
,
83 history::HistoryDatabase
* db
) OVERRIDE
{
84 db
->GetBriefVisitInfoOfMostRecentVisits(max_visits_
, visit_history_
.get());
88 virtual void DoneRunOnMainThread() OVERRIDE
{
89 local_predictor_
->OnGetInitialVisitHistory(visit_history_
.Pass());
93 virtual ~GetVisitHistoryTask() {}
95 PrerenderLocalPredictor
* local_predictor_
;
97 scoped_ptr
<std::vector
<history::BriefVisitInfo
> > visit_history_
;
98 DISALLOW_COPY_AND_ASSIGN(GetVisitHistoryTask
);
101 // Maximum visit history to retrieve from the visit database.
102 const int kMaxVisitHistory
= 100 * 1000;
104 // Visit history size at which to trigger pruning, and number of items to prune.
105 const int kVisitHistoryPruneThreshold
= 120 * 1000;
106 const int kVisitHistoryPruneAmount
= 20 * 1000;
108 const int kMaxLocalPredictionTimeMs
= 300 * 1000;
109 const int kMinLocalPredictionTimeMs
= 500;
111 bool IsBackForward(PageTransition transition
) {
112 return (transition
& content::PAGE_TRANSITION_FORWARD_BACK
) != 0;
115 bool IsHomePage(PageTransition transition
) {
116 return (transition
& content::PAGE_TRANSITION_HOME_PAGE
) != 0;
119 bool IsIntermediateRedirect(PageTransition transition
) {
120 return (transition
& content::PAGE_TRANSITION_CHAIN_END
) == 0;
123 bool ShouldExcludeTransitionForPrediction(PageTransition transition
) {
124 return IsBackForward(transition
) || IsHomePage(transition
) ||
125 IsIntermediateRedirect(transition
);
128 base::Time
GetCurrentTime() {
129 return base::Time::Now();
132 bool StrCaseStr(std::string haystack
, std::string needle
) {
133 std::transform(haystack
.begin(), haystack
.end(), haystack
.begin(), ::tolower
);
134 std::transform(needle
.begin(), needle
.end(), needle
.begin(), ::tolower
);
135 return (haystack
.find(needle
)!=std::string::npos
);
138 bool IsRootPageURL(const GURL
& url
) {
139 return (url
.path() == "/" || url
.path() == "") && (!url
.has_query()) &&
145 struct PrerenderLocalPredictor::PrerenderData
{
146 PrerenderData(URLID url_id
, const GURL
& url
, double priority
,
147 base::Time start_time
)
151 start_time(start_time
) {
157 // For expiration purposes, this is a synthetic start time consisting either
158 // of the actual start time, or of the last time the page was re-requested
159 // for prerendering - 10 seconds (unless the original request came after
160 // that). This is to emulate the effect of re-prerendering a page that is
161 // about to expire, because it was re-requested for prerendering a second
162 // time after the actual prerender being kept around.
163 base::Time start_time
;
164 // The actual time this page was last requested for prerendering.
165 base::Time actual_start_time
;
168 DISALLOW_IMPLICIT_CONSTRUCTORS(PrerenderData
);
171 PrerenderLocalPredictor::PrerenderLocalPredictor(
172 PrerenderManager
* prerender_manager
)
173 : prerender_manager_(prerender_manager
) {
174 RecordEvent(EVENT_CONSTRUCTED
);
175 if (MessageLoop::current()) {
176 timer_
.Start(FROM_HERE
,
177 base::TimeDelta::FromMilliseconds(kInitDelayMs
),
179 &PrerenderLocalPredictor::Init
);
180 RecordEvent(EVENT_INIT_SCHEDULED
);
184 PrerenderLocalPredictor::~PrerenderLocalPredictor() {
185 if (observing_history_service_
.get())
186 observing_history_service_
->RemoveVisitDatabaseObserver(this);
189 void PrerenderLocalPredictor::OnAddVisit(const history::BriefVisitInfo
& info
) {
190 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
191 RecordEvent(EVENT_ADD_VISIT
);
192 if (!visit_history_
.get())
194 visit_history_
->push_back(info
);
195 if (static_cast<int>(visit_history_
->size()) > kVisitHistoryPruneThreshold
) {
196 visit_history_
->erase(visit_history_
->begin(),
197 visit_history_
->begin() + kVisitHistoryPruneAmount
);
199 RecordEvent(EVENT_ADD_VISIT_INITIALIZED
);
200 if (current_prerender_
.get() &&
201 current_prerender_
->url_id
== info
.url_id
&&
202 IsPrerenderStillValid(current_prerender_
.get())) {
203 prerender_manager_
->histograms()->RecordLocalPredictorTimeUntilUsed(
204 GetCurrentTime() - current_prerender_
->actual_start_time
,
205 base::TimeDelta::FromMilliseconds(kMaxLocalPredictionTimeMs
));
206 last_swapped_in_prerender_
.reset(current_prerender_
.release());
207 RecordEvent(EVENT_ADD_VISIT_PRERENDER_IDENTIFIED
);
209 if (ShouldExcludeTransitionForPrediction(info
.transition
))
211 RecordEvent(EVENT_ADD_VISIT_RELEVANT_TRANSITION
);
212 base::TimeDelta max_age
=
213 base::TimeDelta::FromMilliseconds(kMaxLocalPredictionTimeMs
);
214 base::TimeDelta min_age
=
215 base::TimeDelta::FromMilliseconds(kMinLocalPredictionTimeMs
);
216 std::set
<URLID
> next_urls_currently_found
;
217 std::map
<URLID
, int> next_urls_num_found
;
218 int num_occurrences_of_current_visit
= 0;
219 base::Time last_visited
;
220 URLID best_next_url
= 0;
221 int best_next_url_count
= 0;
222 const std::vector
<history::BriefVisitInfo
>& visits
= *(visit_history_
.get());
223 for (int i
= 0; i
< static_cast<int>(visits
.size()); i
++) {
224 if (!ShouldExcludeTransitionForPrediction(visits
[i
].transition
)) {
225 if (visits
[i
].url_id
== info
.url_id
) {
226 last_visited
= visits
[i
].time
;
227 num_occurrences_of_current_visit
++;
228 next_urls_currently_found
.clear();
231 if (!last_visited
.is_null() &&
232 last_visited
> visits
[i
].time
- max_age
&&
233 last_visited
< visits
[i
].time
- min_age
) {
234 next_urls_currently_found
.insert(visits
[i
].url_id
);
237 if (i
== static_cast<int>(visits
.size()) - 1 ||
238 visits
[i
+1].url_id
== info
.url_id
) {
239 for (std::set
<URLID
>::iterator it
= next_urls_currently_found
.begin();
240 it
!= next_urls_currently_found
.end();
242 std::pair
<std::map
<URLID
, int>::iterator
, bool> insert_ret
=
243 next_urls_num_found
.insert(std::pair
<URLID
, int>(*it
, 0));
244 std::map
<URLID
, int>::iterator num_found_it
= insert_ret
.first
;
245 num_found_it
->second
++;
246 if (num_found_it
->second
> best_next_url_count
) {
247 best_next_url_count
= num_found_it
->second
;
254 // Only consider a candidate next page for prerendering if it was viewed
255 // at least twice, and at least 10% of the time.
256 if (num_occurrences_of_current_visit
> 0 &&
257 best_next_url_count
> 1 &&
258 best_next_url_count
* 10 >= num_occurrences_of_current_visit
) {
259 RecordEvent(EVENT_ADD_VISIT_IDENTIFIED_PRERENDER_CANDIDATE
);
260 double priority
= static_cast<double>(best_next_url_count
) /
261 static_cast<double>(num_occurrences_of_current_visit
);
262 if (ShouldReplaceCurrentPrerender(priority
)) {
263 RecordEvent(EVENT_START_URL_LOOKUP
);
264 HistoryService
* history
= GetHistoryIfExists();
266 history
->ScheduleDBTask(
267 new GetURLForURLIDTask(
269 base::Bind(&PrerenderLocalPredictor::OnLookupURL
,
270 base::Unretained(this),
273 &history_db_consumer_
);
279 void PrerenderLocalPredictor::OnLookupURL(history::URLID url_id
,
282 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT
);
284 base::Time current_time
= GetCurrentTime();
285 if (ShouldReplaceCurrentPrerender(priority
)) {
286 if (IsRootPageURL(url
)) {
287 RecordEvent(EVENT_ADD_VISIT_PRERENDERING
);
288 if (current_prerender_
.get() && current_prerender_
->url_id
== url_id
) {
289 RecordEvent(EVENT_ADD_VISIT_PRERENDERING_EXTENDED
);
290 if (priority
> current_prerender_
->priority
)
291 current_prerender_
->priority
= priority
;
292 // If the prerender already existed, we want to extend it. However,
293 // we do not want to set its start_time to the current time to
294 // disadvantage PLT computations when the prerender is swapped in.
295 // So we set the new start time to current_time - 10s (since the vast
296 // majority of PLTs are < 10s), provided that is not before the actual
297 // time the prerender was started (so as to not artificially advantage
298 // the PLT computation).
299 base::Time simulated_new_start_time
=
300 current_time
- base::TimeDelta::FromSeconds(10);
301 if (simulated_new_start_time
> current_prerender_
->start_time
)
302 current_prerender_
->start_time
= simulated_new_start_time
;
304 current_prerender_
.reset(
305 new PrerenderData(url_id
, url
, priority
, current_time
));
307 current_prerender_
->actual_start_time
= current_time
;
309 RecordEvent(EVENT_ADD_VISIT_NOT_ROOTPAGE
);
313 if (IsRootPageURL(url
))
314 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT_ROOT_PAGE
);
315 if (url
.SchemeIs("http"))
316 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT_IS_HTTP
);
318 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT_HAS_QUERY_STRING
);
319 if (StrCaseStr(url
.spec().c_str(), "logout") ||
320 StrCaseStr(url
.spec().c_str(), "signout"))
321 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT_CONTAINS_LOGOUT
);
322 if (StrCaseStr(url
.spec().c_str(), "login") ||
323 StrCaseStr(url
.spec().c_str(), "signin"))
324 RecordEvent(EVENT_PRERENDER_URL_LOOKUP_RESULT_CONTAINS_LOGIN
);
327 void PrerenderLocalPredictor::OnGetInitialVisitHistory(
328 scoped_ptr
<std::vector
<history::BriefVisitInfo
> > visit_history
) {
329 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
330 DCHECK(!visit_history_
.get());
331 RecordEvent(EVENT_INIT_SUCCEEDED
);
332 // Since the visit history has descending timestamps, we must reverse it.
333 visit_history_
.reset(new std::vector
<history::BriefVisitInfo
>(
334 visit_history
->rbegin(), visit_history
->rend()));
337 HistoryService
* PrerenderLocalPredictor::GetHistoryIfExists() const {
338 Profile
* profile
= prerender_manager_
->profile();
341 return HistoryServiceFactory::GetForProfileIfExists(profile
);
344 void PrerenderLocalPredictor::Init() {
345 DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI
));
346 RecordEvent(EVENT_INIT_STARTED
);
347 HistoryService
* history
= GetHistoryIfExists();
349 RecordEvent(EVENT_INIT_FAILED_NO_HISTORY
);
352 history
->ScheduleDBTask(
353 new GetVisitHistoryTask(this, kMaxVisitHistory
),
354 &history_db_consumer_
);
355 observing_history_service_
= history
;
356 observing_history_service_
->AddVisitDatabaseObserver(this);
359 void PrerenderLocalPredictor::OnPLTEventForURL(const GURL
& url
,
360 base::TimeDelta page_load_time
) {
361 scoped_ptr
<PrerenderData
> prerender
;
362 if (DoesPrerenderMatchPLTRecord(last_swapped_in_prerender_
.get(),
363 url
, page_load_time
)) {
364 prerender
.reset(last_swapped_in_prerender_
.release());
366 if (DoesPrerenderMatchPLTRecord(current_prerender_
.get(),
367 url
, page_load_time
)) {
368 prerender
.reset(current_prerender_
.release());
370 if (!prerender
.get())
372 if (IsPrerenderStillValid(prerender
.get())) {
373 base::TimeDelta prerender_age
= GetCurrentTime() - prerender
->start_time
;
374 prerender_manager_
->histograms()->RecordSimulatedLocalBrowsingBaselinePLT(
375 page_load_time
, url
);
376 if (prerender_age
> page_load_time
) {
377 base::TimeDelta new_plt
;
378 if (prerender_age
< 2 * page_load_time
)
379 new_plt
= 2 * page_load_time
- prerender_age
;
380 prerender_manager_
->histograms()->RecordSimulatedLocalBrowsingPLT(
387 bool PrerenderLocalPredictor::IsPrerenderStillValid(
388 PrerenderLocalPredictor::PrerenderData
* prerender
) const {
390 (prerender
->start_time
+
391 base::TimeDelta::FromMilliseconds(kMaxLocalPredictionTimeMs
))
395 void PrerenderLocalPredictor::RecordEvent(PrerenderLocalPredictor::Event event
)
397 prerender_manager_
->histograms()->RecordLocalPredictorEvent(event
);
400 bool PrerenderLocalPredictor::DoesPrerenderMatchPLTRecord(
401 PrerenderData
* prerender
, const GURL
& url
, base::TimeDelta plt
) const {
402 if (prerender
&& prerender
->start_time
< GetCurrentTime() - plt
) {
403 if (prerender
->url
.is_empty())
404 RecordEvent(EVENT_ERROR_NO_PRERENDER_URL_FOR_PLT
);
405 return (prerender
->url
== url
);
411 bool PrerenderLocalPredictor::ShouldReplaceCurrentPrerender(
412 double priority
) const {
413 base::TimeDelta max_age
=
414 base::TimeDelta::FromMilliseconds(kMaxLocalPredictionTimeMs
);
415 return (!current_prerender_
.get()) ||
416 current_prerender_
->priority
< priority
||
417 current_prerender_
->start_time
< GetCurrentTime() - max_age
;
420 } // namespace prerender