Index: components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
diff --git a/components/ntp_snippets/category_rankers/click_based_category_ranker.cc b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
index 5b7ca83b02ef93fc436c1589512772024abebc82..fa6a28c7ad440245c9b7ac531faee2aec4ad7488 100644 |
--- a/components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
+++ b/components/ntp_snippets/category_rankers/click_based_category_ranker.cc |
@@ -38,6 +38,23 @@ const int kNumTopCategoriesWithExtraMargin = 3; |
// value than the first non-top category). |
const int kExtraPassingMargin = 2; |
+// The ranker must "forget" history with time, so that changes in the user |
+// behavior are reflected by the order in reasonable time. This is done using |
+// click count decay with time. However, if there is not enough data, there is |
+// no need in "forgetting" it. This value defines how many total clicks (across |
+// categories) are considered enough to decay. |
+const int kMinNumClicksToDecay = 30; |
+ |
+// Time between two consecutive decays (assuming enough clicks). |
+const base::TimeDelta kTimeBetweenDecays = base::TimeDelta::FromDays(1); |
+ |
+// Decay factor as a fraction. The current value approximates the seventh root |
+// of 0.5. This yields a 50% decay per seven decays. Seven weak decays are used |
+// instead of one 50% decay in order to decrease difference of click weight in |
+// time. |
+const int kDecayFactorNumerator = 91; |
+const int kDecayFactorDenominator = 100; // pow(0.91, 7) = 0.517 |
+ |
// Number of positions by which a dismissed category is downgraded. |
const int kDismissedCategoryPenalty = 1; |
@@ -46,14 +63,20 @@ const char kClicksKey[] = "clicks"; |
} // namespace |
-ClickBasedCategoryRanker::ClickBasedCategoryRanker(PrefService* pref_service) |
- : pref_service_(pref_service) { |
+ClickBasedCategoryRanker::ClickBasedCategoryRanker( |
+ PrefService* pref_service, |
+ std::unique_ptr<base::Clock> clock) |
+ : pref_service_(pref_service), clock_(std::move(clock)) { |
if (!ReadOrderFromPrefs(&ordered_categories_)) { |
// TODO(crbug.com/676273): Handle adding new hardcoded KnownCategories to |
// existing order from prefs. Currently such new category is completely |
// ignored and may be never shown. |
RestoreDefaultOrder(); |
} |
+ |
+ if (ReadLastDecayTimeFromPrefs() == base::Time::FromInternalValue(0)) { |
+ StoreLastDecayTimeToPrefs(clock_->Now()); |
+ } |
} |
ClickBasedCategoryRanker::~ClickBasedCategoryRanker() = default; |
@@ -86,6 +109,8 @@ bool ClickBasedCategoryRanker::Compare(Category left, Category right) const { |
} |
void ClickBasedCategoryRanker::ClearHistory(base::Time begin, base::Time end) { |
+ StoreLastDecayTimeToPrefs(base::Time::FromInternalValue(0)); |
+ |
// The categories added through |AppendCategoryIfNecessary| cannot be |
// completely removed, since no one is required to reregister them. Instead |
// they are preserved in the default order (sorted by id). |
@@ -128,15 +153,20 @@ void ClickBasedCategoryRanker::OnSuggestionOpened(Category category) { |
return; |
} |
+ DecayClicksIfNeeded(); |
+ |
std::vector<RankedCategory>::iterator current = FindCategory(category); |
+ DCHECK_GE(current->clicks, 0); |
+ // The overflow is ignored. It is unlikely to happen, because of click count |
+ // decay. |
current->clicks++; |
- // TODO(crbug.com/676279): Prevent overflow. |
+ // Move the category up if appropriate. |
if (current != ordered_categories_.begin()) { |
std::vector<RankedCategory>::iterator previous = current - 1; |
const int passing_margin = GetPositionPassingMargin(previous); |
if (current->clicks >= previous->clicks + passing_margin) { |
- // It is intended to move only by one position at a time in order to avoid |
+ // It is intended to move only by one position per click in order to avoid |
// dramatic changes, which could confuse the user. |
std::swap(*current, *previous); |
} |
@@ -172,10 +202,16 @@ void ClickBasedCategoryRanker::OnCategoryDismissed(Category category) { |
StoreOrderToPrefs(ordered_categories_); |
} |
+base::Time ClickBasedCategoryRanker::GetLastDecayTime() const { |
+ return ReadLastDecayTimeFromPrefs(); |
+} |
+ |
// static |
void ClickBasedCategoryRanker::RegisterProfilePrefs( |
PrefRegistrySimple* registry) { |
registry->RegisterListPref(prefs::kClickBasedCategoryRankerOrderWithClicks); |
+ registry->RegisterInt64Pref(prefs::kClickBasedCategoryRankerLastDecayTime, |
+ /*default_value=*/0); |
} |
// static |
@@ -231,7 +267,7 @@ void ClickBasedCategoryRanker::AppendKnownCategory( |
} |
bool ClickBasedCategoryRanker::ReadOrderFromPrefs( |
- std::vector<RankedCategory>* result_categories) { |
+ std::vector<RankedCategory>* result_categories) const { |
result_categories->clear(); |
const base::ListValue* list = |
pref_service_->GetList(prefs::kClickBasedCategoryRankerOrderWithClicks); |
@@ -291,4 +327,59 @@ bool ClickBasedCategoryRanker::ContainsCategory(Category category) const { |
return false; |
} |
+base::Time ClickBasedCategoryRanker::ReadLastDecayTimeFromPrefs() const { |
+ return base::Time::FromInternalValue( |
+ pref_service_->GetInt64(prefs::kClickBasedCategoryRankerLastDecayTime)); |
+} |
+ |
+void ClickBasedCategoryRanker::StoreLastDecayTimeToPrefs( |
+ base::Time last_decay_time) { |
+ pref_service_->SetInt64(prefs::kClickBasedCategoryRankerLastDecayTime, |
+ last_decay_time.ToInternalValue()); |
+} |
+ |
+bool ClickBasedCategoryRanker::IsEnoughClicksToDecay() const { |
+ int64_t num_clicks = 0; |
+ for (const RankedCategory& ranked_category : ordered_categories_) { |
+ num_clicks += ranked_category.clicks; |
+ } |
+ return num_clicks >= kMinNumClicksToDecay; |
+} |
+ |
+bool ClickBasedCategoryRanker::DecayClicksIfNeeded() { |
+ base::Time now = clock_->Now(); |
+ base::Time last_decay = ReadLastDecayTimeFromPrefs(); |
+ if (last_decay == base::Time::FromInternalValue(0)) { |
+ // No last decay time, start from now. |
+ StoreLastDecayTimeToPrefs(clock_->Now()); |
+ return false; |
+ } |
+ DCHECK_LE(last_decay, now); |
+ |
+ int num_pending_decays = (now - last_decay) / kTimeBetweenDecays; |
+ int executed_decays = 0; |
+ while (executed_decays < num_pending_decays && IsEnoughClicksToDecay()) { |
+ for (RankedCategory& ranked_category : ordered_categories_) { |
+ DCHECK_GE(ranked_category.clicks, 0); |
+ const int64_t old_clicks = static_cast<int64_t>(ranked_category.clicks); |
+ ranked_category.clicks = |
+ old_clicks * kDecayFactorNumerator / kDecayFactorDenominator; |
+ } |
+ |
+ ++executed_decays; |
+ } |
+ |
+ // No matter how many decays were actually executed, all of them are marked |
+ // done. Even if some were ignored due to absense of clicks, they would have |
+ // no effect anyway for the same reason. |
+ StoreLastDecayTimeToPrefs(last_decay + |
+ num_pending_decays * kTimeBetweenDecays); |
+ |
+ if (executed_decays > 0) { |
+ StoreOrderToPrefs(ordered_categories_); |
+ return true; |
+ } |
+ return false; |
+} |
+ |
} // namespace ntp_snippets |