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. 4 5 #include "content/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 "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 "storage/browser/database/database_util.h" 21 #include "storage/common/database/database_identifier.h" 22 #include "storage/common/fileapi/file_system_util.h" 23 24 using storage::DatabaseUtil; 25 26 namespace content { 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 = storage::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 storage::GetOriginFromIdentifier(origin_id); 56 } 57 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), 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(kPerStorageAreaQuota + 87 kPerStorageAreaOverQuotaAllowance)), 88 session_storage_backing_(session_storage_backing), 89 is_initial_import_done_(true), 90 is_shutdown_(false), 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; 97 } 98 } 99 100 DOMStorageArea::~DOMStorageArea() { 101 } 102 103 void DOMStorageArea::ExtractValues(DOMStorageValuesMap* map) { 104 if (is_shutdown_) 105 return; 106 InitialImportIfNeeded(); 107 map_->ExtractValues(map); 108 } 109 110 unsigned DOMStorageArea::Length() { 111 if (is_shutdown_) 112 return 0; 113 InitialImportIfNeeded(); 114 return map_->Length(); 115 } 116 117 base::NullableString16 DOMStorageArea::Key(unsigned index) { 118 if (is_shutdown_) 119 return base::NullableString16(); 120 InitialImportIfNeeded(); 121 return map_->Key(index); 122 } 123 124 base::NullableString16 DOMStorageArea::GetItem(const base::string16& key) { 125 if (is_shutdown_) 126 return base::NullableString16(); 127 InitialImportIfNeeded(); 128 return map_->GetItem(key); 129 } 130 131 bool DOMStorageArea::SetItem(const base::string16& key, 132 const base::string16& value, 133 base::NullableString16* old_value) { 134 if (is_shutdown_) 135 return false; 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); 143 } 144 return success; 145 } 146 147 bool DOMStorageArea::RemoveItem(const base::string16& key, 148 base::string16* old_value) { 149 if (is_shutdown_) 150 return false; 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(); 158 } 159 return success; 160 } 161 162 bool DOMStorageArea::Clear() { 163 if (is_shutdown_) 164 return false; 165 InitialImportIfNeeded(); 166 if (map_->Length() == 0) 167 return false; 168 169 map_ = new DOMStorageMap(kPerStorageAreaQuota + 170 kPerStorageAreaOverQuotaAllowance); 171 172 if (backing_) { 173 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); 174 commit_batch->clear_all_first = true; 175 commit_batch->changed_values.clear(); 176 } 177 178 return true; 179 } 180 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. 185 if (is_shutdown_) 186 return; 187 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; 193 194 if (backing_) { 195 CommitBatch* commit_batch = CreateCommitBatchIfNeeded(); 196 commit_batch->clear_all_first = true; 197 commit_batch->changed_values.clear(); 198 } 199 } 200 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); 206 207 DOMStorageArea* copy = new DOMStorageArea( 208 destination_namespace_id, destination_persistent_namespace_id, origin_, 209 session_storage_backing_.get(), task_runner_.get()); 210 copy->map_ = map_; 211 copy->is_shutdown_ = is_shutdown_; 212 copy->is_initial_import_done_ = true; 213 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 217 // when it fires. 218 if (commit_batch_) 219 OnCommitTimer(); 220 return copy; 221 } 222 223 bool DOMStorageArea::HasUncommittedChanges() const { 224 DCHECK(!is_shutdown_); 225 return commit_batch_.get() || commit_batches_in_flight_; 226 } 227 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 237 // quickly. 238 Clear(); 239 return; 240 } 241 map_ = new DOMStorageMap(kPerStorageAreaQuota + 242 kPerStorageAreaOverQuotaAllowance); 243 if (backing_) { 244 is_initial_import_done_ = false; 245 backing_->Reset(); 246 backing_->DeleteFiles(); 247 } 248 } 249 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. 257 return; 258 259 // Drop the in memory cache, we'll reload when needed. 260 is_initial_import_done_ = false; 261 map_ = new DOMStorageMap(kPerStorageAreaQuota + 262 kPerStorageAreaOverQuotaAllowance); 263 264 // Recreate the database object, this frees up the open sqlite connection 265 // and its page cache. 266 backing_->Reset(); 267 } 268 269 void DOMStorageArea::Shutdown() { 270 DCHECK(!is_shutdown_); 271 is_shutdown_ = true; 272 map_ = NULL; 273 if (!backing_) 274 return; 275 276 bool success = task_runner_->PostShutdownBlockingTask( 277 FROM_HERE, 278 DOMStorageTaskRunner::COMMIT_SEQUENCE, 279 base::Bind(&DOMStorageArea::ShutdownInCommitSequence, this)); 280 DCHECK(success); 281 } 282 283 void DOMStorageArea::InitialImportIfNeeded() { 284 if (is_initial_import_done_) 285 return; 286 287 DCHECK(backing_.get()); 288 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", 296 time_to_import); 297 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, 304 0, 6 * 1024, 50); 305 if (local_storage_size_kb < 100) { 306 UMA_HISTOGRAM_TIMES( 307 "LocalStorage.BrowserTimeToPrimeLocalStorageUnder100KB", 308 time_to_import); 309 } else if (local_storage_size_kb < 1000) { 310 UMA_HISTOGRAM_TIMES( 311 "LocalStorage.BrowserTimeToPrimeLocalStorage100KBTo1MB", 312 time_to_import); 313 } else { 314 UMA_HISTOGRAM_TIMES( 315 "LocalStorage.BrowserTimeToPrimeLocalStorage1MBTo5MB", 316 time_to_import); 317 } 318 } 319 320 DOMStorageArea::CommitBatch* DOMStorageArea::CreateCommitBatchIfNeeded() { 321 DCHECK(!is_shutdown_); 322 if (!commit_batch_) { 323 commit_batch_.reset(new CommitBatch()); 324 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( 330 FROM_HERE, 331 base::Bind(&DOMStorageArea::OnCommitTimer, this), 332 base::TimeDelta::FromSeconds(kCommitTimerSeconds)); 333 } 334 } 335 return commit_batch_.get(); 336 } 337 338 void DOMStorageArea::OnCommitTimer() { 339 if (is_shutdown_) 340 return; 341 342 DCHECK(backing_.get()); 343 344 // It's possible that there is nothing to commit, since a shallow copy occured 345 // before the timer fired. 346 if (!commit_batch_) 347 return; 348 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( 353 FROM_HERE, 354 DOMStorageTaskRunner::COMMIT_SEQUENCE, 355 base::Bind(&DOMStorageArea::CommitChanges, this, 356 base::Owned(commit_batch_.release()))); 357 ++commit_batches_in_flight_; 358 DCHECK(success); 359 } 360 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( 369 FROM_HERE, 370 base::Bind(&DOMStorageArea::OnCommitComplete, this)); 371 } 372 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_; 377 if (is_shutdown_) 378 return; 379 if (commit_batch_.get() && !commit_batches_in_flight_) { 380 // More changes have accrued, restart the timer. 381 task_runner_->PostDelayedTask( 382 FROM_HERE, 383 base::Bind(&DOMStorageArea::OnCommitTimer, this), 384 base::TimeDelta::FromSeconds(kCommitTimerSeconds)); 385 } 386 } 387 388 void DOMStorageArea::ShutdownInCommitSequence() { 389 // This method executes on the commit sequence. 390 DCHECK(task_runner_->IsRunningOnCommitSequence()); 391 DCHECK(backing_.get()); 392 if (commit_batch_) { 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); 397 DCHECK(success); 398 } 399 commit_batch_.reset(); 400 backing_.reset(); 401 session_storage_backing_ = NULL; 402 } 403 404 } // namespace content 405