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"
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
;
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 kMaxBytesPerDay
= kPerStorageAreaQuota
* 2;
41 const int kMaxCommitsPerHour
= 6;
45 DOMStorageArea::RateLimiter::RateLimiter(size_t desired_rate
,
46 base::TimeDelta time_quantum
)
47 : rate_(desired_rate
), samples_(0), time_quantum_(time_quantum
) {
48 DCHECK_GT(desired_rate
, 0ul);
51 base::TimeDelta
DOMStorageArea::RateLimiter::ComputeTimeNeeded() const {
52 return time_quantum_
* (samples_
/ rate_
);
55 base::TimeDelta
DOMStorageArea::RateLimiter::ComputeDelayNeeded(
56 const base::TimeDelta elapsed_time
) const {
57 base::TimeDelta time_needed
= ComputeTimeNeeded();
58 if (time_needed
> elapsed_time
)
59 return time_needed
- elapsed_time
;
60 return base::TimeDelta();
63 DOMStorageArea::CommitBatch::CommitBatch() : clear_all_first(false) {
65 DOMStorageArea::CommitBatch::~CommitBatch() {}
67 size_t DOMStorageArea::CommitBatch::GetDataSize() const {
68 return DOMStorageMap::CountBytes(changed_values
);
72 const base::FilePath::CharType
DOMStorageArea::kDatabaseFileExtension
[] =
73 FILE_PATH_LITERAL(".localstorage");
76 base::FilePath
DOMStorageArea::DatabaseFileNameFromOrigin(const GURL
& origin
) {
77 std::string filename
= storage::GetIdentifierFromOrigin(origin
);
78 // There is no base::FilePath.AppendExtension() method, so start with just the
79 // extension as the filename, and then InsertBeforeExtension the desired
81 return base::FilePath().Append(kDatabaseFileExtension
).
82 InsertBeforeExtensionASCII(filename
);
86 GURL
DOMStorageArea::OriginFromDatabaseFileName(const base::FilePath
& name
) {
87 DCHECK(name
.MatchesExtension(kDatabaseFileExtension
));
88 std::string origin_id
=
89 name
.BaseName().RemoveExtension().MaybeAsASCII();
90 return storage::GetOriginFromIdentifier(origin_id
);
93 DOMStorageArea::DOMStorageArea(const GURL
& origin
,
94 const base::FilePath
& directory
,
95 DOMStorageTaskRunner
* task_runner
)
96 : namespace_id_(kLocalStorageNamespaceId
),
98 directory_(directory
),
99 task_runner_(task_runner
),
100 map_(new DOMStorageMap(kPerStorageAreaQuota
+
101 kPerStorageAreaOverQuotaAllowance
)),
102 is_initial_import_done_(true),
104 commit_batches_in_flight_(0),
105 start_time_(base::TimeTicks::Now()),
106 data_rate_limiter_(kMaxBytesPerDay
, base::TimeDelta::FromHours(24)),
107 commit_rate_limiter_(kMaxCommitsPerHour
, base::TimeDelta::FromHours(1)) {
108 if (!directory
.empty()) {
109 base::FilePath path
= directory
.Append(DatabaseFileNameFromOrigin(origin_
));
110 backing_
.reset(new LocalStorageDatabaseAdapter(path
));
111 is_initial_import_done_
= false;
115 DOMStorageArea::DOMStorageArea(int64 namespace_id
,
116 const std::string
& persistent_namespace_id
,
118 SessionStorageDatabase
* session_storage_backing
,
119 DOMStorageTaskRunner
* task_runner
)
120 : namespace_id_(namespace_id
),
121 persistent_namespace_id_(persistent_namespace_id
),
123 task_runner_(task_runner
),
124 map_(new DOMStorageMap(kPerStorageAreaQuota
+
125 kPerStorageAreaOverQuotaAllowance
)),
126 session_storage_backing_(session_storage_backing
),
127 is_initial_import_done_(true),
129 commit_batches_in_flight_(0),
130 start_time_(base::TimeTicks::Now()),
131 data_rate_limiter_(kMaxBytesPerDay
, base::TimeDelta::FromHours(24)),
132 commit_rate_limiter_(kMaxCommitsPerHour
, base::TimeDelta::FromHours(1)) {
133 DCHECK(namespace_id
!= kLocalStorageNamespaceId
);
134 if (session_storage_backing
) {
135 backing_
.reset(new SessionStorageDatabaseAdapter(
136 session_storage_backing
, persistent_namespace_id
, origin
));
137 is_initial_import_done_
= false;
141 DOMStorageArea::~DOMStorageArea() {
144 void DOMStorageArea::ExtractValues(DOMStorageValuesMap
* map
) {
147 InitialImportIfNeeded();
148 map_
->ExtractValues(map
);
151 unsigned DOMStorageArea::Length() {
154 InitialImportIfNeeded();
155 return map_
->Length();
158 base::NullableString16
DOMStorageArea::Key(unsigned index
) {
160 return base::NullableString16();
161 InitialImportIfNeeded();
162 return map_
->Key(index
);
165 base::NullableString16
DOMStorageArea::GetItem(const base::string16
& key
) {
167 return base::NullableString16();
168 InitialImportIfNeeded();
169 return map_
->GetItem(key
);
172 bool DOMStorageArea::SetItem(const base::string16
& key
,
173 const base::string16
& value
,
174 base::NullableString16
* old_value
) {
177 InitialImportIfNeeded();
178 if (!map_
->HasOneRef())
179 map_
= map_
->DeepCopy();
180 bool success
= map_
->SetItem(key
, value
, old_value
);
181 if (success
&& backing_
&&
182 (old_value
->is_null() || old_value
->string() != value
)) {
183 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
184 commit_batch
->changed_values
[key
] = base::NullableString16(value
, false);
189 bool DOMStorageArea::RemoveItem(const base::string16
& key
,
190 base::string16
* old_value
) {
193 InitialImportIfNeeded();
194 if (!map_
->HasOneRef())
195 map_
= map_
->DeepCopy();
196 bool success
= map_
->RemoveItem(key
, old_value
);
197 if (success
&& backing_
) {
198 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
199 commit_batch
->changed_values
[key
] = base::NullableString16();
204 bool DOMStorageArea::Clear() {
207 InitialImportIfNeeded();
208 if (map_
->Length() == 0)
211 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
212 kPerStorageAreaOverQuotaAllowance
);
215 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
216 commit_batch
->clear_all_first
= true;
217 commit_batch
->changed_values
.clear();
223 void DOMStorageArea::FastClear() {
224 // TODO(marja): Unify clearing localStorage and sessionStorage. The problem is
225 // to make the following 3 to work together: 1) FastClear, 2) PurgeMemory and
226 // 3) not creating events when clearing an empty area.
230 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
231 kPerStorageAreaOverQuotaAllowance
);
232 // This ensures no import will happen while we're waiting to clear the data
233 // from the database. This mechanism fails if PurgeMemory is called.
234 is_initial_import_done_
= true;
237 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
238 commit_batch
->clear_all_first
= true;
239 commit_batch
->changed_values
.clear();
243 DOMStorageArea
* DOMStorageArea::ShallowCopy(
244 int64 destination_namespace_id
,
245 const std::string
& destination_persistent_namespace_id
) {
246 DCHECK_NE(kLocalStorageNamespaceId
, namespace_id_
);
247 DCHECK_NE(kLocalStorageNamespaceId
, destination_namespace_id
);
249 DOMStorageArea
* copy
= new DOMStorageArea(
250 destination_namespace_id
, destination_persistent_namespace_id
, origin_
,
251 session_storage_backing_
.get(), task_runner_
.get());
253 copy
->is_shutdown_
= is_shutdown_
;
254 copy
->is_initial_import_done_
= true;
256 // All the uncommitted changes to this area need to happen before the actual
257 // shallow copy is made (scheduled by the upper layer sometime after return).
259 ScheduleImmediateCommit();
263 bool DOMStorageArea::HasUncommittedChanges() const {
264 return commit_batch_
.get() || commit_batches_in_flight_
;
267 void DOMStorageArea::ScheduleImmediateCommit() {
268 DCHECK(HasUncommittedChanges());
272 void DOMStorageArea::DeleteOrigin() {
273 DCHECK(!is_shutdown_
);
274 // This function shouldn't be called for sessionStorage.
275 DCHECK(!session_storage_backing_
.get());
276 if (HasUncommittedChanges()) {
277 // TODO(michaeln): This logically deletes the data immediately,
278 // and in a matter of a second, deletes the rows from the backing
279 // database file, but the file itself will linger until shutdown
280 // or purge time. Ideally, this should delete the file more
285 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
286 kPerStorageAreaOverQuotaAllowance
);
288 is_initial_import_done_
= false;
290 backing_
->DeleteFiles();
294 void DOMStorageArea::PurgeMemory() {
295 DCHECK(!is_shutdown_
);
296 // Purging sessionStorage is not supported; it won't work with FastClear.
297 DCHECK(!session_storage_backing_
.get());
298 if (!is_initial_import_done_
|| // We're not using any memory.
299 !backing_
.get() || // We can't purge anything.
300 HasUncommittedChanges()) // We leave things alone with changes pending.
303 // Drop the in memory cache, we'll reload when needed.
304 is_initial_import_done_
= false;
305 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
306 kPerStorageAreaOverQuotaAllowance
);
308 // Recreate the database object, this frees up the open sqlite connection
309 // and its page cache.
313 void DOMStorageArea::Shutdown() {
314 DCHECK(!is_shutdown_
);
320 bool success
= task_runner_
->PostShutdownBlockingTask(
322 DOMStorageTaskRunner::COMMIT_SEQUENCE
,
323 base::Bind(&DOMStorageArea::ShutdownInCommitSequence
, this));
327 void DOMStorageArea::InitialImportIfNeeded() {
328 if (is_initial_import_done_
)
331 DCHECK(backing_
.get());
333 base::TimeTicks before
= base::TimeTicks::Now();
334 DOMStorageValuesMap initial_values
;
335 backing_
->ReadAllValues(&initial_values
);
336 map_
->SwapValues(&initial_values
);
337 is_initial_import_done_
= true;
338 base::TimeDelta time_to_import
= base::TimeTicks::Now() - before
;
339 UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage",
342 size_t local_storage_size_kb
= map_
->bytes_used() / 1024;
343 // Track localStorage size, from 0-6MB. Note that the maximum size should be
344 // 5MB, but we add some slop since we want to make sure the max size is always
345 // above what we see in practice, since histograms can't change.
346 UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB",
347 local_storage_size_kb
,
349 if (local_storage_size_kb
< 100) {
351 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB",
353 } else if (local_storage_size_kb
< 1000) {
355 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB",
359 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB",
364 DOMStorageArea::CommitBatch
* DOMStorageArea::CreateCommitBatchIfNeeded() {
365 DCHECK(!is_shutdown_
);
366 if (!commit_batch_
) {
367 commit_batch_
.reset(new CommitBatch());
368 BrowserThread::PostAfterStartupTask(
369 FROM_HERE
, task_runner_
,
370 base::Bind(&DOMStorageArea::StartCommitTimer
, this));
372 return commit_batch_
.get();
375 void DOMStorageArea::StartCommitTimer() {
376 if (is_shutdown_
|| !commit_batch_
)
379 // Start a timer to commit any changes that accrue in the batch, but only if
380 // no commits are currently in flight. In that case the timer will be
381 // started after the commits have happened.
382 if (commit_batches_in_flight_
)
385 task_runner_
->PostDelayedTask(
386 FROM_HERE
, base::Bind(&DOMStorageArea::OnCommitTimer
, this),
387 ComputeCommitDelay());
390 base::TimeDelta
DOMStorageArea::ComputeCommitDelay() const {
391 base::TimeDelta elapsed_time
= base::TimeTicks::Now() - start_time_
;
392 base::TimeDelta delay
= std::max(
393 base::TimeDelta::FromSeconds(kCommitDefaultDelaySecs
),
394 std::max(commit_rate_limiter_
.ComputeDelayNeeded(elapsed_time
),
395 data_rate_limiter_
.ComputeDelayNeeded(elapsed_time
)));
396 UMA_HISTOGRAM_LONG_TIMES("LocalStorage.CommitDelay", delay
);
400 void DOMStorageArea::OnCommitTimer() {
404 // It's possible that there is nothing to commit if an immediate
405 // commit occured after the timer was scheduled but before it fired.
412 void DOMStorageArea::PostCommitTask() {
413 if (is_shutdown_
|| !commit_batch_
)
416 DCHECK(backing_
.get());
418 commit_rate_limiter_
.add_samples(1);
419 data_rate_limiter_
.add_samples(commit_batch_
->GetDataSize());
421 // This method executes on the primary sequence, we schedule
422 // a task for immediate execution on the commit sequence.
423 DCHECK(task_runner_
->IsRunningOnPrimarySequence());
424 bool success
= task_runner_
->PostShutdownBlockingTask(
426 DOMStorageTaskRunner::COMMIT_SEQUENCE
,
427 base::Bind(&DOMStorageArea::CommitChanges
, this,
428 base::Owned(commit_batch_
.release())));
429 ++commit_batches_in_flight_
;
433 void DOMStorageArea::CommitChanges(const CommitBatch
* commit_batch
) {
434 // This method executes on the commit sequence.
435 DCHECK(task_runner_
->IsRunningOnCommitSequence());
436 backing_
->CommitChanges(commit_batch
->clear_all_first
,
437 commit_batch
->changed_values
);
438 // TODO(michaeln): what if CommitChanges returns false (e.g., we're trying to
439 // commit to a DB which is in an inconsistent state?)
440 task_runner_
->PostTask(
442 base::Bind(&DOMStorageArea::OnCommitComplete
, this));
445 void DOMStorageArea::OnCommitComplete() {
446 // We're back on the primary sequence in this method.
447 DCHECK(task_runner_
->IsRunningOnPrimarySequence());
448 --commit_batches_in_flight_
;
451 if (commit_batch_
.get() && !commit_batches_in_flight_
) {
452 // More changes have accrued, restart the timer.
453 task_runner_
->PostDelayedTask(
454 FROM_HERE
, base::Bind(&DOMStorageArea::OnCommitTimer
, this),
455 ComputeCommitDelay());
459 void DOMStorageArea::ShutdownInCommitSequence() {
460 // This method executes on the commit sequence.
461 DCHECK(task_runner_
->IsRunningOnCommitSequence());
462 DCHECK(backing_
.get());
464 // Commit any changes that accrued prior to the timer firing.
465 bool success
= backing_
->CommitChanges(
466 commit_batch_
->clear_all_first
,
467 commit_batch_
->changed_values
);
470 commit_batch_
.reset();
472 session_storage_backing_
= NULL
;
475 } // namespace content