blob: d55682f67258e1cb68b04e0f2d7073f78dfeb670 [file] [log] [blame]
harkness883658b2016-07-18 11:37:531// Copyright 2016 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/budget_service/budget_database.h"
6
harkness804b612a62016-07-27 12:53:317#include "base/containers/adapters.h"
harknessf8c93432016-08-08 15:41:598#include "base/time/clock.h"
9#include "base/time/default_clock.h"
harkness883658b2016-07-18 11:37:5310#include "chrome/browser/budget_service/budget.pb.h"
11#include "components/leveldb_proto/proto_database_impl.h"
12#include "content/public/browser/browser_thread.h"
13#include "url/gurl.h"
14
harkness804b612a62016-07-27 12:53:3115using content::BrowserThread;
16
harkness883658b2016-07-18 11:37:5317namespace {
18
19// UMA are logged for the database with this string as part of the name.
harknessbea56c22016-08-16 07:23:0020// They will be LevelDB.*.BudgetManager. Changes here should be synchronized
21// with histograms.xml.
22const char kDatabaseUMAName[] = "BudgetManager";
harkness883658b2016-07-18 11:37:5323
harknessf8c93432016-08-08 15:41:5924// The default amount of time during which a budget will be valid.
25// This is 3 days = 72 hours.
26constexpr double kBudgetDurationInHours = 72;
27
harkness883658b2016-07-18 11:37:5328} // namespace
29
harknesse0e26092016-08-24 06:29:3230BudgetDatabase::BudgetInfo::BudgetInfo() {}
31
32BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other)
33 : last_engagement_award(other.last_engagement_award) {
34 chunks = std::move(other.chunks);
35}
36
37BudgetDatabase::BudgetInfo::~BudgetInfo() {}
38
harkness883658b2016-07-18 11:37:5339BudgetDatabase::BudgetDatabase(
40 const base::FilePath& database_dir,
41 const scoped_refptr<base::SequencedTaskRunner>& task_runner)
42 : db_(new leveldb_proto::ProtoDatabaseImpl<budget_service::Budget>(
43 task_runner)),
harknessf8c93432016-08-08 15:41:5944 clock_(base::WrapUnique(new base::DefaultClock)),
harkness883658b2016-07-18 11:37:5345 weak_ptr_factory_(this) {
46 db_->Init(kDatabaseUMAName, database_dir,
47 base::Bind(&BudgetDatabase::OnDatabaseInit,
48 weak_ptr_factory_.GetWeakPtr()));
49}
50
51BudgetDatabase::~BudgetDatabase() {}
52
harkness804b612a62016-07-27 12:53:3153void BudgetDatabase::GetBudgetDetails(
54 const GURL& origin,
55 const GetBudgetDetailsCallback& callback) {
56 DCHECK_EQ(origin.GetOrigin(), origin);
57
58 // If this origin is already in the cache, immediately return the data.
harkness5186e6d822016-08-09 16:54:1459 if (IsCached(origin)) {
harkness804b612a62016-07-27 12:53:3160 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE,
61 base::Bind(&BudgetDatabase::DidGetBudget,
62 weak_ptr_factory_.GetWeakPtr(), origin,
63 callback, true /* success */));
64 return;
65 }
66
67 // Otherwise, query for the data, add it to the cache, then return the result.
68 AddToCacheCallback cache_callback =
69 base::Bind(&BudgetDatabase::DidGetBudget, weak_ptr_factory_.GetWeakPtr(),
70 origin, callback);
71 db_->GetEntry(origin.spec(), base::Bind(&BudgetDatabase::AddToCache,
72 weak_ptr_factory_.GetWeakPtr(),
73 origin, cache_callback));
74}
75
harknessf8c93432016-08-08 15:41:5976void BudgetDatabase::AddBudget(const GURL& origin,
77 double amount,
78 const StoreBudgetCallback& callback) {
79 DCHECK_EQ(origin.GetOrigin(), origin);
80
harkness5186e6d822016-08-09 16:54:1481 // Add a new chunk of budget for the origin at the default expiration time.
harknessf8c93432016-08-08 15:41:5982 base::Time expiration =
83 clock_->Now() + base::TimeDelta::FromHours(kBudgetDurationInHours);
harknesse0e26092016-08-24 06:29:3284 budget_map_[origin.spec()].chunks.emplace_back(amount, expiration);
harknessf8c93432016-08-08 15:41:5985
86 // Now that the cache is updated, write the data to the database.
87 WriteCachedValuesToDatabase(origin, callback);
88}
89
harknesse0e26092016-08-24 06:29:3290void BudgetDatabase::AddEngagementBudget(const GURL& origin,
91 double score,
92 const StoreBudgetCallback& callback) {
93 DCHECK_EQ(origin.GetOrigin(), origin);
94
95 // By default we award the "full" award. Then that ratio is decreased if
96 // there have been other awards recently.
97 double ratio = 1.0;
98
99 // Calculate how much budget should be awarded. If the origin is not cached,
100 // then we award a full amount.
101 if (IsCached(origin)) {
102 base::TimeDelta elapsed =
103 clock_->Now() - budget_map_[origin.spec()].last_engagement_award;
104 int elapsed_hours = elapsed.InHours();
105 if (elapsed_hours == 0) {
106 // Don't give engagement awards for periods less than an hour.
107 callback.Run(true);
108 return;
109 }
110 if (elapsed_hours < kBudgetDurationInHours)
111 ratio = elapsed_hours / kBudgetDurationInHours;
112 }
113
114 // Update the last_engagement_award to the current time. If the origin wasn't
115 // already in the map, this adds a new entry for it.
116 budget_map_[origin.spec()].last_engagement_award = clock_->Now();
117
118 // Pass to the base AddBudget to update the cache and write to the database.
119 AddBudget(origin, score * ratio, callback);
120}
121
122void BudgetDatabase::SpendBudget(const GURL& origin,
123 double amount,
124 const StoreBudgetCallback& callback) {
125 DCHECK_EQ(origin.GetOrigin(), origin);
126
127 // First, cleanup any expired budget chunks for the origin.
128 CleanupExpiredBudget(origin);
129
130 if (!IsCached(origin)) {
131 callback.Run(false);
132 return;
133 }
134
135 // Walk the list of budget chunks to see if the origin has enough budget.
136 double total = 0;
137 BudgetInfo& info = budget_map_[origin.spec()];
138 for (const BudgetChunk& chunk : info.chunks)
139 total += chunk.amount;
140
141 if (total < amount) {
142 callback.Run(false);
143 return;
144 }
145
146 // Walk the chunks and remove enough budget to cover the needed amount.
147 double bill = amount;
148 for (auto iter = info.chunks.begin(); iter != info.chunks.end();) {
149 if (iter->amount > bill) {
150 iter->amount -= bill;
151 bill = 0;
152 ++iter;
153 break;
154 }
155 bill -= iter->amount;
156 iter = info.chunks.erase(iter);
157 }
158
159 // There should have been enough budget to cover the entire bill.
160 DCHECK_EQ(0, bill);
161
162 // Now that the cache is updated, write the data to the database.
163 // TODO(harkness): Consider adding a second parameter to the callback so the
164 // caller can distinguish between not enough budget and a failed database
165 // write.
166 // TODO(harkness): If the database write fails, the cache will be out of sync
167 // with the database. Consider ways to mitigate this.
168 WriteCachedValuesToDatabase(origin, callback);
169}
170
harkness804b612a62016-07-27 12:53:31171void BudgetDatabase::OnDatabaseInit(bool success) {
172 // TODO(harkness): Consider caching the budget database now?
173}
174
harkness5186e6d822016-08-09 16:54:14175bool BudgetDatabase::IsCached(const GURL& origin) const {
176 return budget_map_.find(origin.spec()) != budget_map_.end();
177}
178
harkness804b612a62016-07-27 12:53:31179void BudgetDatabase::AddToCache(
180 const GURL& origin,
181 const AddToCacheCallback& callback,
182 bool success,
183 std::unique_ptr<budget_service::Budget> budget_proto) {
184 // If the database read failed, there's nothing to add to the cache.
harknessf8c93432016-08-08 15:41:59185 if (!success || !budget_proto) {
harkness804b612a62016-07-27 12:53:31186 callback.Run(success);
187 return;
188 }
189
190 // Add the data to the cache, converting from the proto format to an STL
191 // format which is better for removing things from the list.
harknesse0e26092016-08-24 06:29:32192 BudgetInfo& info = budget_map_[origin.spec()];
harknessbf8b3852016-08-10 18:42:02193 for (const auto& chunk : budget_proto->budget()) {
harknesse0e26092016-08-24 06:29:32194 info.chunks.emplace_back(chunk.amount(),
195 base::Time::FromInternalValue(chunk.expiration()));
harknessbf8b3852016-08-10 18:42:02196 }
harkness804b612a62016-07-27 12:53:31197
harknesse0e26092016-08-24 06:29:32198 info.last_engagement_award =
199 base::Time::FromInternalValue(budget_proto->engagement_last_updated());
harkness804b612a62016-07-27 12:53:31200
201 callback.Run(success);
202}
203
204void BudgetDatabase::DidGetBudget(const GURL& origin,
205 const GetBudgetDetailsCallback& callback,
206 bool success) {
207 // If the database wasn't able to read the information, return the
harknesse0e26092016-08-24 06:29:32208 // failure and an empty BudgetPrediction.
harkness804b612a62016-07-27 12:53:31209 if (!success) {
harknesse0e26092016-08-24 06:29:32210 callback.Run(success, BudgetPrediction());
harkness804b612a62016-07-27 12:53:31211 return;
212 }
213
harkness5186e6d822016-08-09 16:54:14214 // First, cleanup any expired budget chunks for the origin.
215 CleanupExpiredBudget(origin);
216
217 // Now, build up the BudgetExpection. This is different from the format
harkness804b612a62016-07-27 12:53:31218 // in which the cache stores the data. The cache stores chunks of budget and
harknesse0e26092016-08-24 06:29:32219 // when that budget expires. The BudgetPrediction describes a set of times
harkness804b612a62016-07-27 12:53:31220 // and the budget at those times.
harknesse0e26092016-08-24 06:29:32221 BudgetPrediction prediction;
harkness804b612a62016-07-27 12:53:31222 double total = 0;
harkness804b612a62016-07-27 12:53:31223
harkness5186e6d822016-08-09 16:54:14224 if (IsCached(origin)) {
225 // Starting with the chunks that expire the farthest in the future, build up
harknesse0e26092016-08-24 06:29:32226 // the budget predictions for those future times.
227 const BudgetChunks& chunks = budget_map_[origin.spec()].chunks;
harknessbf8b3852016-08-10 18:42:02228 for (const auto& chunk : base::Reversed(chunks)) {
harknesse0e26092016-08-24 06:29:32229 prediction.emplace_front(total, chunk.expiration);
harknessbf8b3852016-08-10 18:42:02230 total += chunk.amount;
harkness5186e6d822016-08-09 16:54:14231 }
harkness804b612a62016-07-27 12:53:31232 }
233
harkness5186e6d822016-08-09 16:54:14234 // Always add one entry at the front of the list for the total budget now.
harknesse0e26092016-08-24 06:29:32235 prediction.emplace_front(total, clock_->Now());
harkness804b612a62016-07-27 12:53:31236
harknesse0e26092016-08-24 06:29:32237 callback.Run(true /* success */, prediction);
harkness804b612a62016-07-27 12:53:31238}
harknessf8c93432016-08-08 15:41:59239
240void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) {
241 clock_ = std::move(clock);
242}
harkness5186e6d822016-08-09 16:54:14243
244void BudgetDatabase::WriteCachedValuesToDatabase(
245 const GURL& origin,
246 const StoreBudgetCallback& callback) {
247 // First, cleanup any expired budget chunks for the origin.
248 CleanupExpiredBudget(origin);
249
250 // Create the data structures that are passed to the ProtoDatabase.
251 std::unique_ptr<
252 leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector>
253 entries(new leveldb_proto::ProtoDatabase<
254 budget_service::Budget>::KeyEntryVector());
255 std::unique_ptr<std::vector<std::string>> keys_to_remove(
256 new std::vector<std::string>());
257
258 // Each operation can either update the existing budget or remove the origin's
259 // budget information.
260 if (IsCached(origin)) {
261 // Build the Budget proto object.
262 budget_service::Budget budget;
harknesse0e26092016-08-24 06:29:32263 const BudgetInfo& info = budget_map_[origin.spec()];
264 for (const auto& chunk : info.chunks) {
harkness5186e6d822016-08-09 16:54:14265 budget_service::BudgetChunk* budget_chunk = budget.add_budget();
harknessbf8b3852016-08-10 18:42:02266 budget_chunk->set_amount(chunk.amount);
267 budget_chunk->set_expiration(chunk.expiration.ToInternalValue());
harkness5186e6d822016-08-09 16:54:14268 }
harknesse0e26092016-08-24 06:29:32269 budget.set_engagement_last_updated(
270 info.last_engagement_award.ToInternalValue());
harkness5186e6d822016-08-09 16:54:14271 entries->push_back(std::make_pair(origin.spec(), budget));
272 } else {
273 // If the origin doesn't exist in the cache, this is a remove operation.
274 keys_to_remove->push_back(origin.spec());
275 }
276
277 // Send the updates to the database.
278 db_->UpdateEntries(std::move(entries), std::move(keys_to_remove), callback);
279}
280
281void BudgetDatabase::CleanupExpiredBudget(const GURL& origin) {
282 if (!IsCached(origin))
283 return;
284
285 base::Time now = clock_->Now();
286
harknesse0e26092016-08-24 06:29:32287 BudgetChunks& chunks = budget_map_[origin.spec()].chunks;
harkness5186e6d822016-08-09 16:54:14288 auto cleanup_iter = chunks.begin();
289
290 // This relies on the list of chunks being in timestamp order.
harknessbf8b3852016-08-10 18:42:02291 while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now)
harkness5186e6d822016-08-09 16:54:14292 cleanup_iter = chunks.erase(cleanup_iter);
293
harknesse0e26092016-08-24 06:29:32294 // If the entire budget is empty now AND there have been no engagements
295 // in the last kBudgetDurationInHours hours, remove this from the cache.
296 if (chunks.empty() &&
297 budget_map_[origin.spec()].last_engagement_award <
298 clock_->Now() - base::TimeDelta::FromHours(kBudgetDurationInHours))
harkness5186e6d822016-08-09 16:54:14299 budget_map_.erase(origin.spec());
300}