| 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 |