1 // Copyright 2018 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/resource_coordinator/tab_ranker/tab_score_predictor.h"
6
7 #include <memory>
8 #include <string>
9
10 #include "base/logging.h"
11 #include "base/memory/ref_counted_memory.h"
12 #include "base/memory/scoped_refptr.h"
13 #include "base/strings/strcat.h"
14 #include "chrome/browser/resource_coordinator/tab_manager_features.h"
15 #include "chrome/browser/resource_coordinator/tab_ranker/native_inference.h"
16 #include "chrome/browser/resource_coordinator/tab_ranker/pairwise_inference.h"
17 #include "chrome/browser/resource_coordinator/tab_ranker/tab_features.h"
18 #include "chrome/grit/browser_resources.h"
19 #include "components/assist_ranker/example_preprocessing.h"
20 #include "components/assist_ranker/proto/example_preprocessor.pb.h"
21 #include "components/assist_ranker/proto/ranker_example.pb.h"
22 #include "ui/base/resource/resource_bundle.h"
23
24 namespace tab_ranker {
25 namespace {
26
27 using resource_coordinator::GetDiscardCountPenaltyTabRanker;
28 using resource_coordinator::GetMRUScorerPenaltyTabRanker;
29 using resource_coordinator::GetScorerTypeForTabRanker;
30
31 // Maps the |mru_index| to it's reverse rank in (0.0, 1.0).
32 // High score means more likely to be reactivated.
33 // We use inverse rank because we think that the first several |mru_index| is
34 // more significant than the larger ones.
MruToScore(const float mru_index)35 inline float MruToScore(const float mru_index) {
36 DCHECK_GE(mru_index, 0.0f);
37 return 1.0f / (1.0f + mru_index);
38 }
39
40 // Maps the |discard_count| to a score in (0.0, 1.0), for which
41 // High score means more likely to be reactivated.
42 // We use std::exp because we think that the first several |discard_count| is
43 // not as significant as the larger ones.
DiscardCountToScore(const float discard_count)44 inline float DiscardCountToScore(const float discard_count) {
45 return std::exp(discard_count);
46 }
47
48 // Loads the preprocessor config protobuf, which lists each feature, their
49 // types, bucket configurations, etc.
50 // Returns nullptr if the protobuf was not successfully populated.
51 std::unique_ptr<assist_ranker::ExamplePreprocessorConfig>
LoadExamplePreprocessorConfig(const int resource_id)52 LoadExamplePreprocessorConfig(const int resource_id) {
53 auto config = std::make_unique<assist_ranker::ExamplePreprocessorConfig>();
54
55 scoped_refptr<base::RefCountedMemory> raw_config =
56 ui::ResourceBundle::GetSharedInstance().LoadDataResourceBytes(
57 resource_id);
58 if (!raw_config || !raw_config->front()) {
59 LOG(ERROR) << "Failed to load TabRanker example preprocessor config.";
60 return nullptr;
61 }
62
63 if (!config->ParseFromArray(raw_config->front(), raw_config->size())) {
64 LOG(ERROR) << "Failed to parse TabRanker example preprocessor config.";
65 return nullptr;
66 }
67
68 return config;
69 }
70
71 } // namespace
72
TabScorePredictor()73 TabScorePredictor::TabScorePredictor()
74 : discard_count_penalty_(GetDiscardCountPenaltyTabRanker()),
75 mru_scorer_penalty_(GetMRUScorerPenaltyTabRanker()),
76 type_(static_cast<ScorerType>(GetScorerTypeForTabRanker())) {}
77
78 TabScorePredictor::~TabScorePredictor() = default;
79
ScoreTab(const TabFeatures & tab,float * score)80 TabRankerResult TabScorePredictor::ScoreTab(const TabFeatures& tab,
81 float* score) {
82 DCHECK(score);
83
84 // No error is expected, but something could conceivably be misconfigured.
85 TabRankerResult result = TabRankerResult::kSuccess;
86
87 if (type_ == kMRUScorer) {
88 result = ScoreTabWithMRUScorer(tab, score);
89 } else if (type_ == kMLScorer) {
90 result = ScoreTabWithMLScorer(tab, score);
91 } else if (type_ == kPairwiseScorer) {
92 result = ScoreTabsPairs(tab, TabFeatures(), score);
93 } else if (type_ == kFrecencyScorer) {
94 result = ScoreTabWithFrecencyScorer(tab, score);
95 } else {
96 return TabRankerResult::kUnrecognizableScorer;
97 }
98
99 // Applies DiscardCount adjustment.
100 // The default value of discard_count_penalty_ is 0.0f, which will not change
101 // the score.
102 // The larger the |discard_count_penalty_| is (set from Finch), the quicker
103 // the score increases based on the discard_count.
104 *score *= DiscardCountToScore(tab.discard_count * discard_count_penalty_);
105
106 return result;
107 }
108
ScoreTabs(const std::map<int32_t,base::Optional<TabFeatures>> & tabs)109 std::map<int32_t, float> TabScorePredictor::ScoreTabs(
110 const std::map<int32_t, base::Optional<TabFeatures>>& tabs) {
111 if (type_ != kPairwiseScorer) {
112 std::map<int32_t, float> reactivation_scores;
113 for (const auto& pair : tabs) {
114 float score = 0.0f;
115 if (pair.second && (ScoreTab(pair.second.value(), &score) ==
116 TabRankerResult::kSuccess)) {
117 reactivation_scores[pair.first] = score;
118 } else {
119 reactivation_scores[pair.first] = std::numeric_limits<float>::max();
120 }
121 }
122 return reactivation_scores;
123 } else {
124 return ScoreTabsWithPairwiseScorer(tabs);
125 }
126 }
127
ScoreTabWithMLScorer(const TabFeatures & tab,float * score)128 TabRankerResult TabScorePredictor::ScoreTabWithMLScorer(const TabFeatures& tab,
129 float* score) {
130 // Lazy-load the preprocessor config.
131 LazyInitialize();
132 if (!preprocessor_config_ || !tfnative_alloc_) {
133 return TabRankerResult::kPreprocessorInitializationFailed;
134 }
135 // Build the RankerExample using the tab's features.
136 assist_ranker::RankerExample example;
137 PopulateTabFeaturesToRankerExample(tab, &example);
138 return PredictWithPreprocess(&example, score);
139 }
140
PredictWithPreprocess(assist_ranker::RankerExample * example,float * score)141 TabRankerResult TabScorePredictor::PredictWithPreprocess(
142 assist_ranker::RankerExample* example,
143 float* score) {
144 // Process the RankerExample with the tab ranker config to vectorize the
145 // feature list for inference.
146 int preprocessor_error = assist_ranker::ExamplePreprocessor::Process(
147 *preprocessor_config_, example, true);
148 if (preprocessor_error) {
149 // kNoFeatureIndexFound can occur normally (e.g., when the domain name
150 // isn't known to the model or a rarely seen enum value is used).
151 DCHECK_EQ(assist_ranker::ExamplePreprocessor::kNoFeatureIndexFound,
152 preprocessor_error);
153 }
154
155 // This vector will be provided to the inference function.
156 const auto& vectorized_features =
157 example->features()
158 .at(assist_ranker::ExamplePreprocessor::kVectorizedFeatureDefaultName)
159 .float_list()
160 .float_value();
161
162 // Call correct inference function based on the type_.
163 if (type_ == kMLScorer)
164 tfnative_model::Inference(vectorized_features.data(), score,
165 tfnative_alloc_.get());
166 if (type_ == kPairwiseScorer)
167 pairwise_model::Inference(vectorized_features.data(), score,
168 pairwise_alloc_.get());
169
170 if (preprocessor_error != assist_ranker::ExamplePreprocessor::kSuccess &&
171 preprocessor_error !=
172 assist_ranker::ExamplePreprocessor::kNoFeatureIndexFound) {
173 // May indicate something is wrong with how we create the RankerExample.
174 return TabRankerResult::kPreprocessorOtherError;
175 } else {
176 return TabRankerResult::kSuccess;
177 }
178 }
179
ScoreTabWithMRUScorer(const TabFeatures & tab,float * score)180 TabRankerResult TabScorePredictor::ScoreTabWithMRUScorer(const TabFeatures& tab,
181 float* score) {
182 *score = MruToScore(tab.mru_index * mru_scorer_penalty_);
183 return TabRankerResult::kSuccess;
184 }
185
ScoreTabsPairs(const TabFeatures & tab1,const TabFeatures & tab2,float * score)186 TabRankerResult TabScorePredictor::ScoreTabsPairs(const TabFeatures& tab1,
187 const TabFeatures& tab2,
188 float* score) {
189 if (type_ == kPairwiseScorer) {
190 // Lazy-load the preprocessor config.
191 LazyInitialize();
192 if (!preprocessor_config_ || !pairwise_alloc_) {
193 return TabRankerResult::kPreprocessorInitializationFailed;
194 }
195
196 // Build the RankerExamples using the tab's features.
197 assist_ranker::RankerExample example1, example2;
198 PopulateTabFeaturesToRankerExample(tab1, &example1);
199 PopulateTabFeaturesToRankerExample(tab2, &example2);
200
201 // Merge features from example2 to example1.
202 auto& features = *example1.mutable_features();
203 for (const auto& feature : example2.features()) {
204 const std::string new_name = base::StrCat({feature.first, "_1"});
205 features[new_name] = feature.second;
206 }
207
208 // Inference on example1.
209 return PredictWithPreprocess(&example1, score);
210 } else {
211 // For non-pairwise scorer, we simply calculate the score of each tab and
212 // return the difference.
213 float score1, score2;
214 const TabRankerResult result1 = ScoreTab(tab1, &score1);
215 const TabRankerResult result2 = ScoreTab(tab2, &score2);
216 *score = score1 - score2;
217 return std::max(result1, result2);
218 }
219 }
220
ScoreTabsWithPairwiseScorer(const std::map<int32_t,base::Optional<TabFeatures>> & tabs)221 std::map<int32_t, float> TabScorePredictor::ScoreTabsWithPairwiseScorer(
222 const std::map<int32_t, base::Optional<TabFeatures>>& tabs) {
223 const int N = tabs.size();
224
225 std::vector<int32_t> ids;
226 for (const auto& pair : tabs) {
227 ids.push_back(pair.first);
228 }
229
230 // Sort ids by MRU first.
231 // Put the tabs without TabFeatures in front so that they won't be discarded
232 // mistakenly (including current Foregrounded tab).
233 std::sort(ids.begin(), ids.end(),
234 [&tabs](const int32_t id1, const int32_t id2) {
235 const auto& tab1 = tabs.at(id1);
236 const auto& tab2 = tabs.at(id2);
237 if (!tab2)
238 return false;
239 if (!tab1)
240 return true;
241 return tab1->mru_index < tab2->mru_index;
242 });
243
244 std::map<int32_t, float> reactivation_scores;
245
246 // start_index is the first one that has tab_features.
247 int start_index = 0;
248 for (int i = 0; i < N; ++i) {
249 if (!tabs.at(ids[i])) {
250 reactivation_scores[ids[i]] = N - i;
251 start_index = i + 1;
252 } else {
253 break;
254 }
255 }
256
257 // winning_indices records what's the best tab to be put at pos i.
258 std::vector<int> winning_indices;
259 for (int i = 0; i < N; ++i)
260 winning_indices.push_back(i);
261
262 int winning_index = N - 1;
263 int swapped_index = N - 1;
264 for (int j = start_index; j < N; ++j) {
265 // Find the best candidate at j.
266
267 // swapped_index < N - 1 means that one element has
268 // just been swapped to swapped_index, we should re-calculate
269 // winning_indices from swapped_index to j;
270 if (swapped_index < N - 1) {
271 // Set winning_index as the winning_indices at swapped_index + 1, since
272 // ids from ids.back() to ids[swapped_index + 1] are not
273 // changed.
274 winning_index = winning_indices[swapped_index + 1];
275 }
276
277 for (int i = swapped_index; i >= j; --i) {
278 // Compare ids[i] with ids[winning_index]; inference score > 0 means
279 // that ids[i] is more likely to be reactivated, so we should prefer
280 // ids[i] as new winning_index.
281 float score = 0.0f;
282 const TabRankerResult result = ScoreTabsPairs(
283 tabs.at(ids[i]).value(), tabs.at(ids[winning_index]).value(), &score);
284 if (result == TabRankerResult::kSuccess && score > 0.0f) {
285 winning_index = i;
286 }
287
288 // Always update winning_indices.
289 winning_indices[i] = winning_index;
290 }
291
292 // swap winning_index with j;
293 std::swap(ids[winning_index], ids[j]);
294 swapped_index = winning_index;
295
296 // Find the best candidate for position j, set the score for ids[j].
297 reactivation_scores[ids[j]] = N - j;
298 }
299 return reactivation_scores;
300 }
LazyInitialize()301 void TabScorePredictor::LazyInitialize() {
302 // Load correct config and alloc based on type_.
303 if (type_ == kMLScorer) {
304 if (!preprocessor_config_)
305 preprocessor_config_ = LoadExamplePreprocessorConfig(
306 IDR_TAB_RANKER_EXAMPLE_PREPROCESSOR_CONFIG_PB);
307 if (!tfnative_alloc_)
308 tfnative_alloc_ = std::make_unique<tfnative_model::FixedAllocations>();
309 DCHECK(preprocessor_config_);
310 DCHECK_EQ(preprocessor_config_->feature_indices().size(),
311 static_cast<std::size_t>(tfnative_model::FEATURES_SIZE));
312 }
313
314 if (type_ == kPairwiseScorer) {
315 if (!preprocessor_config_)
316 preprocessor_config_ = LoadExamplePreprocessorConfig(
317 IDR_TAB_RANKER_PAIRWISE_EXAMPLE_PREPROCESSOR_CONFIG_PB);
318 if (!pairwise_alloc_)
319 pairwise_alloc_ = std::make_unique<pairwise_model::FixedAllocations>();
320 DCHECK(preprocessor_config_);
321 DCHECK_EQ(preprocessor_config_->feature_indices().size(),
322 static_cast<std::size_t>(pairwise_model::FEATURES_SIZE));
323 }
324 }
325
326 // Simply returns the frecency_score in the TabFeatures.
ScoreTabWithFrecencyScorer(const TabFeatures & tab,float * score)327 TabRankerResult TabScorePredictor::ScoreTabWithFrecencyScorer(
328 const TabFeatures& tab,
329 float* score) {
330 *score = tab.frecency_score;
331 return TabRankerResult::kSuccess;
332 }
333
334 } // namespace tab_ranker
335