From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- dom/storage/LocalStorageCache.cpp | 615 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 615 insertions(+) create mode 100644 dom/storage/LocalStorageCache.cpp (limited to 'dom/storage/LocalStorageCache.cpp') diff --git a/dom/storage/LocalStorageCache.cpp b/dom/storage/LocalStorageCache.cpp new file mode 100644 index 0000000000..4b55482441 --- /dev/null +++ b/dom/storage/LocalStorageCache.cpp @@ -0,0 +1,615 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "LocalStorageCache.h" + +#include "Storage.h" +#include "StorageDBThread.h" +#include "StorageIPC.h" +#include "StorageUtils.h" +#include "LocalStorageManager.h" + +#include "nsDOMString.h" +#include "nsXULAppAPI.h" +#include "mozilla/Unused.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom { + +#define DOM_STORAGE_CACHE_KEEP_ALIVE_TIME_MS 20000 + +namespace { + +const uint32_t kDefaultSet = 0; +const uint32_t kSessionSet = 1; + +inline uint32_t GetDataSetIndex(bool aPrivateBrowsing, + bool aSessionScopedOrLess) { + if (!aPrivateBrowsing && aSessionScopedOrLess) { + return kSessionSet; + } + + return kDefaultSet; +} + +inline uint32_t GetDataSetIndex(const LocalStorage* aStorage) { + return GetDataSetIndex(aStorage->IsPrivateBrowsing(), + aStorage->IsSessionScopedOrLess()); +} + +} // namespace + +// LocalStorageCacheBridge + +NS_IMPL_ADDREF(LocalStorageCacheBridge) + +// Since there is no consumer of return value of Release, we can turn this +// method to void to make implementation of asynchronous +// LocalStorageCache::Release much simpler. +NS_IMETHODIMP_(void) LocalStorageCacheBridge::Release(void) { + MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "LocalStorageCacheBridge"); + if (0 == count) { + mRefCnt = 1; /* stabilize */ + /* enable this to find non-threadsafe destructors: */ + /* NS_ASSERT_OWNINGTHREAD(_class); */ + delete (this); + } +} + +// LocalStorageCache + +LocalStorageCache::LocalStorageCache(const nsACString* aOriginNoSuffix) + : mActor(nullptr), + mOriginNoSuffix(*aOriginNoSuffix), + mMonitor("LocalStorageCache"), + mLoaded(false), + mLoadResult(NS_OK), + mInitialized(false), + mPersistent(false), + mPreloadTelemetryRecorded(false) { + MOZ_COUNT_CTOR(LocalStorageCache); +} + +LocalStorageCache::~LocalStorageCache() { + if (mActor) { + mActor->SendDeleteMeInternal(); + MOZ_ASSERT(!mActor, "SendDeleteMeInternal should have cleared!"); + } + + if (mManager) { + mManager->DropCache(this); + } + + MOZ_COUNT_DTOR(LocalStorageCache); +} + +void LocalStorageCache::SetActor(LocalStorageCacheChild* aActor) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aActor); + MOZ_ASSERT(!mActor); + + mActor = aActor; +} + +NS_IMETHODIMP_(void) +LocalStorageCache::Release(void) { + // We must actually release on the main thread since the cache removes it + // self from the manager's hash table. And we don't want to lock access to + // that hash table. + if (NS_IsMainThread()) { + LocalStorageCacheBridge::Release(); + return; + } + + RefPtr> event = + NewNonOwningRunnableMethod("dom::LocalStorageCacheBridge::Release", + static_cast(this), + &LocalStorageCacheBridge::Release); + + nsresult rv = NS_DispatchToMainThread(event); + if (NS_FAILED(rv)) { + NS_WARNING("LocalStorageCache::Release() on a non-main thread"); + LocalStorageCacheBridge::Release(); + } +} + +void LocalStorageCache::Init(LocalStorageManager* aManager, bool aPersistent, + nsIPrincipal* aPrincipal, + const nsACString& aQuotaOriginScope) { + MOZ_ASSERT(!aQuotaOriginScope.IsEmpty()); + + if (mInitialized) { + return; + } + + mInitialized = true; + aPrincipal->OriginAttributesRef().CreateSuffix(mOriginSuffix); + mPrivateBrowsingId = aPrincipal->GetPrivateBrowsingId(); + mPersistent = aPersistent; + mQuotaOriginScope = aQuotaOriginScope; + + if (mPersistent) { + mManager = aManager; + Preload(); + } + + // Check the quota string has (or has not) the identical origin suffix as + // this storage cache is bound to. + MOZ_ASSERT(StringBeginsWith(mQuotaOriginScope, mOriginSuffix)); + MOZ_ASSERT(mOriginSuffix.IsEmpty() != + StringBeginsWith(mQuotaOriginScope, "^"_ns)); + + mUsage = aManager->GetOriginUsage(mQuotaOriginScope, mPrivateBrowsingId); +} + +void LocalStorageCache::NotifyObservers(const LocalStorage* aStorage, + const nsAString& aKey, + const nsAString& aOldValue, + const nsAString& aNewValue) { + AssertIsOnOwningThread(); + MOZ_ASSERT(aStorage); + + if (!mActor) { + return; + } + + // We want to send a message to the parent in order to broadcast the + // StorageEvent correctly to any child process. + + Unused << mActor->SendNotify(aStorage->DocumentURI(), aKey, aOldValue, + aNewValue); +} + +inline bool LocalStorageCache::Persist(const LocalStorage* aStorage) const { + return mPersistent && + (aStorage->IsPrivateBrowsing() || !aStorage->IsSessionScopedOrLess()); +} + +const nsCString LocalStorageCache::Origin() const { + return LocalStorageManager::CreateOrigin(mOriginSuffix, mOriginNoSuffix); +} + +LocalStorageCache::Data& LocalStorageCache::DataSet( + const LocalStorage* aStorage) { + return mData[GetDataSetIndex(aStorage)]; +} + +bool LocalStorageCache::ProcessUsageDelta(const LocalStorage* aStorage, + int64_t aDelta, + const MutationSource aSource) { + return ProcessUsageDelta(GetDataSetIndex(aStorage), aDelta, aSource); +} + +bool LocalStorageCache::ProcessUsageDelta(uint32_t aGetDataSetIndex, + const int64_t aDelta, + const MutationSource aSource) { + // Check limit per this origin + Data& data = mData[aGetDataSetIndex]; + uint64_t newOriginUsage = data.mOriginQuotaUsage + aDelta; + if (aSource == ContentMutation && aDelta > 0 && + newOriginUsage > LocalStorageManager::GetOriginQuota()) { + return false; + } + + // Now check eTLD+1 limit + if (mUsage && + !mUsage->CheckAndSetETLD1UsageDelta(aGetDataSetIndex, aDelta, aSource)) { + return false; + } + + // Update size in our data set + data.mOriginQuotaUsage = newOriginUsage; + return true; +} + +void LocalStorageCache::Preload() { + if (mLoaded || !mPersistent) { + return; + } + + StorageDBChild* storageChild = + StorageDBChild::GetOrCreate(mPrivateBrowsingId); + if (!storageChild) { + mLoaded = true; + mLoadResult = NS_ERROR_FAILURE; + return; + } + + storageChild->AsyncPreload(this); +} + +void LocalStorageCache::WaitForPreload(Telemetry::HistogramID aTelemetryID) { + if (!mPersistent) { + return; + } + + bool loaded = mLoaded; + + // Telemetry of rates of pending preloads + if (!mPreloadTelemetryRecorded) { + mPreloadTelemetryRecorded = true; + Telemetry::Accumulate( + Telemetry::LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS, !loaded); + } + + if (loaded) { + return; + } + + // Measure which operation blocks and for how long + Telemetry::RuntimeAutoTimer timer(aTelemetryID); + + // If preload already started (i.e. we got some first data, but not all) + // SyncPreload will just wait for it to finish rather then synchronously + // read from the database. It seems to me more optimal. + + // TODO place for A/B testing (force main thread load vs. let preload finish) + + // No need to check sDatabase for being non-null since preload is either + // done before we've shut the DB down or when the DB could not start, + // preload has not even be started. + StorageDBChild::Get(mPrivateBrowsingId)->SyncPreload(this); +} + +nsresult LocalStorageCache::GetLength(const LocalStorage* aStorage, + uint32_t* aRetval) { + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + return mLoadResult; + } + } + + *aRetval = DataSet(aStorage).mKeys.Count(); + return NS_OK; +} + +nsresult LocalStorageCache::GetKey(const LocalStorage* aStorage, + uint32_t aIndex, nsAString& aRetval) { + // XXX: This does a linear search for the key at index, which would + // suck if there's a large numer of indexes. Do we care? If so, + // maybe we need to have a lazily populated key array here or + // something? + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETKEY_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + return mLoadResult; + } + } + + aRetval.SetIsVoid(true); + for (auto iter = DataSet(aStorage).mKeys.Iter(); !iter.Done(); iter.Next()) { + if (aIndex == 0) { + aRetval = iter.Key(); + break; + } + aIndex--; + } + + return NS_OK; +} + +void LocalStorageCache::GetKeys(const LocalStorage* aStorage, + nsTArray& aKeys) { + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS); + } + + if (NS_FAILED(mLoadResult)) { + return; + } + + AppendToArray(aKeys, DataSet(aStorage).mKeys.Keys()); +} + +nsresult LocalStorageCache::GetItem(const LocalStorage* aStorage, + const nsAString& aKey, nsAString& aRetval) { + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + return mLoadResult; + } + } + + // not using AutoString since we don't want to copy buffer to result + nsString value; + if (!DataSet(aStorage).mKeys.Get(aKey, &value)) { + SetDOMStringToNull(value); + } + + aRetval = value; + + return NS_OK; +} + +nsresult LocalStorageCache::SetItem(const LocalStorage* aStorage, + const nsAString& aKey, + const nsAString& aValue, nsString& aOld, + const MutationSource aSource) { + // Size of the cache that will change after this action. + int64_t delta = 0; + + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + return mLoadResult; + } + } + + Data& data = DataSet(aStorage); + if (!data.mKeys.Get(aKey, &aOld)) { + SetDOMStringToNull(aOld); + + // We only consider key size if the key doesn't exist before. + delta += static_cast(aKey.Length()); + } + + delta += static_cast(aValue.Length()) - + static_cast(aOld.Length()); + + if (!ProcessUsageDelta(aStorage, delta, aSource)) { + return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; + } + + if (aValue == aOld && DOMStringIsNull(aValue) == DOMStringIsNull(aOld)) { + return NS_SUCCESS_DOM_NO_OPERATION; + } + + data.mKeys.InsertOrUpdate(aKey, aValue); + + if (aSource != ContentMutation) { + return NS_OK; + } + +#if !defined(MOZ_WIDGET_ANDROID) + NotifyObservers(aStorage, aKey, aOld, aValue); +#endif + + if (Persist(aStorage)) { + StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); + if (!storageChild) { + NS_ERROR( + "Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + if (DOMStringIsNull(aOld)) { + return storageChild->AsyncAddItem(this, aKey, aValue); + } + + return storageChild->AsyncUpdateItem(this, aKey, aValue); + } + + return NS_OK; +} + +nsresult LocalStorageCache::RemoveItem(const LocalStorage* aStorage, + const nsAString& aKey, nsString& aOld, + const MutationSource aSource) { + if (Persist(aStorage)) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + return mLoadResult; + } + } + + Data& data = DataSet(aStorage); + if (!data.mKeys.Get(aKey, &aOld)) { + SetDOMStringToNull(aOld); + return NS_SUCCESS_DOM_NO_OPERATION; + } + + // Recalculate the cached data size + const int64_t delta = -(static_cast(aOld.Length()) + + static_cast(aKey.Length())); + Unused << ProcessUsageDelta(aStorage, delta, aSource); + data.mKeys.Remove(aKey); + + if (aSource != ContentMutation) { + return NS_OK; + } + +#if !defined(MOZ_WIDGET_ANDROID) + NotifyObservers(aStorage, aKey, aOld, VoidString()); +#endif + + if (Persist(aStorage)) { + StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); + if (!storageChild) { + NS_ERROR( + "Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + return storageChild->AsyncRemoveItem(this, aKey); + } + + return NS_OK; +} + +nsresult LocalStorageCache::Clear(const LocalStorage* aStorage, + const MutationSource aSource) { + bool refresh = false; + if (Persist(aStorage)) { + // We need to preload all data (know the size) before we can proceeed + // to correctly decrease cached usage number. + // XXX as in case of unload, this is not technically needed now, but + // after super-scope quota introduction we have to do this. Get telemetry + // right now. + WaitForPreload(Telemetry::LOCALDOMSTORAGE_CLEAR_BLOCKING_MS); + if (NS_FAILED(mLoadResult)) { + // When we failed to load data from the database, force delete of the + // scope data and make use of the storage possible again. + refresh = true; + mLoadResult = NS_OK; + } + } + + Data& data = DataSet(aStorage); + bool hadData = !!data.mKeys.Count(); + + if (hadData) { + Unused << ProcessUsageDelta(aStorage, -data.mOriginQuotaUsage, aSource); + data.mKeys.Clear(); + } + + if (aSource != ContentMutation) { + return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; + } + +#if !defined(MOZ_WIDGET_ANDROID) + if (hadData) { + NotifyObservers(aStorage, VoidString(), VoidString(), VoidString()); + } +#endif + + if (Persist(aStorage) && (refresh || hadData)) { + StorageDBChild* storageChild = StorageDBChild::Get(mPrivateBrowsingId); + if (!storageChild) { + NS_ERROR( + "Writing to localStorage after the database has been shut down" + ", data lose!"); + return NS_ERROR_NOT_INITIALIZED; + } + + return storageChild->AsyncClear(this); + } + + return hadData ? NS_OK : NS_SUCCESS_DOM_NO_OPERATION; +} + +int64_t LocalStorageCache::GetOriginQuotaUsage( + const LocalStorage* aStorage) const { + return mData[GetDataSetIndex(aStorage)].mOriginQuotaUsage; +} + +void LocalStorageCache::UnloadItems(uint32_t aUnloadFlags) { + if (aUnloadFlags & kUnloadDefault) { + // Must wait for preload to pass correct usage to ProcessUsageDelta + // XXX this is not technically needed right now since there is just + // per-origin isolated quota handling, but when we introduce super- + // -scope quotas, we have to do this. Better to start getting + // telemetry right now. + WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS); + + mData[kDefaultSet].mKeys.Clear(); + ProcessUsageDelta(kDefaultSet, -mData[kDefaultSet].mOriginQuotaUsage); + } + + if (aUnloadFlags & kUnloadSession) { + mData[kSessionSet].mKeys.Clear(); + ProcessUsageDelta(kSessionSet, -mData[kSessionSet].mOriginQuotaUsage); + } + +#ifdef DOM_STORAGE_TESTS + if (aUnloadFlags & kTestReload) { + WaitForPreload(Telemetry::LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS); + + mData[kDefaultSet].mKeys.Clear(); + mLoaded = false; // This is only used in testing code + Preload(); + } +#endif +} + +// LocalStorageCacheBridge + +uint32_t LocalStorageCache::LoadedCount() { + MonitorAutoLock monitor(mMonitor); + Data& data = mData[kDefaultSet]; + return data.mKeys.Count(); +} + +bool LocalStorageCache::LoadItem(const nsAString& aKey, + const nsAString& aValue) { + MonitorAutoLock monitor(mMonitor); + if (mLoaded) { + return false; + } + + Data& data = mData[kDefaultSet]; + data.mKeys.LookupOrInsertWith(aKey, [&] { + data.mOriginQuotaUsage += aKey.Length() + aValue.Length(); + return nsString(aValue); + }); + return true; +} + +void LocalStorageCache::LoadDone(nsresult aRv) { + MonitorAutoLock monitor(mMonitor); + mLoadResult = aRv; + mLoaded = true; + monitor.Notify(); +} + +void LocalStorageCache::LoadWait() { + MonitorAutoLock monitor(mMonitor); + while (!mLoaded) { + monitor.Wait(); + } +} + +// StorageUsage + +StorageUsage::StorageUsage(const nsACString& aOriginScope) + : mOriginScope(aOriginScope) { + mUsage[kDefaultSet] = mUsage[kSessionSet] = 0LL; +} + +namespace { + +class LoadUsageRunnable : public Runnable { + public: + LoadUsageRunnable(int64_t* aUsage, const int64_t aDelta) + : Runnable("dom::LoadUsageRunnable"), mTarget(aUsage), mDelta(aDelta) {} + + private: + int64_t* mTarget; + int64_t mDelta; + + NS_IMETHOD Run() override { + *mTarget = mDelta; + return NS_OK; + } +}; + +} // namespace + +void StorageUsage::LoadUsage(const int64_t aUsage) { + // Using kDefaultSet index since it is the index for the persitent data + // stored in the database we have just loaded usage for. + if (!NS_IsMainThread()) { + // In single process scenario we get this call from the DB thread + RefPtr r = + new LoadUsageRunnable(mUsage + kDefaultSet, aUsage); + NS_DispatchToMainThread(r); + } else { + // On a child process we get this on the main thread already + mUsage[kDefaultSet] += aUsage; + } +} + +bool StorageUsage::CheckAndSetETLD1UsageDelta( + uint32_t aDataSetIndex, const int64_t aDelta, + const LocalStorageCache::MutationSource aSource) { + MOZ_ASSERT(NS_IsMainThread()); + + int64_t newUsage = mUsage[aDataSetIndex] + aDelta; + if (aSource == LocalStorageCache::ContentMutation && aDelta > 0 && + newUsage > LocalStorageManager::GetSiteQuota()) { + return false; + } + + mUsage[aDataSetIndex] = newUsage; + return true; +} + +} // namespace mozilla::dom -- cgit v1.2.3