1// Copyright (c) 2011 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.
4
5#include "chrome/browser/prerender/prerender_manager.h"
6
7#include "base/logging.h"
8#include "base/metrics/field_trial.h"
9#include "base/metrics/histogram.h"
10#include "base/time.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/prerender/prerender_contents.h"
13#include "chrome/browser/prerender/prerender_final_status.h"
14#include "chrome/browser/profiles/profile.h"
15#include "content/browser/browser_thread.h"
16#include "content/browser/renderer_host/render_view_host.h"
17#include "content/browser/renderer_host/render_process_host.h"
18#include "content/browser/renderer_host/resource_dispatcher_host.h"
19#include "content/browser/tab_contents/render_view_host_manager.h"
20#include "content/browser/tab_contents/tab_contents.h"
21#include "content/common/notification_service.h"
22#include "content/common/view_messages.h"
23#include "googleurl/src/url_parse.h"
24#include "googleurl/src/url_canon.h"
25#include "googleurl/src/url_util.h"
26
27namespace prerender {
28
29// static
30int PrerenderManager::prerenders_per_session_count_ = 0;
31
32// static
33base::TimeTicks PrerenderManager::last_prefetch_seen_time_;
34
35// static
36PrerenderManager::PrerenderManagerMode PrerenderManager::mode_ =
37    PRERENDER_MODE_ENABLED;
38
39// static
40PrerenderManager::PrerenderManagerMode PrerenderManager::GetMode() {
41  return mode_;
42}
43
44// static
45void PrerenderManager::SetMode(PrerenderManagerMode mode) {
46  mode_ = mode;
47}
48
49// static
50bool PrerenderManager::IsPrerenderingPossible() {
51  return
52      GetMode() == PRERENDER_MODE_ENABLED ||
53      GetMode() == PRERENDER_MODE_EXPERIMENT_PRERENDER_GROUP ||
54      GetMode() == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP;
55}
56
57// static
58bool PrerenderManager::IsControlGroup() {
59  return GetMode() == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP;
60}
61
62// static
63bool PrerenderManager::MaybeGetQueryStringBasedAliasURL(
64    const GURL& url, GURL* alias_url) {
65  DCHECK(alias_url);
66  url_parse::Parsed parsed;
67  url_parse::ParseStandardURL(url.spec().c_str(), url.spec().length(),
68                              &parsed);
69  url_parse::Component query = parsed.query;
70  url_parse::Component key, value;
71  while (url_parse::ExtractQueryKeyValue(url.spec().c_str(), &query, &key,
72                                         &value)) {
73    if (key.len != 3 || strncmp(url.spec().c_str() + key.begin, "url", key.len))
74      continue;
75    // We found a url= query string component.
76    if (value.len < 1)
77      continue;
78    url_canon::RawCanonOutputW<1024> decoded_url;
79    url_util::DecodeURLEscapeSequences(url.spec().c_str() + value.begin,
80                                       value.len, &decoded_url);
81    GURL new_url(string16(decoded_url.data(), decoded_url.length()));
82    if (!new_url.is_empty() && new_url.is_valid()) {
83      *alias_url = new_url;
84      return true;
85    }
86    return false;
87  }
88  return false;
89}
90
91struct PrerenderManager::PrerenderContentsData {
92  PrerenderContents* contents_;
93  base::Time start_time_;
94  PrerenderContentsData(PrerenderContents* contents, base::Time start_time)
95      : contents_(contents),
96        start_time_(start_time) {
97  }
98};
99
100struct PrerenderManager::PendingContentsData {
101  PendingContentsData(const GURL& url, const std::vector<GURL>& alias_urls,
102                      const GURL& referrer)
103      : url_(url), alias_urls_(alias_urls), referrer_(referrer) { }
104  ~PendingContentsData() {}
105  GURL url_;
106  std::vector<GURL> alias_urls_;
107  GURL referrer_;
108};
109
110
111PrerenderManager::PrerenderManager(Profile* profile)
112    : rate_limit_enabled_(true),
113      enabled_(true),
114      profile_(profile),
115      max_prerender_age_(base::TimeDelta::FromSeconds(
116          kDefaultMaxPrerenderAgeSeconds)),
117      max_elements_(kDefaultMaxPrerenderElements),
118      prerender_contents_factory_(PrerenderContents::CreateFactory()),
119      last_prerender_start_time_(GetCurrentTimeTicks() -
120          base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs)) {
121}
122
123PrerenderManager::~PrerenderManager() {
124  while (!prerender_list_.empty()) {
125    PrerenderContentsData data = prerender_list_.front();
126    prerender_list_.pop_front();
127    data.contents_->set_final_status(FINAL_STATUS_MANAGER_SHUTDOWN);
128    delete data.contents_;
129  }
130}
131
132void PrerenderManager::SetPrerenderContentsFactory(
133    PrerenderContents::Factory* prerender_contents_factory) {
134  prerender_contents_factory_.reset(prerender_contents_factory);
135}
136
137bool PrerenderManager::AddPreload(const GURL& url,
138                                  const std::vector<GURL>& alias_urls,
139                                  const GURL& referrer) {
140  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
141  DeleteOldEntries();
142  if (FindEntry(url))
143    return false;
144
145  // Local copy, since we may have to add an additional entry to it.
146  std::vector<GURL> all_alias_urls = alias_urls;
147
148  GURL additional_alias_url;
149  if (IsControlGroup() &&
150      PrerenderManager::MaybeGetQueryStringBasedAliasURL(
151          url, &additional_alias_url))
152    all_alias_urls.push_back(additional_alias_url);
153
154  // Do not prerender if there are too many render processes, and we would
155  // have to use an existing one.  We do not want prerendering to happen in
156  // a shared process, so that we can always reliably lower the CPU
157  // priority for prerendering.
158  // In single-process mode, ShouldTryToUseExistingProcessHost() always returns
159  // true, so that case needs to be explicitly checked for.
160  // TODO(tburkard): Figure out how to cancel prerendering in the opposite
161  // case, when a new tab is added to a process used for prerendering.
162  if (RenderProcessHost::ShouldTryToUseExistingProcessHost() &&
163      !RenderProcessHost::run_renderer_in_process()) {
164    // Only record the status if we are not in the control group.
165    if (!IsControlGroup())
166      RecordFinalStatus(FINAL_STATUS_TOO_MANY_PROCESSES);
167    return false;
168  }
169
170  // Check if enough time has passed since the last prerender.
171  if (!DoesRateLimitAllowPrerender()) {
172    // Cancel the prerender. We could add it to the pending prerender list but
173    // this doesn't make sense as the next prerender request will be triggered
174    // by a navigation and is unlikely to be the same site.
175    RecordFinalStatus(FINAL_STATUS_RATE_LIMIT_EXCEEDED);
176
177    return false;
178  }
179
180  // TODO(cbentzel): Move invalid checks here instead of PrerenderContents?
181  PrerenderContentsData data(CreatePrerenderContents(url, all_alias_urls,
182                                                     referrer),
183                             GetCurrentTime());
184
185  prerender_list_.push_back(data);
186  if (IsControlGroup()) {
187    data.contents_->set_final_status(FINAL_STATUS_CONTROL_GROUP);
188  } else {
189    last_prerender_start_time_ = GetCurrentTimeTicks();
190    data.contents_->StartPrerendering();
191  }
192  while (prerender_list_.size() > max_elements_) {
193    data = prerender_list_.front();
194    prerender_list_.pop_front();
195    data.contents_->set_final_status(FINAL_STATUS_EVICTED);
196    delete data.contents_;
197  }
198  StartSchedulingPeriodicCleanups();
199  return true;
200}
201
202void PrerenderManager::AddPendingPreload(
203    const std::pair<int,int>& child_route_id_pair,
204    const GURL& url,
205    const std::vector<GURL>& alias_urls,
206    const GURL& referrer) {
207  // Check if this is coming from a valid prerender rvh.
208  bool is_valid_prerender = false;
209  for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
210       it != prerender_list_.end(); ++it) {
211    PrerenderContents* pc = it->contents_;
212
213    int child_id;
214    int route_id;
215    bool has_child_id = pc->GetChildId(&child_id);
216    bool has_route_id = has_child_id && pc->GetRouteId(&route_id);
217
218    if (has_child_id && has_route_id &&
219        child_id == child_route_id_pair.first &&
220        route_id == child_route_id_pair.second) {
221      is_valid_prerender = true;
222      break;
223    }
224  }
225
226  // If not, we could check to see if the RenderViewHost specified by the
227  // child_route_id_pair exists and if so just start prerendering, as this
228  // suggests that the link was clicked, though this might prerender something
229  // that the user has already navigated away from. For now, we'll be
230  // conservative and skip the prerender which will mean some prerender requests
231  // from prerendered pages will be missed if the user navigates quickly.
232  if (!is_valid_prerender) {
233    RecordFinalStatus(FINAL_STATUS_PENDING_SKIPPED);
234    return;
235  }
236
237  PendingPrerenderList::iterator it =
238      pending_prerender_list_.find(child_route_id_pair);
239  if (it == pending_prerender_list_.end()) {
240    PendingPrerenderList::value_type el = std::make_pair(child_route_id_pair,
241                                            std::vector<PendingContentsData>());
242    it = pending_prerender_list_.insert(el).first;
243  }
244
245  it->second.push_back(PendingContentsData(url, alias_urls, referrer));
246}
247
248void PrerenderManager::DeleteOldEntries() {
249  while (!prerender_list_.empty()) {
250    PrerenderContentsData data = prerender_list_.front();
251    if (IsPrerenderElementFresh(data.start_time_))
252      return;
253    prerender_list_.pop_front();
254    data.contents_->set_final_status(FINAL_STATUS_TIMED_OUT);
255    delete data.contents_;
256  }
257  if (prerender_list_.empty())
258    StopSchedulingPeriodicCleanups();
259}
260
261PrerenderContents* PrerenderManager::GetEntry(const GURL& url) {
262  DeleteOldEntries();
263  for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
264       it != prerender_list_.end();
265       ++it) {
266    PrerenderContents* pc = it->contents_;
267    if (pc->MatchesURL(url)) {
268      prerender_list_.erase(it);
269      return pc;
270    }
271  }
272  // Entry not found.
273  return NULL;
274}
275
276bool PrerenderManager::MaybeUsePreloadedPage(TabContents* tc, const GURL& url) {
277  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
278  scoped_ptr<PrerenderContents> pc(GetEntry(url));
279  if (pc.get() == NULL)
280    return false;
281
282  // If we are just in the control group (which can be detected by noticing
283  // that prerendering hasn't even started yet), record that this TC now would
284  // be showing a prerendered contents, but otherwise, don't do anything.
285  if (!pc->prerendering_has_started()) {
286    MarkTabContentsAsWouldBePrerendered(tc);
287    return false;
288  }
289
290  if (!pc->load_start_time().is_null())
291    RecordTimeUntilUsed(GetCurrentTimeTicks() - pc->load_start_time());
292
293  UMA_HISTOGRAM_COUNTS("Prerender.PrerendersPerSessionCount",
294                       ++prerenders_per_session_count_);
295  pc->set_final_status(FINAL_STATUS_USED);
296
297  int child_id;
298  int route_id;
299  CHECK(pc->GetChildId(&child_id));
300  CHECK(pc->GetRouteId(&route_id));
301
302  RenderViewHost* rvh = pc->render_view_host();
303  // RenderViewHosts in PrerenderContents start out hidden.
304  // Since we are actually using it now, restore it.
305  rvh->WasRestored();
306  pc->set_render_view_host(NULL);
307  rvh->Send(new ViewMsg_DisplayPrerenderedPage(rvh->routing_id()));
308  tc->SwapInRenderViewHost(rvh);
309  MarkTabContentsAsPrerendered(tc);
310
311  // See if we have any pending prerender requests for this routing id and start
312  // the preload if we do.
313  std::pair<int, int> child_route_pair = std::make_pair(child_id, route_id);
314  PendingPrerenderList::iterator pending_it =
315      pending_prerender_list_.find(child_route_pair);
316  if (pending_it != pending_prerender_list_.end()) {
317    for (std::vector<PendingContentsData>::iterator content_it =
318            pending_it->second.begin();
319         content_it != pending_it->second.end(); ++content_it) {
320      AddPreload(content_it->url_, content_it->alias_urls_,
321                 content_it->referrer_);
322    }
323    pending_prerender_list_.erase(pending_it);
324  }
325
326  NotificationService::current()->Notify(
327      NotificationType::PRERENDER_CONTENTS_USED,
328      Source<std::pair<int, int> >(&child_route_pair),
329      NotificationService::NoDetails());
330
331  ViewHostMsg_FrameNavigate_Params* p = pc->navigate_params();
332  if (p != NULL)
333    tc->DidNavigate(rvh, *p);
334
335  string16 title = pc->title();
336  if (!title.empty())
337    tc->UpdateTitle(rvh, pc->page_id(), UTF16ToWideHack(title));
338
339  GURL icon_url = pc->icon_url();
340  if (!icon_url.is_empty()) {
341    std::vector<FaviconURL> urls;
342    urls.push_back(FaviconURL(icon_url, FaviconURL::FAVICON));
343    tc->favicon_helper().OnUpdateFaviconURL(pc->page_id(), urls);
344  }
345
346  if (pc->has_stopped_loading())
347    tc->DidStopLoading();
348
349  return true;
350}
351
352void PrerenderManager::RemoveEntry(PrerenderContents* entry) {
353  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
354  for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
355       it != prerender_list_.end();
356       ++it) {
357    if (it->contents_ == entry) {
358      RemovePendingPreload(entry);
359      prerender_list_.erase(it);
360      break;
361    }
362  }
363  DeleteOldEntries();
364}
365
366base::Time PrerenderManager::GetCurrentTime() const {
367  return base::Time::Now();
368}
369
370base::TimeTicks PrerenderManager::GetCurrentTimeTicks() const {
371  return base::TimeTicks::Now();
372}
373
374bool PrerenderManager::IsPrerenderElementFresh(const base::Time start) const {
375  base::Time now = GetCurrentTime();
376  return (now - start < max_prerender_age_);
377}
378
379PrerenderContents* PrerenderManager::CreatePrerenderContents(
380    const GURL& url,
381    const std::vector<GURL>& alias_urls,
382    const GURL& referrer) {
383  return prerender_contents_factory_->CreatePrerenderContents(
384      this, profile_, url, alias_urls, referrer);
385}
386
387// Helper macro for histograms.
388#define RECORD_PLT(tag, perceived_page_load_time) { \
389    UMA_HISTOGRAM_CUSTOM_TIMES( \
390        base::FieldTrial::MakeName(std::string("Prerender.") + tag, \
391                                   "Prefetch"), \
392        perceived_page_load_time, \
393        base::TimeDelta::FromMilliseconds(10), \
394        base::TimeDelta::FromSeconds(60), \
395        100); \
396  }
397
398// static
399void PrerenderManager::RecordPerceivedPageLoadTime(
400    base::TimeDelta perceived_page_load_time,
401    TabContents* tab_contents) {
402  bool within_window = WithinWindow();
403  PrerenderManager* prerender_manager =
404      tab_contents->profile()->GetPrerenderManager();
405  if (!prerender_manager)
406    return;
407  if (!prerender_manager->is_enabled())
408    return;
409  RECORD_PLT("PerceivedPLT", perceived_page_load_time);
410  if (within_window)
411    RECORD_PLT("PerceivedPLTWindowed", perceived_page_load_time);
412  if (prerender_manager &&
413      ((mode_ == PRERENDER_MODE_EXPERIMENT_CONTROL_GROUP &&
414        prerender_manager->WouldTabContentsBePrerendered(tab_contents)) ||
415       (mode_ == PRERENDER_MODE_EXPERIMENT_PRERENDER_GROUP &&
416        prerender_manager->IsTabContentsPrerendered(tab_contents)))) {
417    RECORD_PLT("PerceivedPLTMatched", perceived_page_load_time);
418  } else {
419    if (within_window)
420      RECORD_PLT("PerceivedPLTWindowNotMatched", perceived_page_load_time);
421  }
422}
423
424void PrerenderManager::RecordTimeUntilUsed(base::TimeDelta time_until_used) {
425  UMA_HISTOGRAM_CUSTOM_TIMES(
426      "Prerender.TimeUntilUsed",
427      time_until_used,
428      base::TimeDelta::FromMilliseconds(10),
429      base::TimeDelta::FromSeconds(kDefaultMaxPrerenderAgeSeconds),
430      50);
431}
432
433bool PrerenderManager::is_enabled() const {
434  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
435  return enabled_;
436}
437
438void PrerenderManager::set_enabled(bool enabled) {
439  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
440  enabled_ = enabled;
441}
442
443PrerenderContents* PrerenderManager::FindEntry(const GURL& url) {
444  for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
445       it != prerender_list_.end();
446       ++it) {
447    if (it->contents_->MatchesURL(url))
448      return it->contents_;
449  }
450  // Entry not found.
451  return NULL;
452}
453
454PrerenderManager::PendingContentsData*
455    PrerenderManager::FindPendingEntry(const GURL& url) {
456  for (PendingPrerenderList::iterator map_it = pending_prerender_list_.begin();
457       map_it != pending_prerender_list_.end();
458       ++map_it) {
459    for (std::vector<PendingContentsData>::iterator content_it =
460            map_it->second.begin();
461         content_it != map_it->second.end();
462         ++content_it) {
463      if (content_it->url_ == url) {
464        return &(*content_it);
465      }
466    }
467  }
468
469  return NULL;
470}
471
472// static
473void PrerenderManager::RecordPrefetchTagObserved() {
474  // Ensure that we are in the UI thread, and post to the UI thread if
475  // necessary.
476  if (!BrowserThread::CurrentlyOn(BrowserThread::UI)) {
477    BrowserThread::PostTask(
478        BrowserThread::UI,
479        FROM_HERE,
480        NewRunnableFunction(
481            &PrerenderManager::RecordPrefetchTagObservedOnUIThread));
482  } else {
483    RecordPrefetchTagObservedOnUIThread();
484  }
485}
486
487// static
488void PrerenderManager::RecordPrefetchTagObservedOnUIThread() {
489  // Once we get here, we have to be on the UI thread.
490  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
491
492  // If we observe multiple tags within the 30 second window, we will still
493  // reset the window to begin at the most recent occurrence, so that we will
494  // always be in a window in the 30 seconds from each occurrence.
495  last_prefetch_seen_time_ = base::TimeTicks::Now();
496}
497
498void PrerenderManager::RemovePendingPreload(PrerenderContents* entry) {
499  int child_id;
500  int route_id;
501  bool has_child_id = entry->GetChildId(&child_id);
502  bool has_route_id = has_child_id && entry->GetRouteId(&route_id);
503
504  // If the entry doesn't have a RenderViewHost then it didn't start
505  // prerendering and there shouldn't be any pending preloads to remove.
506  if (has_child_id && has_route_id) {
507    std::pair<int, int> child_route_pair = std::make_pair(child_id, route_id);
508    pending_prerender_list_.erase(child_route_pair);
509  }
510}
511
512// static
513bool PrerenderManager::WithinWindow() {
514  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
515  if (last_prefetch_seen_time_.is_null())
516    return false;
517  base::TimeDelta elapsed_time =
518      base::TimeTicks::Now() - last_prefetch_seen_time_;
519  return elapsed_time <= base::TimeDelta::FromSeconds(kWindowDurationSeconds);
520}
521
522bool PrerenderManager::DoesRateLimitAllowPrerender() const {
523  DCHECK(BrowserThread::CurrentlyOn(BrowserThread::UI));
524  base::TimeDelta elapsed_time =
525      GetCurrentTimeTicks() - last_prerender_start_time_;
526  UMA_HISTOGRAM_TIMES("Prerender.TimeBetweenPrerenderRequests",
527                      elapsed_time);
528  if (!rate_limit_enabled_)
529    return true;
530  return elapsed_time >
531      base::TimeDelta::FromMilliseconds(kMinTimeBetweenPrerendersMs);
532}
533
534void PrerenderManager::StartSchedulingPeriodicCleanups() {
535  if (repeating_timer_.IsRunning())
536    return;
537  repeating_timer_.Start(
538      base::TimeDelta::FromMilliseconds(kPeriodicCleanupIntervalMs),
539      this,
540      &PrerenderManager::PeriodicCleanup);
541}
542
543void PrerenderManager::StopSchedulingPeriodicCleanups() {
544  repeating_timer_.Stop();
545}
546
547void PrerenderManager::PeriodicCleanup() {
548  DeleteOldEntries();
549  // Grab a copy of the current PrerenderContents pointers, so that we
550  // will not interfere with potential deletions of the list.
551  std::vector<PrerenderContents*> prerender_contents;
552  for (std::list<PrerenderContentsData>::iterator it = prerender_list_.begin();
553       it != prerender_list_.end();
554       ++it) {
555    prerender_contents.push_back(it->contents_);
556  }
557  for (std::vector<PrerenderContents*>::iterator it =
558           prerender_contents.begin();
559       it != prerender_contents.end();
560       ++it) {
561    (*it)->DestroyWhenUsingTooManyResources();
562  }
563}
564
565void PrerenderManager::MarkTabContentsAsPrerendered(TabContents* tc) {
566  prerendered_tc_set_.insert(tc);
567}
568
569void PrerenderManager::MarkTabContentsAsWouldBePrerendered(TabContents* tc) {
570  would_be_prerendered_tc_set_.insert(tc);
571}
572
573void PrerenderManager::MarkTabContentsAsNotPrerendered(TabContents* tc) {
574  prerendered_tc_set_.erase(tc);
575  would_be_prerendered_tc_set_.erase(tc);
576}
577
578bool PrerenderManager::IsTabContentsPrerendered(TabContents* tc) const {
579  return prerendered_tc_set_.count(tc) > 0;
580}
581
582bool PrerenderManager::WouldTabContentsBePrerendered(TabContents* tc) const {
583  return would_be_prerendered_tc_set_.count(tc) > 0;
584}
585
586}  // namespace prerender
587