Home | History | Annotate | Download | only in dom_storage
      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