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 return GetDataSetIndex(aStorage
->IsPrivateBrowsing(),
41 aStorage
->IsSessionScopedOrLess());
46 // LocalStorageCacheBridge
48 NS_IMPL_ADDREF(LocalStorageCacheBridge
)
50 // Since there is no consumer of return value of Release, we can turn this
51 // method to void to make implementation of asynchronous
52 // LocalStorageCache::Release much simpler.
53 NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) {
54 MOZ_ASSERT(int32_t(mRefCnt
) > 0, "dup release");
55 nsrefcnt count
= --mRefCnt
;
56 NS_LOG_RELEASE(this, count
, "LocalStorageCacheBridge");
58 mRefCnt
= 1; /* stabilize */
59 /* enable this to find non-threadsafe destructors: */
60 /* NS_ASSERT_OWNINGTHREAD(_class); */
67 LocalStorageCache::LocalStorageCache(const nsACString
* aOriginNoSuffix
)
69 mOriginNoSuffix(*aOriginNoSuffix
),
70 mMonitor("LocalStorageCache"),
75 mPreloadTelemetryRecorded(false) {
76 MOZ_COUNT_CTOR(LocalStorageCache
);
79 LocalStorageCache::~LocalStorageCache() {
81 mActor
->SendDeleteMeInternal();
82 MOZ_ASSERT(!mActor
, "SendDeleteMeInternal should have cleared!");
86 mManager
->DropCache(this);
89 MOZ_COUNT_DTOR(LocalStorageCache
);
92 void LocalStorageCache::SetActor(LocalStorageCacheChild
* aActor
) {
93 AssertIsOnOwningThread();
101 LocalStorageCache::Release(void) {
102 // We must actually release on the main thread since the cache removes it
103 // self from the manager's hash table. And we don't want to lock access to
105 if (NS_IsMainThread()) {
106 LocalStorageCacheBridge::Release();
110 RefPtr
<nsRunnableMethod
<LocalStorageCacheBridge
, void, false>> event
=
111 NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release",
112 static_cast<LocalStorageCacheBridge
*>(this),
113 &LocalStorageCacheBridge::Release
);
115 nsresult rv
= NS_DispatchToMainThread(event
);
117 NS_WARNING("LocalStorageCache::Release() on a non-main thread");
118 LocalStorageCacheBridge::Release();
122 void LocalStorageCache::Init(LocalStorageManager
* aManager
, bool aPersistent
,
123 nsIPrincipal
* aPrincipal
,
124 const nsACString
& aQuotaOriginScope
) {
125 MOZ_ASSERT(!aQuotaOriginScope
.IsEmpty());
132 aPrincipal
->OriginAttributesRef().CreateSuffix(mOriginSuffix
);
133 mPrivateBrowsingId
= aPrincipal
->GetPrivateBrowsingId();
134 mPersistent
= aPersistent
;
135 mQuotaOriginScope
= aQuotaOriginScope
;
142 // Check the quota string has (or has not) the identical origin suffix as
143 // this storage cache is bound to.
144 MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope
, mOriginSuffix
));
145 MOZ_ASSERT(mOriginSuffix
.IsEmpty() !=
146 StringBeginsWith(mQuotaOriginScope
, "^"_ns
));
148 mUsage
= aManager
->GetOriginUsage(mQuotaOriginScope
, mPrivateBrowsingId
);
151 void LocalStorageCache::NotifyObservers(const LocalStorage
* aStorage
,
152 const nsAString
& aKey
,
153 const nsAString
& aOldValue
,
154 const nsAString
& aNewValue
) {
155 AssertIsOnOwningThread();
156 MOZ_ASSERT(aStorage
);
162 // We want to send a message to the parent in order to broadcast the
163 // StorageEvent correctly to any child process.
165 Unused
<< mActor
->SendNotify(aStorage
->DocumentURI(), aKey
, aOldValue
,
169 inline bool LocalStorageCache::Persist(const LocalStorage
* aStorage
) const {
170 return mPersistent
&&
171 (aStorage
->IsPrivateBrowsing() || !aStorage
->IsSessionScopedOrLess());
174 const nsCString
LocalStorageCache::Origin() const {
175 return LocalStorageManager::CreateOrigin(mOriginSuffix
, mOriginNoSuffix
);
178 LocalStorageCache::Data
& LocalStorageCache::DataSet(
179 const LocalStorage
* aStorage
) {
180 return mData
[GetDataSetIndex(aStorage
)];
183 bool LocalStorageCache::ProcessUsageDelta(const LocalStorage
* aStorage
,
185 const MutationSource aSource
) {
186 return ProcessUsageDelta(GetDataSetIndex(aStorage
), aDelta
, aSource
);
189 bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex
,
190 const int64_t aDelta
,
191 const MutationSource aSource
) {
192 // Check limit per this origin
193 Data
& data
= mData
[aGetDataSetIndex
];
194 uint64_t newOriginUsage
= data
.mOriginQuotaUsage
+ aDelta
;
195 if (aSource
== ContentMutation
&& aDelta
> 0 &&
196 newOriginUsage
> LocalStorageManager::GetOriginQuota()) {
200 // Now check eTLD+1 limit
202 !mUsage
->CheckAndSetETLD1UsageDelta(aGetDataSetIndex
, aDelta
, aSource
)) {
206 // Update size in our data set
207 data
.mOriginQuotaUsage
= newOriginUsage
;
211 void LocalStorageCache::Preload() {
212 if (mLoaded
|| !mPersistent
) {
216 StorageDBChild
* storageChild
=
217 StorageDBChild::GetOrCreate(mPrivateBrowsingId
);
220 mLoadResult
= NS_ERROR_FAILURE
;
224 storageChild
->AsyncPreload(this);
227 void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID
) {
232 bool loaded
= mLoaded
;
234 // Telemetry of rates of pending preloads
235 if (!mPreloadTelemetryRecorded
) {
236 mPreloadTelemetryRecorded
= true;
237 Telemetry::Accumulate(
238 Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS
, !loaded
);
245 // Measure which operation blocks and for how long
246 Telemetry::RuntimeAutoTimer
timer(aTelemetryID
);
248 // If preload already started (i.e. we got some first data, but not all)
249 // SyncPreload will just wait for it to finish rather then synchronously
250 // read from the database. It seems to me more optimal.
252 // TODO place for A/B testing (force main thread load vs. let preload finish)
254 // No need to check sDatabase for being non-null since preload is either
255 // done before we've shut the DB down or when the DB could not start,
256 // preload has not even be started.
257 StorageDBChild::Get(mPrivateBrowsingId
)->SyncPreload(this);
260 nsresult
LocalStorageCache::GetLength(const LocalStorage
* aStorage
,
262 if (Persist(aStorage
)) {
263 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS
);
264 if (NS_FAILED(mLoadResult
)) {
269 *aRetval
= DataSet(aStorage
).mKeys
.Count();
273 nsresult
LocalStorageCache::GetKey(const LocalStorage
* aStorage
,
274 uint32_t aIndex
, nsAString
& aRetval
) {
275 // XXX: This does a linear search for the key at index, which would
276 // suck if there's a large numer of indexes. Do we care? If so,
277 // maybe we need to have a lazily populated key array here or
279 if (Persist(aStorage
)) {
280 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS
);
281 if (NS_FAILED(mLoadResult
)) {
286 aRetval
.SetIsVoid(true);
287 for (auto iter
= DataSet(aStorage
).mKeys
.Iter(); !iter
.Done(); iter
.Next()) {
289 aRetval
= iter
.Key();
298 void LocalStorageCache::GetKeys(const LocalStorage
* aStorage
,
299 nsTArray
<nsString
>& aKeys
) {
300 if (Persist(aStorage
)) {
301 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS
);
304 if (NS_FAILED(mLoadResult
)) {
308 AppendToArray(aKeys
, DataSet(aStorage
).mKeys
.Keys());
311 nsresult
LocalStorageCache::GetItem(const LocalStorage
* aStorage
,
312 const nsAString
& aKey
, nsAString
& aRetval
) {
313 if (Persist(aStorage
)) {
314 WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS
);
315 if (NS_FAILED(mLoadResult
)) {
320 // not using AutoString since we don't want to copy buffer to result
322 if (!DataSet(aStorage
).mKeys
.Get(aKey
, &value
)) {
323 SetDOMStringToNull(value
);
331 nsresult
LocalStorageCache::SetItem(const LocalStorage
* aStorage
,
332 const nsAString
& aKey
,
333 const nsAString
& aValue
, nsString
& aOld
,
334 const MutationSource aSource
) {
335 // Size of the cache that will change after this action.
338 if (Persist(aStorage
)) {
339 WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS
);
340 if (NS_FAILED(mLoadResult
)) {
345 Data
& data
= DataSet(aStorage
);
346 if (!data
.mKeys
.Get(aKey
, &aOld
)) {
347 SetDOMStringToNull(aOld
);
349 // We only consider key size if the key doesn't exist before.
350 delta
+= static_cast<int64_t>(aKey
.Length());
353 delta
+= static_cast<int64_t>(aValue
.Length()) -
354 static_cast<int64_t>(aOld
.Length());
356 if (!ProcessUsageDelta(aStorage
, delta
, aSource
)) {
357 return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR
;
360 if (aValue
== aOld
&& DOMStringIsNull(aValue
) == DOMStringIsNull(aOld
)) {
361 return NS_SUCCESS_DOM_NO_OPERATION
;
364 data
.mKeys
.InsertOrUpdate(aKey
, aValue
);
366 if (aSource
!= ContentMutation
) {
370 #if !defined(MOZ_WIDGET_ANDROID)
371 NotifyObservers(aStorage
, aKey
, aOld
, aValue
);
374 if (Persist(aStorage
)) {
375 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
378 "Writing to localStorage after the database has been shut down"
380 return NS_ERROR_NOT_INITIALIZED
;
383 if (DOMStringIsNull(aOld
)) {
384 return storageChild
->AsyncAddItem(this, aKey
, aValue
);
387 return storageChild
->AsyncUpdateItem(this, aKey
, aValue
);
393 nsresult
LocalStorageCache::RemoveItem(const LocalStorage
* aStorage
,
394 const nsAString
& aKey
, nsString
& aOld
,
395 const MutationSource aSource
) {
396 if (Persist(aStorage
)) {
397 WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS
);
398 if (NS_FAILED(mLoadResult
)) {
403 Data
& data
= DataSet(aStorage
);
404 if (!data
.mKeys
.Get(aKey
, &aOld
)) {
405 SetDOMStringToNull(aOld
);
406 return NS_SUCCESS_DOM_NO_OPERATION
;
409 // Recalculate the cached data size
410 const int64_t delta
= -(static_cast<int64_t>(aOld
.Length()) +
411 static_cast<int64_t>(aKey
.Length()));
412 Unused
<< ProcessUsageDelta(aStorage
, delta
, aSource
);
413 data
.mKeys
.Remove(aKey
);
415 if (aSource
!= ContentMutation
) {
419 #if !defined(MOZ_WIDGET_ANDROID)
420 NotifyObservers(aStorage
, aKey
, aOld
, VoidString());
423 if (Persist(aStorage
)) {
424 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
427 "Writing to localStorage after the database has been shut down"
429 return NS_ERROR_NOT_INITIALIZED
;
432 return storageChild
->AsyncRemoveItem(this, aKey
);
438 nsresult
LocalStorageCache::Clear(const LocalStorage
* aStorage
,
439 const MutationSource aSource
) {
440 bool refresh
= false;
441 if (Persist(aStorage
)) {
442 // We need to preload all data (know the size) before we can proceeed
443 // to correctly decrease cached usage number.
444 // XXX as in case of unload, this is not technically needed now, but
445 // after super-scope quota introduction we have to do this. Get telemetry
447 WaitForPreload(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS
);
448 if (NS_FAILED(mLoadResult
)) {
449 // When we failed to load data from the database, force delete of the
450 // scope data and make use of the storage possible again.
456 Data
& data
= DataSet(aStorage
);
457 bool hadData
= !!data
.mKeys
.Count();
460 Unused
<< ProcessUsageDelta(aStorage
, -data
.mOriginQuotaUsage
, aSource
);
464 if (aSource
!= ContentMutation
) {
465 return hadData
? NS_OK
: NS_SUCCESS_DOM_NO_OPERATION
;
468 #if !defined(MOZ_WIDGET_ANDROID)
470 NotifyObservers(aStorage
, VoidString(), VoidString(), VoidString());
474 if (Persist(aStorage
) && (refresh
|| hadData
)) {
475 StorageDBChild
* storageChild
= StorageDBChild::Get(mPrivateBrowsingId
);
478 "Writing to localStorage after the database has been shut down"
480 return NS_ERROR_NOT_INITIALIZED
;
483 return storageChild
->AsyncClear(this);
486 return hadData
? NS_OK
: NS_SUCCESS_DOM_NO_OPERATION
;
489 int64_t LocalStorageCache::GetOriginQuotaUsage(
490 const LocalStorage
* aStorage
) const {
491 return mData
[GetDataSetIndex(aStorage
)].mOriginQuotaUsage
;
494 void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags
) {
495 if (aUnloadFlags
& kUnloadDefault
) {
496 // Must wait for preload to pass correct usage to ProcessUsageDelta
497 // XXX this is not technically needed right now since there is just
498 // per-origin isolated quota handling, but when we introduce super-
499 // -scope quotas, we have to do this. Better to start getting
500 // telemetry right now.
501 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS
);
503 mData
[kDefaultSet
].mKeys
.Clear();
504 ProcessUsageDelta(kDefaultSet
, -mData
[kDefaultSet
].mOriginQuotaUsage
);
507 if (aUnloadFlags
& kUnloadSession
) {
508 mData
[kSessionSet
].mKeys
.Clear();
509 ProcessUsageDelta(kSessionSet
, -mData
[kSessionSet
].mOriginQuotaUsage
);
512 #ifdef DOM_STORAGE_TESTS
513 if (aUnloadFlags
& kTestReload
) {
514 WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS
);
516 mData
[kDefaultSet
].mKeys
.Clear();
517 mLoaded
= false; // This is only used in testing code
523 // LocalStorageCacheBridge
525 uint32_t LocalStorageCache::LoadedCount() {
526 MonitorAutoLock
monitor(mMonitor
);
527 Data
& data
= mData
[kDefaultSet
];
528 return data
.mKeys
.Count();
531 bool LocalStorageCache::LoadItem(const nsAString
& aKey
,
532 const nsAString
& aValue
) {
533 MonitorAutoLock
monitor(mMonitor
);
538 Data
& data
= mData
[kDefaultSet
];
539 data
.mKeys
.LookupOrInsertWith(aKey
, [&] {
540 data
.mOriginQuotaUsage
+= aKey
.Length() + aValue
.Length();
541 return nsString(aValue
);
546 void LocalStorageCache::LoadDone(nsresult aRv
) {
547 MonitorAutoLock
monitor(mMonitor
);
553 void LocalStorageCache::LoadWait() {
554 MonitorAutoLock
monitor(mMonitor
);
562 StorageUsage::StorageUsage(const nsACString
& aOriginScope
)
563 : mOriginScope(aOriginScope
) {
564 mUsage
[kDefaultSet
] = mUsage
[kSessionSet
] = 0LL;
569 class LoadUsageRunnable
: public Runnable
{
571 LoadUsageRunnable(int64_t* aUsage
, const int64_t aDelta
)
572 : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage
), mDelta(aDelta
) {}
578 NS_IMETHOD
Run() override
{
586 void StorageUsage::LoadUsage(const int64_t aUsage
) {
587 // Using kDefaultSet index since it is the index for the persitent data
588 // stored in the database we have just loaded usage for.
589 if (!NS_IsMainThread()) {
590 // In single process scenario we get this call from the DB thread
591 RefPtr
<LoadUsageRunnable
> r
=
592 new LoadUsageRunnable(mUsage
+ kDefaultSet
, aUsage
);
593 NS_DispatchToMainThread(r
);
595 // On a child process we get this on the main thread already
596 mUsage
[kDefaultSet
] += aUsage
;
600 bool StorageUsage::CheckAndSetETLD1UsageDelta(
601 uint32_t aDataSetIndex
, const int64_t aDelta
,
602 const LocalStorageCache::MutationSource aSource
) {
603 MOZ_ASSERT(NS_IsMainThread());
605 int64_t newUsage
= mUsage
[aDataSetIndex
] + aDelta
;
606 if (aSource
== LocalStorageCache::ContentMutation
&& aDelta
> 0 &&
607 newUsage
> LocalStorageManager::GetSiteQuota()) {
611 mUsage
[aDataSetIndex
] = newUsage
;
615 } // namespace mozilla::dom