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"
8 #include "base/location.h"
9 #include "base/logging.h"
10 #include "base/metrics/histogram.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "base/time/time.h"
13 #include "content/browser/dom_storage/dom_storage_namespace.h"
14 #include "content/browser/dom_storage/dom_storage_task_runner.h"
15 #include "content/browser/dom_storage/local_storage_database_adapter.h"
16 #include "content/browser/dom_storage/session_storage_database.h"
17 #include "content/browser/dom_storage/session_storage_database_adapter.h"
18 #include "content/common/dom_storage/dom_storage_map.h"
19 #include "content/common/dom_storage/dom_storage_types.h"
20 #include "webkit/browser/database/database_util.h"
21 #include "webkit/common/database/database_identifier.h"
22 #include "webkit/common/fileapi/file_system_util.h"
24 using webkit_database::DatabaseUtil
;
28 static const int kCommitTimerSeconds
= 1;
30 DOMStorageArea::CommitBatch::CommitBatch()
31 : clear_all_first(false) {
33 DOMStorageArea::CommitBatch::~CommitBatch() {}
37 const base::FilePath::CharType
DOMStorageArea::kDatabaseFileExtension
[] =
38 FILE_PATH_LITERAL(".localstorage");
41 base::FilePath
DOMStorageArea::DatabaseFileNameFromOrigin(const GURL
& origin
) {
42 std::string filename
= webkit_database::GetIdentifierFromOrigin(origin
);
43 // There is no base::FilePath.AppendExtension() method, so start with just the
44 // extension as the filename, and then InsertBeforeExtension the desired
46 return base::FilePath().Append(kDatabaseFileExtension
).
47 InsertBeforeExtensionASCII(filename
);
51 GURL
DOMStorageArea::OriginFromDatabaseFileName(const base::FilePath
& name
) {
52 DCHECK(name
.MatchesExtension(kDatabaseFileExtension
));
53 std::string origin_id
=
54 name
.BaseName().RemoveExtension().MaybeAsASCII();
55 return webkit_database::GetOriginFromIdentifier(origin_id
);
58 DOMStorageArea::DOMStorageArea(
59 const GURL
& origin
, const base::FilePath
& directory
,
60 DOMStorageTaskRunner
* task_runner
)
61 : namespace_id_(kLocalStorageNamespaceId
), origin_(origin
),
62 directory_(directory
),
63 task_runner_(task_runner
),
64 map_(new DOMStorageMap(kPerStorageAreaQuota
+
65 kPerStorageAreaOverQuotaAllowance
)),
66 is_initial_import_done_(true),
68 commit_batches_in_flight_(0) {
69 if (!directory
.empty()) {
70 base::FilePath path
= directory
.Append(DatabaseFileNameFromOrigin(origin_
));
71 backing_
.reset(new LocalStorageDatabaseAdapter(path
));
72 is_initial_import_done_
= false;
76 DOMStorageArea::DOMStorageArea(
78 const std::string
& persistent_namespace_id
,
80 SessionStorageDatabase
* session_storage_backing
,
81 DOMStorageTaskRunner
* task_runner
)
82 : namespace_id_(namespace_id
),
83 persistent_namespace_id_(persistent_namespace_id
),
85 task_runner_(task_runner
),
86 map_(new DOMStorageMap(kPerStorageAreaQuota
+
87 kPerStorageAreaOverQuotaAllowance
)),
88 session_storage_backing_(session_storage_backing
),
89 is_initial_import_done_(true),
91 commit_batches_in_flight_(0) {
92 DCHECK(namespace_id
!= kLocalStorageNamespaceId
);
93 if (session_storage_backing
) {
94 backing_
.reset(new SessionStorageDatabaseAdapter(
95 session_storage_backing
, persistent_namespace_id
, origin
));
96 is_initial_import_done_
= false;
100 DOMStorageArea::~DOMStorageArea() {
103 void DOMStorageArea::ExtractValues(DOMStorageValuesMap
* map
) {
106 InitialImportIfNeeded();
107 map_
->ExtractValues(map
);
110 unsigned DOMStorageArea::Length() {
113 InitialImportIfNeeded();
114 return map_
->Length();
117 base::NullableString16
DOMStorageArea::Key(unsigned index
) {
119 return base::NullableString16();
120 InitialImportIfNeeded();
121 return map_
->Key(index
);
124 base::NullableString16
DOMStorageArea::GetItem(const base::string16
& key
) {
126 return base::NullableString16();
127 InitialImportIfNeeded();
128 return map_
->GetItem(key
);
131 bool DOMStorageArea::SetItem(const base::string16
& key
,
132 const base::string16
& value
,
133 base::NullableString16
* old_value
) {
136 InitialImportIfNeeded();
137 if (!map_
->HasOneRef())
138 map_
= map_
->DeepCopy();
139 bool success
= map_
->SetItem(key
, value
, old_value
);
140 if (success
&& backing_
) {
141 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
142 commit_batch
->changed_values
[key
] = base::NullableString16(value
, false);
147 bool DOMStorageArea::RemoveItem(const base::string16
& key
,
148 base::string16
* old_value
) {
151 InitialImportIfNeeded();
152 if (!map_
->HasOneRef())
153 map_
= map_
->DeepCopy();
154 bool success
= map_
->RemoveItem(key
, old_value
);
155 if (success
&& backing_
) {
156 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
157 commit_batch
->changed_values
[key
] = base::NullableString16();
162 bool DOMStorageArea::Clear() {
165 InitialImportIfNeeded();
166 if (map_
->Length() == 0)
169 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
170 kPerStorageAreaOverQuotaAllowance
);
173 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
174 commit_batch
->clear_all_first
= true;
175 commit_batch
->changed_values
.clear();
181 void DOMStorageArea::FastClear() {
182 // TODO(marja): Unify clearing localStorage and sessionStorage. The problem is
183 // to make the following 3 to work together: 1) FastClear, 2) PurgeMemory and
184 // 3) not creating events when clearing an empty area.
188 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
189 kPerStorageAreaOverQuotaAllowance
);
190 // This ensures no import will happen while we're waiting to clear the data
191 // from the database. This mechanism fails if PurgeMemory is called.
192 is_initial_import_done_
= true;
195 CommitBatch
* commit_batch
= CreateCommitBatchIfNeeded();
196 commit_batch
->clear_all_first
= true;
197 commit_batch
->changed_values
.clear();
201 DOMStorageArea
* DOMStorageArea::ShallowCopy(
202 int64 destination_namespace_id
,
203 const std::string
& destination_persistent_namespace_id
) {
204 DCHECK_NE(kLocalStorageNamespaceId
, namespace_id_
);
205 DCHECK_NE(kLocalStorageNamespaceId
, destination_namespace_id
);
207 DOMStorageArea
* copy
= new DOMStorageArea(
208 destination_namespace_id
, destination_persistent_namespace_id
, origin_
,
209 session_storage_backing_
.get(), task_runner_
.get());
211 copy
->is_shutdown_
= is_shutdown_
;
212 copy
->is_initial_import_done_
= true;
214 // All the uncommitted changes to this area need to happen before the actual
215 // shallow copy is made (scheduled by the upper layer). Another OnCommitTimer
216 // call might be in the event queue at this point, but it's handled gracefully
223 bool DOMStorageArea::HasUncommittedChanges() const {
224 DCHECK(!is_shutdown_
);
225 return commit_batch_
.get() || commit_batches_in_flight_
;
228 void DOMStorageArea::DeleteOrigin() {
229 DCHECK(!is_shutdown_
);
230 // This function shouldn't be called for sessionStorage.
231 DCHECK(!session_storage_backing_
.get());
232 if (HasUncommittedChanges()) {
233 // TODO(michaeln): This logically deletes the data immediately,
234 // and in a matter of a second, deletes the rows from the backing
235 // database file, but the file itself will linger until shutdown
236 // or purge time. Ideally, this should delete the file more
241 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
242 kPerStorageAreaOverQuotaAllowance
);
244 is_initial_import_done_
= false;
246 backing_
->DeleteFiles();
250 void DOMStorageArea::PurgeMemory() {
251 DCHECK(!is_shutdown_
);
252 // Purging sessionStorage is not supported; it won't work with FastClear.
253 DCHECK(!session_storage_backing_
.get());
254 if (!is_initial_import_done_
|| // We're not using any memory.
255 !backing_
.get() || // We can't purge anything.
256 HasUncommittedChanges()) // We leave things alone with changes pending.
259 // Drop the in memory cache, we'll reload when needed.
260 is_initial_import_done_
= false;
261 map_
= new DOMStorageMap(kPerStorageAreaQuota
+
262 kPerStorageAreaOverQuotaAllowance
);
264 // Recreate the database object, this frees up the open sqlite connection
265 // and its page cache.
269 void DOMStorageArea::Shutdown() {
270 DCHECK(!is_shutdown_
);
276 bool success
= task_runner_
->PostShutdownBlockingTask(
278 DOMStorageTaskRunner::COMMIT_SEQUENCE
,
279 base::Bind(&DOMStorageArea::ShutdownInCommitSequence
, this));
283 void DOMStorageArea::InitialImportIfNeeded() {
284 if (is_initial_import_done_
)
287 DCHECK(backing_
.get());
289 base::TimeTicks before
= base::TimeTicks::Now();
290 DOMStorageValuesMap initial_values
;
291 backing_
->ReadAllValues(&initial_values
);
292 map_
->SwapValues(&initial_values
);
293 is_initial_import_done_
= true;
294 base::TimeDelta time_to_import
= base::TimeTicks::Now() - before
;
295 UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage",
298 size_t local_storage_size_kb
= map_
->bytes_used() / 1024;
299 // Track localStorage size, from 0-6MB. Note that the maximum size should be
300 // 5MB, but we add some slop since we want to make sure the max size is always
301 // above what we see in practice, since histograms can't change.
302 UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB",
303 local_storage_size_kb
,
305 if (local_storage_size_kb
< 100) {
307 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB",
309 } else if (local_storage_size_kb
< 1000) {
311 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB",
315 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB",
320 DOMStorageArea::CommitBatch
* DOMStorageArea::CreateCommitBatchIfNeeded() {
321 DCHECK(!is_shutdown_
);
322 if (!commit_batch_
) {
323 commit_batch_
.reset(new CommitBatch());
325 // Start a timer to commit any changes that accrue in the batch, but only if
326 // no commits are currently in flight. In that case the timer will be
327 // started after the commits have happened.
328 if (!commit_batches_in_flight_
) {
329 task_runner_
->PostDelayedTask(
331 base::Bind(&DOMStorageArea::OnCommitTimer
, this),
332 base::TimeDelta::FromSeconds(kCommitTimerSeconds
));
335 return commit_batch_
.get();
338 void DOMStorageArea::OnCommitTimer() {
342 DCHECK(backing_
.get());
344 // It's possible that there is nothing to commit, since a shallow copy occured
345 // before the timer fired.
349 // This method executes on the primary sequence, we schedule
350 // a task for immediate execution on the commit sequence.
351 DCHECK(task_runner_
->IsRunningOnPrimarySequence());
352 bool success
= task_runner_
->PostShutdownBlockingTask(
354 DOMStorageTaskRunner::COMMIT_SEQUENCE
,
355 base::Bind(&DOMStorageArea::CommitChanges
, this,
356 base::Owned(commit_batch_
.release())));
357 ++commit_batches_in_flight_
;
361 void DOMStorageArea::CommitChanges(const CommitBatch
* commit_batch
) {
362 // This method executes on the commit sequence.
363 DCHECK(task_runner_
->IsRunningOnCommitSequence());
364 backing_
->CommitChanges(commit_batch
->clear_all_first
,
365 commit_batch
->changed_values
);
366 // TODO(michaeln): what if CommitChanges returns false (e.g., we're trying to
367 // commit to a DB which is in an inconsistent state?)
368 task_runner_
->PostTask(
370 base::Bind(&DOMStorageArea::OnCommitComplete
, this));
373 void DOMStorageArea::OnCommitComplete() {
374 // We're back on the primary sequence in this method.
375 DCHECK(task_runner_
->IsRunningOnPrimarySequence());
376 --commit_batches_in_flight_
;
379 if (commit_batch_
.get() && !commit_batches_in_flight_
) {
380 // More changes have accrued, restart the timer.
381 task_runner_
->PostDelayedTask(
383 base::Bind(&DOMStorageArea::OnCommitTimer
, this),
384 base::TimeDelta::FromSeconds(kCommitTimerSeconds
));
388 void DOMStorageArea::ShutdownInCommitSequence() {
389 // This method executes on the commit sequence.
390 DCHECK(task_runner_
->IsRunningOnCommitSequence());
391 DCHECK(backing_
.get());
393 // Commit any changes that accrued prior to the timer firing.
394 bool success
= backing_
->CommitChanges(
395 commit_batch_
->clear_all_first
,
396 commit_batch_
->changed_values
);
399 commit_batch_
.reset();
401 session_storage_backing_
= NULL
;
404 } // namespace content