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/predictors/autocomplete_action_predictor.h"
11 #include "base/bind.h"
12 #include "base/guid.h"
13 #include "base/i18n/case_conversion.h"
14 #include "base/metrics/histogram.h"
15 #include "base/strings/string_util.h"
16 #include "base/strings/stringprintf.h"
17 #include "base/strings/utf_string_conversions.h"
18 #include "chrome/browser/autocomplete/autocomplete_match.h"
19 #include "chrome/browser/autocomplete/autocomplete_result.h"
20 #include "chrome/browser/chrome_notification_types.h"
21 #include "chrome/browser/history/history_notifications.h"
22 #include "chrome/browser/history/history_service.h"
23 #include "chrome/browser/history/history_service_factory.h"
24 #include "chrome/browser/history/in_memory_database.h"
25 #include "chrome/browser/omnibox/omnibox_log.h"
26 #include "chrome/browser/predictors/autocomplete_action_predictor_factory.h"
27 #include "chrome/browser/predictors/predictor_database.h"
28 #include "chrome/browser/predictors/predictor_database_factory.h"
29 #include "chrome/browser/prerender/prerender_field_trial.h"
30 #include "chrome/browser/prerender/prerender_handle.h"
31 #include "chrome/browser/prerender/prerender_manager.h"
32 #include "chrome/browser/prerender/prerender_manager_factory.h"
33 #include "chrome/browser/profiles/profile.h"
34 #include "content/public/browser/browser_thread.h"
35 #include "content/public/browser/notification_details.h"
36 #include "content/public/browser/notification_service.h"
37 #include "content/public/browser/notification_source.h"
41 const float kConfidenceCutoff
[] = {
46 COMPILE_ASSERT(arraysize(kConfidenceCutoff
) ==
47 predictors::AutocompleteActionPredictor::LAST_PREDICT_ACTION
,
48 ConfidenceCutoff_count_mismatch
);
50 const size_t kMinimumUserTextLength
= 1;
51 const int kMinimumNumberOfHits
= 3;
55 DATABASE_ACTION_UPDATE
,
56 DATABASE_ACTION_DELETE_SOME
,
57 DATABASE_ACTION_DELETE_ALL
,
63 namespace predictors
{
65 const int AutocompleteActionPredictor::kMaximumDaysToKeepEntry
= 14;
67 AutocompleteActionPredictor::AutocompleteActionPredictor(Profile
* profile
)
69 main_profile_predictor_(NULL
),
70 incognito_predictor_(NULL
),
72 if (profile_
->IsOffTheRecord()) {
73 main_profile_predictor_
= AutocompleteActionPredictorFactory::GetForProfile(
74 profile_
->GetOriginalProfile());
75 DCHECK(main_profile_predictor_
);
76 main_profile_predictor_
->incognito_predictor_
= this;
77 if (main_profile_predictor_
->initialized_
)
78 CopyFromMainProfile();
80 // Request the in-memory database from the history to force it to load so
81 // it's available as soon as possible.
82 HistoryService
* history_service
= HistoryServiceFactory::GetForProfile(
83 profile_
, Profile::EXPLICIT_ACCESS
);
85 history_service
->InMemoryDatabase();
88 PredictorDatabaseFactory::GetForProfile(profile_
)->autocomplete_table();
90 // Observe all main frame loads so we can wait for the first to complete
91 // before accessing DB and IO threads to build the local cache.
92 notification_registrar_
.Add(this,
93 content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME
,
94 content::NotificationService::AllSources());
98 AutocompleteActionPredictor::~AutocompleteActionPredictor() {
99 if (main_profile_predictor_
)
100 main_profile_predictor_
->incognito_predictor_
= NULL
;
101 else if (incognito_predictor_
)
102 incognito_predictor_
->main_profile_predictor_
= NULL
;
103 if (prerender_handle_
.get())
104 prerender_handle_
->OnCancel();
107 void AutocompleteActionPredictor::RegisterTransitionalMatches(
108 const base::string16
& user_text
,
109 const AutocompleteResult
& result
) {
110 if (user_text
.length() < kMinimumUserTextLength
)
112 const base::string16
lower_user_text(base::i18n::ToLower(user_text
));
114 // Merge this in to an existing match if we already saw |user_text|
115 std::vector
<TransitionalMatch
>::iterator match_it
=
116 std::find(transitional_matches_
.begin(), transitional_matches_
.end(),
119 if (match_it
== transitional_matches_
.end()) {
120 TransitionalMatch transitional_match
;
121 transitional_match
.user_text
= lower_user_text
;
122 match_it
= transitional_matches_
.insert(transitional_matches_
.end(),
126 for (AutocompleteResult::const_iterator
i(result
.begin()); i
!= result
.end();
128 if (std::find(match_it
->urls
.begin(), match_it
->urls
.end(),
129 i
->destination_url
) == match_it
->urls
.end()) {
130 match_it
->urls
.push_back(i
->destination_url
);
135 void AutocompleteActionPredictor::ClearTransitionalMatches() {
136 transitional_matches_
.clear();
139 void AutocompleteActionPredictor::StartPrerendering(
141 const content::SessionStorageNamespaceMap
& session_storage_namespace_map
,
142 const gfx::Size
& size
) {
143 // Only cancel the old prerender after starting the new one, so if the URLs
144 // are the same, the underlying prerender will be reused.
145 scoped_ptr
<prerender::PrerenderHandle
> old_prerender_handle(
146 prerender_handle_
.release());
147 if (prerender::PrerenderManager
* prerender_manager
=
148 prerender::PrerenderManagerFactory::GetForProfile(profile_
)) {
149 content::SessionStorageNamespace
* session_storage_namespace
= NULL
;
150 content::SessionStorageNamespaceMap::const_iterator it
=
151 session_storage_namespace_map
.find(std::string());
152 if (it
!= session_storage_namespace_map
.end())
153 session_storage_namespace
= it
->second
.get();
154 prerender_handle_
.reset(prerender_manager
->AddPrerenderFromOmnibox(
155 url
, session_storage_namespace
, size
));
157 if (old_prerender_handle
)
158 old_prerender_handle
->OnCancel();
161 // Given a match, return a recommended action.
162 AutocompleteActionPredictor::Action
163 AutocompleteActionPredictor::RecommendAction(
164 const base::string16
& user_text
,
165 const AutocompleteMatch
& match
) const {
166 bool is_in_db
= false;
167 const double confidence
= CalculateConfidence(user_text
, match
, &is_in_db
);
168 DCHECK(confidence
>= 0.0 && confidence
<= 1.0);
170 UMA_HISTOGRAM_BOOLEAN("AutocompleteActionPredictor.MatchIsInDb", is_in_db
);
173 // Multiple enties with the same URL are fine as the confidence may be
175 tracked_urls_
.push_back(std::make_pair(match
.destination_url
, confidence
));
176 UMA_HISTOGRAM_COUNTS_100("AutocompleteActionPredictor.Confidence",
180 // Map the confidence to an action.
181 Action action
= ACTION_NONE
;
182 for (int i
= 0; i
< LAST_PREDICT_ACTION
; ++i
) {
183 if (confidence
>= kConfidenceCutoff
[i
]) {
184 action
= static_cast<Action
>(i
);
189 // Downgrade prerender to preconnect if this is a search match or if omnibox
190 // prerendering is disabled. There are cases when Instant will not handle a
191 // search suggestion and in those cases it would be good to prerender the
192 // search results, however search engines have not been set up to correctly
193 // handle being prerendered and until they are we should avoid it.
194 // http://crbug.com/117495
195 if (action
== ACTION_PRERENDER
&&
196 (AutocompleteMatch::IsSearchType(match
.type
) ||
197 !prerender::IsOmniboxEnabled(profile_
))) {
198 action
= ACTION_PRECONNECT
;
204 // Return true if the suggestion type warrants a TCP/IP preconnection.
205 // i.e., it is now quite likely that the user will select the related domain.
207 bool AutocompleteActionPredictor::IsPreconnectable(
208 const AutocompleteMatch
& match
) {
209 return AutocompleteMatch::IsSearchType(match
.type
);
212 void AutocompleteActionPredictor::Observe(
214 const content::NotificationSource
& source
,
215 const content::NotificationDetails
& details
) {
217 case content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME
:
218 CreateLocalCachesFromDatabase();
219 notification_registrar_
.Remove(
221 content::NOTIFICATION_LOAD_COMPLETED_MAIN_FRAME
,
222 content::NotificationService::AllSources());
225 case chrome::NOTIFICATION_HISTORY_URLS_DELETED
: {
226 DCHECK(initialized_
);
227 const content::Details
<const history::URLsDeletedDetails
>
228 urls_deleted_details
=
229 content::Details
<const history::URLsDeletedDetails
>(details
);
230 if (urls_deleted_details
->all_history
)
233 DeleteRowsWithURLs(urls_deleted_details
->rows
);
237 // This notification does not catch all instances of the user navigating
238 // from the Omnibox, but it does catch the cases where the dropdown is open
239 // and those are the events we're most interested in.
240 case chrome::NOTIFICATION_OMNIBOX_OPENED_URL
: {
241 DCHECK(initialized_
);
243 // TODO(dominich): This doesn't need to be synchronous. Investigate
244 // posting it as a task to be run later.
245 OnOmniboxOpenedUrl(*content::Details
<OmniboxLog
>(details
).ptr());
249 case chrome::NOTIFICATION_HISTORY_LOADED
: {
250 TryDeleteOldEntries(content::Details
<HistoryService
>(details
).ptr());
252 notification_registrar_
.Remove(this,
253 chrome::NOTIFICATION_HISTORY_LOADED
,
254 content::Source
<Profile
>(profile_
));
259 NOTREACHED() << "Unexpected notification observed.";
264 void AutocompleteActionPredictor::CreateLocalCachesFromDatabase() {
265 // Create local caches using the database as loaded. We will garbage collect
266 // rows from the caches and the database once the history service is
268 std::vector
<AutocompleteActionPredictorTable::Row
>* rows
=
269 new std::vector
<AutocompleteActionPredictorTable::Row
>();
270 content::BrowserThread::PostTaskAndReply(content::BrowserThread::DB
,
272 base::Bind(&AutocompleteActionPredictorTable::GetAllRows
, table_
, rows
),
273 base::Bind(&AutocompleteActionPredictor::CreateCaches
, AsWeakPtr(),
277 void AutocompleteActionPredictor::DeleteAllRows() {
282 db_id_cache_
.clear();
285 content::BrowserThread::PostTask(content::BrowserThread::DB
, FROM_HERE
,
286 base::Bind(&AutocompleteActionPredictorTable::DeleteAllRows
,
290 UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.DatabaseAction",
291 DATABASE_ACTION_DELETE_ALL
, DATABASE_ACTION_COUNT
);
294 void AutocompleteActionPredictor::DeleteRowsWithURLs(
295 const history::URLRows
& rows
) {
299 std::vector
<AutocompleteActionPredictorTable::Row::Id
> id_list
;
301 for (DBCacheMap::iterator it
= db_cache_
.begin(); it
!= db_cache_
.end();) {
302 if (std::find_if(rows
.begin(), rows
.end(),
303 history::URLRow::URLRowHasURL(it
->first
.url
)) != rows
.end()) {
304 const DBIdCacheMap::iterator id_it
= db_id_cache_
.find(it
->first
);
305 DCHECK(id_it
!= db_id_cache_
.end());
306 id_list
.push_back(id_it
->second
);
307 db_id_cache_
.erase(id_it
);
308 db_cache_
.erase(it
++);
315 content::BrowserThread::PostTask(content::BrowserThread::DB
, FROM_HERE
,
316 base::Bind(&AutocompleteActionPredictorTable::DeleteRows
, table_
,
320 UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.DatabaseAction",
321 DATABASE_ACTION_DELETE_SOME
, DATABASE_ACTION_COUNT
);
324 void AutocompleteActionPredictor::OnOmniboxOpenedUrl(const OmniboxLog
& log
) {
325 if (log
.text
.length() < kMinimumUserTextLength
)
328 const AutocompleteMatch
& match
= log
.result
.match_at(log
.selected_index
);
330 UMA_HISTOGRAM_BOOLEAN(
331 base::StringPrintf("Prerender.OmniboxNavigationsCouldPrerender%s",
332 prerender::PrerenderManager::GetModeString()).c_str(),
333 prerender::IsOmniboxEnabled(profile_
));
335 const GURL
& opened_url
= match
.destination_url
;
336 const base::string16
lower_user_text(base::i18n::ToLower(log
.text
));
338 // Traverse transitional matches for those that have a user_text that is a
339 // prefix of |lower_user_text|.
340 std::vector
<AutocompleteActionPredictorTable::Row
> rows_to_add
;
341 std::vector
<AutocompleteActionPredictorTable::Row
> rows_to_update
;
343 for (std::vector
<TransitionalMatch
>::const_iterator it
=
344 transitional_matches_
.begin(); it
!= transitional_matches_
.end();
346 if (!StartsWith(lower_user_text
, it
->user_text
, true))
349 // Add entries to the database for those matches.
350 for (std::vector
<GURL
>::const_iterator url_it
= it
->urls
.begin();
351 url_it
!= it
->urls
.end(); ++url_it
) {
352 DCHECK(it
->user_text
.length() >= kMinimumUserTextLength
);
353 const DBCacheKey key
= { it
->user_text
, *url_it
};
354 const bool is_hit
= (*url_it
== opened_url
);
356 AutocompleteActionPredictorTable::Row row
;
357 row
.user_text
= key
.user_text
;
360 DBCacheMap::iterator it
= db_cache_
.find(key
);
361 if (it
== db_cache_
.end()) {
362 row
.id
= base::GenerateGUID();
363 row
.number_of_hits
= is_hit
? 1 : 0;
364 row
.number_of_misses
= is_hit
? 0 : 1;
366 rows_to_add
.push_back(row
);
368 DCHECK(db_id_cache_
.find(key
) != db_id_cache_
.end());
369 row
.id
= db_id_cache_
.find(key
)->second
;
370 row
.number_of_hits
= it
->second
.number_of_hits
+ (is_hit
? 1 : 0);
371 row
.number_of_misses
= it
->second
.number_of_misses
+ (is_hit
? 0 : 1);
373 rows_to_update
.push_back(row
);
377 if (rows_to_add
.size() > 0 || rows_to_update
.size() > 0)
378 AddAndUpdateRows(rows_to_add
, rows_to_update
);
380 ClearTransitionalMatches();
382 // Check against tracked urls and log accuracy for the confidence we
384 for (std::vector
<std::pair
<GURL
, double> >::const_iterator it
=
385 tracked_urls_
.begin(); it
!= tracked_urls_
.end();
387 if (opened_url
== it
->first
) {
388 UMA_HISTOGRAM_COUNTS_100("AutocompleteActionPredictor.AccurateCount",
392 tracked_urls_
.clear();
395 void AutocompleteActionPredictor::AddAndUpdateRows(
396 const AutocompleteActionPredictorTable::Rows
& rows_to_add
,
397 const AutocompleteActionPredictorTable::Rows
& rows_to_update
) {
401 for (AutocompleteActionPredictorTable::Rows::const_iterator it
=
402 rows_to_add
.begin(); it
!= rows_to_add
.end(); ++it
) {
403 const DBCacheKey key
= { it
->user_text
, it
->url
};
404 DBCacheValue value
= { it
->number_of_hits
, it
->number_of_misses
};
406 DCHECK(db_cache_
.find(key
) == db_cache_
.end());
408 db_cache_
[key
] = value
;
409 db_id_cache_
[key
] = it
->id
;
410 UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.DatabaseAction",
411 DATABASE_ACTION_ADD
, DATABASE_ACTION_COUNT
);
413 for (AutocompleteActionPredictorTable::Rows::const_iterator it
=
414 rows_to_update
.begin(); it
!= rows_to_update
.end(); ++it
) {
415 const DBCacheKey key
= { it
->user_text
, it
->url
};
417 DBCacheMap::iterator db_it
= db_cache_
.find(key
);
418 DCHECK(db_it
!= db_cache_
.end());
419 DCHECK(db_id_cache_
.find(key
) != db_id_cache_
.end());
421 db_it
->second
.number_of_hits
= it
->number_of_hits
;
422 db_it
->second
.number_of_misses
= it
->number_of_misses
;
423 UMA_HISTOGRAM_ENUMERATION("AutocompleteActionPredictor.DatabaseAction",
424 DATABASE_ACTION_UPDATE
, DATABASE_ACTION_COUNT
);
428 content::BrowserThread::PostTask(content::BrowserThread::DB
, FROM_HERE
,
429 base::Bind(&AutocompleteActionPredictorTable::AddAndUpdateRows
,
430 table_
, rows_to_add
, rows_to_update
));
434 void AutocompleteActionPredictor::CreateCaches(
435 std::vector
<AutocompleteActionPredictorTable::Row
>* rows
) {
436 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
437 DCHECK(!profile_
->IsOffTheRecord());
438 DCHECK(!initialized_
);
439 DCHECK(db_cache_
.empty());
440 DCHECK(db_id_cache_
.empty());
442 for (std::vector
<AutocompleteActionPredictorTable::Row
>::const_iterator it
=
443 rows
->begin(); it
!= rows
->end(); ++it
) {
444 const DBCacheKey key
= { it
->user_text
, it
->url
};
445 const DBCacheValue value
= { it
->number_of_hits
, it
->number_of_misses
};
446 db_cache_
[key
] = value
;
447 db_id_cache_
[key
] = it
->id
;
450 // If the history service is ready, delete any old or invalid entries.
451 HistoryService
* history_service
=
452 HistoryServiceFactory::GetForProfile(profile_
, Profile::EXPLICIT_ACCESS
);
453 if (!TryDeleteOldEntries(history_service
)) {
454 // Wait for the notification that the history service is ready and the URL
456 notification_registrar_
.Add(this, chrome::NOTIFICATION_HISTORY_LOADED
,
457 content::Source
<Profile
>(profile_
));
461 bool AutocompleteActionPredictor::TryDeleteOldEntries(HistoryService
* service
) {
462 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
463 DCHECK(!profile_
->IsOffTheRecord());
464 DCHECK(!initialized_
);
469 history::URLDatabase
* url_db
= service
->InMemoryDatabase();
473 DeleteOldEntries(url_db
);
477 void AutocompleteActionPredictor::DeleteOldEntries(
478 history::URLDatabase
* url_db
) {
479 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
480 DCHECK(!profile_
->IsOffTheRecord());
481 DCHECK(!initialized_
);
482 DCHECK(table_
.get());
484 std::vector
<AutocompleteActionPredictorTable::Row::Id
> ids_to_delete
;
485 DeleteOldIdsFromCaches(url_db
, &ids_to_delete
);
487 content::BrowserThread::PostTask(content::BrowserThread::DB
, FROM_HERE
,
488 base::Bind(&AutocompleteActionPredictorTable::DeleteRows
, table_
,
491 FinishInitialization();
492 if (incognito_predictor_
)
493 incognito_predictor_
->CopyFromMainProfile();
496 void AutocompleteActionPredictor::DeleteOldIdsFromCaches(
497 history::URLDatabase
* url_db
,
498 std::vector
<AutocompleteActionPredictorTable::Row::Id
>* id_list
) {
499 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
500 DCHECK(!profile_
->IsOffTheRecord());
501 DCHECK(!initialized_
);
506 for (DBCacheMap::iterator it
= db_cache_
.begin(); it
!= db_cache_
.end();) {
507 history::URLRow url_row
;
509 if ((url_db
->GetRowForURL(it
->first
.url
, &url_row
) == 0) ||
510 ((base::Time::Now() - url_row
.last_visit()).InDays() >
511 kMaximumDaysToKeepEntry
)) {
512 const DBIdCacheMap::iterator id_it
= db_id_cache_
.find(it
->first
);
513 DCHECK(id_it
!= db_id_cache_
.end());
514 id_list
->push_back(id_it
->second
);
515 db_id_cache_
.erase(id_it
);
516 db_cache_
.erase(it
++);
523 void AutocompleteActionPredictor::CopyFromMainProfile() {
524 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
525 DCHECK(profile_
->IsOffTheRecord());
526 DCHECK(!initialized_
);
527 DCHECK(main_profile_predictor_
);
528 DCHECK(main_profile_predictor_
->initialized_
);
530 db_cache_
= main_profile_predictor_
->db_cache_
;
531 db_id_cache_
= main_profile_predictor_
->db_id_cache_
;
532 FinishInitialization();
535 void AutocompleteActionPredictor::FinishInitialization() {
536 CHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::UI
));
537 DCHECK(!initialized_
);
539 // Incognito and normal profiles should listen only to omnibox notifications
540 // from their own profile, but both should listen to history deletions from
541 // the main profile, since opening the history page in either case actually
542 // opens the non-incognito history (and lets users delete from there).
543 notification_registrar_
.Add(this, chrome::NOTIFICATION_OMNIBOX_OPENED_URL
,
544 content::Source
<Profile
>(profile_
));
545 notification_registrar_
.Add(this, chrome::NOTIFICATION_HISTORY_URLS_DELETED
,
546 content::Source
<Profile
>(profile_
->GetOriginalProfile()));
550 double AutocompleteActionPredictor::CalculateConfidence(
551 const base::string16
& user_text
,
552 const AutocompleteMatch
& match
,
553 bool* is_in_db
) const {
554 const DBCacheKey key
= { user_text
, match
.destination_url
};
557 if (user_text
.length() < kMinimumUserTextLength
)
560 const DBCacheMap::const_iterator iter
= db_cache_
.find(key
);
561 if (iter
== db_cache_
.end())
565 return CalculateConfidenceForDbEntry(iter
);
568 double AutocompleteActionPredictor::CalculateConfidenceForDbEntry(
569 DBCacheMap::const_iterator iter
) const {
570 const DBCacheValue
& value
= iter
->second
;
571 if (value
.number_of_hits
< kMinimumNumberOfHits
)
574 const double number_of_hits
= static_cast<double>(value
.number_of_hits
);
575 return number_of_hits
/ (number_of_hits
+ value
.number_of_misses
);
578 AutocompleteActionPredictor::TransitionalMatch::TransitionalMatch() {
581 AutocompleteActionPredictor::TransitionalMatch::~TransitionalMatch() {
584 } // namespace predictors