• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  
CommitBatch()30  DOMStorageArea::CommitBatch::CommitBatch()
31    : clear_all_first(false) {
32  }
~CommitBatch()33  DOMStorageArea::CommitBatch::~CommitBatch() {}
34  
35  
36  // static
37  const base::FilePath::CharType DOMStorageArea::kDatabaseFileExtension[] =
38      FILE_PATH_LITERAL(".localstorage");
39  
40  // static
DatabaseFileNameFromOrigin(const GURL & origin)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
OriginFromDatabaseFileName(const base::FilePath & name)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  
DOMStorageArea(const GURL & origin,const base::FilePath & directory,DOMStorageTaskRunner * task_runner)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  
DOMStorageArea(int64 namespace_id,const std::string & persistent_namespace_id,const GURL & origin,SessionStorageDatabase * session_storage_backing,DOMStorageTaskRunner * task_runner)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  
~DOMStorageArea()100  DOMStorageArea::~DOMStorageArea() {
101  }
102  
ExtractValues(DOMStorageValuesMap * map)103  void DOMStorageArea::ExtractValues(DOMStorageValuesMap* map) {
104    if (is_shutdown_)
105      return;
106    InitialImportIfNeeded();
107    map_->ExtractValues(map);
108  }
109  
Length()110  unsigned DOMStorageArea::Length() {
111    if (is_shutdown_)
112      return 0;
113    InitialImportIfNeeded();
114    return map_->Length();
115  }
116  
Key(unsigned index)117  base::NullableString16 DOMStorageArea::Key(unsigned index) {
118    if (is_shutdown_)
119      return base::NullableString16();
120    InitialImportIfNeeded();
121    return map_->Key(index);
122  }
123  
GetItem(const base::string16 & key)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  
SetItem(const base::string16 & key,const base::string16 & value,base::NullableString16 * old_value)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  
RemoveItem(const base::string16 & key,base::string16 * old_value)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  
Clear()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  
FastClear()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  
ShallowCopy(int64 destination_namespace_id,const std::string & destination_persistent_namespace_id)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  
HasUncommittedChanges() const223  bool DOMStorageArea::HasUncommittedChanges() const {
224    DCHECK(!is_shutdown_);
225    return commit_batch_.get() || commit_batches_in_flight_;
226  }
227  
DeleteOrigin()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  
PurgeMemory()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  
Shutdown()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  
InitialImportIfNeeded()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  
CreateCommitBatchIfNeeded()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  
OnCommitTimer()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  
CommitChanges(const CommitBatch * commit_batch)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  
OnCommitComplete()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  
ShutdownInCommitSequence()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