OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012 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 "webkit/browser/dom_storage/dom_storage_area.h" | |
6 | |
7 #include "base/bind.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 "webkit/browser/database/database_util.h" | |
14 #include "webkit/browser/dom_storage/dom_storage_namespace.h" | |
15 #include "webkit/browser/dom_storage/dom_storage_task_runner.h" | |
16 #include "webkit/browser/dom_storage/local_storage_database_adapter.h" | |
17 #include "webkit/browser/dom_storage/session_storage_database.h" | |
18 #include "webkit/browser/dom_storage/session_storage_database_adapter.h" | |
19 #include "webkit/common/database/database_identifier.h" | |
20 #include "webkit/common/dom_storage/dom_storage_map.h" | |
21 #include "webkit/common/dom_storage/dom_storage_types.h" | |
22 #include "webkit/common/fileapi/file_system_util.h" | |
23 | |
24 using webkit_database::DatabaseUtil; | |
25 | |
26 namespace dom_storage { | |
27 | |
28 static const int kCommitTimerSeconds = 1; | |
29 | |
30 DomStorageArea::CommitBatch::CommitBatch() | |
31 : clear_all_first(false) { | |
32 } | |
33 DomStorageArea::CommitBatch::~CommitBatch() {} | |
34 | |
35 | |
36 // static | |
37 const base::FilePath::CharType DomStorageArea::kDatabaseFileExtension[] = | |
38 FILE_PATH_LITERAL(".localstorage"); | |
39 | |
40 // static | |
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 | |
45 // name. | |
46 return base::FilePath().Append(kDatabaseFileExtension). | |
47 InsertBeforeExtensionASCII(filename); | |
48 } | |
49 | |
50 // static | |
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); | |
56 } | |
57 | |
58 DomStorageArea::DomStorageArea(const GURL& origin, const base::FilePath& directo
ry, | |
59 DomStorageTaskRunner* task_runner) | |
60 : namespace_id_(kLocalStorageNamespaceId), origin_(origin), | |
61 directory_(directory), | |
62 task_runner_(task_runner), | |
63 map_(new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance)), | |
64 is_initial_import_done_(true), | |
65 is_shutdown_(false), | |
66 commit_batches_in_flight_(0) { | |
67 if (!directory.empty()) { | |
68 base::FilePath path = directory.Append(DatabaseFileNameFromOrigin(origin_)); | |
69 backing_.reset(new LocalStorageDatabaseAdapter(path)); | |
70 is_initial_import_done_ = false; | |
71 } | |
72 } | |
73 | |
74 DomStorageArea::DomStorageArea( | |
75 int64 namespace_id, | |
76 const std::string& persistent_namespace_id, | |
77 const GURL& origin, | |
78 SessionStorageDatabase* session_storage_backing, | |
79 DomStorageTaskRunner* task_runner) | |
80 : namespace_id_(namespace_id), | |
81 persistent_namespace_id_(persistent_namespace_id), | |
82 origin_(origin), | |
83 task_runner_(task_runner), | |
84 map_(new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance)), | |
85 session_storage_backing_(session_storage_backing), | |
86 is_initial_import_done_(true), | |
87 is_shutdown_(false), | |
88 commit_batches_in_flight_(0) { | |
89 DCHECK(namespace_id != kLocalStorageNamespaceId); | |
90 if (session_storage_backing) { | |
91 backing_.reset(new SessionStorageDatabaseAdapter( | |
92 session_storage_backing, persistent_namespace_id, origin)); | |
93 is_initial_import_done_ = false; | |
94 } | |
95 } | |
96 | |
97 DomStorageArea::~DomStorageArea() { | |
98 } | |
99 | |
100 void DomStorageArea::ExtractValues(ValuesMap* map) { | |
101 if (is_shutdown_) | |
102 return; | |
103 InitialImportIfNeeded(); | |
104 map_->ExtractValues(map); | |
105 } | |
106 | |
107 unsigned DomStorageArea::Length() { | |
108 if (is_shutdown_) | |
109 return 0; | |
110 InitialImportIfNeeded(); | |
111 return map_->Length(); | |
112 } | |
113 | |
114 base::NullableString16 DomStorageArea::Key(unsigned index) { | |
115 if (is_shutdown_) | |
116 return base::NullableString16(); | |
117 InitialImportIfNeeded(); | |
118 return map_->Key(index); | |
119 } | |
120 | |
121 base::NullableString16 DomStorageArea::GetItem(const base::string16& key) { | |
122 if (is_shutdown_) | |
123 return base::NullableString16(); | |
124 InitialImportIfNeeded(); | |
125 return map_->GetItem(key); | |
126 } | |
127 | |
128 bool DomStorageArea::SetItem(const base::string16& key, | |
129 const base::string16& value, | |
130 base::NullableString16* old_value) { | |
131 if (is_shutdown_) | |
132 return false; | |
133 InitialImportIfNeeded(); | |
134 if (!map_->HasOneRef()) | |
135 map_ = map_->DeepCopy(); | |
136 bool success = map_->SetItem(key, value, old_value); | |
137 if (success && backing_) { | |
138 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); | |
139 commit_batch->changed_values[key] = base::NullableString16(value, false); | |
140 } | |
141 return success; | |
142 } | |
143 | |
144 bool DomStorageArea::RemoveItem(const base::string16& key, | |
145 base::string16* old_value) { | |
146 if (is_shutdown_) | |
147 return false; | |
148 InitialImportIfNeeded(); | |
149 if (!map_->HasOneRef()) | |
150 map_ = map_->DeepCopy(); | |
151 bool success = map_->RemoveItem(key, old_value); | |
152 if (success && backing_) { | |
153 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); | |
154 commit_batch->changed_values[key] = base::NullableString16(); | |
155 } | |
156 return success; | |
157 } | |
158 | |
159 bool DomStorageArea::Clear() { | |
160 if (is_shutdown_) | |
161 return false; | |
162 InitialImportIfNeeded(); | |
163 if (map_->Length() == 0) | |
164 return false; | |
165 | |
166 map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); | |
167 | |
168 if (backing_) { | |
169 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); | |
170 commit_batch->clear_all_first = true; | |
171 commit_batch->changed_values.clear(); | |
172 } | |
173 | |
174 return true; | |
175 } | |
176 | |
177 void DomStorageArea::FastClear() { | |
178 // TODO(marja): Unify clearing localStorage and sessionStorage. The problem is | |
179 // to make the following 3 to work together: 1) FastClear, 2) PurgeMemory and | |
180 // 3) not creating events when clearing an empty area. | |
181 if (is_shutdown_) | |
182 return; | |
183 | |
184 map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); | |
185 // This ensures no import will happen while we're waiting to clear the data | |
186 // from the database. This mechanism fails if PurgeMemory is called. | |
187 is_initial_import_done_ = true; | |
188 | |
189 if (backing_) { | |
190 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); | |
191 commit_batch->clear_all_first = true; | |
192 commit_batch->changed_values.clear(); | |
193 } | |
194 } | |
195 | |
196 DomStorageArea* DomStorageArea::ShallowCopy( | |
197 int64 destination_namespace_id, | |
198 const std::string& destination_persistent_namespace_id) { | |
199 DCHECK_NE(kLocalStorageNamespaceId, namespace_id_); | |
200 DCHECK_NE(kLocalStorageNamespaceId, destination_namespace_id); | |
201 | |
202 DomStorageArea* copy = new DomStorageArea( | |
203 destination_namespace_id, destination_persistent_namespace_id, origin_, | |
204 session_storage_backing_.get(), task_runner_.get()); | |
205 copy->map_ = map_; | |
206 copy->is_shutdown_ = is_shutdown_; | |
207 copy->is_initial_import_done_ = true; | |
208 | |
209 // All the uncommitted changes to this area need to happen before the actual | |
210 // shallow copy is made (scheduled by the upper layer). Another OnCommitTimer | |
211 // call might be in the event queue at this point, but it's handled gracefully | |
212 // when it fires. | |
213 if (commit_batch_) | |
214 OnCommitTimer(); | |
215 return copy; | |
216 } | |
217 | |
218 bool DomStorageArea::HasUncommittedChanges() const { | |
219 DCHECK(!is_shutdown_); | |
220 return commit_batch_.get() || commit_batches_in_flight_; | |
221 } | |
222 | |
223 void DomStorageArea::DeleteOrigin() { | |
224 DCHECK(!is_shutdown_); | |
225 // This function shouldn't be called for sessionStorage. | |
226 DCHECK(!session_storage_backing_.get()); | |
227 if (HasUncommittedChanges()) { | |
228 // TODO(michaeln): This logically deletes the data immediately, | |
229 // and in a matter of a second, deletes the rows from the backing | |
230 // database file, but the file itself will linger until shutdown | |
231 // or purge time. Ideally, this should delete the file more | |
232 // quickly. | |
233 Clear(); | |
234 return; | |
235 } | |
236 map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); | |
237 if (backing_) { | |
238 is_initial_import_done_ = false; | |
239 backing_->Reset(); | |
240 backing_->DeleteFiles(); | |
241 } | |
242 } | |
243 | |
244 void DomStorageArea::PurgeMemory() { | |
245 DCHECK(!is_shutdown_); | |
246 // Purging sessionStorage is not supported; it won't work with FastClear. | |
247 DCHECK(!session_storage_backing_.get()); | |
248 if (!is_initial_import_done_ || // We're not using any memory. | |
249 !backing_.get() || // We can't purge anything. | |
250 HasUncommittedChanges()) // We leave things alone with changes pending. | |
251 return; | |
252 | |
253 // Drop the in memory cache, we'll reload when needed. | |
254 is_initial_import_done_ = false; | |
255 map_ = new DomStorageMap(kPerAreaQuota + kPerAreaOverQuotaAllowance); | |
256 | |
257 // Recreate the database object, this frees up the open sqlite connection | |
258 // and its page cache. | |
259 backing_->Reset(); | |
260 } | |
261 | |
262 void DomStorageArea::Shutdown() { | |
263 DCHECK(!is_shutdown_); | |
264 is_shutdown_ = true; | |
265 map_ = NULL; | |
266 if (!backing_) | |
267 return; | |
268 | |
269 bool success = task_runner_->PostShutdownBlockingTask( | |
270 FROM_HERE, | |
271 DomStorageTaskRunner::COMMIT_SEQUENCE, | |
272 base::Bind(&DomStorageArea::ShutdownInCommitSequence, this)); | |
273 DCHECK(success); | |
274 } | |
275 | |
276 void DomStorageArea::InitialImportIfNeeded() { | |
277 if (is_initial_import_done_) | |
278 return; | |
279 | |
280 DCHECK(backing_.get()); | |
281 | |
282 base::TimeTicks before = base::TimeTicks::Now(); | |
283 ValuesMap initial_values; | |
284 backing_->ReadAllValues(&initial_values); | |
285 map_->SwapValues(&initial_values); | |
286 is_initial_import_done_ = true; | |
287 base::TimeDelta time_to_import = base::TimeTicks::Now() - before; | |
288 UMA_HISTOGRAM_TIMES("LocalStorage.BrowserTimeToPrimeLocalStorage", | |
289 time_to_import); | |
290 | |
291 size_t local_storage_size_kb = map_->bytes_used() / 1024; | |
292 // Track localStorage size, from 0-6MB. Note that the maximum size should be | |
293 // 5MB, but we add some slop since we want to make sure the max size is always | |
294 // above what we see in practice, since histograms can't change. | |
295 UMA_HISTOGRAM_CUSTOM_COUNTS("LocalStorage.BrowserLocalStorageSizeInKB", | |
296 local_storage_size_kb, | |
297 0, 6 * 1024, 50); | |
298 if (local_storage_size_kb < 100) { | |
299 UMA_HISTOGRAM_TIMES( | |
300 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB", | |
301 time_to_import); | |
302 } else if (local_storage_size_kb < 1000) { | |
303 UMA_HISTOGRAM_TIMES( | |
304 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB", | |
305 time_to_import); | |
306 } else { | |
307 UMA_HISTOGRAM_TIMES( | |
308 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB", | |
309 time_to_import); | |
310 } | |
311 } | |
312 | |
313 DomStorageArea::CommitBatch* DomStorageArea::CreateCommitBatchIfNeeded() { | |
314 DCHECK(!is_shutdown_); | |
315 if (!commit_batch_) { | |
316 commit_batch_.reset(new CommitBatch()); | |
317 | |
318 // Start a timer to commit any changes that accrue in the batch, but only if | |
319 // no commits are currently in flight. In that case the timer will be | |
320 // started after the commits have happened. | |
321 if (!commit_batches_in_flight_) { | |
322 task_runner_->PostDelayedTask( | |
323 FROM_HERE, | |
324 base::Bind(&DomStorageArea::OnCommitTimer, this), | |
325 base::TimeDelta::FromSeconds(kCommitTimerSeconds)); | |
326 } | |
327 } | |
328 return commit_batch_.get(); | |
329 } | |
330 | |
331 void DomStorageArea::OnCommitTimer() { | |
332 if (is_shutdown_) | |
333 return; | |
334 | |
335 DCHECK(backing_.get()); | |
336 | |
337 // It's possible that there is nothing to commit, since a shallow copy occured | |
338 // before the timer fired. | |
339 if (!commit_batch_) | |
340 return; | |
341 | |
342 // This method executes on the primary sequence, we schedule | |
343 // a task for immediate execution on the commit sequence. | |
344 DCHECK(task_runner_->IsRunningOnPrimarySequence()); | |
345 bool success = task_runner_->PostShutdownBlockingTask( | |
346 FROM_HERE, | |
347 DomStorageTaskRunner::COMMIT_SEQUENCE, | |
348 base::Bind(&DomStorageArea::CommitChanges, this, | |
349 base::Owned(commit_batch_.release()))); | |
350 ++commit_batches_in_flight_; | |
351 DCHECK(success); | |
352 } | |
353 | |
354 void DomStorageArea::CommitChanges(const CommitBatch* commit_batch) { | |
355 // This method executes on the commit sequence. | |
356 DCHECK(task_runner_->IsRunningOnCommitSequence()); | |
357 bool success = backing_->CommitChanges(commit_batch->clear_all_first, | |
358 commit_batch->changed_values); | |
359 DCHECK(success); // TODO(michaeln): what if it fails? | |
360 task_runner_->PostTask( | |
361 FROM_HERE, | |
362 base::Bind(&DomStorageArea::OnCommitComplete, this)); | |
363 } | |
364 | |
365 void DomStorageArea::OnCommitComplete() { | |
366 // We're back on the primary sequence in this method. | |
367 DCHECK(task_runner_->IsRunningOnPrimarySequence()); | |
368 --commit_batches_in_flight_; | |
369 if (is_shutdown_) | |
370 return; | |
371 if (commit_batch_.get() && !commit_batches_in_flight_) { | |
372 // More changes have accrued, restart the timer. | |
373 task_runner_->PostDelayedTask( | |
374 FROM_HERE, | |
375 base::Bind(&DomStorageArea::OnCommitTimer, this), | |
376 base::TimeDelta::FromSeconds(kCommitTimerSeconds)); | |
377 } | |
378 } | |
379 | |
380 void DomStorageArea::ShutdownInCommitSequence() { | |
381 // This method executes on the commit sequence. | |
382 DCHECK(task_runner_->IsRunningOnCommitSequence()); | |
383 DCHECK(backing_.get()); | |
384 if (commit_batch_) { | |
385 // Commit any changes that accrued prior to the timer firing. | |
386 bool success = backing_->CommitChanges( | |
387 commit_batch_->clear_all_first, | |
388 commit_batch_->changed_values); | |
389 DCHECK(success); | |
390 } | |
391 commit_batch_.reset(); | |
392 backing_.reset(); | |
393 session_storage_backing_ = NULL; | |
394 } | |
395 | |
396 } // namespace dom_storage | |
OLD | NEW |