1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "LocalStorageCache.h"
10 #include "StorageDBThread.h"
11 #include "StorageIPC.h"
12 #include "StorageUtils.h"
13 #include "LocalStorageManager.h"
15 #include "nsDOMString.h"
16 #include "nsXULAppAPI.h"
17 #include "mozilla/Unused.h"
18 #include "nsProxyRelease.h"
19 #include "nsThreadUtils.h"
21 namespace mozilla::dom
{
23 #define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000
27 const uint32_t kDefaultSet
= 0;
28 const uint32_t kSessionSet
= 1;
30 inline uint32_t GetDataSetIndex(bool aPrivateBrowsing
,
31 bool aSessionScopedOrLess
) {
32 if (!aPrivateBrowsing
&& aSessionScopedOrLess
) {
39 inline uint32_t GetDataSetIndex(const LocalStorage
* aStorage
) {
40 // A session only mode doesn't exist anymore, so having a separate data set
41 // for it here is basically useless. This code is only kept until we remove
42 // the old / legacy LocalStorage implementation.
43 return GetDataSetIndex(aStorage
->IsPrivateBrowsing(),
44 aStorage
->IsPrivateBrowsingOrLess());
49 // LocalStorageCacheBridge
51 NS_IMPL_ADDREF(LocalStorageCacheBridge
)
53 // Since there is no consumer of return value of Release, we can turn this
54 // method to void to make implementation of asynchronous
55 // LocalStorageCache::Release much simpler.
56 NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) {
57 MOZ_ASSERT(int32_t(mRefCnt
) > 0, "dup release");
58 nsrefcnt count
= --mRefCnt
;
59 NS_LOG_RELEASE(this, count
, "LocalStorageCacheBridge");
61 mRefCnt
= 1; /* stabilize */
62 /* enable this to find non-threadsafe destructors: */
63 /* NS_ASSERT_OWNINGTHREAD(_class); */
70 LocalStorageCache::LocalStorageCache(const nsACString
* aOriginNoSuffix
)
72 mOriginNoSuffix(*aOriginNoSuffix
),
73 mMonitor("LocalStorageCache"),
78 mPreloadTelemetryRecorded(false) {
79 MOZ_COUNT_CTOR(LocalStorageCache
);
82 LocalStorageCache::~LocalStorageCache() {
84 mActor
->SendDeleteMeInternal();
85 MOZ_ASSERT(!mActor
, "SendDeleteMeInternal should have cleared!");
89 mManager
->DropCache(this);
92 MOZ_COUNT_DTOR(LocalStorageCache
);
95 void LocalStorageCache::SetActor(LocalStorageCacheChild
* aActor
) {
96 AssertIsOnOwningThread();
104 LocalStorageCache::Release(void) {
105 // We must actually release on the main thread since the cache removes it
106 // self from the manager's hash table. And we don't want to lock access to
108 if (NS_IsMainThread()) {
109 LocalStorageCacheBridge::Release();
113 RefPtr
<nsRunnableMethod
<LocalStorageCacheBridge
, void, false>> event
=
114 NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release",
115 static_cast<LocalStorageCacheBridge
*>(this),
116 &LocalStorageCacheBridge::Release
);
118 nsresult rv
= NS_DispatchToMainThread(event
);
120 NS_WARNING("LocalStorageCache::Release() on a non-main thread");
121 LocalStorageCacheBridge::Release();
125 void LocalStorageCache::Init(LocalStorageManager
* aManager
, bool aPersistent
,
126 nsIPrincipal
* aPrincipal
,
127 const nsACString
& aQuotaOriginScope
) {
128 MOZ_ASSERT(!aQuotaOriginScope
.IsEmpty());
135 aPrincipal
->OriginAttributesRef().CreateSuffix(mOriginSuffix
);
136 mPrivateBrowsingId
= aPrincipal
->GetPrivateBrowsingId();
137 mPersistent
= aPersistent
;
138 mQuotaOriginScope
= aQuotaOriginScope
;
145 // Check the quota string has (or has not) the identical origin suffix as
146 // this storage cache is bound to.
147 MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope
, mOriginSuffix
));
148 MOZ_ASSERT(mOriginSuffix
.IsEmpty() !=
149 StringBeginsWith(mQuotaOriginScope
, "^"_ns
));
151 mUsage
= aManager
->GetOriginUsage(mQuotaOriginScope
, mPrivateBrowsingId
);
154 void LocalStorageCache::NotifyObservers(const LocalStorage
* aStorage
,
155 const nsAString
& aKey
,
156 const nsAString
& aOldValue
,
157 const nsAString
& aNewValue
) {
158 AssertIsOnOwningThread();
159 MOZ_ASSERT(aStorage
);
165 // We want to send a message to the parent in order to broadcast the
166 // StorageEvent correctly to any child process.
168 Unused
<< mActor
->SendNotify(aStorage
->DocumentURI(), aKey
, aOldValue
,
172 inline bool LocalStorageCache::Persist(const LocalStorage
* aStorage
) const {
173 return mPersistent
&& (aStorage
->IsPrivateBrowsing() ||
174 !aStorage
->IsPrivateBrowsingOrLess());
177 const nsCString
LocalStorageCache::Origin() const {
178 return LocalStorageManager::CreateOrigin(mOriginSuffix
, mOriginNoSuffix
);
181 LocalStorageCache::Data
& LocalStorageCache::DataSet(
182 const LocalStorage
* aStorage
) {
183 return mData
[GetDataSetIndex(aStorage
)];
186 bool LocalStorageCache::ProcessUsageDelta(const LocalStorage
* aStorage
,
188 const MutationSource aSource
) {
189 return ProcessUsageDelta(GetDataSetIndex(aStorage
), aDelta
, aSource
);
192 bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex
,
193 const int64_t aDelta
,
194 const MutationSource aSource
) {
195 // Check limit per this origin
196 Data
& data
= mData
[aGetDataSetIndex
];
197 uint64_t newOriginUsage
= data
.mOriginQuotaUsage
+ aDelta
;
198 if (aSource
== ContentMutation
&& aDelta
> 0 &&
199 newOriginUsage
> LocalStorageManager::GetOriginQuota()) {
203 // Now check eTLD+1 limit
205 !mUsage
->CheckAndSetETLD1UsageDelta(aGetDataSetIndex
, aDelta
, aSource
)) {
209 // Update size in our data set
210 data
.mOriginQuotaUsage
= newOriginUsage
;
214 void LocalStorageCache::Preload() {
215 if (mLoaded
|| !mPersistent
) {
219 StorageDBChild
* storageChild
=
220 StorageDBChild::GetOrCreate(mPrivateBrowsingId
);
223 mLoadResult
= NS_ERROR_FAILURE
;
227 storageChild
->AsyncPreload(this);
230 void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID
) {
235 bool loaded
= mLoaded
;
237 // Telemetry of rates of pending preloads
238 if (!mPreloadTelemetryRecorded
) {
239 mPreloadTelemetryRecorded
= true;
240 Telemetry::Accumulate(
241 Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS
, !loaded
);
248 // Measure which operation blocks and for how long
249 Telemetry::RuntimeAutoTimer
timer(aTelemetryID
);
251 // If preload already started (i.e. we got some first data, but not all)
252 // SyncPreload will just wait for it to finish rather then synchronously
253 // read from the database. It seems to me more optimal.
255 // TODO place for A/B testing (force main thread load vs. let preload finish)
257 // No need to check sDatabase for being non-null since preload is either
258 // done before we've shut the DB down or when the DB could not start,
259 // preload has not even be started.
260 StorageDBChild::Get(mPrivateBrowsingId
)->SyncPreload(this);
263 nsresult
LocalStorageCache::GetLength(const LocalStorage
* aStorage
,
265 if (Persist(aStorage
)) {
266 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS
);
267 if (NS_FAILED(mLoadResult
)) {
272 *aRetval
= DataSet(aStorage
).mKeys
.Count();
276 nsresult
LocalStorageCache::GetKey(const LocalStorage
* aStorage
,
277 uint32_t aIndex
, nsAString
& aRetval
) {
278 // XXX: This does a linear search for the key at index, which would
279 // suck if there's a large numer of indexes. Do we care? If so,
280 // maybe we need to have a lazily populated key array here or
282 if (Persist(aStorage
)) {
283 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS
);
284 if (NS_FAILED(mLoadResult
)) {
289 aRetval
.SetIsVoid(true);
290 for (auto iter
= DataSet(aStorage
).mKeys
.Iter(); !iter
.Done(); iter
.Next()) {
292 aRetval
= iter
.Key();
301 void LocalStorageCache::GetKeys(const LocalStorage
* aStorage
,
302 nsTArray
<nsString
>& aKeys
) {
303 if (Persist(aStorage
)) {
304 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS
);
307 if (NS_FAILED(mLoadResult
)) {
311 AppendToArray(aKeys
, DataSet(aStorage
).mKeys
.Keys());
314 nsresult
LocalStorageCache::GetItem(const LocalStorage
* aStorage
,
315 const nsAString
& aKey
, nsAString
& aRetval
) {
316 if (Persist(aStorage
)) {
317 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS
);
318 if (NS_FAILED(mLoadResult
)) {
323 // not using AutoString since we don't want to copy buffer to result
325 if (!DataSet(aStorage
).mKeys
.Get(aKey
, &value
)) {
326 SetDOMStringToNull(value
);
334 nsresult
LocalStorageCache::SetItem(const LocalStorage
* aStorage
,
335 const nsAString
& aKey
,
336 const nsAString
& aValue
, nsString
& aOld
,
337 const MutationSource aSource
) {
338 // Size of the cache that will change after this action.
341 if (Persist(aStorage
)) {
342 WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS
);
343 if (NS_FAILED(mLoadResult
)) {
348 Data
& data
= DataSet(aStorage
);
349 if (!data
.mKeys
.Get(aKey
, &aOld
)) {
350 SetDOMStringToNull(aOld
);
352 // We only consider key size if the key doesn't exist before.
353 delta
+= static_cast<int64_t>(aKey
.Length());
356 delta
+= static_cast<int64_t>(aValue
.Length()) -
357 static_cast<int64_t>(aOld
.Length());
359 if (!ProcessUsageDelta(aStorage
, delta
, aSource
)) {
360 return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR
;
363 if (aValue
== aOld
&& DOMStringIsNull(aValue
) == DOMStringIsNull(aOld
)) {
364 return NS_SUCCESS_DOM_NO_OPERATION
;
367 data
.mKeys
.InsertOrUpdate(aKey
, aValue
);
369 if (aSource
!= ContentMutation
) {
373 #if !defined(MOZ_WIDGET_ANDROID)
374 NotifyObservers(aStorage
, aKey
, aOld
, aValue
);
377 if (Persist(aStorage
)) {
378 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
381 "Writing to localStorage after the database has been shut down"
383 return NS_ERROR_NOT_INITIALIZED
;
386 if (DOMStringIsNull(aOld
)) {
387 return storageChild
->AsyncAddItem(this, aKey
, aValue
);
390 return storageChild
->AsyncUpdateItem(this, aKey
, aValue
);
396 nsresult
LocalStorageCache::RemoveItem(const LocalStorage
* aStorage
,
397 const nsAString
& aKey
, nsString
& aOld
,
398 const MutationSource aSource
) {
399 if (Persist(aStorage
)) {
400 WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS
);
401 if (NS_FAILED(mLoadResult
)) {
406 Data
& data
= DataSet(aStorage
);
407 if (!data
.mKeys
.Get(aKey
, &aOld
)) {
408 SetDOMStringToNull(aOld
);
409 return NS_SUCCESS_DOM_NO_OPERATION
;
412 // Recalculate the cached data size
413 const int64_t delta
= -(static_cast<int64_t>(aOld
.Length()) +
414 static_cast<int64_t>(aKey
.Length()));
415 Unused
<< ProcessUsageDelta(aStorage
, delta
, aSource
);
416 data
.mKeys
.Remove(aKey
);
418 if (aSource
!= ContentMutation
) {
422 #if !defined(MOZ_WIDGET_ANDROID)
423 NotifyObservers(aStorage
, aKey
, aOld
, VoidString());
426 if (Persist(aStorage
)) {
427 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
430 "Writing to localStorage after the database has been shut down"
432 return NS_ERROR_NOT_INITIALIZED
;
435 return storageChild
->AsyncRemoveItem(this, aKey
);
441 nsresult
LocalStorageCache::Clear(const LocalStorage
* aStorage
,
442 const MutationSource aSource
) {
443 bool refresh
= false;
444 if (Persist(aStorage
)) {
445 // We need to preload all data (know the size) before we can proceeed
446 // to correctly decrease cached usage number.
447 // XXX as in case of unload, this is not technically needed now, but
448 // after super-scope quota introduction we have to do this. Get telemetry
450 WaitForPreload(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS
);
451 if (NS_FAILED(mLoadResult
)) {
452 // When we failed to load data from the database, force delete of the
453 // scope data and make use of the storage possible again.
459 Data
& data
= DataSet(aStorage
);
460 bool hadData
= !!data
.mKeys
.Count();
463 Unused
<< ProcessUsageDelta(aStorage
, -data
.mOriginQuotaUsage
, aSource
);
467 if (aSource
!= ContentMutation
) {
468 return hadData
? NS_OK
: NS_SUCCESS_DOM_NO_OPERATION
;
471 #if !defined(MOZ_WIDGET_ANDROID)
473 NotifyObservers(aStorage
, VoidString(), VoidString(), VoidString());
477 if (Persist(aStorage
) && (refresh
|| hadData
)) {
478 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
481 "Writing to localStorage after the database has been shut down"
483 return NS_ERROR_NOT_INITIALIZED
;
486 return storageChild
->AsyncClear(this);
489 return hadData
? NS_OK
: NS_SUCCESS_DOM_NO_OPERATION
;
492 int64_t LocalStorageCache::GetOriginQuotaUsage(
493 const LocalStorage
* aStorage
) const {
494 return mData
[GetDataSetIndex(aStorage
)].mOriginQuotaUsage
;
497 void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags
) {
498 if (aUnloadFlags
& kUnloadDefault
) {
499 // Must wait for preload to pass correct usage to ProcessUsageDelta
500 // XXX this is not technically needed right now since there is just
501 // per-origin isolated quota handling, but when we introduce super-
502 // -scope quotas, we have to do this. Better to start getting
503 // telemetry right now.
504 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS
);
506 mData
[kDefaultSet
].mKeys
.Clear();
507 ProcessUsageDelta(kDefaultSet
, -mData
[kDefaultSet
].mOriginQuotaUsage
);
510 if (aUnloadFlags
& kUnloadSession
) {
511 mData
[kSessionSet
].mKeys
.Clear();
512 ProcessUsageDelta(kSessionSet
, -mData
[kSessionSet
].mOriginQuotaUsage
);
515 #ifdef DOM_STORAGE_TESTS
516 if (aUnloadFlags
& kTestReload
) {
517 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS
);
519 mData
[kDefaultSet
].mKeys
.Clear();
520 mLoaded
= false; // This is only used in testing code
526 // LocalStorageCacheBridge
528 uint32_t LocalStorageCache::LoadedCount() {
529 MonitorAutoLock
monitor(mMonitor
);
530 Data
& data
= mData
[kDefaultSet
];
531 return data
.mKeys
.Count();
534 bool LocalStorageCache::LoadItem(const nsAString
& aKey
,
535 const nsAString
& aValue
) {
536 MonitorAutoLock
monitor(mMonitor
);
541 Data
& data
= mData
[kDefaultSet
];
542 data
.mKeys
.LookupOrInsertWith(aKey
, [&] {
543 data
.mOriginQuotaUsage
+= aKey
.Length() + aValue
.Length();
544 return nsString(aValue
);
549 void LocalStorageCache::LoadDone(nsresult aRv
) {
550 MonitorAutoLock
monitor(mMonitor
);
556 void LocalStorageCache::LoadWait() {
557 MonitorAutoLock
monitor(mMonitor
);
565 StorageUsage::StorageUsage(const nsACString
& aOriginScope
)
566 : mOriginScope(aOriginScope
) {
567 mUsage
[kDefaultSet
] = mUsage
[kSessionSet
] = 0LL;
572 class LoadUsageRunnable
: public Runnable
{
574 LoadUsageRunnable(int64_t* aUsage
, const int64_t aDelta
)
575 : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage
), mDelta(aDelta
) {}
581 NS_IMETHOD
Run() override
{
589 void StorageUsage::LoadUsage(const int64_t aUsage
) {
590 // Using kDefaultSet index since it is the index for the persitent data
591 // stored in the database we have just loaded usage for.
592 if (!NS_IsMainThread()) {
593 // In single process scenario we get this call from the DB thread
594 RefPtr
<LoadUsageRunnable
> r
=
595 new LoadUsageRunnable(mUsage
+ kDefaultSet
, aUsage
);
596 NS_DispatchToMainThread(r
);
598 // On a child process we get this on the main thread already
599 mUsage
[kDefaultSet
] += aUsage
;
603 bool StorageUsage::CheckAndSetETLD1UsageDelta(
604 uint32_t aDataSetIndex
, const int64_t aDelta
,
605 const LocalStorageCache::MutationSource aSource
) {
606 MOZ_ASSERT(NS_IsMainThread());
608 int64_t newUsage
= mUsage
[aDataSetIndex
] + aDelta
;
609 if (aSource
== LocalStorageCache::ContentMutation
&& aDelta
> 0 &&
610 newUsage
> LocalStorageManager::GetSiteQuota()) {
614 mUsage
[aDataSetIndex
] = newUsage
;
618 } // namespace mozilla::dom