Add ICU message format support
[chromium-blink-merge.git] / content / browser / dom_storage / dom_storage_area.cc
blobe76f5c94d5e1806bf265ac74ed1e57ce0af97d98
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 "content/public/browser/browser_thread.h"
24 #include "storage/browser/database/database_util.h"
25 #include "storage/common/database/database_identifier.h"
26 #include "storage/common/fileapi/file_system_util.h"
28 using storage::DatabaseUtil;
30 namespace content {
32 namespace {
34 // Delay for a moment after a value is set in anticipation
35 // of other values being set, so changes are batched.
36 const int kCommitDefaultDelaySecs = 5;
38 // To avoid excessive IO we apply limits to the amount of data being written
39 // and the frequency of writes. The specific values used are somewhat arbitrary.
40 const int kMaxBytesPerHour = kPerStorageAreaQuota;
41 const int kMaxCommitsPerHour = 60;
43 } // namespace
45 bool DOMStorageArea::s_aggressive_flushing_enabled_ = false;
47 DOMStorageArea::RateLimiter::RateLimiter(size_t desired_rate,
48 base::TimeDelta time_quantum)
49 : rate_(desired_rate), samples_(0), time_quantum_(time_quantum) {
50 DCHECK_GT(desired_rate, 0ul);
53 base::TimeDelta DOMStorageArea::RateLimiter::ComputeTimeNeeded() const {
54 return time_quantum_ * (samples_ / rate_);
57 base::TimeDelta DOMStorageArea::RateLimiter::ComputeDelayNeeded(
58 const base::TimeDelta elapsed_time) const {
59 base::TimeDelta time_needed = ComputeTimeNeeded();
60 if (time_needed > elapsed_time)
61 return time_needed - elapsed_time;
62 return base::TimeDelta();
65 DOMStorageArea::CommitBatch::CommitBatch() : clear_all_first(false) {
67 DOMStorageArea::CommitBatch::~CommitBatch() {}
69 size_t DOMStorageArea::CommitBatch::GetDataSize() const {
70 return DOMStorageMap::CountBytes(changed_values);
73 // static
74 const base::FilePath::CharType DOMStorageArea::kDatabaseFileExtension[] =
75 FILE_PATH_LITERAL(".localstorage");
77 // static
78 base::FilePath DOMStorageArea::DatabaseFileNameFromOrigin(const GURL& origin) {
79 std::string filename = storage::GetIdentifierFromOrigin(origin);
80 // There is no base::FilePath.AppendExtension() method, so start with just the
81 // extension as the filename, and then InsertBeforeExtension the desired
82 // name.
83 return base::FilePath().Append(kDatabaseFileExtension).
84 InsertBeforeExtensionASCII(filename);
87 // static
88 GURL DOMStorageArea::OriginFromDatabaseFileName(const base::FilePath& name) {
89 DCHECK(name.MatchesExtension(kDatabaseFileExtension));
90 std::string origin_id =
91 name.BaseName().RemoveExtension().MaybeAsASCII();
92 return storage::GetOriginFromIdentifier(origin_id);
95 void DOMStorageArea::EnableAggressiveCommitDelay() {
96 s_aggressive_flushing_enabled_ = true;
99 DOMStorageArea::DOMStorageArea(const GURL& origin,
100 const base::FilePath& directory,
101 DOMStorageTaskRunner* task_runner)
102 : namespace_id_(kLocalStorageNamespaceId),
103 origin_(origin),
104 directory_(directory),
105 task_runner_(task_runner),
106 map_(new DOMStorageMap(kPerStorageAreaQuota +
107 kPerStorageAreaOverQuotaAllowance)),
108 is_initial_import_done_(true),
109 is_shutdown_(false),
110 commit_batches_in_flight_(0),
111 start_time_(base::TimeTicks::Now()),
112 data_rate_limiter_(kMaxBytesPerHour, base::TimeDelta::FromHours(1)),
113 commit_rate_limiter_(kMaxCommitsPerHour, base::TimeDelta::FromHours(1)) {
114 if (!directory.empty()) {
115 base::FilePath path = directory.Append(DatabaseFileNameFromOrigin(origin_));
116 backing_.reset(new LocalStorageDatabaseAdapter(path));
117 is_initial_import_done_ = false;
121 DOMStorageArea::DOMStorageArea(int64 namespace_id,
122 const std::string& persistent_namespace_id,
123 const GURL& origin,
124 SessionStorageDatabase* session_storage_backing,
125 DOMStorageTaskRunner* task_runner)
126 : namespace_id_(namespace_id),
127 persistent_namespace_id_(persistent_namespace_id),
128 origin_(origin),
129 task_runner_(task_runner),
130 map_(new DOMStorageMap(kPerStorageAreaQuota +
131 kPerStorageAreaOverQuotaAllowance)),
132 session_storage_backing_(session_storage_backing),
133 is_initial_import_done_(true),
134 is_shutdown_(false),
135 commit_batches_in_flight_(0),
136 start_time_(base::TimeTicks::Now()),
137 data_rate_limiter_(kMaxBytesPerHour, base::TimeDelta::FromHours(1)),
138 commit_rate_limiter_(kMaxCommitsPerHour, base::TimeDelta::FromHours(1)) {
139 DCHECK(namespace_id != kLocalStorageNamespaceId);
140 if (session_storage_backing) {
141 backing_.reset(new SessionStorageDatabaseAdapter(
142 session_storage_backing, persistent_namespace_id, origin));
143 is_initial_import_done_ = false;
147 DOMStorageArea::~DOMStorageArea() {
150 void DOMStorageArea::ExtractValues(DOMStorageValuesMap* map) {
151 if (is_shutdown_)
152 return;
153 InitialImportIfNeeded();
154 map_->ExtractValues(map);
157 unsigned DOMStorageArea::Length() {
158 if (is_shutdown_)
159 return 0;
160 InitialImportIfNeeded();
161 return map_->Length();
164 base::NullableString16 DOMStorageArea::Key(unsigned index) {
165 if (is_shutdown_)
166 return base::NullableString16();
167 InitialImportIfNeeded();
168 return map_->Key(index);
171 base::NullableString16 DOMStorageArea::GetItem(const base::string16& key) {
172 if (is_shutdown_)
173 return base::NullableString16();
174 InitialImportIfNeeded();
175 return map_->GetItem(key);
178 bool DOMStorageArea::SetItem(const base::string16& key,
179 const base::string16& value,
180 base::NullableString16* old_value) {
181 if (is_shutdown_)
182 return false;
183 InitialImportIfNeeded();
184 if (!map_->HasOneRef())
185 map_ = map_->DeepCopy();
186 bool success = map_->SetItem(key, value, old_value);
187 if (success && backing_ &&
188 (old_value->is_null() || old_value->string() != value)) {
189 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
190 commit_batch->changed_values[key] = base::NullableString16(value, false);
192 return success;
195 bool DOMStorageArea::RemoveItem(const base::string16& key,
196 base::string16* old_value) {
197 if (is_shutdown_)
198 return false;
199 InitialImportIfNeeded();
200 if (!map_->HasOneRef())
201 map_ = map_->DeepCopy();
202 bool success = map_->RemoveItem(key, old_value);
203 if (success && backing_) {
204 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
205 commit_batch->changed_values[key] = base::NullableString16();
207 return success;
210 bool DOMStorageArea::Clear() {
211 if (is_shutdown_)
212 return false;
213 InitialImportIfNeeded();
214 if (map_->Length() == 0)
215 return false;
217 map_ = new DOMStorageMap(kPerStorageAreaQuota +
218 kPerStorageAreaOverQuotaAllowance);
220 if (backing_) {
221 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
222 commit_batch->clear_all_first = true;
223 commit_batch->changed_values.clear();
226 return true;
229 void DOMStorageArea::FastClear() {
230 // TODO(marja): Unify clearing localStorage and sessionStorage. The problem is
231 // to make the following 3 to work together: 1) FastClear, 2) PurgeMemory and
232 // 3) not creating events when clearing an empty area.
233 if (is_shutdown_)
234 return;
236 map_ = new DOMStorageMap(kPerStorageAreaQuota +
237 kPerStorageAreaOverQuotaAllowance);
238 // This ensures no import will happen while we're waiting to clear the data
239 // from the database. This mechanism fails if PurgeMemory is called.
240 is_initial_import_done_ = true;
242 if (backing_) {
243 CommitBatch* commit_batch = CreateCommitBatchIfNeeded();
244 commit_batch->clear_all_first = true;
245 commit_batch->changed_values.clear();
249 DOMStorageArea* DOMStorageArea::ShallowCopy(
250 int64 destination_namespace_id,
251 const std::string& destination_persistent_namespace_id) {
252 DCHECK_NE(kLocalStorageNamespaceId, namespace_id_);
253 DCHECK_NE(kLocalStorageNamespaceId, destination_namespace_id);
255 DOMStorageArea* copy = new DOMStorageArea(
256 destination_namespace_id, destination_persistent_namespace_id, origin_,
257 session_storage_backing_.get(), task_runner_.get());
258 copy->map_ = map_;
259 copy->is_shutdown_ = is_shutdown_;
260 copy->is_initial_import_done_ = true;
262 // All the uncommitted changes to this area need to happen before the actual
263 // shallow copy is made (scheduled by the upper layer sometime after return).
264 if (commit_batch_)
265 ScheduleImmediateCommit();
266 return copy;
269 bool DOMStorageArea::HasUncommittedChanges() const {
270 return commit_batch_.get() || commit_batches_in_flight_;
273 void DOMStorageArea::ScheduleImmediateCommit() {
274 DCHECK(HasUncommittedChanges());
275 PostCommitTask();
278 void DOMStorageArea::DeleteOrigin() {
279 DCHECK(!is_shutdown_);
280 // This function shouldn't be called for sessionStorage.
281 DCHECK(!session_storage_backing_.get());
282 if (HasUncommittedChanges()) {
283 // TODO(michaeln): This logically deletes the data immediately,
284 // and in a matter of a second, deletes the rows from the backing
285 // database file, but the file itself will linger until shutdown
286 // or purge time. Ideally, this should delete the file more
287 // quickly.
288 Clear();
289 return;
291 map_ = new DOMStorageMap(kPerStorageAreaQuota +
292 kPerStorageAreaOverQuotaAllowance);
293 if (backing_) {
294 is_initial_import_done_ = false;
295 backing_->Reset();
296 backing_->DeleteFiles();
300 void DOMStorageArea::PurgeMemory() {
301 DCHECK(!is_shutdown_);
302 // Purging sessionStorage is not supported; it won't work with FastClear.
303 DCHECK(!session_storage_backing_.get());
304 if (!is_initial_import_done_ || // We're not using any memory.
305 !backing_.get() || // We can't purge anything.
306 HasUncommittedChanges()) // We leave things alone with changes pending.
307 return;
309 // Drop the in memory cache, we'll reload when needed.
310 is_initial_import_done_ = false;
311 map_ = new DOMStorageMap(kPerStorageAreaQuota +
312 kPerStorageAreaOverQuotaAllowance);
314 // Recreate the database object, this frees up the open sqlite connection
315 // and its page cache.
316 backing_->Reset();
319 void DOMStorageArea::Shutdown() {
320 DCHECK(!is_shutdown_);
321 is_shutdown_ = true;
322 map_ = NULL;
323 if (!backing_)
324 return;
326 bool success = task_runner_->PostShutdownBlockingTask(
327 FROM_HERE,
328 DOMStorageTaskRunner::COMMIT_SEQUENCE,
329 base::Bind(&DOMStorageArea::ShutdownInCommitSequence, this));
330 DCHECK(success);
333 void DOMStorageArea::InitialImportIfNeeded() {
334 if (is_initial_import_done_)
335 return;
337 DCHECK(backing_.get());
339 base::TimeTicks before = base::TimeTicks::Now();
340 DOMStorageValuesMap initial_values;
341 backing_->ReadAllValues(&initial_values);
342 map_->SwapValues(&initial_values);
343 is_initial_import_done_ = true;
344 base::TimeDelta time_to_import = base::TimeTicks::Now() - before;
345 UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage",
346 time_to_import);
348 size_t local_storage_size_kb = map_->bytes_used() / 1024;
349 // Track localStorage size, from 0-6MB. Note that the maximum size should be
350 // 5MB, but we add some slop since we want to make sure the max size is always
351 // above what we see in practice, since histograms can't change.
352 UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB",
353 local_storage_size_kb,
354 0, 6 * 1024, 50);
355 if (local_storage_size_kb < 100) {
356 UMA_HISTOGRAM_TIMES(
357 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB",
358 time_to_import);
359 } else if (local_storage_size_kb < 1000) {
360 UMA_HISTOGRAM_TIMES(
361 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB",
362 time_to_import);
363 } else {
364 UMA_HISTOGRAM_TIMES(
365 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB",
366 time_to_import);
370 DOMStorageArea::CommitBatch* DOMStorageArea::CreateCommitBatchIfNeeded() {
371 DCHECK(!is_shutdown_);
372 if (!commit_batch_) {
373 commit_batch_.reset(new CommitBatch());
374 BrowserThread::PostAfterStartupTask(
375 FROM_HERE, task_runner_,
376 base::Bind(&DOMStorageArea::StartCommitTimer, this));
378 return commit_batch_.get();
381 void DOMStorageArea::StartCommitTimer() {
382 if (is_shutdown_ || !commit_batch_)
383 return;
385 // Start a timer to commit any changes that accrue in the batch, but only if
386 // no commits are currently in flight. In that case the timer will be
387 // started after the commits have happened.
388 if (commit_batches_in_flight_)
389 return;
391 task_runner_->PostDelayedTask(
392 FROM_HERE, base::Bind(&DOMStorageArea::OnCommitTimer, this),
393 ComputeCommitDelay());
396 base::TimeDelta DOMStorageArea::ComputeCommitDelay() const {
397 if (s_aggressive_flushing_enabled_)
398 return base::TimeDelta::FromSeconds(1);
400 base::TimeDelta elapsed_time = base::TimeTicks::Now() - start_time_;
401 base::TimeDelta delay = std::max(
402 base::TimeDelta::FromSeconds(kCommitDefaultDelaySecs),
403 std::max(commit_rate_limiter_.ComputeDelayNeeded(elapsed_time),
404 data_rate_limiter_.ComputeDelayNeeded(elapsed_time)));
405 UMA_HISTOGRAM_LONG_TIMES("LocalStorage.CommitDelay", delay);
406 return delay;
409 void DOMStorageArea::OnCommitTimer() {
410 if (is_shutdown_)
411 return;
413 // It's possible that there is nothing to commit if an immediate
414 // commit occured after the timer was scheduled but before it fired.
415 if (!commit_batch_)
416 return;
418 PostCommitTask();
421 void DOMStorageArea::PostCommitTask() {
422 if (is_shutdown_ || !commit_batch_)
423 return;
425 DCHECK(backing_.get());
427 commit_rate_limiter_.add_samples(1);
428 data_rate_limiter_.add_samples(commit_batch_->GetDataSize());
430 // This method executes on the primary sequence, we schedule
431 // a task for immediate execution on the commit sequence.
432 DCHECK(task_runner_->IsRunningOnPrimarySequence());
433 bool success = task_runner_->PostShutdownBlockingTask(
434 FROM_HERE,
435 DOMStorageTaskRunner::COMMIT_SEQUENCE,
436 base::Bind(&DOMStorageArea::CommitChanges, this,
437 base::Owned(commit_batch_.release())));
438 ++commit_batches_in_flight_;
439 DCHECK(success);
442 void DOMStorageArea::CommitChanges(const CommitBatch* commit_batch) {
443 // This method executes on the commit sequence.
444 DCHECK(task_runner_->IsRunningOnCommitSequence());
445 backing_->CommitChanges(commit_batch->clear_all_first,
446 commit_batch->changed_values);
447 // TODO(michaeln): what if CommitChanges returns false (e.g., we're trying to
448 // commit to a DB which is in an inconsistent state?)
449 task_runner_->PostTask(
450 FROM_HERE,
451 base::Bind(&DOMStorageArea::OnCommitComplete, this));
454 void DOMStorageArea::OnCommitComplete() {
455 // We're back on the primary sequence in this method.
456 DCHECK(task_runner_->IsRunningOnPrimarySequence());
457 --commit_batches_in_flight_;
458 if (is_shutdown_)
459 return;
460 if (commit_batch_.get() && !commit_batches_in_flight_) {
461 // More changes have accrued, restart the timer.
462 task_runner_->PostDelayedTask(
463 FROM_HERE, base::Bind(&DOMStorageArea::OnCommitTimer, this),
464 ComputeCommitDelay());
468 void DOMStorageArea::ShutdownInCommitSequence() {
469 // This method executes on the commit sequence.
470 DCHECK(task_runner_->IsRunningOnCommitSequence());
471 DCHECK(backing_.get());
472 if (commit_batch_) {
473 // Commit any changes that accrued prior to the timer firing.
474 bool success = backing_->CommitChanges(
475 commit_batch_->clear_all_first,
476 commit_batch_->changed_values);
477 DCHECK(success);
479 commit_batch_.reset();
480 backing_.reset();
481 session_storage_backing_ = NULL;
484 } // namespace content