1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/browsing_topics/epoch_topics.h"
7 #include "base/containers/contains.h"
8 #include "base/hash/legacy_hash.h"
9 #include "base/json/values_util.h"
10 #include "base/logging.h"
11 #include "base/numerics/checked_math.h"
12 #include "components/browsing_topics/common/semantic_tree.h"
13 #include "components/browsing_topics/util.h"
14 #include "third_party/blink/public/common/features.h"
17 namespace browsing_topics {
21 const char kTopTopicsAndObservingDomainsNameKey[] =
22 "top_topics_and_observing_domains";
23 const char kPaddedTopTopicsStartIndexNameKey[] =
24 "padded_top_topics_start_index";
25 const char kConfigVersionNameKey[] = "config_version";
26 const char kTaxonomyVersionNameKey[] = "taxonomy_version";
27 const char kModelVersionNameKey[] = "model_version";
28 const char kCalculationTimeNameKey[] = "calculation_time";
29 // `taxonomy_size` is a deprecated key. Do not reuse.
31 bool ShouldUseRandomTopic(uint64_t random_or_top_topic_decision_hash) {
32 return base::checked_cast<int>(random_or_top_topic_decision_hash % 100) <
33 blink::features::kBrowsingTopicsUseRandomTopicProbabilityPercent.Get();
38 EpochTopics::EpochTopics(base::Time calculation_time)
39 : calculation_time_(calculation_time) {}
41 EpochTopics::EpochTopics(
42 std::vector<TopicAndDomains> top_topics_and_observing_domains,
43 size_t padded_top_topics_start_index,
46 int64_t model_version,
47 base::Time calculation_time,
48 bool from_manually_triggered_calculation)
49 : top_topics_and_observing_domains_(
50 std::move(top_topics_and_observing_domains)),
51 padded_top_topics_start_index_(padded_top_topics_start_index),
52 config_version_(config_version),
53 taxonomy_version_(taxonomy_version),
54 model_version_(model_version),
55 calculation_time_(calculation_time),
56 from_manually_triggered_calculation_(
57 from_manually_triggered_calculation) {
58 DCHECK_EQ(base::checked_cast<int>(top_topics_and_observing_domains_.size()),
59 blink::features::kBrowsingTopicsNumberOfTopTopicsPerEpoch.Get());
60 DCHECK_LE(padded_top_topics_start_index,
61 top_topics_and_observing_domains_.size());
62 DCHECK_GT(config_version_, 0);
63 DCHECK_GT(taxonomy_version_, 0);
64 DCHECK_GT(model_version_, 0);
67 EpochTopics::EpochTopics(EpochTopics&&) = default;
69 EpochTopics& EpochTopics::operator=(EpochTopics&&) = default;
71 EpochTopics::~EpochTopics() = default;
74 EpochTopics EpochTopics::FromDictValue(const base::Value::Dict& dict_value) {
75 const base::Value* calculation_time_value =
76 dict_value.Find(kCalculationTimeNameKey);
77 if (!calculation_time_value)
78 return EpochTopics(base::Time());
80 base::Time calculation_time =
81 base::ValueToTime(calculation_time_value).value();
83 std::vector<TopicAndDomains> top_topics_and_observing_domains;
84 const base::Value::List* top_topics_and_observing_domains_value =
85 dict_value.FindList(kTopTopicsAndObservingDomainsNameKey);
86 if (!top_topics_and_observing_domains_value)
87 return EpochTopics(calculation_time);
89 for (const base::Value& topic_and_observing_domains_value :
90 *top_topics_and_observing_domains_value) {
91 const base::Value::Dict* topic_and_observing_domains_dict_value =
92 topic_and_observing_domains_value.GetIfDict();
93 if (!topic_and_observing_domains_dict_value)
94 return EpochTopics(calculation_time);
96 top_topics_and_observing_domains.push_back(TopicAndDomains::FromDictValue(
97 *topic_and_observing_domains_dict_value));
100 if (top_topics_and_observing_domains.empty())
101 return EpochTopics(calculation_time);
103 absl::optional<int> padded_top_topics_start_index_value =
104 dict_value.FindInt(kPaddedTopTopicsStartIndexNameKey);
105 if (!padded_top_topics_start_index_value)
106 return EpochTopics(calculation_time);
108 size_t padded_top_topics_start_index =
109 static_cast<size_t>(*padded_top_topics_start_index_value);
111 absl::optional<int> config_version_value =
112 dict_value.FindInt(kConfigVersionNameKey);
114 // `kConfigVersionNameKey` is introduced after the initial release. Instead of
115 // treating it as an error and using a fresh epoch, we'll use version 1 which
116 // is the only valid version before this field is introduced.
117 int config_version = config_version_value ? *config_version_value : 1;
119 absl::optional<int> taxonomy_version_value =
120 dict_value.FindInt(kTaxonomyVersionNameKey);
121 if (!taxonomy_version_value)
122 return EpochTopics(calculation_time);
124 int taxonomy_version = *taxonomy_version_value;
126 const base::Value* model_version_value =
127 dict_value.Find(kModelVersionNameKey);
128 if (!model_version_value)
129 return EpochTopics(calculation_time);
131 absl::optional<int64_t> model_version_int64_value =
132 base::ValueToInt64(model_version_value);
133 if (!model_version_int64_value)
134 return EpochTopics(calculation_time);
136 int64_t model_version = *model_version_int64_value;
138 return EpochTopics(std::move(top_topics_and_observing_domains),
139 padded_top_topics_start_index, config_version,
140 taxonomy_version, model_version, calculation_time,
141 /*from_manually_triggered_calculation=*/false);
144 base::Value::Dict EpochTopics::ToDictValue() const {
145 base::Value::List top_topics_and_observing_domains_list;
146 for (const TopicAndDomains& topic_and_domains :
147 top_topics_and_observing_domains_) {
148 top_topics_and_observing_domains_list.Append(
149 topic_and_domains.ToDictValue());
152 base::Value::Dict result_dict;
153 result_dict.Set(kTopTopicsAndObservingDomainsNameKey,
154 std::move(top_topics_and_observing_domains_list));
155 result_dict.Set(kPaddedTopTopicsStartIndexNameKey,
156 base::checked_cast<int>(padded_top_topics_start_index_));
157 result_dict.Set(kConfigVersionNameKey, config_version_);
158 result_dict.Set(kTaxonomyVersionNameKey, taxonomy_version_);
159 result_dict.Set(kModelVersionNameKey, base::Int64ToValue(model_version_));
160 result_dict.Set(kCalculationTimeNameKey,
161 base::TimeToValue(calculation_time_));
165 CandidateTopic EpochTopics::CandidateTopicForSite(
166 const std::string& top_domain,
167 const HashedDomain& hashed_context_domain,
168 ReadOnlyHmacKey hmac_key) const {
169 // The topics calculation failed, or the topics has been cleared.
171 return CandidateTopic::CreateInvalid();
173 uint64_t random_or_top_topic_decision_hash =
174 HashTopDomainForRandomOrTopTopicDecision(hmac_key, calculation_time_,
177 uint64_t top_topic_index_decision_hash =
178 HashTopDomainForTopTopicIndexDecision(hmac_key, calculation_time_,
181 size_t top_topic_index =
182 top_topic_index_decision_hash % top_topics_and_observing_domains_.size();
184 const TopicAndDomains& topic_and_observing_domains =
185 top_topics_and_observing_domains_[top_topic_index];
187 if (!topic_and_observing_domains.IsValid())
188 return CandidateTopic::CreateInvalid();
190 bool is_true_topic = (top_topic_index < padded_top_topics_start_index_);
192 bool should_be_filtered = !topic_and_observing_domains.hashed_domains().count(
193 hashed_context_domain);
195 if (ShouldUseRandomTopic(random_or_top_topic_decision_hash)) {
196 uint64_t random_topic_index_decision =
197 HashTopDomainForRandomTopicIndexDecision(hmac_key, calculation_time_,
199 Topic topic = SemanticTree().GetRandomTopic(taxonomy_version(),
200 random_topic_index_decision);
201 return CandidateTopic::Create(topic, /*is_true_topic=*/false,
202 should_be_filtered, config_version(),
203 taxonomy_version(), model_version());
206 return CandidateTopic::Create(
207 topic_and_observing_domains.topic(), is_true_topic, should_be_filtered,
208 config_version(), taxonomy_version(), model_version());
211 void EpochTopics::ClearTopics() {
212 top_topics_and_observing_domains_.clear();
213 padded_top_topics_start_index_ = 0;
216 void EpochTopics::ClearTopic(Topic topic) {
217 for (TopicAndDomains& top_topic_and_domains :
218 top_topics_and_observing_domains_) {
219 // Invalidate `topic_and_domains`. We cannot delete the entry from
220 // `top_topics_and_observing_domains_` because it would modify the list of
221 // topics, and would break the ability to return the same topic for the same
222 // site for the epoch .
223 if (top_topic_and_domains.topic() == topic) {
224 top_topic_and_domains = TopicAndDomains();
227 SemanticTree semantic_tree;
228 std::vector<Topic> top_topic_ancestors =
229 semantic_tree.GetAncestorTopics(top_topic_and_domains.topic());
230 if (base::Contains(top_topic_ancestors, topic)) {
231 top_topic_and_domains = TopicAndDomains();
236 void EpochTopics::ClearContextDomain(
237 const HashedDomain& hashed_context_domain) {
238 for (TopicAndDomains& topic_and_domains : top_topics_and_observing_domains_) {
239 topic_and_domains.ClearDomain(hashed_context_domain);
243 } // namespace browsing_topics