harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 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 |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | #include "chrome/browser/budget_service/budget_database.h" |
| 6 | |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 7 | #include "base/containers/adapters.h" |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 8 | #include "base/time/clock.h" |
| 9 | #include "base/time/default_clock.h" |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 10 | #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 | |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 15 | using content::BrowserThread; |
| 16 | |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 17 | namespace { |
| 18 | |
| 19 | // UMA are logged for the database with this string as part of the name. |
harkness | bea56c2 | 2016-08-16 07:23:00 | [diff] [blame] | 20 | // They will be LevelDB.*.BudgetManager. Changes here should be synchronized |
| 21 | // with histograms.xml. |
| 22 | const char kDatabaseUMAName[] = "BudgetManager"; |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 23 | |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 24 | // The default amount of time during which a budget will be valid. |
| 25 | // This is 3 days = 72 hours. |
| 26 | constexpr double kBudgetDurationInHours = 72; |
| 27 | |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 28 | } // namespace |
| 29 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 30 | BudgetDatabase::BudgetInfo::BudgetInfo() {} |
| 31 | |
| 32 | BudgetDatabase::BudgetInfo::BudgetInfo(const BudgetInfo&& other) |
| 33 | : last_engagement_award(other.last_engagement_award) { |
| 34 | chunks = std::move(other.chunks); |
| 35 | } |
| 36 | |
| 37 | BudgetDatabase::BudgetInfo::~BudgetInfo() {} |
| 38 | |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 39 | BudgetDatabase::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)), |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 44 | clock_(base::WrapUnique(new base::DefaultClock)), |
harkness | 883658b | 2016-07-18 11:37:53 | [diff] [blame] | 45 | weak_ptr_factory_(this) { |
| 46 | db_->Init(kDatabaseUMAName, database_dir, |
| 47 | base::Bind(&BudgetDatabase::OnDatabaseInit, |
| 48 | weak_ptr_factory_.GetWeakPtr())); |
| 49 | } |
| 50 | |
| 51 | BudgetDatabase::~BudgetDatabase() {} |
| 52 | |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 53 | void 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. |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 59 | if (IsCached(origin)) { |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 60 | 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 | |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 76 | void BudgetDatabase::AddBudget(const GURL& origin, |
| 77 | double amount, |
| 78 | const StoreBudgetCallback& callback) { |
| 79 | DCHECK_EQ(origin.GetOrigin(), origin); |
| 80 | |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 81 | // Add a new chunk of budget for the origin at the default expiration time. |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 82 | base::Time expiration = |
| 83 | clock_->Now() + base::TimeDelta::FromHours(kBudgetDurationInHours); |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 84 | budget_map_[origin.spec()].chunks.emplace_back(amount, expiration); |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 85 | |
| 86 | // Now that the cache is updated, write the data to the database. |
| 87 | WriteCachedValuesToDatabase(origin, callback); |
| 88 | } |
| 89 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 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 | } |
| 121 | |
| 122 | void 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 | |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 171 | void BudgetDatabase::OnDatabaseInit(bool success) { |
| 172 | // TODO(harkness): Consider caching the budget database now? |
| 173 | } |
| 174 | |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 175 | bool BudgetDatabase::IsCached(const GURL& origin) const { |
| 176 | return budget_map_.find(origin.spec()) != budget_map_.end(); |
| 177 | } |
| 178 | |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 179 | void 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. |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 185 | if (!success || !budget_proto) { |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 186 | 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. |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 192 | BudgetInfo& info = budget_map_[origin.spec()]; |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 193 | for (const auto& chunk : budget_proto->budget()) { |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 194 | info.chunks.emplace_back(chunk.amount(), |
| 195 | base::Time::FromInternalValue(chunk.expiration())); |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 196 | } |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 197 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 198 | info.last_engagement_award = |
| 199 | base::Time::FromInternalValue(budget_proto->engagement_last_updated()); |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 200 | |
| 201 | callback.Run(success); |
| 202 | } |
| 203 | |
| 204 | void 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 |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 208 | // failure and an empty BudgetPrediction. |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 209 | if (!success) { |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 210 | callback.Run(success, BudgetPrediction()); |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 211 | return; |
| 212 | } |
| 213 | |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 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 |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 218 | // in which the cache stores the data. The cache stores chunks of budget and |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 219 | // when that budget expires. The BudgetPrediction describes a set of times |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 220 | // and the budget at those times. |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 221 | BudgetPrediction prediction; |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 222 | double total = 0; |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 223 | |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 224 | if (IsCached(origin)) { |
| 225 | // Starting with the chunks that expire the farthest in the future, build up |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 226 | // the budget predictions for those future times. |
| 227 | const BudgetChunks& chunks = budget_map_[origin.spec()].chunks; |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 228 | for (const auto& chunk : base::Reversed(chunks)) { |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 229 | prediction.emplace_front(total, chunk.expiration); |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 230 | total += chunk.amount; |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 231 | } |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 232 | } |
| 233 | |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 234 | // Always add one entry at the front of the list for the total budget now. |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 235 | prediction.emplace_front(total, clock_->Now()); |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 236 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 237 | callback.Run(true /* success */, prediction); |
harkness | 804b612a6 | 2016-07-27 12:53:31 | [diff] [blame] | 238 | } |
harkness | f8c9343 | 2016-08-08 15:41:59 | [diff] [blame] | 239 | |
| 240 | void BudgetDatabase::SetClockForTesting(std::unique_ptr<base::Clock> clock) { |
| 241 | clock_ = std::move(clock); |
| 242 | } |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 243 | |
| 244 | void 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; |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 263 | const BudgetInfo& info = budget_map_[origin.spec()]; |
| 264 | for (const auto& chunk : info.chunks) { |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 265 | budget_service::BudgetChunk* budget_chunk = budget.add_budget(); |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 266 | budget_chunk->set_amount(chunk.amount); |
| 267 | budget_chunk->set_expiration(chunk.expiration.ToInternalValue()); |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 268 | } |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 269 | budget.set_engagement_last_updated( |
| 270 | info.last_engagement_award.ToInternalValue()); |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 271 | 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 | |
| 281 | void BudgetDatabase::CleanupExpiredBudget(const GURL& origin) { |
| 282 | if (!IsCached(origin)) |
| 283 | return; |
| 284 | |
| 285 | base::Time now = clock_->Now(); |
| 286 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 287 | BudgetChunks& chunks = budget_map_[origin.spec()].chunks; |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 288 | auto cleanup_iter = chunks.begin(); |
| 289 | |
| 290 | // This relies on the list of chunks being in timestamp order. |
harkness | bf8b385 | 2016-08-10 18:42:02 | [diff] [blame] | 291 | while (cleanup_iter != chunks.end() && cleanup_iter->expiration <= now) |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 292 | cleanup_iter = chunks.erase(cleanup_iter); |
| 293 | |
harkness | e0e2609 | 2016-08-24 06:29:32 | [diff] [blame] | 294 | // 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)) |
harkness | 5186e6d82 | 2016-08-09 16:54:14 | [diff] [blame] | 299 | budget_map_.erase(origin.spec()); |
| 300 | } |