Re-subimission of https://codereview.chromium.org/1041213003/
[chromium-blink-merge.git] / content / browser / dom_storage / dom_storage_area.cc
blob9ff7bfb37c4c395dcd1674223177e9acd9c09d80
1 // Copyright 2013 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.
5 #include "content/browser/dom_storage/dom_storage_area.h"
7 #include <algorithm>
9 #include "base/bind.h"
10 #include "base/location.h"
11 #include "base/logging.h"
12 #include "base/metrics/histogram.h"
13 #include "base/process/process_info.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/time/time.h"
16 #include "content/browser/dom_storage/dom_storage_namespace.h"
17 #include "content/browser/dom_storage/dom_storage_task_runner.h"
18 #include "content/browser/dom_storage/local_storage_database_adapter.h"
19 #include "content/browser/dom_storage/session_storage_database.h"
20 #include "content/browser/dom_storage/session_storage_database_adapter.h"
21 #include "content/common/dom_storage/dom_storage_map.h"
22 #include "content/common/dom_storage/dom_storage_types.h"
23 #include "storage/browser/database/database_util.h"
24 #include "storage/common/database/database_identifier.h"
25 #include "storage/common/fileapi/file_system_util.h"
27 using storage::DatabaseUtil;
29 namespace content {
31 namespace {
33 // Delay for a moment after a value is set in anticipation
34 // of other values being set, so changes are batched.
35 const int kCommitDefaultDelaySecs = 5;
37 // To avoid excessive IO we apply limits to the amount of data being written
38 // and the frequency of writes. The specific values used are somewhat arbitrary.
39 const int kMaxBytesPerDay = kPerStorageAreaQuota * 2;
40 const int kMaxCommitsPerHour = 6;
42 } // namespace
44 DOMStorageArea::RateLimiter::RateLimiter(size_t desired_rate,
45 base::TimeDelta time_quantum)
46 : rate_(desired_rate), samples_(0), time_quantum_(time_quantum) {
47 DCHECK_GT(desired_rate, 0ul);
50 base::TimeDelta DOMStorageArea::RateLimiter::ComputeTimeNeeded() const {
51 return time_quantum_ * (samples_ / rate_);
54 base::TimeDelta DOMStorageArea::RateLimiter::ComputeDelayNeeded(
55 const base::TimeDelta elapsed_time) const {
56 base::TimeDelta time_needed = ComputeTimeNeeded();
57 if (time_needed > elapsed_time)
58 return time_needed - elapsed_time;
59 return base::TimeDelta();
62 DOMStorageArea::CommitBatch::CommitBatch() : clear_all_first(false) {
64 DOMStorageArea::CommitBatch::~CommitBatch() {}
66 size_t DOMStorageArea::CommitBatch::GetDataSize() const {
67 return DOMStorageMap::CountBytes(changed_values);
70 // static
71 const base::FilePath::CharType DOMStorageArea::kDatabaseFileExtension[] =
72 FILE_PATH_LITERAL(".localstorage");
74 // static
75 base::FilePath DOMStorageArea::DatabaseFileNameFromOrigin(const GURL& origin) {
76 std::string filename = storage::GetIdentifierFromOrigin(origin);
77 // There is no base::FilePath.AppendExtension() method, so start with just the
78 // extension as the filename, and then InsertBeforeExtension the desired
79 // name.
80 return base::FilePath().Append(kDatabaseFileExtension).
81 InsertBeforeExtensionASCII(filename);
84 // static
85 GURL DOMStorageArea::OriginFromDatabaseFileName(const base::FilePath& name) {
86 DCHECK(name.MatchesExtension(kDatabaseFileExtension));
87 std::string origin_id =
88 name.BaseName().RemoveExtension().MaybeAsASCII();
89 return storage::GetOriginFromIdentifier(origin_id);
92 DOMStorageArea::DOMStorageArea(const GURL& origin,
93 const base::FilePath& directory,
94 DOMStorageTaskRunner* task_runner)
95 : namespace_id_(kLocalStorageNamespaceId),
96 origin_(origin),
97 directory_(directory),
98 task_runner_(task_runner),
99 map_(new DOMStorageMap(kPerStorageAreaQuota +
100 kPerStorageAreaOverQuotaAllowance)),
101 is_initial_import_done_(true),
102 is_shutdown_(false),
103 commit_batches_in_flight_(0),
104 start_time_(base::TimeTicks::Now()),
105 data_rate_limiter_(kMaxBytesPerDay, base::TimeDelta::FromHours(24)),
106 commit_rate_limiter_(kMaxCommitsPerHour, base::TimeDelta::FromHours(1)) {
107 if (!directory.empty()) {
108 base::FilePath path = directory.Append(DatabaseFileNameFromOrigin(origin_));
109 backing_.reset(new LocalStorageDatabaseAdapter(path));
110 is_initial_import_done_ = false;
114 DOMStorageArea::DOMStorageArea(int64 namespace_id,
115 const std::string& persistent_namespace_id,
116 const GURL& origin,
117 SessionStorageDatabase* session_storage_backing,
118 DOMStorageTaskRunner* task_runner)
119 : namespace_id_(namespace_id),
120 persistent_namespace_id_(persistent_namespace_id),
121 origin_(origin),
122 task_runner_(task_runner),
123 map_(new DOMStorageMap(kPerStorageAreaQuota +
124 kPerStorageAreaOverQuotaAllowance)),
125 session_storage_backing_(session_storage_backing),
126 is_initial_import_done_(true),
127 is_shutdown_(false),
128 commit_batches_in_flight_(0),
129 start_time_(base::TimeTicks::Now()),
130 data_rate_limiter_(kMaxBytesPerDay, base::TimeDelta::FromHours(24)),
131 commit_rate_limiter_(kMaxCommitsPerHour, base::TimeDelta::FromHours(1)) {
132 DCHECK(namespace_id != kLocalStorageNamespaceId);
133 if (session_storage_backing) {
134 backing_.reset(new SessionStorageDatabaseAdapter(
135 session_storage_backing, persistent_namespace_id, origin));
136 is_initial_import_done_ = false;
140 DOMStorageArea::~DOMStorageArea() {
143 void DOMStorageArea::ExtractValues(DOMStorageValuesMap* map) {
144 if (is_shutdown_)
145 return;
146 InitialImportIfNeeded();
147 map_->ExtractValues(map);
150 unsigned DOMStorageArea::Length() {
151 if (is_shutdown_)
152 return 0;
153 InitialImportIfNeeded();
154 return map_->Length();
157 base::NullableString16 DOMStorageArea::Key(unsigned index) {
158 if (is_shutdown_)
159 return base::NullableString16();
160 InitialImportIfNeeded();
161 return map_->Key(index);
164 base::NullableString16 DOMStorageArea::GetItem(const base::string16& key) {
165 if (is_shutdown_)
166 return base::NullableString16();
167 InitialImportIfNeeded();
168 return map_->GetItem(key);
171 bool DOMStorageArea::SetItem(const base::string16& key,
172 const base::string16& value,
173 base::NullableString16* old_value) {
174 if (is_shutdown_)
175 return false;
176 InitialImportIfNeeded();
177 if (!map_->HasOneRef())
178 map_ = map_->DeepCopy();
179 bool success = map_->SetItem(key, value, old_value);
180 if (success && backing_ &&
181 (old_value->is_null() || old_value->string() != value)) {
182 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
183 commit_batch->changed_values[key] = base::NullableString16(value, false);
185 return success;
188 bool DOMStorageArea::RemoveItem(const base::string16& key,
189 base::string16* old_value) {
190 if (is_shutdown_)
191 return false;
192 InitialImportIfNeeded();
193 if (!map_->HasOneRef())
194 map_ = map_->DeepCopy();
195 bool success = map_->RemoveItem(key, old_value);
196 if (success && backing_) {
197 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
198 commit_batch->changed_values[key] = base::NullableString16();
200 return success;
203 bool DOMStorageArea::Clear() {
204 if (is_shutdown_)
205 return false;
206 InitialImportIfNeeded();
207 if (map_->Length() == 0)
208 return false;
210 map_ = new DOMStorageMap(kPerStorageAreaQuota +
211 kPerStorageAreaOverQuotaAllowance);
213 if (backing_) {
214 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
215 commit_batch->clear_all_first = true;
216 commit_batch->changed_values.clear();
219 return true;
222 void DOMStorageArea::FastClear() {
223 // TODO(marja): Unify clearing localStorage and sessionStorage. The problem is
224 // to make the following 3 to work together: 1) FastClear, 2) PurgeMemory and
225 // 3) not creating events when clearing an empty area.
226 if (is_shutdown_)
227 return;
229 map_ = new DOMStorageMap(kPerStorageAreaQuota +
230 kPerStorageAreaOverQuotaAllowance);
231 // This ensures no import will happen while we're waiting to clear the data
232 // from the database. This mechanism fails if PurgeMemory is called.
233 is_initial_import_done_ = true;
235 if (backing_) {
236 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
237 commit_batch->clear_all_first = true;
238 commit_batch->changed_values.clear();
242 DOMStorageArea* DOMStorageArea::ShallowCopy(
243 int64 destination_namespace_id,
244 const std::string& destination_persistent_namespace_id) {
245 DCHECK_NE(kLocalStorageNamespaceId, namespace_id_);
246 DCHECK_NE(kLocalStorageNamespaceId, destination_namespace_id);
248 DOMStorageArea* copy = new DOMStorageArea(
249 destination_namespace_id, destination_persistent_namespace_id, origin_,
250 session_storage_backing_.get(), task_runner_.get());
251 copy->map_ = map_;
252 copy->is_shutdown_ = is_shutdown_;
253 copy->is_initial_import_done_ = true;
255 // All the uncommitted changes to this area need to happen before the actual
256 // shallow copy is made (scheduled by the upper layer sometime after return).
257 if (commit_batch_)
258 ScheduleImmediateCommit();
259 return copy;
262 bool DOMStorageArea::HasUncommittedChanges() const {
263 return commit_batch_.get() || commit_batches_in_flight_;
266 void DOMStorageArea::ScheduleImmediateCommit() {
267 DCHECK(HasUncommittedChanges());
268 PostCommitTask();
271 void DOMStorageArea::DeleteOrigin() {
272 DCHECK(!is_shutdown_);
273 // This function shouldn't be called for sessionStorage.
274 DCHECK(!session_storage_backing_.get());
275 if (HasUncommittedChanges()) {
276 // TODO(michaeln): This logically deletes the data immediately,
277 // and in a matter of a second, deletes the rows from the backing
278 // database file, but the file itself will linger until shutdown
279 // or purge time. Ideally, this should delete the file more
280 // quickly.
281 Clear();
282 return;
284 map_ = new DOMStorageMap(kPerStorageAreaQuota +
285 kPerStorageAreaOverQuotaAllowance);
286 if (backing_) {
287 is_initial_import_done_ = false;
288 backing_->Reset();
289 backing_->DeleteFiles();
293 void DOMStorageArea::PurgeMemory() {
294 DCHECK(!is_shutdown_);
295 // Purging sessionStorage is not supported; it won't work with FastClear.
296 DCHECK(!session_storage_backing_.get());
297 if (!is_initial_import_done_ || // We're not using any memory.
298 !backing_.get() || // We can't purge anything.
299 HasUncommittedChanges()) // We leave things alone with changes pending.
300 return;
302 // Drop the in memory cache, we'll reload when needed.
303 is_initial_import_done_ = false;
304 map_ = new DOMStorageMap(kPerStorageAreaQuota +
305 kPerStorageAreaOverQuotaAllowance);
307 // Recreate the database object, this frees up the open sqlite connection
308 // and its page cache.
309 backing_->Reset();
312 void DOMStorageArea::Shutdown() {
313 DCHECK(!is_shutdown_);
314 is_shutdown_ = true;
315 map_ = NULL;
316 if (!backing_)
317 return;
319 bool success = task_runner_->PostShutdownBlockingTask(
320 FROM_HERE,
321 DOMStorageTaskRunner::COMMIT_SEQUENCE,
322 base::Bind(&DOMStorageArea::ShutdownInCommitSequence, this));
323 DCHECK(success);
326 void DOMStorageArea::InitialImportIfNeeded() {
327 if (is_initial_import_done_)
328 return;
330 DCHECK(backing_.get());
332 base::TimeTicks before = base::TimeTicks::Now();
333 DOMStorageValuesMap initial_values;
334 backing_->ReadAllValues(&initial_values);
335 map_->SwapValues(&initial_values);
336 is_initial_import_done_ = true;
337 base::TimeDelta time_to_import = base::TimeTicks::Now() - before;
338 UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage",
339 time_to_import);
341 size_t local_storage_size_kb = map_->bytes_used() / 1024;
342 // Track localStorage size, from 0-6MB. Note that the maximum size should be
343 // 5MB, but we add some slop since we want to make sure the max size is always
344 // above what we see in practice, since histograms can't change.
345 UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB",
346 local_storage_size_kb,
347 0, 6 * 1024, 50);
348 if (local_storage_size_kb < 100) {
349 UMA_HISTOGRAM_TIMES(
350 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB",
351 time_to_import);
352 } else if (local_storage_size_kb < 1000) {
353 UMA_HISTOGRAM_TIMES(
354 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB",
355 time_to_import);
356 } else {
357 UMA_HISTOGRAM_TIMES(
358 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB",
359 time_to_import);
363 DOMStorageArea::CommitBatch* DOMStorageArea::CreateCommitBatchIfNeeded() {
364 DCHECK(!is_shutdown_);
365 if (!commit_batch_) {
366 commit_batch_.reset(new CommitBatch());
368 // Start a timer to commit any changes that accrue in the batch, but only if
369 // no commits are currently in flight. In that case the timer will be
370 // started after the commits have happened.
371 if (!commit_batches_in_flight_) {
372 task_runner_->PostDelayedTask(
373 FROM_HERE, base::Bind(&DOMStorageArea::OnCommitTimer, this),
374 ComputeCommitDelay());
377 return commit_batch_.get();
380 base::TimeDelta DOMStorageArea::ComputeCommitDelay() const {
381 base::TimeDelta elapsed_time = base::TimeTicks::Now() - start_time_;
382 base::TimeDelta delay = std::max(
383 base::TimeDelta::FromSeconds(kCommitDefaultDelaySecs),
384 std::max(commit_rate_limiter_.ComputeDelayNeeded(elapsed_time),
385 data_rate_limiter_.ComputeDelayNeeded(elapsed_time)));
386 UMA_HISTOGRAM_LONG_TIMES("LocalStorage.CommitDelay", delay);
387 return delay;
390 void DOMStorageArea::OnCommitTimer() {
391 if (is_shutdown_)
392 return;
394 // It's possible that there is nothing to commit if an immediate
395 // commit occured after the timer was scheduled but before it fired.
396 if (!commit_batch_)
397 return;
399 PostCommitTask();
402 void DOMStorageArea::PostCommitTask() {
403 if (is_shutdown_ || !commit_batch_)
404 return;
406 DCHECK(backing_.get());
408 commit_rate_limiter_.add_samples(1);
409 data_rate_limiter_.add_samples(commit_batch_->GetDataSize());
411 // This method executes on the primary sequence, we schedule
412 // a task for immediate execution on the commit sequence.
413 DCHECK(task_runner_->IsRunningOnPrimarySequence());
414 bool success = task_runner_->PostShutdownBlockingTask(
415 FROM_HERE,
416 DOMStorageTaskRunner::COMMIT_SEQUENCE,
417 base::Bind(&DOMStorageArea::CommitChanges, this,
418 base::Owned(commit_batch_.release())));
419 ++commit_batches_in_flight_;
420 DCHECK(success);
423 void DOMStorageArea::CommitChanges(const CommitBatch* commit_batch) {
424 // This method executes on the commit sequence.
425 DCHECK(task_runner_->IsRunningOnCommitSequence());
426 backing_->CommitChanges(commit_batch->clear_all_first,
427 commit_batch->changed_values);
428 // TODO(michaeln): what if CommitChanges returns false (e.g., we're trying to
429 // commit to a DB which is in an inconsistent state?)
430 task_runner_->PostTask(
431 FROM_HERE,
432 base::Bind(&DOMStorageArea::OnCommitComplete, this));
435 void DOMStorageArea::OnCommitComplete() {
436 // We're back on the primary sequence in this method.
437 DCHECK(task_runner_->IsRunningOnPrimarySequence());
438 --commit_batches_in_flight_;
439 if (is_shutdown_)
440 return;
441 if (commit_batch_.get() && !commit_batches_in_flight_) {
442 // More changes have accrued, restart the timer.
443 task_runner_->PostDelayedTask(
444 FROM_HERE, base::Bind(&DOMStorageArea::OnCommitTimer, this),
445 ComputeCommitDelay());
449 void DOMStorageArea::ShutdownInCommitSequence() {
450 // This method executes on the commit sequence.
451 DCHECK(task_runner_->IsRunningOnCommitSequence());
452 DCHECK(backing_.get());
453 if (commit_batch_) {
454 // Commit any changes that accrued prior to the timer firing.
455 bool success = backing_->CommitChanges(
456 commit_batch_->clear_all_first,
457 commit_batch_->changed_values);
458 DCHECK(success);
460 commit_batch_.reset();
461 backing_.reset();
462 session_storage_backing_ = NULL;
465 } // namespace content