Home | History | Annotate | Download | only in suggestions
      1 // Copyright 2014 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/search/suggestions/suggestions_service.h"
      6 
      7 #include "base/memory/scoped_ptr.h"
      8 #include "base/metrics/field_trial.h"
      9 #include "base/metrics/histogram.h"
     10 #include "base/metrics/sparse_histogram.h"
     11 #include "base/strings/string_number_conversions.h"
     12 #include "base/strings/string_util.h"
     13 #include "base/time/time.h"
     14 #include "chrome/browser/browser_process.h"
     15 #include "chrome/browser/history/history_types.h"
     16 #include "chrome/browser/metrics/variations/variations_http_header_provider.h"
     17 #include "chrome/browser/profiles/profile.h"
     18 #include "chrome/browser/search/suggestions/suggestions_store.h"
     19 #include "components/pref_registry/pref_registry_syncable.h"
     20 #include "components/variations/variations_associated_data.h"
     21 #include "content/public/browser/browser_thread.h"
     22 #include "net/base/escape.h"
     23 #include "net/base/load_flags.h"
     24 #include "net/base/net_errors.h"
     25 #include "net/http/http_response_headers.h"
     26 #include "net/http/http_status_code.h"
     27 #include "net/http/http_util.h"
     28 #include "net/url_request/url_fetcher.h"
     29 #include "net/url_request/url_request_status.h"
     30 #include "url/gurl.h"
     31 
     32 using base::CancelableClosure;
     33 using content::BrowserThread;
     34 
     35 namespace suggestions {
     36 
     37 namespace {
     38 
     39 // Used to UMA log the state of the last response from the server.
     40 enum SuggestionsResponseState {
     41   RESPONSE_EMPTY,
     42   RESPONSE_INVALID,
     43   RESPONSE_VALID,
     44   RESPONSE_STATE_SIZE
     45 };
     46 
     47 // Will log the supplied response |state|.
     48 void LogResponseState(SuggestionsResponseState state) {
     49   UMA_HISTOGRAM_ENUMERATION("Suggestions.ResponseState", state,
     50                             RESPONSE_STATE_SIZE);
     51 }
     52 
     53 // Obtains the experiment parameter under the supplied |key|, or empty string
     54 // if the parameter does not exist.
     55 std::string GetExperimentParam(const std::string& key) {
     56   return chrome_variations::GetVariationParamValue(kSuggestionsFieldTrialName,
     57                                                    key);
     58 }
     59 
     60 // Runs each callback in |requestors| on |suggestions|, then deallocates
     61 // |requestors|.
     62 void DispatchRequestsAndClear(
     63     const SuggestionsProfile& suggestions,
     64     std::vector<SuggestionsService::ResponseCallback>* requestors) {
     65   std::vector<SuggestionsService::ResponseCallback>::iterator it;
     66   for (it = requestors->begin(); it != requestors->end(); ++it) {
     67     it->Run(suggestions);
     68   }
     69   std::vector<SuggestionsService::ResponseCallback>().swap(*requestors);
     70 }
     71 
     72 const int kDefaultRequestTimeoutMs = 200;
     73 
     74 }  // namespace
     75 
     76 const char kSuggestionsFieldTrialName[] = "ChromeSuggestions";
     77 const char kSuggestionsFieldTrialURLParam[] = "url";
     78 const char kSuggestionsFieldTrialSuggestionsSuffixParam[] =
     79     "suggestions_suffix";
     80 const char kSuggestionsFieldTrialBlacklistSuffixParam[] = "blacklist_suffix";
     81 const char kSuggestionsFieldTrialStateParam[] = "state";
     82 const char kSuggestionsFieldTrialControlParam[] = "control";
     83 const char kSuggestionsFieldTrialStateEnabled[] = "enabled";
     84 const char kSuggestionsFieldTrialTimeoutMs[] = "timeout_ms";
     85 
     86 SuggestionsService::SuggestionsService(
     87     Profile* profile, scoped_ptr<SuggestionsStore> suggestions_store)
     88     : suggestions_store_(suggestions_store.Pass()),
     89       thumbnail_manager_(new ThumbnailManager(profile)),
     90       profile_(profile),
     91       weak_ptr_factory_(this),
     92       request_timeout_ms_(kDefaultRequestTimeoutMs) {
     93   // Obtain various parameters from Variations.
     94   suggestions_url_ =
     95       GURL(GetExperimentParam(kSuggestionsFieldTrialURLParam) +
     96            GetExperimentParam(kSuggestionsFieldTrialSuggestionsSuffixParam));
     97   blacklist_url_prefix_ =
     98       GetExperimentParam(kSuggestionsFieldTrialURLParam) +
     99       GetExperimentParam(kSuggestionsFieldTrialBlacklistSuffixParam);
    100   std::string timeout = GetExperimentParam(kSuggestionsFieldTrialTimeoutMs);
    101   int temp_timeout;
    102   if (!timeout.empty() && base::StringToInt(timeout, &temp_timeout)) {
    103     request_timeout_ms_ = temp_timeout;
    104   }
    105 }
    106 
    107 SuggestionsService::~SuggestionsService() {}
    108 
    109 // static
    110 bool SuggestionsService::IsEnabled() {
    111   return GetExperimentParam(kSuggestionsFieldTrialStateParam) ==
    112          kSuggestionsFieldTrialStateEnabled;
    113 }
    114 
    115 // static
    116 bool SuggestionsService::IsControlGroup() {
    117   return GetExperimentParam(kSuggestionsFieldTrialControlParam) ==
    118          kSuggestionsFieldTrialStateEnabled;
    119 }
    120 
    121 void SuggestionsService::FetchSuggestionsData(
    122     SuggestionsService::ResponseCallback callback) {
    123   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    124 
    125   FetchSuggestionsDataNoTimeout(callback);
    126 
    127   // Post a task to serve the cached suggestions if the request hasn't completed
    128   // after some time. Cancels the previous such task, if one existed.
    129   pending_timeout_closure_.reset(new CancelableClosure(base::Bind(
    130       &SuggestionsService::OnRequestTimeout, weak_ptr_factory_.GetWeakPtr())));
    131   BrowserThread::PostDelayedTask(
    132       BrowserThread::UI, FROM_HERE, pending_timeout_closure_->callback(),
    133       base::TimeDelta::FromMilliseconds(request_timeout_ms_));
    134 }
    135 
    136 void SuggestionsService::FetchSuggestionsDataNoTimeout(
    137     SuggestionsService::ResponseCallback callback) {
    138   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    139   if (pending_request_.get()) {
    140     // Request already exists, so just add requestor to queue.
    141     waiting_requestors_.push_back(callback);
    142     return;
    143   }
    144 
    145   // Form new request.
    146   DCHECK(waiting_requestors_.empty());
    147   waiting_requestors_.push_back(callback);
    148 
    149   pending_request_.reset(CreateSuggestionsRequest(suggestions_url_));
    150   pending_request_->Start();
    151   last_request_started_time_ = base::TimeTicks::Now();
    152 }
    153 
    154 void SuggestionsService::GetPageThumbnail(
    155     const GURL& url,
    156     base::Callback<void(const GURL&, const SkBitmap*)> callback) {
    157   thumbnail_manager_->GetPageThumbnail(url, callback);
    158 }
    159 
    160 void SuggestionsService::BlacklistURL(
    161     const GURL& candidate_url, SuggestionsService::ResponseCallback callback) {
    162   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    163   waiting_requestors_.push_back(callback);
    164 
    165   if (pending_request_.get()) {
    166     if (IsBlacklistRequest(pending_request_.get())) {
    167       // Pending request is a blacklist request. Silently drop the new blacklist
    168       // request. TODO - handle this case.
    169       return;
    170     } else {
    171       // Pending request is not a blacklist request - cancel it and go on to
    172       // issuing a blacklist request. Also ensure the timeout closure does not
    173       // run; instead we'll wait for the updated suggestions before servicing
    174       // requestors.
    175       pending_request_.reset(NULL);
    176       pending_timeout_closure_.reset(NULL);
    177     }
    178   }
    179 
    180   // Send blacklisting request.
    181   // TODO(manzagop): make this a PUT request instead of a GET request.
    182   GURL url(blacklist_url_prefix_ +
    183            net::EscapeQueryParamValue(candidate_url.spec(), true));
    184   pending_request_.reset(CreateSuggestionsRequest(url));
    185   pending_request_->Start();
    186   last_request_started_time_ = base::TimeTicks::Now();
    187 }
    188 
    189 // static
    190 void SuggestionsService::RegisterProfilePrefs(
    191     user_prefs::PrefRegistrySyncable* registry) {
    192   SuggestionsStore::RegisterProfilePrefs(registry);
    193 }
    194 
    195 void SuggestionsService::OnRequestTimeout() {
    196   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    197   ServeFromCache();
    198 }
    199 
    200 void SuggestionsService::OnURLFetchComplete(const net::URLFetcher* source) {
    201   DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI));
    202   DCHECK_EQ(pending_request_.get(), source);
    203 
    204   // We no longer need the timeout closure. Delete it whether or not it has run
    205   // (if it hasn't, this cancels it).
    206   pending_timeout_closure_.reset();
    207 
    208   // The fetcher will be deleted when the request is handled.
    209   scoped_ptr<const net::URLFetcher> request(pending_request_.release());
    210   const net::URLRequestStatus& request_status = request->GetStatus();
    211   if (request_status.status() != net::URLRequestStatus::SUCCESS) {
    212     UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FailedRequestErrorCode",
    213                                 -request_status.error());
    214     DVLOG(1) << "Suggestions server request failed with error: "
    215              << request_status.error() << ": "
    216              << net::ErrorToString(request_status.error());
    217     // Dispatch the cached profile on error.
    218     ServeFromCache();
    219     return;
    220   }
    221 
    222   // Log the response code.
    223   const int response_code = request->GetResponseCode();
    224   UMA_HISTOGRAM_SPARSE_SLOWLY("Suggestions.FetchResponseCode", response_code);
    225   if (response_code != net::HTTP_OK) {
    226     // Aggressively clear the store.
    227     suggestions_store_->ClearSuggestions();
    228     DispatchRequestsAndClear(SuggestionsProfile(), &waiting_requestors_);
    229     return;
    230   }
    231 
    232   const base::TimeDelta latency =
    233       base::TimeTicks::Now() - last_request_started_time_;
    234   UMA_HISTOGRAM_MEDIUM_TIMES("Suggestions.FetchSuccessLatency", latency);
    235 
    236   std::string suggestions_data;
    237   bool success = request->GetResponseAsString(&suggestions_data);
    238   DCHECK(success);
    239 
    240   // Compute suggestions, and dispatch then to requestors. On error still
    241   // dispatch empty suggestions.
    242   SuggestionsProfile suggestions;
    243   if (suggestions_data.empty()) {
    244     LogResponseState(RESPONSE_EMPTY);
    245     suggestions_store_->ClearSuggestions();
    246   } else if (suggestions.ParseFromString(suggestions_data)) {
    247     LogResponseState(RESPONSE_VALID);
    248     thumbnail_manager_->InitializeThumbnailMap(suggestions);
    249     suggestions_store_->StoreSuggestions(suggestions);
    250   } else {
    251     LogResponseState(RESPONSE_INVALID);
    252     suggestions_store_->LoadSuggestions(&suggestions);
    253   }
    254 
    255   DispatchRequestsAndClear(suggestions, &waiting_requestors_);
    256 }
    257 
    258 void SuggestionsService::Shutdown() {
    259   // Cancel pending request and timeout closure, then serve existing requestors
    260   // from cache.
    261   pending_request_.reset(NULL);
    262   pending_timeout_closure_.reset(NULL);
    263   ServeFromCache();
    264 }
    265 
    266 bool SuggestionsService::IsBlacklistRequest(net::URLFetcher* request) const {
    267   DCHECK(request);
    268   return StartsWithASCII(request->GetOriginalURL().spec(),
    269                          blacklist_url_prefix_, true);
    270 }
    271 
    272 net::URLFetcher* SuggestionsService::CreateSuggestionsRequest(const GURL& url) {
    273   net::URLFetcher* request =
    274       net::URLFetcher::Create(0, url, net::URLFetcher::GET, this);
    275   request->SetLoadFlags(net::LOAD_DISABLE_CACHE);
    276   request->SetRequestContext(profile_->GetRequestContext());
    277   // Add Chrome experiment state to the request headers.
    278   net::HttpRequestHeaders headers;
    279   chrome_variations::VariationsHttpHeaderProvider::GetInstance()->AppendHeaders(
    280       request->GetOriginalURL(), profile_->IsOffTheRecord(), false, &headers);
    281   request->SetExtraRequestHeaders(headers.ToString());
    282   return request;
    283 }
    284 
    285 void SuggestionsService::ServeFromCache() {
    286   SuggestionsProfile suggestions;
    287   suggestions_store_->LoadSuggestions(&suggestions);
    288   DispatchRequestsAndClear(suggestions, &waiting_requestors_);
    289 }
    290 
    291 }  // namespace suggestions
    292