Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(336)

Side by Side Diff: chrome/browser/budget_service/budget_database.cc

Issue 2272563005: Start plumbing connections from the BudgetManager to the BudgetDatabase (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Rebase and remove BudgetManager::CostType references Created 4 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2016 The Chromium Authors. All rights reserved. 1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 #include "chrome/browser/budget_service/budget_database.h" 5 #include "chrome/browser/budget_service/budget_database.h"
6 6
7 #include "base/containers/adapters.h" 7 #include "base/containers/adapters.h"
8 #include "base/time/clock.h" 8 #include "base/time/clock.h"
9 #include "base/time/default_clock.h" 9 #include "base/time/default_clock.h"
10 #include "chrome/browser/budget_service/budget.pb.h" 10 #include "chrome/browser/budget_service/budget.pb.h"
11 #include "chrome/browser/engagement/site_engagement_score.h"
12 #include "chrome/browser/engagement/site_engagement_service.h"
13 #include "chrome/browser/profiles/profile.h"
11 #include "components/leveldb_proto/proto_database_impl.h" 14 #include "components/leveldb_proto/proto_database_impl.h"
12 #include "content/public/browser/browser_thread.h" 15 #include "content/public/browser/browser_thread.h"
13 #include "url/gurl.h" 16 #include "url/gurl.h"
14 17
15 using content::BrowserThread; 18 using content::BrowserThread;
16 19
17 namespace { 20 namespace {
18 21
19 // UMA are logged for the database with this string as part of the name. 22 // UMA are logged for the database with this string as part of the name.
20 // They will be LevelDB.*.BudgetManager. Changes here should be synchronized 23 // They will be LevelDB.*.BudgetManager. Changes here should be synchronized
21 // with histograms.xml. 24 // with histograms.xml.
22 const char kDatabaseUMAName[] = "BudgetManager"; 25 const char kDatabaseUMAName[] = "BudgetManager";
23 26
24 // The default amount of time during which a budget will be valid. 27 // The default amount of time during which a budget will be valid.
25 // This is 3 days = 72 hours. 28 // This is 3 days = 72 hours.
26 constexpr double kBudgetDurationInHours = 72; 29 constexpr double kBudgetDurationInHours = 72;
27 30
28 } // namespace 31 } // namespace
29 32
30 BudgetDatabase::BudgetInfo::BudgetInfo() {} 33 BudgetDatabase::BudgetInfo::BudgetInfo() {}
31 34
32 BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other) 35 BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other)
33 : last_engagement_award(other.last_engagement_award) { 36 : last_engagement_award(other.last_engagement_award) {
34 chunks = std::move(other.chunks); 37 chunks = std::move(other.chunks);
35 } 38 }
36 39
37 BudgetDatabase::BudgetInfo::~BudgetInfo() {} 40 BudgetDatabase::BudgetInfo::~BudgetInfo() {}
38 41
39 BudgetDatabase::BudgetDatabase( 42 BudgetDatabase::BudgetDatabase(
43 Profile* profile,
40 const base::FilePath& database_dir, 44 const base::FilePath& database_dir,
41 const scoped_refptr<base::SequencedTaskRunner>& task_runner) 45 const scoped_refptr<base::SequencedTaskRunner>& task_runner)
42 : db_(new leveldb_proto::ProtoDatabaseImpl<budget_service::Budget>( 46 : profile_(profile),
47 db_(new leveldb_proto::ProtoDatabaseImpl<budget_service::Budget>(
43 task_runner)), 48 task_runner)),
44 clock_(base::WrapUnique(new base::DefaultClock)), 49 clock_(base::WrapUnique(new base::DefaultClock)),
45 weak_ptr_factory_(this) { 50 weak_ptr_factory_(this) {
46 db_->Init(kDatabaseUMAName, database_dir, 51 db_->Init(kDatabaseUMAName, database_dir,
47 base::Bind(&BudgetDatabase::OnDatabaseInit, 52 base::Bind(&BudgetDatabase::OnDatabaseInit,
48 weak_ptr_factory_.GetWeakPtr())); 53 weak_ptr_factory_.GetWeakPtr()));
49 } 54 }
50 55
51 BudgetDatabase::~BudgetDatabase() {} 56 BudgetDatabase::~BudgetDatabase() {}
52 57
53 void BudgetDatabase::GetBudgetDetails( 58 void BudgetDatabase::GetBudgetDetails(
54 const GURL& origin, 59 const GURL& origin,
55 const GetBudgetDetailsCallback& callback) { 60 const GetBudgetDetailsCallback& callback) {
56 DCHECK_EQ(origin.GetOrigin(), origin); 61 DCHECK_EQ(origin.GetOrigin(), origin);
57 62
58 // If this origin is already in the cache, immediately return the data. 63 // First, synchronize the cache.
Peter Beverloo 2016/08/25 15:31:19 nit: drop. This is implied by the fact that you ca
harkness 2016/08/25 16:36:13 Done.
59 if (IsCached(origin)) { 64 SyncCacheCallback cache_callback =
60 BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, 65 base::Bind(&BudgetDatabase::GetBudgetAfterSync,
61 base::Bind(&BudgetDatabase::DidGetBudget, 66 weak_ptr_factory_.GetWeakPtr(), origin, callback);
62 weak_ptr_factory_.GetWeakPtr(), origin, 67 SyncCache(origin, cache_callback);
Peter Beverloo 2016/08/25 15:31:19 nit: just inline the callback, no need to store it
harkness 2016/08/25 16:36:13 Done.
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
76 void BudgetDatabase::AddBudget(const GURL& origin,
77 double amount,
78 const StoreBudgetCallback& callback) {
79 DCHECK_EQ(origin.GetOrigin(), origin);
80
81 // Add a new chunk of budget for the origin at the default expiration time.
82 base::Time expiration =
83 clock_->Now() + base::TimeDelta::FromHours(kBudgetDurationInHours);
84 budget_map_[origin.spec()].chunks.emplace_back(amount, expiration);
85
86 // Now that the cache is updated, write the data to the database.
87 WriteCachedValuesToDatabase(origin, callback);
88 }
89
90 void 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 } 68 }
121 69
122 void BudgetDatabase::SpendBudget(const GURL& origin, 70 void BudgetDatabase::SpendBudget(const GURL& origin,
123 double amount, 71 double amount,
124 const StoreBudgetCallback& callback) { 72 const StoreBudgetCallback& callback) {
125 DCHECK_EQ(origin.GetOrigin(), origin); 73 // First, synchronize the cache.
74 SyncCacheCallback cache_callback =
75 base::Bind(&BudgetDatabase::SpendBudgetAfterSync,
76 weak_ptr_factory_.GetWeakPtr(), origin, amount, callback);
77 SyncCache(origin, cache_callback);
78 }
126 79
127 // First, cleanup any expired budget chunks for the origin. 80 void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) {
128 CleanupExpiredBudget(origin); 81 clock_ = std::move(clock);
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 } 82 }
170 83
171 void BudgetDatabase::OnDatabaseInit(bool success) { 84 void BudgetDatabase::OnDatabaseInit(bool success) {
172 // TODO(harkness): Consider caching the budget database now? 85 // TODO(harkness): Consider caching the budget database now?
173 } 86 }
174 87
175 bool BudgetDatabase::IsCached(const GURL& origin) const { 88 bool BudgetDatabase::IsCached(const GURL& origin) const {
176 return budget_map_.find(origin.spec()) != budget_map_.end(); 89 return budget_map_.find(origin.spec()) != budget_map_.end();
177 } 90 }
178 91
179 void BudgetDatabase::AddToCache( 92 void BudgetDatabase::AddToCache(
180 const GURL& origin, 93 const GURL& origin,
181 const AddToCacheCallback& callback, 94 const AddToCacheCallback& callback,
182 bool success, 95 bool success,
183 std::unique_ptr<budget_service::Budget> budget_proto) { 96 std::unique_ptr<budget_service::Budget> budget_proto) {
184 // If the database read failed, there's nothing to add to the cache. 97 // If the database read failed, there's nothing to add to the cache.
185 if (!success || !budget_proto) { 98 if (!success || !budget_proto) {
186 callback.Run(success); 99 callback.Run(success);
187 return; 100 return;
188 } 101 }
189 102
103 // If there were two simultaneous loads, don't overwrite the cache value,
104 // which might have been updated after the previous load.
105 if (IsCached(origin)) {
106 callback.Run(success);
107 return;
108 }
109
190 // Add the data to the cache, converting from the proto format to an STL 110 // 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. 111 // format which is better for removing things from the list.
192 BudgetInfo& info = budget_map_[origin.spec()]; 112 BudgetInfo& info = budget_map_[origin.spec()];
193 for (const auto& chunk : budget_proto->budget()) { 113 for (const auto& chunk : budget_proto->budget()) {
194 info.chunks.emplace_back(chunk.amount(), 114 info.chunks.emplace_back(chunk.amount(),
195 base::Time::FromInternalValue(chunk.expiration())); 115 base::Time::FromInternalValue(chunk.expiration()));
196 } 116 }
197 117
198 info.last_engagement_award = 118 info.last_engagement_award =
199 base::Time::FromInternalValue(budget_proto->engagement_last_updated()); 119 base::Time::FromInternalValue(budget_proto->engagement_last_updated());
200 120
201 callback.Run(success); 121 callback.Run(success);
202 } 122 }
203 123
204 void BudgetDatabase::DidGetBudget(const GURL& origin, 124 void BudgetDatabase::GetBudgetAfterSync(
205 const GetBudgetDetailsCallback& callback, 125 const GURL& origin,
206 bool success) { 126 const GetBudgetDetailsCallback& callback,
127 bool success) {
Peter Beverloo 2016/08/25 15:31:19 nit: can this be `const` now?
harkness 2016/08/25 16:36:13 It can't because we use the [] operator on the map
207 // If the database wasn't able to read the information, return the 128 // If the database wasn't able to read the information, return the
208 // failure and an empty BudgetPrediction. 129 // failure and an empty BudgetPrediction.
209 if (!success) { 130 if (!success) {
210 callback.Run(success, BudgetPrediction()); 131 callback.Run(success, BudgetPrediction());
211 return; 132 return;
212 } 133 }
213 134
214 // 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 135 // Now, build up the BudgetExpection. This is different from the format
218 // in which the cache stores the data. The cache stores chunks of budget and 136 // in which the cache stores the data. The cache stores chunks of budget and
219 // when that budget expires. The BudgetPrediction describes a set of times 137 // when that budget expires. The BudgetPrediction describes a set of times
220 // and the budget at those times. 138 // and the budget at those times.
221 BudgetPrediction prediction; 139 BudgetPrediction prediction;
222 double total = 0; 140 double total = 0;
223 141
224 if (IsCached(origin)) { 142 // Starting with the chunks that expire the farthest in the future, build up
225 // Starting with the chunks that expire the farthest in the future, build up 143 // the budget predictions for those future times.
226 // the budget predictions for those future times. 144 const BudgetChunks& chunks = budget_map_[origin.spec()].chunks;
227 const BudgetChunks& chunks = budget_map_[origin.spec()].chunks; 145 for (const auto& chunk : base::Reversed(chunks)) {
228 for (const auto& chunk : base::Reversed(chunks)) { 146 prediction.emplace_front(total, chunk.expiration);
229 prediction.emplace_front(total, chunk.expiration); 147 total += chunk.amount;
230 total += chunk.amount;
231 }
232 } 148 }
233 149
234 // Always add one entry at the front of the list for the total budget now. 150 // Always add one entry at the front of the list for the total budget now.
235 prediction.emplace_front(total, clock_->Now()); 151 prediction.emplace_front(total, clock_->Now());
236 152
237 callback.Run(true /* success */, prediction); 153 callback.Run(true /* success */, prediction);
238 } 154 }
239 155
240 void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) { 156 void BudgetDatabase::SpendBudgetAfterSync(const GURL& origin,
241 clock_ = std::move(clock); 157 double amount,
158 const StoreBudgetCallback& callback,
159 bool success) {
160 if (!success) {
161 callback.Run(false);
Peter Beverloo 2016/08/25 15:31:19 nit (here & lines 172, 256): false -> false /* suc
harkness 2016/08/25 16:36:13 Done.
162 return;
163 }
164
165 // Walk the list of budget chunks to see if the origin has enough budget.
166 double total = 0;
167 BudgetInfo& info = budget_map_[origin.spec()];
168 for (const BudgetChunk& chunk : info.chunks)
169 total += chunk.amount;
170
171 if (total < amount) {
172 callback.Run(false);
173 return;
174 }
175
176 // Walk the chunks and remove enough budget to cover the needed amount.
177 double bill = amount;
178 for (auto iter = info.chunks.begin(); iter != info.chunks.end();) {
179 if (iter->amount > bill) {
180 iter->amount -= bill;
181 bill = 0;
182 ++iter;
Peter Beverloo 2016/08/25 15:31:19 nit: delete, this doesn't do anything since you `b
harkness 2016/08/25 16:36:13 Done.
183 break;
184 }
185 bill -= iter->amount;
186 iter = info.chunks.erase(iter);
187 }
188
189 // There should have been enough budget to cover the entire bill.
190 DCHECK_EQ(0, bill);
191
192 // Now that the cache is updated, write the data to the database.
193 // TODO(harkness): Consider adding a second parameter to the callback so the
194 // caller can distinguish between not enough budget and a failed database
195 // write.
196 // TODO(harkness): If the database write fails, the cache will be out of sync
197 // with the database. Consider ways to mitigate this.
198 WriteCachedValuesToDatabase(origin, callback);
242 } 199 }
243 200
244 void BudgetDatabase::WriteCachedValuesToDatabase( 201 void BudgetDatabase::WriteCachedValuesToDatabase(
245 const GURL& origin, 202 const GURL& origin,
246 const StoreBudgetCallback& callback) { 203 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. 204 // Create the data structures that are passed to the ProtoDatabase.
251 std::unique_ptr< 205 std::unique_ptr<
252 leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector> 206 leveldb_proto::ProtoDatabase<budget_service::Budget>::KeyEntryVector>
253 entries(new leveldb_proto::ProtoDatabase< 207 entries(new leveldb_proto::ProtoDatabase<
254 budget_service::Budget>::KeyEntryVector()); 208 budget_service::Budget>::KeyEntryVector());
255 std::unique_ptr<std::vector<std::string>> keys_to_remove( 209 std::unique_ptr<std::vector<std::string>> keys_to_remove(
256 new std::vector<std::string>()); 210 new std::vector<std::string>());
257 211
258 // Each operation can either update the existing budget or remove the origin's 212 // Each operation can either update the existing budget or remove the origin's
259 // budget information. 213 // budget information.
(...skipping 11 matching lines...) Expand all
271 entries->push_back(std::make_pair(origin.spec(), budget)); 225 entries->push_back(std::make_pair(origin.spec(), budget));
272 } else { 226 } else {
273 // If the origin doesn't exist in the cache, this is a remove operation. 227 // If the origin doesn't exist in the cache, this is a remove operation.
274 keys_to_remove->push_back(origin.spec()); 228 keys_to_remove->push_back(origin.spec());
275 } 229 }
276 230
277 // Send the updates to the database. 231 // Send the updates to the database.
278 db_->UpdateEntries(std::move(entries), std::move(keys_to_remove), callback); 232 db_->UpdateEntries(std::move(entries), std::move(keys_to_remove), callback);
279 } 233 }
280 234
281 void BudgetDatabase::CleanupExpiredBudget(const GURL& origin) { 235 void BudgetDatabase::SyncCache(const GURL& origin,
282 if (!IsCached(origin)) 236 const SyncCacheCallback& callback) {
237 DCHECK_EQ(origin, origin.GetOrigin());
238
239 // If the origin isn't already cached, add it to the cache.
240 if (!IsCached(origin)) {
241 AddToCacheCallback add_callback =
242 base::Bind(&BudgetDatabase::SyncLoadedCache,
243 weak_ptr_factory_.GetWeakPtr(), origin, callback);
244 db_->GetEntry(origin.spec(), base::Bind(&BudgetDatabase::AddToCache,
245 weak_ptr_factory_.GetWeakPtr(),
246 origin, add_callback));
247 return;
248 }
249 SyncLoadedCache(origin, callback, true /* success */);
250 }
251
252 void BudgetDatabase::SyncLoadedCache(const GURL& origin,
253 const SyncCacheCallback& callback,
254 bool success) {
255 if (!success) {
256 callback.Run(false);
257 return;
258 }
259
260 // Get the SES score and add engagement budget for the site.
261 AddEngagementBudget(origin);
262
263 // Now, cleanup any expired budget chunks for the origin.
264 bool needs_write = CleanupExpiredBudget(origin);
265
266 if (needs_write)
267 WriteCachedValuesToDatabase(origin, callback);
268 else
269 callback.Run(success);
270 }
271
272 void BudgetDatabase::AddEngagementBudget(const GURL& origin) {
273 // Get the current SES score, which we'll use to set a new budget.
274 SiteEngagementService* service = SiteEngagementService::Get(profile_);
275 double score = service->GetScore(origin);
276
277 // Don't force new sites to start with a bad chunk.
278 if (score == 0)
283 return; 279 return;
284 280
281 // By default we award the "full" award. Then that ratio is decreased if
282 // there have been other awards recently.
283 double ratio = 1.0;
284
285 // Calculate how much budget should be awarded. If there is no entry in the
286 // cache then we award a full amount.
287 if (IsCached(origin)) {
288 base::TimeDelta elapsed =
289 clock_->Now() - budget_map_[origin.spec()].last_engagement_award;
290 int elapsed_hours = elapsed.InHours();
291 // Don't give engagement awards for periods less than an hour.
292 if (elapsed_hours < 1)
293 return;
294 if (elapsed_hours < kBudgetDurationInHours)
295 ratio = elapsed_hours / kBudgetDurationInHours;
296 }
297
298 // Update the last_engagement_award to the current time. If the origin wasn't
299 // already in the map, this adds a new entry for it.
300 budget_map_[origin.spec()].last_engagement_award = clock_->Now();
301
302 // Add a new chunk of budget for the origin at the default expiration time.
303 base::Time expiration =
304 clock_->Now() + base::TimeDelta::FromHours(kBudgetDurationInHours);
305 budget_map_[origin.spec()].chunks.emplace_back(ratio * score, expiration);
306 }
307
308 // Cleans up budget in the cache. Relies on the caller eventually writing the
309 // cache back to the database.
310 bool BudgetDatabase::CleanupExpiredBudget(const GURL& origin) {
311 if (!IsCached(origin))
312 return false;
313
285 base::Time now = clock_->Now(); 314 base::Time now = clock_->Now();
286
287 BudgetChunks& chunks = budget_map_[origin.spec()].chunks; 315 BudgetChunks& chunks = budget_map_[origin.spec()].chunks;
288 auto cleanup_iter = chunks.begin(); 316 auto cleanup_iter = chunks.begin();
289 317
290 // This relies on the list of chunks being in timestamp order. 318 // This relies on the list of chunks being in timestamp order.
291 while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now) 319 while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now)
292 cleanup_iter = chunks.erase(cleanup_iter); 320 cleanup_iter = chunks.erase(cleanup_iter);
293 321
294 // If the entire budget is empty now AND there have been no engagements 322 // If the entire budget is empty now AND there have been no engagements
295 // in the last kBudgetDurationInHours hours, remove this from the cache. 323 // in the last kBudgetDurationInHours hours, remove this from the cache.
296 if (chunks.empty() && 324 if (chunks.empty() &&
297 budget_map_[origin.spec()].last_engagement_award < 325 budget_map_[origin.spec()].last_engagement_award <
298 clock_->Now() - base::TimeDelta::FromHours(kBudgetDurationInHours)) 326 clock_->Now() - base::TimeDelta::FromHours(kBudgetDurationInHours)) {
299 budget_map_.erase(origin.spec()); 327 budget_map_.erase(origin.spec());
328 return true;
329 }
330
331 // Although some things may have expired, there are some chunks still valid.
332 // Don't write to the DB now, write either when all chunks expire or when the
333 // origin spends some budget.
334 return false;
300 } 335 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698