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