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