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 --- netwerk/cache2/CacheEntry.cpp | 1952 +++++++++++ netwerk/cache2/CacheEntry.h | 583 ++++ netwerk/cache2/CacheFile.cpp | 2589 +++++++++++++++ netwerk/cache2/CacheFile.h | 288 ++ netwerk/cache2/CacheFileChunk.cpp | 840 +++++ netwerk/cache2/CacheFileChunk.h | 238 ++ netwerk/cache2/CacheFileContextEvictor.cpp | 741 +++++ netwerk/cache2/CacheFileContextEvictor.h | 100 + netwerk/cache2/CacheFileIOManager.cpp | 4447 ++++++++++++++++++++++++++ netwerk/cache2/CacheFileIOManager.h | 490 +++ netwerk/cache2/CacheFileInputStream.cpp | 734 +++++ netwerk/cache2/CacheFileInputStream.h | 80 + netwerk/cache2/CacheFileMetadata.cpp | 1041 ++++++ netwerk/cache2/CacheFileMetadata.h | 245 ++ netwerk/cache2/CacheFileOutputStream.cpp | 481 +++ netwerk/cache2/CacheFileOutputStream.h | 70 + netwerk/cache2/CacheFileUtils.cpp | 667 ++++ netwerk/cache2/CacheFileUtils.h | 238 ++ netwerk/cache2/CacheHashUtils.cpp | 226 ++ netwerk/cache2/CacheHashUtils.h | 69 + netwerk/cache2/CacheIOThread.cpp | 583 ++++ netwerk/cache2/CacheIOThread.h | 149 + netwerk/cache2/CacheIndex.cpp | 3921 +++++++++++++++++++++++ netwerk/cache2/CacheIndex.h | 1311 ++++++++ netwerk/cache2/CacheIndexContextIterator.cpp | 24 + netwerk/cache2/CacheIndexContextIterator.h | 31 + netwerk/cache2/CacheIndexIterator.cpp | 110 + netwerk/cache2/CacheIndexIterator.h | 62 + netwerk/cache2/CacheLog.cpp | 20 + netwerk/cache2/CacheLog.h | 20 + netwerk/cache2/CacheObserver.cpp | 254 ++ netwerk/cache2/CacheObserver.h | 109 + netwerk/cache2/CachePurgeLock.cpp | 115 + netwerk/cache2/CachePurgeLock.h | 24 + netwerk/cache2/CacheStorage.cpp | 196 ++ netwerk/cache2/CacheStorage.h | 63 + netwerk/cache2/CacheStorageService.cpp | 2369 ++++++++++++++ netwerk/cache2/CacheStorageService.h | 451 +++ netwerk/cache2/moz.build | 66 + netwerk/cache2/nsICacheEntry.idl | 369 +++ netwerk/cache2/nsICacheEntryDoomCallback.idl | 15 + netwerk/cache2/nsICacheEntryOpenCallback.idl | 79 + netwerk/cache2/nsICachePurgeLock.idl | 40 + netwerk/cache2/nsICacheStorage.idl | 145 + netwerk/cache2/nsICacheStorageService.idl | 138 + netwerk/cache2/nsICacheStorageVisitor.idl | 36 + netwerk/cache2/nsICacheTesting.idl | 20 + 47 files changed, 26839 insertions(+) create mode 100644 netwerk/cache2/CacheEntry.cpp create mode 100644 netwerk/cache2/CacheEntry.h create mode 100644 netwerk/cache2/CacheFile.cpp create mode 100644 netwerk/cache2/CacheFile.h create mode 100644 netwerk/cache2/CacheFileChunk.cpp create mode 100644 netwerk/cache2/CacheFileChunk.h create mode 100644 netwerk/cache2/CacheFileContextEvictor.cpp create mode 100644 netwerk/cache2/CacheFileContextEvictor.h create mode 100644 netwerk/cache2/CacheFileIOManager.cpp create mode 100644 netwerk/cache2/CacheFileIOManager.h create mode 100644 netwerk/cache2/CacheFileInputStream.cpp create mode 100644 netwerk/cache2/CacheFileInputStream.h create mode 100644 netwerk/cache2/CacheFileMetadata.cpp create mode 100644 netwerk/cache2/CacheFileMetadata.h create mode 100644 netwerk/cache2/CacheFileOutputStream.cpp create mode 100644 netwerk/cache2/CacheFileOutputStream.h create mode 100644 netwerk/cache2/CacheFileUtils.cpp create mode 100644 netwerk/cache2/CacheFileUtils.h create mode 100644 netwerk/cache2/CacheHashUtils.cpp create mode 100644 netwerk/cache2/CacheHashUtils.h create mode 100644 netwerk/cache2/CacheIOThread.cpp create mode 100644 netwerk/cache2/CacheIOThread.h create mode 100644 netwerk/cache2/CacheIndex.cpp create mode 100644 netwerk/cache2/CacheIndex.h create mode 100644 netwerk/cache2/CacheIndexContextIterator.cpp create mode 100644 netwerk/cache2/CacheIndexContextIterator.h create mode 100644 netwerk/cache2/CacheIndexIterator.cpp create mode 100644 netwerk/cache2/CacheIndexIterator.h create mode 100644 netwerk/cache2/CacheLog.cpp create mode 100644 netwerk/cache2/CacheLog.h create mode 100644 netwerk/cache2/CacheObserver.cpp create mode 100644 netwerk/cache2/CacheObserver.h create mode 100644 netwerk/cache2/CachePurgeLock.cpp create mode 100644 netwerk/cache2/CachePurgeLock.h create mode 100644 netwerk/cache2/CacheStorage.cpp create mode 100644 netwerk/cache2/CacheStorage.h create mode 100644 netwerk/cache2/CacheStorageService.cpp create mode 100644 netwerk/cache2/CacheStorageService.h create mode 100644 netwerk/cache2/moz.build create mode 100644 netwerk/cache2/nsICacheEntry.idl create mode 100644 netwerk/cache2/nsICacheEntryDoomCallback.idl create mode 100644 netwerk/cache2/nsICacheEntryOpenCallback.idl create mode 100644 netwerk/cache2/nsICachePurgeLock.idl create mode 100644 netwerk/cache2/nsICacheStorage.idl create mode 100644 netwerk/cache2/nsICacheStorageService.idl create mode 100644 netwerk/cache2/nsICacheStorageVisitor.idl create mode 100644 netwerk/cache2/nsICacheTesting.idl (limited to 'netwerk/cache2') diff --git a/netwerk/cache2/CacheEntry.cpp b/netwerk/cache2/CacheEntry.cpp new file mode 100644 index 0000000000..142d32d274 --- /dev/null +++ b/netwerk/cache2/CacheEntry.cpp @@ -0,0 +1,1952 @@ +/* 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 +#include + +#include "CacheEntry.h" + +#include "CacheFileUtils.h" +#include "CacheIndex.h" +#include "CacheLog.h" +#include "CacheObserver.h" +#include "CacheStorageService.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Telemetry.h" +#include "mozilla/psm/TransportSecurityInfo.h" +#include "nsComponentManagerUtils.h" +#include "nsIAsyncOutputStream.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheStorage.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISeekableStream.h" +#include "nsISizeOf.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsProxyRelease.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsThreadUtils.h" + +namespace mozilla::net { + +static uint32_t const ENTRY_WANTED = nsICacheEntryOpenCallback::ENTRY_WANTED; +static uint32_t const RECHECK_AFTER_WRITE_FINISHED = + nsICacheEntryOpenCallback::RECHECK_AFTER_WRITE_FINISHED; +static uint32_t const ENTRY_NEEDS_REVALIDATION = + nsICacheEntryOpenCallback::ENTRY_NEEDS_REVALIDATION; +static uint32_t const ENTRY_NOT_WANTED = + nsICacheEntryOpenCallback::ENTRY_NOT_WANTED; + +NS_IMPL_ISUPPORTS(CacheEntryHandle, nsICacheEntry) + +// CacheEntryHandle + +CacheEntryHandle::CacheEntryHandle(CacheEntry* aEntry) : mEntry(aEntry) { +#ifdef DEBUG + if (!mEntry->HandlesCount()) { + // CacheEntry.mHandlesCount must go from zero to one only under + // the service lock. Can access CacheStorageService::Self() w/o a check + // since CacheEntry hrefs it. + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + } +#endif + + mEntry->AddHandleRef(); + + LOG(("New CacheEntryHandle %p for entry %p", this, aEntry)); +} + +NS_IMETHODIMP CacheEntryHandle::Dismiss() { + LOG(("CacheEntryHandle::Dismiss %p", this)); + + if (mClosed.compareExchange(false, true)) { + mEntry->OnHandleClosed(this); + return NS_OK; + } + + LOG((" already dropped")); + return NS_ERROR_UNEXPECTED; +} + +CacheEntryHandle::~CacheEntryHandle() { + mEntry->ReleaseHandleRef(); + Dismiss(); + + LOG(("CacheEntryHandle::~CacheEntryHandle %p", this)); +} + +// CacheEntry::Callback + +CacheEntry::Callback::Callback(CacheEntry* aEntry, + nsICacheEntryOpenCallback* aCallback, + bool aReadOnly, bool aCheckOnAnyThread, + bool aSecret) + : mEntry(aEntry), + mCallback(aCallback), + mTarget(GetCurrentSerialEventTarget()), + mReadOnly(aReadOnly), + mRevalidating(false), + mCheckOnAnyThread(aCheckOnAnyThread), + mRecheckAfterWrite(false), + mNotWanted(false), + mSecret(aSecret), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::Callback(CacheEntry* aEntry, + bool aDoomWhenFoundInPinStatus) + : mEntry(aEntry), + mReadOnly(false), + mRevalidating(false), + mCheckOnAnyThread(true), + mRecheckAfterWrite(false), + mNotWanted(false), + mSecret(false), + mDoomWhenFoundPinned(aDoomWhenFoundInPinStatus), + mDoomWhenFoundNonPinned(!aDoomWhenFoundInPinStatus) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::Callback(CacheEntry::Callback const& aThat) + : mEntry(aThat.mEntry), + mCallback(aThat.mCallback), + mTarget(aThat.mTarget), + mReadOnly(aThat.mReadOnly), + mRevalidating(aThat.mRevalidating), + mCheckOnAnyThread(aThat.mCheckOnAnyThread), + mRecheckAfterWrite(aThat.mRecheckAfterWrite), + mNotWanted(aThat.mNotWanted), + mSecret(aThat.mSecret), + mDoomWhenFoundPinned(aThat.mDoomWhenFoundPinned), + mDoomWhenFoundNonPinned(aThat.mDoomWhenFoundNonPinned) { + MOZ_COUNT_CTOR(CacheEntry::Callback); + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::~Callback() { + ProxyRelease("CacheEntry::Callback::mCallback", mCallback, mTarget); + + mEntry->ReleaseHandleRef(); + MOZ_COUNT_DTOR(CacheEntry::Callback); +} + +// We have locks on both this and aEntry +void CacheEntry::Callback::ExchangeEntry(CacheEntry* aEntry) { + aEntry->mLock.AssertCurrentThreadOwns(); + mEntry->mLock.AssertCurrentThreadOwns(); + if (mEntry == aEntry) return; + + // The counter may go from zero to non-null only under the service lock + // but here we expect it to be already positive. + MOZ_ASSERT(aEntry->HandlesCount()); + aEntry->AddHandleRef(); + mEntry->ReleaseHandleRef(); + mEntry = aEntry; +} + +// This is called on entries in another entry's mCallback array, under the lock +// of that other entry. No other threads can access this entry at this time. +bool CacheEntry::Callback::DeferDoom(bool* aDoom) const + MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_ASSERT(mEntry->mPinningKnown); + + if (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) || + MOZ_UNLIKELY(mDoomWhenFoundPinned)) { + *aDoom = + (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) && + MOZ_LIKELY(!mEntry->mPinned)) || + (MOZ_UNLIKELY(mDoomWhenFoundPinned) && MOZ_UNLIKELY(mEntry->mPinned)); + + return true; + } + + return false; +} + +nsresult CacheEntry::Callback::OnCheckThread(bool* aOnCheckThread) const { + if (!mCheckOnAnyThread) { + // Check we are on the target + return mTarget->IsOnCurrentThread(aOnCheckThread); + } + + // We can invoke check anywhere + *aOnCheckThread = true; + return NS_OK; +} + +nsresult CacheEntry::Callback::OnAvailThread(bool* aOnAvailThread) const { + return mTarget->IsOnCurrentThread(aOnAvailThread); +} + +// CacheEntry + +NS_IMPL_ISUPPORTS(CacheEntry, nsIRunnable, CacheFileListener) + +/* static */ +uint64_t CacheEntry::GetNextId() { + static Atomic id(0); + return ++id; +} + +CacheEntry::CacheEntry(const nsACString& aStorageID, const nsACString& aURI, + const nsACString& aEnhanceID, bool aUseDisk, + bool aSkipSizeCheck, bool aPin) + : mURI(aURI), + mEnhanceID(aEnhanceID), + mStorageID(aStorageID), + mUseDisk(aUseDisk), + mSkipSizeCheck(aSkipSizeCheck), + mPinned(aPin), + mSecurityInfoLoaded(false), + mPreventCallbacks(false), + mHasData(false), + mPinningKnown(false), + mCacheEntryId(GetNextId()) { + LOG(("CacheEntry::CacheEntry [this=%p]", this)); + + mService = CacheStorageService::Self(); + + CacheStorageService::Self()->RecordMemoryOnlyEntry(this, !aUseDisk, + true /* overwrite */); +} + +CacheEntry::~CacheEntry() { LOG(("CacheEntry::~CacheEntry [this=%p]", this)); } + +char const* CacheEntry::StateString(uint32_t aState) { + switch (aState) { + case NOTLOADED: + return "NOTLOADED"; + case LOADING: + return "LOADING"; + case EMPTY: + return "EMPTY"; + case WRITING: + return "WRITING"; + case READY: + return "READY"; + case REVALIDATING: + return "REVALIDATING"; + } + + return "?"; +} + +nsresult CacheEntry::HashingKeyWithStorage(nsACString& aResult) const { + return HashingKey(mStorageID, mEnhanceID, mURI, aResult); +} + +nsresult CacheEntry::HashingKey(nsACString& aResult) const { + return HashingKey(""_ns, mEnhanceID, mURI, aResult); +} + +// static +nsresult CacheEntry::HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, nsIURI* aURI, + nsACString& aResult) { + nsAutoCString spec; + nsresult rv = aURI->GetAsciiSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + return HashingKey(aStorageID, aEnhanceID, spec, aResult); +} + +// static +nsresult CacheEntry::HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, + const nsACString& aURISpec, + nsACString& aResult) { + /** + * This key is used to salt hash that is a base for disk file name. + * Changing it will cause we will not be able to find files on disk. + */ + + aResult.Assign(aStorageID); + + if (!aEnhanceID.IsEmpty()) { + CacheFileUtils::AppendTagWithValue(aResult, '~', aEnhanceID); + } + + // Appending directly + aResult.Append(':'); + aResult.Append(aURISpec); + + return NS_OK; +} + +void CacheEntry::AsyncOpen(nsICacheEntryOpenCallback* aCallback, + uint32_t aFlags) { + bool readonly = aFlags & nsICacheStorage::OPEN_READONLY; + bool bypassIfBusy = aFlags & nsICacheStorage::OPEN_BYPASS_IF_BUSY; + bool truncate = aFlags & nsICacheStorage::OPEN_TRUNCATE; + bool priority = aFlags & nsICacheStorage::OPEN_PRIORITY; + bool multithread = aFlags & nsICacheStorage::CHECK_MULTITHREADED; + bool secret = aFlags & nsICacheStorage::OPEN_SECRETLY; + + if (MOZ_LOG_TEST(gCache2Log, LogLevel::Debug)) { + MutexAutoLock lock(mLock); + LOG(("CacheEntry::AsyncOpen [this=%p, state=%s, flags=%d, callback=%p]", + this, StateString(mState), aFlags, aCallback)); + } +#ifdef DEBUG + { + // yes, if logging is on in DEBUG we'll take the lock twice in a row + MutexAutoLock lock(mLock); + MOZ_ASSERT(!readonly || !truncate, "Bad flags combination"); + MOZ_ASSERT(!(truncate && mState > LOADING), + "Must not call truncate on already loaded entry"); + } +#endif + + Callback callback(this, aCallback, readonly, multithread, secret); + + if (!Open(callback, truncate, priority, bypassIfBusy)) { + // We get here when the callback wants to bypass cache when it's busy. + LOG((" writing or revalidating, callback wants to bypass cache")); + callback.mNotWanted = true; + InvokeAvailableCallback(callback); + } +} + +bool CacheEntry::Open(Callback& aCallback, bool aTruncate, bool aPriority, + bool aBypassIfBusy) { + mozilla::MutexAutoLock lock(mLock); + + // Check state under the lock + if (aBypassIfBusy && (mState == WRITING || mState == REVALIDATING)) { + return false; + } + + RememberCallback(aCallback); + + // Load() opens the lock + if (Load(aTruncate, aPriority)) { + // Loading is in progress... + return true; + } + + InvokeCallbacks(); + + return true; +} + +bool CacheEntry::Load(bool aTruncate, bool aPriority) { + LOG(("CacheEntry::Load [this=%p, trunc=%d]", this, aTruncate)); + + mLock.AssertCurrentThreadOwns(); + + if (mState > LOADING) { + LOG((" already loaded")); + return false; + } + + if (mState == LOADING) { + LOG((" already loading")); + return true; + } + + mState = LOADING; + + MOZ_ASSERT(!mFile); + + nsresult rv; + + nsAutoCString fileKey; + rv = HashingKeyWithStorage(fileKey); + + bool reportMiss = false; + + // Check the index under two conditions for two states and take appropriate + // action: + // 1. When this is a disk entry and not told to truncate, check there is a + // disk file. + // If not, set the 'truncate' flag to true so that this entry will open + // instantly as a new one. + // 2. When this is a memory-only entry, check there is a disk file. + // If there is or could be, doom that file. + if ((!aTruncate || !mUseDisk) && NS_SUCCEEDED(rv)) { + // Check the index right now to know we have or have not the entry + // as soon as possible. + CacheIndex::EntryStatus status; + if (NS_SUCCEEDED(CacheIndex::HasEntry(fileKey, &status))) { + switch (status) { + case CacheIndex::DOES_NOT_EXIST: + // Doesn't apply to memory-only entries, Load() is called only once + // for them and never again for their session lifetime. + if (!aTruncate && mUseDisk) { + LOG( + (" entry doesn't exist according information from the index, " + "truncating")); + reportMiss = true; + aTruncate = true; + } + break; + case CacheIndex::EXISTS: + case CacheIndex::DO_NOT_KNOW: + if (!mUseDisk) { + LOG( + (" entry open as memory-only, but there is a file, status=%d, " + "dooming it", + status)); + CacheFileIOManager::DoomFileByKey(fileKey, nullptr); + } + break; + } + } + } + + mFile = new CacheFile(); + + BackgroundOp(Ops::REGISTER); + + bool directLoad = aTruncate || !mUseDisk; + if (directLoad) { + // mLoadStart will be used to calculate telemetry of life-time of this + // entry. Low resulution is then enough. + mLoadStart = TimeStamp::NowLoRes(); + mPinningKnown = true; + } else { + mLoadStart = TimeStamp::Now(); + } + + { + mozilla::MutexAutoUnlock unlock(mLock); + + if (reportMiss) { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::MISS, mLoadStart); + } + + LOG((" performing load, file=%p", mFile.get())); + if (NS_SUCCEEDED(rv)) { + rv = mFile->Init(fileKey, aTruncate, !mUseDisk, mSkipSizeCheck, aPriority, + mPinned, directLoad ? nullptr : this); + } + + if (NS_FAILED(rv)) { + mFileStatus = rv; + AsyncDoom(nullptr); + return false; + } + } + + if (directLoad) { + // Just fake the load has already been done as "new". + mFileStatus = NS_OK; + mState = EMPTY; + } + + return mState == LOADING; +} + +NS_IMETHODIMP CacheEntry::OnFileReady(nsresult aResult, bool aIsNew) { + LOG(("CacheEntry::OnFileReady [this=%p, rv=0x%08" PRIx32 ", new=%d]", this, + static_cast(aResult), aIsNew)); + + MOZ_ASSERT(!mLoadStart.IsNull()); + + if (NS_SUCCEEDED(aResult)) { + if (aIsNew) { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::MISS, mLoadStart); + } else { + CacheFileUtils::DetailedCacheHitTelemetry::AddRecord( + CacheFileUtils::DetailedCacheHitTelemetry::HIT, mLoadStart); + } + } + + // OnFileReady, that is the only code that can transit from LOADING + // to any follow-on state and can only be invoked ones on an entry. + // Until this moment there is no consumer that could manipulate + // the entry state. + + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState == LOADING); + + mState = (aIsNew || NS_FAILED(aResult)) ? EMPTY : READY; + + mFileStatus = aResult; + + mPinned = mFile->IsPinned(); + + mPinningKnown = true; + LOG((" pinning=%d", (bool)mPinned)); + + if (mState == READY) { + mHasData = true; + + uint32_t frecency; + mFile->GetFrecency(&frecency); + // mFrecency is held in a double to increase computance precision. + // It is ok to persist frecency only as a uint32 with some math involved. + mFrecency = INT2FRECENCY(frecency); + } + + InvokeCallbacks(); + + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::OnFileDoomed(nsresult aResult) { + if (mDoomCallback) { + RefPtr event = + new DoomCallbackRunnable(this, aResult); + NS_DispatchToMainThread(event); + } + + return NS_OK; +} + +already_AddRefed CacheEntry::ReopenTruncated( + bool aMemoryOnly, nsICacheEntryOpenCallback* aCallback) { + LOG(("CacheEntry::ReopenTruncated [this=%p]", this)); + + mLock.AssertCurrentThreadOwns(); + + // Hold callbacks invocation, AddStorageEntry would invoke from doom + // prematurly + mPreventCallbacks = true; + + RefPtr handle; + RefPtr newEntry; + { + if (mPinned) { + MOZ_ASSERT(mUseDisk); + // We want to pin even no-store entries (the case we recreate a disk entry + // as a memory-only entry.) + aMemoryOnly = false; + } + + mozilla::MutexAutoUnlock unlock(mLock); + + // The following call dooms this entry (calls DoomAlreadyRemoved on us) + nsresult rv = CacheStorageService::Self()->AddStorageEntry( + GetStorageID(), GetURI(), GetEnhanceID(), mUseDisk && !aMemoryOnly, + mSkipSizeCheck, mPinned, + nsICacheStorage::OPEN_TRUNCATE, // truncate existing (this one) + getter_AddRefs(handle)); + + if (NS_SUCCEEDED(rv)) { + newEntry = handle->Entry(); + LOG((" exchanged entry %p by entry %p, rv=0x%08" PRIx32, this, + newEntry.get(), static_cast(rv))); + newEntry->AsyncOpen(aCallback, nsICacheStorage::OPEN_TRUNCATE); + } else { + LOG((" exchanged of entry %p failed, rv=0x%08" PRIx32, this, + static_cast(rv))); + AsyncDoom(nullptr); + } + } + + mPreventCallbacks = false; + + if (!newEntry) return nullptr; + + newEntry->TransferCallbacks(*this); + mCallbacks.Clear(); + + // Must return a new write handle, since the consumer is expected to + // write to this newly recreated entry. The |handle| is only a common + // reference counter and doesn't revert entry state back when write + // fails and also doesn't update the entry frecency. Not updating + // frecency causes entries to not be purged from our memory pools. + RefPtr writeHandle = newEntry->NewWriteHandle(); + return writeHandle.forget(); +} + +void CacheEntry::TransferCallbacks(CacheEntry& aFromEntry) { + mozilla::MutexAutoLock lock(mLock); + aFromEntry.mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::TransferCallbacks [entry=%p, from=%p]", this, &aFromEntry)); + + if (!mCallbacks.Length()) { + mCallbacks.SwapElements(aFromEntry.mCallbacks); + } else { + mCallbacks.AppendElements(aFromEntry.mCallbacks); + } + + uint32_t callbacksLength = mCallbacks.Length(); + if (callbacksLength) { + // Carry the entry reference (unfortunately, needs to be done manually...) + for (uint32_t i = 0; i < callbacksLength; ++i) { + mCallbacks[i].ExchangeEntry(this); + } + + BackgroundOp(Ops::CALLBACKS, true); + } +} + +void CacheEntry::RememberCallback(Callback& aCallback) { + mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::RememberCallback [this=%p, cb=%p, state=%s]", this, + aCallback.mCallback.get(), StateString(mState))); + + mCallbacks.AppendElement(aCallback); +} + +void CacheEntry::InvokeCallbacksLock() { + mozilla::MutexAutoLock lock(mLock); + InvokeCallbacks(); +} + +void CacheEntry::InvokeCallbacks() { + mLock.AssertCurrentThreadOwns(); + + LOG(("CacheEntry::InvokeCallbacks BEGIN [this=%p]", this)); + + // Invoke first all r/w callbacks, then all r/o callbacks. + if (InvokeCallbacks(false)) InvokeCallbacks(true); + + LOG(("CacheEntry::InvokeCallbacks END [this=%p]", this)); +} + +bool CacheEntry::InvokeCallbacks(bool aReadOnly) { + mLock.AssertCurrentThreadOwns(); + + RefPtr recreatedHandle; + + uint32_t i = 0; + while (i < mCallbacks.Length()) { + if (mPreventCallbacks) { + LOG((" callbacks prevented!")); + return false; + } + + if (!mIsDoomed && (mState == WRITING || mState == REVALIDATING)) { + LOG((" entry is being written/revalidated")); + return false; + } + + bool recreate; + if (mCallbacks[i].DeferDoom(&recreate)) { + mCallbacks.RemoveElementAt(i); + if (!recreate) { + continue; + } + + LOG((" defer doom marker callback hit positive, recreating")); + recreatedHandle = ReopenTruncated(!mUseDisk, nullptr); + break; + } + + if (mCallbacks[i].mReadOnly != aReadOnly) { + // Callback is not r/w or r/o, go to another one in line + ++i; + continue; + } + + bool onCheckThread; + nsresult rv = mCallbacks[i].OnCheckThread(&onCheckThread); + + if (NS_SUCCEEDED(rv) && !onCheckThread) { + // Redispatch to the target thread + rv = mCallbacks[i].mTarget->Dispatch( + NewRunnableMethod("net::CacheEntry::InvokeCallbacksLock", this, + &CacheEntry::InvokeCallbacksLock), + nsIEventTarget::DISPATCH_NORMAL); + if (NS_SUCCEEDED(rv)) { + LOG((" re-dispatching to target thread")); + return false; + } + } + + Callback callback = mCallbacks[i]; + mCallbacks.RemoveElementAt(i); + + if (NS_SUCCEEDED(rv) && !InvokeCallback(callback)) { + // Callback didn't fire, put it back and go to another one in line. + // Only reason InvokeCallback returns false is that onCacheEntryCheck + // returns RECHECK_AFTER_WRITE_FINISHED. If we would stop the loop, other + // readers or potential writers would be unnecessarily kept from being + // invoked. + size_t pos = std::min(mCallbacks.Length(), static_cast(i)); + mCallbacks.InsertElementAt(pos, callback); + ++i; + } + } + + if (recreatedHandle) { + // Must be released outside of the lock, enters InvokeCallback on the new + // entry + mozilla::MutexAutoUnlock unlock(mLock); + recreatedHandle = nullptr; + } + + return true; +} + +bool CacheEntry::InvokeCallback(Callback& aCallback) { + mLock.AssertCurrentThreadOwns(); + LOG(("CacheEntry::InvokeCallback [this=%p, state=%s, cb=%p]", this, + StateString(mState), aCallback.mCallback.get())); + + // When this entry is doomed we want to notify the callback any time + if (!mIsDoomed) { + // When we are here, the entry must be loaded from disk + MOZ_ASSERT(mState > LOADING); + + if (mState == WRITING || mState == REVALIDATING) { + // Prevent invoking other callbacks since one of them is now writing + // or revalidating this entry. No consumers should get this entry + // until metadata are filled with values downloaded from the server + // or the entry revalidated and output stream has been opened. + LOG((" entry is being written/revalidated, callback bypassed")); + return false; + } + + // mRecheckAfterWrite flag already set means the callback has already passed + // the onCacheEntryCheck call. Until the current write is not finished this + // callback will be bypassed. + if (!aCallback.mRecheckAfterWrite) { + if (!aCallback.mReadOnly) { + if (mState == EMPTY) { + // Advance to writing state, we expect to invoke the callback and let + // it fill content of this entry. Must set and check the state here + // to prevent more then one + mState = WRITING; + LOG((" advancing to WRITING state")); + } + + if (!aCallback.mCallback) { + // We can be given no callback only in case of recreate, it is ok + // to advance to WRITING state since the caller of recreate is + // expected to write this entry now. + return true; + } + } + + if (mState == READY) { + // Metadata present, validate the entry + uint32_t checkResult; + { + // mayhemer: TODO check and solve any potential races of concurent + // OnCacheEntryCheck + mozilla::MutexAutoUnlock unlock(mLock); + + RefPtr handle = NewHandle(); + + nsresult rv = + aCallback.mCallback->OnCacheEntryCheck(handle, &checkResult); + LOG((" OnCacheEntryCheck: rv=0x%08" PRIx32 ", result=%" PRId32, + static_cast(rv), static_cast(checkResult))); + + if (NS_FAILED(rv)) checkResult = ENTRY_NOT_WANTED; + } + + aCallback.mRevalidating = checkResult == ENTRY_NEEDS_REVALIDATION; + + switch (checkResult) { + case ENTRY_WANTED: + // Nothing more to do here, the consumer is responsible to handle + // the result of OnCacheEntryCheck it self. + // Proceed to callback... + break; + + case RECHECK_AFTER_WRITE_FINISHED: + LOG( + (" consumer will check on the entry again after write is " + "done")); + // The consumer wants the entry to complete first. + aCallback.mRecheckAfterWrite = true; + break; + + case ENTRY_NEEDS_REVALIDATION: + LOG((" will be holding callbacks until entry is revalidated")); + // State is READY now and from that state entry cannot transit to + // any other state then REVALIDATING for which cocurrency is not an + // issue. Potentially no need to lock here. + mState = REVALIDATING; + break; + + case ENTRY_NOT_WANTED: + LOG((" consumer not interested in the entry")); + // Do not give this entry to the consumer, it is not interested in + // us. + aCallback.mNotWanted = true; + break; + } + } + } + } + + if (aCallback.mCallback) { + if (!mIsDoomed && aCallback.mRecheckAfterWrite) { + // If we don't have data and the callback wants a complete entry, + // don't invoke now. + bool bypass = !mHasData; + if (!bypass && NS_SUCCEEDED(mFileStatus)) { + int64_t _unused; + bypass = !mFile->DataSize(&_unused); + } + + if (bypass) { + LOG((" bypassing, entry data still being written")); + return false; + } + + // Entry is complete now, do the check+avail call again + aCallback.mRecheckAfterWrite = false; + return InvokeCallback(aCallback); + } + + mozilla::MutexAutoUnlock unlock(mLock); + InvokeAvailableCallback(aCallback); + } + + return true; +} + +void CacheEntry::InvokeAvailableCallback(Callback const& aCallback) { + nsresult rv; + uint32_t state; + { + mozilla::MutexAutoLock lock(mLock); + state = mState; + LOG( + ("CacheEntry::InvokeAvailableCallback [this=%p, state=%s, cb=%p, " + "r/o=%d, " + "n/w=%d]", + this, StateString(mState), aCallback.mCallback.get(), + aCallback.mReadOnly, aCallback.mNotWanted)); + + // When we are here, the entry must be loaded from disk + MOZ_ASSERT(state > LOADING || mIsDoomed); + } + + bool onAvailThread; + rv = aCallback.OnAvailThread(&onAvailThread); + if (NS_FAILED(rv)) { + LOG((" target thread dead?")); + return; + } + + if (!onAvailThread) { + // Dispatch to the right thread + RefPtr event = + new AvailableCallbackRunnable(this, aCallback); + + rv = aCallback.mTarget->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + LOG((" redispatched, (rv = 0x%08" PRIx32 ")", static_cast(rv))); + return; + } + + if (mIsDoomed || aCallback.mNotWanted) { + LOG( + (" doomed or not wanted, notifying OCEA with " + "NS_ERROR_CACHE_KEY_NOT_FOUND")); + aCallback.mCallback->OnCacheEntryAvailable(nullptr, false, + NS_ERROR_CACHE_KEY_NOT_FOUND); + return; + } + + if (state == READY) { + LOG((" ready/has-meta, notifying OCEA with entry and NS_OK")); + + if (!aCallback.mSecret) { + mozilla::MutexAutoLock lock(mLock); + BackgroundOp(Ops::FRECENCYUPDATE); + } + + OnFetched(aCallback); + + RefPtr handle = NewHandle(); + aCallback.mCallback->OnCacheEntryAvailable(handle, false, NS_OK); + return; + } + + // R/O callbacks may do revalidation, let them fall through + if (aCallback.mReadOnly && !aCallback.mRevalidating) { + LOG( + (" r/o and not ready, notifying OCEA with " + "NS_ERROR_CACHE_KEY_NOT_FOUND")); + aCallback.mCallback->OnCacheEntryAvailable(nullptr, false, + NS_ERROR_CACHE_KEY_NOT_FOUND); + return; + } + + // This is a new or potentially non-valid entry and needs to be fetched first. + // The CacheEntryHandle blocks other consumers until the channel + // either releases the entry or marks metadata as filled or whole entry valid, + // i.e. until MetaDataReady() or SetValid() on the entry is called + // respectively. + + // Consumer will be responsible to fill or validate the entry metadata and + // data. + + OnFetched(aCallback); + + RefPtr handle = NewWriteHandle(); + rv = aCallback.mCallback->OnCacheEntryAvailable(handle, state == WRITING, + NS_OK); + + if (NS_FAILED(rv)) { + LOG((" writing/revalidating failed (0x%08" PRIx32 ")", + static_cast(rv))); + + // Consumer given a new entry failed to take care of the entry. + OnHandleClosed(handle); + return; + } + + LOG((" writing/revalidating")); +} + +void CacheEntry::OnFetched(Callback const& aCallback) { + if (NS_SUCCEEDED(mFileStatus) && !aCallback.mSecret) { + // Let the last-fetched and fetch-count properties be updated. + mFile->OnFetched(); + } +} + +CacheEntryHandle* CacheEntry::NewHandle() { return new CacheEntryHandle(this); } + +CacheEntryHandle* CacheEntry::NewWriteHandle() { + mozilla::MutexAutoLock lock(mLock); + + // Ignore the OPEN_SECRETLY flag on purpose here, which should actually be + // used only along with OPEN_READONLY, but there is no need to enforce that. + BackgroundOp(Ops::FRECENCYUPDATE); + + return (mWriter = NewHandle()); +} + +void CacheEntry::OnHandleClosed(CacheEntryHandle const* aHandle) { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::OnHandleClosed [this=%p, state=%s, handle=%p]", this, + StateString(mState), aHandle)); + + if (mIsDoomed && NS_SUCCEEDED(mFileStatus) && + // Note: mHandlesCount is dropped before this method is called + (mHandlesCount == 0 || + (mHandlesCount == 1 && mWriter && mWriter != aHandle))) { + // This entry is no longer referenced from outside and is doomed. + // We can do this also when there is just reference from the writer, + // no one else could ever reach the written data. + // Tell the file to kill the handle, i.e. bypass any I/O operations + // on it except removing the file. + mFile->Kill(); + } + + if (mWriter != aHandle) { + LOG((" not the writer")); + return; + } + + if (mOutputStream) { + LOG((" abandoning phantom output stream")); + // No one took our internal output stream, so there are no data + // and output stream has to be open symultaneously with input stream + // on this entry again. + mHasData = false; + // This asynchronously ends up invoking callbacks on this entry + // through OnOutputClosed() call. + mOutputStream->Close(); + mOutputStream = nullptr; + } else { + // We must always redispatch, otherwise there is a risk of stack + // overflow. This code can recurse deeply. It won't execute sooner + // than we release mLock. + BackgroundOp(Ops::CALLBACKS, true); + } + + mWriter = nullptr; + + if (mState == WRITING) { + LOG((" reverting to state EMPTY - write failed")); + mState = EMPTY; + } else if (mState == REVALIDATING) { + LOG((" reverting to state READY - reval failed")); + mState = READY; + } + + if (mState == READY && !mHasData) { + // We may get to this state when following steps happen: + // 1. a new entry is given to a consumer + // 2. the consumer calls MetaDataReady(), we transit to READY + // 3. abandons the entry w/o opening the output stream, mHasData left false + // + // In this case any following consumer will get a ready entry (with + // metadata) but in state like the entry data write was still happening (was + // in progress) and will indefinitely wait for the entry data or even the + // entry itself when RECHECK_AFTER_WRITE is returned from onCacheEntryCheck. + LOG( + (" we are in READY state, pretend we have data regardless it" + " has actully been never touched")); + mHasData = true; + } +} + +void CacheEntry::OnOutputClosed() { + // Called when the file's output stream is closed. Invoke any callbacks + // waiting for complete entry. + + mozilla::MutexAutoLock lock(mLock); + InvokeCallbacks(); +} + +bool CacheEntry::IsReferenced() const { + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + + // Increasing this counter from 0 to non-null and this check both happen only + // under the service lock. + return mHandlesCount > 0; +} + +bool CacheEntry::IsFileDoomed() { + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->IsDoomed(); + } + + return false; +} + +uint32_t CacheEntry::GetMetadataMemoryConsumption() { + NS_ENSURE_SUCCESS(mFileStatus, 0); + + uint32_t size; + if (NS_FAILED(mFile->ElementsSize(&size))) return 0; + + return size; +} + +// nsICacheEntry + +nsresult CacheEntry::GetPersistent(bool* aPersistToDisk) { + // No need to sync when only reading. + // When consumer needs to be consistent with state of the memory storage + // entries table, then let it use GetUseDisk getter that must be called under + // the service lock. + *aPersistToDisk = mUseDisk; + return NS_OK; +} + +nsresult CacheEntry::GetKey(nsACString& aKey) { + aKey.Assign(mURI); + return NS_OK; +} + +nsresult CacheEntry::GetCacheEntryId(uint64_t* aCacheEntryId) { + *aCacheEntryId = mCacheEntryId; + return NS_OK; +} + +nsresult CacheEntry::GetFetchCount(uint32_t* aFetchCount) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetFetchCount(aFetchCount); +} + +nsresult CacheEntry::GetLastFetched(uint32_t* aLastFetched) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastFetched(aLastFetched); +} + +nsresult CacheEntry::GetLastModified(uint32_t* aLastModified) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastModified(aLastModified); +} + +nsresult CacheEntry::GetExpirationTime(uint32_t* aExpirationTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetExpirationTime(aExpirationTime); +} + +nsresult CacheEntry::GetOnStartTime(uint64_t* aTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + return mFile->GetOnStartTime(aTime); +} + +nsresult CacheEntry::GetOnStopTime(uint64_t* aTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + return mFile->GetOnStopTime(aTime); +} + +nsresult CacheEntry::SetNetworkTimes(uint64_t aOnStartTime, + uint64_t aOnStopTime) { + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->SetNetworkTimes(aOnStartTime, aOnStopTime); + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::SetContentType(uint8_t aContentType) { + NS_ENSURE_ARG_MAX(aContentType, nsICacheEntry::CONTENT_TYPE_LAST - 1); + + if (NS_SUCCEEDED(mFileStatus)) { + return mFile->SetContentType(aContentType); + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::GetIsForcedValid(bool* aIsForcedValid) { + NS_ENSURE_ARG(aIsForcedValid); + +#ifdef DEBUG + { + mozilla::MutexAutoLock lock(mLock); + MOZ_ASSERT(mState > LOADING); + } +#endif + if (mPinned) { + *aIsForcedValid = true; + return NS_OK; + } + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + *aIsForcedValid = + CacheStorageService::Self()->IsForcedValidEntry(mStorageID, key); + LOG(("CacheEntry::GetIsForcedValid [this=%p, IsForcedValid=%d]", this, + *aIsForcedValid)); + + return NS_OK; +} + +nsresult CacheEntry::ForceValidFor(uint32_t aSecondsToTheFuture) { + LOG(("CacheEntry::ForceValidFor [this=%p, aSecondsToTheFuture=%d]", this, + aSecondsToTheFuture)); + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + CacheStorageService::Self()->ForceEntryValidFor(mStorageID, key, + aSecondsToTheFuture); + + return NS_OK; +} + +nsresult CacheEntry::MarkForcedValidUse() { + LOG(("CacheEntry::MarkForcedValidUse [this=%p, ]", this)); + + nsAutoCString key; + nsresult rv = HashingKey(key); + if (NS_FAILED(rv)) { + return rv; + } + + CacheStorageService::Self()->MarkForcedValidEntryUse(mStorageID, key); + return NS_OK; +} + +nsresult CacheEntry::SetExpirationTime(uint32_t aExpirationTime) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsresult rv = mFile->SetExpirationTime(aExpirationTime); + NS_ENSURE_SUCCESS(rv, rv); + + // Aligned assignment, thus atomic. + mSortingExpirationTime = aExpirationTime; + return NS_OK; +} + +nsresult CacheEntry::OpenInputStream(int64_t offset, nsIInputStream** _retval) { + LOG(("CacheEntry::OpenInputStream [this=%p]", this)); + return OpenInputStreamInternal(offset, nullptr, _retval); +} + +nsresult CacheEntry::OpenAlternativeInputStream(const nsACString& type, + nsIInputStream** _retval) { + LOG(("CacheEntry::OpenAlternativeInputStream [this=%p, type=%s]", this, + PromiseFlatCString(type).get())); + return OpenInputStreamInternal(0, PromiseFlatCString(type).get(), _retval); +} + +nsresult CacheEntry::OpenInputStreamInternal(int64_t offset, + const char* aAltDataType, + nsIInputStream** _retval) { + LOG(("CacheEntry::OpenInputStreamInternal [this=%p]", this)); + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsresult rv; + + RefPtr selfHandle = NewHandle(); + + nsCOMPtr stream; + if (aAltDataType) { + rv = mFile->OpenAlternativeInputStream(selfHandle, aAltDataType, + getter_AddRefs(stream)); + if (NS_FAILED(rv)) { + // Failure of this method may be legal when the alternative data requested + // is not avaialble or of a different type. Console error logs are + // ensured by CacheFile::OpenAlternativeInputStream. + return rv; + } + } else { + rv = mFile->OpenInputStream(selfHandle, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr seekable = do_QueryInterface(stream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData) { + // So far output stream on this new entry not opened, do it now. + LOG((" creating phantom output stream")); + rv = OpenOutputStreamInternal(0, getter_AddRefs(mOutputStream)); + NS_ENSURE_SUCCESS(rv, rv); + } + + stream.forget(_retval); + return NS_OK; +} + +nsresult CacheEntry::OpenOutputStream(int64_t offset, int64_t predictedSize, + nsIOutputStream** _retval) { + LOG(("CacheEntry::OpenOutputStream [this=%p]", this)); + + nsresult rv; + + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState > EMPTY); + + if (mFile->EntryWouldExceedLimit(0, predictedSize, false)) { + LOG((" entry would exceed size limit")); + return NS_ERROR_FILE_TOO_BIG; + } + + if (mOutputStream && !mIsDoomed) { + LOG((" giving phantom output stream")); + mOutputStream.forget(_retval); + } else { + rv = OpenOutputStreamInternal(offset, _retval); + if (NS_FAILED(rv)) return rv; + } + + // Entry considered ready when writer opens output stream. + if (mState < READY) mState = READY; + + // Invoke any pending readers now. + InvokeCallbacks(); + + return NS_OK; +} + +nsresult CacheEntry::OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) { + LOG(("CacheEntry::OpenAlternativeOutputStream [this=%p, type=%s]", this, + PromiseFlatCString(type).get())); + + nsresult rv; + + if (type.IsEmpty()) { + // The empty string is reserved to mean no alt-data available. + return NS_ERROR_INVALID_ARG; + } + + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData || mState < READY || mOutputStream || mIsDoomed) { + LOG((" entry not in state to write alt-data")); + return NS_ERROR_NOT_AVAILABLE; + } + + if (mFile->EntryWouldExceedLimit(0, predictedSize, true)) { + LOG((" entry would exceed size limit")); + return NS_ERROR_FILE_TOO_BIG; + } + + nsCOMPtr stream; + rv = mFile->OpenAlternativeOutputStream( + nullptr, PromiseFlatCString(type).get(), getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + stream.swap(*_retval); + return NS_OK; +} + +nsresult CacheEntry::OpenOutputStreamInternal(int64_t offset, + nsIOutputStream** _retval) { + LOG(("CacheEntry::OpenOutputStreamInternal [this=%p]", this)); + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + mLock.AssertCurrentThreadOwns(); + + if (mIsDoomed) { + LOG((" doomed...")); + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT(mState > LOADING); + + nsresult rv; + + // No need to sync on mUseDisk here, we don't need to be consistent + // with content of the memory storage entries hash table. + if (!mUseDisk) { + rv = mFile->SetMemoryOnly(); + NS_ENSURE_SUCCESS(rv, rv); + } + + RefPtr listener = + new CacheOutputCloseListener(this); + + nsCOMPtr stream; + rv = mFile->OpenOutputStream(listener, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr seekable = do_QueryInterface(stream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = seekable->Seek(nsISeekableStream::NS_SEEK_SET, offset); + NS_ENSURE_SUCCESS(rv, rv); + + // Prevent opening output stream again. + mHasData = true; + + stream.swap(*_retval); + return NS_OK; +} + +nsresult CacheEntry::GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo) { + { + mozilla::MutexAutoLock lock(mLock); + if (mSecurityInfoLoaded) { + *aSecurityInfo = do_AddRef(mSecurityInfo).take(); + return NS_OK; + } + } + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsCString info; + nsresult rv = mFile->GetElement("security-info", getter_Copies(info)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr securityInfo; + if (!info.IsVoid()) { + rv = mozilla::psm::TransportSecurityInfo::Read( + info, getter_AddRefs(securityInfo)); + NS_ENSURE_SUCCESS(rv, rv); + } + if (!securityInfo) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo.swap(securityInfo); + mSecurityInfoLoaded = true; + + *aSecurityInfo = do_AddRef(mSecurityInfo).take(); + } + + return NS_OK; +} + +nsresult CacheEntry::SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo) { + nsresult rv; + + NS_ENSURE_SUCCESS(mFileStatus, mFileStatus); + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo = aSecurityInfo; + mSecurityInfoLoaded = true; + } + + nsCString info; + if (aSecurityInfo) { + rv = aSecurityInfo->ToString(info); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mFile->SetElement("security-info", info.Length() ? info.get() : nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheEntry::GetStorageDataSize(uint32_t* aStorageDataSize) { + NS_ENSURE_ARG(aStorageDataSize); + + int64_t dataSize; + nsresult rv = GetDataSize(&dataSize); + if (NS_FAILED(rv)) return rv; + + *aStorageDataSize = (uint32_t)std::min(int64_t(uint32_t(-1)), dataSize); + + return NS_OK; +} + +nsresult CacheEntry::AsyncDoom(nsICacheEntryDoomCallback* aCallback) { + LOG(("CacheEntry::AsyncDoom [this=%p]", this)); + + { + mozilla::MutexAutoLock lock(mLock); + + if (mIsDoomed || mDoomCallback) { + return NS_ERROR_IN_PROGRESS; // to aggregate have DOOMING state + } + + RemoveForcedValidity(); + + mIsDoomed = true; + mDoomCallback = aCallback; + } + + // This immediately removes the entry from the master hashtable and also + // immediately dooms the file. This way we make sure that any consumer + // after this point asking for the same entry won't get + // a) this entry + // b) a new entry with the same file + PurgeAndDoom(); + + return NS_OK; +} + +nsresult CacheEntry::GetMetaDataElement(const char* aKey, char** aRetval) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetElement(aKey, aRetval); +} + +nsresult CacheEntry::SetMetaDataElement(const char* aKey, const char* aValue) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->SetElement(aKey, aValue); +} + +nsresult CacheEntry::VisitMetaData(nsICacheEntryMetaDataVisitor* aVisitor) { + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->VisitMetaData(aVisitor); +} + +nsresult CacheEntry::MetaDataReady() { + mozilla::MutexAutoLock lock(mLock); + + LOG(("CacheEntry::MetaDataReady [this=%p, state=%s]", this, + StateString(mState))); + + MOZ_ASSERT(mState > EMPTY); + + if (mState == WRITING) mState = READY; + + InvokeCallbacks(); + + return NS_OK; +} + +nsresult CacheEntry::SetValid() { + nsCOMPtr outputStream; + + { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::SetValid [this=%p, state=%s]", this, + StateString(mState))); + + MOZ_ASSERT(mState > EMPTY); + + mState = READY; + mHasData = true; + + InvokeCallbacks(); + + outputStream.swap(mOutputStream); + } + + if (outputStream) { + LOG((" abandoning phantom output stream")); + outputStream->Close(); + } + + return NS_OK; +} + +nsresult CacheEntry::Recreate(bool aMemoryOnly, nsICacheEntry** _retval) { + mozilla::MutexAutoLock lock(mLock); + LOG(("CacheEntry::Recreate [this=%p, state=%s]", this, StateString(mState))); + + RefPtr handle = ReopenTruncated(aMemoryOnly, nullptr); + if (handle) { + handle.forget(_retval); + return NS_OK; + } + + BackgroundOp(Ops::CALLBACKS, true); + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult CacheEntry::GetDataSize(int64_t* aDataSize) { + LOG(("CacheEntry::GetDataSize [this=%p]", this)); + *aDataSize = 0; + + { + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData) { + LOG((" write in progress (no data)")); + return NS_ERROR_IN_PROGRESS; + } + } + + NS_ENSURE_SUCCESS(mFileStatus, mFileStatus); + + // mayhemer: TODO Problem with compression? + if (!mFile->DataSize(aDataSize)) { + LOG((" write in progress (stream active)")); + return NS_ERROR_IN_PROGRESS; + } + + LOG((" size=%" PRId64, *aDataSize)); + return NS_OK; +} + +nsresult CacheEntry::GetAltDataSize(int64_t* aDataSize) { + LOG(("CacheEntry::GetAltDataSize [this=%p]", this)); + if (NS_FAILED(mFileStatus)) { + return mFileStatus; + } + return mFile->GetAltDataSize(aDataSize); +} + +nsresult CacheEntry::GetAltDataType(nsACString& aType) { + LOG(("CacheEntry::GetAltDataType [this=%p]", this)); + if (NS_FAILED(mFileStatus)) { + return mFileStatus; + } + return mFile->GetAltDataType(aType); +} + +nsresult CacheEntry::MarkValid() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::MaybeMarkValid() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::HasWriteAccess(bool aWriteAllowed, bool* aWriteAccess) { + *aWriteAccess = aWriteAllowed; + return NS_OK; +} + +nsresult CacheEntry::Close() { + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +nsresult CacheEntry::GetDiskStorageSizeInKB(uint32_t* aDiskStorageSize) { + if (NS_FAILED(mFileStatus)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mFile->GetDiskStorageSizeInKB(aDiskStorageSize); +} + +nsresult CacheEntry::GetLoadContextInfo(nsILoadContextInfo** aInfo) { + nsCOMPtr info = CacheFileUtils::ParseKey(mStorageID); + if (!info) { + return NS_ERROR_FAILURE; + } + + info.forget(aInfo); + + return NS_OK; +} + +// nsIRunnable + +NS_IMETHODIMP CacheEntry::Run() { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + mozilla::MutexAutoLock lock(mLock); + + BackgroundOp(mBackgroundOperations.Grab()); + return NS_OK; +} + +// Management methods + +double CacheEntry::GetFrecency() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mFrecency; +} + +uint32_t CacheEntry::GetExpirationTime() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mSortingExpirationTime; +} + +bool CacheEntry::IsRegistered() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mRegistration == REGISTERED; +} + +bool CacheEntry::CanRegister() const { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + return mRegistration == NEVERREGISTERED; +} + +void CacheEntry::SetRegistered(bool aRegistered) { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + if (aRegistered) { + MOZ_ASSERT(mRegistration == NEVERREGISTERED); + mRegistration = REGISTERED; + } else { + MOZ_ASSERT(mRegistration == REGISTERED); + mRegistration = DEREGISTERED; + } +} + +bool CacheEntry::DeferOrBypassRemovalOnPinStatus(bool aPinned) { + LOG(("CacheEntry::DeferOrBypassRemovalOnPinStatus [this=%p]", this)); + + mozilla::MutexAutoLock lock(mLock); + if (mPinningKnown) { + LOG((" pinned=%d, caller=%d", (bool)mPinned, aPinned)); + // Bypass when the pin status of this entry doesn't match the pin status + // caller wants to remove + return mPinned != aPinned; + } + + LOG((" pinning unknown, caller=%d", aPinned)); + // Oterwise, remember to doom after the status is determined for any + // callback opening the entry after this point... + Callback c(this, aPinned); + RememberCallback(c); + // ...and always bypass + return true; +} + +bool CacheEntry::Purge(uint32_t aWhat) { + LOG(("CacheEntry::Purge [this=%p, what=%d]", this, aWhat)); + + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + switch (aWhat) { + case PURGE_DATA_ONLY_DISK_BACKED: + case PURGE_WHOLE_ONLY_DISK_BACKED: + // This is an in-memory only entry, don't purge it + if (!mUseDisk) { + LOG((" not using disk")); + return false; + } + } + + { + mozilla::MutexAutoLock lock(mLock); + + if (mState == WRITING || mState == LOADING || mFrecency == 0) { + // In-progress (write or load) entries should (at least for consistency + // and from the logical point of view) stay in memory. Zero-frecency + // entries are those which have never been given to any consumer, those + // are actually very fresh and should not go just because frecency had not + // been set so far. + LOG((" state=%s, frecency=%1.10f", StateString(mState), mFrecency)); + return false; + } + } + + if (NS_SUCCEEDED(mFileStatus) && mFile->IsWriteInProgress()) { + // The file is used when there are open streams or chunks/metadata still + // waiting for write. In this case, this entry cannot be purged, + // otherwise reopenned entry would may not even find the data on disk - + // CacheFile is not shared and cannot be left orphan when its job is not + // done, hence keep the whole entry. + LOG((" file still under use")); + return false; + } + + switch (aWhat) { + case PURGE_WHOLE_ONLY_DISK_BACKED: + case PURGE_WHOLE: { + if (!CacheStorageService::Self()->RemoveEntry(this, true)) { + LOG((" not purging, still referenced")); + return false; + } + + CacheStorageService::Self()->UnregisterEntry(this); + + // Entry removed it self from control arrays, return true + return true; + } + + case PURGE_DATA_ONLY_DISK_BACKED: { + NS_ENSURE_SUCCESS(mFileStatus, false); + + mFile->ThrowMemoryCachedData(); + + // Entry has been left in control arrays, return false (not purged) + return false; + } + } + + LOG((" ?")); + return false; +} + +void CacheEntry::PurgeAndDoom() { + LOG(("CacheEntry::PurgeAndDoom [this=%p]", this)); + + CacheStorageService::Self()->RemoveEntry(this); + DoomAlreadyRemoved(); +} + +void CacheEntry::DoomAlreadyRemoved() { + LOG(("CacheEntry::DoomAlreadyRemoved [this=%p]", this)); + + mozilla::MutexAutoLock lock(mLock); + + RemoveForcedValidity(); + + mIsDoomed = true; + + // Pretend pinning is know. This entry is now doomed for good, so don't + // bother with defering doom because of unknown pinning state any more. + mPinningKnown = true; + + // This schedules dooming of the file, dooming is ensured to happen + // sooner than demand to open the same file made after this point + // so that we don't get this file for any newer opened entry(s). + DoomFile(); + + // Must force post here since may be indirectly called from + // InvokeCallbacks of this entry and we don't want reentrancy here. + BackgroundOp(Ops::CALLBACKS, true); + // Process immediately when on the management thread. + BackgroundOp(Ops::UNREGISTER); +} + +void CacheEntry::DoomFile() { + nsresult rv = NS_ERROR_NOT_AVAILABLE; + + if (NS_SUCCEEDED(mFileStatus)) { + if (mHandlesCount == 0 || (mHandlesCount == 1 && mWriter)) { + // We kill the file also when there is just reference from the writer, + // no one else could ever reach the written data. Obvisouly also + // when there is no reference at all (should we ever end up here + // in that case.) + // Tell the file to kill the handle, i.e. bypass any I/O operations + // on it except removing the file. + mFile->Kill(); + } + + // Always calls the callback asynchronously. + rv = mFile->Doom(mDoomCallback ? this : nullptr); + if (NS_SUCCEEDED(rv)) { + LOG((" file doomed")); + return; + } + + if (NS_ERROR_FILE_NOT_FOUND == rv) { + // File is set to be just memory-only, notify the callbacks + // and pretend dooming has succeeded. From point of view of + // the entry it actually did - the data is gone and cannot be + // reused. + rv = NS_OK; + } + } + + // Always posts to the main thread. + OnFileDoomed(rv); +} + +void CacheEntry::RemoveForcedValidity() { + mLock.AssertCurrentThreadOwns(); + + nsresult rv; + + if (mIsDoomed) { + return; + } + + nsAutoCString entryKey; + rv = HashingKey(entryKey); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + CacheStorageService::Self()->RemoveEntryForceValid(mStorageID, entryKey); +} + +void CacheEntry::BackgroundOp(uint32_t aOperations, bool aForceAsync) { + mLock.AssertCurrentThreadOwns(); + + if (!CacheStorageService::IsOnManagementThread() || aForceAsync) { + if (mBackgroundOperations.Set(aOperations)) { + CacheStorageService::Self()->Dispatch(this); + } + + LOG(("CacheEntry::BackgroundOp this=%p dipatch of %x", this, aOperations)); + return; + } + + { + mozilla::MutexAutoUnlock unlock(mLock); + + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + if (aOperations & Ops::FRECENCYUPDATE) { + ++mUseCount; + +#ifndef M_LN2 +# define M_LN2 0.69314718055994530942 +#endif + + // Half-life is dynamic, in seconds. + static double half_life = CacheObserver::HalfLifeSeconds(); + // Must convert from seconds to milliseconds since PR_Now() gives usecs. + static double const decay = + (M_LN2 / half_life) / static_cast(PR_USEC_PER_SEC); + + double now_decay = static_cast(PR_Now()) * decay; + + if (mFrecency == 0) { + mFrecency = now_decay; + } else { + // TODO: when C++11 enabled, use std::log1p(n) which is equal to log(n + + // 1) but more precise. + mFrecency = log(exp(mFrecency - now_decay) + 1) + now_decay; + } + LOG(("CacheEntry FRECENCYUPDATE [this=%p, frecency=%1.10f]", this, + mFrecency)); + + // Because CacheFile::Set*() are not thread-safe to use (uses + // WeakReference that is not thread-safe) we must post to the main + // thread... + NS_DispatchToMainThread( + NewRunnableMethod("net::CacheEntry::StoreFrecency", this, + &CacheEntry::StoreFrecency, mFrecency)); + } + + if (aOperations & Ops::REGISTER) { + LOG(("CacheEntry REGISTER [this=%p]", this)); + + CacheStorageService::Self()->RegisterEntry(this); + } + + if (aOperations & Ops::UNREGISTER) { + LOG(("CacheEntry UNREGISTER [this=%p]", this)); + + CacheStorageService::Self()->UnregisterEntry(this); + } + } // unlock + + if (aOperations & Ops::CALLBACKS) { + LOG(("CacheEntry CALLBACKS (invoke) [this=%p]", this)); + + InvokeCallbacks(); + } +} + +void CacheEntry::StoreFrecency(double aFrecency) { + MOZ_ASSERT(NS_IsMainThread()); + + if (NS_SUCCEEDED(mFileStatus)) { + mFile->SetFrecency(FRECENCY2INT(aFrecency)); + } +} + +// CacheOutputCloseListener + +CacheOutputCloseListener::CacheOutputCloseListener(CacheEntry* aEntry) + : Runnable("net::CacheOutputCloseListener"), mEntry(aEntry) {} + +void CacheOutputCloseListener::OnOutputClosed() { + // We need this class and to redispatch since this callback is invoked + // under the file's lock and to do the job we need to enter the entry's + // lock too. That would lead to potential deadlocks. + // This function may be reached while XPCOM is already shutting down, + // and we might be unable to obtain the main thread or the sts. #1826661 + + if (NS_IsMainThread()) { + // If we're already on the main thread, dispatch to the main thread instead + // of the sts. Always dispatching to the sts can cause problems late in + // shutdown, when threadpools may no longer be available (bug 1806332). + // + // This may also avoid some unnecessary thread-hops when invoking callbacks, + // which can require that they be called on the main thread. + + nsCOMPtr thread; + nsresult rv = NS_GetMainThread(getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv)) { + MOZ_ALWAYS_SUCCEEDS(thread->Dispatch(do_AddRef(this))); + } + return; + } + + nsCOMPtr sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID); + MOZ_DIAGNOSTIC_ASSERT(sts); + if (sts) { + MOZ_ALWAYS_SUCCEEDS(sts->Dispatch(do_AddRef(this))); + } +} + +NS_IMETHODIMP CacheOutputCloseListener::Run() { + mEntry->OnOutputClosed(); + return NS_OK; +} + +// Memory reporting + +size_t CacheEntry::SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) { + size_t n = 0; + + MutexAutoLock lock(mLock); + n += mCallbacks.ShallowSizeOfExcludingThis(mallocSizeOf); + if (mFile) { + n += mFile->SizeOfIncludingThis(mallocSizeOf); + } + + n += mURI.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mEnhanceID.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mStorageID.SizeOfExcludingThisIfUnshared(mallocSizeOf); + + // mDoomCallback is an arbitrary class that is probably reported elsewhere. + // mOutputStream is reported in mFile. + // mWriter is one of many handles we create, but (intentionally) not keep + // any reference to, so those unfortunately cannot be reported. Handles are + // small, though. + // mSecurityInfo doesn't impl nsISizeOf. + + return n; +} + +size_t CacheEntry::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheEntry.h b/netwerk/cache2/CacheEntry.h new file mode 100644 index 0000000000..68369b152a --- /dev/null +++ b/netwerk/cache2/CacheEntry.h @@ -0,0 +1,583 @@ +/* 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/. */ + +#ifndef CacheEntry__h__ +#define CacheEntry__h__ + +#include "nsICacheEntry.h" +#include "CacheFile.h" + +#include "nsIRunnable.h" +#include "nsIOutputStream.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheEntryDoomCallback.h" +#include "nsITransportSecurityInfo.h" + +#include "nsCOMPtr.h" +#include "nsRefPtrHashtable.h" +#include "nsHashKeys.h" +#include "nsString.h" +#include "nsCOMArray.h" +#include "nsThreadUtils.h" +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "mozilla/TimeStamp.h" + +static inline uint32_t PRTimeToSeconds(PRTime t_usec) { + return uint32_t(t_usec / PR_USEC_PER_SEC); +} + +#define NowInSeconds() PRTimeToSeconds(PR_Now()) + +class nsIOutputStream; +class nsIURI; +class nsIThread; + +namespace mozilla { +namespace net { + +class CacheStorageService; +class CacheStorage; +class CacheOutputCloseListener; +class CacheEntryHandle; + +class CacheEntry final : public nsIRunnable, public CacheFileListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + static uint64_t GetNextId(); + + CacheEntry(const nsACString& aStorageID, const nsACString& aURI, + const nsACString& aEnhanceID, bool aUseDisk, bool aSkipSizeCheck, + bool aPin); + + void AsyncOpen(nsICacheEntryOpenCallback* aCallback, uint32_t aFlags); + + CacheEntryHandle* NewHandle(); + // For a new and recreated entry w/o a callback, we need to wrap it + // with a handle to detect writing consumer is gone. + CacheEntryHandle* NewWriteHandle(); + + // Forwarded to from CacheEntryHandle : nsICacheEntry + nsresult GetKey(nsACString& aKey); + nsresult GetCacheEntryId(uint64_t* aCacheEntryId); + nsresult GetPersistent(bool* aPersistToDisk); + nsresult GetFetchCount(uint32_t* aFetchCount); + nsresult GetLastFetched(uint32_t* aLastFetched); + nsresult GetLastModified(uint32_t* aLastModified); + nsresult GetExpirationTime(uint32_t* aExpirationTime); + nsresult SetExpirationTime(uint32_t expirationTime); + nsresult GetOnStartTime(uint64_t* aTime); + nsresult GetOnStopTime(uint64_t* aTime); + nsresult SetNetworkTimes(uint64_t onStartTime, uint64_t onStopTime); + nsresult SetContentType(uint8_t aContentType); + nsresult ForceValidFor(uint32_t aSecondsToTheFuture); + nsresult GetIsForcedValid(bool* aIsForcedValid); + nsresult MarkForcedValidUse(); + nsresult OpenInputStream(int64_t offset, nsIInputStream** _retval); + nsresult OpenOutputStream(int64_t offset, int64_t predictedSize, + nsIOutputStream** _retval); + nsresult GetSecurityInfo(nsITransportSecurityInfo** aSecurityInfo); + nsresult SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo); + nsresult GetStorageDataSize(uint32_t* aStorageDataSize); + nsresult AsyncDoom(nsICacheEntryDoomCallback* aCallback); + nsresult GetMetaDataElement(const char* key, char** aRetval); + nsresult SetMetaDataElement(const char* key, const char* value); + nsresult VisitMetaData(nsICacheEntryMetaDataVisitor* visitor); + nsresult MetaDataReady(void); + nsresult SetValid(void); + nsresult GetDiskStorageSizeInKB(uint32_t* aDiskStorageSizeInKB); + nsresult Recreate(bool aMemoryOnly, nsICacheEntry** _retval); + nsresult GetDataSize(int64_t* aDataSize); + nsresult GetAltDataSize(int64_t* aDataSize); + nsresult GetAltDataType(nsACString& aAltDataType); + nsresult OpenAlternativeOutputStream(const nsACString& type, + int64_t predictedSize, + nsIAsyncOutputStream** _retval); + nsresult OpenAlternativeInputStream(const nsACString& type, + nsIInputStream** _retval); + nsresult GetLoadContextInfo(nsILoadContextInfo** aInfo); + nsresult Close(void); + nsresult MarkValid(void); + nsresult MaybeMarkValid(void); + nsresult HasWriteAccess(bool aWriteAllowed, bool* aWriteAccess); + + public: + uint32_t GetMetadataMemoryConsumption(); + nsCString const& GetStorageID() const { return mStorageID; } + nsCString const& GetEnhanceID() const { return mEnhanceID; } + nsCString const& GetURI() const { return mURI; } + // Accessible at any time + bool IsUsingDisk() const { return mUseDisk; } + bool IsReferenced() const MOZ_NO_THREAD_SAFETY_ANALYSIS; + bool IsFileDoomed(); + bool IsDoomed() const { return mIsDoomed; } + bool IsPinned() const { return mPinned; } + + // Methods for entry management (eviction from memory), + // called only on the management thread. + + // TODO make these inline + double GetFrecency() const; + uint32_t GetExpirationTime() const; + uint32_t UseCount() const { return mUseCount; } + + bool IsRegistered() const; + bool CanRegister() const; + void SetRegistered(bool aRegistered); + + TimeStamp const& LoadStart() const { return mLoadStart; } + + enum EPurge { + PURGE_DATA_ONLY_DISK_BACKED, + PURGE_WHOLE_ONLY_DISK_BACKED, + PURGE_WHOLE, + }; + + bool DeferOrBypassRemovalOnPinStatus(bool aPinned); + bool Purge(uint32_t aWhat); + void PurgeAndDoom(); + void DoomAlreadyRemoved(); + + nsresult HashingKeyWithStorage(nsACString& aResult) const; + nsresult HashingKey(nsACString& aResult) const; + + static nsresult HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, nsIURI* aURI, + nsACString& aResult); + + static nsresult HashingKey(const nsACString& aStorageID, + const nsACString& aEnhanceID, + const nsACString& aURISpec, nsACString& aResult); + + // Accessed only on the service management thread + double mFrecency{0}; + ::mozilla::Atomic mSortingExpirationTime{ + uint32_t(-1)}; + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf); + + private: + virtual ~CacheEntry(); + + // CacheFileListener + NS_IMETHOD OnFileReady(nsresult aResult, bool aIsNew) override; + NS_IMETHOD OnFileDoomed(nsresult aResult) override; + + // Keep the service alive during life-time of an entry + RefPtr mService; + + // We must monitor when a cache entry whose consumer is responsible + // for writing it the first time gets released. We must then invoke + // waiting callbacks to not break the chain. + class Callback { + public: + Callback(CacheEntry* aEntry, nsICacheEntryOpenCallback* aCallback, + bool aReadOnly, bool aCheckOnAnyThread, bool aSecret); + // Special constructor for Callback objects added to the chain + // just to ensure proper defer dooming (recreation) of this entry. + Callback(CacheEntry* aEntry, bool aDoomWhenFoundInPinStatus); + Callback(Callback const& aThat); + ~Callback(); + + // Called when this callback record changes it's owning entry, + // mainly during recreation. + void ExchangeEntry(CacheEntry* aEntry) MOZ_REQUIRES(aEntry->mLock); + + // Returns true when an entry is about to be "defer" doomed and this is + // a "defer" callback. The caller must hold a lock (this entry is in the + // caller's mCallback array) + bool DeferDoom(bool* aDoom) const; + + // We are raising reference count here to take into account the pending + // callback (that virtually holds a ref to this entry before it gets + // it's pointer). + RefPtr mEntry; + nsCOMPtr mCallback; + nsCOMPtr mTarget; + bool mReadOnly : 1; + bool mRevalidating : 1; + bool mCheckOnAnyThread : 1; + bool mRecheckAfterWrite : 1; + bool mNotWanted : 1; + bool mSecret : 1; + + // These are set only for the defer-doomer Callback instance inserted + // to the callback chain. When any of these is set and also any of + // the corressponding flags on the entry is set, this callback will + // indicate (via DeferDoom()) the entry have to be recreated/doomed. + bool mDoomWhenFoundPinned : 1; + bool mDoomWhenFoundNonPinned : 1; + + nsresult OnCheckThread(bool* aOnCheckThread) const; + nsresult OnAvailThread(bool* aOnAvailThread) const; + }; + + // Since OnCacheEntryAvailable must be invoked on the main thread + // we need a runnable for it... + class AvailableCallbackRunnable : public Runnable { + public: + AvailableCallbackRunnable(CacheEntry* aEntry, Callback const& aCallback) + : Runnable("CacheEntry::AvailableCallbackRunnable"), + mEntry(aEntry), + mCallback(aCallback) {} + + private: + NS_IMETHOD Run() override { + mEntry->InvokeAvailableCallback(mCallback); + return NS_OK; + } + + RefPtr mEntry; + Callback mCallback; + }; + + // Since OnCacheEntryDoomed must be invoked on the main thread + // we need a runnable for it... + class DoomCallbackRunnable : public Runnable { + public: + DoomCallbackRunnable(CacheEntry* aEntry, nsresult aRv) + : Runnable("net::CacheEntry::DoomCallbackRunnable"), + mEntry(aEntry), + mRv(aRv) {} + + private: + NS_IMETHOD Run() override { + nsCOMPtr callback; + { + mozilla::MutexAutoLock lock(mEntry->mLock); + mEntry->mDoomCallback.swap(callback); + } + + if (callback) callback->OnCacheEntryDoomed(mRv); + return NS_OK; + } + + RefPtr mEntry; + nsresult mRv; + }; + + // Starts the load or just invokes the callback, bypasses (when required) + // if busy. Returns true on job done, false on bypass. + bool Open(Callback& aCallback, bool aTruncate, bool aPriority, + bool aBypassIfBusy); + // Loads from disk asynchronously + bool Load(bool aTruncate, bool aPriority); + + void RememberCallback(Callback& aCallback) MOZ_REQUIRES(mLock); + void InvokeCallbacksLock(); + void InvokeCallbacks(); + bool InvokeCallbacks(bool aReadOnly); + bool InvokeCallback(Callback& aCallback); + void InvokeAvailableCallback(Callback const& aCallback); + void OnFetched(Callback const& aCallback); + + nsresult OpenOutputStreamInternal(int64_t offset, nsIOutputStream** _retval); + nsresult OpenInputStreamInternal(int64_t offset, const char* aAltDataType, + nsIInputStream** _retval); + + void OnHandleClosed(CacheEntryHandle const* aHandle); + + private: + friend class CacheEntryHandle; + // Increment/decrements the number of handles keeping this entry. + void AddHandleRef() MOZ_REQUIRES(mLock) { ++mHandlesCount; } + void ReleaseHandleRef() MOZ_REQUIRES(mLock) { --mHandlesCount; } + // Current number of handles keeping this entry. + uint32_t HandlesCount() const MOZ_REQUIRES(mLock) { return mHandlesCount; } + + private: + friend class CacheOutputCloseListener; + void OnOutputClosed(); + + private: + // Schedules a background operation on the management thread. + // When executed on the management thread directly, the operation(s) + // is (are) executed immediately. + void BackgroundOp(uint32_t aOperation, bool aForceAsync = false); + void StoreFrecency(double aFrecency); + + // Called only from DoomAlreadyRemoved() + void DoomFile() MOZ_REQUIRES(mLock); + // When this entry is doomed the first time, this method removes + // any force-valid timing info for this entry. + void RemoveForcedValidity(); + + already_AddRefed ReopenTruncated( + bool aMemoryOnly, nsICacheEntryOpenCallback* aCallback); + void TransferCallbacks(CacheEntry& aFromEntry); + + mozilla::Mutex mLock{"CacheEntry"}; + + // Reflects the number of existing handles for this entry + ::mozilla::ThreadSafeAutoRefCnt mHandlesCount MOZ_GUARDED_BY(mLock); + + nsTArray mCallbacks MOZ_GUARDED_BY(mLock); + nsCOMPtr mDoomCallback; + + // Set in CacheEntry::Load(), only - shouldn't need to be under lock + // XXX FIX? is this correct? + RefPtr mFile; + + // Using ReleaseAcquire since we only control access to mFile with this. + // When mFileStatus is read and found success it is ensured there is mFile and + // that it is after a successful call to Init(). + Atomic mFileStatus{NS_ERROR_NOT_INITIALIZED}; + // Set in constructor + nsCString const mURI; + nsCString const mEnhanceID; + nsCString const mStorageID; + + // mUseDisk, mSkipSizeCheck, mIsDoomed are plain "bool", not "bool:1", + // so as to avoid bitfield races with the byte containing + // mSecurityInfoLoaded et al. See bug 1278524. + // + // Whether it's allowed to persist the data to disk + bool const mUseDisk; + // Whether it should skip max size check. + bool const mSkipSizeCheck; + // Set when entry is doomed with AsyncDoom() or DoomAlreadyRemoved(). + Atomic mIsDoomed{false}; + // The indication of pinning this entry was open with + Atomic mPinned; + + // Following flags are all synchronized with the cache entry lock. + + // Whether security info has already been looked up in metadata. + bool mSecurityInfoLoaded : 1 MOZ_GUARDED_BY(mLock); + // Prevents any callback invocation + bool mPreventCallbacks : 1 MOZ_GUARDED_BY(mLock); + // true: after load and an existing file, or after output stream has been + // opened. + // note - when opening an input stream, and this flag is false, output + // stream is open along ; this makes input streams on new entries + // behave correctly when EOF is reached (WOULD_BLOCK is returned). + // false: after load and a new file, or dropped to back to false when a + // writer fails to open an output stream. + bool mHasData : 1 MOZ_GUARDED_BY(mLock); + // Whether the pinning state of the entry is known (equals to the actual state + // of the cache file) + bool mPinningKnown : 1 MOZ_GUARDED_BY(mLock); + + static char const* StateString(uint32_t aState); + + enum EState { // transiting to: + NOTLOADED = 0, // -> LOADING | EMPTY + LOADING = 1, // -> EMPTY | READY + EMPTY = 2, // -> WRITING + WRITING = 3, // -> EMPTY | READY + READY = 4, // -> REVALIDATING + REVALIDATING = 5 // -> READY + }; + + // State of this entry. + EState mState MOZ_GUARDED_BY(mLock){NOTLOADED}; + + enum ERegistration { + NEVERREGISTERED = 0, // The entry has never been registered + REGISTERED = 1, // The entry is stored in the memory pool index + DEREGISTERED = 2 // The entry has been removed from the pool + }; + + // Accessed only on the management thread. Records the state of registration + // this entry in the memory pool intermediate cache. + ERegistration mRegistration{NEVERREGISTERED}; + + // If a new (empty) entry is requested to open an input stream before + // output stream has been opened, we must open output stream internally + // on CacheFile and hold until writer releases the entry or opens the output + // stream for read (then we trade him mOutputStream). + nsCOMPtr mOutputStream MOZ_GUARDED_BY(mLock); + + // Weak reference to the current writter. There can be more then one + // writer at a time and OnHandleClosed() must be processed only for the + // current one. + CacheEntryHandle* mWriter MOZ_GUARDED_BY(mLock){nullptr}; + + // Background thread scheduled operation. Set (under the lock) one + // of this flags to tell the background thread what to do. + class Ops { + public: + static uint32_t const REGISTER = 1 << 0; + static uint32_t const FRECENCYUPDATE = 1 << 1; + static uint32_t const CALLBACKS = 1 << 2; + static uint32_t const UNREGISTER = 1 << 3; + + Ops() = default; + uint32_t Grab() { + uint32_t flags = mFlags; + mFlags = 0; + return flags; + } + bool Set(uint32_t aFlags) { + if (mFlags & aFlags) return false; + mFlags |= aFlags; + return true; + } + + private: + uint32_t mFlags{0}; + } mBackgroundOperations; + + nsCOMPtr mSecurityInfo; + mozilla::TimeStamp mLoadStart; + uint32_t mUseCount{0}; + + const uint64_t mCacheEntryId; +}; + +class CacheEntryHandle final : public nsICacheEntry { + public: + explicit CacheEntryHandle(CacheEntry* aEntry); + CacheEntry* Entry() const { return mEntry; } + + NS_DECL_THREADSAFE_ISUPPORTS + + // Default implementation is simply safely forwarded. + NS_IMETHOD GetKey(nsACString& aKey) override { return mEntry->GetKey(aKey); } + NS_IMETHOD GetCacheEntryId(uint64_t* aCacheEntryId) override { + return mEntry->GetCacheEntryId(aCacheEntryId); + } + NS_IMETHOD GetPersistent(bool* aPersistent) override { + return mEntry->GetPersistent(aPersistent); + } + NS_IMETHOD GetFetchCount(uint32_t* aFetchCount) override { + return mEntry->GetFetchCount(aFetchCount); + } + NS_IMETHOD GetLastFetched(uint32_t* aLastFetched) override { + return mEntry->GetLastFetched(aLastFetched); + } + NS_IMETHOD GetLastModified(uint32_t* aLastModified) override { + return mEntry->GetLastModified(aLastModified); + } + NS_IMETHOD GetExpirationTime(uint32_t* aExpirationTime) override { + return mEntry->GetExpirationTime(aExpirationTime); + } + NS_IMETHOD SetExpirationTime(uint32_t expirationTime) override { + return mEntry->SetExpirationTime(expirationTime); + } + NS_IMETHOD GetOnStartTime(uint64_t* aOnStartTime) override { + return mEntry->GetOnStartTime(aOnStartTime); + } + NS_IMETHOD GetOnStopTime(uint64_t* aOnStopTime) override { + return mEntry->GetOnStopTime(aOnStopTime); + } + NS_IMETHOD SetNetworkTimes(uint64_t onStartTime, + uint64_t onStopTime) override { + return mEntry->SetNetworkTimes(onStartTime, onStopTime); + } + NS_IMETHOD SetContentType(uint8_t contentType) override { + return mEntry->SetContentType(contentType); + } + NS_IMETHOD ForceValidFor(uint32_t aSecondsToTheFuture) override { + return mEntry->ForceValidFor(aSecondsToTheFuture); + } + NS_IMETHOD GetIsForcedValid(bool* aIsForcedValid) override { + return mEntry->GetIsForcedValid(aIsForcedValid); + } + NS_IMETHOD MarkForcedValidUse() override { + return mEntry->MarkForcedValidUse(); + } + NS_IMETHOD OpenInputStream(int64_t offset, + nsIInputStream** _retval) override { + return mEntry->OpenInputStream(offset, _retval); + } + NS_IMETHOD OpenOutputStream(int64_t offset, int64_t predictedSize, + nsIOutputStream** _retval) override { + return mEntry->OpenOutputStream(offset, predictedSize, _retval); + } + NS_IMETHOD GetSecurityInfo( + nsITransportSecurityInfo** aSecurityInfo) override { + return mEntry->GetSecurityInfo(aSecurityInfo); + } + NS_IMETHOD SetSecurityInfo(nsITransportSecurityInfo* aSecurityInfo) override { + return mEntry->SetSecurityInfo(aSecurityInfo); + } + NS_IMETHOD GetStorageDataSize(uint32_t* aStorageDataSize) override { + return mEntry->GetStorageDataSize(aStorageDataSize); + } + NS_IMETHOD AsyncDoom(nsICacheEntryDoomCallback* listener) override { + return mEntry->AsyncDoom(listener); + } + NS_IMETHOD GetMetaDataElement(const char* key, char** _retval) override { + return mEntry->GetMetaDataElement(key, _retval); + } + NS_IMETHOD SetMetaDataElement(const char* key, const char* value) override { + return mEntry->SetMetaDataElement(key, value); + } + NS_IMETHOD VisitMetaData(nsICacheEntryMetaDataVisitor* visitor) override { + return mEntry->VisitMetaData(visitor); + } + NS_IMETHOD MetaDataReady(void) override { return mEntry->MetaDataReady(); } + NS_IMETHOD SetValid(void) override { return mEntry->SetValid(); } + NS_IMETHOD GetDiskStorageSizeInKB(uint32_t* aDiskStorageSizeInKB) override { + return mEntry->GetDiskStorageSizeInKB(aDiskStorageSizeInKB); + } + NS_IMETHOD Recreate(bool aMemoryOnly, nsICacheEntry** _retval) override { + return mEntry->Recreate(aMemoryOnly, _retval); + } + NS_IMETHOD GetDataSize(int64_t* aDataSize) override { + return mEntry->GetDataSize(aDataSize); + } + NS_IMETHOD GetAltDataSize(int64_t* aAltDataSize) override { + return mEntry->GetAltDataSize(aAltDataSize); + } + NS_IMETHOD GetAltDataType(nsACString& aType) override { + return mEntry->GetAltDataType(aType); + } + NS_IMETHOD OpenAlternativeOutputStream( + const nsACString& type, int64_t predictedSize, + nsIAsyncOutputStream** _retval) override { + return mEntry->OpenAlternativeOutputStream(type, predictedSize, _retval); + } + NS_IMETHOD OpenAlternativeInputStream(const nsACString& type, + nsIInputStream** _retval) override { + return mEntry->OpenAlternativeInputStream(type, _retval); + } + NS_IMETHOD GetLoadContextInfo( + nsILoadContextInfo** aLoadContextInfo) override { + return mEntry->GetLoadContextInfo(aLoadContextInfo); + } + NS_IMETHOD Close(void) override { return mEntry->Close(); } + NS_IMETHOD MarkValid(void) override { return mEntry->MarkValid(); } + NS_IMETHOD MaybeMarkValid(void) override { return mEntry->MaybeMarkValid(); } + NS_IMETHOD HasWriteAccess(bool aWriteAllowed, bool* _retval) override { + return mEntry->HasWriteAccess(aWriteAllowed, _retval); + } + + // Specific implementation: + NS_IMETHOD Dismiss() override; + + private: + virtual ~CacheEntryHandle(); + RefPtr mEntry; + + // This is |false| until Dismiss() was called and prevents OnHandleClosed + // being called more than once. + Atomic mClosed{false}; +}; + +class CacheOutputCloseListener final : public Runnable { + public: + void OnOutputClosed(); + + private: + friend class CacheEntry; + + virtual ~CacheOutputCloseListener() = default; + + NS_DECL_NSIRUNNABLE + explicit CacheOutputCloseListener(CacheEntry* aEntry); + + private: + RefPtr mEntry; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFile.cpp b/netwerk/cache2/CacheFile.cpp new file mode 100644 index 0000000000..02a9f3bce7 --- /dev/null +++ b/netwerk/cache2/CacheFile.cpp @@ -0,0 +1,2589 @@ +/* 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 "CacheFile.h" + +#include +#include + +#include "CacheFileChunk.h" +#include "CacheFileInputStream.h" +#include "CacheFileOutputStream.h" +#include "CacheFileUtils.h" +#include "CacheIndex.h" +#include "CacheLog.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" +#include "nsComponentManagerUtils.h" +#include "nsICacheEntry.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" + +// When CACHE_CHUNKS is defined we always cache unused chunks in mCacheChunks. +// When it is not defined, we always release the chunks ASAP, i.e. we cache +// unused chunks only when: +// - CacheFile is memory-only +// - CacheFile is still waiting for the handle +// - the chunk is preloaded + +// #define CACHE_CHUNKS + +namespace mozilla::net { + +using CacheFileUtils::CacheFileLock; + +class NotifyCacheFileListenerEvent : public Runnable { + public: + NotifyCacheFileListenerEvent(CacheFileListener* aCallback, nsresult aResult, + bool aIsNew) + : Runnable("net::NotifyCacheFileListenerEvent"), + mCallback(aCallback), + mRV(aResult), + mIsNew(aIsNew) { + LOG( + ("NotifyCacheFileListenerEvent::NotifyCacheFileListenerEvent() " + "[this=%p]", + this)); + } + + protected: + ~NotifyCacheFileListenerEvent() { + LOG( + ("NotifyCacheFileListenerEvent::~NotifyCacheFileListenerEvent() " + "[this=%p]", + this)); + } + + public: + NS_IMETHOD Run() override { + LOG(("NotifyCacheFileListenerEvent::Run() [this=%p]", this)); + + mCallback->OnFileReady(mRV, mIsNew); + return NS_OK; + } + + protected: + nsCOMPtr mCallback; + nsresult mRV; + bool mIsNew; +}; + +class NotifyChunkListenerEvent : public Runnable { + public: + NotifyChunkListenerEvent(CacheFileChunkListener* aCallback, nsresult aResult, + uint32_t aChunkIdx, CacheFileChunk* aChunk) + : Runnable("net::NotifyChunkListenerEvent"), + mCallback(aCallback), + mRV(aResult), + mChunkIdx(aChunkIdx), + mChunk(aChunk) { + LOG(("NotifyChunkListenerEvent::NotifyChunkListenerEvent() [this=%p]", + this)); + } + + protected: + ~NotifyChunkListenerEvent() { + LOG(("NotifyChunkListenerEvent::~NotifyChunkListenerEvent() [this=%p]", + this)); + } + + public: + NS_IMETHOD Run() override { + LOG(("NotifyChunkListenerEvent::Run() [this=%p]", this)); + + mCallback->OnChunkAvailable(mRV, mChunkIdx, mChunk); + return NS_OK; + } + + protected: + nsCOMPtr mCallback; + nsresult mRV; + uint32_t mChunkIdx; + RefPtr mChunk; +}; + +class DoomFileHelper : public CacheFileIOListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit DoomFileHelper(CacheFileListener* aListener) + : mListener(aListener) {} + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override { + MOZ_CRASH("DoomFileHelper::OnFileOpened should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override { + MOZ_CRASH("DoomFileHelper::OnDataWritten should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override { + MOZ_CRASH("DoomFileHelper::OnDataRead should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override { + if (mListener) mListener->OnFileDoomed(aResult); + return NS_OK; + } + + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override { + MOZ_CRASH("DoomFileHelper::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, + nsresult aResult) override { + MOZ_CRASH("DoomFileHelper::OnFileRenamed should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + private: + virtual ~DoomFileHelper() = default; + + nsCOMPtr mListener; +}; + +NS_IMPL_ISUPPORTS(DoomFileHelper, CacheFileIOListener) + +NS_IMPL_ADDREF(CacheFile) +NS_IMPL_RELEASE(CacheFile) +NS_INTERFACE_MAP_BEGIN(CacheFile) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileChunkListener) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileIOListener) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileMetadataListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, + mozilla::net::CacheFileChunkListener) +NS_INTERFACE_MAP_END + +CacheFile::CacheFile() : mLock(new CacheFileLock()) { + LOG(("CacheFile::CacheFile() [this=%p]", this)); +} + +CacheFile::~CacheFile() { + LOG(("CacheFile::~CacheFile() [this=%p]", this)); + + MutexAutoLock lock(mLock->Lock()); + if (!mMemoryOnly && mReady && !mKill) { + // mReady flag indicates we have metadata plus in a valid state. + WriteMetadataIfNeededLocked(true); + } +} + +nsresult CacheFile::Init(const nsACString& aKey, bool aCreateNew, + bool aMemoryOnly, bool aSkipSizeCheck, bool aPriority, + bool aPinned, CacheFileListener* aCallback) + MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_ASSERT(!mListener); + MOZ_ASSERT(!mHandle); + + MOZ_ASSERT(!(aMemoryOnly && aPinned)); + + nsresult rv; + + mKey = aKey; + mOpenAsMemoryOnly = mMemoryOnly = aMemoryOnly; + mSkipSizeCheck = aSkipSizeCheck; + mPriority = aPriority; + mPinned = aPinned; + + // Some consumers (at least nsHTTPCompressConv) assume that Read() can read + // such amount of data that was announced by Available(). + // CacheFileInputStream::Available() uses also preloaded chunks to compute + // number of available bytes in the input stream, so we have to make sure the + // preloadChunkCount won't change during CacheFile's lifetime since otherwise + // we could potentially release some cached chunks that was used to calculate + // available bytes but would not be available later during call to + // CacheFileInputStream::Read(). + mPreloadChunkCount = CacheObserver::PreloadChunkCount(); + + LOG( + ("CacheFile::Init() [this=%p, key=%s, createNew=%d, memoryOnly=%d, " + "priority=%d, listener=%p]", + this, mKey.get(), aCreateNew, aMemoryOnly, aPriority, aCallback)); + + if (mMemoryOnly) { + MOZ_ASSERT(!aCallback); + + mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, false, mKey, + WrapNotNull(mLock)); + mReady = true; + mDataSize = mMetadata->Offset(); + return NS_OK; + } + uint32_t flags; + if (aCreateNew) { + MOZ_ASSERT(!aCallback); + flags = CacheFileIOManager::CREATE_NEW; + + // make sure we can use this entry immediately + mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey, + WrapNotNull(mLock)); + mReady = true; + mDataSize = mMetadata->Offset(); + } else { + flags = CacheFileIOManager::CREATE; + } + + if (mPriority) { + flags |= CacheFileIOManager::PRIORITY; + } + + if (mPinned) { + flags |= CacheFileIOManager::PINNED; + } + + mOpeningFile = true; + mListener = aCallback; + rv = CacheFileIOManager::OpenFile(mKey, flags, this); + if (NS_FAILED(rv)) { + mListener = nullptr; + mOpeningFile = false; + + if (mPinned) { + LOG( + ("CacheFile::Init() - CacheFileIOManager::OpenFile() failed " + "but we want to pin, fail the file opening. [this=%p]", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (aCreateNew) { + NS_WARNING("Forcing memory-only entry since OpenFile failed"); + LOG( + ("CacheFile::Init() - CacheFileIOManager::OpenFile() failed " + "synchronously. We can continue in memory-only mode since " + "aCreateNew == true. [this=%p]", + this)); + + mMemoryOnly = true; + } else if (rv == NS_ERROR_NOT_INITIALIZED) { + NS_WARNING( + "Forcing memory-only entry since CacheIOManager isn't " + "initialized."); + LOG( + ("CacheFile::Init() - CacheFileIOManager isn't initialized, " + "initializing entry as memory-only. [this=%p]", + this)); + + mMemoryOnly = true; + mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey, + WrapNotNull(mLock)); + mReady = true; + mDataSize = mMetadata->Offset(); + + RefPtr ev; + ev = new NotifyCacheFileListenerEvent(aCallback, NS_OK, true); + rv = NS_DispatchToCurrentThread(ev); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +void CacheFile::Key(nsACString& aKey) { + CacheFileAutoLock lock(this); + aKey = mKey; +} + +bool CacheFile::IsPinned() { + CacheFileAutoLock lock(this); + return mPinned; +} + +nsresult CacheFile::OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) { + CacheFileAutoLock lock(this); + + nsresult rv; + + uint32_t index = aChunk->Index(); + + LOG(("CacheFile::OnChunkRead() [this=%p, rv=0x%08" PRIx32 + ", chunk=%p, idx=%u]", + this, static_cast(aResult), aChunk, index)); + + if (aChunk->mDiscardedChunk) { + // We discard only unused chunks, so it must be still unused when reading + // data finishes. + MOZ_ASSERT(aChunk->mRefCnt == 2); + aChunk->mActiveChunk = false; + ReleaseOutsideLock( + RefPtr(std::move(aChunk->mFile))); + + DebugOnly removed = mDiscardedChunks.RemoveElement(aChunk); + MOZ_ASSERT(removed); + return NS_OK; + } + + if (NS_FAILED(aResult)) { + SetError(aResult); + } + + if (HaveChunkListeners(index)) { + rv = NotifyChunkListeners(index, aResult, aChunk); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +nsresult CacheFile::OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) { + // In case the chunk was reused, made dirty and released between calls to + // CacheFileChunk::Write() and CacheFile::OnChunkWritten(), we must write + // the chunk to the disk again. When the chunk is unused and is dirty simply + // addref and release (outside the lock) the chunk which ensures that + // CacheFile::DeactivateChunk() will be called again. + RefPtr deactivateChunkAgain; + + CacheFileAutoLock lock(this); + + nsresult rv; + + LOG(("CacheFile::OnChunkWritten() [this=%p, rv=0x%08" PRIx32 + ", chunk=%p, idx=%u]", + this, static_cast(aResult), aChunk, aChunk->Index())); + + MOZ_ASSERT(!mMemoryOnly); + MOZ_ASSERT(!mOpeningFile); + MOZ_ASSERT(mHandle); + + if (aChunk->mDiscardedChunk) { + // We discard only unused chunks, so it must be still unused when writing + // data finishes. + MOZ_ASSERT(aChunk->mRefCnt == 2); + aChunk->mActiveChunk = false; + ReleaseOutsideLock( + RefPtr(std::move(aChunk->mFile))); + + DebugOnly removed = mDiscardedChunks.RemoveElement(aChunk); + MOZ_ASSERT(removed); + return NS_OK; + } + + if (NS_FAILED(aResult)) { + SetError(aResult); + } + + if (NS_SUCCEEDED(aResult) && !aChunk->IsDirty()) { + // update hash value in metadata + mMetadata->SetHash(aChunk->Index(), aChunk->Hash()); + } + + // notify listeners if there is any + if (HaveChunkListeners(aChunk->Index())) { + // don't release the chunk since there are some listeners queued + rv = NotifyChunkListeners(aChunk->Index(), aResult, aChunk); + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(aChunk->mRefCnt != 2); + return NS_OK; + } + } + + if (aChunk->mRefCnt != 2) { + LOG( + ("CacheFile::OnChunkWritten() - Chunk is still used [this=%p, chunk=%p," + " refcnt=%" PRIuPTR "]", + this, aChunk, aChunk->mRefCnt.get())); + + return NS_OK; + } + + if (aChunk->IsDirty()) { + LOG( + ("CacheFile::OnChunkWritten() - Unused chunk is dirty. We must go " + "through deactivation again. [this=%p, chunk=%p]", + this, aChunk)); + + deactivateChunkAgain = aChunk; + return NS_OK; + } + + bool keepChunk = false; + if (NS_SUCCEEDED(aResult)) { + keepChunk = ShouldCacheChunk(aChunk->Index()); + LOG(("CacheFile::OnChunkWritten() - %s unused chunk [this=%p, chunk=%p]", + keepChunk ? "Caching" : "Releasing", this, aChunk)); + } else { + LOG( + ("CacheFile::OnChunkWritten() - Releasing failed chunk [this=%p, " + "chunk=%p]", + this, aChunk)); + } + + RemoveChunkInternal(aChunk, keepChunk); + + WriteMetadataIfNeededLocked(); + + return NS_OK; +} + +nsresult CacheFile::OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFile::OnChunkAvailable should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFile::OnChunkUpdated(CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFile::OnChunkUpdated should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFile::OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) { + // Using an 'auto' class to perform doom or fail the listener + // outside the CacheFile's lock. + class AutoFailDoomListener { + public: + explicit AutoFailDoomListener(CacheFileHandle* aHandle) + : mHandle(aHandle), mAlreadyDoomed(false) {} + ~AutoFailDoomListener() { + if (!mListener) return; + + if (mHandle) { + if (mAlreadyDoomed) { + mListener->OnFileDoomed(mHandle, NS_OK); + } else { + CacheFileIOManager::DoomFile(mHandle, mListener); + } + } else { + mListener->OnFileDoomed(nullptr, NS_ERROR_NOT_AVAILABLE); + } + } + + CacheFileHandle* mHandle; + nsCOMPtr mListener; + bool mAlreadyDoomed; + } autoDoom(aHandle); + + RefPtr metadata; + nsCOMPtr listener; + bool isNew = false; + nsresult retval = NS_OK; + + { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mOpeningFile); + MOZ_ASSERT((NS_SUCCEEDED(aResult) && aHandle) || + (NS_FAILED(aResult) && !aHandle)); + MOZ_ASSERT((mListener && !mMetadata) || // !createNew + (!mListener && mMetadata)); // createNew + MOZ_ASSERT(!mMemoryOnly || mMetadata); // memory-only was set on new entry + + LOG(("CacheFile::OnFileOpened() [this=%p, rv=0x%08" PRIx32 ", handle=%p]", + this, static_cast(aResult), aHandle)); + + mOpeningFile = false; + + autoDoom.mListener.swap(mDoomAfterOpenListener); + + if (mMemoryOnly) { + // We can be here only in case the entry was initilized as createNew and + // SetMemoryOnly() was called. + + // Just don't store the handle into mHandle and exit + autoDoom.mAlreadyDoomed = true; + return NS_OK; + } + + if (NS_FAILED(aResult)) { + if (mMetadata) { + // This entry was initialized as createNew, just switch to memory-only + // mode. + NS_WARNING("Forcing memory-only entry since OpenFile failed"); + LOG( + ("CacheFile::OnFileOpened() - CacheFileIOManager::OpenFile() " + "failed asynchronously. We can continue in memory-only mode since " + "aCreateNew == true. [this=%p]", + this)); + + mMemoryOnly = true; + return NS_OK; + } + + if (aResult == NS_ERROR_FILE_INVALID_PATH) { + // CacheFileIOManager doesn't have mCacheDirectory, switch to + // memory-only mode. + NS_WARNING( + "Forcing memory-only entry since CacheFileIOManager doesn't " + "have mCacheDirectory."); + LOG( + ("CacheFile::OnFileOpened() - CacheFileIOManager doesn't have " + "mCacheDirectory, initializing entry as memory-only. [this=%p]", + this)); + + mMemoryOnly = true; + mMetadata = new CacheFileMetadata(mOpenAsMemoryOnly, mPinned, mKey, + WrapNotNull(mLock)); + mReady = true; + mDataSize = mMetadata->Offset(); + + isNew = true; + retval = NS_OK; + } else { + // CacheFileIOManager::OpenFile() failed for another reason. + isNew = false; + retval = aResult; + } + + mListener.swap(listener); + } else { + mHandle = aHandle; + if (NS_FAILED(mStatus)) { + CacheFileIOManager::DoomFile(mHandle, nullptr); + } + + if (mMetadata) { + InitIndexEntry(); + + // The entry was initialized as createNew, don't try to read metadata. + mMetadata->SetHandle(mHandle); + + // Write all cached chunks, otherwise they may stay unwritten. + for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + RefPtr& chunk = iter.Data(); + + LOG(("CacheFile::OnFileOpened() - write [this=%p, idx=%u, chunk=%p]", + this, idx, chunk.get())); + + mChunks.InsertOrUpdate(idx, RefPtr{chunk}); + chunk->mFile = this; + chunk->mActiveChunk = true; + + MOZ_ASSERT(chunk->IsReady()); + + // This would be cleaner if we had an nsRefPtr constructor that took + // a RefPtr. + ReleaseOutsideLock(std::move(chunk)); + + iter.Remove(); + } + + return NS_OK; + } + } + if (listener) { + lock.Unlock(); + listener->OnFileReady(retval, isNew); + return NS_OK; + } + + MOZ_ASSERT(NS_SUCCEEDED(aResult)); + MOZ_ASSERT(!mMetadata); + MOZ_ASSERT(mListener); + + // mMetaData is protected by a lock, but ReadMetaData has to be called + // without the lock. Alternatively we could make a + // "ReadMetaDataLocked", and temporarily unlock to call OnFileReady + metadata = mMetadata = + new CacheFileMetadata(mHandle, mKey, WrapNotNull(mLock)); + } + metadata->ReadMetadata(this); + return NS_OK; +} + +nsresult CacheFile::OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) { + MOZ_CRASH("CacheFile::OnDataWritten should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFile::OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) { + MOZ_CRASH("CacheFile::OnDataRead should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFile::OnMetadataRead(nsresult aResult) { + nsCOMPtr listener; + bool isNew = false; + { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mListener); + + LOG(("CacheFile::OnMetadataRead() [this=%p, rv=0x%08" PRIx32 "]", this, + static_cast(aResult))); + + if (NS_SUCCEEDED(aResult)) { + mPinned = mMetadata->Pinned(); + mReady = true; + mDataSize = mMetadata->Offset(); + if (mDataSize == 0 && mMetadata->ElementsSize() == 0) { + isNew = true; + mMetadata->MarkDirty(); + } else { + const char* altData = + mMetadata->GetElement(CacheFileUtils::kAltDataKey); + if (altData && (NS_FAILED(CacheFileUtils::ParseAlternativeDataInfo( + altData, &mAltDataOffset, &mAltDataType)) || + (mAltDataOffset > mDataSize))) { + // alt-metadata cannot be parsed or alt-data offset is invalid + mMetadata->InitEmptyMetadata(); + isNew = true; + mAltDataOffset = -1; + mAltDataType.Truncate(); + mDataSize = 0; + } else { + PreloadChunks(0); + } + } + + InitIndexEntry(); + } + + mListener.swap(listener); + } + listener->OnFileReady(aResult, isNew); + return NS_OK; +} + +nsresult CacheFile::OnMetadataWritten(nsresult aResult) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::OnMetadataWritten() [this=%p, rv=0x%08" PRIx32 "]", this, + static_cast(aResult))); + + MOZ_ASSERT(mWritingMetadata); + mWritingMetadata = false; + + MOZ_ASSERT(!mMemoryOnly); + MOZ_ASSERT(!mOpeningFile); + + if (NS_WARN_IF(NS_FAILED(aResult))) { + // TODO close streams with an error ??? + SetError(aResult); + } + + if (mOutput || mInputs.Length() || mChunks.Count()) return NS_OK; + + if (IsDirty()) WriteMetadataIfNeededLocked(); + + if (!mWritingMetadata) { + LOG(("CacheFile::OnMetadataWritten() - Releasing file handle [this=%p]", + this)); + CacheFileIOManager::ReleaseNSPRHandle(mHandle); + } + + return NS_OK; +} + +nsresult CacheFile::OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) { + nsCOMPtr listener; + + { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mListener); + + LOG(("CacheFile::OnFileDoomed() [this=%p, rv=0x%08" PRIx32 ", handle=%p]", + this, static_cast(aResult), aHandle)); + + mListener.swap(listener); + } + + listener->OnFileDoomed(aResult); + return NS_OK; +} + +nsresult CacheFile::OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheFile::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFile::OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheFile::OnFileRenamed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +bool CacheFile::IsKilled() { + bool killed = mKill; + if (killed) { + LOG(("CacheFile is killed, this=%p", this)); + } + + return killed; +} + +nsresult CacheFile::OpenInputStream(nsICacheEntry* aEntryHandle, + nsIInputStream** _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + if (!mReady) { + LOG(("CacheFile::OpenInputStream() - CacheFile is not ready [this=%p]", + this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (NS_FAILED(mStatus)) { + LOG( + ("CacheFile::OpenInputStream() - CacheFile is in a failure state " + "[this=%p, status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + // Don't allow opening the input stream when this CacheFile is in + // a failed state. This is the only way to protect consumers correctly + // from reading a broken entry. When the file is in the failed state, + // it's also doomed, so reopening the entry won't make any difference - + // data will still be inaccessible anymore. Note that for just doomed + // files, we must allow reading the data. + return mStatus; + } + + // Once we open input stream we no longer allow preloading of chunks without + // input stream, i.e. we will no longer keep first few chunks preloaded when + // the last input stream is closed. + mPreloadWithoutInputStreams = false; + + CacheFileInputStream* input = + new CacheFileInputStream(this, aEntryHandle, false); + LOG(("CacheFile::OpenInputStream() - Creating new input stream %p [this=%p]", + input, this)); + + mInputs.AppendElement(input); + NS_ADDREF(input); + + mDataAccessed = true; + *_retval = do_AddRef(input).take(); + return NS_OK; +} + +nsresult CacheFile::OpenAlternativeInputStream(nsICacheEntry* aEntryHandle, + const char* aAltDataType, + nsIInputStream** _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + if (NS_WARN_IF(!mReady)) { + LOG( + ("CacheFile::OpenAlternativeInputStream() - CacheFile is not ready " + "[this=%p]", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (mAltDataOffset == -1) { + LOG( + ("CacheFile::OpenAlternativeInputStream() - Alternative data is not " + "available [this=%p]", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (NS_FAILED(mStatus)) { + LOG( + ("CacheFile::OpenAlternativeInputStream() - CacheFile is in a failure " + "state [this=%p, status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + // Don't allow opening the input stream when this CacheFile is in + // a failed state. This is the only way to protect consumers correctly + // from reading a broken entry. When the file is in the failed state, + // it's also doomed, so reopening the entry won't make any difference - + // data will still be inaccessible anymore. Note that for just doomed + // files, we must allow reading the data. + return mStatus; + } + + if (mAltDataType != aAltDataType) { + LOG( + ("CacheFile::OpenAlternativeInputStream() - Alternative data is of a " + "different type than requested [this=%p, availableType=%s, " + "requestedType=%s]", + this, mAltDataType.get(), aAltDataType)); + return NS_ERROR_NOT_AVAILABLE; + } + + // Once we open input stream we no longer allow preloading of chunks without + // input stream, i.e. we will no longer keep first few chunks preloaded when + // the last input stream is closed. + mPreloadWithoutInputStreams = false; + + CacheFileInputStream* input = + new CacheFileInputStream(this, aEntryHandle, true); + + LOG( + ("CacheFile::OpenAlternativeInputStream() - Creating new input stream %p " + "[this=%p]", + input, this)); + + mInputs.AppendElement(input); + NS_ADDREF(input); + + mDataAccessed = true; + *_retval = do_AddRef(input).take(); + + return NS_OK; +} + +nsresult CacheFile::OpenOutputStream(CacheOutputCloseListener* aCloseListener, + nsIOutputStream** _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + nsresult rv; + + if (!mReady) { + LOG(("CacheFile::OpenOutputStream() - CacheFile is not ready [this=%p]", + this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (mOutput) { + LOG( + ("CacheFile::OpenOutputStream() - We already have output stream %p " + "[this=%p]", + mOutput, this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (NS_FAILED(mStatus)) { + LOG( + ("CacheFile::OpenOutputStream() - CacheFile is in a failure state " + "[this=%p, status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + // The CacheFile is already doomed. It make no sense to allow to write any + // data to such entry. + return mStatus; + } + + // Fail if there is any input stream opened for alternative data + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + if (mInputs[i]->IsAlternativeData()) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + if (mAltDataOffset != -1) { + // Remove alt-data + rv = Truncate(mAltDataOffset); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::OpenOutputStream() - Truncating alt-data failed " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + SetAltMetadata(nullptr); + mAltDataOffset = -1; + mAltDataType.Truncate(); + } + + // Once we open output stream we no longer allow preloading of chunks without + // input stream. There is no reason to believe that some input stream will be + // opened soon. Otherwise we would cache unused chunks of all newly created + // entries until the CacheFile is destroyed. + mPreloadWithoutInputStreams = false; + + mOutput = new CacheFileOutputStream(this, aCloseListener, false); + + LOG( + ("CacheFile::OpenOutputStream() - Creating new output stream %p " + "[this=%p]", + mOutput, this)); + + mDataAccessed = true; + *_retval = do_AddRef(mOutput).take(); + return NS_OK; +} + +nsresult CacheFile::OpenAlternativeOutputStream( + CacheOutputCloseListener* aCloseListener, const char* aAltDataType, + nsIAsyncOutputStream** _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + if (!mReady) { + LOG( + ("CacheFile::OpenAlternativeOutputStream() - CacheFile is not ready " + "[this=%p]", + this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (mOutput) { + LOG( + ("CacheFile::OpenAlternativeOutputStream() - We already have output " + "stream %p [this=%p]", + mOutput, this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (NS_FAILED(mStatus)) { + LOG( + ("CacheFile::OpenAlternativeOutputStream() - CacheFile is in a failure " + "state [this=%p, status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + // The CacheFile is already doomed. It make no sense to allow to write any + // data to such entry. + return mStatus; + } + + // Fail if there is any input stream opened for alternative data + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + if (mInputs[i]->IsAlternativeData()) { + return NS_ERROR_NOT_AVAILABLE; + } + } + + nsresult rv; + + if (mAltDataOffset != -1) { + // Truncate old alt-data + rv = Truncate(mAltDataOffset); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::OpenAlternativeOutputStream() - Truncating old alt-data " + "failed [rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + } else { + mAltDataOffset = mDataSize; + } + + nsAutoCString altMetadata; + CacheFileUtils::BuildAlternativeDataInfo(aAltDataType, mAltDataOffset, + altMetadata); + rv = SetAltMetadata(altMetadata.get()); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::OpenAlternativeOutputStream() - Set Metadata for alt-data" + "failed [rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + + // Once we open output stream we no longer allow preloading of chunks without + // input stream. There is no reason to believe that some input stream will be + // opened soon. Otherwise we would cache unused chunks of all newly created + // entries until the CacheFile is destroyed. + mPreloadWithoutInputStreams = false; + + mOutput = new CacheFileOutputStream(this, aCloseListener, true); + + LOG( + ("CacheFile::OpenAlternativeOutputStream() - Creating new output stream " + "%p [this=%p]", + mOutput, this)); + + mDataAccessed = true; + mAltDataType = aAltDataType; + *_retval = do_AddRef(mOutput).take(); + return NS_OK; +} + +nsresult CacheFile::SetMemoryOnly() { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetMemoryOnly() mMemoryOnly=%d [this=%p]", mMemoryOnly, + this)); + + if (mMemoryOnly) return NS_OK; + + MOZ_ASSERT(mReady); + + if (!mReady) { + LOG(("CacheFile::SetMemoryOnly() - CacheFile is not ready [this=%p]", + this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (mDataAccessed) { + LOG(("CacheFile::SetMemoryOnly() - Data was already accessed [this=%p]", + this)); + return NS_ERROR_NOT_AVAILABLE; + } + + // TODO what to do when this isn't a new entry and has an existing metadata??? + mMemoryOnly = true; + return NS_OK; +} + +nsresult CacheFile::Doom(CacheFileListener* aCallback) { + LOG(("CacheFile::Doom() [this=%p, listener=%p]", this, aCallback)); + + CacheFileAutoLock lock(this); + + return DoomLocked(aCallback); +} + +nsresult CacheFile::DoomLocked(CacheFileListener* aCallback) { + AssertOwnsLock(); + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + LOG(("CacheFile::DoomLocked() [this=%p, listener=%p]", this, aCallback)); + + nsresult rv = NS_OK; + + if (mMemoryOnly) { + return NS_ERROR_FILE_NOT_FOUND; + } + + if (mHandle && mHandle->IsDoomed()) { + return NS_ERROR_FILE_NOT_FOUND; + } + + nsCOMPtr listener; + if (aCallback || !mHandle) { + listener = new DoomFileHelper(aCallback); + } + if (mHandle) { + rv = CacheFileIOManager::DoomFile(mHandle, listener); + } else if (mOpeningFile) { + mDoomAfterOpenListener = listener; + } + + return rv; +} + +nsresult CacheFile::ThrowMemoryCachedData() { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::ThrowMemoryCachedData() [this=%p]", this)); + + if (mMemoryOnly) { + // This method should not be called when the CacheFile was initialized as + // memory-only, but it can be called when CacheFile end up as memory-only + // due to e.g. IO failure since CacheEntry doesn't know it. + LOG( + ("CacheFile::ThrowMemoryCachedData() - Ignoring request because the " + "entry is memory-only. [this=%p]", + this)); + + return NS_ERROR_NOT_AVAILABLE; + } + + if (mOpeningFile) { + // mayhemer, note: we shouldn't get here, since CacheEntry prevents loading + // entries from being purged. + + LOG( + ("CacheFile::ThrowMemoryCachedData() - Ignoring request because the " + "entry is still opening the file [this=%p]", + this)); + + return NS_ERROR_ABORT; + } + + // We cannot release all cached chunks since we need to keep preloaded chunks + // in memory. See initialization of mPreloadChunkCount for explanation. + CleanUpCachedChunks(); + + return NS_OK; +} + +nsresult CacheFile::GetElement(const char* aKey, char** _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + const char* value; + value = mMetadata->GetElement(aKey); + if (!value) return NS_ERROR_NOT_AVAILABLE; + + *_retval = NS_xstrdup(value); + return NS_OK; +} + +nsresult CacheFile::SetElement(const char* aKey, const char* aValue) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetElement() this=%p", this)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + if (!strcmp(aKey, CacheFileUtils::kAltDataKey)) { + NS_ERROR( + "alt-data element is reserved for internal use and must not be " + "changed via CacheFile::SetElement()"); + return NS_ERROR_FAILURE; + } + + PostWriteTimer(); + return mMetadata->SetElement(aKey, aValue); +} + +nsresult CacheFile::VisitMetaData(nsICacheEntryMetaDataVisitor* aVisitor) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + MOZ_ASSERT(mReady); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + mMetadata->Visit(aVisitor); + return NS_OK; +} + +nsresult CacheFile::ElementsSize(uint32_t* _retval) { + CacheFileAutoLock lock(this); + + if (!mMetadata) return NS_ERROR_NOT_AVAILABLE; + + *_retval = mMetadata->ElementsSize(); + return NS_OK; +} + +nsresult CacheFile::SetExpirationTime(uint32_t aExpirationTime) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetExpirationTime() this=%p, expiration=%u", this, + aExpirationTime)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + mMetadata->SetExpirationTime(aExpirationTime); + return NS_OK; +} + +nsresult CacheFile::GetExpirationTime(uint32_t* _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + *_retval = mMetadata->GetExpirationTime(); + return NS_OK; +} + +nsresult CacheFile::SetFrecency(uint32_t aFrecency) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetFrecency() this=%p, frecency=%u", this, aFrecency)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + + if (mHandle && !mHandle->IsDoomed()) { + CacheFileIOManager::UpdateIndexEntry(mHandle, &aFrecency, nullptr, nullptr, + nullptr, nullptr); + } + + mMetadata->SetFrecency(aFrecency); + return NS_OK; +} + +nsresult CacheFile::GetFrecency(uint32_t* _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + *_retval = mMetadata->GetFrecency(); + return NS_OK; +} + +nsresult CacheFile::SetNetworkTimes(uint64_t aOnStartTime, + uint64_t aOnStopTime) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetNetworkTimes() this=%p, aOnStartTime=%" PRIu64 + ", aOnStopTime=%" PRIu64 "", + this, aOnStartTime, aOnStopTime)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + + nsAutoCString onStartTime; + onStartTime.AppendInt(aOnStartTime); + nsresult rv = + mMetadata->SetElement("net-response-time-onstart", onStartTime.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString onStopTime; + onStopTime.AppendInt(aOnStopTime); + rv = mMetadata->SetElement("net-response-time-onstop", onStopTime.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + uint16_t onStartTime16 = aOnStartTime <= kIndexTimeOutOfBound + ? aOnStartTime + : kIndexTimeOutOfBound; + uint16_t onStopTime16 = + aOnStopTime <= kIndexTimeOutOfBound ? aOnStopTime : kIndexTimeOutOfBound; + + if (mHandle && !mHandle->IsDoomed()) { + CacheFileIOManager::UpdateIndexEntry( + mHandle, nullptr, nullptr, &onStartTime16, &onStopTime16, nullptr); + } + return NS_OK; +} + +nsresult CacheFile::GetOnStartTime(uint64_t* _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mMetadata); + const char* onStartTimeStr = + mMetadata->GetElement("net-response-time-onstart"); + if (!onStartTimeStr) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv; + *_retval = nsDependentCString(onStartTimeStr).ToInteger64(&rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_OK; +} + +nsresult CacheFile::GetOnStopTime(uint64_t* _retval) { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mMetadata); + const char* onStopTimeStr = mMetadata->GetElement("net-response-time-onstop"); + if (!onStopTimeStr) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv; + *_retval = nsDependentCString(onStopTimeStr).ToInteger64(&rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return NS_OK; +} + +nsresult CacheFile::SetContentType(uint8_t aContentType) { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::SetContentType() this=%p, contentType=%u", this, + aContentType)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + + // Save the content type to metadata for case we need to rebuild the index. + nsAutoCString contentType; + contentType.AppendInt(aContentType); + nsresult rv = mMetadata->SetElement("ctid", contentType.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (mHandle && !mHandle->IsDoomed()) { + CacheFileIOManager::UpdateIndexEntry(mHandle, nullptr, nullptr, nullptr, + nullptr, &aContentType); + } + return NS_OK; +} + +nsresult CacheFile::SetAltMetadata(const char* aAltMetadata) { + AssertOwnsLock(); + LOG(("CacheFile::SetAltMetadata() this=%p, aAltMetadata=%s", this, + aAltMetadata ? aAltMetadata : "")); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + + nsresult rv = + mMetadata->SetElement(CacheFileUtils::kAltDataKey, aAltMetadata); + + bool hasAltData = !!aAltMetadata; + + if (NS_FAILED(rv)) { + // Removing element shouldn't fail because it doesn't allocate memory. + mMetadata->SetElement(CacheFileUtils::kAltDataKey, nullptr); + + mAltDataOffset = -1; + mAltDataType.Truncate(); + hasAltData = false; + } + + if (mHandle && !mHandle->IsDoomed()) { + CacheFileIOManager::UpdateIndexEntry(mHandle, nullptr, &hasAltData, nullptr, + nullptr, nullptr); + } + return rv; +} + +nsresult CacheFile::GetLastModified(uint32_t* _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + *_retval = mMetadata->GetLastModified(); + return NS_OK; +} + +nsresult CacheFile::GetLastFetched(uint32_t* _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + *_retval = mMetadata->GetLastFetched(); + return NS_OK; +} + +nsresult CacheFile::GetFetchCount(uint32_t* _retval) { + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + *_retval = mMetadata->GetFetchCount(); + return NS_OK; +} + +nsresult CacheFile::GetDiskStorageSizeInKB(uint32_t* aDiskStorageSize) { + CacheFileAutoLock lock(this); + if (!mHandle) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aDiskStorageSize = mHandle->FileSizeInK(); + return NS_OK; +} + +nsresult CacheFile::OnFetched() { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::OnFetched() this=%p", this)); + + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + PostWriteTimer(); + + mMetadata->OnFetched(); + return NS_OK; +} + +void CacheFile::ReleaseOutsideLock(RefPtr aObject) { + AssertOwnsLock(); + + mObjsToRelease.AppendElement(std::move(aObject)); +} + +nsresult CacheFile::GetChunkLocked(uint32_t aIndex, ECallerType aCaller, + CacheFileChunkListener* aCallback, + CacheFileChunk** _retval) { + AssertOwnsLock(); + + LOG(("CacheFile::GetChunkLocked() [this=%p, idx=%u, caller=%d, listener=%p]", + this, aIndex, aCaller, aCallback)); + + MOZ_ASSERT(mReady); + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + MOZ_ASSERT((aCaller == READER && aCallback) || + (aCaller == WRITER && !aCallback) || + (aCaller == PRELOADER && !aCallback)); + + // Preload chunks from disk when this is disk backed entry and the listener + // is reader. + bool preload = !mMemoryOnly && (aCaller == READER); + + nsresult rv; + + RefPtr chunk; + if (mChunks.Get(aIndex, getter_AddRefs(chunk))) { + LOG(("CacheFile::GetChunkLocked() - Found chunk %p in mChunks [this=%p]", + chunk.get(), this)); + + // Preloader calls this method to preload only non-loaded chunks. + MOZ_ASSERT(aCaller != PRELOADER, "Unexpected!"); + + // We might get failed chunk between releasing the lock in + // CacheFileChunk::OnDataWritten/Read and CacheFile::OnChunkWritten/Read + rv = chunk->GetStatus(); + if (NS_FAILED(rv)) { + SetError(rv); + LOG( + ("CacheFile::GetChunkLocked() - Found failed chunk in mChunks " + "[this=%p]", + this)); + return rv; + } + + if (chunk->IsReady() || aCaller == WRITER) { + chunk.swap(*_retval); + } else { + QueueChunkListener(aIndex, aCallback); + } + + if (preload) { + PreloadChunks(aIndex + 1); + } + + return NS_OK; + } + + if (mCachedChunks.Get(aIndex, getter_AddRefs(chunk))) { + LOG(("CacheFile::GetChunkLocked() - Reusing cached chunk %p [this=%p]", + chunk.get(), this)); + + // Preloader calls this method to preload only non-loaded chunks. + MOZ_ASSERT(aCaller != PRELOADER, "Unexpected!"); + + mChunks.InsertOrUpdate(aIndex, RefPtr{chunk}); + mCachedChunks.Remove(aIndex); + chunk->mFile = this; + chunk->mActiveChunk = true; + + MOZ_ASSERT(chunk->IsReady()); + + chunk.swap(*_retval); + + if (preload) { + PreloadChunks(aIndex + 1); + } + + return NS_OK; + } + + int64_t off = aIndex * static_cast(kChunkSize); + + if (off < mDataSize) { + // We cannot be here if this is memory only entry since the chunk must exist + MOZ_ASSERT(!mMemoryOnly); + if (mMemoryOnly) { + // If this ever really happen it is better to fail rather than crashing on + // a null handle. + LOG( + ("CacheFile::GetChunkLocked() - Unexpected state! Offset < mDataSize " + "for memory-only entry. [this=%p, off=%" PRId64 + ", mDataSize=%" PRId64 "]", + this, off, mDataSize)); + + return NS_ERROR_UNEXPECTED; + } + + chunk = new CacheFileChunk(this, aIndex, aCaller == WRITER); + mChunks.InsertOrUpdate(aIndex, RefPtr{chunk}); + chunk->mActiveChunk = true; + + LOG( + ("CacheFile::GetChunkLocked() - Reading newly created chunk %p from " + "the disk [this=%p]", + chunk.get(), this)); + + // Read the chunk from the disk + rv = chunk->Read(mHandle, + std::min(static_cast(mDataSize - off), + static_cast(kChunkSize)), + mMetadata->GetHash(aIndex), this); + if (NS_WARN_IF(NS_FAILED(rv))) { + RemoveChunkInternal(chunk, false); + return rv; + } + + if (aCaller == WRITER) { + chunk.swap(*_retval); + } else if (aCaller != PRELOADER) { + QueueChunkListener(aIndex, aCallback); + } + + if (preload) { + PreloadChunks(aIndex + 1); + } + + return NS_OK; + } + if (off == mDataSize) { + if (aCaller == WRITER) { + // this listener is going to write to the chunk + chunk = new CacheFileChunk(this, aIndex, true); + mChunks.InsertOrUpdate(aIndex, RefPtr{chunk}); + chunk->mActiveChunk = true; + + LOG(("CacheFile::GetChunkLocked() - Created new empty chunk %p [this=%p]", + chunk.get(), this)); + + chunk->InitNew(); + mMetadata->SetHash(aIndex, chunk->Hash()); + + if (HaveChunkListeners(aIndex)) { + rv = NotifyChunkListeners(aIndex, NS_OK, chunk); + NS_ENSURE_SUCCESS(rv, rv); + } + + chunk.swap(*_retval); + return NS_OK; + } + } else { + if (aCaller == WRITER) { + // this chunk was requested by writer, but we need to fill the gap first + + // Fill with zero the last chunk if it is incomplete + if (mDataSize % kChunkSize) { + rv = PadChunkWithZeroes(mDataSize / kChunkSize); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(!(mDataSize % kChunkSize)); + } + + uint32_t startChunk = mDataSize / kChunkSize; + + if (mMemoryOnly) { + // We need to create all missing CacheFileChunks if this is memory-only + // entry + for (uint32_t i = startChunk; i < aIndex; i++) { + rv = PadChunkWithZeroes(i); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + // We don't need to create CacheFileChunk for other empty chunks unless + // there is some input stream waiting for this chunk. + + if (startChunk != aIndex) { + // Make sure the file contains zeroes at the end of the file + rv = CacheFileIOManager::TruncateSeekSetEOF( + mHandle, startChunk * kChunkSize, aIndex * kChunkSize, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + } + + for (uint32_t i = startChunk; i < aIndex; i++) { + if (HaveChunkListeners(i)) { + rv = PadChunkWithZeroes(i); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mMetadata->SetHash(i, kEmptyChunkHash); + mDataSize = (i + 1) * kChunkSize; + } + } + } + + MOZ_ASSERT(mDataSize == off); + rv = GetChunkLocked(aIndex, WRITER, nullptr, getter_AddRefs(chunk)); + NS_ENSURE_SUCCESS(rv, rv); + + chunk.swap(*_retval); + return NS_OK; + } + } + + // We can be here only if the caller is reader since writer always create a + // new chunk above and preloader calls this method to preload only chunks that + // are not loaded but that do exist. + MOZ_ASSERT(aCaller == READER, "Unexpected!"); + + if (mOutput) { + // the chunk doesn't exist but mOutput may create it + QueueChunkListener(aIndex, aCallback); + } else { + return NS_ERROR_NOT_AVAILABLE; + } + + return NS_OK; +} + +void CacheFile::PreloadChunks(uint32_t aIndex) { + AssertOwnsLock(); + + uint32_t limit = aIndex + mPreloadChunkCount; + + for (uint32_t i = aIndex; i < limit; ++i) { + int64_t off = i * static_cast(kChunkSize); + + if (off >= mDataSize) { + // This chunk is beyond EOF. + return; + } + + if (mChunks.GetWeak(i) || mCachedChunks.GetWeak(i)) { + // This chunk is already in memory or is being read right now. + continue; + } + + LOG(("CacheFile::PreloadChunks() - Preloading chunk [this=%p, idx=%u]", + this, i)); + + RefPtr chunk; + GetChunkLocked(i, PRELOADER, nullptr, getter_AddRefs(chunk)); + // We've checked that we don't have this chunk, so no chunk must be + // returned. + MOZ_ASSERT(!chunk); + } +} + +bool CacheFile::ShouldCacheChunk(uint32_t aIndex) { + AssertOwnsLock(); + +#ifdef CACHE_CHUNKS + // We cache all chunks. + return true; +#else + + if (mPreloadChunkCount != 0 && mInputs.Length() == 0 && + mPreloadWithoutInputStreams && aIndex < mPreloadChunkCount) { + // We don't have any input stream yet, but it is likely that some will be + // opened soon. Keep first mPreloadChunkCount chunks in memory. The + // condition is here instead of in MustKeepCachedChunk() since these + // chunks should be preloaded and can be kept in memory as an optimization, + // but they can be released at any time until they are considered as + // preloaded chunks for any input stream. + return true; + } + + // Cache only chunks that we really need to keep. + return MustKeepCachedChunk(aIndex); +#endif +} + +bool CacheFile::MustKeepCachedChunk(uint32_t aIndex) { + AssertOwnsLock(); + + // We must keep the chunk when this is memory only entry or we don't have + // a handle yet. + if (mMemoryOnly || mOpeningFile) { + return true; + } + + if (mPreloadChunkCount == 0) { + // Preloading of chunks is disabled + return false; + } + + // Check whether this chunk should be considered as preloaded chunk for any + // existing input stream. + + // maxPos is the position of the last byte in the given chunk + int64_t maxPos = static_cast(aIndex + 1) * kChunkSize - 1; + + // minPos is the position of the first byte in a chunk that precedes the given + // chunk by mPreloadChunkCount chunks + int64_t minPos; + if (mPreloadChunkCount >= aIndex) { + minPos = 0; + } else { + minPos = static_cast(aIndex - mPreloadChunkCount) * kChunkSize; + } + + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + int64_t inputPos = mInputs[i]->GetPosition(); + if (inputPos >= minPos && inputPos <= maxPos) { + return true; + } + } + + return false; +} + +nsresult CacheFile::DeactivateChunk(CacheFileChunk* aChunk) { + nsresult rv; + + // Avoid lock reentrancy by increasing the RefCnt + RefPtr chunk = aChunk; + + { + CacheFileAutoLock lock(this); + + LOG(("CacheFile::DeactivateChunk() [this=%p, chunk=%p, idx=%u]", this, + aChunk, aChunk->Index())); + + MOZ_ASSERT(mReady); + MOZ_ASSERT((mHandle && !mMemoryOnly && !mOpeningFile) || + (!mHandle && mMemoryOnly && !mOpeningFile) || + (!mHandle && !mMemoryOnly && mOpeningFile)); + + if (aChunk->mRefCnt != 2) { + LOG( + ("CacheFile::DeactivateChunk() - Chunk is still used [this=%p, " + "chunk=%p, refcnt=%" PRIuPTR "]", + this, aChunk, aChunk->mRefCnt.get())); + + // somebody got the reference before the lock was acquired + return NS_OK; + } + + if (aChunk->mDiscardedChunk) { + aChunk->mActiveChunk = false; + ReleaseOutsideLock( + RefPtr(std::move(aChunk->mFile))); + + DebugOnly removed = mDiscardedChunks.RemoveElement(aChunk); + MOZ_ASSERT(removed); + return NS_OK; + } + +#ifdef DEBUG + { + // We can be here iff the chunk is in the hash table + RefPtr chunkCheck; + mChunks.Get(chunk->Index(), getter_AddRefs(chunkCheck)); + MOZ_ASSERT(chunkCheck == chunk); + + // We also shouldn't have any queued listener for this chunk + ChunkListeners* listeners; + mChunkListeners.Get(chunk->Index(), &listeners); + MOZ_ASSERT(!listeners); + } +#endif + + if (NS_FAILED(chunk->GetStatus())) { + SetError(chunk->GetStatus()); + } + + if (NS_FAILED(mStatus)) { + // Don't write any chunk to disk since this entry will be doomed + LOG( + ("CacheFile::DeactivateChunk() - Releasing chunk because of status " + "[this=%p, chunk=%p, mStatus=0x%08" PRIx32 "]", + this, chunk.get(), static_cast(mStatus))); + + RemoveChunkInternal(chunk, false); + return mStatus; + } + + if (chunk->IsDirty() && !mMemoryOnly && !mOpeningFile) { + LOG( + ("CacheFile::DeactivateChunk() - Writing dirty chunk to the disk " + "[this=%p]", + this)); + + mDataIsDirty = true; + + rv = chunk->Write(mHandle, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::DeactivateChunk() - CacheFileChunk::Write() failed " + "synchronously. Removing it. [this=%p, chunk=%p, rv=0x%08" PRIx32 + "]", + this, chunk.get(), static_cast(rv))); + + RemoveChunkInternal(chunk, false); + + SetError(rv); + return rv; + } + + // Chunk will be removed in OnChunkWritten if it is still unused + + // chunk needs to be released under the lock to be able to rely on + // CacheFileChunk::mRefCnt in CacheFile::OnChunkWritten() + chunk = nullptr; + return NS_OK; + } + + bool keepChunk = ShouldCacheChunk(aChunk->Index()); + LOG(("CacheFile::DeactivateChunk() - %s unused chunk [this=%p, chunk=%p]", + keepChunk ? "Caching" : "Releasing", this, chunk.get())); + + RemoveChunkInternal(chunk, keepChunk); + + if (!mMemoryOnly) WriteMetadataIfNeededLocked(); + } + + return NS_OK; +} + +void CacheFile::RemoveChunkInternal(CacheFileChunk* aChunk, bool aCacheChunk) { + AssertOwnsLock(); + + aChunk->mActiveChunk = false; + ReleaseOutsideLock(RefPtr(std::move(aChunk->mFile))); + + if (aCacheChunk) { + mCachedChunks.InsertOrUpdate(aChunk->Index(), RefPtr{aChunk}); + } + + mChunks.Remove(aChunk->Index()); +} + +bool CacheFile::OutputStreamExists(bool aAlternativeData) { + AssertOwnsLock(); + + if (!mOutput) { + return false; + } + + return mOutput->IsAlternativeData() == aAlternativeData; +} + +int64_t CacheFile::BytesFromChunk(uint32_t aIndex, bool aAlternativeData) { + AssertOwnsLock(); + + int64_t dataSize; + + if (mAltDataOffset != -1) { + if (aAlternativeData) { + dataSize = mDataSize; + } else { + dataSize = mAltDataOffset; + } + } else { + MOZ_ASSERT(!aAlternativeData); + dataSize = mDataSize; + } + + if (!dataSize) { + return 0; + } + + // Index of the last existing chunk. + uint32_t lastChunk = (dataSize - 1) / kChunkSize; + if (aIndex > lastChunk) { + return 0; + } + + // We can use only preloaded chunks for the given stream to calculate + // available bytes if this is an entry stored on disk, since only those + // chunks are guaranteed not to be released. + uint32_t maxPreloadedChunk; + if (mMemoryOnly) { + maxPreloadedChunk = lastChunk; + } else { + maxPreloadedChunk = std::min(aIndex + mPreloadChunkCount, lastChunk); + } + + uint32_t i; + for (i = aIndex; i <= maxPreloadedChunk; ++i) { + CacheFileChunk* chunk; + + chunk = mChunks.GetWeak(i); + if (chunk) { + MOZ_ASSERT(i == lastChunk || chunk->DataSize() == kChunkSize); + if (chunk->IsReady()) { + continue; + } + + // don't search this chunk in cached + break; + } + + chunk = mCachedChunks.GetWeak(i); + if (chunk) { + MOZ_ASSERT(i == lastChunk || chunk->DataSize() == kChunkSize); + continue; + } + + break; + } + + // theoretic bytes in advance + int64_t advance = int64_t(i - aIndex) * kChunkSize; + // real bytes till the end of the file + int64_t tail = dataSize - (aIndex * kChunkSize); + + return std::min(advance, tail); +} + +nsresult CacheFile::Truncate(int64_t aOffset) { + AssertOwnsLock(); + + LOG(("CacheFile::Truncate() [this=%p, offset=%" PRId64 "]", this, aOffset)); + + nsresult rv; + + // If we ever need to truncate on non alt-data boundary, we need to handle + // existing input streams. + MOZ_ASSERT(aOffset == mAltDataOffset, + "Truncating normal data not implemented"); + MOZ_ASSERT(mReady); + MOZ_ASSERT(!mOutput); + + uint32_t lastChunk = 0; + if (mDataSize > 0) { + lastChunk = (mDataSize - 1) / kChunkSize; + } + + uint32_t newLastChunk = 0; + if (aOffset > 0) { + newLastChunk = (aOffset - 1) / kChunkSize; + } + + uint32_t bytesInNewLastChunk = aOffset - newLastChunk * kChunkSize; + + LOG( + ("CacheFileTruncate() - lastChunk=%u, newLastChunk=%u, " + "bytesInNewLastChunk=%u", + lastChunk, newLastChunk, bytesInNewLastChunk)); + + // Remove all truncated chunks from mCachedChunks + for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + + if (idx > newLastChunk) { + // This is unused chunk, simply remove it. + LOG(("CacheFile::Truncate() - removing cached chunk [idx=%u]", idx)); + iter.Remove(); + } + } + + // We need to make sure no input stream holds a reference to a chunk we're + // going to discard. In theory, if alt-data begins at chunk boundary, input + // stream for normal data can get the chunk containing only alt-data via + // EnsureCorrectChunk() call. The input stream won't read the data from such + // chunk, but it will keep the reference until the stream is closed and we + // cannot simply discard this chunk. + int64_t maxInputChunk = -1; + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + int64_t inputChunk = mInputs[i]->GetChunkIdx(); + + if (maxInputChunk < inputChunk) { + maxInputChunk = inputChunk; + } + + MOZ_RELEASE_ASSERT(mInputs[i]->GetPosition() <= aOffset); + } + + MOZ_RELEASE_ASSERT(maxInputChunk <= newLastChunk + 1); + if (maxInputChunk == newLastChunk + 1) { + // Truncating must be done at chunk boundary + MOZ_RELEASE_ASSERT(bytesInNewLastChunk == kChunkSize); + newLastChunk++; + bytesInNewLastChunk = 0; + LOG( + ("CacheFile::Truncate() - chunk %p is still in use, using " + "newLastChunk=%u and bytesInNewLastChunk=%u", + mChunks.GetWeak(newLastChunk), newLastChunk, bytesInNewLastChunk)); + } + + // Discard all truncated chunks in mChunks + for (auto iter = mChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + + if (idx > newLastChunk) { + RefPtr& chunk = iter.Data(); + LOG(("CacheFile::Truncate() - discarding chunk [idx=%u, chunk=%p]", idx, + chunk.get())); + + if (HaveChunkListeners(idx)) { + NotifyChunkListeners(idx, NS_ERROR_NOT_AVAILABLE, chunk); + } + + chunk->mDiscardedChunk = true; + mDiscardedChunks.AppendElement(chunk); + iter.Remove(); + } + } + + // Remove hashes of all removed chunks from the metadata + for (uint32_t i = lastChunk; i > newLastChunk; --i) { + mMetadata->RemoveHash(i); + } + + // Truncate new last chunk + if (bytesInNewLastChunk == kChunkSize) { + LOG(("CacheFile::Truncate() - not truncating last chunk.")); + } else { + RefPtr chunk; + if (mChunks.Get(newLastChunk, getter_AddRefs(chunk))) { + LOG(("CacheFile::Truncate() - New last chunk %p got from mChunks.", + chunk.get())); + } else if (mCachedChunks.Get(newLastChunk, getter_AddRefs(chunk))) { + LOG(("CacheFile::Truncate() - New last chunk %p got from mCachedChunks.", + chunk.get())); + } else { + // New last chunk isn't loaded but we need to update the hash. + MOZ_ASSERT(!mMemoryOnly); + MOZ_ASSERT(mHandle); + + rv = GetChunkLocked(newLastChunk, PRELOADER, nullptr, + getter_AddRefs(chunk)); + if (NS_FAILED(rv)) { + return rv; + } + // We've checked that we don't have this chunk, so no chunk must be + // returned. + MOZ_ASSERT(!chunk); + + if (!mChunks.Get(newLastChunk, getter_AddRefs(chunk))) { + return NS_ERROR_UNEXPECTED; + } + + LOG(("CacheFile::Truncate() - New last chunk %p got from preloader.", + chunk.get())); + } + + rv = chunk->GetStatus(); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::Truncate() - New last chunk is failed " + "[status=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + + chunk->Truncate(bytesInNewLastChunk); + + // If the chunk is ready set the new hash now. If it's still being loaded + // CacheChunk::Truncate() made the chunk dirty and the hash will be updated + // in OnChunkWritten(). + if (chunk->IsReady()) { + mMetadata->SetHash(newLastChunk, chunk->Hash()); + } + } + + if (mHandle) { + rv = CacheFileIOManager::TruncateSeekSetEOF(mHandle, aOffset, aOffset, + nullptr); + if (NS_FAILED(rv)) { + return rv; + } + } + + mDataSize = aOffset; + + return NS_OK; +} + +static uint32_t StatusToTelemetryEnum(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + return 0; + } + + switch (aStatus) { + case NS_BASE_STREAM_CLOSED: + return 0; // Log this as a success + case NS_ERROR_OUT_OF_MEMORY: + return 2; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + return 3; + case NS_ERROR_FILE_CORRUPTED: + return 4; + case NS_ERROR_FILE_NOT_FOUND: + return 5; + case NS_BINDING_ABORTED: + return 6; + default: + return 1; // other error + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); +} + +void CacheFile::RemoveInput(CacheFileInputStream* aInput, nsresult aStatus) { + AssertOwnsLock(); + + LOG(("CacheFile::RemoveInput() [this=%p, input=%p, status=0x%08" PRIx32 "]", + this, aInput, static_cast(aStatus))); + + DebugOnly found{}; + found = mInputs.RemoveElement(aInput); + MOZ_ASSERT(found); + + ReleaseOutsideLock( + already_AddRefed(static_cast(aInput))); + + if (!mMemoryOnly) WriteMetadataIfNeededLocked(); + + // If the input didn't read all data, there might be left some preloaded + // chunks that won't be used anymore. + CleanUpCachedChunks(); + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_V2_INPUT_STREAM_STATUS, + StatusToTelemetryEnum(aStatus)); +} + +void CacheFile::RemoveOutput(CacheFileOutputStream* aOutput, nsresult aStatus) { + AssertOwnsLock(); + + nsresult rv; + + LOG(("CacheFile::RemoveOutput() [this=%p, output=%p, status=0x%08" PRIx32 "]", + this, aOutput, static_cast(aStatus))); + + if (mOutput != aOutput) { + LOG( + ("CacheFile::RemoveOutput() - This output was already removed, ignoring" + " call [this=%p]", + this)); + return; + } + + mOutput = nullptr; + + // Cancel all queued chunk and update listeners that cannot be satisfied + NotifyListenersAboutOutputRemoval(); + + if (!mMemoryOnly) WriteMetadataIfNeededLocked(); + + // Make sure the CacheFile status is set to a failure when the output stream + // is closed with a fatal error. This way we propagate correctly and w/o any + // windows the failure state of this entry to end consumers. + if (NS_SUCCEEDED(mStatus) && NS_FAILED(aStatus) && + aStatus != NS_BASE_STREAM_CLOSED) { + if (aOutput->IsAlternativeData()) { + MOZ_ASSERT(mAltDataOffset != -1); + // If there is no alt-data input stream truncate only alt-data, otherwise + // doom the entry. + bool altDataInputExists = false; + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + if (mInputs[i]->IsAlternativeData()) { + altDataInputExists = true; + break; + } + } + if (altDataInputExists) { + SetError(aStatus); + } else { + rv = Truncate(mAltDataOffset); + if (NS_FAILED(rv)) { + LOG( + ("CacheFile::RemoveOutput() - Truncating alt-data failed " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + SetError(aStatus); + } else { + SetAltMetadata(nullptr); + mAltDataOffset = -1; + mAltDataType.Truncate(); + } + } + } else { + SetError(aStatus); + } + } + + // Notify close listener as the last action + aOutput->NotifyCloseListener(); + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS, + StatusToTelemetryEnum(aStatus)); +} + +nsresult CacheFile::NotifyChunkListener(CacheFileChunkListener* aCallback, + nsIEventTarget* aTarget, + nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) { + LOG( + ("CacheFile::NotifyChunkListener() [this=%p, listener=%p, target=%p, " + "rv=0x%08" PRIx32 ", idx=%u, chunk=%p]", + this, aCallback, aTarget, static_cast(aResult), aChunkIdx, + aChunk)); + + RefPtr ev; + ev = new NotifyChunkListenerEvent(aCallback, aResult, aChunkIdx, aChunk); + if (aTarget) { + return aTarget->Dispatch(ev, NS_DISPATCH_NORMAL); + } + return NS_DispatchToCurrentThread(ev); +} + +void CacheFile::QueueChunkListener(uint32_t aIndex, + CacheFileChunkListener* aCallback) { + LOG(("CacheFile::QueueChunkListener() [this=%p, idx=%u, listener=%p]", this, + aIndex, aCallback)); + + AssertOwnsLock(); + + MOZ_ASSERT(aCallback); + + ChunkListenerItem* item = new ChunkListenerItem(); + item->mTarget = CacheFileIOManager::IOTarget(); + if (!item->mTarget) { + LOG( + ("CacheFile::QueueChunkListener() - Cannot get Cache I/O thread! Using " + "main thread for callback.")); + item->mTarget = GetMainThreadSerialEventTarget(); + } + item->mCallback = aCallback; + + mChunkListeners.GetOrInsertNew(aIndex)->mItems.AppendElement(item); +} + +nsresult CacheFile::NotifyChunkListeners(uint32_t aIndex, nsresult aResult, + CacheFileChunk* aChunk) { + LOG(("CacheFile::NotifyChunkListeners() [this=%p, idx=%u, rv=0x%08" PRIx32 + ", " + "chunk=%p]", + this, aIndex, static_cast(aResult), aChunk)); + + AssertOwnsLock(); + + nsresult rv, rv2; + + ChunkListeners* listeners; + mChunkListeners.Get(aIndex, &listeners); + MOZ_ASSERT(listeners); + + rv = NS_OK; + for (uint32_t i = 0; i < listeners->mItems.Length(); i++) { + ChunkListenerItem* item = listeners->mItems[i]; + rv2 = NotifyChunkListener(item->mCallback, item->mTarget, aResult, aIndex, + aChunk); + if (NS_FAILED(rv2) && NS_SUCCEEDED(rv)) rv = rv2; + delete item; + } + + mChunkListeners.Remove(aIndex); + + return rv; +} + +bool CacheFile::HaveChunkListeners(uint32_t aIndex) { + AssertOwnsLock(); + ChunkListeners* listeners; + mChunkListeners.Get(aIndex, &listeners); + return !!listeners; +} + +void CacheFile::NotifyListenersAboutOutputRemoval() { + LOG(("CacheFile::NotifyListenersAboutOutputRemoval() [this=%p]", this)); + + AssertOwnsLock(); + + // First fail all chunk listeners that wait for non-existent chunk + for (auto iter = mChunkListeners.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + auto* listeners = iter.UserData(); + + LOG( + ("CacheFile::NotifyListenersAboutOutputRemoval() - fail " + "[this=%p, idx=%u]", + this, idx)); + + RefPtr chunk; + mChunks.Get(idx, getter_AddRefs(chunk)); + if (chunk) { + // Skip these listeners because the chunk is being read. We don't have + // assertion here to check its state because it might be already in READY + // state while CacheFile::OnChunkRead() is waiting on Cache I/O thread for + // a lock so the listeners hasn't been notified yet. In any case, the + // listeners will be notified from CacheFile::OnChunkRead(). + continue; + } + + for (uint32_t i = 0; i < listeners->mItems.Length(); i++) { + ChunkListenerItem* item = listeners->mItems[i]; + NotifyChunkListener(item->mCallback, item->mTarget, + NS_ERROR_NOT_AVAILABLE, idx, nullptr); + delete item; + } + + iter.Remove(); + } + + // Fail all update listeners + for (const auto& entry : mChunks) { + const RefPtr& chunk = entry.GetData(); + LOG( + ("CacheFile::NotifyListenersAboutOutputRemoval() - fail2 " + "[this=%p, idx=%u]", + this, entry.GetKey())); + + if (chunk->IsReady()) { + chunk->NotifyUpdateListeners(); + } + } +} + +bool CacheFile::DataSize(int64_t* aSize) { + CacheFileAutoLock lock(this); + + if (OutputStreamExists(false)) { + return false; + } + + if (mAltDataOffset == -1) { + *aSize = mDataSize; + } else { + *aSize = mAltDataOffset; + } + + return true; +} + +nsresult CacheFile::GetAltDataSize(int64_t* aSize) { + CacheFileAutoLock lock(this); + if (mOutput) { + return NS_ERROR_IN_PROGRESS; + } + + if (mAltDataOffset == -1) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aSize = mDataSize - mAltDataOffset; + return NS_OK; +} + +nsresult CacheFile::GetAltDataType(nsACString& aType) { + CacheFileAutoLock lock(this); + + if (mAltDataOffset == -1) { + return NS_ERROR_NOT_AVAILABLE; + } + + aType = mAltDataType; + return NS_OK; +} + +bool CacheFile::IsDoomed() { + CacheFileAutoLock lock(this); + + if (!mHandle) return false; + + return mHandle->IsDoomed(); +} + +bool CacheFile::IsWriteInProgress() { + CacheFileAutoLock lock(this); + + bool result = false; + + if (!mMemoryOnly) { + result = + mDataIsDirty || (mMetadata && mMetadata->IsDirty()) || mWritingMetadata; + } + + result = result || mOpeningFile || mOutput || mChunks.Count(); + + return result; +} + +bool CacheFile::EntryWouldExceedLimit(int64_t aOffset, int64_t aSize, + bool aIsAltData) { + CacheFileAutoLock lock(this); + + if (mSkipSizeCheck || aSize < 0) { + return false; + } + + int64_t totalSize = aOffset + aSize; + if (aIsAltData) { + totalSize += (mAltDataOffset == -1) ? mDataSize : mAltDataOffset; + } + + return CacheObserver::EntryIsTooBig(totalSize, !mMemoryOnly); +} + +bool CacheFile::IsDirty() { return mDataIsDirty || mMetadata->IsDirty(); } + +void CacheFile::WriteMetadataIfNeeded() { + LOG(("CacheFile::WriteMetadataIfNeeded() [this=%p]", this)); + + CacheFileAutoLock lock(this); + + if (!mMemoryOnly) WriteMetadataIfNeededLocked(); +} + +void CacheFile::WriteMetadataIfNeededLocked(bool aFireAndForget) { + // When aFireAndForget is set to true, we are called from dtor. + // |this| must not be referenced after this method returns! + + LOG(("CacheFile::WriteMetadataIfNeededLocked() [this=%p]", this)); + + nsresult rv; + + AssertOwnsLock(); + MOZ_ASSERT(!mMemoryOnly); + + if (!mMetadata) { + MOZ_CRASH("Must have metadata here"); + return; + } + + if (NS_FAILED(mStatus)) return; + + if (!IsDirty() || mOutput || mInputs.Length() || mChunks.Count() || + mWritingMetadata || mOpeningFile || mKill) { + return; + } + + if (!aFireAndForget) { + // if aFireAndForget is set, we are called from dtor. Write + // scheduler hard-refers CacheFile otherwise, so we cannot be here. + CacheFileIOManager::UnscheduleMetadataWrite(this); + } + + LOG(("CacheFile::WriteMetadataIfNeededLocked() - Writing metadata [this=%p]", + this)); + + rv = mMetadata->WriteMetadata(mDataSize, aFireAndForget ? nullptr : this); + if (NS_SUCCEEDED(rv)) { + mWritingMetadata = true; + mDataIsDirty = false; + } else { + LOG( + ("CacheFile::WriteMetadataIfNeededLocked() - Writing synchronously " + "failed [this=%p]", + this)); + // TODO: close streams with error + SetError(rv); + } +} + +void CacheFile::PostWriteTimer() { + if (mMemoryOnly) return; + LOG(("CacheFile::PostWriteTimer() [this=%p]", this)); + + CacheFileIOManager::ScheduleMetadataWrite(this); +} + +void CacheFile::CleanUpCachedChunks() { + for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + const RefPtr& chunk = iter.Data(); + + LOG(("CacheFile::CleanUpCachedChunks() [this=%p, idx=%u, chunk=%p]", this, + idx, chunk.get())); + + if (MustKeepCachedChunk(idx)) { + LOG(("CacheFile::CleanUpCachedChunks() - Keeping chunk")); + continue; + } + + LOG(("CacheFile::CleanUpCachedChunks() - Removing chunk")); + iter.Remove(); + } +} + +nsresult CacheFile::PadChunkWithZeroes(uint32_t aChunkIdx) { + AssertOwnsLock(); + + // This method is used to pad last incomplete chunk with zeroes or create + // a new chunk full of zeroes + MOZ_ASSERT(mDataSize / kChunkSize == aChunkIdx); + + nsresult rv; + RefPtr chunk; + rv = GetChunkLocked(aChunkIdx, WRITER, nullptr, getter_AddRefs(chunk)); + NS_ENSURE_SUCCESS(rv, rv); + + LOG( + ("CacheFile::PadChunkWithZeroes() - Zeroing hole in chunk %d, range %d-%d" + " [this=%p]", + aChunkIdx, chunk->DataSize(), kChunkSize - 1, this)); + + CacheFileChunkWriteHandle hnd = chunk->GetWriteHandle(kChunkSize); + if (!hnd.Buf()) { + ReleaseOutsideLock(std::move(chunk)); + SetError(NS_ERROR_OUT_OF_MEMORY); + return NS_ERROR_OUT_OF_MEMORY; + } + + uint32_t offset = hnd.DataSize(); + memset(hnd.Buf() + offset, 0, kChunkSize - offset); + hnd.UpdateDataSize(offset, kChunkSize - offset); + + ReleaseOutsideLock(std::move(chunk)); + + return NS_OK; +} + +void CacheFile::SetError(nsresult aStatus) { + AssertOwnsLock(); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + if (mHandle) { + CacheFileIOManager::DoomFile(mHandle, nullptr); + } + } +} + +nsresult CacheFile::InitIndexEntry() { + AssertOwnsLock(); + MOZ_ASSERT(mHandle); + + if (mHandle->IsDoomed()) return NS_OK; + + nsresult rv; + + rv = CacheFileIOManager::InitIndexEntry( + mHandle, GetOriginAttrsHash(mMetadata->OriginAttributes()), + mMetadata->IsAnonymous(), mPinned); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t frecency = mMetadata->GetFrecency(); + + bool hasAltData = + mMetadata->GetElement(CacheFileUtils::kAltDataKey) != nullptr; + + static auto toUint16 = [](const char* s) -> uint16_t { + if (s) { + nsresult rv; + uint64_t n64 = nsDependentCString(s).ToInteger64(&rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return n64 <= kIndexTimeOutOfBound ? n64 : kIndexTimeOutOfBound; + } + return kIndexTimeNotAvailable; + }; + + const char* onStartTimeStr = + mMetadata->GetElement("net-response-time-onstart"); + uint16_t onStartTime = toUint16(onStartTimeStr); + + const char* onStopTimeStr = mMetadata->GetElement("net-response-time-onstop"); + uint16_t onStopTime = toUint16(onStopTimeStr); + + const char* contentTypeStr = mMetadata->GetElement("ctid"); + uint8_t contentType = nsICacheEntry::CONTENT_TYPE_UNKNOWN; + if (contentTypeStr) { + int64_t n64 = nsDependentCString(contentTypeStr).ToInteger64(&rv); + if (NS_FAILED(rv) || n64 < nsICacheEntry::CONTENT_TYPE_UNKNOWN || + n64 >= nsICacheEntry::CONTENT_TYPE_LAST) { + n64 = nsICacheEntry::CONTENT_TYPE_UNKNOWN; + } + contentType = n64; + } + + rv = CacheFileIOManager::UpdateIndexEntry( + mHandle, &frecency, &hasAltData, &onStartTime, &onStopTime, &contentType); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +size_t CacheFile::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + CacheFileAutoLock lock(const_cast(this)); + + size_t n = 0; + n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mChunks.ShallowSizeOfExcludingThis(mallocSizeOf); + for (const auto& chunk : mChunks.Values()) { + n += chunk->SizeOfIncludingThis(mallocSizeOf); + } + n += mCachedChunks.ShallowSizeOfExcludingThis(mallocSizeOf); + for (const auto& chunk : mCachedChunks.Values()) { + n += chunk->SizeOfIncludingThis(mallocSizeOf); + } + // Ignore metadata if it's still being read. It's not safe to access buffers + // in CacheFileMetadata because they might be reallocated on another thread + // outside CacheFile's lock. + if (mMetadata && mReady) { + n += mMetadata->SizeOfIncludingThis(mallocSizeOf); + } + + // Input streams are not elsewhere reported. + n += mInputs.ShallowSizeOfExcludingThis(mallocSizeOf); + for (uint32_t i = 0; i < mInputs.Length(); ++i) { + n += mInputs[i]->SizeOfIncludingThis(mallocSizeOf); + } + + // Output streams are not elsewhere reported. + if (mOutput) { + n += mOutput->SizeOfIncludingThis(mallocSizeOf); + } + + // The listeners are usually classes reported just above. + n += mChunkListeners.ShallowSizeOfExcludingThis(mallocSizeOf); + n += mObjsToRelease.ShallowSizeOfExcludingThis(mallocSizeOf); + + // mHandle reported directly from CacheFileIOManager. + + return n; +} + +size_t CacheFile::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFile.h b/netwerk/cache2/CacheFile.h new file mode 100644 index 0000000000..97f986c143 --- /dev/null +++ b/netwerk/cache2/CacheFile.h @@ -0,0 +1,288 @@ +/* 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/. */ + +#ifndef CacheFile__h__ +#define CacheFile__h__ + +#include "CacheFileChunk.h" +#include "CacheFileIOManager.h" +#include "CacheFileMetadata.h" +#include "nsRefPtrHashtable.h" +#include "nsClassHashtable.h" +#include "mozilla/Mutex.h" + +class nsIAsyncOutputStream; +class nsICacheEntry; +class nsICacheEntryMetaDataVisitor; +class nsIInputStream; +class nsIOutputStream; + +namespace mozilla { +namespace net { + +class CacheFileInputStream; +class CacheFileOutputStream; +class CacheOutputCloseListener; +class MetadataWriteTimer; + +namespace CacheFileUtils { +class CacheFileLock; +}; + +#define CACHEFILELISTENER_IID \ + { /* 95e7f284-84ba-48f9-b1fc-3a7336b4c33c */ \ + 0x95e7f284, 0x84ba, 0x48f9, { \ + 0xb1, 0xfc, 0x3a, 0x73, 0x36, 0xb4, 0xc3, 0x3c \ + } \ + } + +class CacheFileListener : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(CACHEFILELISTENER_IID) + + NS_IMETHOD OnFileReady(nsresult aResult, bool aIsNew) = 0; + NS_IMETHOD OnFileDoomed(nsresult aResult) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(CacheFileListener, CACHEFILELISTENER_IID) + +class MOZ_CAPABILITY("mutex") CacheFile final + : public CacheFileChunkListener, + public CacheFileIOListener, + public CacheFileMetadataListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + CacheFile(); + + nsresult Init(const nsACString& aKey, bool aCreateNew, bool aMemoryOnly, + bool aSkipSizeCheck, bool aPriority, bool aPinned, + CacheFileListener* aCallback); + + NS_IMETHOD OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkUpdated(CacheFileChunk* aChunk) override; + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) override; + virtual bool IsKilled() override; + + NS_IMETHOD OnMetadataRead(nsresult aResult) override; + NS_IMETHOD OnMetadataWritten(nsresult aResult) override; + + NS_IMETHOD OpenInputStream(nsICacheEntry* aCacheEntryHandle, + nsIInputStream** _retval); + NS_IMETHOD OpenAlternativeInputStream(nsICacheEntry* aCacheEntryHandle, + const char* aAltDataType, + nsIInputStream** _retval); + NS_IMETHOD OpenOutputStream(CacheOutputCloseListener* aCloseListener, + nsIOutputStream** _retval); + NS_IMETHOD OpenAlternativeOutputStream( + CacheOutputCloseListener* aCloseListener, const char* aAltDataType, + nsIAsyncOutputStream** _retval); + NS_IMETHOD SetMemoryOnly(); + NS_IMETHOD Doom(CacheFileListener* aCallback); + + void Kill() { mKill = true; } + nsresult ThrowMemoryCachedData(); + + nsresult GetAltDataSize(int64_t* aSize); + nsresult GetAltDataType(nsACString& aType); + + // metadata forwarders + nsresult GetElement(const char* aKey, char** _retval); + nsresult SetElement(const char* aKey, const char* aValue); + nsresult VisitMetaData(nsICacheEntryMetaDataVisitor* aVisitor); + nsresult ElementsSize(uint32_t* _retval); + nsresult SetExpirationTime(uint32_t aExpirationTime); + nsresult GetExpirationTime(uint32_t* _retval); + nsresult SetFrecency(uint32_t aFrecency); + nsresult GetFrecency(uint32_t* _retval); + nsresult SetNetworkTimes(uint64_t aOnStartTime, uint64_t aOnStopTime); + nsresult SetContentType(uint8_t aContentType); + nsresult GetOnStartTime(uint64_t* _retval); + nsresult GetOnStopTime(uint64_t* _retval); + nsresult GetLastModified(uint32_t* _retval); + nsresult GetLastFetched(uint32_t* _retval); + nsresult GetFetchCount(uint32_t* _retval); + nsresult GetDiskStorageSizeInKB(uint32_t* aDiskStorageSize); + // Called by upper layers to indicated the entry has been fetched, + // i.e. delivered to the consumer. + nsresult OnFetched(); + + bool DataSize(int64_t* aSize); + void Key(nsACString& aKey); + bool IsDoomed(); + bool IsPinned(); + // Returns true when there is a potentially unfinished write operation. + bool IsWriteInProgress(); + bool EntryWouldExceedLimit(int64_t aOffset, int64_t aSize, bool aIsAltData); + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + friend class CacheFileIOManager; + friend class CacheFileChunk; + friend class CacheFileInputStream; + friend class CacheFileOutputStream; + friend class CacheFileAutoLock; + friend class MetadataWriteTimer; + + virtual ~CacheFile(); + + void Lock() MOZ_CAPABILITY_ACQUIRE() { mLock->Lock().Lock(); } + void Unlock() MOZ_CAPABILITY_RELEASE() { + // move the elements out of mObjsToRelease + // so that they can be released after we unlock + nsTArray> objs = std::move(mObjsToRelease); + + mLock->Lock().Unlock(); + } + void AssertOwnsLock() const MOZ_ASSERT_CAPABILITY(this) { + mLock->Lock().AssertCurrentThreadOwns(); + } + void ReleaseOutsideLock(RefPtr aObject); + + enum ECallerType { READER = 0, WRITER = 1, PRELOADER = 2 }; + + nsresult DoomLocked(CacheFileListener* aCallback); + + nsresult GetChunkLocked(uint32_t aIndex, ECallerType aCaller, + CacheFileChunkListener* aCallback, + CacheFileChunk** _retval); + + void PreloadChunks(uint32_t aIndex); + bool ShouldCacheChunk(uint32_t aIndex); + bool MustKeepCachedChunk(uint32_t aIndex); + + nsresult DeactivateChunk(CacheFileChunk* aChunk); + void RemoveChunkInternal(CacheFileChunk* aChunk, bool aCacheChunk); + + bool OutputStreamExists(bool aAlternativeData); + // Returns number of bytes that are available and can be read by input stream + // without waiting for the data. The amount is counted from the start of + // aIndex chunk and it is guaranteed that this data won't be released by + // CleanUpCachedChunks(). + int64_t BytesFromChunk(uint32_t aIndex, bool aAlternativeData); + nsresult Truncate(int64_t aOffset); + + void RemoveInput(CacheFileInputStream* aInput, nsresult aStatus); + void RemoveOutput(CacheFileOutputStream* aOutput, nsresult aStatus); + nsresult NotifyChunkListener(CacheFileChunkListener* aCallback, + nsIEventTarget* aTarget, nsresult aResult, + uint32_t aChunkIdx, CacheFileChunk* aChunk); + void QueueChunkListener(uint32_t aIndex, CacheFileChunkListener* aCallback); + nsresult NotifyChunkListeners(uint32_t aIndex, nsresult aResult, + CacheFileChunk* aChunk); + bool HaveChunkListeners(uint32_t aIndex); + void NotifyListenersAboutOutputRemoval(); + + bool IsDirty() MOZ_REQUIRES(this); + void WriteMetadataIfNeeded(); + void WriteMetadataIfNeededLocked(bool aFireAndForget = false) + MOZ_REQUIRES(this); + void PostWriteTimer() MOZ_REQUIRES(this); + + void CleanUpCachedChunks() MOZ_REQUIRES(this); + + nsresult PadChunkWithZeroes(uint32_t aChunkIdx); + + void SetError(nsresult aStatus); + nsresult SetAltMetadata(const char* aAltMetadata); + + nsresult InitIndexEntry(); + + bool mOpeningFile MOZ_GUARDED_BY(this){false}; + bool mReady MOZ_GUARDED_BY(this){false}; + bool mMemoryOnly MOZ_GUARDED_BY(this){false}; + bool mSkipSizeCheck MOZ_GUARDED_BY(this){false}; + bool mOpenAsMemoryOnly MOZ_GUARDED_BY(this){false}; + bool mPinned MOZ_GUARDED_BY(this){false}; + bool mPriority MOZ_GUARDED_BY(this){false}; + bool mDataAccessed MOZ_GUARDED_BY(this){false}; + bool mDataIsDirty MOZ_GUARDED_BY(this){false}; + bool mWritingMetadata MOZ_GUARDED_BY(this){false}; + bool mPreloadWithoutInputStreams MOZ_GUARDED_BY(this){true}; + uint32_t mPreloadChunkCount MOZ_GUARDED_BY(this){0}; + nsresult mStatus MOZ_GUARDED_BY(this){NS_OK}; + // Size of the whole data including eventual alternative data represenation. + int64_t mDataSize MOZ_GUARDED_BY(this){-1}; + + // If there is alternative data present, it contains size of the original + // data, i.e. offset where alternative data starts. Otherwise it is -1. + int64_t mAltDataOffset MOZ_GUARDED_BY(this){-1}; + + nsCString mKey MOZ_GUARDED_BY(this); + nsCString mAltDataType + MOZ_GUARDED_BY(this); // The type of the saved alt-data. May be empty. + + RefPtr mHandle MOZ_GUARDED_BY(this); + RefPtr mMetadata MOZ_GUARDED_BY(this); + nsCOMPtr mListener MOZ_GUARDED_BY(this); + nsCOMPtr mDoomAfterOpenListener MOZ_GUARDED_BY(this); + Atomic mKill{false}; + + nsRefPtrHashtable mChunks + MOZ_GUARDED_BY(this); + nsClassHashtable mChunkListeners + MOZ_GUARDED_BY(this); + nsRefPtrHashtable mCachedChunks + MOZ_GUARDED_BY(this); + // We can truncate data only if there is no input/output stream beyond the + // truncate position, so only unused chunks can be thrown away. But it can + // happen that we need to throw away a chunk that is still in mChunks (i.e. + // an active chunk) because deactivation happens with a small delay. We cannot + // delete such chunk immediately but we need to ensure that such chunk won't + // be returned by GetChunkLocked, so we move this chunk into mDiscardedChunks + // and mark it as discarded. + nsTArray> mDiscardedChunks MOZ_GUARDED_BY(this); + + nsTArray mInputs MOZ_GUARDED_BY(this); + CacheFileOutputStream* mOutput MOZ_GUARDED_BY(this){nullptr}; + + nsTArray> mObjsToRelease MOZ_GUARDED_BY(this); + RefPtr mLock; +}; + +class MOZ_RAII MOZ_SCOPED_CAPABILITY CacheFileAutoLock { + public: + explicit CacheFileAutoLock(CacheFile* aFile) MOZ_CAPABILITY_ACQUIRE(aFile) + : mFile(aFile), mLocked(true) { + mFile->Lock(); + } + ~CacheFileAutoLock() MOZ_CAPABILITY_RELEASE() { + if (mLocked) { + mFile->Unlock(); + } + } + void Lock() MOZ_CAPABILITY_ACQUIRE() { + MOZ_ASSERT(!mLocked); + mFile->Lock(); + mLocked = true; + } + void Unlock() MOZ_CAPABILITY_RELEASE() { + MOZ_ASSERT(mLocked); + mFile->Unlock(); + mLocked = false; + } + + private: + RefPtr mFile; + bool mLocked; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileChunk.cpp b/netwerk/cache2/CacheFileChunk.cpp new file mode 100644 index 0000000000..7dc0d9078c --- /dev/null +++ b/netwerk/cache2/CacheFileChunk.cpp @@ -0,0 +1,840 @@ +/* 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 "CacheLog.h" +#include "CacheFileChunk.h" + +#include "CacheFile.h" +#include "nsThreadUtils.h" + +#include "mozilla/IntegerPrintfMacros.h" + +namespace mozilla::net { + +#define kMinBufSize 512 + +CacheFileChunkBuffer::CacheFileChunkBuffer(CacheFileChunk* aChunk) + : mChunk(aChunk), + mBuf(nullptr), + mBufSize(0), + mDataSize(0), + mReadHandlesCount(0), + mWriteHandleExists(false) {} + +CacheFileChunkBuffer::~CacheFileChunkBuffer() { + if (mBuf) { + CacheFileUtils::FreeBuffer(mBuf); + mBuf = nullptr; + mChunk->BuffersAllocationChanged(mBufSize, 0); + mBufSize = 0; + } +} + +void CacheFileChunkBuffer::CopyFrom(CacheFileChunkBuffer* aOther) { + MOZ_RELEASE_ASSERT(mBufSize >= aOther->mDataSize); + mDataSize = aOther->mDataSize; + memcpy(mBuf, aOther->mBuf, mDataSize); +} + +nsresult CacheFileChunkBuffer::FillInvalidRanges( + CacheFileChunkBuffer* aOther, CacheFileUtils::ValidityMap* aMap) { + nsresult rv; + + rv = EnsureBufSize(aOther->mDataSize); + if (NS_FAILED(rv)) { + return rv; + } + + uint32_t invalidOffset = 0; + uint32_t invalidLength; + + for (uint32_t i = 0; i < aMap->Length(); ++i) { + uint32_t validOffset = (*aMap)[i].Offset(); + uint32_t validLength = (*aMap)[i].Len(); + + MOZ_RELEASE_ASSERT(invalidOffset <= validOffset); + invalidLength = validOffset - invalidOffset; + if (invalidLength > 0) { + MOZ_RELEASE_ASSERT(invalidOffset + invalidLength <= aOther->mDataSize); + memcpy(mBuf + invalidOffset, aOther->mBuf + invalidOffset, invalidLength); + } + invalidOffset = validOffset + validLength; + } + + if (invalidOffset < aOther->mDataSize) { + invalidLength = aOther->mDataSize - invalidOffset; + memcpy(mBuf + invalidOffset, aOther->mBuf + invalidOffset, invalidLength); + } + + return NS_OK; +} + +[[nodiscard]] nsresult CacheFileChunkBuffer::EnsureBufSize(uint32_t aBufSize) { + AssertOwnsLock(); + + if (mBufSize >= aBufSize) { + return NS_OK; + } + + // find smallest power of 2 greater than or equal to aBufSize + aBufSize--; + aBufSize |= aBufSize >> 1; + aBufSize |= aBufSize >> 2; + aBufSize |= aBufSize >> 4; + aBufSize |= aBufSize >> 8; + aBufSize |= aBufSize >> 16; + aBufSize++; + + const uint32_t minBufSize = kMinBufSize; + const uint32_t maxBufSize = kChunkSize; + aBufSize = clamped(aBufSize, minBufSize, maxBufSize); + + if (!mChunk->CanAllocate(aBufSize - mBufSize)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* newBuf = static_cast(realloc(mBuf, aBufSize)); + if (!newBuf) { + return NS_ERROR_OUT_OF_MEMORY; + } + + mChunk->BuffersAllocationChanged(mBufSize, aBufSize); + mBuf = newBuf; + mBufSize = aBufSize; + + return NS_OK; +} + +void CacheFileChunkBuffer::SetDataSize(uint32_t aDataSize) { + MOZ_RELEASE_ASSERT( + // EnsureBufSize must be called before SetDataSize, so the new data size + // is guaranteed to be smaller than or equal to mBufSize. + aDataSize <= mBufSize || + // The only exception is an optimization when we read the data from the + // disk. The data is read to a separate buffer and CacheFileChunk::mBuf is + // empty (see CacheFileChunk::Read). We need to set mBuf::mDataSize + // accordingly so that DataSize() methods return correct value, but we + // don't want to allocate the buffer since it wouldn't be used in most + // cases. + (mBufSize == 0 && mChunk->mState == CacheFileChunk::READING)); + + mDataSize = aDataSize; +} + +void CacheFileChunkBuffer::AssertOwnsLock() const { mChunk->AssertOwnsLock(); } + +void CacheFileChunkBuffer::RemoveReadHandle() { + AssertOwnsLock(); + MOZ_RELEASE_ASSERT(mReadHandlesCount); + MOZ_RELEASE_ASSERT(!mWriteHandleExists); + mReadHandlesCount--; + + if (mReadHandlesCount == 0 && mChunk->mBuf != this) { + DebugOnly removed = mChunk->mOldBufs.RemoveElement(this); + MOZ_ASSERT(removed); + } +} + +void CacheFileChunkBuffer::RemoveWriteHandle() { + AssertOwnsLock(); + MOZ_RELEASE_ASSERT(mReadHandlesCount == 0); + MOZ_RELEASE_ASSERT(mWriteHandleExists); + mWriteHandleExists = false; +} + +size_t CacheFileChunkBuffer::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = mallocSizeOf(this); + + if (mBuf) { + n += mallocSizeOf(mBuf); + } + + return n; +} + +uint32_t CacheFileChunkHandle::DataSize() { + MOZ_ASSERT(mBuf, "Unexpected call on dummy handle"); + mBuf->AssertOwnsLock(); + return mBuf->mDataSize; +} + +uint32_t CacheFileChunkHandle::Offset() { + MOZ_ASSERT(mBuf, "Unexpected call on dummy handle"); + mBuf->AssertOwnsLock(); + return mBuf->mChunk->Index() * kChunkSize; +} + +CacheFileChunkReadHandle::CacheFileChunkReadHandle(CacheFileChunkBuffer* aBuf) { + mBuf = aBuf; + mBuf->mReadHandlesCount++; +} + +CacheFileChunkReadHandle::~CacheFileChunkReadHandle() { + mBuf->RemoveReadHandle(); +} + +const char* CacheFileChunkReadHandle::Buf() { return mBuf->mBuf; } + +CacheFileChunkWriteHandle::CacheFileChunkWriteHandle( + CacheFileChunkBuffer* aBuf) { + mBuf = aBuf; + if (mBuf) { + MOZ_ASSERT(!mBuf->mWriteHandleExists); + mBuf->mWriteHandleExists = true; + } +} + +CacheFileChunkWriteHandle::~CacheFileChunkWriteHandle() { + if (mBuf) { + mBuf->RemoveWriteHandle(); + } +} + +char* CacheFileChunkWriteHandle::Buf() { return mBuf ? mBuf->mBuf : nullptr; } + +void CacheFileChunkWriteHandle::UpdateDataSize(uint32_t aOffset, + uint32_t aLen) { + MOZ_ASSERT(mBuf, "Write performed on dummy handle?"); + MOZ_ASSERT(aOffset <= mBuf->mDataSize); + MOZ_ASSERT(aOffset + aLen <= mBuf->mBufSize); + + if (aOffset + aLen > mBuf->mDataSize) { + mBuf->mDataSize = aOffset + aLen; + } + + mBuf->mChunk->UpdateDataSize(aOffset, aLen); +} + +class NotifyUpdateListenerEvent : public Runnable { + public: + NotifyUpdateListenerEvent(CacheFileChunkListener* aCallback, + CacheFileChunk* aChunk) + : Runnable("net::NotifyUpdateListenerEvent"), + mCallback(aCallback), + mChunk(aChunk) { + LOG(("NotifyUpdateListenerEvent::NotifyUpdateListenerEvent() [this=%p]", + this)); + } + + protected: + ~NotifyUpdateListenerEvent() { + LOG(("NotifyUpdateListenerEvent::~NotifyUpdateListenerEvent() [this=%p]", + this)); + } + + public: + NS_IMETHOD Run() override { + LOG(("NotifyUpdateListenerEvent::Run() [this=%p]", this)); + + mCallback->OnChunkUpdated(mChunk); + return NS_OK; + } + + protected: + nsCOMPtr mCallback; + RefPtr mChunk; +}; + +bool CacheFileChunk::DispatchRelease() { + if (NS_IsMainThread()) { + return false; + } + + NS_DispatchToMainThread(NewNonOwningRunnableMethod( + "net::CacheFileChunk::Release", this, &CacheFileChunk::Release)); + + return true; +} + +NS_IMPL_ADDREF(CacheFileChunk) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileChunk::Release() { + nsrefcnt count = mRefCnt - 1; + if (DispatchRelease()) { + // Redispatched to the main thread. + return count; + } + + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "CacheFileChunk"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + // We can safely access this chunk after decreasing mRefCnt since we re-post + // all calls to Release() happening off the main thread to the main thread. + // I.e. no other Release() that would delete the object could be run before + // we call CacheFile::DeactivateChunk(). + // + // NOTE: we don't grab the CacheFile's lock, so the chunk might be addrefed + // on another thread before CacheFile::DeactivateChunk() grabs the lock on + // this thread. To make sure we won't deactivate chunk that was just returned + // to a new consumer we check mRefCnt once again in + // CacheFile::DeactivateChunk() after we grab the lock. + if (mActiveChunk && count == 1) { + mFile->DeactivateChunk(this); + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(CacheFileChunk) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileIOListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CacheFileChunk::CacheFileChunk(CacheFile* aFile, uint32_t aIndex, + bool aInitByWriter) + : CacheMemoryConsumer(aFile->mOpenAsMemoryOnly ? MEMORY_ONLY : DONT_REPORT), + mIndex(aIndex), + mState(INITIAL), + mStatus(NS_OK), + mActiveChunk(false), + mIsDirty(false), + mDiscardedChunk(false), + mBuffersSize(0), + mLimitAllocation(!aFile->mOpenAsMemoryOnly && aInitByWriter), + mIsPriority(aFile->mPriority), + mExpectedHash(0), + mFile(aFile) { + LOG(("CacheFileChunk::CacheFileChunk() [this=%p, index=%u, initByWriter=%d]", + this, aIndex, aInitByWriter)); + mBuf = new CacheFileChunkBuffer(this); +} + +CacheFileChunk::~CacheFileChunk() { + LOG(("CacheFileChunk::~CacheFileChunk() [this=%p]", this)); +} + +void CacheFileChunk::AssertOwnsLock() const { mFile->AssertOwnsLock(); } + +void CacheFileChunk::InitNew() { + AssertOwnsLock(); + + LOG(("CacheFileChunk::InitNew() [this=%p]", this)); + + MOZ_ASSERT(mState == INITIAL); + MOZ_ASSERT(NS_SUCCEEDED(mStatus)); + MOZ_ASSERT(!mBuf->Buf()); + MOZ_ASSERT(!mWritingStateHandle); + MOZ_ASSERT(!mReadingStateBuf); + MOZ_ASSERT(!mIsDirty); + + mBuf = new CacheFileChunkBuffer(this); + mState = READY; +} + +nsresult CacheFileChunk::Read(CacheFileHandle* aHandle, uint32_t aLen, + CacheHash::Hash16_t aHash, + CacheFileChunkListener* aCallback) { + AssertOwnsLock(); + + LOG(("CacheFileChunk::Read() [this=%p, handle=%p, len=%d, listener=%p]", this, + aHandle, aLen, aCallback)); + + MOZ_ASSERT(mState == INITIAL); + MOZ_ASSERT(NS_SUCCEEDED(mStatus)); + MOZ_ASSERT(!mBuf->Buf()); + MOZ_ASSERT(!mWritingStateHandle); + MOZ_ASSERT(!mReadingStateBuf); + MOZ_ASSERT(aLen); + + nsresult rv; + + mState = READING; + + RefPtr tmpBuf = new CacheFileChunkBuffer(this); + rv = tmpBuf->EnsureBufSize(aLen); + if (NS_FAILED(rv)) { + SetError(rv); + return mStatus; + } + tmpBuf->SetDataSize(aLen); + + rv = CacheFileIOManager::Read(aHandle, mIndex * kChunkSize, tmpBuf->Buf(), + aLen, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + rv = mIndex ? NS_ERROR_FILE_CORRUPTED : NS_ERROR_FILE_NOT_FOUND; + SetError(rv); + } else { + mReadingStateBuf.swap(tmpBuf); + mListener = aCallback; + // mBuf contains no data but we set datasize to size of the data that will + // be read from the disk. No handle is allowed to access the non-existent + // data until reading finishes, but data can be appended or overwritten. + // These pieces are tracked in mValidityMap and will be merged with the data + // read from disk in OnDataRead(). + mBuf->SetDataSize(aLen); + mExpectedHash = aHash; + } + + return rv; +} + +nsresult CacheFileChunk::Write(CacheFileHandle* aHandle, + CacheFileChunkListener* aCallback) { + AssertOwnsLock(); + + LOG(("CacheFileChunk::Write() [this=%p, handle=%p, listener=%p]", this, + aHandle, aCallback)); + + MOZ_ASSERT(mState == READY); + MOZ_ASSERT(NS_SUCCEEDED(mStatus)); + MOZ_ASSERT(!mWritingStateHandle); + MOZ_ASSERT(mBuf->DataSize()); // Don't write chunk when it is empty + MOZ_ASSERT(mBuf->ReadHandlesCount() == 0); + MOZ_ASSERT(!mBuf->WriteHandleExists()); + + nsresult rv; + + mState = WRITING; + mWritingStateHandle = MakeUnique(mBuf); + + rv = CacheFileIOManager::Write( + aHandle, mIndex * kChunkSize, mWritingStateHandle->Buf(), + mWritingStateHandle->DataSize(), false, false, this); + if (NS_WARN_IF(NS_FAILED(rv))) { + mWritingStateHandle = nullptr; + SetError(rv); + } else { + mListener = aCallback; + mIsDirty = false; + } + + return rv; +} + +void CacheFileChunk::WaitForUpdate(CacheFileChunkListener* aCallback) { + AssertOwnsLock(); + mFile->AssertOwnsLock(); // For thread-safety analysis + + LOG(("CacheFileChunk::WaitForUpdate() [this=%p, listener=%p]", this, + aCallback)); + + MOZ_ASSERT(mFile->mOutput); + MOZ_ASSERT(IsReady()); + +#ifdef DEBUG + for (uint32_t i = 0; i < mUpdateListeners.Length(); i++) { + MOZ_ASSERT(mUpdateListeners[i]->mCallback != aCallback); + } +#endif + + ChunkListenerItem* item = new ChunkListenerItem(); + item->mTarget = CacheFileIOManager::IOTarget(); + if (!item->mTarget) { + LOG( + ("CacheFileChunk::WaitForUpdate() - Cannot get Cache I/O thread! Using " + "main thread for callback.")); + item->mTarget = GetMainThreadSerialEventTarget(); + } + item->mCallback = aCallback; + MOZ_ASSERT(item->mTarget); + item->mCallback = aCallback; + + mUpdateListeners.AppendElement(item); +} + +void CacheFileChunk::CancelWait(CacheFileChunkListener* aCallback) { + AssertOwnsLock(); + + LOG(("CacheFileChunk::CancelWait() [this=%p, listener=%p]", this, aCallback)); + + MOZ_ASSERT(IsReady()); + + uint32_t i; + for (i = 0; i < mUpdateListeners.Length(); i++) { + ChunkListenerItem* item = mUpdateListeners[i]; + + if (item->mCallback == aCallback) { + mUpdateListeners.RemoveElementAt(i); + delete item; + break; + } + } + +#ifdef DEBUG + for (; i < mUpdateListeners.Length(); i++) { + MOZ_ASSERT(mUpdateListeners[i]->mCallback != aCallback); + } +#endif +} + +nsresult CacheFileChunk::NotifyUpdateListeners() { + AssertOwnsLock(); + + LOG(("CacheFileChunk::NotifyUpdateListeners() [this=%p]", this)); + + MOZ_ASSERT(IsReady()); + + nsresult rv, rv2; + + rv = NS_OK; + for (uint32_t i = 0; i < mUpdateListeners.Length(); i++) { + ChunkListenerItem* item = mUpdateListeners[i]; + + LOG( + ("CacheFileChunk::NotifyUpdateListeners() - Notifying listener %p " + "[this=%p]", + item->mCallback.get(), this)); + + RefPtr ev; + ev = new NotifyUpdateListenerEvent(item->mCallback, this); + rv2 = item->mTarget->Dispatch(ev, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv2) && NS_SUCCEEDED(rv)) rv = rv2; + delete item; + } + + mUpdateListeners.Clear(); + + return rv; +} + +uint32_t CacheFileChunk::Index() const { return mIndex; } + +CacheHash::Hash16_t CacheFileChunk::Hash() const { + MOZ_ASSERT(IsReady()); + + return CacheHash::Hash16(mBuf->Buf(), mBuf->DataSize()); +} + +uint32_t CacheFileChunk::DataSize() const { return mBuf->DataSize(); } + +void CacheFileChunk::UpdateDataSize(uint32_t aOffset, uint32_t aLen) { + AssertOwnsLock(); + mFile->AssertOwnsLock(); // For thread-safety analysis + + // UpdateDataSize() is called only when we've written some data to the chunk + // and we never write data anymore once some error occurs. + MOZ_ASSERT(NS_SUCCEEDED(mStatus)); + + LOG(("CacheFileChunk::UpdateDataSize() [this=%p, offset=%d, len=%d]", this, + aOffset, aLen)); + + mIsDirty = true; + + int64_t fileSize = static_cast(kChunkSize) * mIndex + aOffset + aLen; + bool notify = false; + + if (fileSize > mFile->mDataSize) { + mFile->mDataSize = fileSize; + notify = true; + } + + if (mState == READY || mState == WRITING) { + MOZ_ASSERT(mValidityMap.Length() == 0); + + if (notify) { + NotifyUpdateListeners(); + } + + return; + } + + // We're still waiting for data from the disk. This chunk cannot be used by + // input stream, so there must be no update listener. We also need to keep + // track of where the data is written so that we can correctly merge the new + // data with the old one. + + MOZ_ASSERT(mUpdateListeners.Length() == 0); + MOZ_ASSERT(mState == READING); + + mValidityMap.AddPair(aOffset, aLen); + mValidityMap.Log(); +} + +void CacheFileChunk::Truncate(uint32_t aOffset) { + MOZ_RELEASE_ASSERT(mState == READY || mState == WRITING || mState == READING); + + if (mState == READING) { + mIsDirty = true; + } + + mBuf->SetDataSize(aOffset); +} + +nsresult CacheFileChunk::OnFileOpened(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileChunk::OnFileOpened should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileChunk::OnDataWritten(CacheFileHandle* aHandle, + const char* aBuf, nsresult aResult) { + LOG(( + "CacheFileChunk::OnDataWritten() [this=%p, handle=%p, result=0x%08" PRIx32 + "]", + this, aHandle, static_cast(aResult))); + + nsCOMPtr listener; + + { + CacheFileAutoLock lock(mFile); + + MOZ_ASSERT(mState == WRITING); + MOZ_ASSERT(mListener); + + mWritingStateHandle = nullptr; + + if (NS_WARN_IF(NS_FAILED(aResult))) { + SetError(aResult); + } + + mState = READY; + mListener.swap(listener); + } + + listener->OnChunkWritten(aResult, this); + + return NS_OK; +} + +nsresult CacheFileChunk::OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) { + LOG(("CacheFileChunk::OnDataRead() [this=%p, handle=%p, result=0x%08" PRIx32 + "]", + this, aHandle, static_cast(aResult))); + + nsCOMPtr listener; + + { + CacheFileAutoLock lock(mFile); + + MOZ_ASSERT(mState == READING); + MOZ_ASSERT(mListener); + MOZ_ASSERT(mReadingStateBuf); + MOZ_RELEASE_ASSERT(mBuf->ReadHandlesCount() == 0); + MOZ_RELEASE_ASSERT(!mBuf->WriteHandleExists()); + + RefPtr tmpBuf; + tmpBuf.swap(mReadingStateBuf); + + if (NS_SUCCEEDED(aResult)) { + CacheHash::Hash16_t hash = + CacheHash::Hash16(tmpBuf->Buf(), tmpBuf->DataSize()); + if (hash != mExpectedHash) { + LOG( + ("CacheFileChunk::OnDataRead() - Hash mismatch! Hash of the data is" + " %hx, hash in metadata is %hx. [this=%p, idx=%d]", + hash, mExpectedHash, this, mIndex)); + aResult = NS_ERROR_FILE_CORRUPTED; + } else { + if (mBuf->DataSize() < tmpBuf->DataSize()) { + // Truncate() was called while the data was being read. + tmpBuf->SetDataSize(mBuf->DataSize()); + } + + if (!mBuf->Buf()) { + // Just swap the buffers if mBuf is still empty + mBuf.swap(tmpBuf); + } else { + LOG(("CacheFileChunk::OnDataRead() - Merging buffers. [this=%p]", + this)); + + mValidityMap.Log(); + aResult = mBuf->FillInvalidRanges(tmpBuf, &mValidityMap); + mValidityMap.Clear(); + } + } + } + + if (NS_FAILED(aResult)) { + aResult = mIndex ? NS_ERROR_FILE_CORRUPTED : NS_ERROR_FILE_NOT_FOUND; + SetError(aResult); + mBuf->SetDataSize(0); + } + + mState = READY; + mListener.swap(listener); + } + + listener->OnChunkRead(aResult, this); + + return NS_OK; +} + +nsresult CacheFileChunk::OnFileDoomed(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileChunk::OnFileDoomed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileChunk::OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheFileChunk::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileChunk::OnFileRenamed(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileChunk::OnFileRenamed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +bool CacheFileChunk::IsKilled() { return mFile->IsKilled(); } + +bool CacheFileChunk::IsReady() const { + return (NS_SUCCEEDED(mStatus) && (mState == READY || mState == WRITING)); +} + +bool CacheFileChunk::IsDirty() const { + AssertOwnsLock(); + + return mIsDirty; +} + +nsresult CacheFileChunk::GetStatus() { return mStatus; } + +void CacheFileChunk::SetError(nsresult aStatus) { + LOG(("CacheFileChunk::SetError() [this=%p, status=0x%08" PRIx32 "]", this, + static_cast(aStatus))); + + MOZ_ASSERT(NS_FAILED(aStatus)); + + if (NS_FAILED(mStatus)) { + // Remember only the first error code. + return; + } + + mStatus = aStatus; +} + +CacheFileChunkReadHandle CacheFileChunk::GetReadHandle() { + LOG(("CacheFileChunk::GetReadHandle() [this=%p]", this)); + + AssertOwnsLock(); + + MOZ_RELEASE_ASSERT(mState == READY || mState == WRITING); + // We don't release the lock when writing the data and CacheFileOutputStream + // doesn't get the read handle, so there cannot be a write handle when read + // handle is obtained. + MOZ_RELEASE_ASSERT(!mBuf->WriteHandleExists()); + + return CacheFileChunkReadHandle(mBuf); +} + +CacheFileChunkWriteHandle CacheFileChunk::GetWriteHandle( + uint32_t aEnsuredBufSize) { + LOG(("CacheFileChunk::GetWriteHandle() [this=%p, ensuredBufSize=%u]", this, + aEnsuredBufSize)); + + AssertOwnsLock(); + + if (NS_FAILED(mStatus)) { + return CacheFileChunkWriteHandle(nullptr); // dummy handle + } + + nsresult rv; + + // We don't support multiple write handles + MOZ_RELEASE_ASSERT(!mBuf->WriteHandleExists()); + + if (mBuf->ReadHandlesCount()) { + LOG( + ("CacheFileChunk::GetWriteHandle() - cloning buffer because of existing" + " read handle")); + + MOZ_RELEASE_ASSERT(mState != READING); + RefPtr newBuf = new CacheFileChunkBuffer(this); + rv = newBuf->EnsureBufSize(std::max(aEnsuredBufSize, mBuf->DataSize())); + if (NS_SUCCEEDED(rv)) { + newBuf->CopyFrom(mBuf); + mOldBufs.AppendElement(mBuf); + mBuf = newBuf; + } + } else { + rv = mBuf->EnsureBufSize(aEnsuredBufSize); + } + + if (NS_FAILED(rv)) { + SetError(NS_ERROR_OUT_OF_MEMORY); + return CacheFileChunkWriteHandle(nullptr); // dummy handle + } + + return CacheFileChunkWriteHandle(mBuf); +} + +// Memory reporting + +size_t CacheFileChunk::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = mBuf->SizeOfIncludingThis(mallocSizeOf); + + if (mReadingStateBuf) { + n += mReadingStateBuf->SizeOfIncludingThis(mallocSizeOf); + } + + for (uint32_t i = 0; i < mOldBufs.Length(); ++i) { + n += mOldBufs[i]->SizeOfIncludingThis(mallocSizeOf); + } + + n += mValidityMap.SizeOfExcludingThis(mallocSizeOf); + + return n; +} + +size_t CacheFileChunk::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +bool CacheFileChunk::CanAllocate(uint32_t aSize) const { + if (!mLimitAllocation) { + return true; + } + + LOG(("CacheFileChunk::CanAllocate() [this=%p, size=%u]", this, aSize)); + + int64_t limit = CacheObserver::MaxDiskChunksMemoryUsage(mIsPriority); + if (limit == 0) { + return true; + } + + limit <<= 10; + if (limit > UINT32_MAX) { + limit = UINT32_MAX; + } + + int64_t usage = ChunksMemoryUsage(); + if (usage + aSize > limit) { + LOG(("CacheFileChunk::CanAllocate() - Returning false. [this=%p]", this)); + return false; + } + + return true; +} + +void CacheFileChunk::BuffersAllocationChanged(uint32_t aFreed, + uint32_t aAllocated) { + uint32_t oldBuffersSize = mBuffersSize; + mBuffersSize += aAllocated; + mBuffersSize -= aFreed; + + DoMemoryReport(sizeof(CacheFileChunk) + mBuffersSize); + + if (!mLimitAllocation) { + return; + } + + ChunksMemoryUsage() -= oldBuffersSize; + ChunksMemoryUsage() += mBuffersSize; + LOG( + ("CacheFileChunk::BuffersAllocationChanged() - %s chunks usage %u " + "[this=%p]", + mIsPriority ? "Priority" : "Normal", + static_cast(ChunksMemoryUsage()), this)); +} + +mozilla::Atomic& CacheFileChunk::ChunksMemoryUsage() + const { + static mozilla::Atomic chunksMemoryUsage(0); + static mozilla::Atomic prioChunksMemoryUsage(0); + return mIsPriority ? prioChunksMemoryUsage : chunksMemoryUsage; +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileChunk.h b/netwerk/cache2/CacheFileChunk.h new file mode 100644 index 0000000000..4d20bf19eb --- /dev/null +++ b/netwerk/cache2/CacheFileChunk.h @@ -0,0 +1,238 @@ +/* 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/. */ + +#ifndef CacheFileChunk__h__ +#define CacheFileChunk__h__ + +#include "CacheFileIOManager.h" +#include "CacheStorageService.h" +#include "CacheHashUtils.h" +#include "CacheFileUtils.h" +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" + +namespace mozilla { +namespace net { + +constexpr int32_t kChunkSize = 256 * 1024; +constexpr size_t kEmptyChunkHash = 0x1826; + +class CacheFileChunk; +class CacheFile; + +class CacheFileChunkBuffer { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheFileChunkBuffer) + + explicit CacheFileChunkBuffer(CacheFileChunk* aChunk); + + nsresult EnsureBufSize(uint32_t aBufSize); + void CopyFrom(CacheFileChunkBuffer* aOther); + nsresult FillInvalidRanges(CacheFileChunkBuffer* aOther, + CacheFileUtils::ValidityMap* aMap); + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + char* Buf() const { return mBuf; } + void SetDataSize(uint32_t aDataSize); + uint32_t DataSize() const { return mDataSize; } + uint32_t ReadHandlesCount() const { return mReadHandlesCount; } + bool WriteHandleExists() const { return mWriteHandleExists; } + + private: + friend class CacheFileChunkHandle; + friend class CacheFileChunkReadHandle; + friend class CacheFileChunkWriteHandle; + + ~CacheFileChunkBuffer(); + + void AssertOwnsLock() const; + + void RemoveReadHandle(); + void RemoveWriteHandle(); + + // We keep a weak reference to the chunk to not create a reference cycle. The + // buffer is referenced only by chunk and handles. Handles are always + // destroyed before the chunk so it is guaranteed that mChunk is a valid + // pointer for the whole buffer's lifetime. + CacheFileChunk* mChunk; + char* mBuf; + uint32_t mBufSize; + uint32_t mDataSize; + uint32_t mReadHandlesCount; + bool mWriteHandleExists; +}; + +class CacheFileChunkHandle { + public: + uint32_t DataSize(); + uint32_t Offset(); + + protected: + RefPtr mBuf; +}; + +class CacheFileChunkReadHandle : public CacheFileChunkHandle { + public: + explicit CacheFileChunkReadHandle(CacheFileChunkBuffer* aBuf); + ~CacheFileChunkReadHandle(); + + const char* Buf(); +}; + +class CacheFileChunkWriteHandle : public CacheFileChunkHandle { + public: + explicit CacheFileChunkWriteHandle(CacheFileChunkBuffer* aBuf); + ~CacheFileChunkWriteHandle(); + + char* Buf(); + void UpdateDataSize(uint32_t aOffset, uint32_t aLen); +}; + +#define CACHEFILECHUNKLISTENER_IID \ + { /* baf16149-2ab5-499c-a9c2-5904eb95c288 */ \ + 0xbaf16149, 0x2ab5, 0x499c, { \ + 0xa9, 0xc2, 0x59, 0x04, 0xeb, 0x95, 0xc2, 0x88 \ + } \ + } + +class CacheFileChunkListener : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(CACHEFILECHUNKLISTENER_IID) + + NS_IMETHOD OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) = 0; + NS_IMETHOD OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) = 0; + NS_IMETHOD OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) = 0; + NS_IMETHOD OnChunkUpdated(CacheFileChunk* aChunk) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(CacheFileChunkListener, + CACHEFILECHUNKLISTENER_IID) + +class ChunkListenerItem { + public: + MOZ_COUNTED_DEFAULT_CTOR(ChunkListenerItem) + MOZ_COUNTED_DTOR(ChunkListenerItem) + + nsCOMPtr mTarget; + nsCOMPtr mCallback; +}; + +class ChunkListeners { + public: + MOZ_COUNTED_DEFAULT_CTOR(ChunkListeners) + MOZ_COUNTED_DTOR(ChunkListeners) + + nsTArray mItems; +}; + +class CacheFileChunk final : public CacheFileIOListener, + public CacheMemoryConsumer { + public: + NS_DECL_THREADSAFE_ISUPPORTS + bool DispatchRelease(); + + CacheFileChunk(CacheFile* aFile, uint32_t aIndex, bool aInitByWriter); + + void InitNew(); + nsresult Read(CacheFileHandle* aHandle, uint32_t aLen, + CacheHash::Hash16_t aHash, CacheFileChunkListener* aCallback); + nsresult Write(CacheFileHandle* aHandle, CacheFileChunkListener* aCallback); + void WaitForUpdate(CacheFileChunkListener* aCallback); + void CancelWait(CacheFileChunkListener* aCallback); + nsresult NotifyUpdateListeners(); + + uint32_t Index() const; + CacheHash::Hash16_t Hash() const; + uint32_t DataSize() const; + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) override; + virtual bool IsKilled() override; + + bool IsReady() const; + bool IsDirty() const; + + nsresult GetStatus(); + void SetError(nsresult aStatus); + + CacheFileChunkReadHandle GetReadHandle(); + CacheFileChunkWriteHandle GetWriteHandle(uint32_t aEnsuredBufSize); + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + friend class CacheFileChunkBuffer; + friend class CacheFileChunkWriteHandle; + friend class CacheFileInputStream; + friend class CacheFileOutputStream; + friend class CacheFile; + + virtual ~CacheFileChunk(); + + void AssertOwnsLock() const; + + void UpdateDataSize(uint32_t aOffset, uint32_t aLen); + void Truncate(uint32_t aOffset); + + bool CanAllocate(uint32_t aSize) const; + void BuffersAllocationChanged(uint32_t aFreed, uint32_t aAllocated); + + mozilla::Atomic& ChunksMemoryUsage() const; + + enum EState { INITIAL = 0, READING = 1, WRITING = 2, READY = 3 }; + + uint32_t mIndex; + EState mState; + nsresult mStatus; + + Atomic mActiveChunk; // Is true iff the chunk is in CacheFile::mChunks. + // Adding/removing chunk to/from mChunks as well + // as changing this member happens under the + // CacheFile's lock. + bool mIsDirty : 1; + bool mDiscardedChunk : 1; + + uint32_t mBuffersSize; + bool const mLimitAllocation : 1; // Whether this chunk respects limit for + // disk chunks memory usage. + bool const mIsPriority : 1; + + // Buffer containing the chunk data. Multiple read handles can access the same + // buffer. When write handle is created and some read handle exists a new copy + // of the buffer is created. This prevents invalidating the buffer when + // CacheFileInputStream::ReadSegments calls the handler outside the lock. + RefPtr mBuf; + + // We need to keep pointers of the old buffers for memory reporting. + nsTArray> mOldBufs; + + // Read handle that is used during writing the chunk to the disk. + UniquePtr mWritingStateHandle; + + // Buffer that is used to read the chunk from the disk. It is allowed to write + // a new data to chunk while we wait for the data from the disk. In this case + // this buffer is merged with mBuf in OnDataRead(). + RefPtr mReadingStateBuf; + CacheHash::Hash16_t mExpectedHash; + + RefPtr mFile; // is null if chunk is cached to + // prevent reference cycles + nsCOMPtr mListener; + nsTArray mUpdateListeners; + CacheFileUtils::ValidityMap mValidityMap; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileContextEvictor.cpp b/netwerk/cache2/CacheFileContextEvictor.cpp new file mode 100644 index 0000000000..0e68089207 --- /dev/null +++ b/netwerk/cache2/CacheFileContextEvictor.cpp @@ -0,0 +1,741 @@ +/* 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 "CacheLog.h" +#include "CacheFileContextEvictor.h" +#include "CacheFileIOManager.h" +#include "CacheFileMetadata.h" +#include "CacheIndex.h" +#include "CacheIndexIterator.h" +#include "CacheFileUtils.h" +#include "CacheObserver.h" +#include "nsIFile.h" +#include "LoadContextInfo.h" +#include "nsThreadUtils.h" +#include "nsString.h" +#include "nsIDirectoryEnumerator.h" +#include "mozilla/Base64.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "nsContentUtils.h" +#include "nsNetUtil.h" + +namespace mozilla::net { + +#define CONTEXT_EVICTION_PREFIX "ce_" +const uint32_t kContextEvictionPrefixLength = + sizeof(CONTEXT_EVICTION_PREFIX) - 1; + +bool CacheFileContextEvictor::sDiskAlreadySearched = false; + +CacheFileContextEvictor::CacheFileContextEvictor() { + LOG(("CacheFileContextEvictor::CacheFileContextEvictor() [this=%p]", this)); +} + +CacheFileContextEvictor::~CacheFileContextEvictor() { + LOG(("CacheFileContextEvictor::~CacheFileContextEvictor() [this=%p]", this)); +} + +nsresult CacheFileContextEvictor::Init(nsIFile* aCacheDirectory) { + LOG(("CacheFileContextEvictor::Init()")); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + CacheIndex::IsUpToDate(&mIndexIsUpToDate); + + mCacheDirectory = aCacheDirectory; + + rv = aCacheDirectory->Clone(getter_AddRefs(mEntriesDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = mEntriesDir->AppendNative(nsLiteralCString(ENTRIES_DIR)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!sDiskAlreadySearched) { + LoadEvictInfoFromDisk(); + if ((mEntries.Length() != 0) && mIndexIsUpToDate) { + CreateIterators(); + StartEvicting(); + } + } + + return NS_OK; +} + +void CacheFileContextEvictor::Shutdown() { + LOG(("CacheFileContextEvictor::Shutdown()")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + CloseIterators(); +} + +uint32_t CacheFileContextEvictor::ContextsCount() { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + return mEntries.Length(); +} + +nsresult CacheFileContextEvictor::AddContext( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin) { + LOG( + ("CacheFileContextEvictor::AddContext() [this=%p, loadContextInfo=%p, " + "pinned=%d]", + this, aLoadContextInfo, aPinned)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + CacheFileContextEvictorEntry* entry = nullptr; + if (aLoadContextInfo) { + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + if (mEntries[i]->mInfo && mEntries[i]->mInfo->Equals(aLoadContextInfo) && + mEntries[i]->mPinned == aPinned && + mEntries[i]->mOrigin.Equals(aOrigin)) { + entry = mEntries[i].get(); + break; + } + } + } else { + // Not providing load context info means we want to delete everything, + // so let's not bother with any currently running context cleanups + // for the same pinning state. + for (uint32_t i = mEntries.Length(); i > 0;) { + --i; + if (mEntries[i]->mInfo && mEntries[i]->mPinned == aPinned) { + RemoveEvictInfoFromDisk(mEntries[i]->mInfo, mEntries[i]->mPinned, + mEntries[i]->mOrigin); + mEntries.RemoveElementAt(i); + } + } + } + + if (!entry) { + entry = new CacheFileContextEvictorEntry(); + entry->mInfo = aLoadContextInfo; + entry->mPinned = aPinned; + entry->mOrigin = aOrigin; + mEntries.AppendElement(WrapUnique(entry)); + } + + entry->mTimeStamp = PR_Now() / PR_USEC_PER_MSEC; + + PersistEvictionInfoToDisk(aLoadContextInfo, aPinned, aOrigin); + + if (mIndexIsUpToDate) { + // Already existing context could be added again, in this case the iterator + // would be recreated. Close the old iterator explicitely. + if (entry->mIterator) { + entry->mIterator->Close(); + entry->mIterator = nullptr; + } + + rv = CacheIndex::GetIterator(aLoadContextInfo, false, + getter_AddRefs(entry->mIterator)); + if (NS_FAILED(rv)) { + // This could probably happen during shutdown. Remove the entry from + // the array, but leave the info on the disk. No entry can be opened + // during shutdown and we'll load the eviction info on next start. + LOG( + ("CacheFileContextEvictor::AddContext() - Cannot get an iterator. " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + mEntries.RemoveElement(entry); + return rv; + } + + StartEvicting(); + } + + return NS_OK; +} + +void CacheFileContextEvictor::CacheIndexStateChanged() { + LOG(("CacheFileContextEvictor::CacheIndexStateChanged() [this=%p]", this)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + bool isUpToDate = false; + CacheIndex::IsUpToDate(&isUpToDate); + if (mEntries.Length() == 0) { + // Just save the state and exit, since there is nothing to do + mIndexIsUpToDate = isUpToDate; + return; + } + + if (!isUpToDate && !mIndexIsUpToDate) { + // Index is outdated and status has not changed, nothing to do. + return; + } + + if (isUpToDate && mIndexIsUpToDate) { + // Status has not changed, but make sure the eviction is running. + if (mEvicting) { + return; + } + + // We're not evicting, but we should be evicting?! + LOG( + ("CacheFileContextEvictor::CacheIndexStateChanged() - Index is up to " + "date, we have some context to evict but eviction is not running! " + "Starting now.")); + } + + mIndexIsUpToDate = isUpToDate; + + if (mIndexIsUpToDate) { + CreateIterators(); + StartEvicting(); + } else { + CloseIterators(); + } +} + +void CacheFileContextEvictor::WasEvicted(const nsACString& aKey, nsIFile* aFile, + bool* aEvictedAsPinned, + bool* aEvictedAsNonPinned) { + LOG(("CacheFileContextEvictor::WasEvicted() [key=%s]", + PromiseFlatCString(aKey).get())); + + *aEvictedAsPinned = false; + *aEvictedAsNonPinned = false; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr info = CacheFileUtils::ParseKey(aKey); + MOZ_ASSERT(info); + if (!info) { + LOG(("CacheFileContextEvictor::WasEvicted() - Cannot parse key!")); + return; + } + + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + const auto& entry = mEntries[i]; + + if (entry->mInfo && !info->Equals(entry->mInfo)) { + continue; + } + + PRTime lastModifiedTime; + if (NS_FAILED(aFile->GetLastModifiedTime(&lastModifiedTime))) { + LOG( + ("CacheFileContextEvictor::WasEvicted() - Cannot get last modified " + "time, returning.")); + return; + } + + if (lastModifiedTime > entry->mTimeStamp) { + // File has been modified since context eviction. + continue; + } + + LOG( + ("CacheFileContextEvictor::WasEvicted() - evicted [pinning=%d, " + "mTimeStamp=%" PRId64 ", lastModifiedTime=%" PRId64 "]", + entry->mPinned, entry->mTimeStamp, lastModifiedTime)); + + if (entry->mPinned) { + *aEvictedAsPinned = true; + } else { + *aEvictedAsNonPinned = true; + } + } +} + +nsresult CacheFileContextEvictor::PersistEvictionInfoToDisk( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin) { + LOG( + ("CacheFileContextEvictor::PersistEvictionInfoToDisk() [this=%p, " + "loadContextInfo=%p]", + this, aLoadContextInfo)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr file; + rv = GetContextFile(aLoadContextInfo, aPinned, aOrigin, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString path = file->HumanReadablePath(); + + PRFileDesc* fd; + rv = + file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, &fd); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileContextEvictor::PersistEvictionInfoToDisk() - Creating file " + "failed! [path=%s, rv=0x%08" PRIx32 "]", + path.get(), static_cast(rv))); + return rv; + } + + PR_Close(fd); + + LOG( + ("CacheFileContextEvictor::PersistEvictionInfoToDisk() - Successfully " + "created file. [path=%s]", + path.get())); + + return NS_OK; +} + +nsresult CacheFileContextEvictor::RemoveEvictInfoFromDisk( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin) { + LOG( + ("CacheFileContextEvictor::RemoveEvictInfoFromDisk() [this=%p, " + "loadContextInfo=%p]", + this, aLoadContextInfo)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr file; + rv = GetContextFile(aLoadContextInfo, aPinned, aOrigin, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCString path = file->HumanReadablePath(); + + rv = file->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileContextEvictor::RemoveEvictionInfoFromDisk() - Removing file" + " failed! [path=%s, rv=0x%08" PRIx32 "]", + path.get(), static_cast(rv))); + return rv; + } + + LOG( + ("CacheFileContextEvictor::RemoveEvictionInfoFromDisk() - Successfully " + "removed file. [path=%s]", + path.get())); + + return NS_OK; +} + +nsresult CacheFileContextEvictor::LoadEvictInfoFromDisk() { + LOG(("CacheFileContextEvictor::LoadEvictInfoFromDisk() [this=%p]", this)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + sDiskAlreadySearched = true; + + nsCOMPtr dirEnum; + rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(dirEnum)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + while (true) { + nsCOMPtr file; + rv = dirEnum->GetNextFile(getter_AddRefs(file)); + if (!file) { + break; + } + + bool isDir = false; + file->IsDirectory(&isDir); + if (isDir) { + continue; + } + + nsAutoCString leaf; + rv = file->GetNativeLeafName(leaf); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::LoadEvictInfoFromDisk() - " + "GetNativeLeafName() failed! Skipping file.")); + continue; + } + + if (leaf.Length() < kContextEvictionPrefixLength) { + continue; + } + + if (!StringBeginsWith(leaf, nsLiteralCString(CONTEXT_EVICTION_PREFIX))) { + continue; + } + + nsAutoCString encoded; + encoded = Substring(leaf, kContextEvictionPrefixLength); + encoded.ReplaceChar('-', '/'); + + nsAutoCString decoded; + rv = Base64Decode(encoded, decoded); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::LoadEvictInfoFromDisk() - Base64 decoding " + "failed. Removing the file. [file=%s]", + leaf.get())); + file->Remove(false); + continue; + } + + bool pinned = decoded[0] == '\t'; + if (pinned) { + decoded = Substring(decoded, 1); + } + + // Let's see if we have an origin. + nsAutoCString origin; + if (decoded.Contains('\t')) { + auto split = decoded.Split('\t'); + MOZ_ASSERT(decoded.CountChar('\t') == 1); + + auto splitIt = split.begin(); + origin = *splitIt; + ++splitIt; + decoded = *splitIt; + } + + nsCOMPtr info; + if (!"*"_ns.Equals(decoded)) { + // "*" is indication of 'delete all', info left null will pass + // to CacheFileContextEvictor::AddContext and clear all the cache data. + info = CacheFileUtils::ParseKey(decoded); + if (!info) { + LOG( + ("CacheFileContextEvictor::LoadEvictInfoFromDisk() - Cannot parse " + "context key, removing file. [contextKey=%s, file=%s]", + decoded.get(), leaf.get())); + file->Remove(false); + continue; + } + } + + PRTime lastModifiedTime; + rv = file->GetLastModifiedTime(&lastModifiedTime); + if (NS_FAILED(rv)) { + continue; + } + + CacheFileContextEvictorEntry* entry = new CacheFileContextEvictorEntry(); + entry->mInfo = info; + entry->mPinned = pinned; + CopyUTF8toUTF16(origin, entry->mOrigin); + entry->mTimeStamp = lastModifiedTime; + mEntries.AppendElement(entry); + } + + return NS_OK; +} + +nsresult CacheFileContextEvictor::GetContextFile( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, nsIFile** _retval) { + nsresult rv; + + nsAutoCString keyPrefix; + if (aPinned) { + // Mark pinned context files with a tab char at the start. + // Tab is chosen because it can never be used as a context key tag. + keyPrefix.Append('\t'); + } + if (aLoadContextInfo) { + CacheFileUtils::AppendKeyPrefix(aLoadContextInfo, keyPrefix); + } else { + keyPrefix.Append('*'); + } + if (!aOrigin.IsEmpty()) { + keyPrefix.Append('\t'); + keyPrefix.Append(NS_ConvertUTF16toUTF8(aOrigin)); + } + + nsAutoCString leafName; + leafName.AssignLiteral(CONTEXT_EVICTION_PREFIX); + + rv = Base64EncodeAppend(keyPrefix, leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Replace '/' with '-' since '/' cannot be part of the filename. + leafName.ReplaceChar('/', '-'); + + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(leafName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + file.swap(*_retval); + return NS_OK; +} + +void CacheFileContextEvictor::CreateIterators() { + LOG(("CacheFileContextEvictor::CreateIterators() [this=%p]", this)); + + CloseIterators(); + + nsresult rv; + + for (uint32_t i = 0; i < mEntries.Length();) { + rv = CacheIndex::GetIterator(mEntries[i]->mInfo, false, + getter_AddRefs(mEntries[i]->mIterator)); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::CreateIterators() - Cannot get an iterator" + ". [rv=0x%08" PRIx32 "]", + static_cast(rv))); + mEntries.RemoveElementAt(i); + continue; + } + + ++i; + } +} + +void CacheFileContextEvictor::CloseIterators() { + LOG(("CacheFileContextEvictor::CloseIterators() [this=%p]", this)); + + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + if (mEntries[i]->mIterator) { + mEntries[i]->mIterator->Close(); + mEntries[i]->mIterator = nullptr; + } + } +} + +void CacheFileContextEvictor::StartEvicting() { + LOG(("CacheFileContextEvictor::StartEvicting() [this=%p]", this)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + if (mEvicting) { + LOG(("CacheFileContextEvictor::StartEvicting() - already evicting.")); + return; + } + + if (mEntries.Length() == 0) { + LOG(("CacheFileContextEvictor::StartEvicting() - no context to evict.")); + return; + } + + nsCOMPtr ev = + NewRunnableMethod("net::CacheFileContextEvictor::EvictEntries", this, + &CacheFileContextEvictor::EvictEntries); + + RefPtr ioThread = CacheFileIOManager::IOThread(); + + nsresult rv = ioThread->Dispatch(ev, CacheIOThread::EVICT); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::StartEvicting() - Cannot dispatch event to " + "IO thread. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + + mEvicting = true; +} + +void CacheFileContextEvictor::EvictEntries() { + LOG(("CacheFileContextEvictor::EvictEntries()")); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + mEvicting = false; + + if (!mIndexIsUpToDate) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Stopping evicting due to " + "outdated index.")); + return; + } + + while (true) { + if (CacheObserver::ShuttingDown()) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Stopping evicting due to " + "shutdown.")); + mEvicting = + true; // We don't want to start eviction again during shutdown + // process. Setting this flag to true ensures it. + return; + } + + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Breaking loop for higher " + "level events.")); + mEvicting = true; + return; + } + + if (mEntries.Length() == 0) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Stopping evicting, there " + "is no context to evict.")); + + // Allow index to notify AsyncGetDiskConsumption callbacks. The size is + // actual again. + CacheIndex::OnAsyncEviction(false); + return; + } + + SHA1Sum::Hash hash; + rv = mEntries[0]->mIterator->GetNextHash(&hash); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - No more entries left in " + "iterator. [iterator=%p, info=%p]", + mEntries[0]->mIterator.get(), mEntries[0]->mInfo.get())); + RemoveEvictInfoFromDisk(mEntries[0]->mInfo, mEntries[0]->mPinned, + mEntries[0]->mOrigin); + mEntries.RemoveElementAt(0); + continue; + } + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Iterator failed to " + "provide next hash (shutdown?), keeping eviction info on disk." + " [iterator=%p, info=%p]", + mEntries[0]->mIterator.get(), mEntries[0]->mInfo.get())); + mEntries.RemoveElementAt(0); + continue; + } + + LOG( + ("CacheFileContextEvictor::EvictEntries() - Processing hash. " + "[hash=%08x%08x%08x%08x%08x, iterator=%p, info=%p]", + LOGSHA1(&hash), mEntries[0]->mIterator.get(), + mEntries[0]->mInfo.get())); + + RefPtr handle; + CacheFileIOManager::gInstance->mHandles.GetHandle(&hash, + getter_AddRefs(handle)); + if (handle) { + // We doom any active handle in CacheFileIOManager::EvictByContext(), so + // this must be a new one. Skip it. + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping entry since we " + "found an active handle. [handle=%p]", + handle.get())); + continue; + } + + CacheIndex::EntryStatus status; + bool pinned = false; + auto callback = [&pinned](const CacheIndexEntry* aEntry) { + pinned = aEntry->IsPinned(); + }; + rv = CacheIndex::HasEntry(hash, &status, callback); + // This must never fail, since eviction (this code) happens only when the + // index is up-to-date and thus the informatin is known. + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (pinned != mEntries[0]->mPinned) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping entry since " + "pinning " + "doesn't match [evicting pinned=%d, entry pinned=%d]", + mEntries[0]->mPinned, pinned)); + continue; + } + + if (!mEntries[0]->mOrigin.IsEmpty()) { + nsCOMPtr file; + CacheFileIOManager::gInstance->GetFile(&hash, getter_AddRefs(file)); + + // Read metadata from the file synchronously + RefPtr metadata = new CacheFileMetadata(); + rv = metadata->SyncReadMetadata(file); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + // Now get the context + enhance id + URL from the key. + nsAutoCString uriSpec; + RefPtr info = + CacheFileUtils::ParseKey(metadata->GetKey(), nullptr, &uriSpec); + MOZ_ASSERT(info); + if (!info) { + continue; + } + + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping entry since " + "NS_NewURI failed to parse the uriSpec")); + continue; + } + + nsAutoString urlOrigin; + rv = nsContentUtils::GetUTFOrigin(uri, urlOrigin); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping entry since " + "We failed to extract an origin")); + continue; + } + + if (!urlOrigin.Equals(mEntries[0]->mOrigin)) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping entry since " + "origin " + "doesn't match")); + continue; + } + } + + nsAutoCString leafName; + CacheFileIOManager::HashToStr(&hash, leafName); + + PRTime lastModifiedTime; + nsCOMPtr file; + rv = mEntriesDir->Clone(getter_AddRefs(file)); + if (NS_SUCCEEDED(rv)) { + rv = file->AppendNative(leafName); + } + if (NS_SUCCEEDED(rv)) { + rv = file->GetLastModifiedTime(&lastModifiedTime); + } + if (NS_FAILED(rv)) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Cannot get last modified " + "time, skipping entry.")); + continue; + } + + if (lastModifiedTime > mEntries[0]->mTimeStamp) { + LOG( + ("CacheFileContextEvictor::EvictEntries() - Skipping newer entry. " + "[mTimeStamp=%" PRId64 ", lastModifiedTime=%" PRId64 "]", + mEntries[0]->mTimeStamp, lastModifiedTime)); + continue; + } + + LOG(("CacheFileContextEvictor::EvictEntries - Removing entry.")); + file->Remove(false); + CacheIndex::RemoveEntry(&hash); + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileContextEvictor.h b/netwerk/cache2/CacheFileContextEvictor.h new file mode 100644 index 0000000000..518d3b7d5f --- /dev/null +++ b/netwerk/cache2/CacheFileContextEvictor.h @@ -0,0 +1,100 @@ +/* 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/. */ + +#ifndef CacheFileContextEvictor__h__ +#define CacheFileContextEvictor__h__ + +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsIFile; +class nsILoadContextInfo; + +namespace mozilla { +namespace net { + +class CacheIndexIterator; + +struct CacheFileContextEvictorEntry { + nsCOMPtr mInfo; + bool mPinned = false; + nsString mOrigin; // it can be empty + PRTime mTimeStamp = 0; // in milliseconds + RefPtr mIterator; +}; + +class CacheFileContextEvictor { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheFileContextEvictor) + + CacheFileContextEvictor(); + + private: + virtual ~CacheFileContextEvictor(); + + public: + nsresult Init(nsIFile* aCacheDirectory); + void Shutdown(); + + // Returns number of contexts that are being evicted. + uint32_t ContextsCount(); + // Start evicting given context and an origin, if not empty. + nsresult AddContext(nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin); + // CacheFileIOManager calls this method when CacheIndex's state changes. We + // check whether the index is up to date and start or stop evicting according + // to index's state. + void CacheIndexStateChanged(); + // CacheFileIOManager calls this method to check whether an entry file should + // be considered as evicted. It returns true when there is a matching context + // info to the given key and the last modified time of the entry file is + // earlier than the time stamp of the time when the context was added to the + // evictor. + void WasEvicted(const nsACString& aKey, nsIFile* aFile, + bool* aEvictedAsPinned, bool* aEvictedAsNonPinned); + + private: + // Writes information about eviction of the given context to the disk. This is + // done for every context added to the evictor to be able to recover eviction + // after a shutdown or crash. When the context file is found after startup, we + // restore mTimeStamp from the last modified time of the file. + nsresult PersistEvictionInfoToDisk(nsILoadContextInfo* aLoadContextInfo, + bool aPinned, const nsAString& aOrigin); + // Once we are done with eviction for the given context, the eviction info is + // removed from the disk. + nsresult RemoveEvictInfoFromDisk(nsILoadContextInfo* aLoadContextInfo, + bool aPinned, const nsAString& aOrigin); + // Tries to load all contexts from the disk. This method is called just once + // after startup. + nsresult LoadEvictInfoFromDisk(); + nsresult GetContextFile(nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, nsIFile** _retval); + + void CreateIterators(); + void CloseIterators(); + void StartEvicting(); + void EvictEntries(); + + // Whether eviction is in progress + bool mEvicting{false}; + // Whether index is up to date. We wait with eviction until the index finishes + // update process when it is outdated. NOTE: We also stop eviction in progress + // when the index is found outdated, the eviction is restarted again once the + // update process finishes. + bool mIndexIsUpToDate{false}; + // Whether we already tried to restore unfinished jobs from previous run after + // startup. + static bool sDiskAlreadySearched; + // Array of contexts being evicted. + nsTArray> mEntries; + nsCOMPtr mCacheDirectory; + nsCOMPtr mEntriesDir; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp new file mode 100644 index 0000000000..41c775ff68 --- /dev/null +++ b/netwerk/cache2/CacheFileIOManager.cpp @@ -0,0 +1,4447 @@ +/* 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 +#include "CacheLog.h" +#include "CacheFileIOManager.h" + +#include "CacheHashUtils.h" +#include "CacheStorageService.h" +#include "CacheIndex.h" +#include "CacheFileUtils.h" +#include "nsError.h" +#include "nsThreadUtils.h" +#include "CacheFile.h" +#include "CacheObserver.h" +#include "nsIFile.h" +#include "CacheFileContextEvictor.h" +#include "nsITimer.h" +#include "nsIDirectoryEnumerator.h" +#include "nsEffectiveTLDService.h" +#include "nsIObserverService.h" +#include "nsISizeOf.h" +#include "mozilla/net/MozURL.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "private/pprio.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Preferences.h" +#include "nsNetUtil.h" + +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasksRunner.h" +# include "nsIBackgroundTasks.h" +#endif + +// include files for ftruncate (or equivalent) +#if defined(XP_UNIX) +# include +#elif defined(XP_WIN) +# include +# undef CreateFile +# undef CREATE_NEW +#else +// XXX add necessary include file for ftruncate (or equivalent) +#endif + +namespace mozilla::net { + +#define kOpenHandlesLimit 128 +#define kMetadataWriteDelay 5000 +#define kRemoveTrashStartDelay 60000 // in milliseconds +#define kSmartSizeUpdateInterval 60000 // in milliseconds + +#ifdef ANDROID +const uint32_t kMaxCacheSizeKB = 512 * 1024; // 512 MB +#else +const uint32_t kMaxCacheSizeKB = 1024 * 1024; // 1 GB +#endif +const uint32_t kMaxClearOnShutdownCacheSizeKB = 150 * 1024; // 150 MB +const auto kPurgeExtension = ".purge.bg_rm"_ns; + +bool CacheFileHandle::DispatchRelease() { + if (CacheFileIOManager::IsOnIOThreadOrCeased()) { + return false; + } + + nsCOMPtr ioTarget = CacheFileIOManager::IOTarget(); + if (!ioTarget) { + return false; + } + + nsresult rv = ioTarget->Dispatch( + NewNonOwningRunnableMethod("net::CacheFileHandle::Release", this, + &CacheFileHandle::Release), + nsIEventTarget::DISPATCH_NORMAL); + return NS_SUCCEEDED(rv); +} + +NS_IMPL_ADDREF(CacheFileHandle) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileHandle::Release() { + nsrefcnt count = mRefCnt - 1; + if (DispatchRelease()) { + // Redispatched to the IO thread. + return count; + } + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + LOG(("CacheFileHandle::Release() [this=%p, refcnt=%" PRIuPTR "]", this, + mRefCnt.get())); + MOZ_ASSERT(0 != mRefCnt, "dup release"); + count = --mRefCnt; + NS_LOG_RELEASE(this, count, "CacheFileHandle"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(CacheFileHandle) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +CacheFileHandle::CacheFileHandle(const SHA1Sum::Hash* aHash, bool aPriority, + PinningStatus aPinning) + : mHash(aHash), + mIsDoomed(false), + mClosed(false), + mPriority(aPriority), + mSpecialFile(false), + mInvalid(false), + mFileExists(false), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false), + mKilled(false), + mPinning(aPinning), + mFileSize(-1), + mFD(nullptr) { + // If we initialize mDoomed in the initialization list, that initialization is + // not guaranteeded to be atomic. Whereas this assignment here is guaranteed + // to be atomic. TSan will see this (atomic) assignment and be satisfied + // that cross-thread accesses to mIsDoomed are properly synchronized. + mIsDoomed = false; + LOG(( + "CacheFileHandle::CacheFileHandle() [this=%p, hash=%08x%08x%08x%08x%08x]", + this, LOGSHA1(aHash))); +} + +CacheFileHandle::CacheFileHandle(const nsACString& aKey, bool aPriority, + PinningStatus aPinning) + : mHash(nullptr), + mIsDoomed(false), + mClosed(false), + mPriority(aPriority), + mSpecialFile(true), + mInvalid(false), + mFileExists(false), + mDoomWhenFoundPinned(false), + mDoomWhenFoundNonPinned(false), + mKilled(false), + mPinning(aPinning), + mFileSize(-1), + mFD(nullptr), + mKey(aKey) { + // See comment above about the initialization of mIsDoomed. + mIsDoomed = false; + LOG(("CacheFileHandle::CacheFileHandle() [this=%p, key=%s]", this, + PromiseFlatCString(aKey).get())); +} + +CacheFileHandle::~CacheFileHandle() { + LOG(("CacheFileHandle::~CacheFileHandle() [this=%p]", this)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + RefPtr ioMan = CacheFileIOManager::gInstance; + if (!IsClosed() && ioMan) { + ioMan->CloseHandleInternal(this); + } +} + +void CacheFileHandle::Log() { + nsAutoCString leafName; + if (mFile) { + mFile->GetNativeLeafName(leafName); + } + + if (mSpecialFile) { + LOG( + ("CacheFileHandle::Log() - special file [this=%p, " + "isDoomed=%d, priority=%d, closed=%d, invalid=%d, " + "pinning=%" PRIu32 ", fileExists=%d, fileSize=%" PRId64 + ", leafName=%s, key=%s]", + this, bool(mIsDoomed), bool(mPriority), bool(mClosed), bool(mInvalid), + static_cast(mPinning), bool(mFileExists), int64_t(mFileSize), + leafName.get(), mKey.get())); + } else { + LOG( + ("CacheFileHandle::Log() - entry file [this=%p, " + "hash=%08x%08x%08x%08x%08x, " + "isDoomed=%d, priority=%d, closed=%d, invalid=%d, " + "pinning=%" PRIu32 ", fileExists=%d, fileSize=%" PRId64 + ", leafName=%s, key=%s]", + this, LOGSHA1(mHash), bool(mIsDoomed), bool(mPriority), bool(mClosed), + bool(mInvalid), static_cast(mPinning), bool(mFileExists), + int64_t(mFileSize), leafName.get(), mKey.get())); + } +} + +uint32_t CacheFileHandle::FileSizeInK() const { + MOZ_ASSERT(mFileSize != -1); + uint64_t size64 = mFileSize; + + size64 += 0x3FF; + size64 >>= 10; + + uint32_t size; + if (size64 >> 32) { + NS_WARNING( + "CacheFileHandle::FileSizeInK() - FileSize is too large, " + "truncating to PR_UINT32_MAX"); + size = PR_UINT32_MAX; + } else { + size = static_cast(size64); + } + + return size; +} + +bool CacheFileHandle::SetPinned(bool aPinned) { + LOG(("CacheFileHandle::SetPinned [this=%p, pinned=%d]", this, aPinned)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + mPinning = aPinned ? PinningStatus::PINNED : PinningStatus::NON_PINNED; + + if ((MOZ_UNLIKELY(mDoomWhenFoundPinned) && aPinned) || + (MOZ_UNLIKELY(mDoomWhenFoundNonPinned) && !aPinned)) { + LOG((" dooming, when: pinned=%d, non-pinned=%d, found: pinned=%d", + bool(mDoomWhenFoundPinned), bool(mDoomWhenFoundNonPinned), aPinned)); + + mDoomWhenFoundPinned = false; + mDoomWhenFoundNonPinned = false; + + return false; + } + + return true; +} + +// Memory reporting + +size_t CacheFileHandle::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = 0; + nsCOMPtr sizeOf; + + sizeOf = do_QueryInterface(mFile); + if (sizeOf) { + n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + } + + n += mallocSizeOf(mFD); + n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); + return n; +} + +size_t CacheFileHandle::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +/****************************************************************************** + * CacheFileHandles::HandleHashKey + *****************************************************************************/ + +void CacheFileHandles::HandleHashKey::AddHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + mHandles.InsertElementAt(0, aHandle); +} + +void CacheFileHandles::HandleHashKey::RemoveHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + DebugOnly found{}; + found = mHandles.RemoveElement(aHandle); + MOZ_ASSERT(found); +} + +already_AddRefed +CacheFileHandles::HandleHashKey::GetNewestHandle() { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + RefPtr handle; + if (mHandles.Length()) { + handle = mHandles[0]; + } + + return handle.forget(); +} + +void CacheFileHandles::HandleHashKey::GetHandles( + nsTArray>& aResult) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + CacheFileHandle* handle = mHandles[i]; + aResult.AppendElement(handle); + } +} + +#ifdef DEBUG + +void CacheFileHandles::HandleHashKey::AssertHandlesState() { + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + CacheFileHandle* handle = mHandles[i]; + MOZ_ASSERT(handle->IsDoomed()); + } +} + +#endif + +size_t CacheFileHandles::HandleHashKey::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + size_t n = 0; + n += mallocSizeOf(mHash.get()); + for (uint32_t i = 0; i < mHandles.Length(); ++i) { + n += mHandles[i]->SizeOfIncludingThis(mallocSizeOf); + } + + return n; +} + +/****************************************************************************** + * CacheFileHandles + *****************************************************************************/ + +CacheFileHandles::CacheFileHandles() { + LOG(("CacheFileHandles::CacheFileHandles() [this=%p]", this)); + MOZ_COUNT_CTOR(CacheFileHandles); +} + +CacheFileHandles::~CacheFileHandles() { + LOG(("CacheFileHandles::~CacheFileHandles() [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileHandles); +} + +nsresult CacheFileHandles::GetHandle(const SHA1Sum::Hash* aHash, + CacheFileHandle** _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHash); + +#ifdef DEBUG_HANDLES + LOG(("CacheFileHandles::GetHandle() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.GetEntry(*aHash); + if (!entry) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "no handle entries found", + LOGSHA1(aHash))); + return NS_ERROR_NOT_AVAILABLE; + } + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + + // Check if the entry is doomed + RefPtr handle = entry->GetNewestHandle(); + if (!handle) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "no handle found %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (handle->IsDoomed()) { + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "found doomed handle %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("CacheFileHandles::GetHandle() hash=%08x%08x%08x%08x%08x " + "found handle %p, entry %p", + LOGSHA1(aHash), handle.get(), entry)); + + handle.forget(_retval); + return NS_OK; +} + +already_AddRefed CacheFileHandles::NewHandle( + const SHA1Sum::Hash* aHash, bool aPriority, + CacheFileHandle::PinningStatus aPinning) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHash); + +#ifdef DEBUG_HANDLES + LOG(("CacheFileHandles::NewHandle() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.PutEntry(*aHash); + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + +#ifdef DEBUG + entry->AssertHandlesState(); +#endif + + RefPtr handle = + new CacheFileHandle(entry->Hash(), aPriority, aPinning); + entry->AddHandle(handle); + + LOG( + ("CacheFileHandles::NewHandle() hash=%08x%08x%08x%08x%08x " + "created new handle %p, entry=%p", + LOGSHA1(aHash), handle.get(), entry)); + return handle.forget(); +} + +void CacheFileHandles::RemoveHandle(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHandle); + + if (!aHandle) { + return; + } + +#ifdef DEBUG_HANDLES + LOG(( + "CacheFileHandles::RemoveHandle() [handle=%p, hash=%08x%08x%08x%08x%08x]", + aHandle, LOGSHA1(aHandle->Hash()))); +#endif + + // find hash entry for key + HandleHashKey* entry = mTable.GetEntry(*aHandle->Hash()); + if (!entry) { + MOZ_ASSERT(CacheFileIOManager::IsShutdown(), + "Should find entry when removing a handle before shutdown"); + + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "no entries found", + LOGSHA1(aHandle->Hash()))); + return; + } + +#ifdef DEBUG_HANDLES + Log(entry); +#endif + + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "removing handle %p", + LOGSHA1(entry->Hash()), aHandle)); + entry->RemoveHandle(aHandle); + + if (entry->IsEmpty()) { + LOG( + ("CacheFileHandles::RemoveHandle() hash=%08x%08x%08x%08x%08x " + "list is empty, removing entry %p", + LOGSHA1(entry->Hash()), entry)); + mTable.RemoveEntry(entry); + } +} + +void CacheFileHandles::GetAllHandles( + nsTArray>* _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetHandles(*_retval); + } +} + +void CacheFileHandles::GetActiveHandles( + nsTArray>* _retval) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + RefPtr handle = iter.Get()->GetNewestHandle(); + MOZ_ASSERT(handle); + + if (!handle->IsDoomed()) { + _retval->AppendElement(handle); + } + } +} + +void CacheFileHandles::ClearAll() { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + mTable.Clear(); +} + +uint32_t CacheFileHandles::HandleCount() { return mTable.Count(); } + +#ifdef DEBUG_HANDLES +void CacheFileHandles::Log(CacheFileHandlesEntry* entry) { + LOG(("CacheFileHandles::Log() BEGIN [entry=%p]", entry)); + + nsTArray> array; + aEntry->GetHandles(array); + + for (uint32_t i = 0; i < array.Length(); ++i) { + CacheFileHandle* handle = array[i]; + handle->Log(); + } + + LOG(("CacheFileHandles::Log() END [entry=%p]", entry)); +} +#endif + +// Memory reporting + +size_t CacheFileHandles::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + return mTable.SizeOfExcludingThis(mallocSizeOf); +} + +// Events + +class ShutdownEvent : public Runnable, nsITimerCallback { + NS_DECL_ISUPPORTS_INHERITED + public: + ShutdownEvent() : Runnable("net::ShutdownEvent") {} + + protected: + ~ShutdownEvent() = default; + + public: + NS_IMETHOD Run() override { + CacheFileIOManager::gInstance->ShutdownInternal(); + + mNotified = true; + + NS_DispatchToMainThread( + NS_NewRunnableFunction("CacheFileIOManager::ShutdownEvent::Run", []() { + // This empty runnable is dispatched just in case the MT event loop + // becomes empty - we need to process a task to break out of + // SpinEventLoopUntil. + })); + + return NS_OK; + } + + NS_IMETHOD Notify(nsITimer* timer) override { + if (mNotified) { + return NS_OK; + } + + // If there is any IO blocking on the IO thread, this will + // try to cancel it. + CacheFileIOManager::gInstance->mIOThread->CancelBlockingIO(); + + // After this runs the first time, the browser_cache_max_shutdown_io_lag + // time has elapsed. The CacheIO thread may pick up more blocking IO tasks + // so we want to block those too if necessary. + mTimer->SetDelay( + StaticPrefs::browser_cache_shutdown_io_time_between_cancellations_ms()); + return NS_OK; + } + + void PostAndWait() { + nsresult rv = CacheFileIOManager::gInstance->mIOThread->Dispatch( + this, + CacheIOThread::WRITE); // When writes and closing of handles is done + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // If we failed to post the even there's no reason to go into the loop + // because mNotified will never be set to true. + if (NS_FAILED(rv)) { + NS_WARNING("Posting ShutdownEvent task failed"); + return; + } + + rv = NS_NewTimerWithCallback( + getter_AddRefs(mTimer), this, + StaticPrefs::browser_cache_max_shutdown_io_lag() * 1000, + nsITimer::TYPE_REPEATING_SLACK); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + mozilla::SpinEventLoopUntil("CacheFileIOManager::ShutdownEvent"_ns, + [&]() { return bool(mNotified); }); + + if (mTimer) { + mTimer->Cancel(); + mTimer = nullptr; + } + } + + protected: + Atomic mNotified{false}; + nsCOMPtr mTimer; +}; + +NS_IMPL_ISUPPORTS_INHERITED(ShutdownEvent, Runnable, nsITimerCallback) + +// Class responsible for reporting IO performance stats +class IOPerfReportEvent { + public: + explicit IOPerfReportEvent(CacheFileUtils::CachePerfStats::EDataType aType) + : mType(aType), mEventCounter(0) {} + + void Start(CacheIOThread* aIOThread) { + mStartTime = TimeStamp::Now(); + mEventCounter = aIOThread->EventCounter(); + } + + void Report(CacheIOThread* aIOThread) { + if (mStartTime.IsNull()) { + return; + } + + // Single IO operations can take less than 1ms. So we use microseconds to + // keep a good resolution of data. + uint32_t duration = (TimeStamp::Now() - mStartTime).ToMicroseconds(); + + // This is a simple prefiltering of values that might differ a lot from the + // average value. Do not add the value to the filtered stats when the event + // had to wait in a long queue. + uint32_t eventCounter = aIOThread->EventCounter(); + bool shortOnly = eventCounter - mEventCounter >= 5; + + CacheFileUtils::CachePerfStats::AddValue(mType, duration, shortOnly); + } + + protected: + CacheFileUtils::CachePerfStats::EDataType mType; + TimeStamp mStartTime; + uint32_t mEventCounter; +}; + +class OpenFileEvent : public Runnable, public IOPerfReportEvent { + public: + OpenFileEvent(const nsACString& aKey, uint32_t aFlags, + CacheFileIOListener* aCallback) + : Runnable("net::OpenFileEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_OPEN), + mFlags(aFlags), + mCallback(aCallback), + mKey(aKey) { + mIOMan = CacheFileIOManager::gInstance; + if (!(mFlags & CacheFileIOManager::SPECIAL_FILE)) { + Start(mIOMan->mIOThread); + } + } + + protected: + ~OpenFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv = NS_OK; + + if (!(mFlags & CacheFileIOManager::SPECIAL_FILE)) { + SHA1Sum sum; + sum.update(mKey.BeginReading(), mKey.Length()); + sum.finish(mHash); + } + + if (!mIOMan) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + if (mFlags & CacheFileIOManager::SPECIAL_FILE) { + rv = mIOMan->OpenSpecialFileInternal(mKey, mFlags, + getter_AddRefs(mHandle)); + } else { + rv = mIOMan->OpenFileInternal(&mHash, mKey, mFlags, + getter_AddRefs(mHandle)); + if (NS_SUCCEEDED(rv)) { + Report(mIOMan->mIOThread); + } + } + mIOMan = nullptr; + if (mHandle) { + if (mHandle->Key().IsEmpty()) { + mHandle->Key() = mKey; + } + } + } + + mCallback->OnFileOpened(mHandle, rv); + return NS_OK; + } + + protected: + SHA1Sum::Hash mHash{}; + uint32_t mFlags; + nsCOMPtr mCallback; + RefPtr mIOMan; + RefPtr mHandle; + nsCString mKey; +}; + +class ReadEvent : public Runnable, public IOPerfReportEvent { + public: + ReadEvent(CacheFileHandle* aHandle, int64_t aOffset, char* aBuf, + int32_t aCount, CacheFileIOListener* aCallback) + : Runnable("net::ReadEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_READ), + mHandle(aHandle), + mOffset(aOffset), + mBuf(aBuf), + mCount(aCount), + mCallback(aCallback) { + if (!mHandle->IsSpecialFile()) { + Start(CacheFileIOManager::gInstance->mIOThread); + } + } + + protected: + ~ReadEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->ReadInternal(mHandle, mOffset, mBuf, + mCount); + if (NS_SUCCEEDED(rv)) { + Report(CacheFileIOManager::gInstance->mIOThread); + } + } + + mCallback->OnDataRead(mHandle, mBuf, rv); + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mOffset; + char* mBuf; + int32_t mCount; + nsCOMPtr mCallback; +}; + +class WriteEvent : public Runnable, public IOPerfReportEvent { + public: + WriteEvent(CacheFileHandle* aHandle, int64_t aOffset, const char* aBuf, + int32_t aCount, bool aValidate, bool aTruncate, + CacheFileIOListener* aCallback) + : Runnable("net::WriteEvent"), + IOPerfReportEvent(CacheFileUtils::CachePerfStats::IO_WRITE), + mHandle(aHandle), + mOffset(aOffset), + mBuf(aBuf), + mCount(aCount), + mValidate(aValidate), + mTruncate(aTruncate), + mCallback(aCallback) { + if (!mHandle->IsSpecialFile()) { + Start(CacheFileIOManager::gInstance->mIOThread); + } + } + + protected: + ~WriteEvent() { + if (!mCallback && mBuf) { + free(const_cast(mBuf)); + } + } + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + // We usually get here only after the internal shutdown + // (i.e. mShuttingDown == true). Pretend write has succeeded + // to avoid any past-shutdown file dooming. + rv = (CacheObserver::IsPastShutdownIOLag() || + CacheFileIOManager::gInstance->mShuttingDown) + ? NS_OK + : NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->WriteInternal( + mHandle, mOffset, mBuf, mCount, mValidate, mTruncate); + if (NS_SUCCEEDED(rv)) { + Report(CacheFileIOManager::gInstance->mIOThread); + } + if (NS_FAILED(rv) && !mCallback) { + // No listener is going to handle the error, doom the file + CacheFileIOManager::gInstance->DoomFileInternal(mHandle); + } + } + if (mCallback) { + mCallback->OnDataWritten(mHandle, mBuf, rv); + } else { + free(const_cast(mBuf)); + mBuf = nullptr; + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mOffset; + const char* mBuf; + int32_t mCount; + bool mValidate : 1; + bool mTruncate : 1; + nsCOMPtr mCallback; +}; + +class DoomFileEvent : public Runnable { + public: + DoomFileEvent(CacheFileHandle* aHandle, CacheFileIOListener* aCallback) + : Runnable("net::DoomFileEvent"), + mCallback(aCallback), + mHandle(aHandle) {} + + protected: + ~DoomFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed()) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->DoomFileInternal(mHandle); + } + + if (mCallback) { + mCallback->OnFileDoomed(mHandle, rv); + } + + return NS_OK; + } + + protected: + nsCOMPtr mCallback; + nsCOMPtr mTarget; + RefPtr mHandle; +}; + +class DoomFileByKeyEvent : public Runnable { + public: + DoomFileByKeyEvent(const nsACString& aKey, CacheFileIOListener* aCallback) + : Runnable("net::DoomFileByKeyEvent"), mCallback(aCallback) { + SHA1Sum sum; + sum.update(aKey.BeginReading(), aKey.Length()); + sum.finish(mHash); + + mIOMan = CacheFileIOManager::gInstance; + } + + protected: + ~DoomFileByKeyEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (!mIOMan) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = mIOMan->DoomFileByKeyInternal(&mHash); + mIOMan = nullptr; + } + + if (mCallback) { + mCallback->OnFileDoomed(nullptr, rv); + } + + return NS_OK; + } + + protected: + SHA1Sum::Hash mHash{}; + nsCOMPtr mCallback; + RefPtr mIOMan; +}; + +class ReleaseNSPRHandleEvent : public Runnable { + public: + explicit ReleaseNSPRHandleEvent(CacheFileHandle* aHandle) + : Runnable("net::ReleaseNSPRHandleEvent"), mHandle(aHandle) {} + + protected: + ~ReleaseNSPRHandleEvent() = default; + + public: + NS_IMETHOD Run() override { + if (!mHandle->IsClosed()) { + CacheFileIOManager::gInstance->MaybeReleaseNSPRHandleInternal(mHandle); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; +}; + +class TruncateSeekSetEOFEvent : public Runnable { + public: + TruncateSeekSetEOFEvent(CacheFileHandle* aHandle, int64_t aTruncatePos, + int64_t aEOFPos, CacheFileIOListener* aCallback) + : Runnable("net::TruncateSeekSetEOFEvent"), + mHandle(aHandle), + mTruncatePos(aTruncatePos), + mEOFPos(aEOFPos), + mCallback(aCallback) {} + + protected: + ~TruncateSeekSetEOFEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed() || (mCallback && mCallback->IsKilled())) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->TruncateSeekSetEOFInternal( + mHandle, mTruncatePos, mEOFPos); + } + + if (mCallback) { + mCallback->OnEOFSet(mHandle, rv); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + int64_t mTruncatePos; + int64_t mEOFPos; + nsCOMPtr mCallback; +}; + +class RenameFileEvent : public Runnable { + public: + RenameFileEvent(CacheFileHandle* aHandle, const nsACString& aNewName, + CacheFileIOListener* aCallback) + : Runnable("net::RenameFileEvent"), + mHandle(aHandle), + mNewName(aNewName), + mCallback(aCallback) {} + + protected: + ~RenameFileEvent() = default; + + public: + NS_IMETHOD Run() override { + nsresult rv; + + if (mHandle->IsClosed()) { + rv = NS_ERROR_NOT_INITIALIZED; + } else { + rv = CacheFileIOManager::gInstance->RenameFileInternal(mHandle, mNewName); + } + + if (mCallback) { + mCallback->OnFileRenamed(mHandle, rv); + } + + return NS_OK; + } + + protected: + RefPtr mHandle; + nsCString mNewName; + nsCOMPtr mCallback; +}; + +class InitIndexEntryEvent : public Runnable { + public: + InitIndexEntryEvent(CacheFileHandle* aHandle, + OriginAttrsHash aOriginAttrsHash, bool aAnonymous, + bool aPinning) + : Runnable("net::InitIndexEntryEvent"), + mHandle(aHandle), + mOriginAttrsHash(aOriginAttrsHash), + mAnonymous(aAnonymous), + mPinning(aPinning) {} + + protected: + ~InitIndexEntryEvent() = default; + + public: + NS_IMETHOD Run() override { + if (mHandle->IsClosed() || mHandle->IsDoomed()) { + return NS_OK; + } + + CacheIndex::InitEntry(mHandle->Hash(), mOriginAttrsHash, mAnonymous, + mPinning); + + // We cannot set the filesize before we init the entry. If we're opening + // an existing entry file, frecency will be set after parsing the entry + // file, but we must set the filesize here since nobody is going to set it + // if there is no write to the file. + uint32_t sizeInK = mHandle->FileSizeInK(); + CacheIndex::UpdateEntry(mHandle->Hash(), nullptr, nullptr, nullptr, nullptr, + nullptr, &sizeInK); + + return NS_OK; + } + + protected: + RefPtr mHandle; + OriginAttrsHash mOriginAttrsHash; + bool mAnonymous; + bool mPinning; +}; + +class UpdateIndexEntryEvent : public Runnable { + public: + UpdateIndexEntryEvent(CacheFileHandle* aHandle, const uint32_t* aFrecency, + const bool* aHasAltData, const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType) + : Runnable("net::UpdateIndexEntryEvent"), + mHandle(aHandle), + mHasFrecency(false), + mHasHasAltData(false), + mHasOnStartTime(false), + mHasOnStopTime(false), + mHasContentType(false), + mFrecency(0), + mHasAltData(false), + mOnStartTime(0), + mOnStopTime(0), + mContentType(nsICacheEntry::CONTENT_TYPE_UNKNOWN) { + if (aFrecency) { + mHasFrecency = true; + mFrecency = *aFrecency; + } + if (aHasAltData) { + mHasHasAltData = true; + mHasAltData = *aHasAltData; + } + if (aOnStartTime) { + mHasOnStartTime = true; + mOnStartTime = *aOnStartTime; + } + if (aOnStopTime) { + mHasOnStopTime = true; + mOnStopTime = *aOnStopTime; + } + if (aContentType) { + mHasContentType = true; + mContentType = *aContentType; + } + } + + protected: + ~UpdateIndexEntryEvent() = default; + + public: + NS_IMETHOD Run() override { + if (mHandle->IsClosed() || mHandle->IsDoomed()) { + return NS_OK; + } + + CacheIndex::UpdateEntry(mHandle->Hash(), + mHasFrecency ? &mFrecency : nullptr, + mHasHasAltData ? &mHasAltData : nullptr, + mHasOnStartTime ? &mOnStartTime : nullptr, + mHasOnStopTime ? &mOnStopTime : nullptr, + mHasContentType ? &mContentType : nullptr, nullptr); + return NS_OK; + } + + protected: + RefPtr mHandle; + + bool mHasFrecency; + bool mHasHasAltData; + bool mHasOnStartTime; + bool mHasOnStopTime; + bool mHasContentType; + + uint32_t mFrecency; + bool mHasAltData; + uint16_t mOnStartTime; + uint16_t mOnStopTime; + uint8_t mContentType; +}; + +class MetadataWriteScheduleEvent : public Runnable { + public: + enum EMode { SCHEDULE, UNSCHEDULE, SHUTDOWN } mMode; + + RefPtr mFile; + RefPtr mIOMan; + + MetadataWriteScheduleEvent(CacheFileIOManager* aManager, CacheFile* aFile, + EMode aMode) + : Runnable("net::MetadataWriteScheduleEvent"), + mMode(aMode), + mFile(aFile), + mIOMan(aManager) {} + + virtual ~MetadataWriteScheduleEvent() = default; + + NS_IMETHOD Run() override { + RefPtr ioMan = CacheFileIOManager::gInstance; + if (!ioMan) { + NS_WARNING( + "CacheFileIOManager already gone in " + "MetadataWriteScheduleEvent::Run()"); + return NS_OK; + } + + switch (mMode) { + case SCHEDULE: + ioMan->ScheduleMetadataWriteInternal(mFile); + break; + case UNSCHEDULE: + ioMan->UnscheduleMetadataWriteInternal(mFile); + break; + case SHUTDOWN: + ioMan->ShutdownMetadataWriteSchedulingInternal(); + break; + } + return NS_OK; + } +}; + +StaticRefPtr CacheFileIOManager::gInstance; + +NS_IMPL_ISUPPORTS(CacheFileIOManager, nsITimerCallback, nsINamed) + +CacheFileIOManager::CacheFileIOManager() + +{ + LOG(("CacheFileIOManager::CacheFileIOManager [this=%p]", this)); + MOZ_ASSERT(!gInstance, "multiple CacheFileIOManager instances!"); +} + +CacheFileIOManager::~CacheFileIOManager() { + LOG(("CacheFileIOManager::~CacheFileIOManager [this=%p]", this)); +} + +// static +nsresult CacheFileIOManager::Init() { + LOG(("CacheFileIOManager::Init()")); + + MOZ_ASSERT(NS_IsMainThread()); + + if (gInstance) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + RefPtr ioMan = new CacheFileIOManager(); + + nsresult rv = ioMan->InitInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + gInstance = std::move(ioMan); + return NS_OK; +} + +nsresult CacheFileIOManager::InitInternal() { + nsresult rv; + + mIOThread = new CacheIOThread(); + + rv = mIOThread->Init(); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Can't create background thread"); + NS_ENSURE_SUCCESS(rv, rv); + + mStartTime = TimeStamp::NowLoRes(); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::Shutdown() { + LOG(("CacheFileIOManager::Shutdown() [gInstance=%p]", gInstance.get())); + + MOZ_ASSERT(NS_IsMainThread()); + + if (!gInstance) { + return NS_ERROR_NOT_INITIALIZED; + } + + Telemetry::AutoTimer shutdownTimer; + + CacheIndex::PreShutdown(); + + ShutdownMetadataWriteScheduling(); + + RefPtr ev = new ShutdownEvent(); + ev->PostAndWait(); + + MOZ_ASSERT(gInstance->mHandles.HandleCount() == 0); + MOZ_ASSERT(gInstance->mHandlesByLastUsed.Length() == 0); + + if (gInstance->mIOThread) { + gInstance->mIOThread->Shutdown(); + } + + CacheIndex::Shutdown(); + + if (CacheObserver::ClearCacheOnShutdown()) { + Telemetry::AutoTimer + totalTimer; + gInstance->SyncRemoveAllCacheFiles(); + } + + gInstance = nullptr; + + return NS_OK; +} + +void CacheFileIOManager::ShutdownInternal() { + LOG(("CacheFileIOManager::ShutdownInternal() [this=%p]", this)); + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + // No new handles can be created after this flag is set + mShuttingDown = true; + + if (mTrashTimer) { + mTrashTimer->Cancel(); + mTrashTimer = nullptr; + } + + // close all handles and delete all associated files + nsTArray> handles; + mHandles.GetAllHandles(&handles); + handles.AppendElements(mSpecialHandles); + + for (uint32_t i = 0; i < handles.Length(); i++) { + CacheFileHandle* h = handles[i]; + h->mClosed = true; + + h->Log(); + + // Close completely written files. + MaybeReleaseNSPRHandleInternal(h); + // Don't bother removing invalid and/or doomed files to improve + // shutdown perfomrance. + // Doomed files are already in the doomed directory from which + // we never reuse files and delete the dir on next session startup. + // Invalid files don't have metadata and thus won't load anyway + // (hashes won't match). + + if (!h->IsSpecialFile() && !h->mIsDoomed && !h->mFileExists) { + CacheIndex::RemoveEntry(h->Hash()); + } + + // Remove the handle from mHandles/mSpecialHandles + if (h->IsSpecialFile()) { + mSpecialHandles.RemoveElement(h); + } else { + mHandles.RemoveHandle(h); + } + + // Pointer to the hash is no longer valid once the last handle with the + // given hash is released. Null out the pointer so that we crash if there + // is a bug in this code and we dereference the pointer after this point. + if (!h->IsSpecialFile()) { + h->mHash = nullptr; + } + } + + // Assert the table is empty. When we are here, no new handles can be added + // and handles will no longer remove them self from this table and we don't + // want to keep invalid handles here. Also, there is no lookup after this + // point to happen. + MOZ_ASSERT(mHandles.HandleCount() == 0); + + // Release trash directory enumerator + if (mTrashDirEnumerator) { + mTrashDirEnumerator->Close(); + mTrashDirEnumerator = nullptr; + } + + if (mContextEvictor) { + mContextEvictor->Shutdown(); + mContextEvictor = nullptr; + } +} + +// static +nsresult CacheFileIOManager::OnProfile() { + LOG(("CacheFileIOManager::OnProfile() [gInstance=%p]", gInstance.get())); + + RefPtr ioMan = gInstance; + if (!ioMan) { + // CacheFileIOManager::Init() failed, probably could not create the IO + // thread, just go with it... + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + + nsCOMPtr directory; + + CacheObserver::ParentDirOverride(getter_AddRefs(directory)); + +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr profilelessDirectory; + char* cachePath = getenv("CACHE_DIRECTORY"); + if (!directory && cachePath && *cachePath) { + rv = NS_NewNativeLocalFile(nsDependentCString(cachePath), true, + getter_AddRefs(directory)); + if (NS_SUCCEEDED(rv)) { + // Save this directory as the profileless path. + rv = directory->Clone(getter_AddRefs(profilelessDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + // Add profile leaf name to the directory name to distinguish + // multiple profiles Fennec supports. + nsCOMPtr profD; + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profD)); + + nsAutoCString leafName; + if (NS_SUCCEEDED(rv)) { + rv = profD->GetNativeLeafName(leafName); + } + if (NS_SUCCEEDED(rv)) { + rv = directory->AppendNative(leafName); + } + if (NS_FAILED(rv)) { + directory = nullptr; + } + } + } +#endif + + if (!directory) { + rv = NS_GetSpecialDirectory(NS_APP_CACHE_PARENT_DIR, + getter_AddRefs(directory)); + } + + if (!directory) { + rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_LOCAL_50_DIR, + getter_AddRefs(directory)); + } + + if (directory) { + rv = directory->Append(u"cache2"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // All functions return a clone. + ioMan->mCacheDirectory.swap(directory); + +#if defined(MOZ_WIDGET_ANDROID) + if (profilelessDirectory) { + rv = profilelessDirectory->Append(u"cache2"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + ioMan->mCacheProfilelessDirectory.swap(profilelessDirectory); +#endif + + if (ioMan->mCacheDirectory) { + CacheIndex::Init(ioMan->mCacheDirectory); + } + + return NS_OK; +} + +static bool inBackgroundTask() { + MOZ_ASSERT(NS_IsMainThread(), "backgroundtasks are main thread only"); +#if defined(MOZ_BACKGROUNDTASKS) + nsCOMPtr backgroundTaskService = + do_GetService("@mozilla.org/backgroundtasks;1"); + if (!backgroundTaskService) { + return false; + } + bool isBackgroundTask = false; + backgroundTaskService->GetIsBackgroundTaskMode(&isBackgroundTask); + return isBackgroundTask; +#else + return false; +#endif +} + +// static +nsresult CacheFileIOManager::OnDelayedStartupFinished() { + // If we don't clear the cache at shutdown, or we don't use a + // background task then there's no need to dispatch a cleanup task + // at startup + if (!CacheObserver::ClearCacheOnShutdown()) { + return NS_OK; + } + if (!StaticPrefs::network_cache_shutdown_purge_in_background_task()) { + return NS_OK; + } + + // If this is a background task already, there's no need to + // dispatch another one. + if (inBackgroundTask()) { + return NS_OK; + } + + RefPtr ioMan = gInstance; + nsCOMPtr target = IOTarget(); + if (NS_WARN_IF(!ioMan || !target)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return target->Dispatch( + NS_NewRunnableFunction("CacheFileIOManager::OnDelayedStartupFinished", + [ioMan = std::move(ioMan)] { + ioMan->DispatchPurgeTask(""_ns, "0"_ns, + kPurgeExtension); + }), + nsIEventTarget::DISPATCH_NORMAL); +} + +// static +already_AddRefed CacheFileIOManager::IOTarget() { + nsCOMPtr target; + if (gInstance && gInstance->mIOThread) { + target = gInstance->mIOThread->Target(); + } + + return target.forget(); +} + +// static +already_AddRefed CacheFileIOManager::IOThread() { + RefPtr thread; + if (gInstance) { + thread = gInstance->mIOThread; + } + + return thread.forget(); +} + +// static +bool CacheFileIOManager::IsOnIOThread() { + RefPtr ioMan = gInstance; + if (ioMan && ioMan->mIOThread) { + return ioMan->mIOThread->IsCurrentThread(); + } + + return false; +} + +// static +bool CacheFileIOManager::IsOnIOThreadOrCeased() { + RefPtr ioMan = gInstance; + if (ioMan && ioMan->mIOThread) { + return ioMan->mIOThread->IsCurrentThread(); + } + + // Ceased... + return true; +} + +// static +bool CacheFileIOManager::IsShutdown() { + if (!gInstance) { + return true; + } + return gInstance->mShuttingDown; +} + +// static +nsresult CacheFileIOManager::ScheduleMetadataWrite(CacheFile* aFile) { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::SCHEDULE); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +nsresult CacheFileIOManager::ScheduleMetadataWriteInternal(CacheFile* aFile) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + nsresult rv; + + if (!mMetadataWritesTimer) { + rv = NS_NewTimerWithCallback(getter_AddRefs(mMetadataWritesTimer), this, + kMetadataWriteDelay, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mScheduledMetadataWrites.IndexOf(aFile) != + nsTArray>::NoIndex) { + return NS_OK; + } + + mScheduledMetadataWrites.AppendElement(aFile); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::UnscheduleMetadataWrite(CacheFile* aFile) { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::UNSCHEDULE); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +void CacheFileIOManager::UnscheduleMetadataWriteInternal(CacheFile* aFile) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + mScheduledMetadataWrites.RemoveElement(aFile); + + if (mScheduledMetadataWrites.Length() == 0 && mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } +} + +// static +nsresult CacheFileIOManager::ShutdownMetadataWriteScheduling() { + RefPtr ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + RefPtr event = new MetadataWriteScheduleEvent( + ioMan, nullptr, MetadataWriteScheduleEvent::SHUTDOWN); + nsCOMPtr target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +void CacheFileIOManager::ShutdownMetadataWriteSchedulingInternal() { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + nsTArray> files = std::move(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile* file = files[i]; + file->WriteMetadataIfNeeded(); + } + + if (mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } +} + +NS_IMETHODIMP +CacheFileIOManager::Notify(nsITimer* aTimer) { + MOZ_ASSERT(IsOnIOThreadOrCeased()); + MOZ_ASSERT(mMetadataWritesTimer == aTimer); + + mMetadataWritesTimer = nullptr; + + nsTArray> files = std::move(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile* file = files[i]; + file->WriteMetadataIfNeeded(); + } + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileIOManager::GetName(nsACString& aName) { + aName.AssignLiteral("CacheFileIOManager"); + return NS_OK; +} + +// static +nsresult CacheFileIOManager::OpenFile(const nsACString& aKey, uint32_t aFlags, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::OpenFile() [key=%s, flags=%d, listener=%p]", + PromiseFlatCString(aKey).get(), aFlags, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool priority = aFlags & CacheFileIOManager::PRIORITY; + RefPtr ev = new OpenFileEvent(aKey, aFlags, aCallback); + rv = ioMan->mIOThread->Dispatch( + ev, priority ? CacheIOThread::OPEN_PRIORITY : CacheIOThread::OPEN); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::OpenFileInternal(const SHA1Sum::Hash* aHash, + const nsACString& aKey, + uint32_t aFlags, + CacheFileHandle** _retval) { + LOG( + ("CacheFileIOManager::OpenFileInternal() [hash=%08x%08x%08x%08x%08x, " + "key=%s, flags=%d]", + LOGSHA1(aHash), PromiseFlatCString(aKey).get(), aFlags)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + CacheIOThread::Cancelable cancelable( + true /* never called for special handles */); + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) return rv; + } + + CacheFileHandle::PinningStatus pinning = + aFlags & PINNED ? CacheFileHandle::PinningStatus::PINNED + : CacheFileHandle::PinningStatus::NON_PINNED; + + nsCOMPtr file; + rv = GetFile(aHash, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr handle; + mHandles.GetHandle(aHash, getter_AddRefs(handle)); + + if ((aFlags & (OPEN | CREATE | CREATE_NEW)) == CREATE_NEW) { + if (handle) { + rv = DoomFileInternal(handle); + NS_ENSURE_SUCCESS(rv, rv); + handle = nullptr; + } + + handle = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + CacheIndex::RemoveEntry(aHash); + + LOG( + ("CacheFileIOManager::OpenFileInternal() - Removing old file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::OpenFileInternal() - Removing old file failed" + ". [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + CacheIndex::AddEntry(aHash); + handle->mFile.swap(file); + handle->mFileSize = 0; + } + + if (handle) { + handle.swap(*_retval); + return NS_OK; + } + + bool exists, evictedAsPinned = false, evictedAsNonPinned = false; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists && mContextEvictor) { + if (mContextEvictor->ContextsCount() == 0) { + mContextEvictor = nullptr; + } else { + mContextEvictor->WasEvicted(aKey, file, &evictedAsPinned, + &evictedAsNonPinned); + } + } + + if (!exists && (aFlags & (OPEN | CREATE | CREATE_NEW)) == OPEN) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (exists) { + // For existing files we determine the pinning status later, after the + // metadata gets parsed. + pinning = CacheFileHandle::PinningStatus::UNKNOWN; + } + + handle = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning); + if (exists) { + // If this file has been found evicted through the context file evictor + // above for any of pinned or non-pinned state, these calls ensure we doom + // the handle ASAP we know the real pinning state after metadta has been + // parsed. DoomFileInternal on the |handle| doesn't doom right now, since + // the pinning state is unknown and we pass down a pinning restriction. + if (evictedAsPinned) { + rv = DoomFileInternal(handle, DOOM_WHEN_PINNED); + MOZ_ASSERT(!handle->IsDoomed() && NS_SUCCEEDED(rv)); + } + if (evictedAsNonPinned) { + rv = DoomFileInternal(handle, DOOM_WHEN_NON_PINNED); + MOZ_ASSERT(!handle->IsDoomed() && NS_SUCCEEDED(rv)); + } + + int64_t fileSize = -1; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + handle->mFileSize = fileSize; + handle->mFileExists = true; + + CacheIndex::EnsureEntryExists(aHash); + } else { + handle->mFileSize = 0; + + CacheIndex::AddEntry(aHash); + } + + handle->mFile.swap(file); + handle.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::OpenSpecialFileInternal( + const nsACString& aKey, uint32_t aFlags, CacheFileHandle** _retval) { + LOG(("CacheFileIOManager::OpenSpecialFileInternal() [key=%s, flags=%d]", + PromiseFlatCString(aKey).get(), aFlags)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) return rv; + } + + nsCOMPtr file; + rv = GetSpecialFile(aKey, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr handle; + for (uint32_t i = 0; i < mSpecialHandles.Length(); i++) { + if (!mSpecialHandles[i]->IsDoomed() && mSpecialHandles[i]->Key() == aKey) { + handle = mSpecialHandles[i]; + break; + } + } + + if ((aFlags & (OPEN | CREATE | CREATE_NEW)) == CREATE_NEW) { + if (handle) { + rv = DoomFileInternal(handle); + NS_ENSURE_SUCCESS(rv, rv); + handle = nullptr; + } + + handle = new CacheFileHandle(aKey, aFlags & PRIORITY, + CacheFileHandle::PinningStatus::NON_PINNED); + mSpecialHandles.AppendElement(handle); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + LOG( + ("CacheFileIOManager::OpenSpecialFileInternal() - Removing file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::OpenSpecialFileInternal() - Removing file " + "failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + handle->mFile.swap(file); + handle->mFileSize = 0; + } + + if (handle) { + handle.swap(*_retval); + return NS_OK; + } + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists && (aFlags & (OPEN | CREATE | CREATE_NEW)) == OPEN) { + return NS_ERROR_NOT_AVAILABLE; + } + + handle = new CacheFileHandle(aKey, aFlags & PRIORITY, + CacheFileHandle::PinningStatus::NON_PINNED); + mSpecialHandles.AppendElement(handle); + + if (exists) { + int64_t fileSize = -1; + rv = file->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + handle->mFileSize = fileSize; + handle->mFileExists = true; + } else { + handle->mFileSize = 0; + } + + handle->mFile.swap(file); + handle.swap(*_retval); + return NS_OK; +} + +void CacheFileIOManager::CloseHandleInternal(CacheFileHandle* aHandle) { + nsresult rv; + LOG(("CacheFileIOManager::CloseHandleInternal() [handle=%p]", aHandle)); + + MOZ_ASSERT(!aHandle->IsClosed()); + + aHandle->Log(); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + // Maybe close file handle (can be legally bypassed after shutdown) + rv = MaybeReleaseNSPRHandleInternal(aHandle); + + // Delete the file if the entry was doomed or invalid and + // filedesc properly closed + if ((aHandle->mIsDoomed || aHandle->mInvalid) && aHandle->mFileExists && + NS_SUCCEEDED(rv)) { + LOG( + ("CacheFileIOManager::CloseHandleInternal() - Removing file from " + "disk")); + + rv = aHandle->mFile->Remove(false); + if (NS_SUCCEEDED(rv)) { + aHandle->mFileExists = false; + } else { + LOG((" failed to remove the file [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + if (!aHandle->IsSpecialFile() && !aHandle->mIsDoomed && + (aHandle->mInvalid || !aHandle->mFileExists)) { + CacheIndex::RemoveEntry(aHandle->Hash()); + } + + // Don't remove handles after shutdown + if (!mShuttingDown) { + if (aHandle->IsSpecialFile()) { + mSpecialHandles.RemoveElement(aHandle); + } else { + mHandles.RemoveHandle(aHandle); + } + } +} + +// static +nsresult CacheFileIOManager::Read(CacheFileHandle* aHandle, int64_t aOffset, + char* aBuf, int32_t aCount, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::Read() [handle=%p, offset=%" PRId64 ", count=%d, " + "listener=%p]", + aHandle, aOffset, aCount, aCallback)); + + if (CacheObserver::ShuttingDown()) { + LOG((" no reads after shutdown")); + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = + new ReadEvent(aHandle, aOffset, aBuf, aCount, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->IsPriority() + ? CacheIOThread::READ_PRIORITY + : CacheIOThread::READ); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::ReadInternal(CacheFileHandle* aHandle, + int64_t aOffset, char* aBuf, + int32_t aCount) { + LOG(("CacheFileIOManager::ReadInternal() [handle=%p, offset=%" PRId64 + ", count=%d]", + aHandle, aOffset, aCount)); + + nsresult rv; + + if (CacheObserver::ShuttingDown()) { + LOG((" no reads after shutdown")); + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aHandle->mFileExists) { + NS_WARNING("Trying to read from non-existent file"); + return NS_ERROR_NOT_AVAILABLE; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + NS_WARNING("Trying to read from non-existent file"); + return NS_ERROR_NOT_AVAILABLE; + } + + int64_t offset = PR_Seek64(aHandle->mFD, aOffset, PR_SEEK_SET); + if (offset == -1) { + return NS_ERROR_FAILURE; + } + + int32_t bytesRead = PR_Read(aHandle->mFD, aBuf, aCount); + if (bytesRead != aCount) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::Write(CacheFileHandle* aHandle, int64_t aOffset, + const char* aBuf, int32_t aCount, + bool aValidate, bool aTruncate, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::Write() [handle=%p, offset=%" PRId64 ", count=%d, " + "validate=%d, truncate=%d, listener=%p]", + aHandle, aOffset, aCount, aValidate, aTruncate, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || (aCallback && aCallback->IsKilled()) || !ioMan) { + if (!aCallback) { + // When no callback is provided, CacheFileIOManager is responsible for + // releasing the buffer. We must release it even in case of failure. + free(const_cast(aBuf)); + } + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new WriteEvent(aHandle, aOffset, aBuf, aCount, + aValidate, aTruncate, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static nsresult TruncFile(PRFileDesc* aFD, int64_t aEOF) { +#if defined(XP_UNIX) + if (ftruncate(PR_FileDesc2NativeHandle(aFD), aEOF) != 0) { + NS_ERROR("ftruncate failed"); + return NS_ERROR_FAILURE; + } +#elif defined(XP_WIN) + int64_t cnt = PR_Seek64(aFD, aEOF, PR_SEEK_SET); + if (cnt == -1) { + return NS_ERROR_FAILURE; + } + if (!SetEndOfFile((HANDLE)PR_FileDesc2NativeHandle(aFD))) { + NS_ERROR("SetEndOfFile failed"); + return NS_ERROR_FAILURE; + } +#else + MOZ_ASSERT(false, "Not implemented!"); + return NS_ERROR_NOT_IMPLEMENTED; +#endif + + return NS_OK; +} + +nsresult CacheFileIOManager::WriteInternal(CacheFileHandle* aHandle, + int64_t aOffset, const char* aBuf, + int32_t aCount, bool aValidate, + bool aTruncate) { + LOG(("CacheFileIOManager::WriteInternal() [handle=%p, offset=%" PRId64 + ", count=%d, " + "validate=%d, truncate=%d]", + aHandle, aOffset, aCount, aValidate, aTruncate)); + + nsresult rv; + + if (aHandle->mKilled) { + LOG((" handle already killed, nothing written")); + return NS_OK; + } + + if (CacheObserver::ShuttingDown() && (!aValidate || !aHandle->mFD)) { + aHandle->mKilled = true; + LOG((" killing the handle, nothing written")); + return NS_OK; + } + + if (CacheObserver::IsPastShutdownIOLag()) { + LOG((" past the shutdown I/O lag, nothing written")); + // Pretend the write has succeeded, otherwise upper layers will doom + // the file and we end up with I/O anyway. + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFileExists) { + rv = CreateFile(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + return NS_ERROR_NOT_AVAILABLE; + } + + // When this operation would increase cache size, check whether the cache size + // reached the hard limit and whether it would cause critical low disk space. + if (aHandle->mFileSize < aOffset + aCount) { + if (mOverLimitEvicting && mCacheSizeOnHardLimit) { + LOG( + ("CacheFileIOManager::WriteInternal() - failing because cache size " + "reached hard limit!")); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + LOG( + ("CacheFileIOManager::WriteInternal() - GetDiskSpaceAvailable() " + "failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aOffset - aCount + aHandle->mFileSize < limit) { + LOG( + ("CacheFileIOManager::WriteInternal() - Low free space, refusing " + "to write! [freeSpace=%" PRId64 "kB, limit=%ukB]", + freeSpace, limit)); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + } + + // Write invalidates the entry by default + aHandle->mInvalid = true; + + int64_t offset = PR_Seek64(aHandle->mFD, aOffset, PR_SEEK_SET); + if (offset == -1) { + return NS_ERROR_FAILURE; + } + + int32_t bytesWritten = PR_Write(aHandle->mFD, aBuf, aCount); + + if (bytesWritten != -1) { + uint32_t oldSizeInK = aHandle->FileSizeInK(); + int64_t writeEnd = aOffset + bytesWritten; + + if (aTruncate) { + rv = TruncFile(aHandle->mFD, writeEnd); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileSize = writeEnd; + } else { + if (aHandle->mFileSize < writeEnd) { + aHandle->mFileSize = writeEnd; + } + } + + uint32_t newSizeInK = aHandle->FileSizeInK(); + + if (oldSizeInK != newSizeInK && !aHandle->IsDoomed() && + !aHandle->IsSpecialFile()) { + CacheIndex::UpdateEntry(aHandle->Hash(), nullptr, nullptr, nullptr, + nullptr, nullptr, &newSizeInK); + + if (oldSizeInK < newSizeInK) { + EvictIfOverLimitInternal(); + } + } + + CacheIndex::UpdateTotalBytesWritten(bytesWritten); + } + + if (bytesWritten != aCount) { + return NS_ERROR_FAILURE; + } + + // Write was successful and this write validates the entry (i.e. metadata) + if (aValidate) { + aHandle->mInvalid = false; + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::DoomFile(CacheFileHandle* aHandle, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::DoomFile() [handle=%p, listener=%p]", aHandle, + aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new DoomFileEvent(aHandle, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->IsPriority() + ? CacheIOThread::OPEN_PRIORITY + : CacheIOThread::OPEN); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::DoomFileInternal( + CacheFileHandle* aHandle, PinningDoomRestriction aPinningDoomRestriction) { + LOG(("CacheFileIOManager::DoomFileInternal() [handle=%p]", aHandle)); + aHandle->Log(); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + nsresult rv; + + if (aHandle->IsDoomed()) { + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (aPinningDoomRestriction > NO_RESTRICTION) { + switch (aHandle->mPinning) { + case CacheFileHandle::PinningStatus::NON_PINNED: + if (MOZ_LIKELY(aPinningDoomRestriction != DOOM_WHEN_NON_PINNED)) { + LOG((" not dooming, it's a non-pinned handle")); + return NS_OK; + } + // Doom now + break; + + case CacheFileHandle::PinningStatus::PINNED: + if (MOZ_UNLIKELY(aPinningDoomRestriction != DOOM_WHEN_PINNED)) { + LOG((" not dooming, it's a pinned handle")); + return NS_OK; + } + // Doom now + break; + + case CacheFileHandle::PinningStatus::UNKNOWN: + if (MOZ_LIKELY(aPinningDoomRestriction == DOOM_WHEN_NON_PINNED)) { + LOG((" doom when non-pinned set")); + aHandle->mDoomWhenFoundNonPinned = true; + } else if (MOZ_UNLIKELY(aPinningDoomRestriction == DOOM_WHEN_PINNED)) { + LOG((" doom when pinned set")); + aHandle->mDoomWhenFoundPinned = true; + } + + LOG((" pinning status not known, deferring doom decision")); + return NS_OK; + } + } + + if (aHandle->mFileExists) { + // we need to move the current file to the doomed directory + rv = MaybeReleaseNSPRHandleInternal(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + // find unused filename + nsCOMPtr file; + rv = GetDoomedFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr parentDir; + rv = file->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString leafName; + rv = file->GetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aHandle->mFile->MoveToNative(parentDir, leafName); + if (NS_ERROR_FILE_NOT_FOUND == rv) { + LOG((" file already removed under our hands")); + aHandle->mFileExists = false; + rv = NS_OK; + } else { + NS_ENSURE_SUCCESS(rv, rv); + aHandle->mFile.swap(file); + } + } + + if (!aHandle->IsSpecialFile()) { + CacheIndex::RemoveEntry(aHandle->Hash()); + } + + aHandle->mIsDoomed = true; + + if (!aHandle->IsSpecialFile()) { + RefPtr storageService = CacheStorageService::Self(); + if (storageService) { + nsAutoCString idExtension, url; + nsCOMPtr info = + CacheFileUtils::ParseKey(aHandle->Key(), &idExtension, &url); + MOZ_ASSERT(info); + if (info) { + storageService->CacheFileDoomed(info, idExtension, url); + } + } + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::DoomFileByKey(const nsACString& aKey, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::DoomFileByKey() [key=%s, listener=%p]", + PromiseFlatCString(aKey).get(), aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new DoomFileByKeyEvent(aKey, aCallback); + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::DoomFileByKeyInternal(const SHA1Sum::Hash* aHash) { + LOG(( + "CacheFileIOManager::DoomFileByKeyInternal() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + nsresult rv; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mCacheDirectory) { + return NS_ERROR_FILE_INVALID_PATH; + } + + // Find active handle + RefPtr handle; + mHandles.GetHandle(aHash, getter_AddRefs(handle)); + + if (handle) { + handle->Log(); + + return DoomFileInternal(handle); + } + + CacheIOThread::Cancelable cancelable(true); + + // There is no handle for this file, delete the file if exists + nsCOMPtr file; + rv = GetFile(aHash, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + return NS_ERROR_NOT_AVAILABLE; + } + + LOG( + ("CacheFileIOManager::DoomFileByKeyInternal() - Removing file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove old entry from the disk"); + LOG( + ("CacheFileIOManager::DoomFileByKeyInternal() - Removing file failed. " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + + CacheIndex::RemoveEntry(aHash); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::ReleaseNSPRHandle(CacheFileHandle* aHandle) { + LOG(("CacheFileIOManager::ReleaseNSPRHandle() [handle=%p]", aHandle)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = new ReleaseNSPRHandleEvent(aHandle); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::MaybeReleaseNSPRHandleInternal( + CacheFileHandle* aHandle, bool aIgnoreShutdownLag) { + LOG( + ("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() [handle=%p, " + "ignore shutdown=%d]", + aHandle, aIgnoreShutdownLag)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + if (aHandle->mFD) { + DebugOnly found{}; + found = mHandlesByLastUsed.RemoveElement(aHandle); + MOZ_ASSERT(found); + } + + PRFileDesc* fd = aHandle->mFD; + aHandle->mFD = nullptr; + + // Leak invalid (w/o metadata) and doomed handles immediately after shutdown. + // Leak other handles when past the shutdown time maximum lag. + if ( +#ifndef DEBUG + ((aHandle->mInvalid || aHandle->mIsDoomed) && + MOZ_UNLIKELY(CacheObserver::ShuttingDown())) || +#endif + MOZ_UNLIKELY(!aIgnoreShutdownLag && + CacheObserver::IsPastShutdownIOLag())) { + // Don't bother closing this file. Return a failure code from here will + // cause any following IO operation on the file (mainly removal) to be + // bypassed, which is what we want. + // For mInvalid == true the entry will never be used, since it doesn't + // have correct metadata, thus we don't need to worry about removing it. + // For mIsDoomed == true the file is already in the doomed sub-dir and + // will be removed on next session start. + LOG((" past the shutdown I/O lag, leaking file handle")); + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + if (!fd) { + // The filedesc has already been closed before, just let go. + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + PRStatus status = PR_Close(fd); + if (status != PR_SUCCESS) { + LOG( + ("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() " + "failed to close [handle=%p, status=%u]", + aHandle, status)); + return NS_ERROR_FAILURE; + } + + LOG(("CacheFileIOManager::MaybeReleaseNSPRHandleInternal() DONE")); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::TruncateSeekSetEOF( + CacheFileHandle* aHandle, int64_t aTruncatePos, int64_t aEOFPos, + CacheFileIOListener* aCallback) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOF() [handle=%p, " + "truncatePos=%" PRId64 ", " + "EOFPos=%" PRId64 ", listener=%p]", + aHandle, aTruncatePos, aEOFPos, aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || (aCallback && aCallback->IsKilled()) || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr ev = + new TruncateSeekSetEOFEvent(aHandle, aTruncatePos, aEOFPos, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +void CacheFileIOManager::GetCacheDirectory(nsIFile** result) { + *result = nullptr; + + RefPtr ioMan = gInstance; + if (!ioMan || !ioMan->mCacheDirectory) { + return; + } + + ioMan->mCacheDirectory->Clone(result); +} + +#if defined(MOZ_WIDGET_ANDROID) + +// static +void CacheFileIOManager::GetProfilelessCacheDirectory(nsIFile** result) { + *result = nullptr; + + RefPtr ioMan = gInstance; + if (!ioMan || !ioMan->mCacheProfilelessDirectory) { + return; + } + + ioMan->mCacheProfilelessDirectory->Clone(result); +} + +#endif + +// static +nsresult CacheFileIOManager::GetEntryInfo( + const SHA1Sum::Hash* aHash, + CacheStorageService::EntryInfoCallback* aCallback) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + RefPtr ioMan = gInstance; + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsAutoCString enhanceId; + nsAutoCString uriSpec; + + RefPtr handle; + ioMan->mHandles.GetHandle(aHash, getter_AddRefs(handle)); + if (handle) { + RefPtr info = + CacheFileUtils::ParseKey(handle->Key(), &enhanceId, &uriSpec); + + MOZ_ASSERT(info); + if (!info) { + return NS_OK; // ignore + } + + RefPtr service = CacheStorageService::Self(); + if (!service) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Invokes OnCacheEntryInfo when an existing entry is found + if (service->GetCacheEntryInfo(info, enhanceId, uriSpec, aCallback)) { + return NS_OK; + } + + // When we are here, there is no existing entry and we need + // to synchrnously load metadata from a disk file. + } + + // Locate the actual file + nsCOMPtr file; + ioMan->GetFile(aHash, getter_AddRefs(file)); + + // Read metadata from the file synchronously + RefPtr metadata = new CacheFileMetadata(); + rv = metadata->SyncReadMetadata(file); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // Now get the context + enhance id + URL from the key. + RefPtr info = + CacheFileUtils::ParseKey(metadata->GetKey(), &enhanceId, &uriSpec); + MOZ_ASSERT(info); + if (!info) { + return NS_OK; + } + + // Pick all data to pass to the callback. + int64_t dataSize = metadata->Offset(); + int64_t altDataSize = 0; + uint32_t fetchCount = metadata->GetFetchCount(); + uint32_t expirationTime = metadata->GetExpirationTime(); + uint32_t lastModified = metadata->GetLastModified(); + + const char* altDataElement = + metadata->GetElement(CacheFileUtils::kAltDataKey); + if (altDataElement) { + int64_t altDataOffset = std::numeric_limits::max(); + if (NS_SUCCEEDED(CacheFileUtils::ParseAlternativeDataInfo( + altDataElement, &altDataOffset, nullptr)) && + altDataOffset < dataSize) { + dataSize = altDataOffset; + altDataSize = metadata->Offset() - altDataOffset; + } else { + LOG(("CacheFileIOManager::GetEntryInfo() invalid alternative data info")); + return NS_OK; + } + } + + // Call directly on the callback. + aCallback->OnEntryInfo(uriSpec, enhanceId, dataSize, altDataSize, fetchCount, + lastModified, expirationTime, metadata->Pinned(), + info); + + return NS_OK; +} + +nsresult CacheFileIOManager::TruncateSeekSetEOFInternal( + CacheFileHandle* aHandle, int64_t aTruncatePos, int64_t aEOFPos) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() [handle=%p, " + "truncatePos=%" PRId64 ", EOFPos=%" PRId64 "]", + aHandle, aTruncatePos, aEOFPos)); + + nsresult rv; + + if (aHandle->mKilled) { + LOG((" handle already killed, file not truncated")); + return NS_OK; + } + + if (CacheObserver::ShuttingDown() && !aHandle->mFD) { + aHandle->mKilled = true; + LOG((" killing the handle, file not truncated")); + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(!aHandle->IsSpecialFile()); + + if (!aHandle->mFileExists) { + rv = CreateFile(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!aHandle->mFD) { + rv = OpenNSPRHandle(aHandle); + NS_ENSURE_SUCCESS(rv, rv); + } else { + NSPRHandleUsed(aHandle); + } + + // Check again, OpenNSPRHandle could figure out the file was gone. + if (!aHandle->mFileExists) { + return NS_ERROR_NOT_AVAILABLE; + } + + // When this operation would increase cache size, check whether the cache size + // reached the hard limit and whether it would cause critical low disk space. + if (aHandle->mFileSize < aEOFPos) { + if (mOverLimitEvicting && mCacheSizeOnHardLimit) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - failing because " + "cache size reached hard limit!")); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aEOFPos + aHandle->mFileSize < limit) { + LOG( + ("CacheFileIOManager::TruncateSeekSetEOFInternal() - Low free space" + ", refusing to write! [freeSpace=%" PRId64 "kB, limit=%ukB]", + freeSpace, limit)); + return NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + } + + // This operation always invalidates the entry + aHandle->mInvalid = true; + + rv = TruncFile(aHandle->mFD, aTruncatePos); + NS_ENSURE_SUCCESS(rv, rv); + + if (aTruncatePos != aEOFPos) { + rv = TruncFile(aHandle->mFD, aEOFPos); + NS_ENSURE_SUCCESS(rv, rv); + } + + uint32_t oldSizeInK = aHandle->FileSizeInK(); + aHandle->mFileSize = aEOFPos; + uint32_t newSizeInK = aHandle->FileSizeInK(); + + if (oldSizeInK != newSizeInK && !aHandle->IsDoomed() && + !aHandle->IsSpecialFile()) { + CacheIndex::UpdateEntry(aHandle->Hash(), nullptr, nullptr, nullptr, nullptr, + nullptr, &newSizeInK); + + if (oldSizeInK < newSizeInK) { + EvictIfOverLimitInternal(); + } + } + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::RenameFile(CacheFileHandle* aHandle, + const nsACString& aNewName, + CacheFileIOListener* aCallback) { + LOG(("CacheFileIOManager::RenameFile() [handle=%p, newName=%s, listener=%p]", + aHandle, PromiseFlatCString(aNewName).get(), aCallback)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = + new RenameFileEvent(aHandle, aNewName, aCallback); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::RenameFileInternal(CacheFileHandle* aHandle, + const nsACString& aNewName) { + LOG(("CacheFileIOManager::RenameFileInternal() [handle=%p, newName=%s]", + aHandle, PromiseFlatCString(aNewName).get())); + + nsresult rv; + + MOZ_ASSERT(aHandle->IsSpecialFile()); + + if (aHandle->IsDoomed()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Doom old handle if it exists and is not doomed + for (uint32_t i = 0; i < mSpecialHandles.Length(); i++) { + if (!mSpecialHandles[i]->IsDoomed() && + mSpecialHandles[i]->Key() == aNewName) { + MOZ_ASSERT(aHandle != mSpecialHandles[i]); + rv = DoomFileInternal(mSpecialHandles[i]); + NS_ENSURE_SUCCESS(rv, rv); + break; + } + } + + nsCOMPtr file; + rv = GetSpecialFile(aNewName, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + LOG( + ("CacheFileIOManager::RenameFileInternal() - Removing old file from " + "disk")); + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING("Cannot remove file from the disk"); + LOG( + ("CacheFileIOManager::RenameFileInternal() - Removing old file failed" + ". [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + } + + if (!aHandle->FileExists()) { + aHandle->mKey = aNewName; + return NS_OK; + } + + rv = MaybeReleaseNSPRHandleInternal(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aHandle->mFile->MoveToNative(nullptr, aNewName); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mKey = aNewName; + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictIfOverLimit() { + LOG(("CacheFileIOManager::EvictIfOverLimit()")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::EvictIfOverLimitInternal", + ioMan, &CacheFileIOManager::EvictIfOverLimitInternal); + + rv = ioMan->mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::EvictIfOverLimitInternal() { + LOG(("CacheFileIOManager::EvictIfOverLimitInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (mOverLimitEvicting) { + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Eviction already " + "running.")); + return NS_OK; + } + + CacheIOThread::Cancelable cancelable(true); + + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + + // Do not change smart size. + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity(); + uint32_t freeSpaceLimit = CacheObserver::DiskFreeSpaceSoftLimit(); + + if (cacheUsage <= cacheLimit && + (freeSpace == -1 || freeSpace >= freeSpaceLimit)) { + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Cache size and free " + "space in limits. [cacheSize=%ukB, cacheSizeLimit=%ukB, " + "freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + cacheUsage, cacheLimit, freeSpace, freeSpaceLimit)); + return NS_OK; + } + + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - Cache size exceeded " + "limit. Starting overlimit eviction. [cacheSize=%ukB, limit=%ukB]", + cacheUsage, cacheLimit)); + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::OverLimitEvictionInternal", + this, &CacheFileIOManager::OverLimitEvictionInternal); + + rv = mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + mOverLimitEvicting = true; + return NS_OK; +} + +nsresult CacheFileIOManager::OverLimitEvictionInternal() { + LOG(("CacheFileIOManager::OverLimitEvictionInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + // mOverLimitEvicting is accessed only on IO thread, so we can set it to false + // here and set it to true again once we dispatch another event that will + // continue with the eviction. The reason why we do so is that we can fail + // early anywhere in this method and the variable will contain a correct + // value. Otherwise we would need to set it to false on every failing place. + mOverLimitEvicting = false; + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + while (true) { + int64_t freeSpace; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + freeSpace = -1; + + // Do not change smart size. + LOG( + ("CacheFileIOManager::EvictIfOverLimitInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08" PRIx32 "]", + static_cast(rv))); + } else { + freeSpace >>= 10; // bytes to kilobytes + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity(); + uint32_t freeSpaceLimit = CacheObserver::DiskFreeSpaceSoftLimit(); + + if (cacheUsage > cacheLimit) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size over " + "limit. [cacheSize=%ukB, limit=%ukB]", + cacheUsage, cacheLimit)); + + // We allow cache size to go over the specified limit. Eviction should + // keep the size within the limit in a long run, but in case the eviction + // is too slow, the cache could go way over the limit. To prevent this we + // set flag mCacheSizeOnHardLimit when the size reaches 105% of the limit + // and WriteInternal() and TruncateSeekSetEOFInternal() fail to cache + // additional data. + if ((cacheUsage - cacheLimit) > (cacheLimit / 20)) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size " + "reached hard limit.")); + mCacheSizeOnHardLimit = true; + } else { + mCacheSizeOnHardLimit = false; + } + } else if (freeSpace != -1 && freeSpace < freeSpaceLimit) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Free space under " + "limit. [freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + freeSpace, freeSpaceLimit)); + } else { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Cache size and " + "free space in limits. [cacheSize=%ukB, cacheSizeLimit=%ukB, " + "freeSpace=%" PRId64 "kB, freeSpaceLimit=%ukB]", + cacheUsage, cacheLimit, freeSpace, freeSpaceLimit)); + + mCacheSizeOnHardLimit = false; + return NS_OK; + } + + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - Breaking loop " + "for higher level events.")); + mOverLimitEvicting = true; + return NS_OK; + } + + SHA1Sum::Hash hash; + uint32_t cnt; + static uint32_t consecutiveFailures = 0; + rv = CacheIndex::GetEntryForEviction(false, &hash, &cnt); + NS_ENSURE_SUCCESS(rv, rv); + + rv = DoomFileByKeyInternal(&hash); + if (NS_SUCCEEDED(rv)) { + consecutiveFailures = 0; + } else if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - " + "DoomFileByKeyInternal() failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + // TODO index is outdated, start update + + // Make sure index won't return the same entry again + CacheIndex::RemoveEntry(&hash); + consecutiveFailures = 0; + } else { + // This shouldn't normally happen, but the eviction must not fail + // completely if we ever encounter this problem. + NS_WARNING( + "CacheFileIOManager::OverLimitEvictionInternal() - Unexpected " + "failure of DoomFileByKeyInternal()"); + + LOG( + ("CacheFileIOManager::OverLimitEvictionInternal() - " + "DoomFileByKeyInternal() failed. [rv=0x%08" PRIx32 "]", + static_cast(rv))); + + // Normally, CacheIndex::UpdateEntry() is called only to update newly + // created/opened entries which are always fresh and UpdateEntry() expects + // and checks this flag. The way we use UpdateEntry() here is a kind of + // hack and we must make sure the flag is set by calling + // EnsureEntryExists(). + rv = CacheIndex::EnsureEntryExists(&hash); + NS_ENSURE_SUCCESS(rv, rv); + + // Move the entry at the end of both lists to make sure we won't end up + // failing on one entry forever. + uint32_t frecency = 0; + rv = CacheIndex::UpdateEntry(&hash, &frecency, nullptr, nullptr, nullptr, + nullptr, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + consecutiveFailures++; + if (consecutiveFailures >= cnt) { + // This doesn't necessarily mean that we've tried to doom every entry + // but we've reached a sane number of tries. It is likely that another + // eviction will start soon. And as said earlier, this normally doesn't + // happen at all. + return NS_OK; + } + } + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictAll() { + LOG(("CacheFileIOManager::EvictAll()")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::EvictAllInternal", ioMan, + &CacheFileIOManager::EvictAllInternal); + + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +namespace { + +class EvictionNotifierRunnable : public Runnable { + public: + EvictionNotifierRunnable() : Runnable("EvictionNotifierRunnable") {} + NS_DECL_NSIRUNNABLE +}; + +NS_IMETHODIMP +EvictionNotifierRunnable::Run() { + nsCOMPtr obsSvc = mozilla::services::GetObserverService(); + if (obsSvc) { + obsSvc->NotifyObservers(nullptr, "cacheservice:empty-cache", nullptr); + } + return NS_OK; +} + +} // namespace + +nsresult CacheFileIOManager::EvictAllInternal() { + LOG(("CacheFileIOManager::EvictAllInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + RefPtr r = new EvictionNotifierRunnable(); + + if (!mCacheDirectory) { + // This is a kind of hack. Somebody called EvictAll() without a profile. + // This happens in xpcshell tests that use cache without profile. We need + // to notify observers in this case since the tests are waiting for it. + NS_DispatchToMainThread(r); + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + // Doom all active handles + nsTArray> handles; + mHandles.GetActiveHandles(&handles); + + for (uint32_t i = 0; i < handles.Length(); ++i) { + rv = DoomFileInternal(handles[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::EvictAllInternal() - Cannot doom handle " + "[handle=%p]", + handles[i].get())); + } + } + + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(nsLiteralCString(ENTRIES_DIR)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Trash current entries directory + rv = TrashDirectory(file); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Files are now inaccessible in entries directory, notify observers. + NS_DispatchToMainThread(r); + + // Create a new empty entries directory + rv = CheckAndCreateDir(mCacheDirectory, ENTRIES_DIR, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + CacheIndex::RemoveAll(); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::EvictByContext( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, const nsAString& aBaseDomain) { + LOG(("CacheFileIOManager::EvictByContext() [loadContextInfo=%p]", + aLoadContextInfo)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr ev; + ev = + NewRunnableMethod, bool, nsString, nsString>( + "net::CacheFileIOManager::EvictByContextInternal", ioMan, + &CacheFileIOManager::EvictByContextInternal, aLoadContextInfo, + aPinned, aOrigin, aBaseDomain); + + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult CacheFileIOManager::EvictByContextInternal( + nsILoadContextInfo* aLoadContextInfo, bool aPinned, + const nsAString& aOrigin, const nsAString& aBaseDomain) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() [loadContextInfo=%p, " + "pinned=%d]", + aLoadContextInfo, aPinned)); + + nsresult rv; + + if (aLoadContextInfo) { + nsAutoCString suffix; + aLoadContextInfo->OriginAttributesPtr()->CreateSuffix(suffix); + LOG((" anonymous=%u, suffix=%s]", aLoadContextInfo->IsAnonymous(), + suffix.get())); + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + MOZ_ASSERT(!aLoadContextInfo->IsPrivate()); + if (aLoadContextInfo->IsPrivate()) { + return NS_ERROR_INVALID_ARG; + } + } + + if (!mCacheDirectory) { + // This is a kind of hack. Somebody called EvictAll() without a profile. + // This happens in xpcshell tests that use cache without profile. We need + // to notify observers in this case since the tests are waiting for it. + // Also notify for aPinned == true, those are interested as well. + if (!aLoadContextInfo) { + RefPtr r = new EvictionNotifierRunnable(); + NS_DispatchToMainThread(r); + } + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + NS_ConvertUTF16toUTF8 origin(aOrigin); + NS_ConvertUTF16toUTF8 baseDomain(aBaseDomain); + + // Doom all active handles that matches the load context + nsTArray> handles; + mHandles.GetActiveHandles(&handles); + + for (uint32_t i = 0; i < handles.Length(); ++i) { + CacheFileHandle* handle = handles[i]; + + const bool shouldRemove = [&] { + nsAutoCString uriSpec; + RefPtr info = + CacheFileUtils::ParseKey(handle->Key(), nullptr, &uriSpec); + if (!info) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() - Cannot parse key " + "in " + "handle! [handle=%p, key=%s]", + handle, handle->Key().get())); + MOZ_CRASH("Unexpected error!"); + } + + // Filter by base domain. + if (!aBaseDomain.IsEmpty()) { + if (StoragePrincipalHelper::PartitionKeyHasBaseDomain( + info->OriginAttributesPtr()->mPartitionKey, aBaseDomain)) { + return true; + } + + // If the partitionKey does not match, check the entry URI next. + + // Get host portion of uriSpec. + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), uriSpec); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + nsAutoCString host; + rv = uri->GetHost(host); + // Some entries may not have valid hosts. We can skip them. + if (NS_FAILED(rv) || host.IsEmpty()) { + return false; + } + + // Clear entry if the host belongs to the given base domain. + bool hasRootDomain = false; + rv = HasRootDomain(host, baseDomain, &hasRootDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + return hasRootDomain; + } + + // Filter by LoadContextInfo. + if (aLoadContextInfo && !info->Equals(aLoadContextInfo)) { + return false; + } + + // Filter by origin. + if (!origin.IsEmpty()) { + RefPtr url; + rv = MozURL::Init(getter_AddRefs(url), uriSpec); + if (NS_FAILED(rv)) { + return false; + } + + nsAutoCString urlOrigin; + url->Origin(urlOrigin); + + if (!urlOrigin.Equals(origin)) { + return false; + } + } + + return true; + }(); + + if (!shouldRemove) { + continue; + } + + // handle will be doomed only when pinning status is known and equal or + // doom decision will be deferred until pinning status is determined. + rv = DoomFileInternal(handle, + aPinned ? CacheFileIOManager::DOOM_WHEN_PINNED + : CacheFileIOManager::DOOM_WHEN_NON_PINNED); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::EvictByContextInternal() - Cannot doom handle" + " [handle=%p]", + handle)); + } + } + + if (!aLoadContextInfo) { + RefPtr r = new EvictionNotifierRunnable(); + NS_DispatchToMainThread(r); + } + + if (!mContextEvictor) { + mContextEvictor = new CacheFileContextEvictor(); + mContextEvictor->Init(mCacheDirectory); + } + + mContextEvictor->AddContext(aLoadContextInfo, aPinned, aOrigin); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::CacheIndexStateChanged() { + LOG(("CacheFileIOManager::CacheIndexStateChanged()")); + + nsresult rv; + + // CacheFileIOManager lives longer than CacheIndex so gInstance must be + // non-null here. + MOZ_ASSERT(gInstance); + + // We have to re-distatch even if we are on IO thread to prevent reentering + // the lock in CacheIndex + nsCOMPtr ev = NewRunnableMethod( + "net::CacheFileIOManager::CacheIndexStateChangedInternal", + gInstance.get(), &CacheFileIOManager::CacheIndexStateChangedInternal); + + nsCOMPtr ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + rv = ioTarget->Dispatch(ev, nsIEventTarget::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +void CacheFileIOManager::CacheIndexStateChangedInternal() { + if (mShuttingDown) { + // ignore notification during shutdown + return; + } + + if (!mContextEvictor) { + return; + } + + mContextEvictor->CacheIndexStateChanged(); +} + +nsresult CacheFileIOManager::TrashDirectory(nsIFile* aFile) { + LOG(("CacheFileIOManager::TrashDirectory() [file=%s]", + aFile->HumanReadablePath().get())); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + MOZ_ASSERT(mCacheDirectory); + + // When the directory is empty, it is cheaper to remove it directly instead of + // using the trash mechanism. + bool isEmpty; + rv = IsEmptyDirectory(aFile, &isEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (isEmpty) { + rv = aFile->Remove(false); + LOG( + ("CacheFileIOManager::TrashDirectory() - Directory removed " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + +#ifdef DEBUG + nsCOMPtr dirCheck; + rv = aFile->GetParent(getter_AddRefs(dirCheck)); + NS_ENSURE_SUCCESS(rv, rv); + + bool equals = false; + rv = dirCheck->Equals(mCacheDirectory, &equals); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(equals); +#endif + + nsCOMPtr dir, trash; + nsAutoCString leaf; + + rv = aFile->Clone(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aFile->Clone(getter_AddRefs(trash)); + NS_ENSURE_SUCCESS(rv, rv); + + const int32_t kMaxTries = 16; + srand(static_cast(PR_Now())); + for (int32_t triesCount = 0;; ++triesCount) { + leaf = TRASH_DIR; + leaf.AppendInt(rand()); + rv = trash->SetNativeLeafName(leaf); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + if (NS_SUCCEEDED(trash->Exists(&exists)) && !exists) { + break; + } + + LOG( + ("CacheFileIOManager::TrashDirectory() - Trash directory already " + "exists [leaf=%s]", + leaf.get())); + + if (triesCount == kMaxTries) { + LOG( + ("CacheFileIOManager::TrashDirectory() - Could not find unused trash " + "directory in %d tries.", + kMaxTries)); + return NS_ERROR_FAILURE; + } + } + + LOG(("CacheFileIOManager::TrashDirectory() - Renaming directory [leaf=%s]", + leaf.get())); + + rv = dir->MoveToNative(nullptr, leaf); + NS_ENSURE_SUCCESS(rv, rv); + + StartRemovingTrash(); + return NS_OK; +} + +// static +void CacheFileIOManager::OnTrashTimer(nsITimer* aTimer, void* aClosure) { + LOG(("CacheFileIOManager::OnTrashTimer() [timer=%p, closure=%p]", aTimer, + aClosure)); + + RefPtr ioMan = gInstance; + + if (!ioMan) { + return; + } + + ioMan->mTrashTimer = nullptr; + ioMan->StartRemovingTrash(); +} + +nsresult CacheFileIOManager::StartRemovingTrash() { + LOG(("CacheFileIOManager::StartRemovingTrash()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!mCacheDirectory) { + return NS_ERROR_FILE_INVALID_PATH; + } + + if (mTrashTimer) { + LOG(("CacheFileIOManager::StartRemovingTrash() - Trash timer exists.")); + return NS_OK; + } + + if (mRemovingTrashDirs) { + LOG( + ("CacheFileIOManager::StartRemovingTrash() - Trash removing in " + "progress.")); + return NS_OK; + } + + uint32_t elapsed = (TimeStamp::NowLoRes() - mStartTime).ToMilliseconds(); + if (elapsed < kRemoveTrashStartDelay) { + nsCOMPtr ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + return NS_NewTimerWithFuncCallback( + getter_AddRefs(mTrashTimer), CacheFileIOManager::OnTrashTimer, nullptr, + kRemoveTrashStartDelay - elapsed, nsITimer::TYPE_ONE_SHOT, + "net::CacheFileIOManager::StartRemovingTrash", ioTarget); + } + + nsCOMPtr ev; + ev = NewRunnableMethod("net::CacheFileIOManager::RemoveTrashInternal", this, + &CacheFileIOManager::RemoveTrashInternal); + + rv = mIOThread->Dispatch(ev, CacheIOThread::EVICT); + NS_ENSURE_SUCCESS(rv, rv); + + mRemovingTrashDirs = true; + return NS_OK; +} + +nsresult CacheFileIOManager::RemoveTrashInternal() { + LOG(("CacheFileIOManager::RemoveTrashInternal()")); + + nsresult rv; + + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + if (mShuttingDown) { + return NS_ERROR_NOT_INITIALIZED; + } + + CacheIOThread::Cancelable cancelable(true); + + MOZ_ASSERT(!mTrashTimer); + MOZ_ASSERT(mRemovingTrashDirs); + + if (!mTreeCreated) { + rv = CreateCacheTree(); + if (NS_FAILED(rv)) { + return rv; + } + } + + // mRemovingTrashDirs is accessed only on IO thread, so we can drop the flag + // here and set it again once we dispatch a continuation event. By doing so, + // we don't have to drop the flag on any possible early return. + mRemovingTrashDirs = false; + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Breaking loop for " + "higher level events.")); + mRemovingTrashDirs = true; + return NS_OK; + } + + // Find some trash directory + if (!mTrashDir) { + MOZ_ASSERT(!mTrashDirEnumerator); + + rv = FindTrashDirToRemove(); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - No trash directory " + "found.")); + return NS_OK; + } + NS_ENSURE_SUCCESS(rv, rv); + + rv = mTrashDir->GetDirectoryEntries(getter_AddRefs(mTrashDirEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + continue; // check elapsed time + } + + // We null out mTrashDirEnumerator once we remove all files in the + // directory, so remove the trash directory if we don't have enumerator. + if (!mTrashDirEnumerator) { + rv = mTrashDir->Remove(false); + if (NS_FAILED(rv)) { + // There is no reason why removing an empty directory should fail, but + // if it does, we should continue and try to remove all other trash + // directories. + nsAutoCString leafName; + mTrashDir->GetNativeLeafName(leafName); + mFailedTrashDirs.AppendElement(leafName); + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Cannot remove " + "trashdir. [name=%s]", + leafName.get())); + } + + mTrashDir = nullptr; + continue; // check elapsed time + } + + nsCOMPtr file; + rv = mTrashDirEnumerator->GetNextFile(getter_AddRefs(file)); + if (!file) { + mTrashDirEnumerator->Close(); + mTrashDirEnumerator = nullptr; + continue; // check elapsed time + } + bool isDir = false; + file->IsDirectory(&isDir); + if (isDir) { + NS_WARNING( + "Found a directory in a trash directory! It will be removed " + "recursively, but this can block IO thread for a while!"); + if (LOG_ENABLED()) { + LOG( + ("CacheFileIOManager::RemoveTrashInternal() - Found a directory in " + "a trash " + "directory! It will be removed recursively, but this can block IO " + "thread for a while! [file=%s]", + file->HumanReadablePath().get())); + } + } + file->Remove(isDir); + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); + return NS_OK; +} + +nsresult CacheFileIOManager::FindTrashDirToRemove() { + LOG(("CacheFileIOManager::FindTrashDirToRemove()")); + + nsresult rv; + + if (!mCacheDirectory) { + return NS_ERROR_UNEXPECTED; + } + + // We call this method on the main thread during shutdown when user wants to + // remove all cache files. + MOZ_ASSERT(mIOThread->IsCurrentThread() || mShuttingDown); + + nsCOMPtr iter; + rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(iter)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file; + while (NS_SUCCEEDED(iter->GetNextFile(getter_AddRefs(file))) && file) { + bool isDir = false; + file->IsDirectory(&isDir); + if (!isDir) { + continue; + } + + nsAutoCString leafName; + rv = file->GetNativeLeafName(leafName); + if (NS_FAILED(rv)) { + continue; + } + + if (leafName.Length() < strlen(TRASH_DIR)) { + continue; + } + + if (!StringBeginsWith(leafName, nsLiteralCString(TRASH_DIR))) { + continue; + } + + if (mFailedTrashDirs.Contains(leafName)) { + continue; + } + + LOG(("CacheFileIOManager::FindTrashDirToRemove() - Returning directory %s", + leafName.get())); + + mTrashDir = file; + return NS_OK; + } + + // When we're here we've tried to delete all trash directories. Clear + // mFailedTrashDirs so we will try to delete them again when we start removing + // trash directories next time. + mFailedTrashDirs.Clear(); + return NS_ERROR_NOT_AVAILABLE; +} + +// static +nsresult CacheFileIOManager::InitIndexEntry(CacheFileHandle* aHandle, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous, bool aPinning) { + LOG( + ("CacheFileIOManager::InitIndexEntry() [handle=%p, " + "originAttrsHash=%" PRIx64 ", " + "anonymous=%d, pinning=%d]", + aHandle, aOriginAttrsHash, aAnonymous, aPinning)); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = + new InitIndexEntryEvent(aHandle, aOriginAttrsHash, aAnonymous, aPinning); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// static +nsresult CacheFileIOManager::UpdateIndexEntry(CacheFileHandle* aHandle, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType) { + LOG( + ("CacheFileIOManager::UpdateIndexEntry() [handle=%p, frecency=%s, " + "hasAltData=%s, onStartTime=%s, onStopTime=%s, contentType=%s]", + aHandle, aFrecency ? nsPrintfCString("%u", *aFrecency).get() : "", + aHasAltData ? (*aHasAltData ? "true" : "false") : "", + aOnStartTime ? nsPrintfCString("%u", *aOnStartTime).get() : "", + aOnStopTime ? nsPrintfCString("%u", *aOnStopTime).get() : "", + aContentType ? nsPrintfCString("%u", *aContentType).get() : "")); + + nsresult rv; + RefPtr ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr ev = new UpdateIndexEntryEvent( + aHandle, aFrecency, aHasAltData, aOnStartTime, aOnStopTime, aContentType); + rv = ioMan->mIOThread->Dispatch(ev, aHandle->mPriority + ? CacheIOThread::WRITE_PRIORITY + : CacheIOThread::WRITE); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheFileIOManager::CreateFile(CacheFileHandle* aHandle) { + MOZ_ASSERT(!aHandle->mFD); + MOZ_ASSERT(aHandle->mFile); + + nsresult rv; + + if (aHandle->IsDoomed()) { + nsCOMPtr file; + + rv = GetDoomedFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFile.swap(file); + } else { + bool exists; + if (NS_SUCCEEDED(aHandle->mFile->Exists(&exists)) && exists) { + NS_WARNING("Found a file that should not exist!"); + } + } + + rv = OpenNSPRHandle(aHandle, true); + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileSize = 0; + return NS_OK; +} + +// static +void CacheFileIOManager::HashToStr(const SHA1Sum::Hash* aHash, + nsACString& _retval) { + _retval.Truncate(); + const char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + for (uint32_t i = 0; i < sizeof(SHA1Sum::Hash); i++) { + _retval.Append(hexChars[(*aHash)[i] >> 4]); + _retval.Append(hexChars[(*aHash)[i] & 0xF]); + } +} + +// static +nsresult CacheFileIOManager::StrToHash(const nsACString& aHash, + SHA1Sum::Hash* _retval) { + if (aHash.Length() != 2 * sizeof(SHA1Sum::Hash)) { + return NS_ERROR_INVALID_ARG; + } + + for (uint32_t i = 0; i < aHash.Length(); i++) { + uint8_t value; + + if (aHash[i] >= '0' && aHash[i] <= '9') { + value = aHash[i] - '0'; + } else if (aHash[i] >= 'A' && aHash[i] <= 'F') { + value = aHash[i] - 'A' + 10; + } else if (aHash[i] >= 'a' && aHash[i] <= 'f') { + value = aHash[i] - 'a' + 10; + } else { + return NS_ERROR_INVALID_ARG; + } + + if (i % 2 == 0) { + (reinterpret_cast(_retval))[i / 2] = value << 4; + } else { + (reinterpret_cast(_retval))[i / 2] += value; + } + } + + return NS_OK; +} + +nsresult CacheFileIOManager::GetFile(const SHA1Sum::Hash* aHash, + nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(nsLiteralCString(ENTRIES_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString leafName; + HashToStr(aHash, leafName); + + rv = file->AppendNative(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::GetSpecialFile(const nsACString& aKey, + nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(aKey); + NS_ENSURE_SUCCESS(rv, rv); + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::GetDoomedFile(nsIFile** _retval) { + nsresult rv; + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(nsLiteralCString(DOOMED_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative("dummyleaf"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + const int32_t kMaxTries = 64; + srand(static_cast(PR_Now())); + nsAutoCString leafName; + for (int32_t triesCount = 0;; ++triesCount) { + leafName.AppendInt(rand()); + rv = file->SetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + if (NS_SUCCEEDED(file->Exists(&exists)) && !exists) { + break; + } + + if (triesCount == kMaxTries) { + LOG( + ("CacheFileIOManager::GetDoomedFile() - Could not find unused file " + "name in %d tries.", + kMaxTries)); + return NS_ERROR_FAILURE; + } + + leafName.Truncate(); + } + + file.swap(*_retval); + return NS_OK; +} + +nsresult CacheFileIOManager::IsEmptyDirectory(nsIFile* aFile, bool* _retval) { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + nsresult rv; + + nsCOMPtr enumerator; + rv = aFile->GetDirectoryEntries(getter_AddRefs(enumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMoreElements = false; + rv = enumerator->HasMoreElements(&hasMoreElements); + NS_ENSURE_SUCCESS(rv, rv); + + *_retval = !hasMoreElements; + return NS_OK; +} + +nsresult CacheFileIOManager::CheckAndCreateDir(nsIFile* aFile, const char* aDir, + bool aEnsureEmptyDir) { + nsresult rv; + + nsCOMPtr file; + if (!aDir) { + file = aFile; + } else { + nsAutoCString dir(aDir); + rv = aFile->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->AppendNative(dir); + NS_ENSURE_SUCCESS(rv, rv); + } + + bool exists = false; + rv = file->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + bool isDirectory = false; + rv = file->IsDirectory(&isDirectory); + if (NS_FAILED(rv) || !isDirectory) { + // Try to remove the file + rv = file->Remove(false); + if (NS_SUCCEEDED(rv)) { + exists = false; + } + } + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aEnsureEmptyDir && NS_SUCCEEDED(rv) && exists) { + bool isEmpty; + rv = IsEmptyDirectory(file, &isEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (!isEmpty) { + // Don't check the result, if this fails, it's OK. We do this + // only for the doomed directory that doesn't need to be deleted + // for the cost of completely disabling the whole browser. + TrashDirectory(file); + } + } + + if (NS_SUCCEEDED(rv) && !exists) { + rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); + } + if (NS_FAILED(rv)) { + NS_WARNING("Cannot create directory"); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult CacheFileIOManager::CreateCacheTree() { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + MOZ_ASSERT(!mTreeCreated); + + if (!mCacheDirectory || mTreeCreationFailed) { + return NS_ERROR_FILE_INVALID_PATH; + } + + nsresult rv; + + // Set the flag here and clear it again below when the tree is created + // successfully. + mTreeCreationFailed = true; + + // ensure parent directory exists + nsCOMPtr parentDir; + rv = mCacheDirectory->GetParent(getter_AddRefs(parentDir)); + NS_ENSURE_SUCCESS(rv, rv); + rv = CheckAndCreateDir(parentDir, nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure cache directory exists + rv = CheckAndCreateDir(mCacheDirectory, nullptr, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure entries directory exists + rv = CheckAndCreateDir(mCacheDirectory, ENTRIES_DIR, false); + NS_ENSURE_SUCCESS(rv, rv); + + // ensure doomed directory exists + rv = CheckAndCreateDir(mCacheDirectory, DOOMED_DIR, true); + NS_ENSURE_SUCCESS(rv, rv); + + mTreeCreated = true; + mTreeCreationFailed = false; + + if (!mContextEvictor) { + RefPtr contextEvictor; + contextEvictor = new CacheFileContextEvictor(); + + // Init() method will try to load unfinished contexts from the disk. Store + // the evictor as a member only when there is some unfinished job. + contextEvictor->Init(mCacheDirectory); + if (contextEvictor->ContextsCount()) { + contextEvictor.swap(mContextEvictor); + } + } + + StartRemovingTrash(); + + return NS_OK; +} + +nsresult CacheFileIOManager::OpenNSPRHandle(CacheFileHandle* aHandle, + bool aCreate) { + LOG(("CacheFileIOManager::OpenNSPRHandle BEGIN, handle=%p", aHandle)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(!aHandle->mFD); + MOZ_ASSERT(mHandlesByLastUsed.IndexOf(aHandle) == mHandlesByLastUsed.NoIndex); + MOZ_ASSERT(mHandlesByLastUsed.Length() <= kOpenHandlesLimit); + MOZ_ASSERT((aCreate && !aHandle->mFileExists) || + (!aCreate && aHandle->mFileExists)); + + nsresult rv; + + if (mHandlesByLastUsed.Length() == kOpenHandlesLimit) { + // close handle that hasn't been used for the longest time + rv = MaybeReleaseNSPRHandleInternal(mHandlesByLastUsed[0], true); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aCreate) { + rv = aHandle->mFile->OpenNSPRFileDesc( + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, &aHandle->mFD); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS || // error from nsLocalFileWin + rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { // error from nsLocalFileUnix + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Cannot create a new file, we" + " might reached a limit on FAT32. Will evict a single entry and try " + "again. [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHandle->Hash()))); + + SHA1Sum::Hash hash; + uint32_t cnt; + + rv = CacheIndex::GetEntryForEviction(true, &hash, &cnt); + if (NS_SUCCEEDED(rv)) { + rv = DoomFileByKeyInternal(&hash); + } + if (NS_SUCCEEDED(rv)) { + rv = aHandle->mFile->OpenNSPRFileDesc( + PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, &aHandle->mFD); + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Successfully evicted entry" + " with hash %08x%08x%08x%08x%08x. %s to create the new file.", + LOGSHA1(&hash), NS_SUCCEEDED(rv) ? "Succeeded" : "Failed")); + + // Report the full size only once per session + static bool sSizeReported = false; + if (!sSizeReported) { + uint32_t cacheUsage; + if (NS_SUCCEEDED(CacheIndex::GetCacheSize(&cacheUsage))) { + cacheUsage >>= 10; + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_SIZE_FULL_FAT, + cacheUsage); + sSizeReported = true; + } + } + } else { + LOG( + ("CacheFileIOManager::OpenNSPRHandle() - Couldn't evict an existing" + " entry.")); + rv = NS_ERROR_FILE_NO_DEVICE_SPACE; + } + } + if (NS_FAILED(rv)) { + LOG( + ("CacheFileIOManager::OpenNSPRHandle() Create failed with " + "0x%08" PRIx32, + static_cast(rv))); + } + NS_ENSURE_SUCCESS(rv, rv); + + aHandle->mFileExists = true; + } else { + rv = aHandle->mFile->OpenNSPRFileDesc(PR_RDWR, 0600, &aHandle->mFD); + if (NS_ERROR_FILE_NOT_FOUND == rv) { + LOG((" file doesn't exists")); + aHandle->mFileExists = false; + return DoomFileInternal(aHandle); + } + if (NS_FAILED(rv)) { + LOG(("CacheFileIOManager::OpenNSPRHandle() Open failed with 0x%08" PRIx32, + static_cast(rv))); + } + NS_ENSURE_SUCCESS(rv, rv); + } + + mHandlesByLastUsed.AppendElement(aHandle); + + LOG(("CacheFileIOManager::OpenNSPRHandle END, handle=%p", aHandle)); + + return NS_OK; +} + +void CacheFileIOManager::NSPRHandleUsed(CacheFileHandle* aHandle) { + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + MOZ_ASSERT(aHandle->mFD); + + DebugOnly found{}; + found = mHandlesByLastUsed.RemoveElement(aHandle); + MOZ_ASSERT(found); + + mHandlesByLastUsed.AppendElement(aHandle); +} + +nsresult CacheFileIOManager::SyncRemoveDir(nsIFile* aFile, const char* aDir) { + nsresult rv; + nsCOMPtr file; + + if (!aFile) { + return NS_ERROR_INVALID_ARG; + } + + if (!aDir) { + file = aFile; + } else { + rv = aFile->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(nsDependentCString(aDir)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + if (LOG_ENABLED()) { + LOG(("CacheFileIOManager::SyncRemoveDir() - Removing directory %s", + file->HumanReadablePath().get())); + } + + rv = file->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::SyncRemoveDir() - Removing failed! " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + } + + return rv; +} + +nsresult CacheFileIOManager::DispatchPurgeTask( + const nsCString& aCacheDirName, const nsCString& aSecondsToWait, + const nsCString& aPurgeExtension) { +#if !defined(MOZ_BACKGROUNDTASKS) + // If background tasks are disabled, then we should just bail out early. + return NS_ERROR_NOT_IMPLEMENTED; +#else + nsCOMPtr cacheDir; + nsresult rv = mCacheDirectory->Clone(getter_AddRefs(cacheDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr profileDir; + rv = cacheDir->GetParent(getter_AddRefs(profileDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr lf; + rv = XRE_GetBinaryPath(getter_AddRefs(lf)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString path; +# if !defined(XP_WIN) + rv = profileDir->GetNativePath(path); +# else + rv = profileDir->GetNativeTarget(path); +# endif + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr runner = + do_GetService("@mozilla.org/backgroundtasksrunner;1"); + + return runner->RemoveDirectoryInDetachedProcess( + path, aCacheDirName, aSecondsToWait, aPurgeExtension, "HttpCache"_ns); +#endif +} + +void CacheFileIOManager::SyncRemoveAllCacheFiles() { + LOG(("CacheFileIOManager::SyncRemoveAllCacheFiles()")); + nsresult rv; + + // If we are already running in a background task, we + // don't want to spawn yet another one at shutdown. + if (inBackgroundTask()) { + return; + } + + if (StaticPrefs::network_cache_shutdown_purge_in_background_task()) { + rv = [&]() -> nsresult { + nsresult rv; + + // If there is no cache directory, there's nothing to remove. + if (!mCacheDirectory) { + return NS_OK; + } + + nsAutoCString leafName; + rv = mCacheDirectory->GetNativeLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + leafName.Append('.'); + + PRExplodedTime now; + PR_ExplodeTime(PR_Now(), PR_GMTParameters, &now); + leafName.Append(nsPrintfCString( + "%04d-%02d-%02d-%02d-%02d-%02d", now.tm_year, now.tm_month + 1, + now.tm_mday, now.tm_hour, now.tm_min, now.tm_sec)); + leafName.Append(kPurgeExtension); + + nsAutoCString secondsToWait; + secondsToWait.AppendInt( + StaticPrefs::network_cache_shutdown_purge_folder_wait_seconds()); + + rv = DispatchPurgeTask(leafName, secondsToWait, kPurgeExtension); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mCacheDirectory->RenameToNative(nullptr, leafName); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + }(); + + // Dispatching to the background task has succeeded. This is finished. + if (NS_SUCCEEDED(rv)) { + return; + } + } + + SyncRemoveDir(mCacheDirectory, ENTRIES_DIR); + SyncRemoveDir(mCacheDirectory, DOOMED_DIR); + + // Clear any intermediate state of trash dir enumeration. + mFailedTrashDirs.Clear(); + mTrashDir = nullptr; + + while (true) { + // FindTrashDirToRemove() fills mTrashDir if there is any trash directory. + rv = FindTrashDirToRemove(); + if (rv == NS_ERROR_NOT_AVAILABLE) { + LOG( + ("CacheFileIOManager::SyncRemoveAllCacheFiles() - No trash directory " + "found.")); + break; + } + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::SyncRemoveAllCacheFiles() - " + "FindTrashDirToRemove() returned an unexpected error. " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + break; + } + + rv = SyncRemoveDir(mTrashDir, nullptr); + if (NS_FAILED(rv)) { + nsAutoCString leafName; + mTrashDir->GetNativeLeafName(leafName); + mFailedTrashDirs.AppendElement(leafName); + } + } +} + +// Returns default ("smart") size (in KB) of cache, given available disk space +// (also in KB) +static uint32_t SmartCacheSize(const int64_t availKB) { + uint32_t maxSize; + + if (CacheObserver::ClearCacheOnShutdown()) { + maxSize = kMaxClearOnShutdownCacheSizeKB; + } else { + maxSize = kMaxCacheSizeKB; + } + + if (availKB > 25 * 1024 * 1024) { + return maxSize; // skip computing if we're over 25 GB + } + + // Grow/shrink in 10 MB units, deliberately, so that in the common case we + // don't shrink cache and evict items every time we startup (it's important + // that we don't slow down startup benchmarks). + uint32_t sz10MBs = 0; + uint32_t avail10MBs = availKB / (1024 * 10); + + // 2.5% of space above 7GB + if (avail10MBs > 700) { + sz10MBs += static_cast((avail10MBs - 700) * .025); + avail10MBs = 700; + } + // 7.5% of space between 500 MB -> 7 GB + if (avail10MBs > 50) { + sz10MBs += static_cast((avail10MBs - 50) * .075); + avail10MBs = 50; + } + +#ifdef ANDROID + // On Android, smaller/older devices may have very little storage and + // device owners may be sensitive to storage footprint: Use a smaller + // percentage of available space and a smaller minimum. + + // 16% of space up to 500 MB (10 MB min) + sz10MBs += std::max(1, static_cast(avail10MBs * .16)); +#else + // 30% of space up to 500 MB (50 MB min) + sz10MBs += std::max(5, static_cast(avail10MBs * .3)); +#endif + + return std::min(maxSize, sz10MBs * 10 * 1024); +} + +nsresult CacheFileIOManager::UpdateSmartCacheSize(int64_t aFreeSpace) { + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + nsresult rv; + + if (!CacheObserver::SmartCacheSizeEnabled()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Wait at least kSmartSizeUpdateInterval before recomputing smart size. + static const TimeDuration kUpdateLimit = + TimeDuration::FromMilliseconds(kSmartSizeUpdateInterval); + if (!mLastSmartSizeTime.IsNull() && + (TimeStamp::NowLoRes() - mLastSmartSizeTime) < kUpdateLimit) { + return NS_OK; + } + + // Do not compute smart size when cache size is not reliable. + bool isUpToDate = false; + CacheIndex::IsUpToDate(&isUpToDate); + if (!isUpToDate) { + return NS_ERROR_NOT_AVAILABLE; + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG( + ("CacheFileIOManager::UpdateSmartCacheSize() - Cannot get cacheUsage! " + "[rv=0x%08" PRIx32 "]", + static_cast(rv))); + return rv; + } + + mLastSmartSizeTime = TimeStamp::NowLoRes(); + + uint32_t smartSize = SmartCacheSize(aFreeSpace + cacheUsage); + + if (smartSize == CacheObserver::DiskCacheCapacity()) { + // Smart size has not changed. + return NS_OK; + } + + CacheObserver::SetSmartDiskCacheCapacity(smartSize); + + return NS_OK; +} + +// Memory reporting + +namespace { + +// A helper class that dispatches and waits for an event that gets result of +// CacheFileIOManager->mHandles.SizeOfExcludingThis() on the I/O thread +// to safely get handles memory report. +// We must do this, since the handle list is only accessed and managed w/o +// locking on the I/O thread. That is by design. +class SizeOfHandlesRunnable : public Runnable { + public: + SizeOfHandlesRunnable(mozilla::MallocSizeOf mallocSizeOf, + CacheFileHandles const& handles, + nsTArray const& specialHandles) + : Runnable("net::SizeOfHandlesRunnable"), + mMonitor("SizeOfHandlesRunnable.mMonitor"), + mMonitorNotified(false), + mMallocSizeOf(mallocSizeOf), + mHandles(handles), + mSpecialHandles(specialHandles), + mSize(0) {} + + size_t Get(CacheIOThread* thread) { + nsCOMPtr target = thread->Target(); + if (!target) { + NS_ERROR("If we have the I/O thread we also must have the I/O target"); + return 0; + } + + mozilla::MonitorAutoLock mon(mMonitor); + mMonitorNotified = false; + nsresult rv = target->Dispatch(this, nsIEventTarget::DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_ERROR("Dispatch failed, cannot do memory report of CacheFileHandles"); + return 0; + } + + while (!mMonitorNotified) { + mon.Wait(); + } + return mSize; + } + + NS_IMETHOD Run() override { + mozilla::MonitorAutoLock mon(mMonitor); + // Excluding this since the object itself is a member of CacheFileIOManager + // reported in CacheFileIOManager::SizeOfIncludingThis as part of |this|. + mSize = mHandles.SizeOfExcludingThis(mMallocSizeOf); + for (uint32_t i = 0; i < mSpecialHandles.Length(); ++i) { + mSize += mSpecialHandles[i]->SizeOfIncludingThis(mMallocSizeOf); + } + + mMonitorNotified = true; + mon.Notify(); + return NS_OK; + } + + private: + mozilla::Monitor mMonitor MOZ_UNANNOTATED; + bool mMonitorNotified; + mozilla::MallocSizeOf mMallocSizeOf; + CacheFileHandles const& mHandles; + nsTArray const& mSpecialHandles; + size_t mSize; +}; + +} // namespace + +size_t CacheFileIOManager::SizeOfExcludingThisInternal( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = 0; + nsCOMPtr sizeOf; + + if (mIOThread) { + n += mIOThread->SizeOfIncludingThis(mallocSizeOf); + + // mHandles and mSpecialHandles must be accessed only on the I/O thread, + // must sync dispatch. + RefPtr sizeOfHandlesRunnable = + new SizeOfHandlesRunnable(mallocSizeOf, mHandles, mSpecialHandles); + n += sizeOfHandlesRunnable->Get(mIOThread); + } + + // mHandlesByLastUsed just refers handles reported by mHandles. + + sizeOf = do_QueryInterface(mCacheDirectory); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mMetadataWritesTimer); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mTrashTimer); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + sizeOf = do_QueryInterface(mTrashDir); + if (sizeOf) n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + + for (uint32_t i = 0; i < mFailedTrashDirs.Length(); ++i) { + n += mFailedTrashDirs[i].SizeOfExcludingThisIfUnshared(mallocSizeOf); + } + + return n; +} + +// static +size_t CacheFileIOManager::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) { + if (!gInstance) return 0; + + return gInstance->SizeOfExcludingThisInternal(mallocSizeOf); +} + +// static +size_t CacheFileIOManager::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) { + return mallocSizeOf(gInstance) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileIOManager.h b/netwerk/cache2/CacheFileIOManager.h new file mode 100644 index 0000000000..62533fb668 --- /dev/null +++ b/netwerk/cache2/CacheFileIOManager.h @@ -0,0 +1,490 @@ +/* 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/. */ + +#ifndef CacheFileIOManager__h__ +#define CacheFileIOManager__h__ + +#include "CacheIOThread.h" +#include "CacheStorageService.h" +#include "CacheHashUtils.h" +#include "nsIEventTarget.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "mozilla/Atomics.h" +#include "mozilla/SHA1.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsTArray.h" +#include "nsString.h" +#include "nsTHashtable.h" +#include "prio.h" + +// #define DEBUG_HANDLES 1 + +class nsIFile; +class nsITimer; +class nsIDirectoryEnumerator; +class nsILoadContextInfo; + +namespace mozilla { +namespace net { + +class CacheFile; +class CacheFileIOListener; + +#ifdef DEBUG_HANDLES +class CacheFileHandlesEntry; +#endif + +#define ENTRIES_DIR "entries" +#define DOOMED_DIR "doomed" +#define TRASH_DIR "trash" + +class CacheFileHandle final : public nsISupports { + public: + enum class PinningStatus : uint32_t { UNKNOWN, NON_PINNED, PINNED }; + + NS_DECL_THREADSAFE_ISUPPORTS + bool DispatchRelease(); + + CacheFileHandle(const SHA1Sum::Hash* aHash, bool aPriority, + PinningStatus aPinning); + CacheFileHandle(const nsACString& aKey, bool aPriority, + PinningStatus aPinning); + void Log(); + bool IsDoomed() const { return mIsDoomed; } + const SHA1Sum::Hash* Hash() const { return mHash; } + int64_t FileSize() const { return mFileSize; } + uint32_t FileSizeInK() const; + bool IsPriority() const { return mPriority; } + bool FileExists() const { return mFileExists; } + bool IsClosed() const { return mClosed; } + bool IsSpecialFile() const { return mSpecialFile; } + nsCString& Key() { return mKey; } + + // Returns false when this handle has been doomed based on the pinning state + // update. + bool SetPinned(bool aPinned); + void SetInvalid() { mInvalid = true; } + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + friend class CacheFileIOManager; + friend class CacheFileHandles; + friend class ReleaseNSPRHandleEvent; + + virtual ~CacheFileHandle(); + + const SHA1Sum::Hash* mHash; + mozilla::Atomic mIsDoomed; + mozilla::Atomic mClosed; + + // mPriority and mSpecialFile are plain "bool", not "bool:1", so as to + // avoid bitfield races with the byte containing mInvalid et al. See + // bug 1278502. + bool const mPriority; + bool const mSpecialFile; + + mozilla::Atomic mInvalid; + + // These bit flags are all accessed only on the IO thread + bool mFileExists : 1; // This means that the file should exists, + // but it can be still deleted by OS/user + // and then a subsequent OpenNSPRFileDesc() + // will fail. + + // Both initially false. Can be raised to true only when this handle is to be + // doomed during the period when the pinning status is unknown. After the + // pinning status determination we check these flags and possibly doom. These + // flags are only accessed on the IO thread. + bool mDoomWhenFoundPinned : 1; + bool mDoomWhenFoundNonPinned : 1; + // Set when after shutdown AND: + // - when writing: writing data (not metadata) OR the physical file handle is + // not currently open + // - when truncating: the physical file handle is not currently open + // When set it prevents any further writes or truncates on such handles to + // happen immediately after shutdown and gives a chance to write metadata of + // already open files quickly as possible (only that renders them actually + // usable by the cache.) + bool mKilled : 1; + // For existing files this is always pre-set to UNKNOWN. The status is + // udpated accordingly after the matadata has been parsed. For new files the + // flag is set according to which storage kind is opening the cache entry and + // remains so for the handle's lifetime. The status can only change from + // UNKNOWN (if set so initially) to one of PINNED or NON_PINNED and it stays + // unchanged afterwards. This status is only accessed on the IO thread. + PinningStatus mPinning; + + nsCOMPtr mFile; + + // file size is atomic because it is used on main thread by + // nsHttpChannel::ReportNetVSCacheTelemetry() + Atomic mFileSize; + PRFileDesc* mFD; // if null then the file doesn't exists on the disk + nsCString mKey; +}; + +class CacheFileHandles { + public: + CacheFileHandles(); + ~CacheFileHandles(); + + nsresult GetHandle(const SHA1Sum::Hash* aHash, CacheFileHandle** _retval); + already_AddRefed NewHandle(const SHA1Sum::Hash*, + bool aPriority, + CacheFileHandle::PinningStatus); + void RemoveHandle(CacheFileHandle* aHandle); + void GetAllHandles(nsTArray >* _retval); + void GetActiveHandles(nsTArray >* _retval); + void ClearAll(); + uint32_t HandleCount(); + +#ifdef DEBUG_HANDLES + void Log(CacheFileHandlesEntry* entry); +#endif + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + class HandleHashKey : public PLDHashEntryHdr { + public: + using KeyType = const SHA1Sum::Hash&; + using KeyTypePointer = const SHA1Sum::Hash*; + + explicit HandleHashKey(KeyTypePointer aKey) { + MOZ_COUNT_CTOR(HandleHashKey); + mHash = MakeUnique(SHA1Sum::kHashSize); + memcpy(mHash.get(), aKey, sizeof(SHA1Sum::Hash)); + } + HandleHashKey(const HandleHashKey& aOther) { + MOZ_ASSERT_UNREACHABLE("HandleHashKey copy constructor is forbidden!"); + } + MOZ_COUNTED_DTOR(HandleHashKey) + + bool KeyEquals(KeyTypePointer aKey) const { + return memcmp(mHash.get(), aKey, sizeof(SHA1Sum::Hash)) == 0; + } + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return (reinterpret_cast(aKey))[0]; + } + + void AddHandle(CacheFileHandle* aHandle); + void RemoveHandle(CacheFileHandle* aHandle); + already_AddRefed GetNewestHandle(); + void GetHandles(nsTArray >& aResult); + + SHA1Sum::Hash* Hash() const { + return reinterpret_cast(mHash.get()); + } + bool IsEmpty() const { return mHandles.Length() == 0; } + + enum { ALLOW_MEMMOVE = true }; + +#ifdef DEBUG + void AssertHandlesState(); +#endif + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + // We can't make this UniquePtr, because you can't have + // UniquePtrs with known bounds. So we settle for this representation + // and using appropriate casts when we need to access it as a + // SHA1Sum::Hash. + UniquePtr mHash; + // Use weak pointers since the hash table access is on a single thread + // only and CacheFileHandle removes itself from this table in its dtor + // that may only be called on the same thread as we work with the hashtable + // since we dispatch its Release() to this thread. + nsTArray mHandles; + }; + + private: + nsTHashtable mTable; +}; + +//////////////////////////////////////////////////////////////////////////////// + +class OpenFileEvent; +class ReadEvent; +class WriteEvent; +class MetadataWriteScheduleEvent; +class CacheFileContextEvictor; + +#define CACHEFILEIOLISTENER_IID \ + { /* dcaf2ddc-17cf-4242-bca1-8c86936375a5 */ \ + 0xdcaf2ddc, 0x17cf, 0x4242, { \ + 0xbc, 0xa1, 0x8c, 0x86, 0x93, 0x63, 0x75, 0xa5 \ + } \ + } + +class CacheFileIOListener : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(CACHEFILEIOLISTENER_IID) + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) = 0; + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) = 0; + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) = 0; + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) = 0; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) = 0; + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) = 0; + + virtual bool IsKilled() { return false; } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(CacheFileIOListener, CACHEFILEIOLISTENER_IID) + +class CacheFileIOManager final : public nsITimerCallback, public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSINAMED + + enum { + OPEN = 0U, + CREATE = 1U, + CREATE_NEW = 2U, + PRIORITY = 4U, + SPECIAL_FILE = 8U, + PINNED = 16U + }; + + CacheFileIOManager(); + + static nsresult Init(); + static nsresult Shutdown(); + static nsresult OnProfile(); + static nsresult OnDelayedStartupFinished(); + static already_AddRefed IOTarget(); + static already_AddRefed IOThread(); + static bool IsOnIOThread(); + static bool IsOnIOThreadOrCeased(); + static bool IsShutdown(); + + // Make aFile's WriteMetadataIfNeeded be called automatically after + // a short interval. + static nsresult ScheduleMetadataWrite(CacheFile* aFile); + // Remove aFile from the scheduling registry array. + // WriteMetadataIfNeeded will not be automatically called. + static nsresult UnscheduleMetadataWrite(CacheFile* aFile); + // Shuts the scheduling off and flushes all pending metadata writes. + static nsresult ShutdownMetadataWriteScheduling(); + + static nsresult OpenFile(const nsACString& aKey, uint32_t aFlags, + CacheFileIOListener* aCallback); + static nsresult Read(CacheFileHandle* aHandle, int64_t aOffset, char* aBuf, + int32_t aCount, CacheFileIOListener* aCallback); + static nsresult Write(CacheFileHandle* aHandle, int64_t aOffset, + const char* aBuf, int32_t aCount, bool aValidate, + bool aTruncate, CacheFileIOListener* aCallback); + // PinningDoomRestriction: + // NO_RESTRICTION + // no restriction is checked, the file is simply always doomed + // DOOM_WHEN_(NON)_PINNED, we branch based on the pinning status of the + // handle: + // UNKNOWN: the handle is marked to be doomed when later found (non)pinned + // PINNED/NON_PINNED: doom only when the restriction matches the pin status + // and the handle has not yet been required to doom during the UNKNOWN + // period + enum PinningDoomRestriction { + NO_RESTRICTION, + DOOM_WHEN_NON_PINNED, + DOOM_WHEN_PINNED + }; + static nsresult DoomFile(CacheFileHandle* aHandle, + CacheFileIOListener* aCallback); + static nsresult DoomFileByKey(const nsACString& aKey, + CacheFileIOListener* aCallback); + static nsresult ReleaseNSPRHandle(CacheFileHandle* aHandle); + static nsresult TruncateSeekSetEOF(CacheFileHandle* aHandle, + int64_t aTruncatePos, int64_t aEOFPos, + CacheFileIOListener* aCallback); + static nsresult RenameFile(CacheFileHandle* aHandle, + const nsACString& aNewName, + CacheFileIOListener* aCallback); + static nsresult EvictIfOverLimit(); + static nsresult EvictAll(); + static nsresult EvictByContext(nsILoadContextInfo* aLoadContextInfo, + bool aPinned, const nsAString& aOrigin, + const nsAString& aBaseDomain = u""_ns); + + static nsresult InitIndexEntry(CacheFileHandle* aHandle, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous, bool aPinning); + static nsresult UpdateIndexEntry(CacheFileHandle* aHandle, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType); + + static nsresult UpdateIndexEntry(); + + enum EEnumerateMode { ENTRIES, DOOMED }; + + static void GetCacheDirectory(nsIFile** result); +#if defined(MOZ_WIDGET_ANDROID) + static void GetProfilelessCacheDirectory(nsIFile** result); +#endif + + // Calls synchronously OnEntryInfo for an entry with the given hash. + // Tries to find an existing entry in the service hashtables first, if not + // found, loads synchronously from disk file. + // Callable on the IO thread only. + static nsresult GetEntryInfo( + const SHA1Sum::Hash* aHash, + CacheStorageService::EntryInfoCallback* aCallback); + + // Memory reporting + static size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); + static size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf); + + private: + friend class CacheFileHandle; + friend class CacheFileChunk; + friend class CacheFile; + friend class ShutdownEvent; + friend class OpenFileEvent; + friend class CloseHandleEvent; + friend class ReadEvent; + friend class WriteEvent; + friend class DoomFileEvent; + friend class DoomFileByKeyEvent; + friend class ReleaseNSPRHandleEvent; + friend class TruncateSeekSetEOFEvent; + friend class RenameFileEvent; + friend class CacheIndex; + friend class MetadataWriteScheduleEvent; + friend class CacheFileContextEvictor; + + virtual ~CacheFileIOManager(); + + nsresult InitInternal(); + void ShutdownInternal(); + + nsresult OpenFileInternal(const SHA1Sum::Hash* aHash, const nsACString& aKey, + uint32_t aFlags, CacheFileHandle** _retval); + nsresult OpenSpecialFileInternal(const nsACString& aKey, uint32_t aFlags, + CacheFileHandle** _retval); + void CloseHandleInternal(CacheFileHandle* aHandle); + nsresult ReadInternal(CacheFileHandle* aHandle, int64_t aOffset, char* aBuf, + int32_t aCount); + nsresult WriteInternal(CacheFileHandle* aHandle, int64_t aOffset, + const char* aBuf, int32_t aCount, bool aValidate, + bool aTruncate); + nsresult DoomFileInternal( + CacheFileHandle* aHandle, + PinningDoomRestriction aPinningDoomRestriction = NO_RESTRICTION); + nsresult DoomFileByKeyInternal(const SHA1Sum::Hash* aHash); + nsresult MaybeReleaseNSPRHandleInternal(CacheFileHandle* aHandle, + bool aIgnoreShutdownLag = false); + nsresult TruncateSeekSetEOFInternal(CacheFileHandle* aHandle, + int64_t aTruncatePos, int64_t aEOFPos); + nsresult RenameFileInternal(CacheFileHandle* aHandle, + const nsACString& aNewName); + nsresult EvictIfOverLimitInternal(); + nsresult OverLimitEvictionInternal(); + nsresult EvictAllInternal(); + nsresult EvictByContextInternal(nsILoadContextInfo* aLoadContextInfo, + bool aPinned, const nsAString& aOrigin, + const nsAString& aBaseDomain = u""_ns); + + nsresult TrashDirectory(nsIFile* aFile); + static void OnTrashTimer(nsITimer* aTimer, void* aClosure); + nsresult StartRemovingTrash(); + nsresult RemoveTrashInternal(); + nsresult FindTrashDirToRemove(); + + nsresult CreateFile(CacheFileHandle* aHandle); + static void HashToStr(const SHA1Sum::Hash* aHash, nsACString& _retval); + static nsresult StrToHash(const nsACString& aHash, SHA1Sum::Hash* _retval); + nsresult GetFile(const SHA1Sum::Hash* aHash, nsIFile** _retval); + nsresult GetSpecialFile(const nsACString& aKey, nsIFile** _retval); + nsresult GetDoomedFile(nsIFile** _retval); + nsresult IsEmptyDirectory(nsIFile* aFile, bool* _retval); + nsresult CheckAndCreateDir(nsIFile* aFile, const char* aDir, + bool aEnsureEmptyDir); + nsresult CreateCacheTree(); + nsresult OpenNSPRHandle(CacheFileHandle* aHandle, bool aCreate = false); + void NSPRHandleUsed(CacheFileHandle* aHandle); + + // Removing all cache files during shutdown + nsresult SyncRemoveDir(nsIFile* aFile, const char* aDir); + void SyncRemoveAllCacheFiles(); + + nsresult ScheduleMetadataWriteInternal(CacheFile* aFile); + void UnscheduleMetadataWriteInternal(CacheFile* aFile); + void ShutdownMetadataWriteSchedulingInternal(); + + static nsresult CacheIndexStateChanged(); + void CacheIndexStateChangedInternal(); + + // Dispatches a purgeHTTP background task to delete the cache directoy + // indicated by aCacheDirName. + // When this feature is enabled, a task will be dispatched at shutdown + // or after browser startup (to cleanup potential left-over directories) + nsresult DispatchPurgeTask(const nsCString& aCacheDirName, + const nsCString& aSecondsToWait, + const nsCString& aPurgeExtension); + + // Smart size calculation. UpdateSmartCacheSize() must be called on IO thread. + // It is called in EvictIfOverLimitInternal() just before we decide whether to + // start overlimit eviction or not and also in OverLimitEvictionInternal() + // before we start an eviction loop. + nsresult UpdateSmartCacheSize(int64_t aFreeSpace); + + // Memory reporting (private part) + size_t SizeOfExcludingThisInternal(mozilla::MallocSizeOf mallocSizeOf) const; + + static StaticRefPtr gInstance; + + TimeStamp mStartTime; + // Set true on the IO thread, CLOSE level as part of the internal shutdown + // procedure. + bool mShuttingDown{false}; + RefPtr mIOThread; + nsCOMPtr mCacheDirectory; +#if defined(MOZ_WIDGET_ANDROID) + // On Android we add the active profile directory name between the path + // and the 'cache2' leaf name. However, to delete any leftover data from + // times before we were doing it, we still need to access the directory + // w/o the profile name in the path. Here it is stored. + nsCOMPtr mCacheProfilelessDirectory; +#endif + bool mTreeCreated{false}; + bool mTreeCreationFailed{false}; + CacheFileHandles mHandles; + nsTArray mHandlesByLastUsed; + nsTArray mSpecialHandles; + nsTArray > mScheduledMetadataWrites; + nsCOMPtr mMetadataWritesTimer; + bool mOverLimitEvicting{false}; + // When overlimit eviction is too slow and cache size reaches 105% of the + // limit, this flag is set and no other content is cached to prevent + // uncontrolled cache growing. + bool mCacheSizeOnHardLimit{false}; + bool mRemovingTrashDirs{false}; + nsCOMPtr mTrashTimer; + nsCOMPtr mTrashDir; + nsCOMPtr mTrashDirEnumerator; + nsTArray mFailedTrashDirs; + RefPtr mContextEvictor; + TimeStamp mLastSmartSizeTime; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileInputStream.cpp b/netwerk/cache2/CacheFileInputStream.cpp new file mode 100644 index 0000000000..99302baf54 --- /dev/null +++ b/netwerk/cache2/CacheFileInputStream.cpp @@ -0,0 +1,734 @@ +/* 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 "CacheLog.h" +#include "CacheFileInputStream.h" + +#include "CacheFile.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include + +namespace mozilla::net { + +NS_IMPL_ADDREF(CacheFileInputStream) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileInputStream::Release() { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "CacheFileInputStream"); + + if (0 == count) { + mRefCnt = 1; + delete (this); + return 0; + } + + if (count == 1) { + CacheFileAutoLock lock(mFile); + mFile->RemoveInput(this, mStatus); + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(CacheFileInputStream) + NS_INTERFACE_MAP_ENTRY(nsIInputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncInputStream) + NS_INTERFACE_MAP_ENTRY(nsISeekableStream) + NS_INTERFACE_MAP_ENTRY(nsITellableStream) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileChunkListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END + +CacheFileInputStream::CacheFileInputStream(CacheFile* aFile, + nsISupports* aEntry, + bool aAlternativeData) + : mFile(aFile), + mPos(0), + mStatus(NS_OK), + mClosed(false), + mInReadSegments(false), + mWaitingForUpdate(false), + mAlternativeData(aAlternativeData), + mListeningForChunk(-1), + mCallbackFlags(0), + mCacheEntryHandle(aEntry) { + LOG(("CacheFileInputStream::CacheFileInputStream() [this=%p]", this)); + + if (mAlternativeData) { + mPos = mFile->mAltDataOffset; + } +} + +CacheFileInputStream::~CacheFileInputStream() { + LOG(("CacheFileInputStream::~CacheFileInputStream() [this=%p]", this)); + MOZ_ASSERT(!mInReadSegments); +} + +// nsIInputStream +NS_IMETHODIMP +CacheFileInputStream::Close() { + LOG(("CacheFileInputStream::Close() [this=%p]", this)); + return CloseWithStatus(NS_OK); +} + +NS_IMETHODIMP +CacheFileInputStream::Available(uint64_t* _retval) { + CacheFileAutoLock lock(mFile); + + if (mClosed) { + LOG( + ("CacheFileInputStream::Available() - Stream is closed. [this=%p, " + "status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + return NS_FAILED(mStatus) ? mStatus : NS_BASE_STREAM_CLOSED; + } + + EnsureCorrectChunk(false); + if (NS_FAILED(mStatus)) { + LOG( + ("CacheFileInputStream::Available() - EnsureCorrectChunk failed. " + "[this=%p, status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + return mStatus; + } + + nsresult rv = NS_OK; + *_retval = 0; + + if (mChunk) { + int64_t canRead = mFile->BytesFromChunk(mChunk->Index(), mAlternativeData); + canRead -= (mPos % kChunkSize); + + if (canRead > 0) { + *_retval = canRead; + } else if (canRead == 0 && !mFile->OutputStreamExists(mAlternativeData)) { + rv = NS_BASE_STREAM_CLOSED; + } + } + + LOG(("CacheFileInputStream::Available() [this=%p, retval=%" PRIu64 + ", rv=0x%08" PRIx32 "]", + this, *_retval, static_cast(rv))); + + return rv; +} + +NS_IMETHODIMP +CacheFileInputStream::StreamStatus() { + CacheFileAutoLock lock(mFile); + + if (mClosed) { + LOG( + ("CacheFileInputStream::StreamStatus() - Stream is closed. [this=%p, " + "status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + return NS_FAILED(mStatus) ? mStatus : NS_BASE_STREAM_CLOSED; + } + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileInputStream::Read(char* aBuf, uint32_t aCount, uint32_t* _retval) { + LOG(("CacheFileInputStream::Read() [this=%p, count=%d]", this, aCount)); + return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, _retval); +} + +NS_IMETHODIMP +CacheFileInputStream::ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileInputStream::ReadSegments() [this=%p, count=%d]", this, + aCount)); + + nsresult rv = NS_OK; + + *_retval = 0; + + if (mInReadSegments) { + LOG( + ("CacheFileInputStream::ReadSegments() - Cannot be called while the " + "stream is in ReadSegments!")); + return NS_ERROR_UNEXPECTED; + } + + if (mClosed) { + LOG( + ("CacheFileInputStream::ReadSegments() - Stream is closed. [this=%p, " + "status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + return NS_OK; + } + + if (aCount == 0) { + return NS_OK; + } + + EnsureCorrectChunk(false); + + while (true) { + if (NS_FAILED(mStatus)) return mStatus; + + if (!mChunk) { + if (mListeningForChunk == -1) { + return NS_OK; + } + return NS_BASE_STREAM_WOULD_BLOCK; + } + + CacheFileChunkReadHandle hnd = mChunk->GetReadHandle(); + int64_t canRead = CanRead(&hnd); + if (NS_FAILED(mStatus)) { + return mStatus; + } + + if (canRead < 0) { + // file was truncated ??? + MOZ_ASSERT(false, "SetEOF is currenty not implemented?!"); + rv = NS_OK; + } else if (canRead > 0) { + uint32_t toRead = std::min(static_cast(canRead), aCount); + uint32_t read; + const char* buf = hnd.Buf() + (mPos - hnd.Offset()); + + mInReadSegments = true; + lock.Unlock(); + + rv = aWriter(this, aClosure, buf, *_retval, toRead, &read); + + lock.Lock(); + mInReadSegments = false; + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(read <= toRead, + "writer should not write more than we asked it to write"); + + *_retval += read; + mPos += read; + aCount -= read; + + if (!mClosed) { + // The last chunk is released after the caller closes this stream. + EnsureCorrectChunk(false); + + if (mChunk && aCount) { + // Check whether there is more data available to read. + continue; + } + } + } + + if (mClosed) { + // The stream was closed from aWriter, do the cleanup. + CleanUp(); + } + + rv = NS_OK; + } else { + if (*_retval == 0 && mFile->OutputStreamExists(mAlternativeData)) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } else { + rv = NS_OK; + } + } + + break; + } + + LOG(("CacheFileInputStream::ReadSegments() [this=%p, rv=0x%08" PRIx32 + ", retval=%d]", + this, static_cast(rv), *_retval)); + + return rv; +} + +NS_IMETHODIMP +CacheFileInputStream::IsNonBlocking(bool* _retval) { + *_retval = true; + return NS_OK; +} + +// nsIAsyncInputStream +NS_IMETHODIMP +CacheFileInputStream::CloseWithStatus(nsresult aStatus) { + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileInputStream::CloseWithStatus() [this=%p, aStatus=0x%08" PRIx32 + "]", + this, static_cast(aStatus))); + + CloseWithStatusLocked(aStatus); + return NS_OK; +} + +void CacheFileInputStream::CloseWithStatusLocked(nsresult aStatus) { + LOG( + ("CacheFileInputStream::CloseWithStatusLocked() [this=%p, " + "aStatus=0x%08" PRIx32 "]", + this, static_cast(aStatus))); + + if (mClosed) { + // We notify listener and null out mCallback immediately after closing + // the stream. If we're in ReadSegments we postpone notification until we + // step out from ReadSegments. So if the stream is already closed the + // following assertion must be true. + MOZ_ASSERT(!mCallback || mInReadSegments); + return; + } + + mClosed = true; + mStatus = NS_FAILED(aStatus) ? aStatus : NS_BASE_STREAM_CLOSED; + + if (!mInReadSegments) { + CleanUp(); + } +} + +void CacheFileInputStream::CleanUp() { + MOZ_ASSERT(!mInReadSegments); + MOZ_ASSERT(mClosed); + + if (mChunk) { + ReleaseChunk(); + } + + // TODO propagate error from input stream to other streams ??? + + MaybeNotifyListener(); + + mFile->ReleaseOutsideLock(std::move(mCacheEntryHandle)); +} + +NS_IMETHODIMP +CacheFileInputStream::AsyncWait(nsIInputStreamCallback* aCallback, + uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + CacheFileAutoLock lock(mFile); + + LOG( + ("CacheFileInputStream::AsyncWait() [this=%p, callback=%p, flags=%d, " + "requestedCount=%d, eventTarget=%p]", + this, aCallback, aFlags, aRequestedCount, aEventTarget)); + + if (mInReadSegments) { + LOG( + ("CacheFileInputStream::AsyncWait() - Cannot be called while the stream" + " is in ReadSegments!")); + MOZ_ASSERT(false, + "Unexpected call. If it's a valid usage implement it. " + "Otherwise fix the caller."); + return NS_ERROR_UNEXPECTED; + } + + mCallback = aCallback; + mCallbackFlags = aFlags; + mCallbackTarget = aEventTarget; + + if (!mCallback) { + if (mWaitingForUpdate) { + mChunk->CancelWait(this); + mWaitingForUpdate = false; + } + return NS_OK; + } + + if (mClosed) { + NotifyListener(); + return NS_OK; + } + + EnsureCorrectChunk(false); + + MaybeNotifyListener(); + + return NS_OK; +} + +// nsISeekableStream +NS_IMETHODIMP +CacheFileInputStream::Seek(int32_t whence, int64_t offset) { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + LOG(("CacheFileInputStream::Seek() [this=%p, whence=%d, offset=%" PRId64 "]", + this, whence, offset)); + + if (mInReadSegments) { + LOG( + ("CacheFileInputStream::Seek() - Cannot be called while the stream is " + "in ReadSegments!")); + return NS_ERROR_UNEXPECTED; + } + + if (mClosed) { + LOG(("CacheFileInputStream::Seek() - Stream is closed. [this=%p]", this)); + return NS_BASE_STREAM_CLOSED; + } + + int64_t newPos = offset; + switch (whence) { + case NS_SEEK_SET: + if (mAlternativeData) { + newPos += mFile->mAltDataOffset; + } + break; + case NS_SEEK_CUR: + newPos += mPos; + break; + case NS_SEEK_END: + if (mAlternativeData) { + newPos += mFile->mDataSize; + } else { + newPos += mFile->mAltDataOffset; + } + break; + default: + NS_ERROR("invalid whence"); + return NS_ERROR_INVALID_ARG; + } + mPos = newPos; + EnsureCorrectChunk(false); + + LOG(("CacheFileInputStream::Seek() [this=%p, pos=%" PRId64 "]", this, mPos)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileInputStream::SetEOF() { + MOZ_ASSERT(false, "Don't call SetEOF on cache input stream"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +// nsITellableStream +NS_IMETHODIMP +CacheFileInputStream::Tell(int64_t* _retval) { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + if (mClosed) { + LOG(("CacheFileInputStream::Tell() - Stream is closed. [this=%p]", this)); + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mPos; + + if (mAlternativeData) { + *_retval -= mFile->mAltDataOffset; + } + + LOG(("CacheFileInputStream::Tell() [this=%p, retval=%" PRId64 "]", this, + *_retval)); + return NS_OK; +} + +// CacheFileChunkListener +nsresult CacheFileInputStream::OnChunkRead(nsresult aResult, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileInputStream::OnChunkRead should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileInputStream::OnChunkWritten(nsresult aResult, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileInputStream::OnChunkWritten should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileInputStream::OnChunkAvailable(nsresult aResult, + uint32_t aChunkIdx, + CacheFileChunk* aChunk) { + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileInputStream::OnChunkAvailable() [this=%p, result=0x%08" PRIx32 + ", " + "idx=%d, chunk=%p]", + this, static_cast(aResult), aChunkIdx, aChunk)); + + MOZ_ASSERT(mListeningForChunk != -1); + + if (mListeningForChunk != static_cast(aChunkIdx)) { + // This is not a chunk that we're waiting for + LOG( + ("CacheFileInputStream::OnChunkAvailable() - Notification is for a " + "different chunk. [this=%p, listeningForChunk=%" PRId64 "]", + this, mListeningForChunk)); + + return NS_OK; + } + + MOZ_ASSERT(!mChunk); + MOZ_ASSERT(!mWaitingForUpdate); + MOZ_ASSERT(!mInReadSegments); + mListeningForChunk = -1; + + if (mClosed) { + MOZ_ASSERT(!mCallback); + + LOG( + ("CacheFileInputStream::OnChunkAvailable() - Stream is closed, " + "ignoring notification. [this=%p]", + this)); + + return NS_OK; + } + + if (NS_SUCCEEDED(aResult)) { + mChunk = aChunk; + } else if (aResult != NS_ERROR_NOT_AVAILABLE) { + // Close the stream with error. The consumer will receive this error later + // in Read(), Available() etc. We need to handle NS_ERROR_NOT_AVAILABLE + // differently since it is returned when the requested chunk is not + // available and there is no writer that could create it, i.e. it means that + // we've reached the end of the file. + CloseWithStatusLocked(aResult); + + return NS_OK; + } + + MaybeNotifyListener(); + + return NS_OK; +} + +nsresult CacheFileInputStream::OnChunkUpdated(CacheFileChunk* aChunk) { + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileInputStream::OnChunkUpdated() [this=%p, idx=%d]", this, + aChunk->Index())); + + if (!mWaitingForUpdate) { + LOG( + ("CacheFileInputStream::OnChunkUpdated() - Ignoring notification since " + "mWaitingforUpdate == false. [this=%p]", + this)); + + return NS_OK; + } + + mWaitingForUpdate = false; + + MOZ_ASSERT(mChunk == aChunk); + + MaybeNotifyListener(); + + return NS_OK; +} + +void CacheFileInputStream::ReleaseChunk() { + mFile->AssertOwnsLock(); + + LOG(("CacheFileInputStream::ReleaseChunk() [this=%p, idx=%d]", this, + mChunk->Index())); + + MOZ_ASSERT(!mInReadSegments); + + if (mWaitingForUpdate) { + LOG( + ("CacheFileInputStream::ReleaseChunk() - Canceling waiting for update. " + "[this=%p]", + this)); + + mChunk->CancelWait(this); + mWaitingForUpdate = false; + } + + mFile->ReleaseOutsideLock(std::move(mChunk)); +} + +void CacheFileInputStream::EnsureCorrectChunk(bool aReleaseOnly) { + mFile->AssertOwnsLock(); + + LOG(("CacheFileInputStream::EnsureCorrectChunk() [this=%p, releaseOnly=%d]", + this, aReleaseOnly)); + + nsresult rv; + + uint32_t chunkIdx = mPos / kChunkSize; + + if (mInReadSegments) { + // We must have correct chunk + MOZ_ASSERT(mChunk); + MOZ_ASSERT(mChunk->Index() == chunkIdx); + return; + } + + if (mChunk) { + if (mChunk->Index() == chunkIdx) { + // we have a correct chunk + LOG( + ("CacheFileInputStream::EnsureCorrectChunk() - Have correct chunk " + "[this=%p, idx=%d]", + this, chunkIdx)); + + return; + } + ReleaseChunk(); + } + + MOZ_ASSERT(!mWaitingForUpdate); + + if (aReleaseOnly) return; + + if (mListeningForChunk == static_cast(chunkIdx)) { + // We're already waiting for this chunk + LOG( + ("CacheFileInputStream::EnsureCorrectChunk() - Already listening for " + "chunk %" PRId64 " [this=%p]", + mListeningForChunk, this)); + + return; + } + + rv = mFile->GetChunkLocked(chunkIdx, CacheFile::READER, this, + getter_AddRefs(mChunk)); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileInputStream::EnsureCorrectChunk() - GetChunkLocked failed. " + "[this=%p, idx=%d, rv=0x%08" PRIx32 "]", + this, chunkIdx, static_cast(rv))); + if (rv != NS_ERROR_NOT_AVAILABLE) { + // Close the stream with error. The consumer will receive this error later + // in Read(), Available() etc. We need to handle NS_ERROR_NOT_AVAILABLE + // differently since it is returned when the requested chunk is not + // available and there is no writer that could create it, i.e. it means + // that we've reached the end of the file. + CloseWithStatusLocked(rv); + + return; + } + } else if (!mChunk) { + mListeningForChunk = static_cast(chunkIdx); + } + + MaybeNotifyListener(); +} + +int64_t CacheFileInputStream::CanRead(CacheFileChunkReadHandle* aHandle) { + mFile->AssertOwnsLock(); + + MOZ_ASSERT(mChunk); + MOZ_ASSERT(mPos / kChunkSize == mChunk->Index()); + + int64_t retval = aHandle->Offset() + aHandle->DataSize(); + + if (!mAlternativeData && mFile->mAltDataOffset != -1 && + mFile->mAltDataOffset < retval) { + retval = mFile->mAltDataOffset; + } + + retval -= mPos; + if (retval <= 0 && NS_FAILED(mChunk->GetStatus())) { + CloseWithStatusLocked(mChunk->GetStatus()); + } + + LOG(("CacheFileInputStream::CanRead() [this=%p, canRead=%" PRId64 "]", this, + retval)); + + return retval; +} + +void CacheFileInputStream::NotifyListener() { + mFile->AssertOwnsLock(); + + LOG(("CacheFileInputStream::NotifyListener() [this=%p]", this)); + + MOZ_ASSERT(mCallback); + MOZ_ASSERT(!mInReadSegments); + + if (!mCallbackTarget) { + mCallbackTarget = CacheFileIOManager::IOTarget(); + if (!mCallbackTarget) { + LOG( + ("CacheFileInputStream::NotifyListener() - Cannot get Cache I/O " + "thread! Using main thread for callback.")); + mCallbackTarget = GetMainThreadSerialEventTarget(); + } + } + + nsCOMPtr asyncCallback = NS_NewInputStreamReadyEvent( + "CacheFileInputStream::NotifyListener", mCallback, mCallbackTarget); + + mCallback = nullptr; + mCallbackTarget = nullptr; + + asyncCallback->OnInputStreamReady(this); +} + +void CacheFileInputStream::MaybeNotifyListener() { + mFile->AssertOwnsLock(); + + LOG( + ("CacheFileInputStream::MaybeNotifyListener() [this=%p, mCallback=%p, " + "mClosed=%d, mStatus=0x%08" PRIx32 + ", mChunk=%p, mListeningForChunk=%" PRId64 ", " + "mWaitingForUpdate=%d]", + this, mCallback.get(), mClosed, static_cast(mStatus), + mChunk.get(), mListeningForChunk, mWaitingForUpdate)); + + MOZ_ASSERT(!mInReadSegments); + + if (!mCallback) return; + + if (mClosed || NS_FAILED(mStatus)) { + NotifyListener(); + return; + } + + if (!mChunk) { + if (mListeningForChunk == -1) { + // EOF, should we notify even if mCallbackFlags == WAIT_CLOSURE_ONLY ?? + NotifyListener(); + } + return; + } + + MOZ_ASSERT(mPos / kChunkSize == mChunk->Index()); + + if (mWaitingForUpdate) return; + + CacheFileChunkReadHandle hnd = mChunk->GetReadHandle(); + int64_t canRead = CanRead(&hnd); + if (NS_FAILED(mStatus)) { + // CanRead() called CloseWithStatusLocked() which called + // MaybeNotifyListener() so the listener was already notified. Stop here. + MOZ_ASSERT(!mCallback); + return; + } + + if (canRead > 0) { + if (!(mCallbackFlags & WAIT_CLOSURE_ONLY)) NotifyListener(); + } else if (canRead == 0) { + if (!mFile->OutputStreamExists(mAlternativeData)) { + // EOF + NotifyListener(); + } else { + mChunk->WaitForUpdate(this); + mWaitingForUpdate = true; + } + } else { + // Output have set EOF before mPos? + MOZ_ASSERT(false, "SetEOF is currenty not implemented?!"); + NotifyListener(); + } +} + +// Memory reporting + +size_t CacheFileInputStream::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + // Everything the stream keeps a reference to is already reported somewhere + // else. mFile reports itself. mChunk reported as part of CacheFile. mCallback + // is usually CacheFile or a class that is reported elsewhere. + return mallocSizeOf(this); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileInputStream.h b/netwerk/cache2/CacheFileInputStream.h new file mode 100644 index 0000000000..8280f9d224 --- /dev/null +++ b/netwerk/cache2/CacheFileInputStream.h @@ -0,0 +1,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/. */ + +#ifndef CacheFileInputStream__h__ +#define CacheFileInputStream__h__ + +#include "nsIAsyncInputStream.h" +#include "nsISeekableStream.h" +#include "nsCOMPtr.h" +#include "CacheFileChunk.h" + +namespace mozilla { +namespace net { + +class CacheFile; + +class CacheFileInputStream : public nsIAsyncInputStream, + public nsISeekableStream, + public CacheFileChunkListener { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + NS_DECL_NSISEEKABLESTREAM + NS_DECL_NSITELLABLESTREAM + + public: + explicit CacheFileInputStream(CacheFile* aFile, nsISupports* aEntry, + bool aAlternativeData); + + NS_IMETHOD OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkUpdated(CacheFileChunk* aChunk) override; + + // Memory reporting + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + uint32_t GetPosition() const { return mPos; }; + bool IsAlternativeData() const { return mAlternativeData; }; + int64_t GetChunkIdx() const { + return mChunk ? static_cast(mChunk->Index()) : -1; + }; + + private: + virtual ~CacheFileInputStream(); + + void CloseWithStatusLocked(nsresult aStatus); + void CleanUp(); + void ReleaseChunk(); + void EnsureCorrectChunk(bool aReleaseOnly); + + // CanRead returns negative value when output stream truncates the data before + // the input stream's mPos. + int64_t CanRead(CacheFileChunkReadHandle* aHandle); + void NotifyListener(); + void MaybeNotifyListener(); + + RefPtr mFile; + RefPtr mChunk; + int64_t mPos; + nsresult mStatus; + bool mClosed : 1; + bool mInReadSegments : 1; + bool mWaitingForUpdate : 1; + bool const mAlternativeData : 1; + int64_t mListeningForChunk; + + nsCOMPtr mCallback; + uint32_t mCallbackFlags; + nsCOMPtr mCallbackTarget; + // Held purely for referencing purposes + RefPtr mCacheEntryHandle; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileMetadata.cpp b/netwerk/cache2/CacheFileMetadata.cpp new file mode 100644 index 0000000000..68d55b1988 --- /dev/null +++ b/netwerk/cache2/CacheFileMetadata.cpp @@ -0,0 +1,1041 @@ +/* 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 "CacheLog.h" +#include "CacheFileMetadata.h" + +#include "CacheFileIOManager.h" +#include "nsICacheEntry.h" +#include "CacheHashUtils.h" +#include "CacheFileChunk.h" +#include "CacheFileUtils.h" +#include "nsILoadContextInfo.h" +#include "nsICacheEntry.h" // for nsICacheEntryMetaDataVisitor +#include "nsIFile.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "prnetdb.h" + +namespace mozilla::net { + +#define kMinMetadataRead 1024 // TODO find optimal value from telemetry +#define kAlignSize 4096 + +// Most of the cache entries fit into one chunk due to current chunk size. Make +// sure to tweak this value if kChunkSize is going to change. +#define kInitialHashArraySize 1 + +// Initial elements buffer size. +#define kInitialBufSize 64 + +// Max size of elements in bytes. +#define kMaxElementsSize (64 * 1024) + +#define NOW_SECONDS() (uint32_t(PR_Now() / PR_USEC_PER_SEC)) + +NS_IMPL_ISUPPORTS(CacheFileMetadata, CacheFileIOListener) + +CacheFileMetadata::CacheFileMetadata( + CacheFileHandle* aHandle, const nsACString& aKey, + NotNull aLock) + : CacheMemoryConsumer(NORMAL), + mHandle(aHandle), + mOffset(-1), + mIsDirty(false), + mAnonymous(false), + mAllocExactSize(false), + mFirstRead(true), + mLock(aLock) { + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p, handle=%p, key=%s]", + this, aHandle, PromiseFlatCString(aKey).get())); + + memset(&mMetaHdr, 0, sizeof(CacheFileMetadataHeader)); + mMetaHdr.mVersion = kCacheEntryVersion; + mMetaHdr.mExpirationTime = nsICacheEntry::NO_EXPIRATION_TIME; + mKey = aKey; + + DebugOnly rv{}; + rv = ParseKey(aKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +CacheFileMetadata::CacheFileMetadata( + bool aMemoryOnly, bool aPinned, const nsACString& aKey, + NotNull aLock) + : CacheMemoryConsumer(aMemoryOnly ? MEMORY_ONLY : NORMAL), + mIsDirty(true), + mAnonymous(false), + mAllocExactSize(false), + mFirstRead(true), + mLock(aLock) { + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p, key=%s]", this, + PromiseFlatCString(aKey).get())); + + memset(&mMetaHdr, 0, sizeof(CacheFileMetadataHeader)); + mMetaHdr.mVersion = kCacheEntryVersion; + if (aPinned) { + AddFlags(kCacheEntryIsPinned); + } + mMetaHdr.mExpirationTime = nsICacheEntry::NO_EXPIRATION_TIME; + mKey = aKey; + mMetaHdr.mKeySize = mKey.Length(); + + DebugOnly rv{}; + rv = ParseKey(aKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +CacheFileMetadata::CacheFileMetadata() + : CacheMemoryConsumer(DONT_REPORT /* This is a helper class */), + mIsDirty(false), + mAnonymous(false), + mAllocExactSize(false), + mFirstRead(true), + mLock(new CacheFileUtils::CacheFileLock()) { + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p]", this)); + + memset(&mMetaHdr, 0, sizeof(CacheFileMetadataHeader)); +} + +CacheFileMetadata::~CacheFileMetadata() { + LOG(("CacheFileMetadata::~CacheFileMetadata() [this=%p]", this)); + + MOZ_ASSERT(!mListener); + + if (mHashArray) { + CacheFileUtils::FreeBuffer(mHashArray); + mHashArray = nullptr; + mHashArraySize = 0; + } + + if (mBuf) { + CacheFileUtils::FreeBuffer(mBuf); + mBuf = nullptr; + mBufSize = 0; + } +} + +void CacheFileMetadata::SetHandle(CacheFileHandle* aHandle) { + LOG(("CacheFileMetadata::SetHandle() [this=%p, handle=%p]", this, aHandle)); + + MOZ_ASSERT(!mHandle); + + mHandle = aHandle; +} + +void CacheFileMetadata::ReadMetadata(CacheFileMetadataListener* aListener) { + LOG(("CacheFileMetadata::ReadMetadata() [this=%p, listener=%p]", this, + aListener)); + + MOZ_ASSERT(!mListener); + MOZ_ASSERT(!mHashArray); + MOZ_ASSERT(!mBuf); + MOZ_ASSERT(!mWriteBuf); + + nsresult rv; + + int64_t size = mHandle->FileSize(); + MOZ_ASSERT(size != -1); + + if (size == 0) { + // this is a new entry + LOG( + ("CacheFileMetadata::ReadMetadata() - Filesize == 0, creating empty " + "metadata. [this=%p]", + this)); + + InitEmptyMetadata(); + aListener->OnMetadataRead(NS_OK); + return; + } + + if (size < int64_t(sizeof(CacheFileMetadataHeader) + 2 * sizeof(uint32_t))) { + // there must be at least checksum, header and offset + LOG( + ("CacheFileMetadata::ReadMetadata() - File is corrupted, creating " + "empty metadata. [this=%p, filesize=%" PRId64 "]", + this, size)); + + InitEmptyMetadata(); + aListener->OnMetadataRead(NS_OK); + return; + } + + // Set offset so that we read at least kMinMetadataRead if the file is big + // enough. + int64_t offset; + if (size < kMinMetadataRead) { + offset = 0; + } else { + offset = size - kMinMetadataRead; + } + + // round offset to kAlignSize blocks + offset = (offset / kAlignSize) * kAlignSize; + + mBufSize = size - offset; + mBuf = static_cast(moz_xmalloc(mBufSize)); + + DoMemoryReport(MemoryUsage()); + + LOG( + ("CacheFileMetadata::ReadMetadata() - Reading metadata from disk, trying " + "offset=%" PRId64 ", filesize=%" PRId64 " [this=%p]", + offset, size, this)); + + mReadStart = mozilla::TimeStamp::Now(); + mListener = aListener; + rv = CacheFileIOManager::Read(mHandle, offset, mBuf, mBufSize, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileMetadata::ReadMetadata() - CacheFileIOManager::Read() failed" + " synchronously, creating empty metadata. [this=%p, rv=0x%08" PRIx32 + "]", + this, static_cast(rv))); + + mListener = nullptr; + InitEmptyMetadata(); + aListener->OnMetadataRead(NS_OK); + } +} + +uint32_t CacheFileMetadata::CalcMetadataSize(uint32_t aElementsSize, + uint32_t aHashCount) { + return sizeof(uint32_t) + // hash of the metadata + aHashCount * sizeof(CacheHash::Hash16_t) + // array of chunk hashes + sizeof(CacheFileMetadataHeader) + // metadata header + mKey.Length() + 1 + // key with trailing null + aElementsSize + // elements + sizeof(uint32_t); // offset +} + +nsresult CacheFileMetadata::WriteMetadata( + uint32_t aOffset, CacheFileMetadataListener* aListener) { + LOG(("CacheFileMetadata::WriteMetadata() [this=%p, offset=%d, listener=%p]", + this, aOffset, aListener)); + + MOZ_ASSERT(!mListener); + MOZ_ASSERT(!mWriteBuf); + + nsresult rv; + + mIsDirty = false; + + mWriteBuf = + static_cast(malloc(CalcMetadataSize(mElementsSize, mHashCount))); + if (!mWriteBuf) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char* p = mWriteBuf + sizeof(uint32_t); + if (mHashCount) { + memcpy(p, mHashArray, mHashCount * sizeof(CacheHash::Hash16_t)); + p += mHashCount * sizeof(CacheHash::Hash16_t); + } + mMetaHdr.WriteToBuf(p); + p += sizeof(CacheFileMetadataHeader); + memcpy(p, mKey.get(), mKey.Length()); + p += mKey.Length(); + *p = 0; + p++; + if (mElementsSize) { + memcpy(p, mBuf, mElementsSize); + p += mElementsSize; + } + + CacheHash::Hash32_t hash; + hash = CacheHash::Hash(mWriteBuf + sizeof(uint32_t), + p - mWriteBuf - sizeof(uint32_t)); + NetworkEndian::writeUint32(mWriteBuf, hash); + + NetworkEndian::writeUint32(p, aOffset); + p += sizeof(uint32_t); + + char* writeBuffer = mWriteBuf; + if (aListener) { + mListener = aListener; + } else { + // We are not going to pass |this| as a callback so the buffer will be + // released by CacheFileIOManager. Just null out mWriteBuf here. + mWriteBuf = nullptr; + } + + rv = CacheFileIOManager::Write(mHandle, aOffset, writeBuffer, p - writeBuffer, + true, true, aListener ? this : nullptr); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileMetadata::WriteMetadata() - CacheFileIOManager::Write() " + "failed synchronously. [this=%p, rv=0x%08" PRIx32 "]", + this, static_cast(rv))); + + mListener = nullptr; + if (mWriteBuf) { + CacheFileUtils::FreeBuffer(mWriteBuf); + mWriteBuf = nullptr; + } + NS_ENSURE_SUCCESS(rv, rv); + } + + DoMemoryReport(MemoryUsage()); + + return NS_OK; +} + +nsresult CacheFileMetadata::SyncReadMetadata(nsIFile* aFile) { + LOG(("CacheFileMetadata::SyncReadMetadata() [this=%p]", this)); + + MOZ_ASSERT(!mListener); + MOZ_ASSERT(!mHandle); + MOZ_ASSERT(!mHashArray); + MOZ_ASSERT(!mBuf); + MOZ_ASSERT(!mWriteBuf); + MOZ_ASSERT(mKey.IsEmpty()); + + nsresult rv; + + int64_t fileSize; + rv = aFile->GetFileSize(&fileSize); + if (NS_FAILED(rv)) { + // Don't bloat the console + return rv; + } + + PRFileDesc* fd; + rv = aFile->OpenNSPRFileDesc(PR_RDONLY, 0600, &fd); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t offset = PR_Seek64(fd, fileSize - sizeof(uint32_t), PR_SEEK_SET); + if (offset == -1) { + PR_Close(fd); + return NS_ERROR_FAILURE; + } + + uint32_t metaOffset; + int32_t bytesRead = PR_Read(fd, &metaOffset, sizeof(uint32_t)); + if (bytesRead != sizeof(uint32_t)) { + PR_Close(fd); + return NS_ERROR_FAILURE; + } + + metaOffset = NetworkEndian::readUint32(&metaOffset); + if (metaOffset > fileSize) { + PR_Close(fd); + return NS_ERROR_FAILURE; + } + + mBuf = static_cast(malloc(fileSize - metaOffset)); + if (!mBuf) { + return NS_ERROR_OUT_OF_MEMORY; + } + mBufSize = fileSize - metaOffset; + + DoMemoryReport(MemoryUsage()); + + offset = PR_Seek64(fd, metaOffset, PR_SEEK_SET); + if (offset == -1) { + PR_Close(fd); + return NS_ERROR_FAILURE; + } + + bytesRead = PR_Read(fd, mBuf, mBufSize); + PR_Close(fd); + if (bytesRead != static_cast(mBufSize)) { + return NS_ERROR_FAILURE; + } + + rv = ParseMetadata(metaOffset, 0, false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +const char* CacheFileMetadata::GetElement(const char* aKey) { + const char* data = mBuf; + const char* limit = mBuf + mElementsSize; + + while (data != limit) { + size_t maxLen = limit - data; + size_t keyLen = strnlen(data, maxLen); + MOZ_RELEASE_ASSERT(keyLen != maxLen, + "Metadata elements corrupted. Key " + "isn't null terminated!"); + MOZ_RELEASE_ASSERT(keyLen + 1 != maxLen, + "Metadata elements corrupted. " + "There is no value for the key!"); + + const char* value = data + keyLen + 1; + maxLen = limit - value; + size_t valueLen = strnlen(value, maxLen); + MOZ_RELEASE_ASSERT(valueLen != maxLen, + "Metadata elements corrupted. Value " + "isn't null terminated!"); + + if (strcmp(data, aKey) == 0) { + LOG(("CacheFileMetadata::GetElement() - Key found [this=%p, key=%s]", + this, aKey)); + return value; + } + + // point to next pair + data += keyLen + valueLen + 2; + } + LOG(("CacheFileMetadata::GetElement() - Key not found [this=%p, key=%s]", + this, aKey)); + return nullptr; +} + +nsresult CacheFileMetadata::SetElement(const char* aKey, const char* aValue) { + LOG(("CacheFileMetadata::SetElement() [this=%p, key=%s, value=%p]", this, + aKey, aValue)); + + mLock->Lock().AssertCurrentThreadOwns(); + + MarkDirty(); + + nsresult rv; + + const uint32_t keySize = strlen(aKey) + 1; + char* pos = const_cast(GetElement(aKey)); + + if (!aValue) { + // No value means remove the key/value pair completely, if existing + if (pos) { + uint32_t oldValueSize = strlen(pos) + 1; + uint32_t offset = pos - mBuf; + uint32_t remainder = mElementsSize - (offset + oldValueSize); + + memmove(pos - keySize, pos + oldValueSize, remainder); + mElementsSize -= keySize + oldValueSize; + } + return NS_OK; + } + + const uint32_t valueSize = strlen(aValue) + 1; + uint32_t newSize = mElementsSize + valueSize; + if (pos) { + const uint32_t oldValueSize = strlen(pos) + 1; + const uint32_t offset = pos - mBuf; + const uint32_t remainder = mElementsSize - (offset + oldValueSize); + + // Update the value in place + newSize -= oldValueSize; + rv = EnsureBuffer(newSize); + if (NS_FAILED(rv)) { + return rv; + } + + // Move the remainder to the right place + pos = mBuf + offset; + memmove(pos + valueSize, pos + oldValueSize, remainder); + } else { + // allocate new meta data element + newSize += keySize; + rv = EnsureBuffer(newSize); + if (NS_FAILED(rv)) { + return rv; + } + + // Add after last element + pos = mBuf + mElementsSize; + memcpy(pos, aKey, keySize); + pos += keySize; + } + + // Update value + memcpy(pos, aValue, valueSize); + mElementsSize = newSize; + + return NS_OK; +} + +void CacheFileMetadata::Visit(nsICacheEntryMetaDataVisitor* aVisitor) { + const char* data = mBuf; + const char* limit = mBuf + mElementsSize; + + while (data < limit) { + // Point to the value part + const char* value = data + strlen(data) + 1; + MOZ_ASSERT(value < limit, "Metadata elements corrupted"); + + aVisitor->OnMetaDataElement(data, value); + + // Skip value part + data = value + strlen(value) + 1; + } + + MOZ_ASSERT(data == limit, "Metadata elements corrupted"); +} + +CacheHash::Hash16_t CacheFileMetadata::GetHash(uint32_t aIndex) { + mLock->Lock().AssertCurrentThreadOwns(); + + MOZ_ASSERT(aIndex < mHashCount); + return NetworkEndian::readUint16(&mHashArray[aIndex]); +} + +nsresult CacheFileMetadata::SetHash(uint32_t aIndex, + CacheHash::Hash16_t aHash) { + LOG(("CacheFileMetadata::SetHash() [this=%p, idx=%d, hash=%x]", this, aIndex, + aHash)); + + mLock->Lock().AssertCurrentThreadOwns(); + + MarkDirty(); + + MOZ_ASSERT(aIndex <= mHashCount); + + if (aIndex > mHashCount) { + return NS_ERROR_INVALID_ARG; + } + if (aIndex == mHashCount) { + if ((aIndex + 1) * sizeof(CacheHash::Hash16_t) > mHashArraySize) { + // reallocate hash array buffer + if (mHashArraySize == 0) { + mHashArraySize = kInitialHashArraySize * sizeof(CacheHash::Hash16_t); + } else { + mHashArraySize *= 2; + } + mHashArray = static_cast( + moz_xrealloc(mHashArray, mHashArraySize)); + } + + mHashCount++; + } + + NetworkEndian::writeUint16(&mHashArray[aIndex], aHash); + + DoMemoryReport(MemoryUsage()); + + return NS_OK; +} + +nsresult CacheFileMetadata::RemoveHash(uint32_t aIndex) { + LOG(("CacheFileMetadata::RemoveHash() [this=%p, idx=%d]", this, aIndex)); + + mLock->Lock().AssertCurrentThreadOwns(); + + MarkDirty(); + + MOZ_ASSERT((aIndex + 1) == mHashCount, "Can remove only last hash!"); + + if (aIndex + 1 != mHashCount) { + return NS_ERROR_INVALID_ARG; + } + + mHashCount--; + return NS_OK; +} + +void CacheFileMetadata::AddFlags(uint32_t aFlags) { + MarkDirty(false); + mMetaHdr.mFlags |= aFlags; +} + +void CacheFileMetadata::RemoveFlags(uint32_t aFlags) { + MarkDirty(false); + mMetaHdr.mFlags &= ~aFlags; +} + +void CacheFileMetadata::SetExpirationTime(uint32_t aExpirationTime) { + LOG(("CacheFileMetadata::SetExpirationTime() [this=%p, expirationTime=%d]", + this, aExpirationTime)); + + MarkDirty(false); + mMetaHdr.mExpirationTime = aExpirationTime; +} + +void CacheFileMetadata::SetFrecency(uint32_t aFrecency) { + LOG(("CacheFileMetadata::SetFrecency() [this=%p, frecency=%f]", this, + (double)aFrecency)); + + MarkDirty(false); + mMetaHdr.mFrecency = aFrecency; +} + +void CacheFileMetadata::OnFetched() { + MarkDirty(false); + + mMetaHdr.mLastFetched = NOW_SECONDS(); + ++mMetaHdr.mFetchCount; +} + +void CacheFileMetadata::MarkDirty(bool aUpdateLastModified) { + mIsDirty = true; + if (aUpdateLastModified) { + mMetaHdr.mLastModified = NOW_SECONDS(); + } +} + +nsresult CacheFileMetadata::OnFileOpened(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileMetadata::OnFileOpened should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileMetadata::OnDataWritten(CacheFileHandle* aHandle, + const char* aBuf, nsresult aResult) { + LOG( + ("CacheFileMetadata::OnDataWritten() [this=%p, handle=%p, " + "result=0x%08" PRIx32 "]", + this, aHandle, static_cast(aResult))); + + nsCOMPtr listener; + { + MutexAutoLock lock(mLock->Lock()); + + MOZ_ASSERT(mListener); + MOZ_ASSERT(mWriteBuf); + + CacheFileUtils::FreeBuffer(mWriteBuf); + mWriteBuf = nullptr; + + mListener.swap(listener); + DoMemoryReport(MemoryUsage()); + } + + listener->OnMetadataWritten(aResult); + + return NS_OK; +} + +nsresult CacheFileMetadata::OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) { + LOG(( + "CacheFileMetadata::OnDataRead() [this=%p, handle=%p, result=0x%08" PRIx32 + "]", + this, aHandle, static_cast(aResult))); + + MOZ_ASSERT(mListener); + + nsresult rv; + nsCOMPtr listener; + + auto notifyListenerOutsideLock = mozilla::MakeScopeExit([&listener] { + if (listener) { + listener->OnMetadataRead(NS_OK); + } + }); + + MutexAutoLock lock(mLock->Lock()); + + if (NS_FAILED(aResult)) { + LOG( + ("CacheFileMetadata::OnDataRead() - CacheFileIOManager::Read() failed" + ", creating empty metadata. [this=%p, rv=0x%08" PRIx32 "]", + this, static_cast(aResult))); + + InitEmptyMetadata(); + + mListener.swap(listener); + return NS_OK; + } + + if (mFirstRead) { + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS, mReadStart); + } else { + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS, mReadStart); + } + + // check whether we have read all necessary data + uint32_t realOffset = + NetworkEndian::readUint32(mBuf + mBufSize - sizeof(uint32_t)); + + int64_t size = mHandle->FileSize(); + MOZ_ASSERT(size != -1); + + if (realOffset >= size) { + LOG( + ("CacheFileMetadata::OnDataRead() - Invalid realOffset, creating " + "empty metadata. [this=%p, realOffset=%u, size=%" PRId64 "]", + this, realOffset, size)); + + InitEmptyMetadata(); + + mListener.swap(listener); + return NS_OK; + } + + uint32_t maxHashCount = size / kChunkSize; + uint32_t maxMetadataSize = CalcMetadataSize(kMaxElementsSize, maxHashCount); + if (size - realOffset > maxMetadataSize) { + LOG( + ("CacheFileMetadata::OnDataRead() - Invalid realOffset, metadata would " + "be too big, creating empty metadata. [this=%p, realOffset=%u, " + "maxMetadataSize=%u, size=%" PRId64 "]", + this, realOffset, maxMetadataSize, size)); + + InitEmptyMetadata(); + + mListener.swap(listener); + return NS_OK; + } + + uint32_t usedOffset = size - mBufSize; + + if (realOffset < usedOffset) { + uint32_t missing = usedOffset - realOffset; + // we need to read more data + char* newBuf = static_cast(realloc(mBuf, mBufSize + missing)); + if (!newBuf) { + LOG( + ("CacheFileMetadata::OnDataRead() - Error allocating %d more bytes " + "for the missing part of the metadata, creating empty metadata. " + "[this=%p]", + missing, this)); + + InitEmptyMetadata(); + + mListener.swap(listener); + return NS_OK; + } + + mBuf = newBuf; + memmove(mBuf + missing, mBuf, mBufSize); + mBufSize += missing; + + DoMemoryReport(MemoryUsage()); + + LOG( + ("CacheFileMetadata::OnDataRead() - We need to read %d more bytes to " + "have full metadata. [this=%p]", + missing, this)); + + mFirstRead = false; + mReadStart = mozilla::TimeStamp::Now(); + rv = CacheFileIOManager::Read(mHandle, realOffset, mBuf, missing, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileMetadata::OnDataRead() - CacheFileIOManager::Read() " + "failed synchronously, creating empty metadata. [this=%p, " + "rv=0x%08" PRIx32 "]", + this, static_cast(rv))); + + InitEmptyMetadata(); + + mListener.swap(listener); + return NS_OK; + } + + return NS_OK; + } + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_METADATA_SIZE_2, + size - realOffset); + + // We have all data according to offset information at the end of the entry. + // Try to parse it. + rv = ParseMetadata(realOffset, realOffset - usedOffset, true); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileMetadata::OnDataRead() - Error parsing metadata, creating " + "empty metadata. [this=%p]", + this)); + InitEmptyMetadata(); + } else { + // Shrink elements buffer. + mBuf = static_cast(moz_xrealloc(mBuf, mElementsSize)); + mBufSize = mElementsSize; + + // There is usually no or just one call to SetMetadataElement() when the + // metadata is parsed from disk. Avoid allocating power of two sized buffer + // which we do in case of newly created metadata. + mAllocExactSize = true; + } + + mListener.swap(listener); + + return NS_OK; +} + +nsresult CacheFileMetadata::OnFileDoomed(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileMetadata::OnFileDoomed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileMetadata::OnEOFSet(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileMetadata::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileMetadata::OnFileRenamed(CacheFileHandle* aHandle, + nsresult aResult) { + MOZ_CRASH("CacheFileMetadata::OnFileRenamed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +void CacheFileMetadata::InitEmptyMetadata() { + if (mBuf) { + CacheFileUtils::FreeBuffer(mBuf); + mBuf = nullptr; + mBufSize = 0; + } + mAllocExactSize = false; + mOffset = 0; + mMetaHdr.mVersion = kCacheEntryVersion; + mMetaHdr.mFetchCount = 0; + mMetaHdr.mExpirationTime = nsICacheEntry::NO_EXPIRATION_TIME; + mMetaHdr.mKeySize = mKey.Length(); + + // Deliberately not touching the "kCacheEntryIsPinned" flag. + + DoMemoryReport(MemoryUsage()); + + // We're creating a new entry. If there is any old data truncate it. + if (mHandle) { + mHandle->SetPinned(Pinned()); + // We can pronounce the handle as invalid now, because it simply + // doesn't have the correct metadata. This will cause IO operations + // be bypassed during shutdown (mainly dooming it, when a channel + // is canceled by closing the window.) + mHandle->SetInvalid(); + if (mHandle->FileExists() && mHandle->FileSize()) { + CacheFileIOManager::TruncateSeekSetEOF(mHandle, 0, 0, nullptr); + } + } +} + +nsresult CacheFileMetadata::ParseMetadata(uint32_t aMetaOffset, + uint32_t aBufOffset, bool aHaveKey) { + LOG( + ("CacheFileMetadata::ParseMetadata() [this=%p, metaOffset=%d, " + "bufOffset=%d, haveKey=%u]", + this, aMetaOffset, aBufOffset, aHaveKey)); + + nsresult rv; + + uint32_t metaposOffset = mBufSize - sizeof(uint32_t); + uint32_t hashesOffset = aBufOffset + sizeof(uint32_t); + uint32_t hashCount = aMetaOffset / kChunkSize; + if (aMetaOffset % kChunkSize) hashCount++; + uint32_t hashesLen = hashCount * sizeof(CacheHash::Hash16_t); + uint32_t hdrOffset = hashesOffset + hashesLen; + uint32_t keyOffset = hdrOffset + sizeof(CacheFileMetadataHeader); + + LOG( + ("CacheFileMetadata::ParseMetadata() [this=%p]\n metaposOffset=%d\n " + "hashesOffset=%d\n hashCount=%d\n hashesLen=%d\n hdfOffset=%d\n " + "keyOffset=%d\n", + this, metaposOffset, hashesOffset, hashCount, hashesLen, hdrOffset, + keyOffset)); + + if (keyOffset > metaposOffset) { + LOG(("CacheFileMetadata::ParseMetadata() - Wrong keyOffset! [this=%p]", + this)); + return NS_ERROR_FILE_CORRUPTED; + } + + mMetaHdr.ReadFromBuf(mBuf + hdrOffset); + + if (mMetaHdr.mVersion == 1) { + // Backward compatibility before we've added flags to the header + keyOffset -= sizeof(uint32_t); + } else if (mMetaHdr.mVersion == 2) { + // Version 2 just lacks the ability to store alternative data. Nothing to do + // here. + } else if (mMetaHdr.mVersion != kCacheEntryVersion) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Not a version we understand to. " + "[version=0x%x, this=%p]", + mMetaHdr.mVersion, this)); + return NS_ERROR_UNEXPECTED; + } + + // Update the version stored in the header to make writes + // store the header in the current version form. + mMetaHdr.mVersion = kCacheEntryVersion; + + uint32_t elementsOffset = mMetaHdr.mKeySize + keyOffset + 1; + + if (elementsOffset > metaposOffset) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Wrong elementsOffset %d " + "[this=%p]", + elementsOffset, this)); + return NS_ERROR_FILE_CORRUPTED; + } + + // check that key ends with \0 + if (mBuf[elementsOffset - 1] != 0) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Elements not null terminated. " + "[this=%p]", + this)); + return NS_ERROR_FILE_CORRUPTED; + } + + if (!aHaveKey) { + // get the key form metadata + mKey.Assign(mBuf + keyOffset, mMetaHdr.mKeySize); + + rv = ParseKey(mKey); + if (NS_FAILED(rv)) return rv; + } else { + if (mMetaHdr.mKeySize != mKey.Length()) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Key collision (1), key=%s " + "[this=%p]", + nsCString(mBuf + keyOffset, mMetaHdr.mKeySize).get(), this)); + return NS_ERROR_FILE_CORRUPTED; + } + + if (memcmp(mKey.get(), mBuf + keyOffset, mKey.Length()) != 0) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Key collision (2), key=%s " + "[this=%p]", + nsCString(mBuf + keyOffset, mMetaHdr.mKeySize).get(), this)); + return NS_ERROR_FILE_CORRUPTED; + } + } + + // check metadata hash (data from hashesOffset to metaposOffset) + CacheHash::Hash32_t hashComputed, hashExpected; + hashComputed = + CacheHash::Hash(mBuf + hashesOffset, metaposOffset - hashesOffset); + hashExpected = NetworkEndian::readUint32(mBuf + aBufOffset); + + if (hashComputed != hashExpected) { + LOG( + ("CacheFileMetadata::ParseMetadata() - Metadata hash mismatch! Hash of " + "the metadata is %x, hash in file is %x [this=%p]", + hashComputed, hashExpected, this)); + return NS_ERROR_FILE_CORRUPTED; + } + + // check elements + rv = CheckElements(mBuf + elementsOffset, metaposOffset - elementsOffset); + if (NS_FAILED(rv)) return rv; + + if (mHandle) { + if (!mHandle->SetPinned(Pinned())) { + LOG( + ("CacheFileMetadata::ParseMetadata() - handle was doomed for this " + "pinning state, truncate the file [this=%p, pinned=%d]", + this, Pinned())); + return NS_ERROR_FILE_CORRUPTED; + } + } + + mHashArraySize = hashesLen; + mHashCount = hashCount; + if (mHashArraySize) { + mHashArray = static_cast(moz_xmalloc(mHashArraySize)); + memcpy(mHashArray, mBuf + hashesOffset, mHashArraySize); + } + + MarkDirty(); + + mElementsSize = metaposOffset - elementsOffset; + memmove(mBuf, mBuf + elementsOffset, mElementsSize); + mOffset = aMetaOffset; + + DoMemoryReport(MemoryUsage()); + + return NS_OK; +} + +nsresult CacheFileMetadata::CheckElements(const char* aBuf, uint32_t aSize) { + if (aSize) { + // Check if the metadata ends with a zero byte. + if (aBuf[aSize - 1] != 0) { + NS_ERROR("Metadata elements are not null terminated"); + LOG( + ("CacheFileMetadata::CheckElements() - Elements are not null " + "terminated. [this=%p]", + this)); + return NS_ERROR_FILE_CORRUPTED; + } + // Check that there are an even number of zero bytes + // to match the pattern { key \0 value \0 } + bool odd = false; + for (uint32_t i = 0; i < aSize; i++) { + if (aBuf[i] == 0) odd = !odd; + } + if (odd) { + NS_ERROR("Metadata elements are malformed"); + LOG( + ("CacheFileMetadata::CheckElements() - Elements are malformed. " + "[this=%p]", + this)); + return NS_ERROR_FILE_CORRUPTED; + } + } + return NS_OK; +} + +nsresult CacheFileMetadata::EnsureBuffer(uint32_t aSize) { + if (aSize > kMaxElementsSize) { + return NS_ERROR_FAILURE; + } + + if (mBufSize < aSize) { + if (mAllocExactSize) { + // If this is not the only allocation, use power of two for following + // allocations. + mAllocExactSize = false; + } else { + // find smallest power of 2 greater than or equal to aSize + --aSize; + aSize |= aSize >> 1; + aSize |= aSize >> 2; + aSize |= aSize >> 4; + aSize |= aSize >> 8; + aSize |= aSize >> 16; + ++aSize; + } + + if (aSize < kInitialBufSize) { + aSize = kInitialBufSize; + } + + char* newBuf = static_cast(realloc(mBuf, aSize)); + if (!newBuf) { + return NS_ERROR_OUT_OF_MEMORY; + } + mBufSize = aSize; + mBuf = newBuf; + + DoMemoryReport(MemoryUsage()); + } + + return NS_OK; +} + +nsresult CacheFileMetadata::ParseKey(const nsACString& aKey) { + nsCOMPtr info = CacheFileUtils::ParseKey(aKey); + NS_ENSURE_TRUE(info, NS_ERROR_FAILURE); + + mAnonymous = info->IsAnonymous(); + mOriginAttributes = *info->OriginAttributesPtr(); + + return NS_OK; +} + +// Memory reporting + +size_t CacheFileMetadata::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + size_t n = 0; + // mHandle reported via CacheFileIOManager. + n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mallocSizeOf(mHashArray); + n += mallocSizeOf(mBuf); + // Ignore mWriteBuf, it's not safe to access it when metadata is being + // written and it's null otherwise. + // mListener is usually the owning CacheFile. + + return n; +} + +size_t CacheFileMetadata::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileMetadata.h b/netwerk/cache2/CacheFileMetadata.h new file mode 100644 index 0000000000..3bec23d6c4 --- /dev/null +++ b/netwerk/cache2/CacheFileMetadata.h @@ -0,0 +1,245 @@ +/* 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/. */ + +#ifndef CacheFileMetadata__h__ +#define CacheFileMetadata__h__ + +#include "CacheFileIOManager.h" +#include "CacheStorageService.h" +#include "CacheHashUtils.h" +#include "CacheObserver.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/BasePrincipal.h" +#include "mozilla/NotNull.h" +#include "nsString.h" + +class nsICacheEntryMetaDataVisitor; + +namespace mozilla { +namespace net { + +namespace CacheFileUtils { +class CacheFileLock; +}; + +// Flags stored in CacheFileMetadataHeader.mFlags + +// Whether an entry is a pinned entry (created with +// nsICacheStorageService.pinningCacheStorage.) +static const uint32_t kCacheEntryIsPinned = 1 << 0; + +// By multiplying with the current half-life we convert the frecency +// to time independent of half-life value. The range fits 32bits. +// When decay time changes on next run of the browser, we convert +// the frecency value to a correct internal representation again. +// It might not be 100% accurate, but for the purpose it suffice. +#define FRECENCY2INT(aFrecency) \ + ((uint32_t)((aFrecency)*CacheObserver::HalfLifeSeconds())) +#define INT2FRECENCY(aInt) \ + ((double)(aInt) / (double)CacheObserver::HalfLifeSeconds()) + +#define kCacheEntryVersion 3 + +#pragma pack(push) +#pragma pack(1) + +class CacheFileMetadataHeader { + public: + uint32_t mVersion; + uint32_t mFetchCount; + uint32_t mLastFetched; + uint32_t mLastModified; + uint32_t mFrecency; + uint32_t mExpirationTime; + uint32_t mKeySize; + uint32_t mFlags; + + void WriteToBuf(void* aBuf) { + EnsureCorrectClassSize(); + + uint8_t* ptr = static_cast(aBuf); + MOZ_ASSERT(mVersion == kCacheEntryVersion); + NetworkEndian::writeUint32(ptr, mVersion); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mFetchCount); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mLastFetched); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mLastModified); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mFrecency); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mExpirationTime); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mKeySize); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint32(ptr, mFlags); + } + + void ReadFromBuf(const void* aBuf) { + EnsureCorrectClassSize(); + + const uint8_t* ptr = static_cast(aBuf); + mVersion = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mFetchCount = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mLastFetched = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mLastModified = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mFrecency = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mExpirationTime = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mKeySize = BigEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + if (mVersion >= 2) { + mFlags = BigEndian::readUint32(ptr); + } else { + mFlags = 0; + } + } + + inline void EnsureCorrectClassSize() { + static_assert( + (sizeof(mVersion) + sizeof(mFetchCount) + sizeof(mLastFetched) + + sizeof(mLastModified) + sizeof(mFrecency) + sizeof(mExpirationTime) + + sizeof(mKeySize)) + + sizeof(mFlags) == + sizeof(CacheFileMetadataHeader), + "Unexpected sizeof(CacheFileMetadataHeader)!"); + } +}; + +#pragma pack(pop) + +#define CACHEFILEMETADATALISTENER_IID \ + { /* a9e36125-3f01-4020-9540-9dafa8d31ba7 */ \ + 0xa9e36125, 0x3f01, 0x4020, { \ + 0x95, 0x40, 0x9d, 0xaf, 0xa8, 0xd3, 0x1b, 0xa7 \ + } \ + } + +class CacheFileMetadataListener : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(CACHEFILEMETADATALISTENER_IID) + + NS_IMETHOD OnMetadataRead(nsresult aResult) = 0; + NS_IMETHOD OnMetadataWritten(nsresult aResult) = 0; + virtual bool IsKilled() = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(CacheFileMetadataListener, + CACHEFILEMETADATALISTENER_IID) + +class CacheFileMetadata final : public CacheFileIOListener, + public CacheMemoryConsumer { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + CacheFileMetadata(CacheFileHandle* aHandle, const nsACString& aKey, + NotNull aLock); + CacheFileMetadata(bool aMemoryOnly, bool aPinned, const nsACString& aKey, + NotNull aLock); + CacheFileMetadata(); + + void SetHandle(CacheFileHandle* aHandle); + + const nsACString& GetKey() const { return mKey; } + + void ReadMetadata(CacheFileMetadataListener* aListener); + uint32_t CalcMetadataSize(uint32_t aElementsSize, uint32_t aHashCount); + nsresult WriteMetadata(uint32_t aOffset, + CacheFileMetadataListener* aListener); + nsresult SyncReadMetadata(nsIFile* aFile); + + bool IsAnonymous() const { return mAnonymous; } + mozilla::OriginAttributes const& OriginAttributes() const { + return mOriginAttributes; + } + bool Pinned() const { return !!(mMetaHdr.mFlags & kCacheEntryIsPinned); } + + const char* GetElement(const char* aKey); + nsresult SetElement(const char* aKey, const char* aValue); + void Visit(nsICacheEntryMetaDataVisitor* aVisitor); + + CacheHash::Hash16_t GetHash(uint32_t aIndex); + nsresult SetHash(uint32_t aIndex, CacheHash::Hash16_t aHash); + nsresult RemoveHash(uint32_t aIndex); + + void AddFlags(uint32_t aFlags); + void RemoveFlags(uint32_t aFlags); + uint32_t GetFlags() const { return mMetaHdr.mFlags; } + void SetExpirationTime(uint32_t aExpirationTime); + uint32_t GetExpirationTime() const { return mMetaHdr.mExpirationTime; } + void SetFrecency(uint32_t aFrecency); + uint32_t GetFrecency() const { return mMetaHdr.mFrecency; } + uint32_t GetLastModified() const { return mMetaHdr.mLastModified; } + uint32_t GetLastFetched() const { return mMetaHdr.mLastFetched; } + uint32_t GetFetchCount() const { return mMetaHdr.mFetchCount; } + // Called by upper layers to indicate the entry this metadata belongs + // with has been fetched, i.e. delivered to the consumer. + void OnFetched(); + + int64_t Offset() { return mOffset; } + uint32_t ElementsSize() { return mElementsSize; } + void MarkDirty(bool aUpdateLastModified = true); + bool IsDirty() { return mIsDirty; } + uint32_t MemoryUsage() { + return sizeof(CacheFileMetadata) + mHashArraySize + mBufSize; + } + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) override; + virtual bool IsKilled() override { + return mListener && mListener->IsKilled(); + } + void InitEmptyMetadata(); + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + virtual ~CacheFileMetadata(); + + nsresult ParseMetadata(uint32_t aMetaOffset, uint32_t aBufOffset, + bool aHaveKey); + nsresult CheckElements(const char* aBuf, uint32_t aSize); + nsresult EnsureBuffer(uint32_t aSize); + nsresult ParseKey(const nsACString& aKey); + + RefPtr mHandle; + nsCString mKey; + CacheHash::Hash16_t* mHashArray{nullptr}; + uint32_t mHashArraySize{0}; + uint32_t mHashCount{0}; + int64_t mOffset{0}; + // used for parsing, then points to elements + char* mBuf{nullptr}; + uint32_t mBufSize{0}; + char* mWriteBuf{nullptr}; + CacheFileMetadataHeader mMetaHdr{0}; + uint32_t mElementsSize{0}; + bool mIsDirty : 1; + bool mAnonymous : 1; + bool mAllocExactSize : 1; + bool mFirstRead : 1; + mozilla::OriginAttributes mOriginAttributes; + mozilla::TimeStamp mReadStart; + nsCOMPtr mListener; + RefPtr mLock; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileOutputStream.cpp b/netwerk/cache2/CacheFileOutputStream.cpp new file mode 100644 index 0000000000..985082efba --- /dev/null +++ b/netwerk/cache2/CacheFileOutputStream.cpp @@ -0,0 +1,481 @@ +/* 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 "CacheLog.h" +#include "CacheFileOutputStream.h" + +#include "CacheFile.h" +#include "CacheEntry.h" +#include "nsStreamUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/DebugOnly.h" +#include + +namespace mozilla::net { + +NS_IMPL_ADDREF(CacheFileOutputStream) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileOutputStream::Release() { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "CacheFileOutputStream"); + + if (0 == count) { + mRefCnt = 1; + { + CacheFileAutoLock lock(mFile); + mFile->RemoveOutput(this, mStatus); + } + delete (this); + return 0; + } + + return count; +} + +NS_INTERFACE_MAP_BEGIN(CacheFileOutputStream) + NS_INTERFACE_MAP_ENTRY(nsIOutputStream) + NS_INTERFACE_MAP_ENTRY(nsIAsyncOutputStream) + NS_INTERFACE_MAP_ENTRY(nsISeekableStream) + NS_INTERFACE_MAP_ENTRY(nsITellableStream) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileChunkListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStream) +NS_INTERFACE_MAP_END + +CacheFileOutputStream::CacheFileOutputStream( + CacheFile* aFile, CacheOutputCloseListener* aCloseListener, + bool aAlternativeData) + : mFile(aFile), + mCloseListener(aCloseListener), + mPos(0), + mClosed(false), + mAlternativeData(aAlternativeData), + mStatus(NS_OK), + mCallbackFlags(0) { + LOG(("CacheFileOutputStream::CacheFileOutputStream() [this=%p]", this)); + + if (mAlternativeData) { + mPos = mFile->mAltDataOffset; + } +} + +CacheFileOutputStream::~CacheFileOutputStream() { + LOG(("CacheFileOutputStream::~CacheFileOutputStream() [this=%p]", this)); +} + +// nsIOutputStream +NS_IMETHODIMP +CacheFileOutputStream::Close() { + LOG(("CacheFileOutputStream::Close() [this=%p]", this)); + return CloseWithStatus(NS_OK); +} + +NS_IMETHODIMP +CacheFileOutputStream::Flush() { + // TODO do we need to implement flush ??? + LOG(("CacheFileOutputStream::Flush() [this=%p]", this)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::StreamStatus() { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + LOG(("CacheFileOutputStream::Close() [this=%p]", this)); + if (mClosed) { + return NS_FAILED(mStatus) ? mStatus : NS_BASE_STREAM_CLOSED; + } + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::Write(const char* aBuf, uint32_t aCount, + uint32_t* _retval) { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + LOG(("CacheFileOutputStream::Write() [this=%p, count=%d]", this, aCount)); + + if (mClosed) { + LOG( + ("CacheFileOutputStream::Write() - Stream is closed. [this=%p, " + "status=0x%08" PRIx32 "]", + this, static_cast(mStatus))); + + return NS_FAILED(mStatus) ? mStatus : NS_BASE_STREAM_CLOSED; + } + + if (!mFile->mSkipSizeCheck && + CacheObserver::EntryIsTooBig(mPos + aCount, !mFile->mMemoryOnly)) { + LOG(("CacheFileOutputStream::Write() - Entry is too big. [this=%p]", this)); + + CloseWithStatusLocked(NS_ERROR_FILE_TOO_BIG); + return NS_ERROR_FILE_TOO_BIG; + } + + // We use 64-bit offset when accessing the file, unfortunately we use 32-bit + // metadata offset, so we cannot handle data bigger than 4GB. + if (mPos + aCount > PR_UINT32_MAX) { + LOG(("CacheFileOutputStream::Write() - Entry's size exceeds 4GB. [this=%p]", + this)); + + CloseWithStatusLocked(NS_ERROR_FILE_TOO_BIG); + return NS_ERROR_FILE_TOO_BIG; + } + + *_retval = aCount; + + while (aCount) { + EnsureCorrectChunk(false); + if (NS_FAILED(mStatus)) { + return mStatus; + } + + FillHole(); + if (NS_FAILED(mStatus)) { + return mStatus; + } + + uint32_t chunkOffset = mPos - (mPos / kChunkSize) * kChunkSize; + uint32_t canWrite = kChunkSize - chunkOffset; + uint32_t thisWrite = std::min(static_cast(canWrite), aCount); + + CacheFileChunkWriteHandle hnd = + mChunk->GetWriteHandle(chunkOffset + thisWrite); + if (!hnd.Buf()) { + CloseWithStatusLocked(NS_ERROR_OUT_OF_MEMORY); + return NS_ERROR_OUT_OF_MEMORY; + } + + memcpy(hnd.Buf() + chunkOffset, aBuf, thisWrite); + hnd.UpdateDataSize(chunkOffset, thisWrite); + + mPos += thisWrite; + aBuf += thisWrite; + aCount -= thisWrite; + } + + EnsureCorrectChunk(true); + + LOG(("CacheFileOutputStream::Write() - Wrote %d bytes [this=%p]", *_retval, + this)); + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::WriteFrom(nsIInputStream* aFromStream, uint32_t aCount, + uint32_t* _retval) { + LOG( + ("CacheFileOutputStream::WriteFrom() - NOT_IMPLEMENTED [this=%p, from=%p" + ", count=%d]", + this, aFromStream, aCount)); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +CacheFileOutputStream::WriteSegments(nsReadSegmentFun aReader, void* aClosure, + uint32_t aCount, uint32_t* _retval) { + LOG( + ("CacheFileOutputStream::WriteSegments() - NOT_IMPLEMENTED [this=%p, " + "count=%d]", + this, aCount)); + + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +CacheFileOutputStream::IsNonBlocking(bool* _retval) { + *_retval = false; + return NS_OK; +} + +// nsIAsyncOutputStream +NS_IMETHODIMP +CacheFileOutputStream::CloseWithStatus(nsresult aStatus) { + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileOutputStream::CloseWithStatus() [this=%p, aStatus=0x%08" PRIx32 + "]", + this, static_cast(aStatus))); + + return CloseWithStatusLocked(aStatus); +} + +nsresult CacheFileOutputStream::CloseWithStatusLocked(nsresult aStatus) { + LOG( + ("CacheFileOutputStream::CloseWithStatusLocked() [this=%p, " + "aStatus=0x%08" PRIx32 "]", + this, static_cast(aStatus))); + + if (mClosed) { + MOZ_ASSERT(!mCallback); + return NS_OK; + } + + mClosed = true; + mStatus = NS_FAILED(aStatus) ? aStatus : NS_BASE_STREAM_CLOSED; + + if (mChunk) { + ReleaseChunk(); + } + + if (mCallback) { + NotifyListener(); + } + + mFile->RemoveOutput(this, mStatus); + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::AsyncWait(nsIOutputStreamCallback* aCallback, + uint32_t aFlags, uint32_t aRequestedCount, + nsIEventTarget* aEventTarget) { + CacheFileAutoLock lock(mFile); + + LOG( + ("CacheFileOutputStream::AsyncWait() [this=%p, callback=%p, flags=%d, " + "requestedCount=%d, eventTarget=%p]", + this, aCallback, aFlags, aRequestedCount, aEventTarget)); + + mCallback = aCallback; + mCallbackFlags = aFlags; + mCallbackTarget = aEventTarget; + + if (!mCallback) return NS_OK; + + // The stream is blocking so it is writable at any time + if (mClosed || !(aFlags & WAIT_CLOSURE_ONLY)) NotifyListener(); + + return NS_OK; +} + +// nsISeekableStream +NS_IMETHODIMP +CacheFileOutputStream::Seek(int32_t whence, int64_t offset) { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + LOG(("CacheFileOutputStream::Seek() [this=%p, whence=%d, offset=%" PRId64 "]", + this, whence, offset)); + + if (mClosed) { + LOG(("CacheFileOutputStream::Seek() - Stream is closed. [this=%p]", this)); + return NS_BASE_STREAM_CLOSED; + } + + int64_t newPos = offset; + switch (whence) { + case NS_SEEK_SET: + if (mAlternativeData) { + newPos += mFile->mAltDataOffset; + } + break; + case NS_SEEK_CUR: + newPos += mPos; + break; + case NS_SEEK_END: + if (mAlternativeData) { + newPos += mFile->mDataSize; + } else { + newPos += mFile->mAltDataOffset; + } + break; + default: + NS_ERROR("invalid whence"); + return NS_ERROR_INVALID_ARG; + } + mPos = newPos; + EnsureCorrectChunk(true); + + LOG(("CacheFileOutputStream::Seek() [this=%p, pos=%" PRId64 "]", this, mPos)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::SetEOF() { + MOZ_ASSERT(false, "CacheFileOutputStream::SetEOF() not implemented"); + // Right now we don't use SetEOF(). If we ever need this method, we need + // to think about what to do with input streams that already points beyond + // new EOF. + return NS_ERROR_NOT_IMPLEMENTED; +} + +// nsITellableStream +NS_IMETHODIMP +CacheFileOutputStream::Tell(int64_t* _retval) { + CacheFileAutoLock lock(mFile); + mFile->AssertOwnsLock(); // For thread-safety analysis + + if (mClosed) { + LOG(("CacheFileOutputStream::Tell() - Stream is closed. [this=%p]", this)); + return NS_BASE_STREAM_CLOSED; + } + + *_retval = mPos; + + if (mAlternativeData) { + *_retval -= mFile->mAltDataOffset; + } + + LOG(("CacheFileOutputStream::Tell() [this=%p, retval=%" PRId64 "]", this, + *_retval)); + return NS_OK; +} + +// CacheFileChunkListener +nsresult CacheFileOutputStream::OnChunkRead(nsresult aResult, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileOutputStream::OnChunkRead should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileOutputStream::OnChunkWritten(nsresult aResult, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileOutputStream::OnChunkWritten should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileOutputStream::OnChunkAvailable(nsresult aResult, + uint32_t aChunkIdx, + CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileOutputStream::OnChunkAvailable should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheFileOutputStream::OnChunkUpdated(CacheFileChunk* aChunk) { + MOZ_CRASH("CacheFileOutputStream::OnChunkUpdated should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +void CacheFileOutputStream::NotifyCloseListener() { + RefPtr listener; + listener.swap(mCloseListener); + if (!listener) return; + + listener->OnOutputClosed(); +} + +void CacheFileOutputStream::ReleaseChunk() { + mFile->AssertOwnsLock(); + + LOG(("CacheFileOutputStream::ReleaseChunk() [this=%p, idx=%d]", this, + mChunk->Index())); + + // If the chunk didn't write any data we need to remove hash for this chunk + // that was added when the chunk was created in CacheFile::GetChunkLocked. + if (mChunk->DataSize() == 0) { + // It must be due to a failure, we don't create a new chunk when we don't + // have data to write. + MOZ_ASSERT(NS_FAILED(mChunk->GetStatus())); + mFile->mMetadata->RemoveHash(mChunk->Index()); + } + + mFile->ReleaseOutsideLock(std::move(mChunk)); +} + +void CacheFileOutputStream::EnsureCorrectChunk(bool aReleaseOnly) { + mFile->AssertOwnsLock(); + + LOG(("CacheFileOutputStream::EnsureCorrectChunk() [this=%p, releaseOnly=%d]", + this, aReleaseOnly)); + + uint32_t chunkIdx = mPos / kChunkSize; + + if (mChunk) { + if (mChunk->Index() == chunkIdx) { + // we have a correct chunk + LOG( + ("CacheFileOutputStream::EnsureCorrectChunk() - Have correct chunk " + "[this=%p, idx=%d]", + this, chunkIdx)); + + return; + } + ReleaseChunk(); + } + + if (aReleaseOnly) return; + + nsresult rv; + rv = mFile->GetChunkLocked(chunkIdx, CacheFile::WRITER, nullptr, + getter_AddRefs(mChunk)); + if (NS_FAILED(rv)) { + LOG( + ("CacheFileOutputStream::EnsureCorrectChunk() - GetChunkLocked failed. " + "[this=%p, idx=%d, rv=0x%08" PRIx32 "]", + this, chunkIdx, static_cast(rv))); + CloseWithStatusLocked(rv); + } +} + +void CacheFileOutputStream::FillHole() { + mFile->AssertOwnsLock(); + + MOZ_ASSERT(mChunk); + MOZ_ASSERT(mPos / kChunkSize == mChunk->Index()); + + uint32_t pos = mPos - (mPos / kChunkSize) * kChunkSize; + if (mChunk->DataSize() >= pos) return; + + LOG( + ("CacheFileOutputStream::FillHole() - Zeroing hole in chunk %d, range " + "%d-%d [this=%p]", + mChunk->Index(), mChunk->DataSize(), pos - 1, this)); + + CacheFileChunkWriteHandle hnd = mChunk->GetWriteHandle(pos); + if (!hnd.Buf()) { + CloseWithStatusLocked(NS_ERROR_OUT_OF_MEMORY); + return; + } + + uint32_t offset = hnd.DataSize(); + memset(hnd.Buf() + offset, 0, pos - offset); + hnd.UpdateDataSize(offset, pos - offset); +} + +void CacheFileOutputStream::NotifyListener() { + mFile->AssertOwnsLock(); + + LOG(("CacheFileOutputStream::NotifyListener() [this=%p]", this)); + + MOZ_ASSERT(mCallback); + + if (!mCallbackTarget) { + mCallbackTarget = CacheFileIOManager::IOTarget(); + if (!mCallbackTarget) { + LOG( + ("CacheFileOutputStream::NotifyListener() - Cannot get Cache I/O " + "thread! Using main thread for callback.")); + mCallbackTarget = GetMainThreadSerialEventTarget(); + } + } + + nsCOMPtr asyncCallback = + NS_NewOutputStreamReadyEvent(mCallback, mCallbackTarget); + + mCallback = nullptr; + mCallbackTarget = nullptr; + + asyncCallback->OnOutputStreamReady(this); +} + +// Memory reporting + +size_t CacheFileOutputStream::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + // Everything the stream keeps a reference to is already reported somewhere + // else. + // mFile reports itself. + // mChunk reported as part of CacheFile. + // mCloseListener is CacheEntry, already reported. + // mCallback is usually CacheFile or a class that is reported elsewhere. + return mallocSizeOf(this); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheFileOutputStream.h b/netwerk/cache2/CacheFileOutputStream.h new file mode 100644 index 0000000000..4d1fc5e4b7 --- /dev/null +++ b/netwerk/cache2/CacheFileOutputStream.h @@ -0,0 +1,70 @@ +/* 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/. */ + +#ifndef CacheFileOutputStream__h__ +#define CacheFileOutputStream__h__ + +#include "nsIAsyncOutputStream.h" +#include "nsISeekableStream.h" +#include "nsCOMPtr.h" +#include "CacheFileChunk.h" + +namespace mozilla { +namespace net { + +class CacheFile; +class CacheOutputCloseListener; + +class CacheFileOutputStream : public nsIAsyncOutputStream, + public nsISeekableStream, + public CacheFileChunkListener { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSIASYNCOUTPUTSTREAM + NS_DECL_NSISEEKABLESTREAM + NS_DECL_NSITELLABLESTREAM + + public: + CacheFileOutputStream(CacheFile* aFile, + CacheOutputCloseListener* aCloseListener, + bool aAlternativeData); + + NS_IMETHOD OnChunkRead(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkWritten(nsresult aResult, CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkAvailable(nsresult aResult, uint32_t aChunkIdx, + CacheFileChunk* aChunk) override; + NS_IMETHOD OnChunkUpdated(CacheFileChunk* aChunk) override; + + void NotifyCloseListener(); + bool IsAlternativeData() const { return mAlternativeData; }; + + // Memory reporting + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + virtual ~CacheFileOutputStream(); + + nsresult CloseWithStatusLocked(nsresult aStatus); + void ReleaseChunk(); + void EnsureCorrectChunk(bool aReleaseOnly); + void FillHole(); + void NotifyListener(); + + RefPtr mFile; + RefPtr mChunk; + RefPtr mCloseListener; + int64_t mPos; + bool mClosed : 1; + bool const mAlternativeData : 1; + nsresult mStatus; + + nsCOMPtr mCallback; + uint32_t mCallbackFlags; + nsCOMPtr mCallbackTarget; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileUtils.cpp b/netwerk/cache2/CacheFileUtils.cpp new file mode 100644 index 0000000000..1e158f394a --- /dev/null +++ b/netwerk/cache2/CacheFileUtils.cpp @@ -0,0 +1,667 @@ +/* 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 "CacheIndex.h" +#include "CacheLog.h" +#include "CacheFileUtils.h" +#include "CacheObserver.h" +#include "LoadContextInfo.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/Telemetry.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include +#include "mozilla/Unused.h" + +namespace mozilla::net::CacheFileUtils { + +// This designates the format for the "alt-data" metadata. +// When the format changes we need to update the version. +static uint32_t const kAltDataVersion = 1; +const char* kAltDataKey = "alt-data"; + +namespace { + +/** + * A simple recursive descent parser for the mapping key. + */ +class KeyParser : protected Tokenizer { + public: + explicit KeyParser(nsACString const& aInput) + : Tokenizer(aInput), + isAnonymous(false) + // Initialize the cache key to a zero length by default + , + lastTag(0) {} + + private: + // Results + OriginAttributes originAttribs; + bool isAnonymous; + nsCString idEnhance; + nsDependentCSubstring cacheKey; + + // Keeps the last tag name, used for alphabetical sort checking + char lastTag; + + // Classifier for the 'tag' character valid range. + // Explicitly using unsigned char as 127 is -1 when signed and it would only + // produce a warning. + static bool TagChar(const char aChar) { + unsigned char c = static_cast(aChar); + return c >= ' ' && c <= '\x7f'; + } + + bool ParseTags() { + // Expects to be at the tag name or at the end + if (CheckEOF()) { + return true; + } + + char tag; + if (!ReadChar(&TagChar, &tag)) { + return false; + } + + // Check the alphabetical order, hard-fail on disobedience + if (!(lastTag < tag || tag == ':')) { + return false; + } + lastTag = tag; + + switch (tag) { + case ':': + // last possible tag, when present there is the cacheKey following, + // not terminated with ',' and no need to unescape. + cacheKey.Rebind(mCursor, mEnd - mCursor); + return true; + case 'O': { + nsAutoCString originSuffix; + if (!ParseValue(&originSuffix) || + !originAttribs.PopulateFromSuffix(originSuffix)) { + return false; + } + break; + } + case 'p': + originAttribs.SyncAttributesWithPrivateBrowsing(true); + break; + case 'b': + // Leaving to be able to read and understand oldformatted entries + originAttribs.mInIsolatedMozBrowser = true; + break; + case 'a': + isAnonymous = true; + break; + case 'i': { + // Leaving to be able to read and understand oldformatted entries + uint32_t deprecatedAppId = 0; + if (!ReadInteger(&deprecatedAppId)) { + return false; // not a valid 32-bit integer + } + break; + } + case '~': + if (!ParseValue(&idEnhance)) { + return false; + } + break; + default: + if (!ParseValue()) { // skip any tag values, optional + return false; + } + break; + } + + // We expect a comma after every tag + if (!CheckChar(',')) { + return false; + } + + // Recurse to the next tag + return ParseTags(); + } + + bool ParseValue(nsACString* result = nullptr) { + // If at the end, fail since we expect a comma ; value may be empty tho + if (CheckEOF()) { + return false; + } + + Token t; + while (Next(t)) { + if (!Token::Char(',').Equals(t)) { + if (result) { + result->Append(t.Fragment()); + } + continue; + } + + if (CheckChar(',')) { + // Two commas in a row, escaping + if (result) { + result->Append(','); + } + continue; + } + + // We must give the comma back since the upper calls expect it + Rollback(); + return true; + } + + return false; + } + + public: + already_AddRefed Parse() { + RefPtr info; + if (ParseTags()) { + info = GetLoadContextInfo(isAnonymous, originAttribs); + } + + return info.forget(); + } + + void URISpec(nsACString& result) { result.Assign(cacheKey); } + + void IdEnhance(nsACString& result) { result.Assign(idEnhance); } +}; + +} // namespace + +already_AddRefed ParseKey(const nsACString& aKey, + nsACString* aIdEnhance, + nsACString* aURISpec) { + KeyParser parser(aKey); + RefPtr info = parser.Parse(); + + if (info) { + if (aIdEnhance) parser.IdEnhance(*aIdEnhance); + if (aURISpec) parser.URISpec(*aURISpec); + } + + return info.forget(); +} + +void AppendKeyPrefix(nsILoadContextInfo* aInfo, nsACString& _retval) { + /** + * This key is used to salt file hashes. When form of the key is changed + * cache entries will fail to find on disk. + * + * IMPORTANT NOTE: + * Keep the attributes list sorted according their ASCII code. + */ + + if (!aInfo) { + return; + } + + OriginAttributes const* oa = aInfo->OriginAttributesPtr(); + nsAutoCString suffix; + oa->CreateSuffix(suffix); + if (!suffix.IsEmpty()) { + AppendTagWithValue(_retval, 'O', suffix); + } + + if (aInfo->IsAnonymous()) { + _retval.AppendLiteral("a,"); + } + + if (aInfo->IsPrivate()) { + _retval.AppendLiteral("p,"); + } +} + +void AppendTagWithValue(nsACString& aTarget, char const aTag, + const nsACString& aValue) { + aTarget.Append(aTag); + + // First check the value string to save some memory copying + // for cases we don't need to escape at all (most likely). + if (!aValue.IsEmpty()) { + if (!aValue.Contains(',')) { + // No need to escape + aTarget.Append(aValue); + } else { + nsAutoCString escapedValue(aValue); + escapedValue.ReplaceSubstring(","_ns, ",,"_ns); + aTarget.Append(escapedValue); + } + } + + aTarget.Append(','); +} + +nsresult KeyMatchesLoadContextInfo(const nsACString& aKey, + nsILoadContextInfo* aInfo, bool* _retval) { + nsCOMPtr info = ParseKey(aKey); + + if (!info) { + return NS_ERROR_FAILURE; + } + + *_retval = info->Equals(aInfo); + return NS_OK; +} + +ValidityPair::ValidityPair(uint32_t aOffset, uint32_t aLen) + : mOffset(aOffset), mLen(aLen) {} + +bool ValidityPair::CanBeMerged(const ValidityPair& aOther) const { + // The pairs can be merged into a single one if the start of one of the pairs + // is placed anywhere in the validity interval of other pair or exactly after + // its end. + return IsInOrFollows(aOther.mOffset) || aOther.IsInOrFollows(mOffset); +} + +bool ValidityPair::IsInOrFollows(uint32_t aOffset) const { + return mOffset <= aOffset && mOffset + mLen >= aOffset; +} + +bool ValidityPair::LessThan(const ValidityPair& aOther) const { + if (mOffset < aOther.mOffset) { + return true; + } + + if (mOffset == aOther.mOffset && mLen < aOther.mLen) { + return true; + } + + return false; +} + +void ValidityPair::Merge(const ValidityPair& aOther) { + MOZ_ASSERT(CanBeMerged(aOther)); + + uint32_t offset = std::min(mOffset, aOther.mOffset); + uint32_t end = std::max(mOffset + mLen, aOther.mOffset + aOther.mLen); + + mOffset = offset; + mLen = end - offset; +} + +void ValidityMap::Log() const { + LOG(("ValidityMap::Log() - number of pairs: %zu", mMap.Length())); + for (uint32_t i = 0; i < mMap.Length(); i++) { + LOG((" (%u, %u)", mMap[i].Offset() + 0, mMap[i].Len() + 0)); + } +} + +uint32_t ValidityMap::Length() const { return mMap.Length(); } + +void ValidityMap::AddPair(uint32_t aOffset, uint32_t aLen) { + ValidityPair pair(aOffset, aLen); + + if (mMap.Length() == 0) { + mMap.AppendElement(pair); + return; + } + + // Find out where to place this pair into the map, it can overlap only with + // one preceding pair and all subsequent pairs. + uint32_t pos = 0; + for (pos = mMap.Length(); pos > 0;) { + --pos; + + if (mMap[pos].LessThan(pair)) { + // The new pair should be either inserted after pos or merged with it. + if (mMap[pos].CanBeMerged(pair)) { + // Merge with the preceding pair + mMap[pos].Merge(pair); + } else { + // They don't overlap, element must be placed after pos element + ++pos; + if (pos == mMap.Length()) { + mMap.AppendElement(pair); + } else { + mMap.InsertElementAt(pos, pair); + } + } + + break; + } + + if (pos == 0) { + // The new pair should be placed in front of all existing pairs. + mMap.InsertElementAt(0, pair); + } + } + + // pos now points to merged or inserted pair, check whether it overlaps with + // subsequent pairs. + while (pos + 1 < mMap.Length()) { + if (mMap[pos].CanBeMerged(mMap[pos + 1])) { + mMap[pos].Merge(mMap[pos + 1]); + mMap.RemoveElementAt(pos + 1); + } else { + break; + } + } +} + +void ValidityMap::Clear() { mMap.Clear(); } + +size_t ValidityMap::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mMap.ShallowSizeOfExcludingThis(mallocSizeOf); +} + +ValidityPair& ValidityMap::operator[](uint32_t aIdx) { + return mMap.ElementAt(aIdx); +} + +StaticMutex DetailedCacheHitTelemetry::sLock; +uint32_t DetailedCacheHitTelemetry::sRecordCnt = 0; +DetailedCacheHitTelemetry::HitRate + DetailedCacheHitTelemetry::sHRStats[kNumOfRanges]; + +DetailedCacheHitTelemetry::HitRate::HitRate() { Reset(); } + +void DetailedCacheHitTelemetry::HitRate::AddRecord(ERecType aType) { + if (aType == HIT) { + ++mHitCnt; + } else { + ++mMissCnt; + } +} + +uint32_t DetailedCacheHitTelemetry::HitRate::GetHitRateBucket( + uint32_t aNumOfBuckets) const { + uint32_t bucketIdx = (aNumOfBuckets * mHitCnt) / (mHitCnt + mMissCnt); + if (bucketIdx == + aNumOfBuckets) { // make sure 100% falls into the last bucket + --bucketIdx; + } + + return bucketIdx; +} + +uint32_t DetailedCacheHitTelemetry::HitRate::Count() { + return mHitCnt + mMissCnt; +} + +void DetailedCacheHitTelemetry::HitRate::Reset() { + mHitCnt = 0; + mMissCnt = 0; +} + +// static +void DetailedCacheHitTelemetry::AddRecord(ERecType aType, + TimeStamp aLoadStart) { + bool isUpToDate = false; + CacheIndex::IsUpToDate(&isUpToDate); + if (!isUpToDate) { + // Ignore the record when the entry file count might be incorrect + return; + } + + uint32_t entryCount; + nsresult rv = CacheIndex::GetEntryFileCount(&entryCount); + if (NS_FAILED(rv)) { + return; + } + + uint32_t rangeIdx = entryCount / kRangeSize; + if (rangeIdx >= kNumOfRanges) { // The last range has no upper limit. + rangeIdx = kNumOfRanges - 1; + } + + uint32_t hitMissValue = 2 * rangeIdx; // 2 values per range + if (aType == MISS) { // The order is HIT, MISS + ++hitMissValue; + } + + StaticMutexAutoLock lock(sLock); + + if (aType == MISS) { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::NETWORK_CACHE_V2_MISS_TIME_MS, aLoadStart); + } else { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::NETWORK_CACHE_V2_HIT_TIME_MS, aLoadStart); + } + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE, + hitMissValue); + + sHRStats[rangeIdx].AddRecord(aType); + ++sRecordCnt; + + if (sRecordCnt < kTotalSamplesReportLimit) { + return; + } + + sRecordCnt = 0; + + for (uint32_t i = 0; i < kNumOfRanges; ++i) { + if (sHRStats[i].Count() >= kHitRateSamplesReportLimit) { + // The telemetry enums are grouped by buckets as follows: + // Telemetry value : 0,1,2,3, ... ,19,20,21,22, ... ,398,399 + // Hit rate bucket : 0,0,0,0, ... , 0, 1, 1, 1, ... , 19, 19 + // Cache size range: 0,1,2,3, ... ,19, 0, 1, 2, ... , 18, 19 + uint32_t bucketOffset = + sHRStats[i].GetHitRateBucket(kHitRateBuckets) * kNumOfRanges; + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE, + bucketOffset + i); + sHRStats[i].Reset(); + } + } +} + +StaticMutex CachePerfStats::sLock; +CachePerfStats::PerfData CachePerfStats::sData[CachePerfStats::LAST]; +uint32_t CachePerfStats::sCacheSlowCnt = 0; +uint32_t CachePerfStats::sCacheNotSlowCnt = 0; + +CachePerfStats::MMA::MMA(uint32_t aTotalWeight, bool aFilter) + : mSum(0), mSumSq(0), mCnt(0), mWeight(aTotalWeight), mFilter(aFilter) {} + +void CachePerfStats::MMA::AddValue(uint32_t aValue) { + if (mFilter) { + // Filter high spikes + uint32_t avg = GetAverage(); + uint32_t stddev = GetStdDev(); + uint32_t maxdiff = avg + (3 * stddev); + if (avg && aValue > avg + maxdiff) { + return; + } + } + + if (mCnt < mWeight) { + // Compute arithmetic average until we have at least mWeight values + CheckedInt newSumSq = CheckedInt(aValue) * aValue; + newSumSq += mSumSq; + if (!newSumSq.isValid()) { + return; // ignore this value + } + mSumSq = newSumSq.value(); + mSum += aValue; + ++mCnt; + } else { + CheckedInt newSumSq = mSumSq - mSumSq / mCnt; + newSumSq += static_cast(aValue) * aValue; + if (!newSumSq.isValid()) { + return; // ignore this value + } + mSumSq = newSumSq.value(); + + // Compute modified moving average for more values: + // newAvg = ((weight - 1) * oldAvg + newValue) / weight + mSum -= GetAverage(); + mSum += aValue; + } +} + +uint32_t CachePerfStats::MMA::GetAverage() { + if (mCnt == 0) { + return 0; + } + + return mSum / mCnt; +} + +uint32_t CachePerfStats::MMA::GetStdDev() { + if (mCnt == 0) { + return 0; + } + + uint32_t avg = GetAverage(); + uint64_t avgSq = static_cast(avg) * avg; + uint64_t variance = mSumSq / mCnt; + if (variance < avgSq) { + // Due to rounding error when using integer data type, it can happen that + // average of squares of the values is smaller than square of the average + // of the values. In this case fix mSumSq. + variance = avgSq; + mSumSq = variance * mCnt; + } + + variance -= avgSq; + return sqrt(static_cast(variance)); +} + +CachePerfStats::PerfData::PerfData() + : mFilteredAvg(50, true), mShortAvg(3, false) {} + +void CachePerfStats::PerfData::AddValue(uint32_t aValue, bool aShortOnly) { + if (!aShortOnly) { + mFilteredAvg.AddValue(aValue); + } + mShortAvg.AddValue(aValue); +} + +uint32_t CachePerfStats::PerfData::GetAverage(bool aFiltered) { + return aFiltered ? mFilteredAvg.GetAverage() : mShortAvg.GetAverage(); +} + +uint32_t CachePerfStats::PerfData::GetStdDev(bool aFiltered) { + return aFiltered ? mFilteredAvg.GetStdDev() : mShortAvg.GetStdDev(); +} + +// static +void CachePerfStats::AddValue(EDataType aType, uint32_t aValue, + bool aShortOnly) { + StaticMutexAutoLock lock(sLock); + sData[aType].AddValue(aValue, aShortOnly); +} + +// static +uint32_t CachePerfStats::GetAverage(EDataType aType, bool aFiltered) { + StaticMutexAutoLock lock(sLock); + return sData[aType].GetAverage(aFiltered); +} + +// static +uint32_t CachePerfStats::GetStdDev(EDataType aType, bool aFiltered) { + StaticMutexAutoLock lock(sLock); + return sData[aType].GetStdDev(aFiltered); +} + +// static +bool CachePerfStats::IsCacheSlow() { + StaticMutexAutoLock lock(sLock); + + // Compare mShortAvg with mFilteredAvg to find out whether cache is getting + // slower. Use only data about single IO operations because ENTRY_OPEN can be + // affected by more factors than a slow disk. + for (uint32_t i = 0; i < ENTRY_OPEN; ++i) { + if (i == IO_WRITE) { + // Skip this data type. IsCacheSlow is used for determining cache slowness + // when opening entries. Writes have low priority and it's normal that + // they are delayed a lot, but this doesn't necessarily affect opening + // cache entries. + continue; + } + + uint32_t avgLong = sData[i].GetAverage(true); + if (avgLong == 0) { + // We have no perf data yet, skip this data type. + continue; + } + uint32_t avgShort = sData[i].GetAverage(false); + uint32_t stddevLong = sData[i].GetStdDev(true); + uint32_t maxdiff = avgLong + (3 * stddevLong); + + if (avgShort > avgLong + maxdiff) { + LOG( + ("CachePerfStats::IsCacheSlow() - result is slow based on perf " + "type %u [avgShort=%u, avgLong=%u, stddevLong=%u]", + i, avgShort, avgLong, stddevLong)); + ++sCacheSlowCnt; + return true; + } + } + + ++sCacheNotSlowCnt; + return false; +} + +// static +void CachePerfStats::GetSlowStats(uint32_t* aSlow, uint32_t* aNotSlow) { + StaticMutexAutoLock lock(sLock); + *aSlow = sCacheSlowCnt; + *aNotSlow = sCacheNotSlowCnt; +} + +void FreeBuffer(void* aBuf) { +#ifndef NS_FREE_PERMANENT_DATA + if (CacheObserver::ShuttingDown()) { + return; + } +#endif + + free(aBuf); +} + +nsresult ParseAlternativeDataInfo(const char* aInfo, int64_t* _offset, + nsACString* _type) { + // The format is: "1;12345,javascript/binary" + // ;, + mozilla::Tokenizer p(aInfo, nullptr, "/"); + uint32_t altDataVersion = 0; + int64_t altDataOffset = -1; + + // The metadata format has a wrong version number. + if (!p.ReadInteger(&altDataVersion) || altDataVersion != kAltDataVersion) { + LOG( + ("ParseAlternativeDataInfo() - altDataVersion=%u, " + "expectedVersion=%u", + altDataVersion, kAltDataVersion)); + return NS_ERROR_NOT_AVAILABLE; + } + + if (!p.CheckChar(';') || !p.ReadInteger(&altDataOffset) || + !p.CheckChar(',')) { + return NS_ERROR_NOT_AVAILABLE; + } + + // The requested alt-data representation is not available + if (altDataOffset < 0) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (_offset) { + *_offset = altDataOffset; + } + + if (_type) { + mozilla::Unused << p.ReadUntil(Tokenizer::Token::EndOfFile(), *_type); + } + + return NS_OK; +} + +void BuildAlternativeDataInfo(const char* aInfo, int64_t aOffset, + nsACString& _retval) { + _retval.Truncate(); + _retval.AppendInt(kAltDataVersion); + _retval.Append(';'); + _retval.AppendInt(aOffset); + _retval.Append(','); + _retval.Append(aInfo); +} + +} // namespace mozilla::net::CacheFileUtils diff --git a/netwerk/cache2/CacheFileUtils.h b/netwerk/cache2/CacheFileUtils.h new file mode 100644 index 0000000000..aa6fb64312 --- /dev/null +++ b/netwerk/cache2/CacheFileUtils.h @@ -0,0 +1,238 @@ +/* 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/. */ + +#ifndef CacheFileUtils__h__ +#define CacheFileUtils__h__ + +#include "nsError.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/TimeStamp.h" + +class nsILoadContextInfo; + +namespace mozilla { +namespace net { +namespace CacheFileUtils { + +extern const char* kAltDataKey; + +already_AddRefed ParseKey(const nsACString& aKey, + nsACString* aIdEnhance = nullptr, + nsACString* aURISpec = nullptr); + +void AppendKeyPrefix(nsILoadContextInfo* aInfo, nsACString& _retval); + +void AppendTagWithValue(nsACString& aTarget, char const aTag, + const nsACString& aValue); + +nsresult KeyMatchesLoadContextInfo(const nsACString& aKey, + nsILoadContextInfo* aInfo, bool* _retval); + +class ValidityPair { + public: + ValidityPair(uint32_t aOffset, uint32_t aLen); + + ValidityPair& operator=(const ValidityPair& aOther) = default; + + // Returns true when two pairs can be merged, i.e. they do overlap or the one + // ends exactly where the other begins. + bool CanBeMerged(const ValidityPair& aOther) const; + + // Returns true when aOffset is placed anywhere in the validity interval or + // exactly after its end. + bool IsInOrFollows(uint32_t aOffset) const; + + // Returns true when this pair has lower offset than the other pair. In case + // both pairs have the same offset it returns true when this pair has a + // shorter length. + bool LessThan(const ValidityPair& aOther) const; + + // Merges two pair into one. + void Merge(const ValidityPair& aOther); + + uint32_t Offset() const { return mOffset; } + uint32_t Len() const { return mLen; } + + private: + uint32_t mOffset; + uint32_t mLen; +}; + +class ValidityMap { + public: + // Prints pairs in the map into log. + void Log() const; + + // Returns number of pairs in the map. + uint32_t Length() const; + + // Adds a new pair to the map. It keeps the pairs ordered and merges pairs + // when possible. + void AddPair(uint32_t aOffset, uint32_t aLen); + + // Removes all pairs from the map. + void Clear(); + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + ValidityPair& operator[](uint32_t aIdx); + + private: + nsTArray mMap; +}; + +class DetailedCacheHitTelemetry { + public: + enum ERecType { HIT = 0, MISS = 1 }; + + static void AddRecord(ERecType aType, TimeStamp aLoadStart); + + private: + class HitRate { + public: + HitRate(); + + void AddRecord(ERecType aType); + // Returns the bucket index that the current hit rate falls into according + // to the given aNumOfBuckets. + uint32_t GetHitRateBucket(uint32_t aNumOfBuckets) const; + uint32_t Count(); + void Reset(); + + private: + uint32_t mHitCnt = 0; + uint32_t mMissCnt = 0; + }; + + // Group the hits and misses statistics by cache files count ranges (0-5000, + // 5001-10000, ... , 95001- ) + static const uint32_t kRangeSize = 5000; + static const uint32_t kNumOfRanges = 20; + + // Use the same ranges to report an average hit rate. Report the hit rates + // (and reset the counters) every kTotalSamplesReportLimit samples. + static const uint32_t kTotalSamplesReportLimit = 1000; + + // Report hit rate for a given cache size range only if it contains + // kHitRateSamplesReportLimit or more samples. This limit should avoid + // reporting a biased statistics. + static const uint32_t kHitRateSamplesReportLimit = 500; + + // All hit rates are accumulated in a single telemetry probe, so to use + // a sane number of enumerated values the hit rate is divided into buckets + // instead of using a percent value. This constant defines number of buckets + // that we divide the hit rates into. I.e. we'll report ranges 0%-5%, 5%-10%, + // 10-%15%, ... + static const uint32_t kHitRateBuckets = 20; + + // Protects sRecordCnt, sHRStats and Telemetry::Accumulated() calls. + static StaticMutex sLock; + + // Counter of samples that is compared against kTotalSamplesReportLimit. + static uint32_t sRecordCnt MOZ_GUARDED_BY(sLock); + + // Hit rate statistics for every cache size range. + static HitRate sHRStats[kNumOfRanges] MOZ_GUARDED_BY(sLock); +}; + +class CachePerfStats { + public: + // perfStatTypes in displayRcwnStats() in toolkit/content/aboutNetworking.js + // must match EDataType + enum EDataType { + IO_OPEN = 0, + IO_READ = 1, + IO_WRITE = 2, + ENTRY_OPEN = 3, + LAST = 4 + }; + + static void AddValue(EDataType aType, uint32_t aValue, bool aShortOnly); + static uint32_t GetAverage(EDataType aType, bool aFiltered); + static uint32_t GetStdDev(EDataType aType, bool aFiltered); + static bool IsCacheSlow(); + static void GetSlowStats(uint32_t* aSlow, uint32_t* aNotSlow); + + private: + // This class computes average and standard deviation, it returns an + // arithmetic avg and stddev until total number of values reaches mWeight. + // Then it returns modified moving average computed as follows: + // + // avg = (1-a)*avg + a*value + // avgsq = (1-a)*avgsq + a*value^2 + // stddev = sqrt(avgsq - avg^2) + // + // where + // avgsq is an average of the square of the values + // a = 1 / weight + class MMA { + public: + MMA(uint32_t aTotalWeight, bool aFilter); + + void AddValue(uint32_t aValue); + uint32_t GetAverage(); + uint32_t GetStdDev(); + + private: + uint64_t mSum; + uint64_t mSumSq; + uint32_t mCnt; + uint32_t mWeight; + bool mFilter; + }; + + class PerfData { + public: + PerfData(); + + void AddValue(uint32_t aValue, bool aShortOnly); + uint32_t GetAverage(bool aFiltered); + uint32_t GetStdDev(bool aFiltered); + + private: + // Contains filtered data (i.e. times when we think the cache and disk was + // not busy) for a longer time. + MMA mFilteredAvg; + + // Contains unfiltered average of few recent values. + MMA mShortAvg; + }; + + static StaticMutex sLock; + + static PerfData sData[LAST] MOZ_GUARDED_BY(sLock); + static uint32_t sCacheSlowCnt MOZ_GUARDED_BY(sLock); + static uint32_t sCacheNotSlowCnt MOZ_GUARDED_BY(sLock); +}; + +void FreeBuffer(void* aBuf); + +nsresult ParseAlternativeDataInfo(const char* aInfo, int64_t* _offset, + nsACString* _type); + +void BuildAlternativeDataInfo(const char* aInfo, int64_t aOffset, + nsACString& _retval); + +class CacheFileLock final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheFileLock) + CacheFileLock() = default; + + mozilla::Mutex& Lock() MOZ_RETURN_CAPABILITY(mLock) { return mLock; } + + private: + ~CacheFileLock() = default; + + mozilla::Mutex mLock{"CacheFile.mLock"}; +}; + +} // namespace CacheFileUtils +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheHashUtils.cpp b/netwerk/cache2/CacheHashUtils.cpp new file mode 100644 index 0000000000..6ac8205931 --- /dev/null +++ b/netwerk/cache2/CacheHashUtils.cpp @@ -0,0 +1,226 @@ +/* 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 "CacheHashUtils.h" + +#include "mozilla/BasePrincipal.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/SHA1.h" + +namespace mozilla::net { + +/** + * CacheHash::Hash(const char * key, uint32_t initval) + * + * See http://burtleburtle.net/bob/hash/evahash.html for more information + * about this hash function. + * + * This algorithm is used to check the data integrity. + */ + +static inline void hashmix(uint32_t& a, uint32_t& b, uint32_t& c) { + a -= b; + a -= c; + a ^= (c >> 13); + b -= c; + b -= a; + b ^= (a << 8); + c -= a; + c -= b; + c ^= (b >> 13); + a -= b; + a -= c; + a ^= (c >> 12); + b -= c; + b -= a; + b ^= (a << 16); + c -= a; + c -= b; + c ^= (b >> 5); + a -= b; + a -= c; + a ^= (c >> 3); + b -= c; + b -= a; + b ^= (a << 10); + c -= a; + c -= b; + c ^= (b >> 15); +} + +CacheHash::Hash32_t CacheHash::Hash(const char* aData, uint32_t aSize, + uint32_t aInitval) { + const uint8_t* k = reinterpret_cast(aData); + uint32_t a, b, c, len; + + /* Set up the internal state */ + len = aSize; + a = b = 0x9e3779b9; /* the golden ratio; an arbitrary value */ + c = aInitval; /* variable initialization of internal state */ + + /*---------------------------------------- handle most of the key */ + while (len >= 12) { + a += k[0] + (uint32_t(k[1]) << 8) + (uint32_t(k[2]) << 16) + + (uint32_t(k[3]) << 24); + b += k[4] + (uint32_t(k[5]) << 8) + (uint32_t(k[6]) << 16) + + (uint32_t(k[7]) << 24); + c += k[8] + (uint32_t(k[9]) << 8) + (uint32_t(k[10]) << 16) + + (uint32_t(k[11]) << 24); + hashmix(a, b, c); + k += 12; + len -= 12; + } + + /*------------------------------------- handle the last 11 bytes */ + c += aSize; + switch (len) { /* all the case statements fall through */ + case 11: + c += (uint32_t(k[10]) << 24); + [[fallthrough]]; + case 10: + c += (uint32_t(k[9]) << 16); + [[fallthrough]]; + case 9: + c += (uint32_t(k[8]) << 8); + [[fallthrough]]; + /* the low-order byte of c is reserved for the length */ + case 8: + b += (uint32_t(k[7]) << 24); + [[fallthrough]]; + case 7: + b += (uint32_t(k[6]) << 16); + [[fallthrough]]; + case 6: + b += (uint32_t(k[5]) << 8); + [[fallthrough]]; + case 5: + b += k[4]; + [[fallthrough]]; + case 4: + a += (uint32_t(k[3]) << 24); + [[fallthrough]]; + case 3: + a += (uint32_t(k[2]) << 16); + [[fallthrough]]; + case 2: + a += (uint32_t(k[1]) << 8); + [[fallthrough]]; + case 1: + a += k[0]; + /* case 0: nothing left to add */ + } + hashmix(a, b, c); + + return c; +} + +CacheHash::Hash16_t CacheHash::Hash16(const char* aData, uint32_t aSize, + uint32_t aInitval) { + Hash32_t hash = Hash(aData, aSize, aInitval); + return (hash & 0xFFFF); +} + +NS_IMPL_ISUPPORTS0(CacheHash) + +CacheHash::CacheHash(uint32_t aInitval) : mC(aInitval) {} + +void CacheHash::Feed(uint32_t aVal, uint8_t aLen) { + switch (mPos) { + case 0: + mA += aVal; + mPos++; + break; + + case 1: + mB += aVal; + mPos++; + break; + + case 2: + mPos = 0; + if (aLen == 4) { + mC += aVal; + hashmix(mA, mB, mC); + } else { + mC += aVal << 8; + } + } + + mLength += aLen; +} + +void CacheHash::Update(const char* aData, uint32_t aLen) { + const uint8_t* data = reinterpret_cast(aData); + + MOZ_ASSERT(!mFinalized); + + if (mBufPos) { + while (mBufPos != 4 && aLen) { + mBuf += uint32_t(*data) << 8 * mBufPos; + data++; + mBufPos++; + aLen--; + } + + if (mBufPos == 4) { + mBufPos = 0; + Feed(mBuf); + mBuf = 0; + } + } + + if (!aLen) return; + + while (aLen >= 4) { + Feed(data[0] + (uint32_t(data[1]) << 8) + (uint32_t(data[2]) << 16) + + (uint32_t(data[3]) << 24)); + data += 4; + aLen -= 4; + } + + switch (aLen) { + case 3: + mBuf += data[2] << 16; + [[fallthrough]]; + case 2: + mBuf += data[1] << 8; + [[fallthrough]]; + case 1: + mBuf += data[0]; + } + + mBufPos = aLen; +} + +CacheHash::Hash32_t CacheHash::GetHash() { + if (!mFinalized) { + if (mBufPos) { + Feed(mBuf, mBufPos); + } + mC += mLength; + hashmix(mA, mB, mC); + mFinalized = true; + } + + return mC; +} + +CacheHash::Hash16_t CacheHash::GetHash16() { + Hash32_t hash = GetHash(); + return (hash & 0xFFFF); +} + +OriginAttrsHash GetOriginAttrsHash(const mozilla::OriginAttributes& aOA) { + nsAutoCString suffix; + aOA.CreateSuffix(suffix); + + SHA1Sum sum; + SHA1Sum::Hash hash; + sum.update(suffix.BeginReading(), suffix.Length()); + sum.finish(hash); + + return BigEndian::readUint64(&hash); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheHashUtils.h b/netwerk/cache2/CacheHashUtils.h new file mode 100644 index 0000000000..6ab7df972a --- /dev/null +++ b/netwerk/cache2/CacheHashUtils.h @@ -0,0 +1,69 @@ +/* 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/. */ + +#ifndef CacheHashUtils__h__ +#define CacheHashUtils__h__ + +#include "nsISupports.h" +#include "mozilla/Types.h" +#include "prnetdb.h" +#include "nsPrintfCString.h" + +#define LOGSHA1(x) \ + PR_htonl((reinterpret_cast(x))[0]), \ + PR_htonl((reinterpret_cast(x))[1]), \ + PR_htonl((reinterpret_cast(x))[2]), \ + PR_htonl((reinterpret_cast(x))[3]), \ + PR_htonl((reinterpret_cast(x))[4]) + +#define SHA1STRING(x) \ + (nsPrintfCString("%08x%08x%08x%08x%08x", LOGSHA1(x)).get()) + +namespace mozilla { + +class OriginAttributes; + +namespace net { + +class CacheHash : public nsISupports { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + using Hash16_t = uint16_t; + using Hash32_t = uint32_t; + + static Hash32_t Hash(const char* aData, uint32_t aSize, + uint32_t aInitval = 0); + static Hash16_t Hash16(const char* aData, uint32_t aSize, + uint32_t aInitval = 0); + + explicit CacheHash(uint32_t aInitval = 0); + + void Update(const char* aData, uint32_t aLen); + Hash32_t GetHash(); + Hash16_t GetHash16(); + + private: + virtual ~CacheHash() = default; + + void Feed(uint32_t aVal, uint8_t aLen = 4); + + static const uint32_t kGoldenRation = 0x9e3779b9; + + uint32_t mA{kGoldenRation}, mB{kGoldenRation}, mC; + uint8_t mPos{0}; + uint32_t mBuf{0}; + uint8_t mBufPos{0}; + uint32_t mLength{0}; + bool mFinalized{false}; +}; + +using OriginAttrsHash = uint64_t; + +OriginAttrsHash GetOriginAttrsHash(const mozilla::OriginAttributes& aOA); + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIOThread.cpp b/netwerk/cache2/CacheIOThread.cpp new file mode 100644 index 0000000000..fe41e67914 --- /dev/null +++ b/netwerk/cache2/CacheIOThread.cpp @@ -0,0 +1,583 @@ +/* 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 "CacheIOThread.h" +#include "CacheFileIOManager.h" +#include "CacheLog.h" +#include "CacheObserver.h" + +#include "nsIRunnable.h" +#include "nsISupportsImpl.h" +#include "nsPrintfCString.h" +#include "nsThread.h" +#include "nsThreadManager.h" +#include "nsThreadUtils.h" +#include "mozilla/EventQueue.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TelemetryHistogramEnums.h" + +#ifdef XP_WIN +# include +#endif + +namespace mozilla::net { + +namespace { // anon + +class CacheIOTelemetry { + public: + using size_type = CacheIOThread::EventQueue::size_type; + static size_type mMinLengthToReport[CacheIOThread::LAST_LEVEL]; + static void Report(uint32_t aLevel, size_type aLength); +}; + +static CacheIOTelemetry::size_type const kGranularity = 30; + +CacheIOTelemetry::size_type + CacheIOTelemetry::mMinLengthToReport[CacheIOThread::LAST_LEVEL] = { + kGranularity, kGranularity, kGranularity, kGranularity, + kGranularity, kGranularity, kGranularity, kGranularity}; + +// static +void CacheIOTelemetry::Report(uint32_t aLevel, + CacheIOTelemetry::size_type aLength) { + if (mMinLengthToReport[aLevel] > aLength) { + return; + } + + static Telemetry::HistogramID telemetryID[] = { + Telemetry::HTTP_CACHE_IO_QUEUE_2_OPEN_PRIORITY, + Telemetry::HTTP_CACHE_IO_QUEUE_2_READ_PRIORITY, + Telemetry::HTTP_CACHE_IO_QUEUE_2_MANAGEMENT, + Telemetry::HTTP_CACHE_IO_QUEUE_2_OPEN, + Telemetry::HTTP_CACHE_IO_QUEUE_2_READ, + Telemetry::HTTP_CACHE_IO_QUEUE_2_WRITE_PRIORITY, + Telemetry::HTTP_CACHE_IO_QUEUE_2_WRITE, + Telemetry::HTTP_CACHE_IO_QUEUE_2_INDEX, + Telemetry::HTTP_CACHE_IO_QUEUE_2_EVICT}; + + // Each bucket is a multiply of kGranularity (30, 60, 90..., 300+) + aLength = (aLength / kGranularity); + // Next time report only when over the current length + kGranularity + mMinLengthToReport[aLevel] = (aLength + 1) * kGranularity; + + // 10 is number of buckets we have in each probe + aLength = std::min(aLength, 10); + + Telemetry::Accumulate(telemetryID[aLevel], aLength - 1); // counted from 0 +} + +} // namespace + +namespace detail { + +/** + * Helper class encapsulating platform-specific code to cancel + * any pending IO operation taking too long. Solely used during + * shutdown to prevent any IO shutdown hangs. + * Mainly designed for using Win32 CancelSynchronousIo function. + */ +class NativeThreadHandle { +#ifdef XP_WIN + // The native handle to the thread + HANDLE mThread; +#endif + + public: + // Created and destroyed on the main thread only + NativeThreadHandle(); + ~NativeThreadHandle(); + + // Called on the IO thread to grab the platform specific + // reference to it. + void InitThread(); + // If there is a blocking operation being handled on the IO + // thread, this is called on the main thread during shutdown. + void CancelBlockingIO(Monitor& aMonitor); +}; + +#ifdef XP_WIN + +NativeThreadHandle::NativeThreadHandle() : mThread(NULL) {} + +NativeThreadHandle::~NativeThreadHandle() { + if (mThread) { + CloseHandle(mThread); + } +} + +void NativeThreadHandle::InitThread() { + // GetCurrentThread() only returns a pseudo handle, hence DuplicateHandle + ::DuplicateHandle(GetCurrentProcess(), GetCurrentThread(), + GetCurrentProcess(), &mThread, 0, FALSE, + DUPLICATE_SAME_ACCESS); +} + +void NativeThreadHandle::CancelBlockingIO(Monitor& aMonitor) { + HANDLE thread; + { + MonitorAutoLock lock(aMonitor); + thread = mThread; + + if (!thread) { + return; + } + } + + LOG(("CacheIOThread: Attempting to cancel a long blocking IO operation")); + BOOL result = ::CancelSynchronousIo(thread); + if (result) { + LOG((" cancelation signal succeeded")); + } else { + DWORD error = GetLastError(); + LOG((" cancelation signal failed with GetLastError=%lu", error)); + } +} + +#else // WIN + +// Stub code only (we don't implement IO cancelation for this platform) + +NativeThreadHandle::NativeThreadHandle() = default; +NativeThreadHandle::~NativeThreadHandle() = default; +void NativeThreadHandle::InitThread() {} +void NativeThreadHandle::CancelBlockingIO(Monitor&) {} + +#endif + +} // namespace detail + +CacheIOThread* CacheIOThread::sSelf = nullptr; + +NS_IMPL_ISUPPORTS(CacheIOThread, nsIThreadObserver) + +CacheIOThread::CacheIOThread() { + for (auto& item : mQueueLength) { + item = 0; + } + + sSelf = this; +} + +CacheIOThread::~CacheIOThread() { + if (mXPCOMThread) { + nsIThread* thread = mXPCOMThread; + thread->Release(); + } + + sSelf = nullptr; +#ifdef DEBUG + for (auto& event : mEventQueue) { + MOZ_ASSERT(!event.Length()); + } +#endif +} + +nsresult CacheIOThread::Init() { + { + MonitorAutoLock lock(mMonitor); + // Yeah, there is not a thread yet, but we want to make sure + // the sequencing is correct. + mNativeThreadHandle = MakeUnique(); + } + + // Increase the reference count while spawning a new thread. + // If PR_CreateThread succeeds, we will forget this reference and the thread + // will be responsible to release it when it completes. + RefPtr self = this; + mThread = + PR_CreateThread(PR_USER_THREAD, ThreadFunc, this, PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 128 * 1024); + if (!mThread) { + return NS_ERROR_FAILURE; + } + + // IMPORTANT: The thread now owns this reference, so it's important that we + // leak it here, otherwise we'll end up with a bad refcount. + // See the dont_AddRef in ThreadFunc(). + Unused << self.forget().take(); + + return NS_OK; +} + +nsresult CacheIOThread::Dispatch(nsIRunnable* aRunnable, uint32_t aLevel) { + return Dispatch(do_AddRef(aRunnable), aLevel); +} + +nsresult CacheIOThread::Dispatch(already_AddRefed aRunnable, + uint32_t aLevel) { + NS_ENSURE_ARG(aLevel < LAST_LEVEL); + + nsCOMPtr runnable(aRunnable); + + // Runnable is always expected to be non-null, hard null-check bellow. + MOZ_ASSERT(runnable); + + MonitorAutoLock lock(mMonitor); + + if (mShutdown && (PR_GetCurrentThread() != mThread)) { + return NS_ERROR_UNEXPECTED; + } + + return DispatchInternal(runnable.forget(), aLevel); +} + +nsresult CacheIOThread::DispatchAfterPendingOpens(nsIRunnable* aRunnable) { + // Runnable is always expected to be non-null, hard null-check bellow. + MOZ_ASSERT(aRunnable); + + MonitorAutoLock lock(mMonitor); + + if (mShutdown && (PR_GetCurrentThread() != mThread)) { + return NS_ERROR_UNEXPECTED; + } + + // Move everything from later executed OPEN level to the OPEN_PRIORITY level + // where we post the (eviction) runnable. + mQueueLength[OPEN_PRIORITY] += mEventQueue[OPEN].Length(); + mQueueLength[OPEN] -= mEventQueue[OPEN].Length(); + mEventQueue[OPEN_PRIORITY].AppendElements(mEventQueue[OPEN]); + mEventQueue[OPEN].Clear(); + + return DispatchInternal(do_AddRef(aRunnable), OPEN_PRIORITY); +} + +nsresult CacheIOThread::DispatchInternal( + already_AddRefed aRunnable, uint32_t aLevel) { + nsCOMPtr runnable(aRunnable); + + LogRunnable::LogDispatch(runnable.get()); + + if (NS_WARN_IF(!runnable)) return NS_ERROR_NULL_POINTER; + + mMonitor.AssertCurrentThreadOwns(); + + ++mQueueLength[aLevel]; + mEventQueue[aLevel].AppendElement(runnable.forget()); + if (mLowestLevelWaiting > aLevel) mLowestLevelWaiting = aLevel; + + mMonitor.NotifyAll(); + + return NS_OK; +} + +bool CacheIOThread::IsCurrentThread() { + return mThread == PR_GetCurrentThread(); +} + +uint32_t CacheIOThread::QueueSize(bool highPriority) { + MonitorAutoLock lock(mMonitor); + if (highPriority) { + return mQueueLength[OPEN_PRIORITY] + mQueueLength[READ_PRIORITY]; + } + + return mQueueLength[OPEN_PRIORITY] + mQueueLength[READ_PRIORITY] + + mQueueLength[MANAGEMENT] + mQueueLength[OPEN] + mQueueLength[READ]; +} + +bool CacheIOThread::YieldInternal() { + if (!IsCurrentThread()) { + NS_WARNING( + "Trying to yield to priority events on non-cache2 I/O thread? " + "You probably do something wrong."); + return false; + } + + if (mCurrentlyExecutingLevel == XPCOM_LEVEL) { + // Doesn't make any sense, since this handler is the one + // that would be executed as the next one. + return false; + } + + if (!EventsPending(mCurrentlyExecutingLevel)) return false; + + mRerunCurrentEvent = true; + return true; +} + +void CacheIOThread::Shutdown() { + if (!mThread) { + return; + } + + { + MonitorAutoLock lock(mMonitor); + mShutdown = true; + mMonitor.NotifyAll(); + } + + PR_JoinThread(mThread); + mThread = nullptr; +} + +void CacheIOThread::CancelBlockingIO() { + // This is an attempt to cancel any blocking I/O operation taking + // too long time. + if (!mNativeThreadHandle) { + return; + } + + if (!mIOCancelableEvents) { + LOG(("CacheIOThread::CancelBlockingIO, no blocking operation to cancel")); + return; + } + + // OK, when we are here, we are processing an IO on the thread that + // can be cancelled. + mNativeThreadHandle->CancelBlockingIO(mMonitor); +} + +already_AddRefed CacheIOThread::Target() { + nsCOMPtr target; + + target = mXPCOMThread; + if (!target && mThread) { + MonitorAutoLock lock(mMonitor); + while (!mXPCOMThread) { + lock.Wait(); + } + + target = mXPCOMThread; + } + + return target.forget(); +} + +// static +void CacheIOThread::ThreadFunc(void* aClosure) { + // XXXmstange We'd like to register this thread with the profiler, but doing + // so causes leaks, see bug 1323100. + NS_SetCurrentThreadName("Cache2 I/O"); + + mozilla::IOInterposer::RegisterCurrentThread(); + // We hold on to this reference for the duration of the thread. + RefPtr thread = + dont_AddRef(static_cast(aClosure)); + thread->ThreadFunc(); + mozilla::IOInterposer::UnregisterCurrentThread(); +} + +void CacheIOThread::ThreadFunc() { + nsCOMPtr threadInternal; + + { + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(mNativeThreadHandle); + mNativeThreadHandle->InitThread(); + + auto queue = + MakeRefPtr(MakeUnique()); + nsCOMPtr xpcomThread = + nsThreadManager::get().CreateCurrentThread(queue, + nsThread::NOT_MAIN_THREAD); + + threadInternal = do_QueryInterface(xpcomThread); + if (threadInternal) threadInternal->SetObserver(this); + + mXPCOMThread = xpcomThread.forget().take(); + nsCOMPtr thread = NS_GetCurrentThread(); + + lock.NotifyAll(); + + do { + loopStart: + // Reset the lowest level now, so that we can detect a new event on + // a lower level (i.e. higher priority) has been scheduled while + // executing any previously scheduled event. + mLowestLevelWaiting = LAST_LEVEL; + + // Process xpcom events first + while (mHasXPCOMEvents) { + mHasXPCOMEvents = false; + mCurrentlyExecutingLevel = XPCOM_LEVEL; + + MonitorAutoUnlock unlock(mMonitor); + + bool processedEvent; + nsresult rv; + do { + rv = thread->ProcessNextEvent(false, &processedEvent); + + ++mEventCounter; + MOZ_ASSERT(mNativeThreadHandle); + } while (NS_SUCCEEDED(rv) && processedEvent); + } + + uint32_t level; + for (level = 0; level < LAST_LEVEL; ++level) { + if (!mEventQueue[level].Length()) { + // no events on this level, go to the next level + continue; + } + + LoopOneLevel(level); + + // Go to the first (lowest) level again + goto loopStart; + } + + if (EventsPending()) { + continue; + } + + if (mShutdown) { + break; + } + + AUTO_PROFILER_LABEL("CacheIOThread::ThreadFunc::Wait", IDLE); + lock.Wait(); + + } while (true); + + MOZ_ASSERT(!EventsPending()); + +#ifdef DEBUG + // This is for correct assertion on XPCOM events dispatch. + mInsideLoop = false; +#endif + } // lock + + if (threadInternal) threadInternal->SetObserver(nullptr); +} + +void CacheIOThread::LoopOneLevel(uint32_t aLevel) { + mMonitor.AssertCurrentThreadOwns(); + EventQueue events = std::move(mEventQueue[aLevel]); + EventQueue::size_type length = events.Length(); + + mCurrentlyExecutingLevel = aLevel; + + bool returnEvents = false; + bool reportTelemetry = true; + + EventQueue::size_type index; + { + MonitorAutoUnlock unlock(mMonitor); + + for (index = 0; index < length; ++index) { + if (EventsPending(aLevel)) { + // Somebody scheduled a new event on a lower level, break and harry + // to execute it! Don't forget to return what we haven't exec. + returnEvents = true; + break; + } + + if (reportTelemetry) { + reportTelemetry = false; + CacheIOTelemetry::Report(aLevel, length); + } + + // Drop any previous flagging, only an event on the current level may set + // this flag. + mRerunCurrentEvent = false; + + LogRunnable::Run log(events[index].get()); + + events[index]->Run(); + + MOZ_ASSERT(mNativeThreadHandle); + + if (mRerunCurrentEvent) { + // The event handler yields to higher priority events and wants to + // rerun. + log.WillRunAgain(); + returnEvents = true; + break; + } + + ++mEventCounter; + --mQueueLength[aLevel]; + + // Release outside the lock. + events[index] = nullptr; + } + } + + if (returnEvents) { + // This code must prevent any AddRef/Release calls on the stored COMPtrs as + // it might be exhaustive and block the monitor's lock for an excessive + // amout of time. + + // 'index' points at the event that was interrupted and asked for re-run, + // all events before have run, been nullified, and can be removed. + events.RemoveElementsAt(0, index); + // Move events that might have been scheduled on this queue to the tail to + // preserve the expected per-queue FIFO order. + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + events.AppendElements(std::move(mEventQueue[aLevel])); + // And finally move everything back to the main queue. + mEventQueue[aLevel] = std::move(events); + } +} + +bool CacheIOThread::EventsPending(uint32_t aLastLevel) { + return mLowestLevelWaiting < aLastLevel || mHasXPCOMEvents; +} + +NS_IMETHODIMP CacheIOThread::OnDispatchedEvent() { + MonitorAutoLock lock(mMonitor); + mHasXPCOMEvents = true; + MOZ_ASSERT(mInsideLoop); + lock.Notify(); + return NS_OK; +} + +NS_IMETHODIMP CacheIOThread::OnProcessNextEvent(nsIThreadInternal* thread, + bool mayWait) { + return NS_OK; +} + +NS_IMETHODIMP CacheIOThread::AfterProcessNextEvent(nsIThreadInternal* thread, + bool eventWasProcessed) { + return NS_OK; +} + +// Memory reporting + +size_t CacheIOThread::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + MonitorAutoLock lock(const_cast(this)->mMonitor); + + size_t n = 0; + for (const auto& event : mEventQueue) { + n += event.ShallowSizeOfExcludingThis(mallocSizeOf); + // Events referenced by the queues are arbitrary objects we cannot be sure + // are reported elsewhere as well as probably not implementing nsISizeOf + // interface. Deliberatly omitting them from reporting here. + } + + return n; +} + +size_t CacheIOThread::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +CacheIOThread::Cancelable::Cancelable(bool aCancelable) + : mCancelable(aCancelable) { + // This will only ever be used on the I/O thread, + // which is expected to be alive longer than this class. + MOZ_ASSERT(CacheIOThread::sSelf); + MOZ_ASSERT(CacheIOThread::sSelf->IsCurrentThread()); + + if (mCancelable) { + ++CacheIOThread::sSelf->mIOCancelableEvents; + } +} + +CacheIOThread::Cancelable::~Cancelable() { + MOZ_ASSERT(CacheIOThread::sSelf); + + if (mCancelable) { + --CacheIOThread::sSelf->mIOCancelableEvents; + } +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheIOThread.h b/netwerk/cache2/CacheIOThread.h new file mode 100644 index 0000000000..4cb8a964b8 --- /dev/null +++ b/netwerk/cache2/CacheIOThread.h @@ -0,0 +1,149 @@ +/* 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/. */ + +#ifndef CacheIOThread__h__ +#define CacheIOThread__h__ + +#include "nsIThreadInternal.h" +#include "nsISupportsImpl.h" +#include "prthread.h" +#include "nsTArray.h" +#include "mozilla/Monitor.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Atomics.h" +#include "mozilla/UniquePtr.h" + +class nsIRunnable; + +namespace mozilla { +namespace net { + +namespace detail { +// A class keeping platform specific information needed to +// cancel any long blocking synchronous IO. Must be predeclared here +// since including windows.h breaks stuff with number of macro definition +// conflicts. +class NativeThreadHandle; +} // namespace detail + +class CacheIOThread final : public nsIThreadObserver { + virtual ~CacheIOThread(); + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + CacheIOThread(); + + using EventQueue = nsTArray>; + + enum ELevel : uint32_t { + OPEN_PRIORITY, + READ_PRIORITY, + MANAGEMENT, // Doesn't do any actual I/O + OPEN, + READ, + WRITE_PRIORITY, + WRITE, + INDEX, + EVICT, + LAST_LEVEL, + + // This is actually executed as the first level, but we want this enum + // value merely as an indicator while other values are used as indexes + // to the queue array. Hence put at end and not as the first. + XPCOM_LEVEL + }; + + nsresult Init(); + nsresult Dispatch(nsIRunnable* aRunnable, uint32_t aLevel); + nsresult Dispatch(already_AddRefed, uint32_t aLevel); + // Makes sure that any previously posted event to OPEN or OPEN_PRIORITY + // levels (such as file opennings and dooms) are executed before aRunnable + // that is intended to evict stuff from the cache. + nsresult DispatchAfterPendingOpens(nsIRunnable* aRunnable); + bool IsCurrentThread(); + + uint32_t QueueSize(bool highPriority); + + uint32_t EventCounter() const { return mEventCounter; } + + /** + * Callable only on this thread, checks if there is an event waiting in + * the event queue with a higher execution priority. If so, the result + * is true and the current event handler should break it's work and return + * from Run() method immediately. The event handler will be rerun again + * when all more priority events are processed. Events pending after this + * handler (i.e. the one that called YieldAndRerun()) will not execute sooner + * then this handler is executed w/o a call to YieldAndRerun(). + */ + static bool YieldAndRerun() { return sSelf ? sSelf->YieldInternal() : false; } + + void Shutdown(); + // This method checks if there is a long blocking IO on the + // IO thread and tries to cancel it. It waits maximum of + // two seconds. + void CancelBlockingIO(); + already_AddRefed Target(); + + // A stack class used to annotate running interruptable I/O event + class Cancelable { + bool mCancelable; + + public: + explicit Cancelable(bool aCancelable); + ~Cancelable(); + }; + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + + private: + static void ThreadFunc(void* aClosure); + void ThreadFunc(); + void LoopOneLevel(uint32_t aLevel) MOZ_REQUIRES(mMonitor); + bool EventsPending(uint32_t aLastLevel = LAST_LEVEL); + nsresult DispatchInternal(already_AddRefed aRunnable, + uint32_t aLevel); + bool YieldInternal(); + + static CacheIOThread* sSelf; + + mozilla::Monitor mMonitor{"CacheIOThread"}; + PRThread* mThread{nullptr}; + // Only set in Init(), before the thread is started, which reads it but never + // writes + UniquePtr mNativeThreadHandle; + Atomic mXPCOMThread{nullptr}; + Atomic mLowestLevelWaiting{LAST_LEVEL}; + uint32_t mCurrentlyExecutingLevel{0}; // only accessed on CacheIO Thread + + // Keeps the length of the each event queue, since LoopOneLevel moves all + // events into a local array. + Atomic mQueueLength[LAST_LEVEL]; + + EventQueue mEventQueue[LAST_LEVEL] MOZ_GUARDED_BY(mMonitor); + // Raised when nsIEventTarget.Dispatch() is called on this thread + Atomic mHasXPCOMEvents{false}; + // See YieldAndRerun() above + bool mRerunCurrentEvent{false}; // Only accessed on the cache thread + // Signal to process all pending events and then shutdown + // Synchronized by mMonitor + bool mShutdown MOZ_GUARDED_BY(mMonitor){false}; + // If > 0 there is currently an I/O operation on the thread that + // can be canceled when after shutdown, see the Shutdown() method + // for usage. Made a counter to allow nesting of the Cancelable class. + Atomic mIOCancelableEvents{0}; + // Event counter that increases with every event processed. + Atomic mEventCounter{0}; +#ifdef DEBUG + bool mInsideLoop MOZ_GUARDED_BY(mMonitor){true}; +#endif +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndex.cpp b/netwerk/cache2/CacheIndex.cpp new file mode 100644 index 0000000000..c0cb63ef6d --- /dev/null +++ b/netwerk/cache2/CacheIndex.cpp @@ -0,0 +1,3921 @@ +/* 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 "CacheIndex.h" + +#include "CacheLog.h" +#include "CacheFileIOManager.h" +#include "CacheFileMetadata.h" +#include "CacheFileUtils.h" +#include "CacheIndexIterator.h" +#include "CacheIndexContextIterator.h" +#include "nsThreadUtils.h" +#include "nsISizeOf.h" +#include "nsPrintfCString.h" +#include "mozilla/DebugOnly.h" +#include "prinrval.h" +#include "nsIFile.h" +#include "nsITimer.h" +#include "mozilla/AutoRestore.h" +#include +#include "mozilla/StaticPrefs_network.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" + +#define kMinUnwrittenChanges 300 +#define kMinDumpInterval 20000 // in milliseconds +#define kMaxBufSize 16384 +#define kIndexVersion 0x0000000A +#define kUpdateIndexStartDelay 50000 // in milliseconds +#define kTelemetryReportBytesLimit (2U * 1024U * 1024U * 1024U) // 2GB + +#define INDEX_NAME "index" +#define TEMP_INDEX_NAME "index.tmp" +#define JOURNAL_NAME "index.log" + +namespace mozilla::net { + +namespace { + +class FrecencyComparator { + public: + bool Equals(const RefPtr& a, + const RefPtr& b) const { + if (!a || !b) { + return false; + } + + return a->Get()->mFrecency == b->Get()->mFrecency; + } + bool LessThan(const RefPtr& a, + const RefPtr& b) const { + // Removed (=null) entries must be at the end of the array. + if (!a) { + return false; + } + if (!b) { + return true; + } + + // Place entries with frecency 0 at the end of the non-removed entries. + if (a->Get()->mFrecency == 0) { + return false; + } + if (b->Get()->mFrecency == 0) { + return true; + } + + return a->Get()->mFrecency < b->Get()->mFrecency; + } +}; + +} // namespace + +// used to dispatch a wrapper deletion the caller's thread +// cannot be used on IOThread after shutdown begins +class DeleteCacheIndexRecordWrapper : public Runnable { + CacheIndexRecordWrapper* mWrapper; + + public: + explicit DeleteCacheIndexRecordWrapper(CacheIndexRecordWrapper* wrapper) + : Runnable("net::CacheIndex::DeleteCacheIndexRecordWrapper"), + mWrapper(wrapper) {} + NS_IMETHOD Run() override { + StaticMutexAutoLock lock(CacheIndex::sLock); + + // if somehow the item is still in the frecency array, remove it + RefPtr index = CacheIndex::gInstance; + if (index) { + bool found = index->mFrecencyArray.RecordExistedUnlocked(mWrapper); + if (found) { + LOG( + ("DeleteCacheIndexRecordWrapper::Run() - \ + record wrapper found in frecency array during deletion")); + index->mFrecencyArray.RemoveRecord(mWrapper, lock); + } + } + + delete mWrapper; + return NS_OK; + } +}; + +void CacheIndexRecordWrapper::DispatchDeleteSelfToCurrentThread() { + // Dispatch during shutdown will not trigger DeleteCacheIndexRecordWrapper + nsCOMPtr event = new DeleteCacheIndexRecordWrapper(this); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(event)); +} + +CacheIndexRecordWrapper::~CacheIndexRecordWrapper() { +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + CacheIndex::sLock.AssertCurrentThreadOwns(); + RefPtr index = CacheIndex::gInstance; + if (index) { + bool found = index->mFrecencyArray.RecordExistedUnlocked(this); + MOZ_DIAGNOSTIC_ASSERT(!found); + } +#endif +} + +/** + * This helper class is responsible for keeping CacheIndex::mIndexStats and + * CacheIndex::mFrecencyArray up to date. + */ +class MOZ_RAII CacheIndexEntryAutoManage { + public: + CacheIndexEntryAutoManage(const SHA1Sum::Hash* aHash, CacheIndex* aIndex, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(CacheIndex::sLock) + : mIndex(aIndex), mProofOfLock(aProofOfLock) { + mHash = aHash; + const CacheIndexEntry* entry = FindEntry(); + mIndex->mIndexStats.BeforeChange(entry); + if (entry && entry->IsInitialized() && !entry->IsRemoved()) { + mOldRecord = entry->mRec; + mOldFrecency = entry->mRec->Get()->mFrecency; + } + } + + ~CacheIndexEntryAutoManage() MOZ_REQUIRES(CacheIndex::sLock) { + const CacheIndexEntry* entry = FindEntry(); + mIndex->mIndexStats.AfterChange(entry); + if (!entry || !entry->IsInitialized() || entry->IsRemoved()) { + entry = nullptr; + } + + if (entry && !mOldRecord) { + mIndex->mFrecencyArray.AppendRecord(entry->mRec, mProofOfLock); + mIndex->AddRecordToIterators(entry->mRec, mProofOfLock); + } else if (!entry && mOldRecord) { + mIndex->mFrecencyArray.RemoveRecord(mOldRecord, mProofOfLock); + mIndex->RemoveRecordFromIterators(mOldRecord, mProofOfLock); + } else if (entry && mOldRecord) { + if (entry->mRec != mOldRecord) { + // record has a different address, we have to replace it + mIndex->ReplaceRecordInIterators(mOldRecord, entry->mRec, mProofOfLock); + + if (entry->mRec->Get()->mFrecency == mOldFrecency) { + // If frecency hasn't changed simply replace the pointer + mIndex->mFrecencyArray.ReplaceRecord(mOldRecord, entry->mRec, + mProofOfLock); + } else { + // Remove old pointer and insert the new one at the end of the array + mIndex->mFrecencyArray.RemoveRecord(mOldRecord, mProofOfLock); + mIndex->mFrecencyArray.AppendRecord(entry->mRec, mProofOfLock); + } + } else if (entry->mRec->Get()->mFrecency != mOldFrecency) { + // Move the element at the end of the array + mIndex->mFrecencyArray.RemoveRecord(entry->mRec, mProofOfLock); + mIndex->mFrecencyArray.AppendRecord(entry->mRec, mProofOfLock); + } + } else { + // both entries were removed or not initialized, do nothing + } + } + + // We cannot rely on nsTHashtable::GetEntry() in case we are removing entries + // while iterating. Destructor is called before the entry is removed. Caller + // must call one of following methods to skip lookup in the hashtable. + void DoNotSearchInIndex() { mDoNotSearchInIndex = true; } + void DoNotSearchInUpdates() { mDoNotSearchInUpdates = true; } + + private: + const CacheIndexEntry* FindEntry() MOZ_REQUIRES(CacheIndex::sLock) { + const CacheIndexEntry* entry = nullptr; + + switch (mIndex->mState) { + case CacheIndex::READING: + case CacheIndex::WRITING: + if (!mDoNotSearchInUpdates) { + entry = mIndex->mPendingUpdates.GetEntry(*mHash); + } + [[fallthrough]]; + case CacheIndex::BUILDING: + case CacheIndex::UPDATING: + case CacheIndex::READY: + if (!entry && !mDoNotSearchInIndex) { + entry = mIndex->mIndex.GetEntry(*mHash); + } + break; + case CacheIndex::INITIAL: + case CacheIndex::SHUTDOWN: + default: + MOZ_ASSERT(false, "Unexpected state!"); + } + + return entry; + } + + const SHA1Sum::Hash* mHash; + RefPtr mIndex; + RefPtr mOldRecord; + uint32_t mOldFrecency{0}; + bool mDoNotSearchInIndex{false}; + bool mDoNotSearchInUpdates{false}; + const StaticMutexAutoLock& mProofOfLock; +}; + +class FileOpenHelper final : public CacheFileIOListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit FileOpenHelper(CacheIndex* aIndex) + : mIndex(aIndex), mCanceled(false) {} + + void Cancel() { + CacheIndex::sLock.AssertCurrentThreadOwns(); + mCanceled = true; + } + + private: + virtual ~FileOpenHelper() = default; + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override { + MOZ_CRASH("FileOpenHelper::OnDataWritten should not be called!"); + return NS_ERROR_UNEXPECTED; + } + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override { + MOZ_CRASH("FileOpenHelper::OnDataRead should not be called!"); + return NS_ERROR_UNEXPECTED; + } + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override { + MOZ_CRASH("FileOpenHelper::OnFileDoomed should not be called!"); + return NS_ERROR_UNEXPECTED; + } + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override { + MOZ_CRASH("FileOpenHelper::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; + } + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, + nsresult aResult) override { + MOZ_CRASH("FileOpenHelper::OnFileRenamed should not be called!"); + return NS_ERROR_UNEXPECTED; + } + + RefPtr mIndex; + bool mCanceled; +}; + +NS_IMETHODIMP FileOpenHelper::OnFileOpened(CacheFileHandle* aHandle, + nsresult aResult) { + StaticMutexAutoLock lock(CacheIndex::sLock); + + if (mCanceled) { + if (aHandle) { + CacheFileIOManager::DoomFile(aHandle, nullptr); + } + + return NS_OK; + } + + mIndex->OnFileOpenedInternal(this, aHandle, aResult, lock); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(FileOpenHelper, CacheFileIOListener); + +StaticRefPtr CacheIndex::gInstance; +StaticMutex CacheIndex::sLock; + +NS_IMPL_ADDREF(CacheIndex) +NS_IMPL_RELEASE(CacheIndex) + +NS_INTERFACE_MAP_BEGIN(CacheIndex) + NS_INTERFACE_MAP_ENTRY(mozilla::net::CacheFileIOListener) + NS_INTERFACE_MAP_ENTRY(nsIRunnable) +NS_INTERFACE_MAP_END + +CacheIndex::CacheIndex() { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::CacheIndex [this=%p]", this)); + MOZ_ASSERT(!gInstance, "multiple CacheIndex instances!"); +} + +CacheIndex::~CacheIndex() { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::~CacheIndex [this=%p]", this)); + + ReleaseBuffer(); +} + +// static +nsresult CacheIndex::Init(nsIFile* aCacheDirectory) { + LOG(("CacheIndex::Init()")); + + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sLock); + + if (gInstance) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + RefPtr idx = new CacheIndex(); + + nsresult rv = idx->InitInternal(aCacheDirectory, lock); + NS_ENSURE_SUCCESS(rv, rv); + + gInstance = std::move(idx); + return NS_OK; +} + +nsresult CacheIndex::InitInternal(nsIFile* aCacheDirectory, + const StaticMutexAutoLock& aProofOfLock) { + nsresult rv; + sLock.AssertCurrentThreadOwns(); + + rv = aCacheDirectory->Clone(getter_AddRefs(mCacheDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + mStartTime = TimeStamp::NowLoRes(); + + ReadIndexFromDisk(aProofOfLock); + + return NS_OK; +} + +// static +nsresult CacheIndex::PreShutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sLock); + + LOG(("CacheIndex::PreShutdown() [gInstance=%p]", gInstance.get())); + + nsresult rv; + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + LOG( + ("CacheIndex::PreShutdown() - [state=%d, indexOnDiskIsValid=%d, " + "dontMarkIndexClean=%d]", + index->mState, index->mIndexOnDiskIsValid, index->mDontMarkIndexClean)); + + LOG(("CacheIndex::PreShutdown() - Closing iterators.")); + for (uint32_t i = 0; i < index->mIterators.Length();) { + rv = index->mIterators[i]->CloseInternal(NS_ERROR_FAILURE); + if (NS_FAILED(rv)) { + // CacheIndexIterator::CloseInternal() removes itself from mIteratos iff + // it returns success. + LOG( + ("CacheIndex::PreShutdown() - Failed to remove iterator %p. " + "[rv=0x%08" PRIx32 "]", + index->mIterators[i], static_cast(rv))); + i++; + } + } + + index->mShuttingDown = true; + + if (index->mState == READY) { + return NS_OK; // nothing to do + } + + nsCOMPtr event; + event = NewRunnableMethod("net::CacheIndex::PreShutdownInternal", index, + &CacheIndex::PreShutdownInternal); + + nsCOMPtr ioTarget = CacheFileIOManager::IOTarget(); + MOZ_ASSERT(ioTarget); + + // PreShutdownInternal() will be executed before any queued event on INDEX + // level. That's OK since we don't want to wait for any operation in progess. + rv = ioTarget->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING("CacheIndex::PreShutdown() - Can't dispatch event"); + LOG(("CacheIndex::PreShutdown() - Can't dispatch event")); + return rv; + } + + return NS_OK; +} + +void CacheIndex::PreShutdownInternal() { + StaticMutexAutoLock lock(sLock); + + LOG( + ("CacheIndex::PreShutdownInternal() - [state=%d, indexOnDiskIsValid=%d, " + "dontMarkIndexClean=%d]", + mState, mIndexOnDiskIsValid, mDontMarkIndexClean)); + + MOZ_ASSERT(mShuttingDown); + + if (mUpdateTimer) { + mUpdateTimer->Cancel(); + mUpdateTimer = nullptr; + } + + switch (mState) { + case WRITING: + FinishWrite(false, lock); + break; + case READY: + // nothing to do, write the journal in Shutdown() + break; + case READING: + FinishRead(false, lock); + break; + case BUILDING: + case UPDATING: + FinishUpdate(false, lock); + break; + default: + MOZ_ASSERT(false, "Implement me!"); + } + + // We should end up in READY state + MOZ_ASSERT(mState == READY); +} + +// static +nsresult CacheIndex::Shutdown() { + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sLock); + + LOG(("CacheIndex::Shutdown() [gInstance=%p]", gInstance.get())); + + RefPtr index = gInstance.forget(); + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool sanitize = CacheObserver::ClearCacheOnShutdown(); + + LOG( + ("CacheIndex::Shutdown() - [state=%d, indexOnDiskIsValid=%d, " + "dontMarkIndexClean=%d, sanitize=%d]", + index->mState, index->mIndexOnDiskIsValid, index->mDontMarkIndexClean, + sanitize)); + + MOZ_ASSERT(index->mShuttingDown); + + EState oldState = index->mState; + index->ChangeState(SHUTDOWN, lock); + + if (oldState != READY) { + LOG( + ("CacheIndex::Shutdown() - Unexpected state. Did posting of " + "PreShutdownInternal() fail?")); + } + + switch (oldState) { + case WRITING: + index->FinishWrite(false, lock); + [[fallthrough]]; + case READY: + if (index->mIndexOnDiskIsValid && !index->mDontMarkIndexClean) { + if (!sanitize && NS_FAILED(index->WriteLogToDisk())) { + index->RemoveJournalAndTempFile(); + } + } else { + index->RemoveJournalAndTempFile(); + } + break; + case READING: + index->FinishRead(false, lock); + break; + case BUILDING: + case UPDATING: + index->FinishUpdate(false, lock); + break; + default: + MOZ_ASSERT(false, "Unexpected state!"); + } + + if (sanitize) { + index->RemoveAllIndexFiles(); + } + + return NS_OK; +} + +// static +nsresult CacheIndex::AddEntry(const SHA1Sum::Hash* aHash) { + LOG(("CacheIndex::AddEntry() [hash=%08x%08x%08x%08x%08x]", LOGSHA1(aHash))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Getters in CacheIndexStats assert when mStateLogged is true since the + // information is incomplete between calls to BeforeChange() and AfterChange() + // (i.e. while CacheIndexEntryAutoManage exists). We need to check whether + // non-fresh entries exists outside the scope of CacheIndexEntryAutoManage. + bool updateIfNonFreshEntriesExist = false; + + { + CacheIndexEntryAutoManage entryMng(aHash, index, lock); + + CacheIndexEntry* entry = index->mIndex.GetEntry(*aHash); + bool entryRemoved = entry && entry->IsRemoved(); + CacheIndexEntryUpdate* updated = nullptr; + + if (index->mState == READY || index->mState == UPDATING || + index->mState == BUILDING) { + MOZ_ASSERT(index->mPendingUpdates.Count() == 0); + + if (entry && !entryRemoved) { + // Found entry in index that shouldn't exist. + + if (entry->IsFresh()) { + // Someone removed the file on disk while FF is running. Update + // process can fix only non-fresh entries (i.e. entries that were not + // added within this session). Start update only if we have such + // entries. + // + // TODO: This should be very rare problem. If it turns out not to be + // true, change the update process so that it also iterates all + // initialized non-empty entries and checks whether the file exists. + + LOG( + ("CacheIndex::AddEntry() - Cache file was removed outside FF " + "process!")); + + updateIfNonFreshEntriesExist = true; + } else if (index->mState == READY) { + // Index is outdated, update it. + LOG( + ("CacheIndex::AddEntry() - Found entry that shouldn't exist, " + "update is needed")); + index->mIndexNeedsUpdate = true; + } else { + // We cannot be here when building index since all entries are fresh + // during building. + MOZ_ASSERT(index->mState == UPDATING); + } + } + + if (!entry) { + entry = index->mIndex.PutEntry(*aHash); + } + } else { // WRITING, READING + updated = index->mPendingUpdates.GetEntry(*aHash); + bool updatedRemoved = updated && updated->IsRemoved(); + + if ((updated && !updatedRemoved) || + (!updated && entry && !entryRemoved && entry->IsFresh())) { + // Fresh entry found, so the file was removed outside FF + LOG( + ("CacheIndex::AddEntry() - Cache file was removed outside FF " + "process!")); + + updateIfNonFreshEntriesExist = true; + } else if (!updated && entry && !entryRemoved) { + if (index->mState == WRITING) { + LOG( + ("CacheIndex::AddEntry() - Found entry that shouldn't exist, " + "update is needed")); + index->mIndexNeedsUpdate = true; + } + // Ignore if state is READING since the index information is partial + } + + updated = index->mPendingUpdates.PutEntry(*aHash); + } + + if (updated) { + updated->InitNew(); + updated->MarkDirty(); + updated->MarkFresh(); + } else { + entry->InitNew(); + entry->MarkDirty(); + entry->MarkFresh(); + } + } + + if (updateIfNonFreshEntriesExist && + index->mIndexStats.Count() != index->mIndexStats.Fresh()) { + index->mIndexNeedsUpdate = true; + } + + index->StartUpdatingIndexIfNeeded(lock); + index->WriteIndexToDiskIfNeeded(lock); + + return NS_OK; +} + +// static +nsresult CacheIndex::EnsureEntryExists(const SHA1Sum::Hash* aHash) { + LOG(("CacheIndex::EnsureEntryExists() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index, lock); + + CacheIndexEntry* entry = index->mIndex.GetEntry(*aHash); + bool entryRemoved = entry && entry->IsRemoved(); + + if (index->mState == READY || index->mState == UPDATING || + index->mState == BUILDING) { + MOZ_ASSERT(index->mPendingUpdates.Count() == 0); + + if (!entry || entryRemoved) { + if (entryRemoved && entry->IsFresh()) { + // This could happen only if somebody copies files to the entries + // directory while FF is running. + LOG( + ("CacheIndex::EnsureEntryExists() - Cache file was added outside " + "FF process! Update is needed.")); + index->mIndexNeedsUpdate = true; + } else if (index->mState == READY || + (entryRemoved && !entry->IsFresh())) { + // Removed non-fresh entries can be present as a result of + // MergeJournal() + LOG( + ("CacheIndex::EnsureEntryExists() - Didn't find entry that should" + " exist, update is needed")); + index->mIndexNeedsUpdate = true; + } + + if (!entry) { + entry = index->mIndex.PutEntry(*aHash); + } + entry->InitNew(); + entry->MarkDirty(); + } + entry->MarkFresh(); + } else { // WRITING, READING + CacheIndexEntryUpdate* updated = index->mPendingUpdates.GetEntry(*aHash); + bool updatedRemoved = updated && updated->IsRemoved(); + + if (updatedRemoved || (!updated && entryRemoved && entry->IsFresh())) { + // Fresh information about missing entry found. This could happen only + // if somebody copies files to the entries directory while FF is + // running. + LOG( + ("CacheIndex::EnsureEntryExists() - Cache file was added outside " + "FF process! Update is needed.")); + index->mIndexNeedsUpdate = true; + } else if (!updated && (!entry || entryRemoved)) { + if (index->mState == WRITING) { + LOG( + ("CacheIndex::EnsureEntryExists() - Didn't find entry that should" + " exist, update is needed")); + index->mIndexNeedsUpdate = true; + } + // Ignore if state is READING since the index information is partial + } + + // We don't need entryRemoved and updatedRemoved info anymore + if (entryRemoved) entry = nullptr; + if (updatedRemoved) updated = nullptr; + + if (updated) { + updated->MarkFresh(); + } else { + if (!entry) { + // Create a new entry + updated = index->mPendingUpdates.PutEntry(*aHash); + updated->InitNew(); + updated->MarkFresh(); + updated->MarkDirty(); + } else { + if (!entry->IsFresh()) { + // To mark the entry fresh we must make a copy of index entry + // since the index is read-only. + updated = index->mPendingUpdates.PutEntry(*aHash); + *updated = *entry; + updated->MarkFresh(); + } + } + } + } + } + + index->StartUpdatingIndexIfNeeded(lock); + index->WriteIndexToDiskIfNeeded(lock); + + return NS_OK; +} + +// static +nsresult CacheIndex::InitEntry(const SHA1Sum::Hash* aHash, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous, bool aPinned) { + LOG( + ("CacheIndex::InitEntry() [hash=%08x%08x%08x%08x%08x, " + "originAttrsHash=%" PRIx64 ", anonymous=%d, pinned=%d]", + LOGSHA1(aHash), aOriginAttrsHash, aAnonymous, aPinned)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index, lock); + + CacheIndexEntry* entry = index->mIndex.GetEntry(*aHash); + CacheIndexEntryUpdate* updated = nullptr; + bool reinitEntry = false; + + if (entry && entry->IsRemoved()) { + entry = nullptr; + } + + if (index->mState == READY || index->mState == UPDATING || + index->mState == BUILDING) { + MOZ_ASSERT(index->mPendingUpdates.Count() == 0); + MOZ_ASSERT(entry); + MOZ_ASSERT(entry->IsFresh()); + + if (!entry) { + LOG(("CacheIndex::InitEntry() - Entry was not found in mIndex!")); + NS_WARNING( + ("CacheIndex::InitEntry() - Entry was not found in mIndex!")); + return NS_ERROR_UNEXPECTED; + } + + if (IsCollision(entry, aOriginAttrsHash, aAnonymous)) { + index->mIndexNeedsUpdate = + true; // TODO Does this really help in case of collision? + reinitEntry = true; + } else { + if (entry->IsInitialized()) { + return NS_OK; + } + } + } else { + updated = index->mPendingUpdates.GetEntry(*aHash); + DebugOnly removed = updated && updated->IsRemoved(); + + MOZ_ASSERT(updated || !removed); + MOZ_ASSERT(updated || entry); + + if (!updated && !entry) { + LOG( + ("CacheIndex::InitEntry() - Entry was found neither in mIndex nor " + "in mPendingUpdates!")); + NS_WARNING( + ("CacheIndex::InitEntry() - Entry was found neither in " + "mIndex nor in mPendingUpdates!")); + return NS_ERROR_UNEXPECTED; + } + + if (updated) { + MOZ_ASSERT(updated->IsFresh()); + + if (IsCollision(updated, aOriginAttrsHash, aAnonymous)) { + index->mIndexNeedsUpdate = true; + reinitEntry = true; + } else { + if (updated->IsInitialized()) { + return NS_OK; + } + } + } else { + MOZ_ASSERT(entry->IsFresh()); + + if (IsCollision(entry, aOriginAttrsHash, aAnonymous)) { + index->mIndexNeedsUpdate = true; + reinitEntry = true; + } else { + if (entry->IsInitialized()) { + return NS_OK; + } + } + + // make a copy of a read-only entry + updated = index->mPendingUpdates.PutEntry(*aHash); + *updated = *entry; + } + } + + if (reinitEntry) { + // There is a collision and we are going to rewrite this entry. Initialize + // it as a new entry. + if (updated) { + updated->InitNew(); + updated->MarkFresh(); + } else { + entry->InitNew(); + entry->MarkFresh(); + } + } + + if (updated) { + updated->Init(aOriginAttrsHash, aAnonymous, aPinned); + updated->MarkDirty(); + } else { + entry->Init(aOriginAttrsHash, aAnonymous, aPinned); + entry->MarkDirty(); + } + } + + index->StartUpdatingIndexIfNeeded(lock); + index->WriteIndexToDiskIfNeeded(lock); + + return NS_OK; +} + +// static +nsresult CacheIndex::RemoveEntry(const SHA1Sum::Hash* aHash) { + LOG(("CacheIndex::RemoveEntry() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(aHash))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index, lock); + + CacheIndexEntry* entry = index->mIndex.GetEntry(*aHash); + bool entryRemoved = entry && entry->IsRemoved(); + + if (index->mState == READY || index->mState == UPDATING || + index->mState == BUILDING) { + MOZ_ASSERT(index->mPendingUpdates.Count() == 0); + + if (!entry || entryRemoved) { + if (entryRemoved && entry->IsFresh()) { + // This could happen only if somebody copies files to the entries + // directory while FF is running. + LOG( + ("CacheIndex::RemoveEntry() - Cache file was added outside FF " + "process! Update is needed.")); + index->mIndexNeedsUpdate = true; + } else if (index->mState == READY || + (entryRemoved && !entry->IsFresh())) { + // Removed non-fresh entries can be present as a result of + // MergeJournal() + LOG( + ("CacheIndex::RemoveEntry() - Didn't find entry that should exist" + ", update is needed")); + index->mIndexNeedsUpdate = true; + } + } else { + if (entry) { + if (!entry->IsDirty() && entry->IsFileEmpty()) { + index->mIndex.RemoveEntry(entry); + entry = nullptr; + } else { + entry->MarkRemoved(); + entry->MarkDirty(); + entry->MarkFresh(); + } + } + } + } else { // WRITING, READING + CacheIndexEntryUpdate* updated = index->mPendingUpdates.GetEntry(*aHash); + bool updatedRemoved = updated && updated->IsRemoved(); + + if (updatedRemoved || (!updated && entryRemoved && entry->IsFresh())) { + // Fresh information about missing entry found. This could happen only + // if somebody copies files to the entries directory while FF is + // running. + LOG( + ("CacheIndex::RemoveEntry() - Cache file was added outside FF " + "process! Update is needed.")); + index->mIndexNeedsUpdate = true; + } else if (!updated && (!entry || entryRemoved)) { + if (index->mState == WRITING) { + LOG( + ("CacheIndex::RemoveEntry() - Didn't find entry that should exist" + ", update is needed")); + index->mIndexNeedsUpdate = true; + } + // Ignore if state is READING since the index information is partial + } + + if (!updated) { + updated = index->mPendingUpdates.PutEntry(*aHash); + updated->InitNew(); + } + + updated->MarkRemoved(); + updated->MarkDirty(); + updated->MarkFresh(); + } + } + index->StartUpdatingIndexIfNeeded(lock); + index->WriteIndexToDiskIfNeeded(lock); + + return NS_OK; +} + +// static +nsresult CacheIndex::UpdateEntry(const SHA1Sum::Hash* aHash, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType, + const uint32_t* aSize) { + LOG( + ("CacheIndex::UpdateEntry() [hash=%08x%08x%08x%08x%08x, " + "frecency=%s, hasAltData=%s, onStartTime=%s, onStopTime=%s, " + "contentType=%s, size=%s]", + LOGSHA1(aHash), aFrecency ? nsPrintfCString("%u", *aFrecency).get() : "", + aHasAltData ? (*aHasAltData ? "true" : "false") : "", + aOnStartTime ? nsPrintfCString("%u", *aOnStartTime).get() : "", + aOnStopTime ? nsPrintfCString("%u", *aOnStopTime).get() : "", + aContentType ? nsPrintfCString("%u", *aContentType).get() : "", + aSize ? nsPrintfCString("%u", *aSize).get() : "")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index, lock); + + CacheIndexEntry* entry = index->mIndex.GetEntry(*aHash); + + if (entry && entry->IsRemoved()) { + entry = nullptr; + } + + if (index->mState == READY || index->mState == UPDATING || + index->mState == BUILDING) { + MOZ_ASSERT(index->mPendingUpdates.Count() == 0); + MOZ_ASSERT(entry); + + if (!entry) { + LOG(("CacheIndex::UpdateEntry() - Entry was not found in mIndex!")); + NS_WARNING( + ("CacheIndex::UpdateEntry() - Entry was not found in mIndex!")); + return NS_ERROR_UNEXPECTED; + } + + if (!HasEntryChanged(entry, aFrecency, aHasAltData, aOnStartTime, + aOnStopTime, aContentType, aSize)) { + return NS_OK; + } + + MOZ_ASSERT(entry->IsFresh()); + MOZ_ASSERT(entry->IsInitialized()); + entry->MarkDirty(); + + if (aFrecency) { + entry->SetFrecency(*aFrecency); + } + + if (aHasAltData) { + entry->SetHasAltData(*aHasAltData); + } + + if (aOnStartTime) { + entry->SetOnStartTime(*aOnStartTime); + } + + if (aOnStopTime) { + entry->SetOnStopTime(*aOnStopTime); + } + + if (aContentType) { + entry->SetContentType(*aContentType); + } + + if (aSize) { + entry->SetFileSize(*aSize); + } + } else { + CacheIndexEntryUpdate* updated = index->mPendingUpdates.GetEntry(*aHash); + DebugOnly removed = updated && updated->IsRemoved(); + + MOZ_ASSERT(updated || !removed); + MOZ_ASSERT(updated || entry); + + if (!updated) { + if (!entry) { + LOG( + ("CacheIndex::UpdateEntry() - Entry was found neither in mIndex " + "nor in mPendingUpdates!")); + NS_WARNING( + ("CacheIndex::UpdateEntry() - Entry was found neither in " + "mIndex nor in mPendingUpdates!")); + return NS_ERROR_UNEXPECTED; + } + + // make a copy of a read-only entry + updated = index->mPendingUpdates.PutEntry(*aHash); + *updated = *entry; + } + + MOZ_ASSERT(updated->IsFresh()); + MOZ_ASSERT(updated->IsInitialized()); + updated->MarkDirty(); + + if (aFrecency) { + updated->SetFrecency(*aFrecency); + } + + if (aHasAltData) { + updated->SetHasAltData(*aHasAltData); + } + + if (aOnStartTime) { + updated->SetOnStartTime(*aOnStartTime); + } + + if (aOnStopTime) { + updated->SetOnStopTime(*aOnStopTime); + } + + if (aContentType) { + updated->SetContentType(*aContentType); + } + + if (aSize) { + updated->SetFileSize(*aSize); + } + } + } + + index->WriteIndexToDiskIfNeeded(lock); + + return NS_OK; +} + +// static +nsresult CacheIndex::RemoveAll() { + LOG(("CacheIndex::RemoveAll()")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr file; + + { + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(!index->mRemovingAll); + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + AutoRestore saveRemovingAll(index->mRemovingAll); + index->mRemovingAll = true; + + // Doom index and journal handles but don't null them out since this will be + // done in FinishWrite/FinishRead methods. + if (index->mIndexHandle) { + CacheFileIOManager::DoomFile(index->mIndexHandle, nullptr); + } else { + // We don't have a handle to index file, so get the file here, but delete + // it outside the lock. Ignore the result since this is not fatal. + index->GetFile(nsLiteralCString(INDEX_NAME), getter_AddRefs(file)); + } + + if (index->mJournalHandle) { + CacheFileIOManager::DoomFile(index->mJournalHandle, nullptr); + } + + switch (index->mState) { + case WRITING: + index->FinishWrite(false, lock); + break; + case READY: + // nothing to do + break; + case READING: + index->FinishRead(false, lock); + break; + case BUILDING: + case UPDATING: + index->FinishUpdate(false, lock); + break; + default: + MOZ_ASSERT(false, "Unexpected state!"); + } + + // We should end up in READY state + MOZ_ASSERT(index->mState == READY); + + // There should not be any handle + MOZ_ASSERT(!index->mIndexHandle); + MOZ_ASSERT(!index->mJournalHandle); + + index->mIndexOnDiskIsValid = false; + index->mIndexNeedsUpdate = false; + + index->mIndexStats.Clear(); + index->mFrecencyArray.Clear(lock); + index->mIndex.Clear(); + + for (uint32_t i = 0; i < index->mIterators.Length();) { + nsresult rv = index->mIterators[i]->CloseInternal(NS_ERROR_NOT_AVAILABLE); + if (NS_FAILED(rv)) { + // CacheIndexIterator::CloseInternal() removes itself from mIterators + // iff it returns success. + LOG( + ("CacheIndex::RemoveAll() - Failed to remove iterator %p. " + "[rv=0x%08" PRIx32 "]", + index->mIterators[i], static_cast(rv))); + i++; + } + } + } + + if (file) { + // Ignore the result. The file might not exist and the failure is not fatal. + file->Remove(false); + } + + return NS_OK; +} + +// static +nsresult CacheIndex::HasEntry( + const nsACString& aKey, EntryStatus* _retval, + const std::function& aCB) { + LOG(("CacheIndex::HasEntry() [key=%s]", PromiseFlatCString(aKey).get())); + + SHA1Sum sum; + SHA1Sum::Hash hash; + sum.update(aKey.BeginReading(), aKey.Length()); + sum.finish(hash); + + return HasEntry(hash, _retval, aCB); +} + +// static +nsresult CacheIndex::HasEntry( + const SHA1Sum::Hash& hash, EntryStatus* _retval, + const std::function& aCB) { + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + const CacheIndexEntry* entry = nullptr; + + switch (index->mState) { + case READING: + case WRITING: + entry = index->mPendingUpdates.GetEntry(hash); + [[fallthrough]]; + case BUILDING: + case UPDATING: + case READY: + if (!entry) { + entry = index->mIndex.GetEntry(hash); + } + break; + case INITIAL: + case SHUTDOWN: + MOZ_ASSERT(false, "Unexpected state!"); + } + + if (!entry) { + if (index->mState == READY || index->mState == WRITING) { + *_retval = DOES_NOT_EXIST; + } else { + *_retval = DO_NOT_KNOW; + } + } else { + if (entry->IsRemoved()) { + if (entry->IsFresh()) { + *_retval = DOES_NOT_EXIST; + } else { + *_retval = DO_NOT_KNOW; + } + } else { + *_retval = EXISTS; + if (aCB) { + aCB(entry); + } + } + } + + LOG(("CacheIndex::HasEntry() - result is %u", *_retval)); + return NS_OK; +} + +// static +nsresult CacheIndex::GetEntryForEviction(bool aIgnoreEmptyEntries, + SHA1Sum::Hash* aHash, uint32_t* aCnt) { + LOG(("CacheIndex::GetEntryForEviction()")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) return NS_ERROR_NOT_INITIALIZED; + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (index->mIndexStats.Size() == 0) { + return NS_ERROR_NOT_AVAILABLE; + } + + int32_t mediaUsage = + round(static_cast(index->mIndexStats.SizeByType( + nsICacheEntry::CONTENT_TYPE_MEDIA)) * + 100.0 / static_cast(index->mIndexStats.Size())); + int32_t mediaUsageLimit = + StaticPrefs::browser_cache_disk_content_type_media_limit(); + bool evictMedia = false; + if (mediaUsage > mediaUsageLimit) { + LOG( + ("CacheIndex::GetEntryForEviction() - media content type is over the " + "limit [mediaUsage=%d, mediaUsageLimit=%d]", + mediaUsage, mediaUsageLimit)); + evictMedia = true; + } + + SHA1Sum::Hash hash; + CacheIndexRecord* foundRecord = nullptr; + uint32_t skipped = 0; + + // find first non-forced valid and unpinned entry with the lowest frecency + index->mFrecencyArray.SortIfNeeded(lock); + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + CacheIndexRecord* rec = iter.Get()->Get(); + + memcpy(&hash, rec->mHash, sizeof(SHA1Sum::Hash)); + + ++skipped; + + if (evictMedia && CacheIndexEntry::GetContentType(rec) != + nsICacheEntry::CONTENT_TYPE_MEDIA) { + continue; + } + + if (IsForcedValidEntry(&hash)) { + continue; + } + + if (CacheIndexEntry::IsPinned(rec)) { + continue; + } + + if (aIgnoreEmptyEntries && !CacheIndexEntry::GetFileSize(*rec)) { + continue; + } + + --skipped; + foundRecord = rec; + break; + } + + if (!foundRecord) return NS_ERROR_NOT_AVAILABLE; + + *aCnt = skipped; + + LOG( + ("CacheIndex::GetEntryForEviction() - returning entry " + "[hash=%08x%08x%08x%08x%08x, cnt=%u, frecency=%u, contentType=%u]", + LOGSHA1(&hash), *aCnt, foundRecord->mFrecency, + CacheIndexEntry::GetContentType(foundRecord))); + + memcpy(aHash, &hash, sizeof(SHA1Sum::Hash)); + + return NS_OK; +} + +// static +bool CacheIndex::IsForcedValidEntry(const SHA1Sum::Hash* aHash) { + RefPtr handle; + + CacheFileIOManager::gInstance->mHandles.GetHandle(aHash, + getter_AddRefs(handle)); + + if (!handle) return false; + + nsCString hashKey = handle->Key(); + return CacheStorageService::Self()->IsForcedValidEntry(hashKey); +} + +// static +nsresult CacheIndex::GetCacheSize(uint32_t* _retval) { + LOG(("CacheIndex::GetCacheSize()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) return NS_ERROR_NOT_INITIALIZED; + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + *_retval = index->mIndexStats.Size(); + LOG(("CacheIndex::GetCacheSize() - returning %u", *_retval)); + return NS_OK; +} + +// static +nsresult CacheIndex::GetEntryFileCount(uint32_t* _retval) { + LOG(("CacheIndex::GetEntryFileCount()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + *_retval = index->mIndexStats.ActiveEntriesCount(); + LOG(("CacheIndex::GetEntryFileCount() - returning %u", *_retval)); + return NS_OK; +} + +// static +nsresult CacheIndex::GetCacheStats(nsILoadContextInfo* aInfo, uint32_t* aSize, + uint32_t* aCount) { + LOG(("CacheIndex::GetCacheStats() [info=%p]", aInfo)); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aSize = 0; + *aCount = 0; + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + if (aInfo && + !CacheIndexEntry::RecordMatchesLoadContextInfo(iter.Get(), aInfo)) { + continue; + } + + *aSize += CacheIndexEntry::GetFileSize(*(iter.Get()->Get())); + ++*aCount; + } + + return NS_OK; +} + +// static +nsresult CacheIndex::AsyncGetDiskConsumption( + nsICacheStorageConsumptionObserver* aObserver) { + LOG(("CacheIndex::AsyncGetDiskConsumption()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr observer = + DiskConsumptionObserver::Init(aObserver); + + NS_ENSURE_ARG(observer); + + if ((index->mState == READY || index->mState == WRITING) && + !index->mAsyncGetDiskConsumptionBlocked) { + LOG(("CacheIndex::AsyncGetDiskConsumption - calling immediately")); + // Safe to call the callback under the lock, + // we always post to the main thread. + observer->OnDiskConsumption(index->mIndexStats.Size() << 10); + return NS_OK; + } + + LOG(("CacheIndex::AsyncGetDiskConsumption - remembering callback")); + // Will be called when the index get to the READY state. + index->mDiskConsumptionObservers.AppendElement(observer); + + // Move forward with index re/building if it is pending + RefPtr ioThread = CacheFileIOManager::IOThread(); + if (ioThread) { + ioThread->Dispatch( + NS_NewRunnableFunction("net::CacheIndex::AsyncGetDiskConsumption", + []() -> void { + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + if (index && index->mUpdateTimer) { + index->mUpdateTimer->Cancel(); + index->DelayedUpdateLocked(lock); + } + }), + CacheIOThread::INDEX); + } + + return NS_OK; +} + +// static +nsresult CacheIndex::GetIterator(nsILoadContextInfo* aInfo, bool aAddNew, + CacheIndexIterator** _retval) { + LOG(("CacheIndex::GetIterator() [info=%p, addNew=%d]", aInfo, aAddNew)); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr idxIter; + if (aInfo) { + idxIter = new CacheIndexContextIterator(index, aAddNew, aInfo); + } else { + idxIter = new CacheIndexIterator(index, aAddNew); + } + + index->mFrecencyArray.SortIfNeeded(lock); + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + idxIter->AddRecord(iter.Get(), lock); + } + + index->mIterators.AppendElement(idxIter); + idxIter.swap(*_retval); + return NS_OK; +} + +// static +nsresult CacheIndex::IsUpToDate(bool* _retval) { + LOG(("CacheIndex::IsUpToDate()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + *_retval = (index->mState == READY || index->mState == WRITING) && + !index->mIndexNeedsUpdate && !index->mShuttingDown; + + LOG(("CacheIndex::IsUpToDate() - returning %d", *_retval)); + return NS_OK; +} + +bool CacheIndex::IsIndexUsable() { + MOZ_ASSERT(mState != INITIAL); + + switch (mState) { + case INITIAL: + case SHUTDOWN: + return false; + + case READING: + case WRITING: + case BUILDING: + case UPDATING: + case READY: + break; + } + + return true; +} + +// static +bool CacheIndex::IsCollision(CacheIndexEntry* aEntry, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous) { + if (!aEntry->IsInitialized()) { + return false; + } + + if (aEntry->Anonymous() != aAnonymous || + aEntry->OriginAttrsHash() != aOriginAttrsHash) { + LOG( + ("CacheIndex::IsCollision() - Collision detected for entry hash=%08x" + "%08x%08x%08x%08x, expected values: originAttrsHash=%" PRIu64 ", " + "anonymous=%d; actual values: originAttrsHash=%" PRIu64 + ", anonymous=%d]", + LOGSHA1(aEntry->Hash()), aOriginAttrsHash, aAnonymous, + aEntry->OriginAttrsHash(), aEntry->Anonymous())); + return true; + } + + return false; +} + +// static +bool CacheIndex::HasEntryChanged( + CacheIndexEntry* aEntry, const uint32_t* aFrecency, const bool* aHasAltData, + const uint16_t* aOnStartTime, const uint16_t* aOnStopTime, + const uint8_t* aContentType, const uint32_t* aSize) { + if (aFrecency && *aFrecency != aEntry->GetFrecency()) { + return true; + } + + if (aHasAltData && *aHasAltData != aEntry->GetHasAltData()) { + return true; + } + + if (aOnStartTime && *aOnStartTime != aEntry->GetOnStartTime()) { + return true; + } + + if (aOnStopTime && *aOnStopTime != aEntry->GetOnStopTime()) { + return true; + } + + if (aContentType && *aContentType != aEntry->GetContentType()) { + return true; + } + + if (aSize && + (*aSize & CacheIndexEntry::kFileSizeMask) != aEntry->GetFileSize()) { + return true; + } + + return false; +} + +void CacheIndex::ProcessPendingOperations( + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::ProcessPendingOperations()")); + + for (auto iter = mPendingUpdates.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntryUpdate* update = iter.Get(); + + LOG(("CacheIndex::ProcessPendingOperations() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(update->Hash()))); + + MOZ_ASSERT(update->IsFresh()); + + CacheIndexEntry* entry = mIndex.GetEntry(*update->Hash()); + { + CacheIndexEntryAutoManage emng(update->Hash(), this, aProofOfLock); + emng.DoNotSearchInUpdates(); + + if (update->IsRemoved()) { + if (entry) { + if (entry->IsRemoved()) { + MOZ_ASSERT(entry->IsFresh()); + MOZ_ASSERT(entry->IsDirty()); + } else if (!entry->IsDirty() && entry->IsFileEmpty()) { + // Entries with empty file are not stored in index on disk. Just + // remove the entry, but only in case the entry is not dirty, i.e. + // the entry file was empty when we wrote the index. + mIndex.RemoveEntry(entry); + entry = nullptr; + } else { + entry->MarkRemoved(); + entry->MarkDirty(); + entry->MarkFresh(); + } + } + } else if (entry) { + // Some information in mIndex can be newer than in mPendingUpdates (see + // bug 1074832). This will copy just those values that were really + // updated. + update->ApplyUpdate(entry); + } else { + // There is no entry in mIndex, copy all information from + // mPendingUpdates to mIndex. + entry = mIndex.PutEntry(*update->Hash()); + *entry = *update; + } + } + iter.Remove(); + } + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + + EnsureCorrectStats(); +} + +bool CacheIndex::WriteIndexToDiskIfNeeded( + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + if (mState != READY || mShuttingDown || mRWPending) { + return false; + } + + if (!mLastDumpTime.IsNull() && + (TimeStamp::NowLoRes() - mLastDumpTime).ToMilliseconds() < + kMinDumpInterval) { + return false; + } + + if (mIndexStats.Dirty() < kMinUnwrittenChanges) { + return false; + } + + WriteIndexToDisk(aProofOfLock); + return true; +} + +void CacheIndex::WriteIndexToDisk(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::WriteIndexToDisk()")); + mIndexStats.Log(); + + nsresult rv; + + MOZ_ASSERT(mState == READY); + MOZ_ASSERT(!mRWBuf); + MOZ_ASSERT(!mRWHash); + MOZ_ASSERT(!mRWPending); + + ChangeState(WRITING, aProofOfLock); + + mProcessEntries = mIndexStats.ActiveEntriesCount(); + + mIndexFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile( + nsLiteralCString(TEMP_INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | CacheFileIOManager::CREATE, + mIndexFileOpener); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::WriteIndexToDisk() - Can't open file [rv=0x%08" PRIx32 + "]", + static_cast(rv))); + FinishWrite(false, aProofOfLock); + return; + } + + // Write index header to a buffer, it will be written to disk together with + // records in WriteRecords() once we open the file successfully. + AllocBuffer(); + mRWHash = new CacheHash(); + + mRWBufPos = 0; + // index version + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, kIndexVersion); + mRWBufPos += sizeof(uint32_t); + // timestamp + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, + static_cast(PR_Now() / PR_USEC_PER_SEC)); + mRWBufPos += sizeof(uint32_t); + // dirty flag + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, 1); + mRWBufPos += sizeof(uint32_t); + // amount of data written to the cache + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, + static_cast(mTotalBytesWritten >> 10)); + mRWBufPos += sizeof(uint32_t); + + mSkipEntries = 0; +} + +void CacheIndex::WriteRecords(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::WriteRecords()")); + + nsresult rv; + + MOZ_ASSERT(mState == WRITING); + MOZ_ASSERT(!mRWPending); + + int64_t fileOffset; + + if (mSkipEntries) { + MOZ_ASSERT(mRWBufPos == 0); + fileOffset = sizeof(CacheIndexHeader); + fileOffset += sizeof(CacheIndexRecord) * mSkipEntries; + } else { + MOZ_ASSERT(mRWBufPos == sizeof(CacheIndexHeader)); + fileOffset = 0; + } + uint32_t hashOffset = mRWBufPos; + + char* buf = mRWBuf + mRWBufPos; + uint32_t skip = mSkipEntries; + uint32_t processMax = (mRWBufSize - mRWBufPos) / sizeof(CacheIndexRecord); + MOZ_ASSERT(processMax != 0 || + mProcessEntries == + 0); // TODO make sure we can write an empty index + uint32_t processed = 0; +#ifdef DEBUG + bool hasMore = false; +#endif + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntry* entry = iter.Get(); + if (entry->IsRemoved() || !entry->IsInitialized() || entry->IsFileEmpty()) { + continue; + } + + if (skip) { + skip--; + continue; + } + + if (processed == processMax) { +#ifdef DEBUG + hasMore = true; +#endif + break; + } + + entry->WriteToBuf(buf); + buf += sizeof(CacheIndexRecord); + processed++; + } + + MOZ_ASSERT(mRWBufPos != static_cast(buf - mRWBuf) || + mProcessEntries == 0); + mRWBufPos = buf - mRWBuf; + mSkipEntries += processed; + MOZ_ASSERT(mSkipEntries <= mProcessEntries); + + mRWHash->Update(mRWBuf + hashOffset, mRWBufPos - hashOffset); + + if (mSkipEntries == mProcessEntries) { + MOZ_ASSERT(!hasMore); + + // We've processed all records + if (mRWBufPos + sizeof(CacheHash::Hash32_t) > mRWBufSize) { + // realloc buffer to spare another write cycle + mRWBufSize = mRWBufPos + sizeof(CacheHash::Hash32_t); + mRWBuf = static_cast(moz_xrealloc(mRWBuf, mRWBufSize)); + } + + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, mRWHash->GetHash()); + mRWBufPos += sizeof(CacheHash::Hash32_t); + } else { + MOZ_ASSERT(hasMore); + } + + rv = CacheFileIOManager::Write(mIndexHandle, fileOffset, mRWBuf, mRWBufPos, + mSkipEntries == mProcessEntries, false, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::WriteRecords() - CacheFileIOManager::Write() failed " + "synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishWrite(false, aProofOfLock); + } else { + mRWPending = true; + } + + mRWBufPos = 0; +} + +void CacheIndex::FinishWrite(bool aSucceeded, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::FinishWrite() [succeeded=%d]", aSucceeded)); + + MOZ_ASSERT((!aSucceeded && mState == SHUTDOWN) || mState == WRITING); + + // If there is write operation pending we must be cancelling writing of the + // index when shutting down or removing the whole index. + MOZ_ASSERT(!mRWPending || (!aSucceeded && (mShuttingDown || mRemovingAll))); + + mIndexHandle = nullptr; + mRWHash = nullptr; + ReleaseBuffer(); + + if (aSucceeded) { + // Opening of the file must not be in progress if writing succeeded. + MOZ_ASSERT(!mIndexFileOpener); + + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntry* entry = iter.Get(); + + bool remove = false; + { + CacheIndexEntryAutoManage emng(entry->Hash(), this, aProofOfLock); + + if (entry->IsRemoved()) { + emng.DoNotSearchInIndex(); + remove = true; + } else if (entry->IsDirty()) { + entry->ClearDirty(); + } + } + if (remove) { + iter.Remove(); + } + } + + mIndexOnDiskIsValid = true; + } else { + if (mIndexFileOpener) { + // If opening of the file is still in progress (e.g. WRITE process was + // canceled by RemoveAll()) then we need to cancel the opener to make sure + // that OnFileOpenedInternal() won't be called. + mIndexFileOpener->Cancel(); + mIndexFileOpener = nullptr; + } + } + + ProcessPendingOperations(aProofOfLock); + mIndexStats.Log(); + + if (mState == WRITING) { + ChangeState(READY, aProofOfLock); + mLastDumpTime = TimeStamp::NowLoRes(); + } +} + +nsresult CacheIndex::GetFile(const nsACString& aName, nsIFile** _retval) { + nsresult rv; + + nsCOMPtr file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(aName); + NS_ENSURE_SUCCESS(rv, rv); + + file.swap(*_retval); + return NS_OK; +} + +void CacheIndex::RemoveFile(const nsACString& aName) { + MOZ_ASSERT(mState == SHUTDOWN); + + nsresult rv; + + nsCOMPtr file; + rv = GetFile(aName, getter_AddRefs(file)); + NS_ENSURE_SUCCESS_VOID(rv); + + rv = file->Remove(false); + if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) { + LOG( + ("CacheIndex::RemoveFile() - Cannot remove old entry file from disk " + "[rv=0x%08" PRIx32 ", name=%s]", + static_cast(rv), PromiseFlatCString(aName).get())); + } +} + +void CacheIndex::RemoveAllIndexFiles() { + LOG(("CacheIndex::RemoveAllIndexFiles()")); + RemoveFile(nsLiteralCString(INDEX_NAME)); + RemoveJournalAndTempFile(); +} + +void CacheIndex::RemoveJournalAndTempFile() { + LOG(("CacheIndex::RemoveJournalAndTempFile()")); + RemoveFile(nsLiteralCString(TEMP_INDEX_NAME)); + RemoveFile(nsLiteralCString(JOURNAL_NAME)); +} + +class WriteLogHelper { + public: + explicit WriteLogHelper(PRFileDesc* aFD) + : mFD(aFD), mBufSize(kMaxBufSize), mBufPos(0) { + mHash = new CacheHash(); + mBuf = static_cast(moz_xmalloc(mBufSize)); + } + + ~WriteLogHelper() { free(mBuf); } + + nsresult AddEntry(CacheIndexEntry* aEntry); + nsresult Finish(); + + private: + nsresult FlushBuffer(); + + PRFileDesc* mFD; + char* mBuf; + uint32_t mBufSize; + int32_t mBufPos; + RefPtr mHash; +}; + +nsresult WriteLogHelper::AddEntry(CacheIndexEntry* aEntry) { + nsresult rv; + + if (mBufPos + sizeof(CacheIndexRecord) > mBufSize) { + mHash->Update(mBuf, mBufPos); + + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(mBufPos + sizeof(CacheIndexRecord) <= mBufSize); + } + + aEntry->WriteToBuf(mBuf + mBufPos); + mBufPos += sizeof(CacheIndexRecord); + + return NS_OK; +} + +nsresult WriteLogHelper::Finish() { + nsresult rv; + + mHash->Update(mBuf, mBufPos); + if (mBufPos + sizeof(CacheHash::Hash32_t) > mBufSize) { + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ASSERT(mBufPos + sizeof(CacheHash::Hash32_t) <= mBufSize); + } + + NetworkEndian::writeUint32(mBuf + mBufPos, mHash->GetHash()); + mBufPos += sizeof(CacheHash::Hash32_t); + + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult WriteLogHelper::FlushBuffer() { + if (CacheObserver::IsPastShutdownIOLag()) { + LOG(("WriteLogHelper::FlushBuffer() - Interrupting writing journal.")); + return NS_ERROR_FAILURE; + } + + int32_t bytesWritten = PR_Write(mFD, mBuf, mBufPos); + + if (bytesWritten != mBufPos) { + return NS_ERROR_FAILURE; + } + + mBufPos = 0; + return NS_OK; +} + +nsresult CacheIndex::WriteLogToDisk() { + LOG(("CacheIndex::WriteLogToDisk()")); + + nsresult rv; + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + MOZ_ASSERT(mState == SHUTDOWN); + + if (CacheObserver::IsPastShutdownIOLag()) { + LOG(("CacheIndex::WriteLogToDisk() - Skipping writing journal.")); + return NS_ERROR_FAILURE; + } + + RemoveFile(nsLiteralCString(TEMP_INDEX_NAME)); + + nsCOMPtr indexFile; + rv = GetFile(nsLiteralCString(INDEX_NAME), getter_AddRefs(indexFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr logFile; + rv = GetFile(nsLiteralCString(JOURNAL_NAME), getter_AddRefs(logFile)); + NS_ENSURE_SUCCESS(rv, rv); + + mIndexStats.Log(); + + PRFileDesc* fd = nullptr; + rv = logFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, 0600, + &fd); + NS_ENSURE_SUCCESS(rv, rv); + + WriteLogHelper wlh(fd); + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntry* entry = iter.Get(); + if (entry->IsRemoved() || entry->IsDirty()) { + rv = wlh.AddEntry(entry); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + } + + rv = wlh.Finish(); + PR_Close(fd); + NS_ENSURE_SUCCESS(rv, rv); + + rv = indexFile->OpenNSPRFileDesc(PR_RDWR, 0600, &fd); + NS_ENSURE_SUCCESS(rv, rv); + + // Seek to dirty flag in the index header and clear it. + static_assert(2 * sizeof(uint32_t) == offsetof(CacheIndexHeader, mIsDirty), + "Unexpected offset of CacheIndexHeader::mIsDirty"); + int64_t offset = PR_Seek64(fd, 2 * sizeof(uint32_t), PR_SEEK_SET); + if (offset == -1) { + PR_Close(fd); + return NS_ERROR_FAILURE; + } + + uint32_t isDirty = 0; + int32_t bytesWritten = PR_Write(fd, &isDirty, sizeof(isDirty)); + PR_Close(fd); + if (bytesWritten != sizeof(isDirty)) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void CacheIndex::ReadIndexFromDisk(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::ReadIndexFromDisk()")); + + nsresult rv; + + MOZ_ASSERT(mState == INITIAL); + + ChangeState(READING, aProofOfLock); + + mIndexFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile( + nsLiteralCString(INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | CacheFileIOManager::OPEN, + mIndexFileOpener); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08" PRIx32 ", file=%s]", + static_cast(rv), INDEX_NAME)); + FinishRead(false, aProofOfLock); + return; + } + + mJournalFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile( + nsLiteralCString(JOURNAL_NAME), + CacheFileIOManager::SPECIAL_FILE | CacheFileIOManager::OPEN, + mJournalFileOpener); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08" PRIx32 ", file=%s]", + static_cast(rv), JOURNAL_NAME)); + FinishRead(false, aProofOfLock); + } + + mTmpFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile( + nsLiteralCString(TEMP_INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | CacheFileIOManager::OPEN, + mTmpFileOpener); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08" PRIx32 ", file=%s]", + static_cast(rv), TEMP_INDEX_NAME)); + FinishRead(false, aProofOfLock); + } +} + +void CacheIndex::StartReadingIndex(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::StartReadingIndex()")); + + nsresult rv; + + MOZ_ASSERT(mIndexHandle); + MOZ_ASSERT(mState == READING); + MOZ_ASSERT(!mIndexOnDiskIsValid); + MOZ_ASSERT(!mDontMarkIndexClean); + MOZ_ASSERT(!mJournalReadSuccessfully); + MOZ_ASSERT(mIndexHandle->FileSize() >= 0); + MOZ_ASSERT(!mRWPending); + + int64_t entriesSize = mIndexHandle->FileSize() - sizeof(CacheIndexHeader) - + sizeof(CacheHash::Hash32_t); + + if (entriesSize < 0 || entriesSize % sizeof(CacheIndexRecord)) { + LOG(("CacheIndex::StartReadingIndex() - Index is corrupted")); + FinishRead(false, aProofOfLock); + return; + } + + AllocBuffer(); + mSkipEntries = 0; + mRWHash = new CacheHash(); + + mRWBufPos = + std::min(mRWBufSize, static_cast(mIndexHandle->FileSize())); + + rv = CacheFileIOManager::Read(mIndexHandle, 0, mRWBuf, mRWBufPos, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::StartReadingIndex() - CacheFileIOManager::Read() failed " + "synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishRead(false, aProofOfLock); + } else { + mRWPending = true; + } +} + +void CacheIndex::ParseRecords(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::ParseRecords()")); + + nsresult rv; + + MOZ_ASSERT(!mRWPending); + + uint32_t entryCnt = (mIndexHandle->FileSize() - sizeof(CacheIndexHeader) - + sizeof(CacheHash::Hash32_t)) / + sizeof(CacheIndexRecord); + uint32_t pos = 0; + + if (!mSkipEntries) { + if (NetworkEndian::readUint32(mRWBuf + pos) != kIndexVersion) { + FinishRead(false, aProofOfLock); + return; + } + pos += sizeof(uint32_t); + + mIndexTimeStamp = NetworkEndian::readUint32(mRWBuf + pos); + pos += sizeof(uint32_t); + + if (NetworkEndian::readUint32(mRWBuf + pos)) { + if (mJournalHandle) { + CacheFileIOManager::DoomFile(mJournalHandle, nullptr); + mJournalHandle = nullptr; + } + } else { + uint32_t* isDirty = + reinterpret_cast(moz_xmalloc(sizeof(uint32_t))); + NetworkEndian::writeUint32(isDirty, 1); + + // Mark index dirty. The buffer is freed by CacheFileIOManager when + // nullptr is passed as the listener and the call doesn't fail + // synchronously. + rv = CacheFileIOManager::Write(mIndexHandle, 2 * sizeof(uint32_t), + reinterpret_cast(isDirty), + sizeof(uint32_t), true, false, nullptr); + if (NS_FAILED(rv)) { + // This is not fatal, just free the memory + free(isDirty); + } + } + pos += sizeof(uint32_t); + + uint64_t dataWritten = NetworkEndian::readUint32(mRWBuf + pos); + pos += sizeof(uint32_t); + dataWritten <<= 10; + mTotalBytesWritten += dataWritten; + } + + uint32_t hashOffset = pos; + + while (pos + sizeof(CacheIndexRecord) <= mRWBufPos && + mSkipEntries != entryCnt) { + CacheIndexRecord* rec = reinterpret_cast(mRWBuf + pos); + CacheIndexEntry tmpEntry(&rec->mHash); + tmpEntry.ReadFromBuf(mRWBuf + pos); + + if (tmpEntry.IsDirty() || !tmpEntry.IsInitialized() || + tmpEntry.IsFileEmpty() || tmpEntry.IsFresh() || tmpEntry.IsRemoved()) { + LOG( + ("CacheIndex::ParseRecords() - Invalid entry found in index, removing" + " whole index [dirty=%d, initialized=%d, fileEmpty=%d, fresh=%d, " + "removed=%d]", + tmpEntry.IsDirty(), tmpEntry.IsInitialized(), tmpEntry.IsFileEmpty(), + tmpEntry.IsFresh(), tmpEntry.IsRemoved())); + FinishRead(false, aProofOfLock); + return; + } + + CacheIndexEntryAutoManage emng(tmpEntry.Hash(), this, aProofOfLock); + + CacheIndexEntry* entry = mIndex.PutEntry(*tmpEntry.Hash()); + *entry = tmpEntry; + + pos += sizeof(CacheIndexRecord); + mSkipEntries++; + } + + mRWHash->Update(mRWBuf + hashOffset, pos - hashOffset); + + if (pos != mRWBufPos) { + memmove(mRWBuf, mRWBuf + pos, mRWBufPos - pos); + } + + mRWBufPos -= pos; + pos = 0; + + int64_t fileOffset = sizeof(CacheIndexHeader) + + mSkipEntries * sizeof(CacheIndexRecord) + mRWBufPos; + + MOZ_ASSERT(fileOffset <= mIndexHandle->FileSize()); + if (fileOffset == mIndexHandle->FileSize()) { + uint32_t expectedHash = NetworkEndian::readUint32(mRWBuf); + if (mRWHash->GetHash() != expectedHash) { + LOG(("CacheIndex::ParseRecords() - Hash mismatch, [is %x, should be %x]", + mRWHash->GetHash(), expectedHash)); + FinishRead(false, aProofOfLock); + return; + } + + mIndexOnDiskIsValid = true; + mJournalReadSuccessfully = false; + + if (mJournalHandle) { + StartReadingJournal(aProofOfLock); + } else { + FinishRead(false, aProofOfLock); + } + + return; + } + + pos = mRWBufPos; + uint32_t toRead = + std::min(mRWBufSize - pos, + static_cast(mIndexHandle->FileSize() - fileOffset)); + mRWBufPos = pos + toRead; + + rv = CacheFileIOManager::Read(mIndexHandle, fileOffset, mRWBuf + pos, toRead, + this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::ParseRecords() - CacheFileIOManager::Read() failed " + "synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishRead(false, aProofOfLock); + return; + } + mRWPending = true; +} + +void CacheIndex::StartReadingJournal(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::StartReadingJournal()")); + + nsresult rv; + + MOZ_ASSERT(mJournalHandle); + MOZ_ASSERT(mIndexOnDiskIsValid); + MOZ_ASSERT(mTmpJournal.Count() == 0); + MOZ_ASSERT(mJournalHandle->FileSize() >= 0); + MOZ_ASSERT(!mRWPending); + + int64_t entriesSize = + mJournalHandle->FileSize() - sizeof(CacheHash::Hash32_t); + + if (entriesSize < 0 || entriesSize % sizeof(CacheIndexRecord)) { + LOG(("CacheIndex::StartReadingJournal() - Journal is corrupted")); + FinishRead(false, aProofOfLock); + return; + } + + mSkipEntries = 0; + mRWHash = new CacheHash(); + + mRWBufPos = + std::min(mRWBufSize, static_cast(mJournalHandle->FileSize())); + + rv = CacheFileIOManager::Read(mJournalHandle, 0, mRWBuf, mRWBufPos, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::StartReadingJournal() - CacheFileIOManager::Read() failed" + " synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishRead(false, aProofOfLock); + } else { + mRWPending = true; + } +} + +void CacheIndex::ParseJournal(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::ParseJournal()")); + + nsresult rv; + + MOZ_ASSERT(!mRWPending); + + uint32_t entryCnt = + (mJournalHandle->FileSize() - sizeof(CacheHash::Hash32_t)) / + sizeof(CacheIndexRecord); + + uint32_t pos = 0; + + while (pos + sizeof(CacheIndexRecord) <= mRWBufPos && + mSkipEntries != entryCnt) { + CacheIndexEntry tmpEntry(reinterpret_cast(mRWBuf + pos)); + tmpEntry.ReadFromBuf(mRWBuf + pos); + + CacheIndexEntry* entry = mTmpJournal.PutEntry(*tmpEntry.Hash()); + *entry = tmpEntry; + + if (entry->IsDirty() || entry->IsFresh()) { + LOG( + ("CacheIndex::ParseJournal() - Invalid entry found in journal, " + "ignoring whole journal [dirty=%d, fresh=%d]", + entry->IsDirty(), entry->IsFresh())); + FinishRead(false, aProofOfLock); + return; + } + + pos += sizeof(CacheIndexRecord); + mSkipEntries++; + } + + mRWHash->Update(mRWBuf, pos); + + if (pos != mRWBufPos) { + memmove(mRWBuf, mRWBuf + pos, mRWBufPos - pos); + } + + mRWBufPos -= pos; + pos = 0; + + int64_t fileOffset = mSkipEntries * sizeof(CacheIndexRecord) + mRWBufPos; + + MOZ_ASSERT(fileOffset <= mJournalHandle->FileSize()); + if (fileOffset == mJournalHandle->FileSize()) { + uint32_t expectedHash = NetworkEndian::readUint32(mRWBuf); + if (mRWHash->GetHash() != expectedHash) { + LOG(("CacheIndex::ParseJournal() - Hash mismatch, [is %x, should be %x]", + mRWHash->GetHash(), expectedHash)); + FinishRead(false, aProofOfLock); + return; + } + + mJournalReadSuccessfully = true; + FinishRead(true, aProofOfLock); + return; + } + + pos = mRWBufPos; + uint32_t toRead = + std::min(mRWBufSize - pos, + static_cast(mJournalHandle->FileSize() - fileOffset)); + mRWBufPos = pos + toRead; + + rv = CacheFileIOManager::Read(mJournalHandle, fileOffset, mRWBuf + pos, + toRead, this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::ParseJournal() - CacheFileIOManager::Read() failed " + "synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishRead(false, aProofOfLock); + return; + } + mRWPending = true; +} + +void CacheIndex::MergeJournal(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::MergeJournal()")); + + for (auto iter = mTmpJournal.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntry* entry = iter.Get(); + + LOG(("CacheIndex::MergeJournal() [hash=%08x%08x%08x%08x%08x]", + LOGSHA1(entry->Hash()))); + + CacheIndexEntry* entry2 = mIndex.GetEntry(*entry->Hash()); + { + CacheIndexEntryAutoManage emng(entry->Hash(), this, aProofOfLock); + if (entry->IsRemoved()) { + if (entry2) { + entry2->MarkRemoved(); + entry2->MarkDirty(); + } + } else { + if (!entry2) { + entry2 = mIndex.PutEntry(*entry->Hash()); + } + + *entry2 = *entry; + entry2->MarkDirty(); + } + } + iter.Remove(); + } + + MOZ_ASSERT(mTmpJournal.Count() == 0); +} + +void CacheIndex::EnsureNoFreshEntry() { +#ifdef DEBUG_STATS + CacheIndexStats debugStats; + debugStats.DisableLogging(); + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + debugStats.BeforeChange(nullptr); + debugStats.AfterChange(iter.Get()); + } + MOZ_ASSERT(debugStats.Fresh() == 0); +#endif +} + +void CacheIndex::EnsureCorrectStats() { +#ifdef DEBUG_STATS + MOZ_ASSERT(mPendingUpdates.Count() == 0); + CacheIndexStats debugStats; + debugStats.DisableLogging(); + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + debugStats.BeforeChange(nullptr); + debugStats.AfterChange(iter.Get()); + } + MOZ_ASSERT(debugStats == mIndexStats); +#endif +} + +void CacheIndex::FinishRead(bool aSucceeded, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::FinishRead() [succeeded=%d]", aSucceeded)); + + MOZ_ASSERT((!aSucceeded && mState == SHUTDOWN) || mState == READING); + + MOZ_ASSERT( + // -> rebuild + (!aSucceeded && !mIndexOnDiskIsValid && !mJournalReadSuccessfully) || + // -> update + (!aSucceeded && mIndexOnDiskIsValid && !mJournalReadSuccessfully) || + // -> ready + (aSucceeded && mIndexOnDiskIsValid && mJournalReadSuccessfully)); + + // If there is read operation pending we must be cancelling reading of the + // index when shutting down or removing the whole index. + MOZ_ASSERT(!mRWPending || (!aSucceeded && (mShuttingDown || mRemovingAll))); + + if (mState == SHUTDOWN) { + RemoveFile(nsLiteralCString(TEMP_INDEX_NAME)); + RemoveFile(nsLiteralCString(JOURNAL_NAME)); + } else { + if (mIndexHandle && !mIndexOnDiskIsValid) { + CacheFileIOManager::DoomFile(mIndexHandle, nullptr); + } + + if (mJournalHandle) { + CacheFileIOManager::DoomFile(mJournalHandle, nullptr); + } + } + + if (mIndexFileOpener) { + mIndexFileOpener->Cancel(); + mIndexFileOpener = nullptr; + } + if (mJournalFileOpener) { + mJournalFileOpener->Cancel(); + mJournalFileOpener = nullptr; + } + if (mTmpFileOpener) { + mTmpFileOpener->Cancel(); + mTmpFileOpener = nullptr; + } + + mIndexHandle = nullptr; + mJournalHandle = nullptr; + mRWHash = nullptr; + ReleaseBuffer(); + + if (mState == SHUTDOWN) { + return; + } + + if (!mIndexOnDiskIsValid) { + MOZ_ASSERT(mTmpJournal.Count() == 0); + EnsureNoFreshEntry(); + ProcessPendingOperations(aProofOfLock); + // Remove all entries that we haven't seen during this session + RemoveNonFreshEntries(aProofOfLock); + StartUpdatingIndex(true, aProofOfLock); + return; + } + + if (!mJournalReadSuccessfully) { + mTmpJournal.Clear(); + EnsureNoFreshEntry(); + ProcessPendingOperations(aProofOfLock); + StartUpdatingIndex(false, aProofOfLock); + return; + } + + MergeJournal(aProofOfLock); + EnsureNoFreshEntry(); + ProcessPendingOperations(aProofOfLock); + mIndexStats.Log(); + + ChangeState(READY, aProofOfLock); + mLastDumpTime = TimeStamp::NowLoRes(); // Do not dump new index immediately +} + +// static +void CacheIndex::DelayedUpdate(nsITimer* aTimer, void* aClosure) { + LOG(("CacheIndex::DelayedUpdate()")); + + StaticMutexAutoLock lock(sLock); + RefPtr index = gInstance; + + if (!index) { + return; + } + + index->DelayedUpdateLocked(lock); +} + +// static +void CacheIndex::DelayedUpdateLocked(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::DelayedUpdateLocked()")); + + nsresult rv; + + mUpdateTimer = nullptr; + + if (!IsIndexUsable()) { + return; + } + + if (mState == READY && mShuttingDown) { + return; + } + + // mUpdateEventPending must be false here since StartUpdatingIndex() won't + // schedule timer if it is true. + MOZ_ASSERT(!mUpdateEventPending); + if (mState != BUILDING && mState != UPDATING) { + LOG(("CacheIndex::DelayedUpdateLocked() - Update was canceled")); + return; + } + + // We need to redispatch to run with lower priority + RefPtr ioThread = CacheFileIOManager::IOThread(); + MOZ_ASSERT(ioThread); + + mUpdateEventPending = true; + rv = ioThread->Dispatch(this, CacheIOThread::INDEX); + if (NS_FAILED(rv)) { + mUpdateEventPending = false; + NS_WARNING("CacheIndex::DelayedUpdateLocked() - Can't dispatch event"); + LOG(("CacheIndex::DelayedUpdate() - Can't dispatch event")); + FinishUpdate(false, aProofOfLock); + } +} + +nsresult CacheIndex::ScheduleUpdateTimer(uint32_t aDelay) { + LOG(("CacheIndex::ScheduleUpdateTimer() [delay=%u]", aDelay)); + + MOZ_ASSERT(!mUpdateTimer); + + nsCOMPtr ioTarget = CacheFileIOManager::IOTarget(); + MOZ_ASSERT(ioTarget); + + return NS_NewTimerWithFuncCallback( + getter_AddRefs(mUpdateTimer), CacheIndex::DelayedUpdate, nullptr, aDelay, + nsITimer::TYPE_ONE_SHOT, "net::CacheIndex::ScheduleUpdateTimer", + ioTarget); +} + +nsresult CacheIndex::SetupDirectoryEnumerator() { + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mDirEnumerator); + + nsresult rv; + nsCOMPtr file; + + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(nsLiteralCString(ENTRIES_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) { + NS_WARNING( + "CacheIndex::SetupDirectoryEnumerator() - Entries directory " + "doesn't exist!"); + LOG( + ("CacheIndex::SetupDirectoryEnumerator() - Entries directory doesn't " + "exist!")); + return NS_ERROR_UNEXPECTED; + } + + // Do not do IO under the lock. + nsCOMPtr dirEnumerator; + { + StaticMutexAutoUnlock unlock(sLock); + rv = file->GetDirectoryEntries(getter_AddRefs(dirEnumerator)); + } + mDirEnumerator = dirEnumerator.forget(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult CacheIndex::InitEntryFromDiskData(CacheIndexEntry* aEntry, + CacheFileMetadata* aMetaData, + int64_t aFileSize) { + nsresult rv; + + aEntry->InitNew(); + aEntry->MarkDirty(); + aEntry->MarkFresh(); + + aEntry->Init(GetOriginAttrsHash(aMetaData->OriginAttributes()), + aMetaData->IsAnonymous(), aMetaData->Pinned()); + + aEntry->SetFrecency(aMetaData->GetFrecency()); + + const char* altData = aMetaData->GetElement(CacheFileUtils::kAltDataKey); + bool hasAltData = altData != nullptr; + if (hasAltData && NS_FAILED(CacheFileUtils::ParseAlternativeDataInfo( + altData, nullptr, nullptr))) { + return NS_ERROR_FAILURE; + } + aEntry->SetHasAltData(hasAltData); + + static auto toUint16 = [](const char* aUint16String) -> uint16_t { + if (!aUint16String) { + return kIndexTimeNotAvailable; + } + nsresult rv; + uint64_t n64 = nsDependentCString(aUint16String).ToInteger64(&rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + return n64 <= kIndexTimeOutOfBound ? n64 : kIndexTimeOutOfBound; + }; + + aEntry->SetOnStartTime( + toUint16(aMetaData->GetElement("net-response-time-onstart"))); + aEntry->SetOnStopTime( + toUint16(aMetaData->GetElement("net-response-time-onstop"))); + + const char* contentTypeStr = aMetaData->GetElement("ctid"); + uint8_t contentType = nsICacheEntry::CONTENT_TYPE_UNKNOWN; + if (contentTypeStr) { + int64_t n64 = nsDependentCString(contentTypeStr).ToInteger64(&rv); + if (NS_FAILED(rv) || n64 < nsICacheEntry::CONTENT_TYPE_UNKNOWN || + n64 >= nsICacheEntry::CONTENT_TYPE_LAST) { + n64 = nsICacheEntry::CONTENT_TYPE_UNKNOWN; + } + contentType = n64; + } + aEntry->SetContentType(contentType); + + aEntry->SetFileSize(static_cast(std::min( + static_cast(PR_UINT32_MAX), (aFileSize + 0x3FF) >> 10))); + return NS_OK; +} + +bool CacheIndex::IsUpdatePending() { + sLock.AssertCurrentThreadOwns(); + + return mUpdateTimer || mUpdateEventPending; +} + +void CacheIndex::BuildIndex(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::BuildIndex()")); + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + + nsresult rv; + + if (!mDirEnumerator) { + rv = SetupDirectoryEnumerator(); + if (mState == SHUTDOWN) { + // The index was shut down while we released the lock. FinishUpdate() was + // already called from Shutdown(), so just simply return here. + return; + } + + if (NS_FAILED(rv)) { + FinishUpdate(false, aProofOfLock); + return; + } + } + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG(( + "CacheIndex::BuildIndex() - Breaking loop for higher level events.")); + mUpdateEventPending = true; + return; + } + + bool fileExists = false; + nsCOMPtr file; + { + // Do not do IO under the lock. + nsCOMPtr dirEnumerator(mDirEnumerator); + sLock.AssertCurrentThreadOwns(); + StaticMutexAutoUnlock unlock(sLock); + rv = dirEnumerator->GetNextFile(getter_AddRefs(file)); + + if (file) { + file->Exists(&fileExists); + } + } + if (mState == SHUTDOWN) { + return; + } + if (!file) { + FinishUpdate(NS_SUCCEEDED(rv), aProofOfLock); + return; + } + + nsAutoCString leaf; + rv = file->GetNativeLeafName(leaf); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::BuildIndex() - GetNativeLeafName() failed! Skipping " + "file.")); + mDontMarkIndexClean = true; + continue; + } + + if (!fileExists) { + LOG( + ("CacheIndex::BuildIndex() - File returned by the iterator was " + "removed in the meantime [name=%s]", + leaf.get())); + continue; + } + + SHA1Sum::Hash hash; + rv = CacheFileIOManager::StrToHash(leaf, &hash); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::BuildIndex() - Filename is not a hash, removing file. " + "[name=%s]", + leaf.get())); + file->Remove(false); + continue; + } + + CacheIndexEntry* entry = mIndex.GetEntry(hash); + if (entry && entry->IsRemoved()) { + LOG( + ("CacheIndex::BuildIndex() - Found file that should not exist. " + "[name=%s]", + leaf.get())); + entry->Log(); + MOZ_ASSERT(entry->IsFresh()); + entry = nullptr; + } + +#ifdef DEBUG + RefPtr handle; + CacheFileIOManager::gInstance->mHandles.GetHandle(&hash, + getter_AddRefs(handle)); +#endif + + if (entry) { + // the entry is up to date + LOG( + ("CacheIndex::BuildIndex() - Skipping file because the entry is up to" + " date. [name=%s]", + leaf.get())); + entry->Log(); + MOZ_ASSERT(entry->IsFresh()); // The entry must be from this session + // there must be an active CacheFile if the entry is not initialized + MOZ_ASSERT(entry->IsInitialized() || handle); + continue; + } + + MOZ_ASSERT(!handle); + + RefPtr meta = new CacheFileMetadata(); + int64_t size = 0; + + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + rv = meta->SyncReadMetadata(file); + + if (NS_SUCCEEDED(rv)) { + rv = file->GetFileSize(&size); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::BuildIndex() - Cannot get filesize of file that was" + " successfully parsed. [name=%s]", + leaf.get())); + } + } + } + if (mState == SHUTDOWN) { + return; + } + + // Nobody could add the entry while the lock was released since we modify + // the index only on IO thread and this loop is executed on IO thread too. + entry = mIndex.GetEntry(hash); + MOZ_ASSERT(!entry || entry->IsRemoved()); + + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::BuildIndex() - CacheFileMetadata::SyncReadMetadata() " + "failed, removing file. [name=%s]", + leaf.get())); + file->Remove(false); + } else { + CacheIndexEntryAutoManage entryMng(&hash, this, aProofOfLock); + entry = mIndex.PutEntry(hash); + if (NS_FAILED(InitEntryFromDiskData(entry, meta, size))) { + LOG( + ("CacheIndex::BuildIndex() - CacheFile::InitEntryFromDiskData() " + "failed, removing file. [name=%s]", + leaf.get())); + file->Remove(false); + entry->MarkRemoved(); + } else { + LOG(("CacheIndex::BuildIndex() - Added entry to index. [name=%s]", + leaf.get())); + entry->Log(); + } + } + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); +} + +bool CacheIndex::StartUpdatingIndexIfNeeded( + const StaticMutexAutoLock& aProofOfLock, bool aSwitchingToReadyState) { + sLock.AssertCurrentThreadOwns(); + // Start updating process when we are in or we are switching to READY state + // and index needs update, but not during shutdown or when removing all + // entries. + if ((mState == READY || aSwitchingToReadyState) && mIndexNeedsUpdate && + !mShuttingDown && !mRemovingAll) { + LOG(("CacheIndex::StartUpdatingIndexIfNeeded() - starting update process")); + mIndexNeedsUpdate = false; + StartUpdatingIndex(false, aProofOfLock); + return true; + } + + return false; +} + +void CacheIndex::StartUpdatingIndex(bool aRebuild, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::StartUpdatingIndex() [rebuild=%d]", aRebuild)); + + nsresult rv; + + mIndexStats.Log(); + + ChangeState(aRebuild ? BUILDING : UPDATING, aProofOfLock); + mDontMarkIndexClean = false; + + if (mShuttingDown || mRemovingAll) { + FinishUpdate(false, aProofOfLock); + return; + } + + if (IsUpdatePending()) { + LOG(("CacheIndex::StartUpdatingIndex() - Update is already pending")); + return; + } + + uint32_t elapsed = (TimeStamp::NowLoRes() - mStartTime).ToMilliseconds(); + if (elapsed < kUpdateIndexStartDelay) { + LOG( + ("CacheIndex::StartUpdatingIndex() - %u ms elapsed since startup, " + "scheduling timer to fire in %u ms.", + elapsed, kUpdateIndexStartDelay - elapsed)); + rv = ScheduleUpdateTimer(kUpdateIndexStartDelay - elapsed); + if (NS_SUCCEEDED(rv)) { + return; + } + + LOG( + ("CacheIndex::StartUpdatingIndex() - ScheduleUpdateTimer() failed. " + "Starting update immediately.")); + } else { + LOG( + ("CacheIndex::StartUpdatingIndex() - %u ms elapsed since startup, " + "starting update now.", + elapsed)); + } + + RefPtr ioThread = CacheFileIOManager::IOThread(); + MOZ_ASSERT(ioThread); + + // We need to dispatch an event even if we are on IO thread since we need to + // update the index with the correct priority. + mUpdateEventPending = true; + rv = ioThread->Dispatch(this, CacheIOThread::INDEX); + if (NS_FAILED(rv)) { + mUpdateEventPending = false; + NS_WARNING("CacheIndex::StartUpdatingIndex() - Can't dispatch event"); + LOG(("CacheIndex::StartUpdatingIndex() - Can't dispatch event")); + FinishUpdate(false, aProofOfLock); + } +} + +void CacheIndex::UpdateIndex(const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::UpdateIndex()")); + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + sLock.AssertCurrentThreadOwns(); + + nsresult rv; + + if (!mDirEnumerator) { + rv = SetupDirectoryEnumerator(); + if (mState == SHUTDOWN) { + // The index was shut down while we released the lock. FinishUpdate() was + // already called from Shutdown(), so just simply return here. + return; + } + + if (NS_FAILED(rv)) { + FinishUpdate(false, aProofOfLock); + return; + } + } + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG( + ("CacheIndex::UpdateIndex() - Breaking loop for higher level " + "events.")); + mUpdateEventPending = true; + return; + } + + bool fileExists = false; + nsCOMPtr file; + { + // Do not do IO under the lock. + nsCOMPtr dirEnumerator(mDirEnumerator); + StaticMutexAutoUnlock unlock(sLock); + rv = dirEnumerator->GetNextFile(getter_AddRefs(file)); + + if (file) { + file->Exists(&fileExists); + } + } + if (mState == SHUTDOWN) { + return; + } + if (!file) { + FinishUpdate(NS_SUCCEEDED(rv), aProofOfLock); + return; + } + + nsAutoCString leaf; + rv = file->GetNativeLeafName(leaf); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - GetNativeLeafName() failed! Skipping " + "file.")); + mDontMarkIndexClean = true; + continue; + } + + if (!fileExists) { + LOG( + ("CacheIndex::UpdateIndex() - File returned by the iterator was " + "removed in the meantime [name=%s]", + leaf.get())); + continue; + } + + SHA1Sum::Hash hash; + rv = CacheFileIOManager::StrToHash(leaf, &hash); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - Filename is not a hash, removing file. " + "[name=%s]", + leaf.get())); + file->Remove(false); + continue; + } + + CacheIndexEntry* entry = mIndex.GetEntry(hash); + if (entry && entry->IsRemoved()) { + if (entry->IsFresh()) { + LOG( + ("CacheIndex::UpdateIndex() - Found file that should not exist. " + "[name=%s]", + leaf.get())); + entry->Log(); + } + entry = nullptr; + } + +#ifdef DEBUG + RefPtr handle; + CacheFileIOManager::gInstance->mHandles.GetHandle(&hash, + getter_AddRefs(handle)); +#endif + + if (entry && entry->IsFresh()) { + // the entry is up to date + LOG( + ("CacheIndex::UpdateIndex() - Skipping file because the entry is up " + " to date. [name=%s]", + leaf.get())); + entry->Log(); + // there must be an active CacheFile if the entry is not initialized + MOZ_ASSERT(entry->IsInitialized() || handle); + continue; + } + + MOZ_ASSERT(!handle); + + if (entry) { + PRTime lastModifiedTime; + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + rv = file->GetLastModifiedTime(&lastModifiedTime); + } + if (mState == SHUTDOWN) { + return; + } + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - Cannot get lastModifiedTime. " + "[name=%s]", + leaf.get())); + // Assume the file is newer than index + } else { + if (mIndexTimeStamp > (lastModifiedTime / PR_MSEC_PER_SEC)) { + LOG( + ("CacheIndex::UpdateIndex() - Skipping file because of last " + "modified time. [name=%s, indexTimeStamp=%" PRIu32 ", " + "lastModifiedTime=%" PRId64 "]", + leaf.get(), mIndexTimeStamp, + lastModifiedTime / PR_MSEC_PER_SEC)); + + CacheIndexEntryAutoManage entryMng(&hash, this, aProofOfLock); + entry->MarkFresh(); + continue; + } + } + } + + RefPtr meta = new CacheFileMetadata(); + int64_t size = 0; + + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + rv = meta->SyncReadMetadata(file); + + if (NS_SUCCEEDED(rv)) { + rv = file->GetFileSize(&size); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - Cannot get filesize of file that " + "was successfully parsed. [name=%s]", + leaf.get())); + } + } + } + if (mState == SHUTDOWN) { + return; + } + + // Nobody could add the entry while the lock was released since we modify + // the index only on IO thread and this loop is executed on IO thread too. + entry = mIndex.GetEntry(hash); + MOZ_ASSERT(!entry || !entry->IsFresh()); + + CacheIndexEntryAutoManage entryMng(&hash, this, aProofOfLock); + + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - CacheFileMetadata::SyncReadMetadata() " + "failed, removing file. [name=%s]", + leaf.get())); + } else { + entry = mIndex.PutEntry(hash); + rv = InitEntryFromDiskData(entry, meta, size); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::UpdateIndex() - CacheIndex::InitEntryFromDiskData " + "failed, removing file. [name=%s]", + leaf.get())); + } + } + + if (NS_FAILED(rv)) { + file->Remove(false); + if (entry) { + entry->MarkRemoved(); + entry->MarkFresh(); + entry->MarkDirty(); + } + } else { + LOG( + ("CacheIndex::UpdateIndex() - Added/updated entry to/in index. " + "[name=%s]", + leaf.get())); + entry->Log(); + } + } + + MOZ_ASSERT_UNREACHABLE("We should never get here"); +} + +void CacheIndex::FinishUpdate(bool aSucceeded, + const StaticMutexAutoLock& aProofOfLock) { + LOG(("CacheIndex::FinishUpdate() [succeeded=%d]", aSucceeded)); + + MOZ_ASSERT(mState == UPDATING || mState == BUILDING || + (!aSucceeded && mState == SHUTDOWN)); + + if (mDirEnumerator) { + if (NS_IsMainThread()) { + LOG( + ("CacheIndex::FinishUpdate() - posting of PreShutdownInternal failed?" + " Cannot safely release mDirEnumerator, leaking it!")); + NS_WARNING(("CacheIndex::FinishUpdate() - Leaking mDirEnumerator!")); + // This can happen only in case dispatching event to IO thread failed in + // CacheIndex::PreShutdown(). + Unused << mDirEnumerator.forget(); // Leak it since dir enumerator is not + // threadsafe + } else { + mDirEnumerator->Close(); + mDirEnumerator = nullptr; + } + } + + if (!aSucceeded) { + mDontMarkIndexClean = true; + } + + if (mState == SHUTDOWN) { + return; + } + + if (mState == UPDATING && aSucceeded) { + // If we've iterated over all entries successfully then all entries that + // really exist on the disk are now marked as fresh. All non-fresh entries + // don't exist anymore and must be removed from the index. + RemoveNonFreshEntries(aProofOfLock); + } + + // Make sure we won't start update. If the build or update failed, there is no + // reason to believe that it will succeed next time. + mIndexNeedsUpdate = false; + + ChangeState(READY, aProofOfLock); + mLastDumpTime = TimeStamp::NowLoRes(); // Do not dump new index immediately +} + +void CacheIndex::RemoveNonFreshEntries( + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + for (auto iter = mIndex.Iter(); !iter.Done(); iter.Next()) { + CacheIndexEntry* entry = iter.Get(); + if (entry->IsFresh()) { + continue; + } + + LOG( + ("CacheIndex::RemoveNonFreshEntries() - Removing entry. " + "[hash=%08x%08x%08x%08x%08x]", + LOGSHA1(entry->Hash()))); + + { + CacheIndexEntryAutoManage emng(entry->Hash(), this, aProofOfLock); + emng.DoNotSearchInIndex(); + } + + iter.Remove(); + } +} + +// static +char const* CacheIndex::StateString(EState aState) { + switch (aState) { + case INITIAL: + return "INITIAL"; + case READING: + return "READING"; + case WRITING: + return "WRITING"; + case BUILDING: + return "BUILDING"; + case UPDATING: + return "UPDATING"; + case READY: + return "READY"; + case SHUTDOWN: + return "SHUTDOWN"; + } + + MOZ_ASSERT(false, "Unexpected state!"); + return "?"; +} + +void CacheIndex::ChangeState(EState aNewState, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::ChangeState() changing state %s -> %s", StateString(mState), + StateString(aNewState))); + + // All pending updates should be processed before changing state + MOZ_ASSERT(mPendingUpdates.Count() == 0); + + // PreShutdownInternal() should change the state to READY from every state. It + // may go through different states, but once we are in READY state the only + // possible transition is to SHUTDOWN state. + MOZ_ASSERT(!mShuttingDown || mState != READY || aNewState == SHUTDOWN); + + // Start updating process when switching to READY state if needed + if (aNewState == READY && StartUpdatingIndexIfNeeded(aProofOfLock, true)) { + return; + } + + // Try to evict entries over limit everytime we're leaving state READING, + // BUILDING or UPDATING, but not during shutdown or when removing all + // entries. + if (!mShuttingDown && !mRemovingAll && aNewState != SHUTDOWN && + (mState == READING || mState == BUILDING || mState == UPDATING)) { + CacheFileIOManager::EvictIfOverLimit(); + } + + mState = aNewState; + + if (mState != SHUTDOWN) { + CacheFileIOManager::CacheIndexStateChanged(); + } + + NotifyAsyncGetDiskConsumptionCallbacks(); +} + +void CacheIndex::NotifyAsyncGetDiskConsumptionCallbacks() { + if ((mState == READY || mState == WRITING) && + !mAsyncGetDiskConsumptionBlocked && mDiskConsumptionObservers.Length()) { + for (uint32_t i = 0; i < mDiskConsumptionObservers.Length(); ++i) { + DiskConsumptionObserver* o = mDiskConsumptionObservers[i]; + // Safe to call under the lock. We always post to the main thread. + o->OnDiskConsumption(mIndexStats.Size() << 10); + } + + mDiskConsumptionObservers.Clear(); + } +} + +void CacheIndex::AllocBuffer() { + switch (mState) { + case WRITING: + mRWBufSize = sizeof(CacheIndexHeader) + sizeof(CacheHash::Hash32_t) + + mProcessEntries * sizeof(CacheIndexRecord); + if (mRWBufSize > kMaxBufSize) { + mRWBufSize = kMaxBufSize; + } + break; + case READING: + mRWBufSize = kMaxBufSize; + break; + default: + MOZ_ASSERT(false, "Unexpected state!"); + } + + mRWBuf = static_cast(moz_xmalloc(mRWBufSize)); +} + +void CacheIndex::ReleaseBuffer() { + sLock.AssertCurrentThreadOwns(); + + if (!mRWBuf || mRWPending) { + return; + } + + LOG(("CacheIndex::ReleaseBuffer() releasing buffer")); + + free(mRWBuf); + mRWBuf = nullptr; + mRWBufSize = 0; + mRWBufPos = 0; +} + +void CacheIndex::FrecencyArray::AppendRecord( + CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG( + ("CacheIndex::FrecencyArray::AppendRecord() [record=%p, hash=%08x%08x%08x" + "%08x%08x]", + aRecord, LOGSHA1(aRecord->Get()->mHash))); + + MOZ_DIAGNOSTIC_ASSERT(!mRecs.Contains(aRecord)); + mRecs.AppendElement(aRecord); + + // If the new frecency is 0, the element should be at the end of the array, + // i.e. this change doesn't affect order of the array + if (aRecord->Get()->mFrecency != 0) { + ++mUnsortedElements; + } +} + +void CacheIndex::FrecencyArray::RemoveRecord( + CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::FrecencyArray::RemoveRecord() [record=%p]", aRecord)); + + decltype(mRecs)::index_type idx; + idx = mRecs.IndexOf(aRecord); + MOZ_RELEASE_ASSERT(idx != mRecs.NoIndex); + // sanity check to ensure correct record removal + MOZ_RELEASE_ASSERT(mRecs[idx] == aRecord); + mRecs[idx] = nullptr; + ++mRemovedElements; + + // Calling SortIfNeeded ensures that we get rid of removed elements in the + // array once we hit the limit. + SortIfNeeded(aProofOfLock); +} + +void CacheIndex::FrecencyArray::ReplaceRecord( + CacheIndexRecordWrapper* aOldRecord, CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG( + ("CacheIndex::FrecencyArray::ReplaceRecord() [oldRecord=%p, " + "newRecord=%p]", + aOldRecord, aNewRecord)); + + decltype(mRecs)::index_type idx; + idx = mRecs.IndexOf(aOldRecord); + MOZ_RELEASE_ASSERT(idx != mRecs.NoIndex); + // sanity check to ensure correct record replaced + MOZ_RELEASE_ASSERT(mRecs[idx] == aOldRecord); + mRecs[idx] = aNewRecord; +} + +void CacheIndex::FrecencyArray::SortIfNeeded( + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + const uint32_t kMaxUnsortedCount = 512; + const uint32_t kMaxUnsortedPercent = 10; + const uint32_t kMaxRemovedCount = 512; + + uint32_t unsortedLimit = std::min( + kMaxUnsortedCount, Length() * kMaxUnsortedPercent / 100); + + if (mUnsortedElements > unsortedLimit || + mRemovedElements > kMaxRemovedCount) { + LOG( + ("CacheIndex::FrecencyArray::SortIfNeeded() - Sorting array " + "[unsortedElements=%u, unsortedLimit=%u, removedElements=%u, " + "maxRemovedCount=%u]", + mUnsortedElements, unsortedLimit, mRemovedElements, kMaxRemovedCount)); + + mRecs.Sort(FrecencyComparator()); + mUnsortedElements = 0; + if (mRemovedElements) { +#if defined(EARLY_BETA_OR_EARLIER) + // validate only null items are removed + for (uint32_t i = Length(); i < mRecs.Length(); ++i) { + MOZ_DIAGNOSTIC_ASSERT(!mRecs[i]); + } +#endif + // Removed elements are at the end after sorting. + mRecs.RemoveElementsAt(Length(), mRemovedElements); + mRemovedElements = 0; + } + } +} + +bool CacheIndex::FrecencyArray::RecordExistedUnlocked( + CacheIndexRecordWrapper* aRecord) { + return mRecs.Contains(aRecord); +} + +void CacheIndex::AddRecordToIterators(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + for (uint32_t i = 0; i < mIterators.Length(); ++i) { + // Add a new record only when iterator is supposed to be updated. + if (mIterators[i]->ShouldBeNewAdded()) { + mIterators[i]->AddRecord(aRecord, aProofOfLock); + } + } +} + +void CacheIndex::RemoveRecordFromIterators( + CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + for (uint32_t i = 0; i < mIterators.Length(); ++i) { + // Remove the record from iterator always, it makes no sence to return + // non-existing entries. Also the pointer to the record is no longer valid + // once the entry is removed from index. + mIterators[i]->RemoveRecord(aRecord, aProofOfLock); + } +} + +void CacheIndex::ReplaceRecordInIterators( + CacheIndexRecordWrapper* aOldRecord, CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + for (uint32_t i = 0; i < mIterators.Length(); ++i) { + // We have to replace the record always since the pointer is no longer + // valid after this point. NOTE: Replacing the record doesn't mean that + // a new entry was added, it just means that the data in the entry was + // changed (e.g. a file size) and we had to track this change in + // mPendingUpdates since mIndex was read-only. + mIterators[i]->ReplaceRecord(aOldRecord, aNewRecord, aProofOfLock); + } +} + +nsresult CacheIndex::Run() { + LOG(("CacheIndex::Run()")); + + StaticMutexAutoLock lock(sLock); + + if (!IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (mState == READY && mShuttingDown) { + return NS_OK; + } + + mUpdateEventPending = false; + + switch (mState) { + case BUILDING: + BuildIndex(lock); + break; + case UPDATING: + UpdateIndex(lock); + break; + default: + LOG(("CacheIndex::Run() - Update/Build was canceled")); + } + + return NS_OK; +} + +void CacheIndex::OnFileOpenedInternal(FileOpenHelper* aOpener, + CacheFileHandle* aHandle, + nsresult aResult, + const StaticMutexAutoLock& aProofOfLock) { + sLock.AssertCurrentThreadOwns(); + LOG( + ("CacheIndex::OnFileOpenedInternal() [opener=%p, handle=%p, " + "result=0x%08" PRIx32 "]", + aOpener, aHandle, static_cast(aResult))); + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + MOZ_RELEASE_ASSERT(IsIndexUsable()); + + if (mState == READY && mShuttingDown) { + return; + } + + switch (mState) { + case WRITING: + MOZ_ASSERT(aOpener == mIndexFileOpener); + mIndexFileOpener = nullptr; + + if (NS_FAILED(aResult)) { + LOG( + ("CacheIndex::OnFileOpenedInternal() - Can't open index file for " + "writing [rv=0x%08" PRIx32 "]", + static_cast(aResult))); + FinishWrite(false, aProofOfLock); + } else { + mIndexHandle = aHandle; + WriteRecords(aProofOfLock); + } + break; + case READING: + if (aOpener == mIndexFileOpener) { + mIndexFileOpener = nullptr; + + if (NS_SUCCEEDED(aResult)) { + if (aHandle->FileSize() == 0) { + FinishRead(false, aProofOfLock); + CacheFileIOManager::DoomFile(aHandle, nullptr); + break; + } + mIndexHandle = aHandle; + } else { + FinishRead(false, aProofOfLock); + break; + } + } else if (aOpener == mJournalFileOpener) { + mJournalFileOpener = nullptr; + mJournalHandle = aHandle; + } else if (aOpener == mTmpFileOpener) { + mTmpFileOpener = nullptr; + mTmpHandle = aHandle; + } else { + MOZ_ASSERT(false, "Unexpected state!"); + } + + if (mIndexFileOpener || mJournalFileOpener || mTmpFileOpener) { + // Some opener still didn't finish + break; + } + + // We fail and cancel all other openers when we opening index file fails. + MOZ_ASSERT(mIndexHandle); + + if (mTmpHandle) { + CacheFileIOManager::DoomFile(mTmpHandle, nullptr); + mTmpHandle = nullptr; + + if (mJournalHandle) { // this shouldn't normally happen + LOG( + ("CacheIndex::OnFileOpenedInternal() - Unexpected state, all " + "files [%s, %s, %s] should never exist. Removing whole index.", + INDEX_NAME, JOURNAL_NAME, TEMP_INDEX_NAME)); + FinishRead(false, aProofOfLock); + break; + } + } + + if (mJournalHandle) { + // Rename journal to make sure we update index on next start in case + // firefox crashes + rv = CacheFileIOManager::RenameFile( + mJournalHandle, nsLiteralCString(TEMP_INDEX_NAME), this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::OnFileOpenedInternal() - CacheFileIOManager::" + "RenameFile() failed synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishRead(false, aProofOfLock); + break; + } + } else { + StartReadingIndex(aProofOfLock); + } + + break; + default: + MOZ_ASSERT(false, "Unexpected state!"); + } +} + +nsresult CacheIndex::OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheIndex::OnFileOpened should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheIndex::OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) { + LOG(("CacheIndex::OnDataWritten() [handle=%p, result=0x%08" PRIx32 "]", + aHandle, static_cast(aResult))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + StaticMutexAutoLock lock(sLock); + + MOZ_RELEASE_ASSERT(IsIndexUsable()); + MOZ_RELEASE_ASSERT(mRWPending); + mRWPending = false; + + if (mState == READY && mShuttingDown) { + return NS_OK; + } + + switch (mState) { + case WRITING: + MOZ_ASSERT(mIndexHandle == aHandle); + + if (NS_FAILED(aResult)) { + FinishWrite(false, lock); + } else { + if (mSkipEntries == mProcessEntries) { + rv = CacheFileIOManager::RenameFile( + mIndexHandle, nsLiteralCString(INDEX_NAME), this); + if (NS_FAILED(rv)) { + LOG( + ("CacheIndex::OnDataWritten() - CacheFileIOManager::" + "RenameFile() failed synchronously [rv=0x%08" PRIx32 "]", + static_cast(rv))); + FinishWrite(false, lock); + } + } else { + WriteRecords(lock); + } + } + break; + default: + // Writing was canceled. + LOG( + ("CacheIndex::OnDataWritten() - ignoring notification since the " + "operation was previously canceled [state=%d]", + mState)); + ReleaseBuffer(); + } + + return NS_OK; +} + +nsresult CacheIndex::OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) { + LOG(("CacheIndex::OnDataRead() [handle=%p, result=0x%08" PRIx32 "]", aHandle, + static_cast(aResult))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + MOZ_RELEASE_ASSERT(IsIndexUsable()); + MOZ_RELEASE_ASSERT(mRWPending); + mRWPending = false; + + switch (mState) { + case READING: + MOZ_ASSERT(mIndexHandle == aHandle || mJournalHandle == aHandle); + + if (NS_FAILED(aResult)) { + FinishRead(false, lock); + } else { + if (!mIndexOnDiskIsValid) { + ParseRecords(lock); + } else { + ParseJournal(lock); + } + } + break; + default: + // Reading was canceled. + LOG( + ("CacheIndex::OnDataRead() - ignoring notification since the " + "operation was previously canceled [state=%d]", + mState)); + ReleaseBuffer(); + } + + return NS_OK; +} + +nsresult CacheIndex::OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheIndex::OnFileDoomed should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheIndex::OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) { + MOZ_CRASH("CacheIndex::OnEOFSet should not be called!"); + return NS_ERROR_UNEXPECTED; +} + +nsresult CacheIndex::OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) { + LOG(("CacheIndex::OnFileRenamed() [handle=%p, result=0x%08" PRIx32 "]", + aHandle, static_cast(aResult))); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + MOZ_RELEASE_ASSERT(IsIndexUsable()); + + if (mState == READY && mShuttingDown) { + return NS_OK; + } + + switch (mState) { + case WRITING: + // This is a result of renaming the new index written to tmpfile to index + // file. This is the last step when writing the index and the whole + // writing process is successful iff renaming was successful. + + if (mIndexHandle != aHandle) { + LOG( + ("CacheIndex::OnFileRenamed() - ignoring notification since it " + "belongs to previously canceled operation [state=%d]", + mState)); + break; + } + + FinishWrite(NS_SUCCEEDED(aResult), lock); + break; + case READING: + // This is a result of renaming journal file to tmpfile. It is renamed + // before we start reading index and journal file and it should normally + // succeed. If it fails give up reading of index. + + if (mJournalHandle != aHandle) { + LOG( + ("CacheIndex::OnFileRenamed() - ignoring notification since it " + "belongs to previously canceled operation [state=%d]", + mState)); + break; + } + + if (NS_FAILED(aResult)) { + FinishRead(false, lock); + } else { + StartReadingIndex(lock); + } + break; + default: + // Reading/writing was canceled. + LOG( + ("CacheIndex::OnFileRenamed() - ignoring notification since the " + "operation was previously canceled [state=%d]", + mState)); + } + + return NS_OK; +} + +// Memory reporting + +size_t CacheIndex::SizeOfExcludingThisInternal( + mozilla::MallocSizeOf mallocSizeOf) const { + sLock.AssertCurrentThreadOwns(); + + size_t n = 0; + nsCOMPtr sizeOf; + + // mIndexHandle and mJournalHandle are reported via SizeOfHandlesRunnable + // in CacheFileIOManager::SizeOfExcludingThisInternal as part of special + // handles array. + + sizeOf = do_QueryInterface(mCacheDirectory); + if (sizeOf) { + n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + } + + sizeOf = do_QueryInterface(mUpdateTimer); + if (sizeOf) { + n += sizeOf->SizeOfIncludingThis(mallocSizeOf); + } + + n += mallocSizeOf(mRWBuf); + n += mallocSizeOf(mRWHash); + + n += mIndex.SizeOfExcludingThis(mallocSizeOf); + n += mPendingUpdates.SizeOfExcludingThis(mallocSizeOf); + n += mTmpJournal.SizeOfExcludingThis(mallocSizeOf); + + // mFrecencyArray items are reported by mIndex/mPendingUpdates + n += mFrecencyArray.mRecs.ShallowSizeOfExcludingThis(mallocSizeOf); + n += mDiskConsumptionObservers.ShallowSizeOfExcludingThis(mallocSizeOf); + + return n; +} + +// static +size_t CacheIndex::SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) { + StaticMutexAutoLock lock(sLock); + + if (!gInstance) return 0; + + return gInstance->SizeOfExcludingThisInternal(mallocSizeOf); +} + +// static +size_t CacheIndex::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) { + StaticMutexAutoLock lock(sLock); + + return mallocSizeOf(gInstance) + + (gInstance ? gInstance->SizeOfExcludingThisInternal(mallocSizeOf) : 0); +} + +// static +void CacheIndex::UpdateTotalBytesWritten(uint32_t aBytesWritten) { + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + if (!index) { + return; + } + + index->mTotalBytesWritten += aBytesWritten; + + // Do telemetry report if enough data has been written and the index is + // in READY state. The data is available also in WRITING state, but we would + // need to deal with pending updates. + if (index->mTotalBytesWritten >= kTelemetryReportBytesLimit && + index->mState == READY && !index->mIndexNeedsUpdate && + !index->mShuttingDown) { + index->DoTelemetryReport(); + index->mTotalBytesWritten = 0; + return; + } +} + +void CacheIndex::DoTelemetryReport() { + static const nsLiteralCString + contentTypeNames[nsICacheEntry::CONTENT_TYPE_LAST] = { + "UNKNOWN"_ns, "OTHER"_ns, "JAVASCRIPT"_ns, "IMAGE"_ns, + "MEDIA"_ns, "STYLESHEET"_ns, "WASM"_ns}; + + for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { + if (mIndexStats.Size() > 0) { + Telemetry::Accumulate( + Telemetry::NETWORK_CACHE_SIZE_SHARE, contentTypeNames[i], + round(static_cast(mIndexStats.SizeByType(i)) * 100.0 / + static_cast(mIndexStats.Size()))); + } + + if (mIndexStats.Count() > 0) { + Telemetry::Accumulate( + Telemetry::NETWORK_CACHE_ENTRY_COUNT_SHARE, contentTypeNames[i], + round(static_cast(mIndexStats.CountByType(i)) * 100.0 / + static_cast(mIndexStats.Count()))); + } + } + + nsCString probeKey; + if (CacheObserver::SmartCacheSizeEnabled()) { + probeKey = "SMARTSIZE"_ns; + } else { + probeKey = "USERDEFINEDSIZE"_ns; + } + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_ENTRY_COUNT, probeKey, + mIndexStats.Count()); + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_SIZE, probeKey, + mIndexStats.Size() >> 10); +} + +// static +void CacheIndex::OnAsyncEviction(bool aEvicting) { + StaticMutexAutoLock lock(sLock); + + RefPtr index = gInstance; + if (!index) { + return; + } + + index->mAsyncGetDiskConsumptionBlocked = aEvicting; + if (!aEvicting) { + index->NotifyAsyncGetDiskConsumptionCallbacks(); + } +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheIndex.h b/netwerk/cache2/CacheIndex.h new file mode 100644 index 0000000000..053c8ce654 --- /dev/null +++ b/netwerk/cache2/CacheIndex.h @@ -0,0 +1,1311 @@ +/* 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/. */ + +#ifndef CacheIndex__h__ +#define CacheIndex__h__ + +#include "CacheLog.h" +#include "CacheFileIOManager.h" +#include "nsIRunnable.h" +#include "CacheHashUtils.h" +#include "nsICacheStorageService.h" +#include "nsICacheEntry.h" +#include "nsILoadContextInfo.h" +#include "nsIWeakReferenceUtils.h" +#include "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/SHA1.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" + +class nsIFile; +class nsIDirectoryEnumerator; +class nsITimer; + +#ifdef DEBUG +# define DEBUG_STATS 1 +#endif + +namespace mozilla { +namespace net { + +class CacheFileMetadata; +class FileOpenHelper; +class CacheIndexIterator; + +const uint16_t kIndexTimeNotAvailable = 0xFFFFU; +const uint16_t kIndexTimeOutOfBound = 0xFFFEU; + +using CacheIndexHeader = struct { + // Version of the index. The index must be ignored and deleted when the file + // on disk was written with a newer version. + uint32_t mVersion; + + // Timestamp of time when the last successful write of the index started. + // During update process we use this timestamp for a quick validation of entry + // files. If last modified time of the file is lower than this timestamp, we + // skip parsing of such file since the information in index should be up to + // date. + uint32_t mTimeStamp; + + // We set this flag as soon as possible after parsing index during startup + // and clean it after we write journal to disk during shutdown. We ignore the + // journal and start update process whenever this flag is set during index + // parsing. + uint32_t mIsDirty; + + // The amount of data written to the cache. When it reaches + // kTelemetryReportBytesLimit a telemetry report is sent and the counter is + // reset. + uint32_t mKBWritten; +}; + +static_assert(sizeof(CacheIndexHeader::mVersion) + + sizeof(CacheIndexHeader::mTimeStamp) + + sizeof(CacheIndexHeader::mIsDirty) + + sizeof(CacheIndexHeader::mKBWritten) == + sizeof(CacheIndexHeader), + "Unexpected sizeof(CacheIndexHeader)!"); + +#pragma pack(push, 1) +struct CacheIndexRecord { + SHA1Sum::Hash mHash{}; + uint32_t mFrecency{0}; + OriginAttrsHash mOriginAttrsHash{0}; + uint16_t mOnStartTime{kIndexTimeNotAvailable}; + uint16_t mOnStopTime{kIndexTimeNotAvailable}; + uint8_t mContentType{nsICacheEntry::CONTENT_TYPE_UNKNOWN}; + + /* + * 1000 0000 0000 0000 0000 0000 0000 0000 : initialized + * 0100 0000 0000 0000 0000 0000 0000 0000 : anonymous + * 0010 0000 0000 0000 0000 0000 0000 0000 : removed + * 0001 0000 0000 0000 0000 0000 0000 0000 : dirty + * 0000 1000 0000 0000 0000 0000 0000 0000 : fresh + * 0000 0100 0000 0000 0000 0000 0000 0000 : pinned + * 0000 0010 0000 0000 0000 0000 0000 0000 : has cached alt data + * 0000 0001 0000 0000 0000 0000 0000 0000 : reserved + * 0000 0000 1111 1111 1111 1111 1111 1111 : file size (in kB) + */ + uint32_t mFlags{0}; + + CacheIndexRecord() = default; +}; +#pragma pack(pop) + +static_assert(sizeof(CacheIndexRecord::mHash) + + sizeof(CacheIndexRecord::mFrecency) + + sizeof(CacheIndexRecord::mOriginAttrsHash) + + sizeof(CacheIndexRecord::mOnStartTime) + + sizeof(CacheIndexRecord::mOnStopTime) + + sizeof(CacheIndexRecord::mContentType) + + sizeof(CacheIndexRecord::mFlags) == + sizeof(CacheIndexRecord), + "Unexpected sizeof(CacheIndexRecord)!"); + +class CacheIndexRecordWrapper final { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DESTROY( + CacheIndexRecordWrapper, DispatchDeleteSelfToCurrentThread()); + + CacheIndexRecordWrapper() : mRec(MakeUnique()) {} + CacheIndexRecord* Get() { return mRec.get(); } + + private: + ~CacheIndexRecordWrapper(); + void DispatchDeleteSelfToCurrentThread(); + UniquePtr mRec; + friend class DeleteCacheIndexRecordWrapper; +}; + +class CacheIndexEntry : public PLDHashEntryHdr { + public: + using KeyType = const SHA1Sum::Hash&; + using KeyTypePointer = const SHA1Sum::Hash*; + + explicit CacheIndexEntry(KeyTypePointer aKey) { + MOZ_COUNT_CTOR(CacheIndexEntry); + mRec = new CacheIndexRecordWrapper(); + LOG(("CacheIndexEntry::CacheIndexEntry() - Created record [rec=%p]", + mRec->Get())); + memcpy(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash)); + } + CacheIndexEntry(const CacheIndexEntry& aOther) { + MOZ_ASSERT_UNREACHABLE("CacheIndexEntry copy constructor is forbidden!"); + } + ~CacheIndexEntry() { + MOZ_COUNT_DTOR(CacheIndexEntry); + LOG(("CacheIndexEntry::~CacheIndexEntry() - Deleting record [rec=%p]", + mRec->Get())); + } + + // KeyEquals(): does this entry match this key? + bool KeyEquals(KeyTypePointer aKey) const { + return memcmp(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash)) == 0; + } + + // KeyToPointer(): Convert KeyType to KeyTypePointer + static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } + + // HashKey(): calculate the hash number + static PLDHashNumber HashKey(KeyTypePointer aKey) { + return (reinterpret_cast(aKey))[0]; + } + + // ALLOW_MEMMOVE can we move this class with memmove(), or do we have + // to use the copy constructor? + enum { ALLOW_MEMMOVE = true }; + + bool operator==(const CacheIndexEntry& aOther) const { + return KeyEquals(&aOther.mRec->Get()->mHash); + } + + CacheIndexEntry& operator=(const CacheIndexEntry& aOther) { + MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aOther.mRec->Get()->mHash, + sizeof(SHA1Sum::Hash)) == 0); + mRec->Get()->mFrecency = aOther.mRec->Get()->mFrecency; + mRec->Get()->mOriginAttrsHash = aOther.mRec->Get()->mOriginAttrsHash; + mRec->Get()->mOnStartTime = aOther.mRec->Get()->mOnStartTime; + mRec->Get()->mOnStopTime = aOther.mRec->Get()->mOnStopTime; + mRec->Get()->mContentType = aOther.mRec->Get()->mContentType; + mRec->Get()->mFlags = aOther.mRec->Get()->mFlags; + return *this; + } + + void InitNew() { + mRec->Get()->mFrecency = 0; + mRec->Get()->mOriginAttrsHash = 0; + mRec->Get()->mOnStartTime = kIndexTimeNotAvailable; + mRec->Get()->mOnStopTime = kIndexTimeNotAvailable; + mRec->Get()->mContentType = nsICacheEntry::CONTENT_TYPE_UNKNOWN; + mRec->Get()->mFlags = 0; + } + + void Init(OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned) { + MOZ_ASSERT(mRec->Get()->mFrecency == 0); + MOZ_ASSERT(mRec->Get()->mOriginAttrsHash == 0); + MOZ_ASSERT(mRec->Get()->mOnStartTime == kIndexTimeNotAvailable); + MOZ_ASSERT(mRec->Get()->mOnStopTime == kIndexTimeNotAvailable); + MOZ_ASSERT(mRec->Get()->mContentType == + nsICacheEntry::CONTENT_TYPE_UNKNOWN); + // When we init the entry it must be fresh and may be dirty + MOZ_ASSERT((mRec->Get()->mFlags & ~kDirtyMask) == kFreshMask); + + mRec->Get()->mOriginAttrsHash = aOriginAttrsHash; + mRec->Get()->mFlags |= kInitializedMask; + if (aAnonymous) { + mRec->Get()->mFlags |= kAnonymousMask; + } + if (aPinned) { + mRec->Get()->mFlags |= kPinnedMask; + } + } + + const SHA1Sum::Hash* Hash() const { return &mRec->Get()->mHash; } + + bool IsInitialized() const { + return !!(mRec->Get()->mFlags & kInitializedMask); + } + + mozilla::net::OriginAttrsHash OriginAttrsHash() const { + return mRec->Get()->mOriginAttrsHash; + } + + bool Anonymous() const { return !!(mRec->Get()->mFlags & kAnonymousMask); } + + bool IsRemoved() const { return !!(mRec->Get()->mFlags & kRemovedMask); } + void MarkRemoved() { mRec->Get()->mFlags |= kRemovedMask; } + + bool IsDirty() const { return !!(mRec->Get()->mFlags & kDirtyMask); } + void MarkDirty() { mRec->Get()->mFlags |= kDirtyMask; } + void ClearDirty() { mRec->Get()->mFlags &= ~kDirtyMask; } + + bool IsFresh() const { return !!(mRec->Get()->mFlags & kFreshMask); } + void MarkFresh() { mRec->Get()->mFlags |= kFreshMask; } + + bool IsPinned() const { return !!(mRec->Get()->mFlags & kPinnedMask); } + + void SetFrecency(uint32_t aFrecency) { mRec->Get()->mFrecency = aFrecency; } + uint32_t GetFrecency() const { return mRec->Get()->mFrecency; } + + void SetHasAltData(bool aHasAltData) { + aHasAltData ? mRec->Get()->mFlags |= kHasAltDataMask + : mRec->Get()->mFlags &= ~kHasAltDataMask; + } + bool GetHasAltData() const { + return !!(mRec->Get()->mFlags & kHasAltDataMask); + } + + void SetOnStartTime(uint16_t aTime) { mRec->Get()->mOnStartTime = aTime; } + uint16_t GetOnStartTime() const { return mRec->Get()->mOnStartTime; } + + void SetOnStopTime(uint16_t aTime) { mRec->Get()->mOnStopTime = aTime; } + uint16_t GetOnStopTime() const { return mRec->Get()->mOnStopTime; } + + void SetContentType(uint8_t aType) { mRec->Get()->mContentType = aType; } + uint8_t GetContentType() const { return GetContentType(mRec->Get()); } + static uint8_t GetContentType(CacheIndexRecord* aRec) { + if (aRec->mContentType >= nsICacheEntry::CONTENT_TYPE_LAST) { + LOG( + ("CacheIndexEntry::GetContentType() - Found invalid content type " + "[hash=%08x%08x%08x%08x%08x, contentType=%u]", + LOGSHA1(aRec->mHash), aRec->mContentType)); + return nsICacheEntry::CONTENT_TYPE_UNKNOWN; + } + return aRec->mContentType; + } + + // Sets filesize in kilobytes. + void SetFileSize(uint32_t aFileSize) { + if (aFileSize > kFileSizeMask) { + LOG( + ("CacheIndexEntry::SetFileSize() - FileSize is too large, " + "truncating to %u", + kFileSizeMask)); + aFileSize = kFileSizeMask; + } + mRec->Get()->mFlags &= ~kFileSizeMask; + mRec->Get()->mFlags |= aFileSize; + } + // Returns filesize in kilobytes. + uint32_t GetFileSize() const { return GetFileSize(*(mRec->Get())); } + static uint32_t GetFileSize(const CacheIndexRecord& aRec) { + return aRec.mFlags & kFileSizeMask; + } + static uint32_t IsPinned(CacheIndexRecord* aRec) { + return aRec->mFlags & kPinnedMask; + } + bool IsFileEmpty() const { return GetFileSize() == 0; } + + void WriteToBuf(void* aBuf) { + uint8_t* ptr = static_cast(aBuf); + memcpy(ptr, mRec->Get()->mHash, sizeof(SHA1Sum::Hash)); + ptr += sizeof(SHA1Sum::Hash); + NetworkEndian::writeUint32(ptr, mRec->Get()->mFrecency); + ptr += sizeof(uint32_t); + NetworkEndian::writeUint64(ptr, mRec->Get()->mOriginAttrsHash); + ptr += sizeof(uint64_t); + NetworkEndian::writeUint16(ptr, mRec->Get()->mOnStartTime); + ptr += sizeof(uint16_t); + NetworkEndian::writeUint16(ptr, mRec->Get()->mOnStopTime); + ptr += sizeof(uint16_t); + *ptr = mRec->Get()->mContentType; + ptr += sizeof(uint8_t); + // Dirty and fresh flags should never go to disk, since they make sense only + // during current session. + NetworkEndian::writeUint32( + ptr, mRec->Get()->mFlags & ~(kDirtyMask | kFreshMask)); + } + + void ReadFromBuf(void* aBuf) { + const uint8_t* ptr = static_cast(aBuf); + MOZ_ASSERT(memcmp(&mRec->Get()->mHash, ptr, sizeof(SHA1Sum::Hash)) == 0); + ptr += sizeof(SHA1Sum::Hash); + mRec->Get()->mFrecency = NetworkEndian::readUint32(ptr); + ptr += sizeof(uint32_t); + mRec->Get()->mOriginAttrsHash = NetworkEndian::readUint64(ptr); + ptr += sizeof(uint64_t); + mRec->Get()->mOnStartTime = NetworkEndian::readUint16(ptr); + ptr += sizeof(uint16_t); + mRec->Get()->mOnStopTime = NetworkEndian::readUint16(ptr); + ptr += sizeof(uint16_t); + mRec->Get()->mContentType = *ptr; + ptr += sizeof(uint8_t); + mRec->Get()->mFlags = NetworkEndian::readUint32(ptr); + } + + void Log() const { + LOG( + ("CacheIndexEntry::Log() [this=%p, hash=%08x%08x%08x%08x%08x, fresh=%u," + " initialized=%u, removed=%u, dirty=%u, anonymous=%u, " + "originAttrsHash=%" PRIx64 ", frecency=%u, hasAltData=%u, " + "onStartTime=%u, onStopTime=%u, contentType=%u, size=%u]", + this, LOGSHA1(mRec->Get()->mHash), IsFresh(), IsInitialized(), + IsRemoved(), IsDirty(), Anonymous(), OriginAttrsHash(), GetFrecency(), + GetHasAltData(), GetOnStartTime(), GetOnStopTime(), GetContentType(), + GetFileSize())); + } + + static bool RecordMatchesLoadContextInfo(CacheIndexRecordWrapper* aRec, + nsILoadContextInfo* aInfo) { + MOZ_ASSERT(aInfo); + + return !aInfo->IsPrivate() && + GetOriginAttrsHash(*aInfo->OriginAttributesPtr()) == + aRec->Get()->mOriginAttrsHash && + aInfo->IsAnonymous() == !!(aRec->Get()->mFlags & kAnonymousMask); + } + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(mRec->Get()); + } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); + } + + private: + friend class CacheIndexEntryUpdate; + friend class CacheIndex; + friend class CacheIndexEntryAutoManage; + friend struct CacheIndexRecord; + + static const uint32_t kInitializedMask = 0x80000000; + static const uint32_t kAnonymousMask = 0x40000000; + + // This flag is set when the entry was removed. We need to keep this + // information in memory until we write the index file. + static const uint32_t kRemovedMask = 0x20000000; + + // This flag is set when the information in memory is not in sync with the + // information in index file on disk. + static const uint32_t kDirtyMask = 0x10000000; + + // This flag is set when the information about the entry is fresh, i.e. + // we've created or opened this entry during this session, or we've seen + // this entry during update or build process. + static const uint32_t kFreshMask = 0x08000000; + + // Indicates a pinned entry. + static const uint32_t kPinnedMask = 0x04000000; + + // Indicates there is cached alternative data in the entry. + static const uint32_t kHasAltDataMask = 0x02000000; + static const uint32_t kReservedMask = 0x01000000; + + // FileSize in kilobytes + static const uint32_t kFileSizeMask = 0x00FFFFFF; + + RefPtr mRec; +}; + +class CacheIndexEntryUpdate : public CacheIndexEntry { + public: + explicit CacheIndexEntryUpdate(CacheIndexEntry::KeyTypePointer aKey) + : CacheIndexEntry(aKey), mUpdateFlags(0) { + MOZ_COUNT_CTOR(CacheIndexEntryUpdate); + LOG(("CacheIndexEntryUpdate::CacheIndexEntryUpdate()")); + } + ~CacheIndexEntryUpdate() { + MOZ_COUNT_DTOR(CacheIndexEntryUpdate); + LOG(("CacheIndexEntryUpdate::~CacheIndexEntryUpdate()")); + } + + CacheIndexEntryUpdate& operator=(const CacheIndexEntry& aOther) { + MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aOther.mRec->Get()->mHash, + sizeof(SHA1Sum::Hash)) == 0); + mUpdateFlags = 0; + *(static_cast(this)) = aOther; + return *this; + } + + void InitNew() { + mUpdateFlags = kFrecencyUpdatedMask | kHasAltDataUpdatedMask | + kOnStartTimeUpdatedMask | kOnStopTimeUpdatedMask | + kContentTypeUpdatedMask | kFileSizeUpdatedMask; + CacheIndexEntry::InitNew(); + } + + void SetFrecency(uint32_t aFrecency) { + mUpdateFlags |= kFrecencyUpdatedMask; + CacheIndexEntry::SetFrecency(aFrecency); + } + + void SetHasAltData(bool aHasAltData) { + mUpdateFlags |= kHasAltDataUpdatedMask; + CacheIndexEntry::SetHasAltData(aHasAltData); + } + + void SetOnStartTime(uint16_t aTime) { + mUpdateFlags |= kOnStartTimeUpdatedMask; + CacheIndexEntry::SetOnStartTime(aTime); + } + + void SetOnStopTime(uint16_t aTime) { + mUpdateFlags |= kOnStopTimeUpdatedMask; + CacheIndexEntry::SetOnStopTime(aTime); + } + + void SetContentType(uint8_t aType) { + mUpdateFlags |= kContentTypeUpdatedMask; + CacheIndexEntry::SetContentType(aType); + } + + void SetFileSize(uint32_t aFileSize) { + mUpdateFlags |= kFileSizeUpdatedMask; + CacheIndexEntry::SetFileSize(aFileSize); + } + + void ApplyUpdate(CacheIndexEntry* aDst) { + MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aDst->mRec->Get()->mHash, + sizeof(SHA1Sum::Hash)) == 0); + if (mUpdateFlags & kFrecencyUpdatedMask) { + aDst->mRec->Get()->mFrecency = mRec->Get()->mFrecency; + } + aDst->mRec->Get()->mOriginAttrsHash = mRec->Get()->mOriginAttrsHash; + if (mUpdateFlags & kOnStartTimeUpdatedMask) { + aDst->mRec->Get()->mOnStartTime = mRec->Get()->mOnStartTime; + } + if (mUpdateFlags & kOnStopTimeUpdatedMask) { + aDst->mRec->Get()->mOnStopTime = mRec->Get()->mOnStopTime; + } + if (mUpdateFlags & kContentTypeUpdatedMask) { + aDst->mRec->Get()->mContentType = mRec->Get()->mContentType; + } + if (mUpdateFlags & kHasAltDataUpdatedMask && + ((aDst->mRec->Get()->mFlags ^ mRec->Get()->mFlags) & kHasAltDataMask)) { + // Toggle the bit if we need to. + aDst->mRec->Get()->mFlags ^= kHasAltDataMask; + } + + if (mUpdateFlags & kFileSizeUpdatedMask) { + // Copy all flags except |HasAltData|. + aDst->mRec->Get()->mFlags |= (mRec->Get()->mFlags & ~kHasAltDataMask); + } else { + // Copy all flags except |HasAltData| and file size. + aDst->mRec->Get()->mFlags &= kFileSizeMask; + aDst->mRec->Get()->mFlags |= + (mRec->Get()->mFlags & ~kHasAltDataMask & ~kFileSizeMask); + } + } + + private: + static const uint32_t kFrecencyUpdatedMask = 0x00000001; + static const uint32_t kContentTypeUpdatedMask = 0x00000002; + static const uint32_t kFileSizeUpdatedMask = 0x00000004; + static const uint32_t kHasAltDataUpdatedMask = 0x00000008; + static const uint32_t kOnStartTimeUpdatedMask = 0x00000010; + static const uint32_t kOnStopTimeUpdatedMask = 0x00000020; + + uint32_t mUpdateFlags; +}; + +class CacheIndexStats { + public: + CacheIndexStats() { + for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { + mCountByType[i] = 0; + mSizeByType[i] = 0; + } + } + + bool operator==(const CacheIndexStats& aOther) const { + for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { + if (mCountByType[i] != aOther.mCountByType[i] || + mSizeByType[i] != aOther.mSizeByType[i]) { + return false; + } + } + + return +#ifdef DEBUG + aOther.mStateLogged == mStateLogged && +#endif + aOther.mCount == mCount && aOther.mNotInitialized == mNotInitialized && + aOther.mRemoved == mRemoved && aOther.mDirty == mDirty && + aOther.mFresh == mFresh && aOther.mEmpty == mEmpty && + aOther.mSize == mSize; + } + +#ifdef DEBUG + void DisableLogging() { mDisableLogging = true; } +#endif + + void Log() { + LOG( + ("CacheIndexStats::Log() [count=%u, notInitialized=%u, removed=%u, " + "dirty=%u, fresh=%u, empty=%u, size=%u]", + mCount, mNotInitialized, mRemoved, mDirty, mFresh, mEmpty, mSize)); + } + + void Clear() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Clear() - state logged!"); + + mCount = 0; + mNotInitialized = 0; + mRemoved = 0; + mDirty = 0; + mFresh = 0; + mEmpty = 0; + mSize = 0; + for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { + mCountByType[i] = 0; + mSizeByType[i] = 0; + } + } + +#ifdef DEBUG + bool StateLogged() { return mStateLogged; } +#endif + + uint32_t Count() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Count() - state logged!"); + return mCount; + } + + uint32_t CountByType(uint8_t aContentType) { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::CountByType() - state logged!"); + MOZ_RELEASE_ASSERT(aContentType < nsICacheEntry::CONTENT_TYPE_LAST); + return mCountByType[aContentType]; + } + + uint32_t Dirty() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Dirty() - state logged!"); + return mDirty; + } + + uint32_t Fresh() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Fresh() - state logged!"); + return mFresh; + } + + uint32_t ActiveEntriesCount() { + MOZ_ASSERT(!mStateLogged, + "CacheIndexStats::ActiveEntriesCount() - state " + "logged!"); + return mCount - mRemoved - mNotInitialized - mEmpty; + } + + uint32_t Size() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Size() - state logged!"); + return mSize; + } + + uint32_t SizeByType(uint8_t aContentType) { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::SizeByType() - state logged!"); + MOZ_RELEASE_ASSERT(aContentType < nsICacheEntry::CONTENT_TYPE_LAST); + return mSizeByType[aContentType]; + } + + void BeforeChange(const CacheIndexEntry* aEntry) { +#ifdef DEBUG_STATS + if (!mDisableLogging) { + LOG(("CacheIndexStats::BeforeChange()")); + Log(); + } +#endif + + MOZ_ASSERT(!mStateLogged, + "CacheIndexStats::BeforeChange() - state " + "logged!"); +#ifdef DEBUG + mStateLogged = true; +#endif + if (aEntry) { + MOZ_ASSERT(mCount); + uint8_t contentType = aEntry->GetContentType(); + mCount--; + mCountByType[contentType]--; + if (aEntry->IsDirty()) { + MOZ_ASSERT(mDirty); + mDirty--; + } + if (aEntry->IsFresh()) { + MOZ_ASSERT(mFresh); + mFresh--; + } + if (aEntry->IsRemoved()) { + MOZ_ASSERT(mRemoved); + mRemoved--; + } else { + if (!aEntry->IsInitialized()) { + MOZ_ASSERT(mNotInitialized); + mNotInitialized--; + } else { + if (aEntry->IsFileEmpty()) { + MOZ_ASSERT(mEmpty); + mEmpty--; + } else { + MOZ_ASSERT(mSize >= aEntry->GetFileSize()); + mSize -= aEntry->GetFileSize(); + mSizeByType[contentType] -= aEntry->GetFileSize(); + } + } + } + } + } + + void AfterChange(const CacheIndexEntry* aEntry) { + MOZ_ASSERT(mStateLogged, + "CacheIndexStats::AfterChange() - state not " + "logged!"); +#ifdef DEBUG + mStateLogged = false; +#endif + if (aEntry) { + uint8_t contentType = aEntry->GetContentType(); + ++mCount; + ++mCountByType[contentType]; + if (aEntry->IsDirty()) { + mDirty++; + } + if (aEntry->IsFresh()) { + mFresh++; + } + if (aEntry->IsRemoved()) { + mRemoved++; + } else { + if (!aEntry->IsInitialized()) { + mNotInitialized++; + } else { + if (aEntry->IsFileEmpty()) { + mEmpty++; + } else { + mSize += aEntry->GetFileSize(); + mSizeByType[contentType] += aEntry->GetFileSize(); + } + } + } + } + +#ifdef DEBUG_STATS + if (!mDisableLogging) { + LOG(("CacheIndexStats::AfterChange()")); + Log(); + } +#endif + } + + private: + uint32_t mCount{0}; + uint32_t mCountByType[nsICacheEntry::CONTENT_TYPE_LAST]{0}; + uint32_t mNotInitialized{0}; + uint32_t mRemoved{0}; + uint32_t mDirty{0}; + uint32_t mFresh{0}; + uint32_t mEmpty{0}; + uint32_t mSize{0}; + uint32_t mSizeByType[nsICacheEntry::CONTENT_TYPE_LAST]{0}; +#ifdef DEBUG + // We completely remove the data about an entry from the stats in + // BeforeChange() and set this flag to true. The entry is then modified, + // deleted or created and the data is again put into the stats and this flag + // set to false. Statistics must not be read during this time since the + // information is not correct. + bool mStateLogged{false}; + + // Disables logging in this instance of CacheIndexStats + bool mDisableLogging{false}; +#endif +}; + +class CacheIndex final : public CacheFileIOListener, public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + CacheIndex(); + + static nsresult Init(nsIFile* aCacheDirectory); + static nsresult PreShutdown(); + static nsresult Shutdown(); + + // Following methods can be called only on IO thread. + + // Add entry to the index. The entry shouldn't be present in index. This + // method is called whenever a new handle for a new entry file is created. The + // newly created entry is not initialized and it must be either initialized + // with InitEntry() or removed with RemoveEntry(). + static nsresult AddEntry(const SHA1Sum::Hash* aHash); + + // Inform index about an existing entry that should be present in index. This + // method is called whenever a new handle for an existing entry file is + // created. Like in case of AddEntry(), either InitEntry() or RemoveEntry() + // must be called on the entry, since the entry is not initizlized if the + // index is outdated. + static nsresult EnsureEntryExists(const SHA1Sum::Hash* aHash); + + // Initialize the entry. It MUST be present in index. Call to AddEntry() or + // EnsureEntryExists() must precede the call to this method. + static nsresult InitEntry(const SHA1Sum::Hash* aHash, + OriginAttrsHash aOriginAttrsHash, bool aAnonymous, + bool aPinned); + + // Remove entry from index. The entry should be present in index. + static nsresult RemoveEntry(const SHA1Sum::Hash* aHash); + + // Update some information in entry. The entry MUST be present in index and + // MUST be initialized. Call to AddEntry() or EnsureEntryExists() and to + // InitEntry() must precede the call to this method. + // Pass nullptr if the value didn't change. + static nsresult UpdateEntry(const SHA1Sum::Hash* aHash, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType, + const uint32_t* aSize); + + // Remove all entries from the index. Called when clearing the whole cache. + static nsresult RemoveAll(); + + enum EntryStatus { EXISTS = 0, DOES_NOT_EXIST = 1, DO_NOT_KNOW = 2 }; + + // Returns status of the entry in index for the given key. It can be called + // on any thread. + // If the optional aCB callback is given, the it will be called with a + // CacheIndexEntry only if _retval is EXISTS when the method returns. + static nsresult HasEntry( + const nsACString& aKey, EntryStatus* _retval, + const std::function& aCB = nullptr); + static nsresult HasEntry( + const SHA1Sum::Hash& hash, EntryStatus* _retval, + const std::function& aCB = nullptr); + + // Returns a hash of the least important entry that should be evicted if the + // cache size is over limit and also returns a total number of all entries in + // the index minus the number of forced valid entries and unpinned entries + // that we encounter when searching (see below) + static nsresult GetEntryForEviction(bool aIgnoreEmptyEntries, + SHA1Sum::Hash* aHash, uint32_t* aCnt); + + // Checks if a cache entry is currently forced valid. Used to prevent an entry + // (that has been forced valid) from being evicted when the cache size reaches + // its limit. + static bool IsForcedValidEntry(const SHA1Sum::Hash* aHash); + + // Returns cache size in kB. + static nsresult GetCacheSize(uint32_t* _retval); + + // Returns number of entry files in the cache + static nsresult GetEntryFileCount(uint32_t* _retval); + + // Synchronously returns the disk occupation and number of entries + // per-context. Callable on any thread. It will ignore loadContextInfo and get + // stats for all entries if the aInfo is a nullptr. + static nsresult GetCacheStats(nsILoadContextInfo* aInfo, uint32_t* aSize, + uint32_t* aCount); + + // Asynchronously gets the disk cache size, used for display in the UI. + static nsresult AsyncGetDiskConsumption( + nsICacheStorageConsumptionObserver* aObserver); + + // Returns an iterator that returns entries matching a given context that were + // present in the index at the time this method was called. If aAddNew is true + // then the iterator will also return entries created after this call. + // NOTE: When some entry is removed from index it is removed also from the + // iterator regardless what aAddNew was passed. + static nsresult GetIterator(nsILoadContextInfo* aInfo, bool aAddNew, + CacheIndexIterator** _retval); + + // Returns true if we _think_ that the index is up to date. I.e. the state is + // READY or WRITING and mIndexNeedsUpdate as well as mShuttingDown is false. + static nsresult IsUpToDate(bool* _retval); + + // Called from CacheStorageService::Clear() and + // CacheFileContextEvictor::EvictEntries(), sets a flag that blocks + // notification to AsyncGetDiskConsumption. + static void OnAsyncEviction(bool aEvicting); + + // We keep track of total bytes written to the cache to be able to do + // a telemetry report after writting certain amount of data to the cache. + static void UpdateTotalBytesWritten(uint32_t aBytesWritten); + + // Memory reporting + static size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); + static size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf); + + private: + friend class CacheIndexEntryAutoManage; + friend class FileOpenHelper; + friend class CacheIndexIterator; + friend class CacheIndexRecordWrapper; + friend class DeleteCacheIndexRecordWrapper; + + virtual ~CacheIndex(); + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; + void OnFileOpenedInternal(FileOpenHelper* aOpener, CacheFileHandle* aHandle, + nsresult aResult, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override; + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) override; + + nsresult InitInternal(nsIFile* aCacheDirectory, + const StaticMutexAutoLock& aProofOfLock); + void PreShutdownInternal(); + + // This method returns false when index is not initialized or is shut down. + bool IsIndexUsable() MOZ_REQUIRES(sLock); + + // This method checks whether the entry has the same values of + // originAttributes and isAnonymous. We don't expect to find a collision + // since these values are part of the key that we hash and we use a strong + // hash function. + static bool IsCollision(CacheIndexEntry* aEntry, + OriginAttrsHash aOriginAttrsHash, bool aAnonymous); + + // Checks whether any of the information about the entry has changed. + static bool HasEntryChanged(CacheIndexEntry* aEntry, + const uint32_t* aFrecency, + const bool* aHasAltData, + const uint16_t* aOnStartTime, + const uint16_t* aOnStopTime, + const uint8_t* aContentType, + const uint32_t* aSize); + + // Merge all pending operations from mPendingUpdates into mIndex. + void ProcessPendingOperations(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + // Following methods perform writing of the index file. + // + // The index is written periodically, but not earlier than once in + // kMinDumpInterval and there must be at least kMinUnwrittenChanges + // differences between index on disk and in memory. Index is always first + // written to a temporary file and the old index file is replaced when the + // writing process succeeds. + // + // Starts writing of index when both limits (minimal delay between writes and + // minimum number of changes in index) were exceeded. + bool WriteIndexToDiskIfNeeded(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Starts writing of index file. + void WriteIndexToDisk(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Serializes part of mIndex hashtable to the write buffer a writes the buffer + // to the file. + void WriteRecords(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Finalizes writing process. + void FinishWrite(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + // Following methods perform writing of the journal during shutdown. All these + // methods must be called only during shutdown since they write/delete files + // directly on the main thread instead of using CacheFileIOManager that does + // it asynchronously on IO thread. Journal contains only entries that are + // dirty, i.e. changes that are not present in the index file on the disk. + // When the log is written successfully, the dirty flag in index file is + // cleared. + nsresult GetFile(const nsACString& aName, nsIFile** _retval); + void RemoveFile(const nsACString& aName) MOZ_REQUIRES(sLock); + void RemoveAllIndexFiles() MOZ_REQUIRES(sLock); + void RemoveJournalAndTempFile() MOZ_REQUIRES(sLock); + // Writes journal to the disk and clears dirty flag in index header. + nsresult WriteLogToDisk() MOZ_REQUIRES(sLock); + + // Following methods perform reading of the index from the disk. + // + // Index is read at startup just after initializing the CacheIndex. There are + // 3 files used when manipulating with index: index file, journal file and + // a temporary file. All files contain the hash of the data, so we can check + // whether the content is valid and complete. Index file contains also a dirty + // flag in the index header which is unset on a clean shutdown. During opening + // and reading of the files we determine the status of the whole index from + // the states of the separate files. Following table shows all possible + // combinations: + // + // index, journal, tmpfile + // M * * - index is missing -> BUILD + // I * * - index is invalid -> BUILD + // D * * - index is dirty -> UPDATE + // C M * - index is dirty -> UPDATE + // C I * - unexpected state -> UPDATE + // C V E - unexpected state -> UPDATE + // C V M - index is up to date -> READY + // + // where the letters mean: + // * - any state + // E - file exists + // M - file is missing + // I - data is invalid (parsing failed or hash didn't match) + // D - dirty (data in index file is correct, but dirty flag is set) + // C - clean (index file is clean) + // V - valid (data in journal file is correct) + // + // Note: We accept the data from journal only when the index is up to date as + // a whole (i.e. C,V,M state). + // + // We rename the journal file to the temporary file as soon as possible after + // initial test to ensure that we start update process on the next startup if + // FF crashes during parsing of the index. + // + // Initiates reading index from disk. + void ReadIndexFromDisk(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Starts reading data from index file. + void StartReadingIndex(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Parses data read from index file. + void ParseRecords(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Starts reading data from journal file. + void StartReadingJournal(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Parses data read from journal file. + void ParseJournal(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Merges entries from journal into mIndex. + void MergeJournal(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // In debug build this method checks that we have no fresh entry in mIndex + // after we finish reading index and before we process pending operations. + void EnsureNoFreshEntry() MOZ_REQUIRES(sLock); + // In debug build this method is called after processing pending operations + // to make sure mIndexStats contains correct information. + void EnsureCorrectStats() MOZ_REQUIRES(sLock); + + // Finalizes reading process. + void FinishRead(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + // Following methods perform updating and building of the index. + // Timer callback that starts update or build process. + static void DelayedUpdate(nsITimer* aTimer, void* aClosure); + void DelayedUpdateLocked(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Posts timer event that start update or build process. + nsresult ScheduleUpdateTimer(uint32_t aDelay) MOZ_REQUIRES(sLock); + nsresult SetupDirectoryEnumerator() MOZ_REQUIRES(sLock); + nsresult InitEntryFromDiskData(CacheIndexEntry* aEntry, + CacheFileMetadata* aMetaData, + int64_t aFileSize); + // Returns true when either a timer is scheduled or event is posted. + bool IsUpdatePending() MOZ_REQUIRES(sLock); + // Iterates through all files in entries directory that we didn't create/open + // during this session, parses them and adds the entries to the index. + void BuildIndex(const StaticMutexAutoLock& aProofOfLock) MOZ_REQUIRES(sLock); + + bool StartUpdatingIndexIfNeeded(const StaticMutexAutoLock& aProofOfLock, + bool aSwitchingToReadyState = false); + // Starts update or build process or fires a timer when it is too early after + // startup. + void StartUpdatingIndex(bool aRebuild, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + // Iterates through all files in entries directory that we didn't create/open + // during this session and theirs last modified time is newer than timestamp + // in the index header. Parses the files and adds the entries to the index. + void UpdateIndex(const StaticMutexAutoLock& aProofOfLock) MOZ_REQUIRES(sLock); + // Finalizes update or build process. + void FinishUpdate(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + void RemoveNonFreshEntries(const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + enum EState { + // Initial state in which the index is not usable + // Possible transitions: + // -> READING + INITIAL = 0, + + // Index is being read from the disk. + // Possible transitions: + // -> INITIAL - We failed to dispatch a read event. + // -> BUILDING - No or corrupted index file was found. + // -> UPDATING - No or corrupted journal file was found. + // - Dirty flag was set in index header. + // -> READY - Index was read successfully or was interrupted by + // pre-shutdown. + // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. + READING = 1, + + // Index is being written to the disk. + // Possible transitions: + // -> READY - Writing of index finished or was interrupted by + // pre-shutdown.. + // -> UPDATING - Writing of index finished, but index was found outdated + // during writing. + // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. + WRITING = 2, + + // Index is being build. + // Possible transitions: + // -> READY - Building of index finished or was interrupted by + // pre-shutdown. + // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. + BUILDING = 3, + + // Index is being updated. + // Possible transitions: + // -> READY - Updating of index finished or was interrupted by + // pre-shutdown. + // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. + UPDATING = 4, + + // Index is ready. + // Possible transitions: + // -> UPDATING - Index was found outdated. + // -> SHUTDOWN - Index is shutting down. + READY = 5, + + // Index is shutting down. + SHUTDOWN = 6 + }; + + static char const* StateString(EState aState); + void ChangeState(EState aNewState, const StaticMutexAutoLock& aProofOfLock); + void NotifyAsyncGetDiskConsumptionCallbacks() MOZ_REQUIRES(sLock); + + // Allocates and releases buffer used for reading and writing index. + void AllocBuffer() MOZ_REQUIRES(sLock); + void ReleaseBuffer() MOZ_REQUIRES(sLock); + + // Methods used by CacheIndexEntryAutoManage to keep the iterators up to date. + void AddRecordToIterators(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + void RemoveRecordFromIterators(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + void ReplaceRecordInIterators(CacheIndexRecordWrapper* aOldRecord, + CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock) + MOZ_REQUIRES(sLock); + + // Memory reporting (private part) + size_t SizeOfExcludingThisInternal(mozilla::MallocSizeOf mallocSizeOf) const + MOZ_REQUIRES(sLock); + + // Reports telemetry about cache, i.e. size, entry count and content type + // stats. + void DoTelemetryReport() MOZ_REQUIRES(sLock); + + static mozilla::StaticRefPtr gInstance MOZ_GUARDED_BY(sLock); + + // sLock guards almost everything here... + // Also guards FileOpenHelper::mCanceled + static StaticMutex sLock; + + nsCOMPtr mCacheDirectory; + + EState mState MOZ_GUARDED_BY(sLock){INITIAL}; + // Timestamp of time when the index was initialized. We use it to delay + // initial update or build of index. + TimeStamp mStartTime MOZ_GUARDED_BY(sLock); + // Set to true in PreShutdown(), it is checked on variaous places to prevent + // starting any process (write, update, etc.) during shutdown. + bool mShuttingDown MOZ_GUARDED_BY(sLock){false}; + // When set to true, update process should start as soon as possible. This + // flag is set whenever we find some inconsistency which would be fixed by + // update process. The flag is checked always when switching to READY state. + // To make sure we start the update process as soon as possible, methods that + // set this flag should also call StartUpdatingIndexIfNeeded() to cover the + // case when we are currently in READY state. + bool mIndexNeedsUpdate MOZ_GUARDED_BY(sLock){false}; + // Set at the beginning of RemoveAll() which clears the whole index. When + // removing all entries we must stop any pending reading, writing, updating or + // building operation. This flag is checked at various places and it prevents + // we won't start another operation (e.g. canceling reading of the index would + // normally start update or build process) + bool mRemovingAll MOZ_GUARDED_BY(sLock){false}; + // Whether the index file on disk exists and is valid. + bool mIndexOnDiskIsValid MOZ_GUARDED_BY(sLock){false}; + // When something goes wrong during updating or building process, we don't + // mark index clean (and also don't write journal) to ensure that update or + // build will be initiated on the next start. + bool mDontMarkIndexClean MOZ_GUARDED_BY(sLock){false}; + // Timestamp value from index file. It is used during update process to skip + // entries that were last modified before this timestamp. + uint32_t mIndexTimeStamp MOZ_GUARDED_BY(sLock){0}; + // Timestamp of last time the index was dumped to disk. + // NOTE: The index might not be necessarily dumped at this time. The value + // is used to schedule next dump of the index. + TimeStamp mLastDumpTime MOZ_GUARDED_BY(sLock); + + // Timer of delayed update/build. + nsCOMPtr mUpdateTimer MOZ_GUARDED_BY(sLock); + // True when build or update event is posted + bool mUpdateEventPending MOZ_GUARDED_BY(sLock){false}; + + // Helper members used when reading/writing index from/to disk. + // Contains number of entries that should be skipped: + // - in hashtable when writing index because they were already written + // - in index file when reading index because they were already read + uint32_t mSkipEntries MOZ_GUARDED_BY(sLock){0}; + // Number of entries that should be written to disk. This is number of entries + // in hashtable that are initialized and are not marked as removed when + // writing begins. + uint32_t mProcessEntries MOZ_GUARDED_BY(sLock){0}; + char* mRWBuf MOZ_GUARDED_BY(sLock){nullptr}; + uint32_t mRWBufSize MOZ_GUARDED_BY(sLock){0}; + uint32_t mRWBufPos MOZ_GUARDED_BY(sLock){0}; + RefPtr mRWHash MOZ_GUARDED_BY(sLock); + + // True if read or write operation is pending. It is used to ensure that + // mRWBuf is not freed until OnDataRead or OnDataWritten is called. + bool mRWPending MOZ_GUARDED_BY(sLock){false}; + + // Reading of journal succeeded if true. + bool mJournalReadSuccessfully MOZ_GUARDED_BY(sLock){false}; + + // Handle used for writing and reading index file. + RefPtr mIndexHandle MOZ_GUARDED_BY(sLock); + // Handle used for reading journal file. + RefPtr mJournalHandle MOZ_GUARDED_BY(sLock); + // Used to check the existence of the file during reading process. + RefPtr mTmpHandle MOZ_GUARDED_BY(sLock); + + RefPtr mIndexFileOpener MOZ_GUARDED_BY(sLock); + RefPtr mJournalFileOpener MOZ_GUARDED_BY(sLock); + RefPtr mTmpFileOpener MOZ_GUARDED_BY(sLock); + + // Directory enumerator used when building and updating index. + nsCOMPtr mDirEnumerator MOZ_GUARDED_BY(sLock); + + // Main index hashtable. + nsTHashtable mIndex MOZ_GUARDED_BY(sLock); + + // We cannot add, remove or change any entry in mIndex in states READING and + // WRITING. We track all changes in mPendingUpdates during these states. + nsTHashtable mPendingUpdates MOZ_GUARDED_BY(sLock); + + // Contains information statistics for mIndex + mPendingUpdates. + CacheIndexStats mIndexStats MOZ_GUARDED_BY(sLock); + + // When reading journal, we must first parse the whole file and apply the + // changes iff the journal was read successfully. mTmpJournal is used to store + // entries from the journal file. We throw away all these entries if parsing + // of the journal fails or the hash does not match. + nsTHashtable mTmpJournal MOZ_GUARDED_BY(sLock); + + // FrecencyArray maintains order of entry records for eviction. Ideally, the + // records would be ordered by frecency all the time, but since this would be + // quite expensive, we allow certain amount of entries to be out of order. + // When the frecency is updated the new value is always bigger than the old + // one. Instead of keeping updated entries at the same position, we move them + // at the end of the array. This protects recently updated entries from + // eviction. The array is sorted once we hit the limit of maximum unsorted + // entries. + class FrecencyArray { + class Iterator { + public: + explicit Iterator(nsTArray>* aRecs) + : mRecs(aRecs), mIdx(0) { + while (!Done() && !(*mRecs)[mIdx]) { + mIdx++; + } + } + + bool Done() const { return mIdx == mRecs->Length(); } + + CacheIndexRecordWrapper* Get() const { + MOZ_ASSERT(!Done()); + return (*mRecs)[mIdx]; + } + + void Next() { + MOZ_ASSERT(!Done()); + ++mIdx; + while (!Done() && !(*mRecs)[mIdx]) { + mIdx++; + } + } + + private: + nsTArray>* mRecs; + uint32_t mIdx; + }; + + public: + Iterator Iter() { return Iterator(&mRecs); } + + FrecencyArray() = default; + + // Methods used by CacheIndexEntryAutoManage to keep the array up to date. + void AppendRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock); + void RemoveRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock); + void ReplaceRecord(CacheIndexRecordWrapper* aOldRecord, + CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock); + void SortIfNeeded(const StaticMutexAutoLock& aProofOfLock); + bool RecordExistedUnlocked(CacheIndexRecordWrapper* aRecord); + + size_t Length() const { return mRecs.Length() - mRemovedElements; } + void Clear(const StaticMutexAutoLock& aProofOfLock) { mRecs.Clear(); } + + private: + friend class CacheIndex; + + nsTArray> mRecs; + uint32_t mUnsortedElements{0}; + // Instead of removing elements from the array immediately, we null them out + // and the iterator skips them when accessing the array. The null pointers + // are placed at the end during sorting and we strip them out all at once. + // This saves moving a lot of memory in nsTArray::RemoveElementsAt. + uint32_t mRemovedElements{0}; + }; + + FrecencyArray mFrecencyArray MOZ_GUARDED_BY(sLock); + + nsTArray mIterators MOZ_GUARDED_BY(sLock); + + // This flag is true iff we are between CacheStorageService:Clear() and + // processing all contexts to be evicted. It will make UI to show + // "calculating" instead of any intermediate cache size. + bool mAsyncGetDiskConsumptionBlocked MOZ_GUARDED_BY(sLock){false}; + + class DiskConsumptionObserver : public Runnable { + public: + static DiskConsumptionObserver* Init( + nsICacheStorageConsumptionObserver* aObserver) { + nsWeakPtr observer = do_GetWeakReference(aObserver); + if (!observer) return nullptr; + + return new DiskConsumptionObserver(observer); + } + + void OnDiskConsumption(int64_t aSize) { + mSize = aSize; + NS_DispatchToMainThread(this); + } + + private: + explicit DiskConsumptionObserver(nsWeakPtr const& aWeakObserver) + : Runnable("net::CacheIndex::DiskConsumptionObserver"), + mObserver(aWeakObserver), + mSize(0) {} + virtual ~DiskConsumptionObserver() { + if (mObserver && !NS_IsMainThread()) { + NS_ReleaseOnMainThread("DiskConsumptionObserver::mObserver", + mObserver.forget()); + } + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr observer = + do_QueryReferent(mObserver); + + mObserver = nullptr; + + if (observer) { + observer->OnNetworkCacheDiskConsumption(mSize); + } + + return NS_OK; + } + + nsWeakPtr mObserver; + int64_t mSize; + }; + + // List of async observers that want to get disk consumption information + nsTArray> mDiskConsumptionObservers + MOZ_GUARDED_BY(sLock); + + // Number of bytes written to the cache since the last telemetry report + uint64_t mTotalBytesWritten MOZ_GUARDED_BY(sLock){0}; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndexContextIterator.cpp b/netwerk/cache2/CacheIndexContextIterator.cpp new file mode 100644 index 0000000000..a523b21915 --- /dev/null +++ b/netwerk/cache2/CacheIndexContextIterator.cpp @@ -0,0 +1,24 @@ +/* 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 "CacheLog.h" +#include "CacheIndexContextIterator.h" +#include "CacheIndex.h" +#include "nsString.h" + +namespace mozilla::net { + +CacheIndexContextIterator::CacheIndexContextIterator(CacheIndex* aIndex, + bool aAddNew, + nsILoadContextInfo* aInfo) + : CacheIndexIterator(aIndex, aAddNew), mInfo(aInfo) {} + +void CacheIndexContextIterator::AddRecord( + CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock) { + if (CacheIndexEntry::RecordMatchesLoadContextInfo(aRecord, mInfo)) { + CacheIndexIterator::AddRecord(aRecord, aProofOfLock); + } +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheIndexContextIterator.h b/netwerk/cache2/CacheIndexContextIterator.h new file mode 100644 index 0000000000..f9c19409e7 --- /dev/null +++ b/netwerk/cache2/CacheIndexContextIterator.h @@ -0,0 +1,31 @@ +/* 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/. */ + +#ifndef CacheIndexContextIterator__h__ +#define CacheIndexContextIterator__h__ + +#include "CacheIndexIterator.h" + +class nsILoadContextInfo; + +namespace mozilla { +namespace net { + +class CacheIndexContextIterator : public CacheIndexIterator { + public: + CacheIndexContextIterator(CacheIndex* aIndex, bool aAddNew, + nsILoadContextInfo* aInfo); + virtual ~CacheIndexContextIterator() = default; + + private: + virtual void AddRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) override; + + nsCOMPtr mInfo; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndexIterator.cpp b/netwerk/cache2/CacheIndexIterator.cpp new file mode 100644 index 0000000000..5f1a78470c --- /dev/null +++ b/netwerk/cache2/CacheIndexIterator.cpp @@ -0,0 +1,110 @@ +/* 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 "CacheLog.h" +#include "CacheIndexIterator.h" +#include "CacheIndex.h" +#include "nsString.h" +#include "mozilla/DebugOnly.h" + +namespace mozilla::net { + +CacheIndexIterator::CacheIndexIterator(CacheIndex* aIndex, bool aAddNew) + : mStatus(NS_OK), mIndex(aIndex), mAddNew(aAddNew) { + LOG(("CacheIndexIterator::CacheIndexIterator() [this=%p]", this)); +} + +CacheIndexIterator::~CacheIndexIterator() { + LOG(("CacheIndexIterator::~CacheIndexIterator() [this=%p]", this)); + + StaticMutexAutoLock lock(CacheIndex::sLock); + ClearRecords(lock); + CloseInternal(NS_ERROR_NOT_AVAILABLE); +} + +nsresult CacheIndexIterator::GetNextHash(SHA1Sum::Hash* aHash) { + LOG(("CacheIndexIterator::GetNextHash() [this=%p]", this)); + + StaticMutexAutoLock lock(CacheIndex::sLock); + + if (NS_FAILED(mStatus)) { + return mStatus; + } + + if (!mRecords.Length()) { + CloseInternal(NS_ERROR_NOT_AVAILABLE); + return mStatus; + } + + memcpy(aHash, mRecords.PopLastElement()->Get()->mHash, sizeof(SHA1Sum::Hash)); + + return NS_OK; +} + +nsresult CacheIndexIterator::Close() { + LOG(("CacheIndexIterator::Close() [this=%p]", this)); + + StaticMutexAutoLock lock(CacheIndex::sLock); + + return CloseInternal(NS_ERROR_NOT_AVAILABLE); +} + +nsresult CacheIndexIterator::CloseInternal(nsresult aStatus) { + LOG(("CacheIndexIterator::CloseInternal() [this=%p, status=0x%08" PRIx32 "]", + this, static_cast(aStatus))); + + // Make sure status will be a failure + MOZ_ASSERT(NS_FAILED(aStatus)); + if (NS_SUCCEEDED(aStatus)) { + aStatus = NS_ERROR_UNEXPECTED; + } + + if (NS_FAILED(mStatus)) { + return NS_ERROR_NOT_AVAILABLE; + } + + CacheIndex::sLock.AssertCurrentThreadOwns(); + DebugOnly removed = mIndex->mIterators.RemoveElement(this); + MOZ_ASSERT(removed); + mStatus = aStatus; + + return NS_OK; +} + +void CacheIndexIterator::ClearRecords(const StaticMutexAutoLock& aProofOfLock) { + mRecords.Clear(); +} + +void CacheIndexIterator::AddRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) { + LOG(("CacheIndexIterator::AddRecord() [this=%p, record=%p]", this, aRecord)); + + mRecords.AppendElement(aRecord); +} + +bool CacheIndexIterator::RemoveRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock) { + LOG(("CacheIndexIterator::RemoveRecord() [this=%p, record=%p]", this, + aRecord)); + + return mRecords.RemoveElement(aRecord); +} + +bool CacheIndexIterator::ReplaceRecord( + CacheIndexRecordWrapper* aOldRecord, CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock) { + LOG( + ("CacheIndexIterator::ReplaceRecord() [this=%p, oldRecord=%p, " + "newRecord=%p]", + this, aOldRecord, aNewRecord)); + + if (RemoveRecord(aOldRecord, aProofOfLock)) { + AddRecord(aNewRecord, aProofOfLock); + return true; + } + + return false; +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheIndexIterator.h b/netwerk/cache2/CacheIndexIterator.h new file mode 100644 index 0000000000..2f9aedb659 --- /dev/null +++ b/netwerk/cache2/CacheIndexIterator.h @@ -0,0 +1,62 @@ +/* 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/. */ + +#ifndef CacheIndexIterator__h__ +#define CacheIndexIterator__h__ + +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "mozilla/SHA1.h" +#include "mozilla/StaticMutex.h" + +namespace mozilla { +namespace net { + +class CacheIndex; +class CacheIndexRecordWrapper; + +class CacheIndexIterator { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheIndexIterator) + + CacheIndexIterator(CacheIndex* aIndex, bool aAddNew); + + protected: + virtual ~CacheIndexIterator(); + + public: + // Returns a hash of a next entry. If there is no entry NS_ERROR_NOT_AVAILABLE + // is returned and the iterator is closed. Other error is returned when the + // iterator is closed for other reason, e.g. shutdown. + nsresult GetNextHash(SHA1Sum::Hash* aHash); + + // Closes the iterator. This means the iterator is removed from the list of + // iterators in CacheIndex. + nsresult Close(); + + protected: + friend class CacheIndex; + + nsresult CloseInternal(nsresult aStatus); + + bool ShouldBeNewAdded() { return mAddNew; } + virtual void AddRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock); + bool RemoveRecord(CacheIndexRecordWrapper* aRecord, + const StaticMutexAutoLock& aProofOfLock); + bool ReplaceRecord(CacheIndexRecordWrapper* aOldRecord, + CacheIndexRecordWrapper* aNewRecord, + const StaticMutexAutoLock& aProofOfLock); + void ClearRecords(const StaticMutexAutoLock& aProofOfLock); + + nsresult mStatus; + RefPtr mIndex; + nsTArray> mRecords; + bool mAddNew; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheLog.cpp b/netwerk/cache2/CacheLog.cpp new file mode 100644 index 0000000000..ea81ff2a19 --- /dev/null +++ b/netwerk/cache2/CacheLog.cpp @@ -0,0 +1,20 @@ +/* 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 "CacheLog.h" + +namespace mozilla::net { + +// Log module for cache2 (2013) cache implementation logging... +// +// To enable logging (see prlog.h for full details): +// +// set MOZ_LOG=cache2:5 +// set MOZ_LOG_FILE=network.log +// +// This enables LogLevel::Debug level information and places all output in +// the file network.log. +LazyLogModule gCache2Log("cache2"); + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheLog.h b/netwerk/cache2/CacheLog.h new file mode 100644 index 0000000000..f4117ea4f7 --- /dev/null +++ b/netwerk/cache2/CacheLog.h @@ -0,0 +1,20 @@ +/* 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/. */ + +#ifndef Cache2Log__h__ +#define Cache2Log__h__ + +#include "mozilla/Logging.h" + +namespace mozilla { +namespace net { + +extern LazyLogModule gCache2Log; +#define LOG(x) MOZ_LOG(gCache2Log, mozilla::LogLevel::Debug, x) +#define LOG_ENABLED() MOZ_LOG_TEST(gCache2Log, mozilla::LogLevel::Debug) + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheObserver.cpp b/netwerk/cache2/CacheObserver.cpp new file mode 100644 index 0000000000..b1e37c88cb --- /dev/null +++ b/netwerk/cache2/CacheObserver.cpp @@ -0,0 +1,254 @@ +/* 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 "CacheObserver.h" + +#include "CacheStorageService.h" +#include "CacheFileIOManager.h" +#include "LoadContextInfo.h" +#include "nsICacheStorage.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "mozilla/TimeStamp.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/net/NeckoCommon.h" +#include "prsystem.h" +#include +#include + +namespace mozilla::net { + +StaticRefPtr CacheObserver::sSelf; + +static float const kDefaultHalfLifeHours = 24.0F; // 24 hours +float CacheObserver::sHalfLifeHours = kDefaultHalfLifeHours; + +// The default value will be overwritten as soon as the correct smart size is +// calculated by CacheFileIOManager::UpdateSmartCacheSize(). It's limited to 1GB +// just for case the size is never calculated which might in theory happen if +// GetDiskSpaceAvailable() always fails. +Atomic CacheObserver::sSmartDiskCacheCapacity(1024 * 1024); + +Atomic CacheObserver::sShutdownDemandedTime( + PR_INTERVAL_NO_TIMEOUT); + +NS_IMPL_ISUPPORTS(CacheObserver, nsIObserver, nsISupportsWeakReference) + +// static +nsresult CacheObserver::Init() { + if (IsNeckoChild()) { + return NS_OK; + } + + if (sSelf) { + return NS_OK; + } + + nsCOMPtr obs = mozilla::services::GetObserverService(); + if (!obs) { + return NS_ERROR_UNEXPECTED; + } + + sSelf = new CacheObserver(); + + obs->AddObserver(sSelf, "prefservice:after-app-defaults", true); + obs->AddObserver(sSelf, "profile-do-change", true); + obs->AddObserver(sSelf, "profile-before-change", true); + obs->AddObserver(sSelf, "xpcom-shutdown", true); + obs->AddObserver(sSelf, "last-pb-context-exited", true); + obs->AddObserver(sSelf, "memory-pressure", true); + obs->AddObserver(sSelf, "browser-delayed-startup-finished", true); + + return NS_OK; +} + +// static +nsresult CacheObserver::Shutdown() { + if (!sSelf) { + return NS_ERROR_NOT_INITIALIZED; + } + + sSelf = nullptr; + return NS_OK; +} + +void CacheObserver::AttachToPreferences() { + mozilla::Preferences::GetComplex( + "browser.cache.disk.parent_directory", NS_GET_IID(nsIFile), + getter_AddRefs(mCacheParentDirectoryOverride)); + + sHalfLifeHours = std::max( + 0.01F, std::min(1440.0F, mozilla::Preferences::GetFloat( + "browser.cache.frecency_half_life_hours", + kDefaultHalfLifeHours))); +} + +// static +uint32_t CacheObserver::MemoryCacheCapacity() { + if (StaticPrefs::browser_cache_memory_capacity() >= 0) { + return StaticPrefs::browser_cache_memory_capacity(); + } + + // Cache of the calculated memory capacity based on the system memory size in + // KB (C++11 guarantees local statics will be initialized once and in a + // thread-safe way.) + static int32_t sAutoMemoryCacheCapacity = ([] { + uint64_t bytes = PR_GetPhysicalMemorySize(); + // If getting the physical memory failed, arbitrarily assume + // 32 MB of RAM. We use a low default to have a reasonable + // size on all the devices we support. + if (bytes == 0) { + bytes = 32 * 1024 * 1024; + } + // Conversion from unsigned int64_t to double doesn't work on all platforms. + // We need to truncate the value at INT64_MAX to make sure we don't + // overflow. + if (bytes > INT64_MAX) { + bytes = INT64_MAX; + } + uint64_t kbytes = bytes >> 10; + double kBytesD = double(kbytes); + double x = log(kBytesD) / log(2.0) - 14; + + int32_t capacity = 0; + if (x > 0) { + // 0.1 is added here for rounding + capacity = (int32_t)(x * x / 3.0 + x + 2.0 / 3 + 0.1); + if (capacity > 32) { + capacity = 32; + } + capacity <<= 10; + } + return capacity; + })(); + + return sAutoMemoryCacheCapacity; +} + +// static +void CacheObserver::SetSmartDiskCacheCapacity(uint32_t aCapacity) { + sSmartDiskCacheCapacity = aCapacity; +} + +// static +uint32_t CacheObserver::DiskCacheCapacity() { + return SmartCacheSizeEnabled() ? sSmartDiskCacheCapacity + : StaticPrefs::browser_cache_disk_capacity(); +} + +// static +void CacheObserver::ParentDirOverride(nsIFile** aDir) { + if (NS_WARN_IF(!aDir)) return; + + *aDir = nullptr; + + if (!sSelf) return; + if (!sSelf->mCacheParentDirectoryOverride) return; + + sSelf->mCacheParentDirectoryOverride->Clone(aDir); +} + +// static +bool CacheObserver::EntryIsTooBig(int64_t aSize, bool aUsingDisk) { + // If custom limit is set, check it. + int64_t preferredLimit = + aUsingDisk ? MaxDiskEntrySize() : MaxMemoryEntrySize(); + + // do not convert to bytes when the limit is -1, which means no limit + if (preferredLimit > 0) { + preferredLimit <<= 10; + } + + if (preferredLimit != -1 && aSize > preferredLimit) return true; + + // Otherwise (or when in the custom limit), check limit based on the global + // limit. It's 1/8 of the respective capacity. + int64_t derivedLimit = + aUsingDisk ? DiskCacheCapacity() : MemoryCacheCapacity(); + derivedLimit <<= (10 - 3); + + return aSize > derivedLimit; +} + +// static +bool CacheObserver::IsPastShutdownIOLag() { +#ifdef DEBUG + return false; +#else + if (sShutdownDemandedTime == PR_INTERVAL_NO_TIMEOUT || + MaxShutdownIOLag() == UINT32_MAX) { + return false; + } + + static const PRIntervalTime kMaxShutdownIOLag = + PR_SecondsToInterval(MaxShutdownIOLag()); + + if ((PR_IntervalNow() - sShutdownDemandedTime) > kMaxShutdownIOLag) { + return true; + } + + return false; +#endif +} + +NS_IMETHODIMP +CacheObserver::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (!strcmp(aTopic, "prefservice:after-app-defaults")) { + CacheFileIOManager::Init(); + return NS_OK; + } + + if (!strcmp(aTopic, "profile-do-change")) { + AttachToPreferences(); + CacheFileIOManager::Init(); + CacheFileIOManager::OnProfile(); + return NS_OK; + } + + if (!strcmp(aTopic, "profile-change-net-teardown") || + !strcmp(aTopic, "profile-before-change") || + !strcmp(aTopic, "xpcom-shutdown")) { + if (sShutdownDemandedTime == PR_INTERVAL_NO_TIMEOUT) { + sShutdownDemandedTime = PR_IntervalNow(); + } + + RefPtr service = CacheStorageService::Self(); + if (service) { + service->Shutdown(); + } + + CacheFileIOManager::Shutdown(); + return NS_OK; + } + + if (!strcmp(aTopic, "last-pb-context-exited")) { + RefPtr service = CacheStorageService::Self(); + if (service) { + service->DropPrivateBrowsingEntries(); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "memory-pressure")) { + RefPtr service = CacheStorageService::Self(); + if (service) { + service->PurgeFromMemory(nsICacheStorageService::PURGE_EVERYTHING); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "browser-delayed-startup-finished")) { + CacheFileIOManager::OnDelayedStartupFinished(); + return NS_OK; + } + + MOZ_ASSERT(false, "Missing observer handler"); + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheObserver.h b/netwerk/cache2/CacheObserver.h new file mode 100644 index 0000000000..633af7eec5 --- /dev/null +++ b/netwerk/cache2/CacheObserver.h @@ -0,0 +1,109 @@ +/* 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/. */ + +#ifndef CacheObserver__h__ +#define CacheObserver__h__ + +#include "nsIObserver.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsWeakReference.h" +#include "mozilla/StaticPrefs_browser.h" +#include "mozilla/StaticPrefs_privacy.h" +#include "mozilla/StaticPtr.h" +#include + +namespace mozilla { +namespace net { + +class CacheObserver : public nsIObserver, public nsSupportsWeakReference { + virtual ~CacheObserver() = default; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + static nsresult Init(); + static nsresult Shutdown(); + static CacheObserver* Self() { return sSelf; } + + // Access to preferences + static bool UseDiskCache() { + return StaticPrefs::browser_cache_disk_enable(); + } + static bool UseMemoryCache() { + return StaticPrefs::browser_cache_memory_enable(); + } + static uint32_t MetadataMemoryLimit() // result in kilobytes. + { + return StaticPrefs::browser_cache_disk_metadata_memory_limit(); + } + static uint32_t MemoryCacheCapacity(); // result in kilobytes. + static uint32_t DiskCacheCapacity(); // result in kilobytes. + static void SetSmartDiskCacheCapacity(uint32_t); // parameter in kilobytes. + static uint32_t DiskFreeSpaceSoftLimit() // result in kilobytes. + { + return StaticPrefs::browser_cache_disk_free_space_soft_limit(); + } + static uint32_t DiskFreeSpaceHardLimit() // result in kilobytes. + { + return StaticPrefs::browser_cache_disk_free_space_hard_limit(); + } + static bool SmartCacheSizeEnabled() { + return StaticPrefs::browser_cache_disk_smart_size_enabled(); + } + static uint32_t PreloadChunkCount() { + return StaticPrefs::browser_cache_disk_preload_chunk_count(); + } + static uint32_t MaxMemoryEntrySize() // result in kilobytes. + { + return StaticPrefs::browser_cache_memory_max_entry_size(); + } + static uint32_t MaxDiskEntrySize() // result in kilobytes. + { + return StaticPrefs::browser_cache_disk_max_entry_size(); + } + static uint32_t MaxDiskChunksMemoryUsage( + bool aPriority) // result in kilobytes. + { + return aPriority + ? StaticPrefs:: + browser_cache_disk_max_priority_chunks_memory_usage() + : StaticPrefs::browser_cache_disk_max_chunks_memory_usage(); + } + static uint32_t HalfLifeSeconds() { return sHalfLifeHours * 60.0F * 60.0F; } + static bool ClearCacheOnShutdown() { + return StaticPrefs::privacy_sanitize_sanitizeOnShutdown() && + StaticPrefs::privacy_clearOnShutdown_cache(); + } + static void ParentDirOverride(nsIFile** aDir); + + static bool EntryIsTooBig(int64_t aSize, bool aUsingDisk); + + static uint32_t MaxShutdownIOLag() { + return StaticPrefs::browser_cache_max_shutdown_io_lag(); + } + static bool IsPastShutdownIOLag(); + + static bool ShuttingDown() { + return sShutdownDemandedTime != PR_INTERVAL_NO_TIMEOUT; + } + + private: + static StaticRefPtr sSelf; + + void AttachToPreferences(); + + static int32_t sAutoMemoryCacheCapacity; + static Atomic sSmartDiskCacheCapacity; + static float sHalfLifeHours; + static Atomic sShutdownDemandedTime; + + // Non static properties, accessible via sSelf + nsCOMPtr mCacheParentDirectoryOverride; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CachePurgeLock.cpp b/netwerk/cache2/CachePurgeLock.cpp new file mode 100644 index 0000000000..6328891669 --- /dev/null +++ b/netwerk/cache2/CachePurgeLock.cpp @@ -0,0 +1,115 @@ +/* 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 "CachePurgeLock.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsAppRunner.h" +#include "mozilla/MultiInstanceLock.h" +#include "nsLocalFile.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(CachePurgeLock, nsICachePurgeLock) + +static nsresult PrepareLockArguments(const nsACString& profileName, + nsCString& lockName, + nsString& appDirPath) { + nsresult rv; + + nsCOMPtr appFile = mozilla::GetNormalizedAppFile(nullptr); + if (!appFile) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr appDirFile; + rv = appFile->GetParent(getter_AddRefs(appDirFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appDirFile->GetPath(appDirPath); + NS_ENSURE_SUCCESS(rv, rv); + + lockName = profileName; + lockName.Append("-cachePurge"); + + return NS_OK; +} + +NS_IMETHODIMP +CachePurgeLock::GetLockFile(const nsACString& profileName, nsIFile** aResult) { + nsresult rv; + nsCString lockName; + nsString appDirPath; + rv = PrepareLockArguments(profileName, lockName, appDirPath); + if (NS_FAILED(rv)) { + return rv; + } + + nsCString filePath; + if (!GetMultiInstanceLockFileName(lockName.get(), appDirPath.get(), + filePath)) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr lockFile = new nsLocalFile(); + rv = lockFile->InitWithNativePath(filePath); + NS_ENSURE_SUCCESS(rv, rv); + + lockFile.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +CachePurgeLock::Lock(const nsACString& profileName) { + nsresult rv; + if (mLock != MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + // Lock is already open. + return NS_OK; + } + + nsCString lockName; + nsString appDirPath; + rv = PrepareLockArguments(profileName, lockName, appDirPath); + if (NS_FAILED(rv)) { + return rv; + } + + mLock = mozilla::OpenMultiInstanceLock(lockName.get(), appDirPath.get()); + if (mLock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +CachePurgeLock::IsOtherInstanceRunning(bool* aResult) { + if (NS_WARN_IF(XRE_GetProcessType() != GeckoProcessType_Default)) { + return NS_ERROR_SERVICE_NOT_AVAILABLE; + } + + if (mLock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool rv = mozilla::IsOtherInstanceRunning(mLock, aResult); + NS_ENSURE_TRUE(rv, NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP +CachePurgeLock::Unlock() { + if (mLock == MULTI_INSTANCE_LOCK_HANDLE_ERROR) { + // Lock is already released. + return NS_OK; + } + + mozilla::ReleaseMultiInstanceLock(mLock); + mLock = MULTI_INSTANCE_LOCK_HANDLE_ERROR; + + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CachePurgeLock.h b/netwerk/cache2/CachePurgeLock.h new file mode 100644 index 0000000000..464b4e681d --- /dev/null +++ b/netwerk/cache2/CachePurgeLock.h @@ -0,0 +1,24 @@ +/* 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/. */ + +#ifndef mozilla_net_CachePurgeLock_h__ +#define mozilla_net_CachePurgeLock_h__ + +#include "nsICachePurgeLock.h" +#include "mozilla/MultiInstanceLock.h" + +namespace mozilla::net { + +class CachePurgeLock : public nsICachePurgeLock { + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEPURGELOCK + private: + virtual ~CachePurgeLock() = default; + + MultiInstLockHandle mLock = MULTI_INSTANCE_LOCK_HANDLE_ERROR; +}; + +} // namespace mozilla::net + +#endif // mozilla_net_CachePurgeLock_h__ diff --git a/netwerk/cache2/CacheStorage.cpp b/netwerk/cache2/CacheStorage.cpp new file mode 100644 index 0000000000..b4177430a2 --- /dev/null +++ b/netwerk/cache2/CacheStorage.cpp @@ -0,0 +1,196 @@ +/* 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 "CacheLog.h" +#include "CacheStorage.h" +#include "CacheStorageService.h" +#include "CacheEntry.h" +#include "CacheObserver.h" + +#include "nsICacheEntryDoomCallback.h" + +#include "nsIURI.h" +#include "nsNetUtil.h" + +namespace mozilla::net { + +NS_IMPL_ISUPPORTS(CacheStorage, nsICacheStorage) + +CacheStorage::CacheStorage(nsILoadContextInfo* aInfo, bool aAllowDisk, + bool aSkipSizeCheck, bool aPinning) + : mLoadContextInfo(aInfo ? GetLoadContextInfo(aInfo) : nullptr), + mWriteToDisk(aAllowDisk), + mSkipSizeCheck(aSkipSizeCheck), + mPinning(aPinning) {} + +NS_IMETHODIMP CacheStorage::AsyncOpenURI(nsIURI* aURI, + const nsACString& aIdExtension, + uint32_t aFlags, + nsICacheEntryOpenCallback* aCallback) { + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + if (MOZ_UNLIKELY(!CacheObserver::UseDiskCache()) && mWriteToDisk && + !(aFlags & OPEN_INTERCEPTED)) { + aCallback->OnCacheEntryAvailable(nullptr, false, NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + + if (MOZ_UNLIKELY(!CacheObserver::UseMemoryCache()) && !mWriteToDisk && + !(aFlags & OPEN_INTERCEPTED)) { + aCallback->OnCacheEntryAvailable(nullptr, false, NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr entry; + rv = CacheStorageService::Self()->AddStorageEntry( + this, asciiSpec, aIdExtension, aFlags, getter_AddRefs(entry)); + if (NS_FAILED(rv)) { + aCallback->OnCacheEntryAvailable(nullptr, false, rv); + return NS_OK; + } + + // May invoke the callback synchronously + entry->Entry()->AsyncOpen(aCallback, aFlags); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorage::OpenTruncate(nsIURI* aURI, + const nsACString& aIdExtension, + nsICacheEntry** aCacheEntry) { + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr handle; + rv = CacheStorageService::Self()->AddStorageEntry( + this, asciiSpec, aIdExtension, + nsICacheStorage::OPEN_TRUNCATE, // replace any existing one + getter_AddRefs(handle)); + NS_ENSURE_SUCCESS(rv, rv); + + // Just open w/o callback, similar to nsICacheEntry.recreate(). + handle->Entry()->AsyncOpen(nullptr, OPEN_TRUNCATE); + + // Return a write handler, consumer is supposed to fill in the entry. + RefPtr writeHandle = handle->Entry()->NewWriteHandle(); + writeHandle.forget(aCacheEntry); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorage::Exists(nsIURI* aURI, const nsACString& aIdExtension, + bool* aResult) { + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aResult); + + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + return CacheStorageService::Self()->CheckStorageEntry(this, asciiSpec, + aIdExtension, aResult); +} + +NS_IMETHODIMP +CacheStorage::GetCacheIndexEntryAttrs(nsIURI* aURI, + const nsACString& aIdExtension, + bool* aHasAltData, uint32_t* aSizeInKB) { + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aHasAltData); + NS_ENSURE_ARG(aSizeInKB); + if (!CacheStorageService::Self()) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + return CacheStorageService::Self()->GetCacheIndexEntryAttrs( + this, asciiSpec, aIdExtension, aHasAltData, aSizeInKB); +} + +NS_IMETHODIMP CacheStorage::AsyncDoomURI(nsIURI* aURI, + const nsACString& aIdExtension, + nsICacheEntryDoomCallback* aCallback) { + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + nsCOMPtr noRefURI; + rv = NS_GetURIWithoutRef(aURI, getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CacheStorageService::Self()->DoomStorageEntry(this, asciiSpec, + aIdExtension, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorage::AsyncEvictStorage( + nsICacheEntryDoomCallback* aCallback) { + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = + CacheStorageService::Self()->DoomStorageEntries(this, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorage::AsyncVisitStorage(nsICacheStorageVisitor* aVisitor, + bool aVisitEntries) { + LOG(("CacheStorage::AsyncVisitStorage [this=%p, cb=%p, disk=%d]", this, + aVisitor, (bool)mWriteToDisk)); + if (!CacheStorageService::Self()) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = CacheStorageService::Self()->WalkStorageEntries( + this, aVisitEntries, aVisitor); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheStorage.h b/netwerk/cache2/CacheStorage.h new file mode 100644 index 0000000000..f4db3e2a96 --- /dev/null +++ b/netwerk/cache2/CacheStorage.h @@ -0,0 +1,63 @@ +/* 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/. */ + +#ifndef CacheStorage__h__ +#define CacheStorage__h__ + +#include "nsICacheStorage.h" +#include "CacheEntry.h" +#include "LoadContextInfo.h" + +#include "nsILoadContextInfo.h" + +class nsIURI; + +namespace mozilla { +namespace net { + +// This dance is needed to make CacheEntryTable declarable-only in headers +// w/o exporting CacheEntry.h file to make nsNetModule.cpp compilable. +using TCacheEntryTable = nsRefPtrHashtable; +class CacheEntryTable : public TCacheEntryTable { + public: + enum EType { MEMORY_ONLY, ALL_ENTRIES }; + + explicit CacheEntryTable(EType aType) : mType(aType) {} + EType Type() const { return mType; } + + private: + EType const mType; + CacheEntryTable() = delete; +}; + +class CacheStorage : public nsICacheStorage { + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHESTORAGE + + public: + CacheStorage(nsILoadContextInfo* aInfo, bool aAllowDisk, bool aSkipSizeCheck, + bool aPinning); + + protected: + virtual ~CacheStorage() = default; + + RefPtr mLoadContextInfo; + bool mWriteToDisk : 1; + bool mSkipSizeCheck : 1; + bool mPinning : 1; + + public: + nsILoadContextInfo* LoadInfo() const { return mLoadContextInfo; } + bool WriteToDisk() const { + return mWriteToDisk && + (!mLoadContextInfo || !mLoadContextInfo->IsPrivate()); + } + bool SkipSizeCheck() const { return mSkipSizeCheck; } + bool Pinning() const { return mPinning; } +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheStorageService.cpp b/netwerk/cache2/CacheStorageService.cpp new file mode 100644 index 0000000000..cb34b64b27 --- /dev/null +++ b/netwerk/cache2/CacheStorageService.cpp @@ -0,0 +1,2369 @@ +/* -*- 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 "CacheLog.h" +#include "CacheStorageService.h" +#include "CacheFileIOManager.h" +#include "CacheObserver.h" +#include "CacheIndex.h" +#include "CacheIndexIterator.h" +#include "CacheStorage.h" +#include "CacheEntry.h" +#include "CacheFileUtils.h" + +#include "nsICacheStorageVisitor.h" +#include "nsIObserverService.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsINetworkPredictor.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsXULAppAPI.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Services.h" +#include "mozilla/StoragePrincipalHelper.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Telemetry.h" +#include "mozilla/StaticPrefs_network.h" + +namespace mozilla::net { + +namespace { + +void AppendMemoryStorageTag(nsAutoCString& key) { + // Using DEL as the very last ascii-7 character we can use in the list of + // attributes + key.Append('\x7f'); + key.Append(','); +} + +} // namespace + +// Not defining as static or class member of CacheStorageService since +// it would otherwise need to include CacheEntry.h and that then would +// need to be exported to make nsNetModule.cpp compilable. +using GlobalEntryTables = nsClassHashtable; + +/** + * Keeps tables of entries. There is one entries table for each distinct load + * context type. The distinction is based on following load context info + * states: which builds a mapping + * key. + * + * Thread-safe to access, protected by the service mutex. + */ +static GlobalEntryTables* sGlobalEntryTables; + +CacheMemoryConsumer::CacheMemoryConsumer(uint32_t aFlags) { + StoreFlags(aFlags); +} + +void CacheMemoryConsumer::DoMemoryReport(uint32_t aCurrentSize) { + if (!(LoadFlags() & DONT_REPORT) && CacheStorageService::Self()) { + CacheStorageService::Self()->OnMemoryConsumptionChange(this, aCurrentSize); + } +} + +CacheStorageService::MemoryPool::MemoryPool(EType aType) : mType(aType) {} + +CacheStorageService::MemoryPool::~MemoryPool() { + if (mMemorySize != 0) { + NS_ERROR( + "Network cache reported memory consumption is not at 0, probably " + "leaking?"); + } +} + +uint32_t CacheStorageService::MemoryPool::Limit() const { + uint32_t limit = 0; + + switch (mType) { + case DISK: + limit = CacheObserver::MetadataMemoryLimit(); + break; + case MEMORY: + limit = CacheObserver::MemoryCacheCapacity(); + break; + default: + MOZ_CRASH("Bad pool type"); + } + + static const uint32_t kMaxLimit = 0x3FFFFF; + if (limit > kMaxLimit) { + LOG((" a memory limit (%u) is unexpectedly high, clipping to %u", limit, + kMaxLimit)); + limit = kMaxLimit; + } + + return limit << 10; +} + +NS_IMPL_ISUPPORTS(CacheStorageService, nsICacheStorageService, + nsIMemoryReporter, nsITimerCallback, nsICacheTesting, + nsINamed) + +CacheStorageService* CacheStorageService::sSelf = nullptr; + +CacheStorageService::CacheStorageService() { + CacheFileIOManager::Init(); + + MOZ_ASSERT(XRE_IsParentProcess()); + MOZ_ASSERT(!sSelf); + + sSelf = this; + sGlobalEntryTables = new GlobalEntryTables(); + + RegisterStrongMemoryReporter(this); +} + +CacheStorageService::~CacheStorageService() { + LOG(("CacheStorageService::~CacheStorageService")); + sSelf = nullptr; +} + +void CacheStorageService::Shutdown() { + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) return; + + LOG(("CacheStorageService::Shutdown - start")); + + mShutdown = true; + + nsCOMPtr event = + NewRunnableMethod("net::CacheStorageService::ShutdownBackground", this, + &CacheStorageService::ShutdownBackground); + Dispatch(event); + +#ifdef NS_FREE_PERMANENT_DATA + sGlobalEntryTables->Clear(); + delete sGlobalEntryTables; +#endif + sGlobalEntryTables = nullptr; + + LOG(("CacheStorageService::Shutdown - done")); +} + +void CacheStorageService::ShutdownBackground() { + LOG(("CacheStorageService::ShutdownBackground - start")); + + MOZ_ASSERT(IsOnManagementThread()); + + { + mozilla::MutexAutoLock lock(mLock); + + // Cancel purge timer to avoid leaking. + if (mPurgeTimer) { + LOG((" freeing the timer")); + mPurgeTimer->Cancel(); + } + } + +#ifdef NS_FREE_PERMANENT_DATA + Pool(false).mFrecencyArray.Clear(); + Pool(false).mExpirationArray.Clear(); + Pool(true).mFrecencyArray.Clear(); + Pool(true).mExpirationArray.Clear(); +#endif + + LOG(("CacheStorageService::ShutdownBackground - done")); +} + +// Internal management methods + +namespace { + +// WalkCacheRunnable +// Base class for particular storage entries visiting +class WalkCacheRunnable : public Runnable, + public CacheStorageService::EntryInfoCallback { + protected: + WalkCacheRunnable(nsICacheStorageVisitor* aVisitor, bool aVisitEntries) + : Runnable("net::WalkCacheRunnable"), + mService(CacheStorageService::Self()), + mCallback(aVisitor) { + MOZ_ASSERT(NS_IsMainThread()); + StoreNotifyStorage(true); + StoreVisitEntries(aVisitEntries); + } + + virtual ~WalkCacheRunnable() { + if (mCallback) { + ProxyReleaseMainThread("WalkCacheRunnable::mCallback", mCallback); + } + } + + RefPtr mService; + nsCOMPtr mCallback; + + uint64_t mSize{0}; + + // clang-format off + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields, 8, ( + (bool, NotifyStorage, 1), + (bool, VisitEntries, 1) + )) + // clang-format on + + Atomic mCancel{false}; +}; + +// WalkMemoryCacheRunnable +// Responsible to visit memory storage and walk +// all entries on it asynchronously. +class WalkMemoryCacheRunnable : public WalkCacheRunnable { + public: + WalkMemoryCacheRunnable(nsILoadContextInfo* aLoadInfo, bool aVisitEntries, + nsICacheStorageVisitor* aVisitor) + : WalkCacheRunnable(aVisitor, aVisitEntries) { + CacheFileUtils::AppendKeyPrefix(aLoadInfo, mContextKey); + MOZ_ASSERT(NS_IsMainThread()); + } + + nsresult Walk() { return mService->Dispatch(this); } + + private: + NS_IMETHOD Run() override { + if (CacheStorageService::IsOnManagementThread()) { + LOG(("WalkMemoryCacheRunnable::Run - collecting [this=%p]", this)); + // First, walk, count and grab all entries from the storage + + mozilla::MutexAutoLock lock(CacheStorageService::Self()->Lock()); + + if (!CacheStorageService::IsRunning()) return NS_ERROR_NOT_INITIALIZED; + + for (const auto& entries : sGlobalEntryTables->Values()) { + if (entries->Type() != CacheEntryTable::MEMORY_ONLY) { + continue; + } + + for (CacheEntry* entry : entries->Values()) { + MOZ_ASSERT(!entry->IsUsingDisk()); + + mSize += entry->GetMetadataMemoryConsumption(); + + int64_t size; + if (NS_SUCCEEDED(entry->GetDataSize(&size))) { + mSize += size; + } + mEntryArray.AppendElement(entry); + } + } + + // Next, we dispatch to the main thread + } else if (NS_IsMainThread()) { + LOG(("WalkMemoryCacheRunnable::Run - notifying [this=%p]", this)); + + if (LoadNotifyStorage()) { + LOG((" storage")); + + uint64_t capacity = CacheObserver::MemoryCacheCapacity(); + capacity <<= 10; // kilobytes to bytes + + // Second, notify overall storage info + mCallback->OnCacheStorageInfo(mEntryArray.Length(), mSize, capacity, + nullptr); + if (!LoadVisitEntries()) return NS_OK; // done + + StoreNotifyStorage(false); + + } else { + LOG((" entry [left=%zu, canceled=%d]", mEntryArray.Length(), + (bool)mCancel)); + + // Third, notify each entry until depleted or canceled + if (!mEntryArray.Length() || mCancel) { + mCallback->OnCacheEntryVisitCompleted(); + return NS_OK; // done + } + + // Grab the next entry + RefPtr entry = mEntryArray[0]; + mEntryArray.RemoveElementAt(0); + + // Invokes this->OnEntryInfo, that calls the callback with all + // information of the entry. + CacheStorageService::GetCacheEntryInfo(entry, this); + } + } else { + MOZ_CRASH("Bad thread"); + return NS_ERROR_FAILURE; + } + + NS_DispatchToMainThread(this); + return NS_OK; + } + + virtual ~WalkMemoryCacheRunnable() { + if (mCallback) { + ProxyReleaseMainThread("WalkMemoryCacheRunnable::mCallback", mCallback); + } + } + + virtual void OnEntryInfo(const nsACString& aURISpec, + const nsACString& aIdEnhance, int64_t aDataSize, + int64_t aAltDataSize, uint32_t aFetchCount, + uint32_t aLastModifiedTime, uint32_t aExpirationTime, + bool aPinned, nsILoadContextInfo* aInfo) override { + nsresult rv; + + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), aURISpec); + if (NS_FAILED(rv)) { + return; + } + + rv = mCallback->OnCacheEntryInfo(uri, aIdEnhance, aDataSize, aAltDataSize, + aFetchCount, aLastModifiedTime, + aExpirationTime, aPinned, aInfo); + if (NS_FAILED(rv)) { + LOG((" callback failed, canceling the walk")); + mCancel = true; + } + } + + private: + nsCString mContextKey; + nsTArray> mEntryArray; +}; + +// WalkDiskCacheRunnable +// Using the cache index information to get the list of files per context. +class WalkDiskCacheRunnable : public WalkCacheRunnable { + public: + WalkDiskCacheRunnable(nsILoadContextInfo* aLoadInfo, bool aVisitEntries, + nsICacheStorageVisitor* aVisitor) + : WalkCacheRunnable(aVisitor, aVisitEntries), + mLoadInfo(aLoadInfo), + mPass(COLLECT_STATS), + mCount(0) {} + + nsresult Walk() { + // TODO, bug 998693 + // Initial index build should be forced here so that about:cache soon + // after startup gives some meaningfull results. + + // Dispatch to the INDEX level in hope that very recent cache entries + // information gets to the index list before we grab the index iterator + // for the first time. This tries to avoid miss of entries that has + // been created right before the visit is required. + RefPtr thread = CacheFileIOManager::IOThread(); + NS_ENSURE_TRUE(thread, NS_ERROR_NOT_INITIALIZED); + + return thread->Dispatch(this, CacheIOThread::INDEX); + } + + private: + // Invokes OnCacheEntryInfo callback for each single found entry. + // There is one instance of this class per one entry. + class OnCacheEntryInfoRunnable : public Runnable { + public: + explicit OnCacheEntryInfoRunnable(WalkDiskCacheRunnable* aWalker) + : Runnable("net::WalkDiskCacheRunnable::OnCacheEntryInfoRunnable"), + mWalker(aWalker) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), mURISpec); + if (NS_FAILED(rv)) { + return NS_OK; + } + + rv = mWalker->mCallback->OnCacheEntryInfo( + uri, mIdEnhance, mDataSize, mAltDataSize, mFetchCount, + mLastModifiedTime, mExpirationTime, mPinned, mInfo); + if (NS_FAILED(rv)) { + mWalker->mCancel = true; + } + + return NS_OK; + } + + RefPtr mWalker; + + nsCString mURISpec; + nsCString mIdEnhance; + int64_t mDataSize{0}; + int64_t mAltDataSize{0}; + uint32_t mFetchCount{0}; + uint32_t mLastModifiedTime{0}; + uint32_t mExpirationTime{0}; + bool mPinned{false}; + nsCOMPtr mInfo; + }; + + NS_IMETHOD Run() override { + // The main loop + nsresult rv; + + if (CacheStorageService::IsOnManagementThread()) { + switch (mPass) { + case COLLECT_STATS: + // Get quickly the cache stats. + uint32_t size; + rv = CacheIndex::GetCacheStats(mLoadInfo, &size, &mCount); + if (NS_FAILED(rv)) { + if (LoadVisitEntries()) { + // both onStorageInfo and onCompleted are expected + NS_DispatchToMainThread(this); + } + return NS_DispatchToMainThread(this); + } + + mSize = static_cast(size) << 10; + + // Invoke onCacheStorageInfo with valid information. + NS_DispatchToMainThread(this); + + if (!LoadVisitEntries()) { + return NS_OK; // done + } + + mPass = ITERATE_METADATA; + [[fallthrough]]; + + case ITERATE_METADATA: + // Now grab the context iterator. + if (!mIter) { + rv = + CacheIndex::GetIterator(mLoadInfo, true, getter_AddRefs(mIter)); + if (NS_FAILED(rv)) { + // Invoke onCacheEntryVisitCompleted now + return NS_DispatchToMainThread(this); + } + } + + while (!mCancel && !CacheObserver::ShuttingDown()) { + if (CacheIOThread::YieldAndRerun()) return NS_OK; + + SHA1Sum::Hash hash; + rv = mIter->GetNextHash(&hash); + if (NS_FAILED(rv)) break; // done (or error?) + + // This synchronously invokes OnEntryInfo on this class where we + // redispatch to the main thread for the consumer callback. + CacheFileIOManager::GetEntryInfo(&hash, this); + } + + // Invoke onCacheEntryVisitCompleted on the main thread + NS_DispatchToMainThread(this); + } + } else if (NS_IsMainThread()) { + if (LoadNotifyStorage()) { + nsCOMPtr dir; + CacheFileIOManager::GetCacheDirectory(getter_AddRefs(dir)); + uint64_t capacity = CacheObserver::DiskCacheCapacity(); + capacity <<= 10; // kilobytes to bytes + mCallback->OnCacheStorageInfo(mCount, mSize, capacity, dir); + StoreNotifyStorage(false); + } else { + mCallback->OnCacheEntryVisitCompleted(); + } + } else { + MOZ_CRASH("Bad thread"); + return NS_ERROR_FAILURE; + } + + return NS_OK; + } + + virtual void OnEntryInfo(const nsACString& aURISpec, + const nsACString& aIdEnhance, int64_t aDataSize, + int64_t aAltDataSize, uint32_t aFetchCount, + uint32_t aLastModifiedTime, uint32_t aExpirationTime, + bool aPinned, nsILoadContextInfo* aInfo) override { + // Called directly from CacheFileIOManager::GetEntryInfo. + + // Invoke onCacheEntryInfo on the main thread for this entry. + RefPtr info = new OnCacheEntryInfoRunnable(this); + info->mURISpec = aURISpec; + info->mIdEnhance = aIdEnhance; + info->mDataSize = aDataSize; + info->mAltDataSize = aAltDataSize; + info->mFetchCount = aFetchCount; + info->mLastModifiedTime = aLastModifiedTime; + info->mExpirationTime = aExpirationTime; + info->mPinned = aPinned; + info->mInfo = aInfo; + + NS_DispatchToMainThread(info); + } + + RefPtr mLoadInfo; + enum { + // First, we collect stats for the load context. + COLLECT_STATS, + + // Second, if demanded, we iterate over the entries gethered + // from the iterator and call CacheFileIOManager::GetEntryInfo + // for each found entry. + ITERATE_METADATA, + } mPass; + + RefPtr mIter; + uint32_t mCount; +}; + +} // namespace + +void CacheStorageService::DropPrivateBrowsingEntries() { + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) return; + + nsTArray keys; + for (const nsACString& key : sGlobalEntryTables->Keys()) { + nsCOMPtr info = CacheFileUtils::ParseKey(key); + if (info && info->IsPrivate()) { + keys.AppendElement(key); + } + } + + for (uint32_t i = 0; i < keys.Length(); ++i) { + DoomStorageEntries(keys[i], nullptr, true, false, nullptr); + } +} + +// Helper methods + +// static +bool CacheStorageService::IsOnManagementThread() { + RefPtr service = Self(); + if (!service) return false; + + nsCOMPtr target = service->Thread(); + if (!target) return false; + + bool currentThread; + nsresult rv = target->IsOnCurrentThread(¤tThread); + return NS_SUCCEEDED(rv) && currentThread; +} + +already_AddRefed CacheStorageService::Thread() const { + return CacheFileIOManager::IOTarget(); +} + +nsresult CacheStorageService::Dispatch(nsIRunnable* aEvent) { + RefPtr cacheIOThread = CacheFileIOManager::IOThread(); + if (!cacheIOThread) return NS_ERROR_NOT_AVAILABLE; + + return cacheIOThread->Dispatch(aEvent, CacheIOThread::MANAGEMENT); +} + +namespace CacheStorageEvictHelper { + +nsresult ClearStorage(bool const aPrivate, bool const aAnonymous, + OriginAttributes& aOa) { + nsresult rv; + + aOa.SyncAttributesWithPrivateBrowsing(aPrivate); + RefPtr info = GetLoadContextInfo(aAnonymous, aOa); + + nsCOMPtr storage; + RefPtr service = CacheStorageService::Self(); + NS_ENSURE_TRUE(service, NS_ERROR_FAILURE); + + // Clear disk storage + rv = service->DiskCacheStorage(info, getter_AddRefs(storage)); + NS_ENSURE_SUCCESS(rv, rv); + rv = storage->AsyncEvictStorage(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + // Clear memory storage + rv = service->MemoryCacheStorage(info, getter_AddRefs(storage)); + NS_ENSURE_SUCCESS(rv, rv); + rv = storage->AsyncEvictStorage(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult Run(OriginAttributes& aOa) { + nsresult rv; + + // Clear all [private X anonymous] combinations + rv = ClearStorage(false, false, aOa); + NS_ENSURE_SUCCESS(rv, rv); + rv = ClearStorage(false, true, aOa); + NS_ENSURE_SUCCESS(rv, rv); + rv = ClearStorage(true, false, aOa); + NS_ENSURE_SUCCESS(rv, rv); + rv = ClearStorage(true, true, aOa); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace CacheStorageEvictHelper + +// nsICacheStorageService + +NS_IMETHODIMP CacheStorageService::MemoryCacheStorage( + nsILoadContextInfo* aLoadContextInfo, nsICacheStorage** _retval) { + NS_ENSURE_ARG(_retval); + + nsCOMPtr storage = + new CacheStorage(aLoadContextInfo, false, false, false); + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::DiskCacheStorage( + nsILoadContextInfo* aLoadContextInfo, nsICacheStorage** _retval) { + NS_ENSURE_ARG(_retval); + + // TODO save some heap granularity - cache commonly used storages. + + // When disk cache is disabled, still provide a storage, but just keep stuff + // in memory. + bool useDisk = CacheObserver::UseDiskCache(); + + nsCOMPtr storage = new CacheStorage( + aLoadContextInfo, useDisk, false /* size limit */, false /* don't pin */); + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::PinningCacheStorage( + nsILoadContextInfo* aLoadContextInfo, nsICacheStorage** _retval) { + NS_ENSURE_ARG(aLoadContextInfo); + NS_ENSURE_ARG(_retval); + + // When disk cache is disabled don't pretend we cache. + if (!CacheObserver::UseDiskCache()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr storage = + new CacheStorage(aLoadContextInfo, true /* use disk */, + true /* ignore size checks */, true /* pin */); + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::Clear() { + nsresult rv; + + // Tell the index to block notification to AsyncGetDiskConsumption. + // Will be allowed again from CacheFileContextEvictor::EvictEntries() + // when all the context have been removed from disk. + CacheIndex::OnAsyncEviction(true); + + mozilla::MutexAutoLock lock(mLock); + + { + mozilla::MutexAutoLock forcedValidEntriesLock(mForcedValidEntriesLock); + mForcedValidEntries.Clear(); + } + + NS_ENSURE_TRUE(!mShutdown, NS_ERROR_NOT_INITIALIZED); + + const auto keys = ToTArray>(sGlobalEntryTables->Keys()); + for (const auto& key : keys) { + DoomStorageEntries(key, nullptr, true, false, nullptr); + } + + // Passing null as a load info means to evict all contexts. + // EvictByContext() respects the entry pinning. EvictAll() does not. + rv = CacheFileIOManager::EvictByContext(nullptr, false, u""_ns); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::ClearOrigin(nsIPrincipal* aPrincipal) { + nsresult rv; + + if (NS_WARN_IF(!aPrincipal)) { + return NS_ERROR_FAILURE; + } + + nsAutoString origin; + rv = nsContentUtils::GetUTFOrigin(aPrincipal, origin); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ClearOriginInternal(origin, aPrincipal->OriginAttributesRef(), true); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ClearOriginInternal(origin, aPrincipal->OriginAttributesRef(), false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::ClearOriginAttributes( + const nsAString& aOriginAttributes) { + nsresult rv; + + if (NS_WARN_IF(aOriginAttributes.IsEmpty())) { + return NS_ERROR_FAILURE; + } + + OriginAttributes oa; + if (!oa.Init(aOriginAttributes)) { + NS_ERROR("Could not parse the argument for OriginAttributes"); + return NS_ERROR_FAILURE; + } + + rv = CacheStorageEvictHelper::Run(oa); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +static bool RemoveExactEntry(CacheEntryTable* aEntries, nsACString const& aKey, + CacheEntry* aEntry, bool aOverwrite) { + RefPtr existingEntry; + if (!aEntries->Get(aKey, getter_AddRefs(existingEntry))) { + LOG(("RemoveExactEntry [entry=%p already gone]", aEntry)); + return false; // Already removed... + } + + if (!aOverwrite && existingEntry != aEntry) { + LOG(("RemoveExactEntry [entry=%p already replaced]", aEntry)); + return false; // Already replaced... + } + + LOG(("RemoveExactEntry [entry=%p removed]", aEntry)); + aEntries->Remove(aKey); + return true; +} + +NS_IMETHODIMP CacheStorageService::ClearBaseDomain( + const nsAString& aBaseDomain) { + if (sGlobalEntryTables) { + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) return NS_ERROR_NOT_AVAILABLE; + + nsCString cBaseDomain = NS_ConvertUTF16toUTF8(aBaseDomain); + + nsTArray keys; + for (const auto& globalEntry : *sGlobalEntryTables) { + // Match by partitionKey base domain. This should cover most cache entries + // because we statically partition the cache. Most first party cache + // entries will also have a partitionKey set where the partitionKey base + // domain will match the entry URI base domain. + const nsACString& key = globalEntry.GetKey(); + nsCOMPtr info = + CacheFileUtils::ParseKey(globalEntry.GetKey()); + + if (info && + StoragePrincipalHelper::PartitionKeyHasBaseDomain( + info->OriginAttributesPtr()->mPartitionKey, aBaseDomain)) { + keys.AppendElement(key); + continue; + } + + // If we didn't get a partitionKey match, try to match by entry URI. This + // requires us to iterate over all entries. + CacheEntryTable* table = globalEntry.GetWeak(); + MOZ_ASSERT(table); + + nsTArray> entriesToDelete; + + for (CacheEntry* entry : table->Values()) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), entry->GetURI()); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + + nsAutoCString host; + rv = uri->GetHost(host); + // Some entries may not have valid hosts. We can skip them. + if (NS_FAILED(rv) || host.IsEmpty()) { + continue; + } + + bool hasRootDomain = false; + rv = HasRootDomain(host, cBaseDomain, &hasRootDomain); + if (NS_WARN_IF(NS_FAILED(rv))) { + continue; + } + if (hasRootDomain) { + entriesToDelete.AppendElement(entry); + } + } + + // Clear individual matched entries. + for (RefPtr& entry : entriesToDelete) { + nsAutoCString entryKey; + nsresult rv = entry->HashingKey(entryKey); + if (NS_FAILED(rv)) { + NS_ERROR("aEntry->HashingKey() failed?"); + return rv; + } + + RemoveExactEntry(table, entryKey, entry, false /* don't overwrite */); + } + } + + // Clear matched keys. + for (uint32_t i = 0; i < keys.Length(); ++i) { + DoomStorageEntries(keys[i], nullptr, true, false, nullptr); + } + } + + return CacheFileIOManager::EvictByContext(nullptr, false /* pinned */, u""_ns, + aBaseDomain); +} + +nsresult CacheStorageService::ClearOriginInternal( + const nsAString& aOrigin, const OriginAttributes& aOriginAttributes, + bool aAnonymous) { + nsresult rv; + + RefPtr info = + GetLoadContextInfo(aAnonymous, aOriginAttributes); + if (NS_WARN_IF(!info)) { + return NS_ERROR_FAILURE; + } + + mozilla::MutexAutoLock lock(mLock); + + if (sGlobalEntryTables) { + for (const auto& globalEntry : *sGlobalEntryTables) { + bool matches = false; + rv = CacheFileUtils::KeyMatchesLoadContextInfo(globalEntry.GetKey(), info, + &matches); + NS_ENSURE_SUCCESS(rv, rv); + if (!matches) { + continue; + } + + CacheEntryTable* table = globalEntry.GetWeak(); + MOZ_ASSERT(table); + + nsTArray> entriesToDelete; + + for (CacheEntry* entry : table->Values()) { + nsCOMPtr uri; + rv = NS_NewURI(getter_AddRefs(uri), entry->GetURI()); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString origin; + rv = nsContentUtils::GetUTFOrigin(uri, origin); + NS_ENSURE_SUCCESS(rv, rv); + + if (origin != aOrigin) { + continue; + } + + entriesToDelete.AppendElement(entry); + } + + for (RefPtr& entry : entriesToDelete) { + nsAutoCString entryKey; + rv = entry->HashingKey(entryKey); + if (NS_FAILED(rv)) { + NS_ERROR("aEntry->HashingKey() failed?"); + return rv; + } + + RemoveExactEntry(table, entryKey, entry, false /* don't overwrite */); + } + } + } + + rv = CacheFileIOManager::EvictByContext(info, false /* pinned */, aOrigin); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::PurgeFromMemory(uint32_t aWhat) { + uint32_t what; + + switch (aWhat) { + case PURGE_DISK_DATA_ONLY: + what = CacheEntry::PURGE_DATA_ONLY_DISK_BACKED; + break; + + case PURGE_DISK_ALL: + what = CacheEntry::PURGE_WHOLE_ONLY_DISK_BACKED; + break; + + case PURGE_EVERYTHING: + what = CacheEntry::PURGE_WHOLE; + break; + + default: + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr event = new PurgeFromMemoryRunnable(this, what); + + return Dispatch(event); +} + +NS_IMETHODIMP CacheStorageService::PurgeFromMemoryRunnable::Run() { + if (NS_IsMainThread()) { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->NotifyObservers( + nullptr, "cacheservice:purge-memory-pools", nullptr); + } + + return NS_OK; + } + + if (mService) { + // TODO not all flags apply to both pools + mService->Pool(true).PurgeAll(mWhat); + mService->Pool(false).PurgeAll(mWhat); + mService = nullptr; + } + + NS_DispatchToMainThread(this); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::AsyncGetDiskConsumption( + nsICacheStorageConsumptionObserver* aObserver) { + NS_ENSURE_ARG(aObserver); + + nsresult rv; + + rv = CacheIndex::AsyncGetDiskConsumption(aObserver); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::GetIoTarget(nsIEventTarget** aEventTarget) { + NS_ENSURE_ARG(aEventTarget); + + nsCOMPtr ioTarget = CacheFileIOManager::IOTarget(); + ioTarget.forget(aEventTarget); + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::AsyncVisitAllStorages( + nsICacheStorageVisitor* aVisitor, bool aVisitEntries) { + LOG(("CacheStorageService::AsyncVisitAllStorages [cb=%p]", aVisitor)); + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + // Walking the disk cache also walks the memory cache. + RefPtr event = + new WalkDiskCacheRunnable(nullptr, aVisitEntries, aVisitor); + return event->Walk(); +} + +// Methods used by CacheEntry for management of in-memory structures. + +namespace { + +class FrecencyComparator { + public: + bool Equals(CacheEntry* a, CacheEntry* b) const { + return a->GetFrecency() == b->GetFrecency(); + } + bool LessThan(CacheEntry* a, CacheEntry* b) const { + // We deliberately want to keep the '0' frecency entries at the tail of the + // aray, because these are new entries and would just slow down purging of + // the pools based on frecency. + if (a->GetFrecency() == 0.0 && b->GetFrecency() > 0.0) { + return false; + } + if (a->GetFrecency() > 0.0 && b->GetFrecency() == 0.0) { + return true; + } + + return a->GetFrecency() < b->GetFrecency(); + } +}; + +class ExpirationComparator { + public: + bool Equals(CacheEntry* a, CacheEntry* b) const { + return a->GetExpirationTime() == b->GetExpirationTime(); + } + bool LessThan(CacheEntry* a, CacheEntry* b) const { + return a->GetExpirationTime() < b->GetExpirationTime(); + } +}; + +} // namespace + +void CacheStorageService::RegisterEntry(CacheEntry* aEntry) { + MOZ_ASSERT(IsOnManagementThread()); + + if (mShutdown || !aEntry->CanRegister()) return; + + TelemetryRecordEntryCreation(aEntry); + + LOG(("CacheStorageService::RegisterEntry [entry=%p]", aEntry)); + + MemoryPool& pool = Pool(aEntry->IsUsingDisk()); + pool.mFrecencyArray.AppendElement(aEntry); + pool.mExpirationArray.AppendElement(aEntry); + + aEntry->SetRegistered(true); +} + +void CacheStorageService::UnregisterEntry(CacheEntry* aEntry) { + MOZ_ASSERT(IsOnManagementThread()); + + if (!aEntry->IsRegistered()) return; + + TelemetryRecordEntryRemoval(aEntry); + + LOG(("CacheStorageService::UnregisterEntry [entry=%p]", aEntry)); + + MemoryPool& pool = Pool(aEntry->IsUsingDisk()); + mozilla::DebugOnly removedFrecency = + pool.mFrecencyArray.RemoveElement(aEntry); + mozilla::DebugOnly removedExpiration = + pool.mExpirationArray.RemoveElement(aEntry); + + MOZ_ASSERT(mShutdown || (removedFrecency && removedExpiration)); + + // Note: aEntry->CanRegister() since now returns false + aEntry->SetRegistered(false); +} + +static bool AddExactEntry(CacheEntryTable* aEntries, nsACString const& aKey, + CacheEntry* aEntry, bool aOverwrite) { + RefPtr existingEntry; + if (!aOverwrite && aEntries->Get(aKey, getter_AddRefs(existingEntry))) { + bool equals = existingEntry == aEntry; + LOG(("AddExactEntry [entry=%p equals=%d]", aEntry, equals)); + return equals; // Already there... + } + + LOG(("AddExactEntry [entry=%p put]", aEntry)); + aEntries->InsertOrUpdate(aKey, RefPtr{aEntry}); + return true; +} + +bool CacheStorageService::RemoveEntry(CacheEntry* aEntry, + bool aOnlyUnreferenced) { + LOG(("CacheStorageService::RemoveEntry [entry=%p]", aEntry)); + + nsAutoCString entryKey; + nsresult rv = aEntry->HashingKey(entryKey); + if (NS_FAILED(rv)) { + NS_ERROR("aEntry->HashingKey() failed?"); + return false; + } + + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) { + LOG((" after shutdown")); + return false; + } + + if (aOnlyUnreferenced) { + if (aEntry->IsReferenced()) { + LOG((" still referenced, not removing")); + return false; + } + + if (!aEntry->IsUsingDisk() && + IsForcedValidEntry(aEntry->GetStorageID(), entryKey)) { + LOG((" forced valid, not removing")); + return false; + } + } + + CacheEntryTable* entries; + if (sGlobalEntryTables->Get(aEntry->GetStorageID(), &entries)) { + RemoveExactEntry(entries, entryKey, aEntry, false /* don't overwrite */); + } + + nsAutoCString memoryStorageID(aEntry->GetStorageID()); + AppendMemoryStorageTag(memoryStorageID); + + if (sGlobalEntryTables->Get(memoryStorageID, &entries)) { + RemoveExactEntry(entries, entryKey, aEntry, false /* don't overwrite */); + } + + return true; +} + +void CacheStorageService::RecordMemoryOnlyEntry(CacheEntry* aEntry, + bool aOnlyInMemory, + bool aOverwrite) { + LOG( + ("CacheStorageService::RecordMemoryOnlyEntry [entry=%p, memory=%d, " + "overwrite=%d]", + aEntry, aOnlyInMemory, aOverwrite)); + // This method is responsible to put this entry to a special record hashtable + // that contains only entries that are stored in memory. + // Keep in mind that every entry, regardless of whether is in-memory-only or + // not is always recorded in the storage master hash table, the one identified + // by CacheEntry.StorageID(). + + mLock.AssertCurrentThreadOwns(); + + if (mShutdown) { + LOG((" after shutdown")); + return; + } + + nsresult rv; + + nsAutoCString entryKey; + rv = aEntry->HashingKey(entryKey); + if (NS_FAILED(rv)) { + NS_ERROR("aEntry->HashingKey() failed?"); + return; + } + + CacheEntryTable* entries = nullptr; + nsAutoCString memoryStorageID(aEntry->GetStorageID()); + AppendMemoryStorageTag(memoryStorageID); + + if (!sGlobalEntryTables->Get(memoryStorageID, &entries)) { + if (!aOnlyInMemory) { + LOG((" not recorded as memory only")); + return; + } + + entries = sGlobalEntryTables + ->InsertOrUpdate( + memoryStorageID, + MakeUnique(CacheEntryTable::MEMORY_ONLY)) + .get(); + LOG((" new memory-only storage table for %s", memoryStorageID.get())); + } + + if (aOnlyInMemory) { + AddExactEntry(entries, entryKey, aEntry, aOverwrite); + } else { + RemoveExactEntry(entries, entryKey, aEntry, aOverwrite); + } +} + +// Checks if a cache entry is forced valid (will be loaded directly from cache +// without further validation) - see nsICacheEntry.idl for further details +bool CacheStorageService::IsForcedValidEntry(nsACString const& aContextKey, + nsACString const& aEntryKey) { + return IsForcedValidEntry(aContextKey + aEntryKey); +} + +bool CacheStorageService::IsForcedValidEntry( + nsACString const& aContextEntryKey) { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + ForcedValidData data; + + if (!mForcedValidEntries.Get(aContextEntryKey, &data)) { + return false; + } + + if (data.validUntil.IsNull()) { + MOZ_ASSERT_UNREACHABLE("the timeStamp should never be null"); + return false; + } + + // Entry timeout not reached yet + if (TimeStamp::NowLoRes() <= data.validUntil) { + return true; + } + + // Entry timeout has been reached + mForcedValidEntries.Remove(aContextEntryKey); + + if (!data.viewed) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::WaitedTooLong); + } + return false; +} + +void CacheStorageService::MarkForcedValidEntryUse(nsACString const& aContextKey, + nsACString const& aEntryKey) { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + ForcedValidData data; + + if (!mForcedValidEntries.Get(aContextKey + aEntryKey, &data)) { + return; + } + + data.viewed = true; + mForcedValidEntries.InsertOrUpdate(aContextKey + aEntryKey, data); +} + +// Allows a cache entry to be loaded directly from cache without further +// validation - see nsICacheEntry.idl for further details +void CacheStorageService::ForceEntryValidFor(nsACString const& aContextKey, + nsACString const& aEntryKey, + uint32_t aSecondsToTheFuture) { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + TimeStamp now = TimeStamp::NowLoRes(); + ForcedValidEntriesPrune(now); + + ForcedValidData data; + data.validUntil = now + TimeDuration::FromSeconds(aSecondsToTheFuture); + data.viewed = false; + + mForcedValidEntries.InsertOrUpdate(aContextKey + aEntryKey, data); +} + +void CacheStorageService::RemoveEntryForceValid(nsACString const& aContextKey, + nsACString const& aEntryKey) { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + LOG(("CacheStorageService::RemoveEntryForceValid context='%s' entryKey=%s", + aContextKey.BeginReading(), aEntryKey.BeginReading())); + ForcedValidData data; + bool ok = mForcedValidEntries.Get(aContextKey + aEntryKey, &data); + if (ok && !data.viewed) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::WaitedTooLong); + } + mForcedValidEntries.Remove(aContextKey + aEntryKey); +} + +// Cleans out the old entries in mForcedValidEntries +void CacheStorageService::ForcedValidEntriesPrune(TimeStamp& now) { + static TimeDuration const oneMinute = TimeDuration::FromSeconds(60); + static TimeStamp dontPruneUntil = now + oneMinute; + if (now < dontPruneUntil) return; + + for (auto iter = mForcedValidEntries.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data().validUntil < now) { + if (!iter.Data().viewed) { + Telemetry::AccumulateCategorical( + Telemetry::LABELS_PREDICTOR_PREFETCH_USE_STATUS::WaitedTooLong); + } + iter.Remove(); + } + } + dontPruneUntil = now + oneMinute; +} + +void CacheStorageService::OnMemoryConsumptionChange( + CacheMemoryConsumer* aConsumer, uint32_t aCurrentMemoryConsumption) { + LOG(("CacheStorageService::OnMemoryConsumptionChange [consumer=%p, size=%u]", + aConsumer, aCurrentMemoryConsumption)); + + uint32_t savedMemorySize = aConsumer->LoadReportedMemoryConsumption(); + if (savedMemorySize == aCurrentMemoryConsumption) return; + + // Exchange saved size with current one. + aConsumer->StoreReportedMemoryConsumption(aCurrentMemoryConsumption); + + bool usingDisk = !(aConsumer->LoadFlags() & CacheMemoryConsumer::MEMORY_ONLY); + bool overLimit = Pool(usingDisk).OnMemoryConsumptionChange( + savedMemorySize, aCurrentMemoryConsumption); + + if (!overLimit) return; + + // It's likely the timer has already been set when we get here, + // check outside the lock to save resources. +#ifdef MOZ_TSAN + if (mPurgeTimerActive) { +#else + if (mPurgeTimer) { +#endif + return; + } + + // We don't know if this is called under the service lock or not, + // hence rather dispatch. + RefPtr cacheIOTarget = Thread(); + if (!cacheIOTarget) return; + + // Dispatch as a priority task, we want to set the purge timer + // ASAP to prevent vain redispatch of this event. + nsCOMPtr event = NewRunnableMethod( + "net::CacheStorageService::SchedulePurgeOverMemoryLimit", this, + &CacheStorageService::SchedulePurgeOverMemoryLimit); + cacheIOTarget->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +bool CacheStorageService::MemoryPool::OnMemoryConsumptionChange( + uint32_t aSavedMemorySize, uint32_t aCurrentMemoryConsumption) { + mMemorySize -= aSavedMemorySize; + mMemorySize += aCurrentMemoryConsumption; + + LOG((" mMemorySize=%u (+%u,-%u)", uint32_t(mMemorySize), + aCurrentMemoryConsumption, aSavedMemorySize)); + + // Bypass purging when memory has not grew up significantly + if (aCurrentMemoryConsumption <= aSavedMemorySize) return false; + + return mMemorySize > Limit(); +} + +void CacheStorageService::SchedulePurgeOverMemoryLimit() { + LOG(("CacheStorageService::SchedulePurgeOverMemoryLimit")); + + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) { + LOG((" past shutdown")); + return; + } + + if (mPurgeTimer) { + LOG((" timer already up")); + return; + } + + mPurgeTimer = NS_NewTimer(); + if (mPurgeTimer) { +#ifdef MOZ_TSAN + mPurgeTimerActive = true; +#endif + nsresult rv; + rv = mPurgeTimer->InitWithCallback(this, 1000, nsITimer::TYPE_ONE_SHOT); + LOG((" timer init rv=0x%08" PRIx32, static_cast(rv))); + } +} + +NS_IMETHODIMP +CacheStorageService::Notify(nsITimer* aTimer) { + LOG(("CacheStorageService::Notify")); + + mozilla::MutexAutoLock lock(mLock); + + if (aTimer == mPurgeTimer) { +#ifdef MOZ_TSAN + mPurgeTimerActive = false; +#endif + mPurgeTimer = nullptr; + + nsCOMPtr event = + NewRunnableMethod("net::CacheStorageService::PurgeOverMemoryLimit", + this, &CacheStorageService::PurgeOverMemoryLimit); + Dispatch(event); + } + + return NS_OK; +} + +NS_IMETHODIMP +CacheStorageService::GetName(nsACString& aName) { + aName.AssignLiteral("CacheStorageService"); + return NS_OK; +} + +void CacheStorageService::PurgeOverMemoryLimit() { + MOZ_ASSERT(IsOnManagementThread()); + + LOG(("CacheStorageService::PurgeOverMemoryLimit")); + + static TimeDuration const kFourSeconds = TimeDuration::FromSeconds(4); + TimeStamp now = TimeStamp::NowLoRes(); + + if (!mLastPurgeTime.IsNull() && now - mLastPurgeTime < kFourSeconds) { + LOG((" bypassed, too soon")); + return; + } + + mLastPurgeTime = now; + + Pool(true).PurgeOverMemoryLimit(); + Pool(false).PurgeOverMemoryLimit(); +} + +void CacheStorageService::MemoryPool::PurgeOverMemoryLimit() { + TimeStamp start(TimeStamp::Now()); + + uint32_t const memoryLimit = Limit(); + if (mMemorySize > memoryLimit) { + LOG((" memory data consumption over the limit, abandon expired entries")); + PurgeExpired(); + } + + // No longer makes sense since: + // Memory entries are never purged partially, only as a whole when the memory + // cache limit is overreached. + // Disk entries throw the data away ASAP so that only metadata are kept. + // TODO when this concept of two separate pools is found working, the code + // should clean up. +#if 0 + if (mMemorySize > memoryLimit) { + LOG((" memory data consumption over the limit, abandon disk backed data")); + PurgeByFrecency(CacheEntry::PURGE_DATA_ONLY_DISK_BACKED); + } + + if (mMemorySize > memoryLimit) { + LOG((" metadata consumtion over the limit, abandon disk backed entries")); + PurgeByFrecency(CacheEntry::PURGE_WHOLE_ONLY_DISK_BACKED); + } +#endif + + if (mMemorySize > memoryLimit) { + LOG((" memory data consumption over the limit, abandon any entry")); + PurgeByFrecency(CacheEntry::PURGE_WHOLE); + } + + LOG((" purging took %1.2fms", (TimeStamp::Now() - start).ToMilliseconds())); +} + +void CacheStorageService::MemoryPool::PurgeExpired() { + MOZ_ASSERT(IsOnManagementThread()); + + mExpirationArray.Sort(ExpirationComparator()); + uint32_t now = NowInSeconds(); + + uint32_t const memoryLimit = Limit(); + + for (uint32_t i = 0; + mMemorySize > memoryLimit && i < mExpirationArray.Length();) { + if (CacheIOThread::YieldAndRerun()) return; + + RefPtr entry = mExpirationArray[i]; + + uint32_t expirationTime = entry->GetExpirationTime(); + if (expirationTime > 0 && expirationTime <= now && + entry->Purge(CacheEntry::PURGE_WHOLE)) { + LOG((" purged expired, entry=%p, exptime=%u (now=%u)", entry.get(), + entry->GetExpirationTime(), now)); + continue; + } + + // not purged, move to the next one + ++i; + } +} + +void CacheStorageService::MemoryPool::PurgeByFrecency(uint32_t aWhat) { + MOZ_ASSERT(IsOnManagementThread()); + + // Pretend the limit is 10% lower so that we get rid of more entries at one + // shot and save the sorting below. + uint32_t const memoryLimit = Limit() * 0.9; + + // Let's do our best and try to shorten the array to at least this size so + // that it doesn't overgrow. We will ignore higher priority events and keep + // looping to try to purge while the array is larget than this size. + static size_t const kFrecencyArrayLengthLimit = 2000; + + LOG(("MemoryPool::PurgeByFrecency, len=%zu", mFrecencyArray.Length())); + + mFrecencyArray.Sort(FrecencyComparator()); + + for (uint32_t i = 0; + mMemorySize > memoryLimit && i < mFrecencyArray.Length();) { + if (mFrecencyArray.Length() <= kFrecencyArrayLengthLimit && + CacheIOThread::YieldAndRerun()) { + LOG(("MemoryPool::PurgeByFrecency interrupted")); + return; + } + + RefPtr entry = mFrecencyArray[i]; + if (entry->Purge(aWhat)) { + LOG((" abandoned (%d), entry=%p, frecency=%1.10f", aWhat, entry.get(), + entry->GetFrecency())); + continue; + } + + // not purged, move to the next one + ++i; + } + + LOG(("MemoryPool::PurgeByFrecency done")); +} + +void CacheStorageService::MemoryPool::PurgeAll(uint32_t aWhat) { + LOG(("CacheStorageService::MemoryPool::PurgeAll aWhat=%d", aWhat)); + MOZ_ASSERT(IsOnManagementThread()); + + for (uint32_t i = 0; i < mFrecencyArray.Length();) { + if (CacheIOThread::YieldAndRerun()) return; + + RefPtr entry = mFrecencyArray[i]; + + if (entry->Purge(aWhat)) { + LOG((" abandoned entry=%p", entry.get())); + continue; + } + + // not purged, move to the next one + ++i; + } +} + +// Methods exposed to and used by CacheStorage. + +nsresult CacheStorageService::AddStorageEntry(CacheStorage const* aStorage, + const nsACString& aURI, + const nsACString& aIdExtension, + uint32_t aFlags, + CacheEntryHandle** aResult) { + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_ARG(aStorage); + + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + return AddStorageEntry(contextKey, aURI, aIdExtension, + aStorage->WriteToDisk(), aStorage->SkipSizeCheck(), + aStorage->Pinning(), aFlags, aResult); +} + +nsresult CacheStorageService::AddStorageEntry( + const nsACString& aContextKey, const nsACString& aURI, + const nsACString& aIdExtension, bool aWriteToDisk, bool aSkipSizeCheck, + bool aPin, uint32_t aFlags, CacheEntryHandle** aResult) { + nsresult rv; + + nsAutoCString entryKey; + rv = CacheEntry::HashingKey(""_ns, aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("CacheStorageService::AddStorageEntry [entryKey=%s, contextKey=%s]", + entryKey.get(), aContextKey.BeginReading())); + + RefPtr entry; + RefPtr handle; + + { + mozilla::MutexAutoLock lock(mLock); + + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + // Ensure storage table + CacheEntryTable* const entries = + sGlobalEntryTables + ->LookupOrInsertWith( + aContextKey, + [&aContextKey] { + LOG((" new storage entries table for context '%s'", + aContextKey.BeginReading())); + return MakeUnique( + CacheEntryTable::ALL_ENTRIES); + }) + .get(); + + bool entryExists = entries->Get(entryKey, getter_AddRefs(entry)); + if (!entryExists && (aFlags & nsICacheStorage::OPEN_READONLY) && + (aFlags & nsICacheStorage::OPEN_SECRETLY) && + StaticPrefs::network_cache_bug1708673()) { + return NS_ERROR_CACHE_KEY_NOT_FOUND; + } + + bool replace = aFlags & nsICacheStorage::OPEN_TRUNCATE; + + if (entryExists && !replace) { + // check whether we want to turn this entry to a memory-only. + if (MOZ_UNLIKELY(!aWriteToDisk) && MOZ_LIKELY(entry->IsUsingDisk())) { + LOG((" entry is persistent but we want mem-only, replacing it")); + replace = true; + } + } + + // If truncate is demanded, delete and doom the current entry + if (entryExists && replace) { + entries->Remove(entryKey); + + LOG((" dooming entry %p for %s because of OPEN_TRUNCATE", entry.get(), + entryKey.get())); + // On purpose called under the lock to prevent races of doom and open on + // I/O thread No need to remove from both memory-only and all-entries + // tables. The new entry will overwrite the shadow entry in its ctor. + entry->DoomAlreadyRemoved(); + + entry = nullptr; + entryExists = false; + + // Would only lead to deleting force-valid timestamp again. We don't need + // the replace information anymore after this point anyway. + replace = false; + } + + // Ensure entry for the particular URL + if (!entryExists) { + // When replacing with a new entry, always remove the current force-valid + // timestamp, this is the only place to do it. + if (replace) { + RemoveEntryForceValid(aContextKey, entryKey); + } + + // Entry is not in the hashtable or has just been truncated... + entry = new CacheEntry(aContextKey, aURI, aIdExtension, aWriteToDisk, + aSkipSizeCheck, aPin); + entries->InsertOrUpdate(entryKey, RefPtr{entry}); + LOG((" new entry %p for %s", entry.get(), entryKey.get())); + } + + if (entry) { + // Here, if this entry was not for a long time referenced by any consumer, + // gets again first 'handles count' reference. + handle = entry->NewHandle(); + } + } + + handle.forget(aResult); + return NS_OK; +} + +nsresult CacheStorageService::CheckStorageEntry(CacheStorage const* aStorage, + const nsACString& aURI, + const nsACString& aIdExtension, + bool* aResult) { + nsresult rv; + + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + if (!aStorage->WriteToDisk()) { + AppendMemoryStorageTag(contextKey); + } + + LOG(("CacheStorageService::CheckStorageEntry [uri=%s, eid=%s, contextKey=%s]", + aURI.BeginReading(), aIdExtension.BeginReading(), contextKey.get())); + + { + mozilla::MutexAutoLock lock(mLock); + + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + nsAutoCString entryKey; + rv = CacheEntry::HashingKey(""_ns, aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + CacheEntryTable* entries; + if ((*aResult = sGlobalEntryTables->Get(contextKey, &entries)) && + entries->GetWeak(entryKey, aResult)) { + LOG((" found in hash tables")); + return NS_OK; + } + } + + if (!aStorage->WriteToDisk()) { + // Memory entry, nothing more to do. + LOG((" not found in hash tables")); + return NS_OK; + } + + // Disk entry, not found in the hashtable, check the index. + nsAutoCString fileKey; + rv = CacheEntry::HashingKey(contextKey, aIdExtension, aURI, fileKey); + + CacheIndex::EntryStatus status; + rv = CacheIndex::HasEntry(fileKey, &status); + if (NS_FAILED(rv) || status == CacheIndex::DO_NOT_KNOW) { + LOG((" index doesn't know, rv=0x%08" PRIx32, static_cast(rv))); + return NS_ERROR_NOT_AVAILABLE; + } + + *aResult = status == CacheIndex::EXISTS; + LOG((" %sfound in index", *aResult ? "" : "not ")); + return NS_OK; +} + +nsresult CacheStorageService::GetCacheIndexEntryAttrs( + CacheStorage const* aStorage, const nsACString& aURI, + const nsACString& aIdExtension, bool* aHasAltData, uint32_t* aFileSizeKb) { + nsresult rv; + + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + LOG( + ("CacheStorageService::GetCacheIndexEntryAttrs [uri=%s, eid=%s, " + "contextKey=%s]", + aURI.BeginReading(), aIdExtension.BeginReading(), contextKey.get())); + + nsAutoCString fileKey; + rv = CacheEntry::HashingKey(contextKey, aIdExtension, aURI, fileKey); + if (NS_FAILED(rv)) { + return rv; + } + + *aHasAltData = false; + *aFileSizeKb = 0; + auto closure = [&aHasAltData, &aFileSizeKb](const CacheIndexEntry* entry) { + *aHasAltData = entry->GetHasAltData(); + *aFileSizeKb = entry->GetFileSize(); + }; + + CacheIndex::EntryStatus status; + rv = CacheIndex::HasEntry(fileKey, &status, closure); + if (NS_FAILED(rv)) { + return rv; + } + + if (status != CacheIndex::EXISTS) { + return NS_ERROR_CACHE_KEY_NOT_FOUND; + } + + return NS_OK; +} + +namespace { + +class CacheEntryDoomByKeyCallback : public CacheFileIOListener, + public nsIRunnable { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + explicit CacheEntryDoomByKeyCallback(nsICacheEntryDoomCallback* aCallback) + : mCallback(aCallback), mResult(NS_ERROR_NOT_INITIALIZED) {} + + private: + virtual ~CacheEntryDoomByKeyCallback(); + + NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override { + return NS_OK; + } + NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, + nsresult aResult) override { + return NS_OK; + } + NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, + nsresult aResult) override { + return NS_OK; + } + NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; + NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override { + return NS_OK; + } + NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, + nsresult aResult) override { + return NS_OK; + } + + nsCOMPtr mCallback; + nsresult mResult; +}; + +CacheEntryDoomByKeyCallback::~CacheEntryDoomByKeyCallback() { + if (mCallback) { + ProxyReleaseMainThread("CacheEntryDoomByKeyCallback::mCallback", mCallback); + } +} + +NS_IMETHODIMP CacheEntryDoomByKeyCallback::OnFileDoomed( + CacheFileHandle* aHandle, nsresult aResult) { + if (!mCallback) return NS_OK; + + mResult = aResult; + if (NS_IsMainThread()) { + Run(); + } else { + NS_DispatchToMainThread(this); + } + + return NS_OK; +} + +NS_IMETHODIMP CacheEntryDoomByKeyCallback::Run() { + mCallback->OnCacheEntryDoomed(mResult); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(CacheEntryDoomByKeyCallback, CacheFileIOListener, + nsIRunnable); + +} // namespace + +nsresult CacheStorageService::DoomStorageEntry( + CacheStorage const* aStorage, const nsACString& aURI, + const nsACString& aIdExtension, nsICacheEntryDoomCallback* aCallback) { + LOG(("CacheStorageService::DoomStorageEntry")); + + NS_ENSURE_ARG(aStorage); + + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + nsAutoCString entryKey; + nsresult rv = CacheEntry::HashingKey(""_ns, aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr entry; + { + mozilla::MutexAutoLock lock(mLock); + + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + CacheEntryTable* entries; + if (sGlobalEntryTables->Get(contextKey, &entries)) { + if (entries->Get(entryKey, getter_AddRefs(entry))) { + if (aStorage->WriteToDisk() || !entry->IsUsingDisk()) { + // When evicting from disk storage, purge + // When evicting from memory storage and the entry is memory-only, + // purge + LOG( + (" purging entry %p for %s [storage use disk=%d, entry use " + "disk=%d]", + entry.get(), entryKey.get(), aStorage->WriteToDisk(), + entry->IsUsingDisk())); + entries->Remove(entryKey); + } else { + // Otherwise, leave it + LOG( + (" leaving entry %p for %s [storage use disk=%d, entry use " + "disk=%d]", + entry.get(), entryKey.get(), aStorage->WriteToDisk(), + entry->IsUsingDisk())); + entry = nullptr; + } + } + } + + if (!entry) { + RemoveEntryForceValid(contextKey, entryKey); + } + } + + if (entry) { + LOG((" dooming entry %p for %s", entry.get(), entryKey.get())); + return entry->AsyncDoom(aCallback); + } + + LOG((" no entry loaded for %s", entryKey.get())); + + if (aStorage->WriteToDisk()) { + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + rv = CacheEntry::HashingKey(contextKey, aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + LOG((" dooming file only for %s", entryKey.get())); + + RefPtr callback( + new CacheEntryDoomByKeyCallback(aCallback)); + rv = CacheFileIOManager::DoomFileByKey(entryKey, callback); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + class Callback : public Runnable { + public: + explicit Callback(nsICacheEntryDoomCallback* aCallback) + : mozilla::Runnable("Callback"), mCallback(aCallback) {} + NS_IMETHOD Run() override { + mCallback->OnCacheEntryDoomed(NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + nsCOMPtr mCallback; + }; + + if (aCallback) { + RefPtr callback = new Callback(aCallback); + return NS_DispatchToMainThread(callback); + } + + return NS_OK; +} + +nsresult CacheStorageService::DoomStorageEntries( + CacheStorage const* aStorage, nsICacheEntryDoomCallback* aCallback) { + LOG(("CacheStorageService::DoomStorageEntries")); + + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + NS_ENSURE_ARG(aStorage); + + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aStorage->LoadInfo(), contextKey); + + mozilla::MutexAutoLock lock(mLock); + + return DoomStorageEntries(contextKey, aStorage->LoadInfo(), + aStorage->WriteToDisk(), aStorage->Pinning(), + aCallback); +} + +nsresult CacheStorageService::DoomStorageEntries( + const nsACString& aContextKey, nsILoadContextInfo* aContext, + bool aDiskStorage, bool aPinned, nsICacheEntryDoomCallback* aCallback) { + LOG(("CacheStorageService::DoomStorageEntries [context=%s]", + aContextKey.BeginReading())); + + mLock.AssertCurrentThreadOwns(); + + NS_ENSURE_TRUE(!mShutdown, NS_ERROR_NOT_INITIALIZED); + + nsAutoCString memoryStorageID(aContextKey); + AppendMemoryStorageTag(memoryStorageID); + + if (aDiskStorage) { + LOG((" dooming disk+memory storage of %s", aContextKey.BeginReading())); + + // Walk one by one and remove entries according their pin status + CacheEntryTable *diskEntries, *memoryEntries; + if (sGlobalEntryTables->Get(aContextKey, &diskEntries)) { + sGlobalEntryTables->Get(memoryStorageID, &memoryEntries); + + for (auto iter = diskEntries->Iter(); !iter.Done(); iter.Next()) { + auto entry = iter.Data(); + if (entry->DeferOrBypassRemovalOnPinStatus(aPinned)) { + continue; + } + + if (memoryEntries) { + RemoveExactEntry(memoryEntries, iter.Key(), entry, false); + } + iter.Remove(); + } + } + + if (aContext && !aContext->IsPrivate()) { + LOG((" dooming disk entries")); + CacheFileIOManager::EvictByContext(aContext, aPinned, u""_ns); + } + } else { + LOG((" dooming memory-only storage of %s", aContextKey.BeginReading())); + + // Remove the memory entries table from the global tables. + // Since we store memory entries also in the disk entries table + // we need to remove the memory entries from the disk table one + // by one manually. + mozilla::UniquePtr memoryEntries; + sGlobalEntryTables->Remove(memoryStorageID, &memoryEntries); + + CacheEntryTable* diskEntries; + if (memoryEntries && sGlobalEntryTables->Get(aContextKey, &diskEntries)) { + for (const auto& memoryEntry : *memoryEntries) { + const auto& entry = memoryEntry.GetData(); + RemoveExactEntry(diskEntries, memoryEntry.GetKey(), entry, false); + } + } + } + + { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + if (aContext) { + for (auto iter = mForcedValidEntries.Iter(); !iter.Done(); iter.Next()) { + bool matches; + DebugOnly rv = CacheFileUtils::KeyMatchesLoadContextInfo( + iter.Key(), aContext, &matches); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + if (matches) { + iter.Remove(); + } + } + } else { + mForcedValidEntries.Clear(); + } + } + + // An artificial callback. This is a candidate for removal tho. In the new + // cache any 'doom' or 'evict' function ensures that the entry or entries + // being doomed is/are not accessible after the function returns. So there is + // probably no need for a callback - has no meaning. But for compatibility + // with the old cache that is still in the tree we keep the API similar to be + // able to make tests as well as other consumers work for now. + class Callback : public Runnable { + public: + explicit Callback(nsICacheEntryDoomCallback* aCallback) + : mozilla::Runnable("Callback"), mCallback(aCallback) {} + NS_IMETHOD Run() override { + mCallback->OnCacheEntryDoomed(NS_OK); + return NS_OK; + } + nsCOMPtr mCallback; + }; + + if (aCallback) { + RefPtr callback = new Callback(aCallback); + return NS_DispatchToMainThread(callback); + } + + return NS_OK; +} + +nsresult CacheStorageService::WalkStorageEntries( + CacheStorage const* aStorage, bool aVisitEntries, + nsICacheStorageVisitor* aVisitor) { + LOG(("CacheStorageService::WalkStorageEntries [cb=%p, visitentries=%d]", + aVisitor, aVisitEntries)); + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_ARG(aStorage); + + if (aStorage->WriteToDisk()) { + RefPtr event = new WalkDiskCacheRunnable( + aStorage->LoadInfo(), aVisitEntries, aVisitor); + return event->Walk(); + } + + RefPtr event = new WalkMemoryCacheRunnable( + aStorage->LoadInfo(), aVisitEntries, aVisitor); + return event->Walk(); +} + +void CacheStorageService::CacheFileDoomed(nsILoadContextInfo* aLoadContextInfo, + const nsACString& aIdExtension, + const nsACString& aURISpec) { + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aLoadContextInfo, contextKey); + + nsAutoCString entryKey; + CacheEntry::HashingKey(""_ns, aIdExtension, aURISpec, entryKey); + + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) { + return; + } + + CacheEntryTable* entries; + RefPtr entry; + + if (sGlobalEntryTables->Get(contextKey, &entries) && + entries->Get(entryKey, getter_AddRefs(entry))) { + if (entry->IsFileDoomed()) { + // Need to remove under the lock to avoid possible race leading + // to duplication of the entry per its key. + RemoveExactEntry(entries, entryKey, entry, false); + entry->DoomAlreadyRemoved(); + } + + // Entry found, but it's not the entry that has been found doomed + // by the lower eviction layer. Just leave everything unchanged. + return; + } + + RemoveEntryForceValid(contextKey, entryKey); +} + +bool CacheStorageService::GetCacheEntryInfo( + nsILoadContextInfo* aLoadContextInfo, const nsACString& aIdExtension, + const nsACString& aURISpec, EntryInfoCallback* aCallback) { + nsAutoCString contextKey; + CacheFileUtils::AppendKeyPrefix(aLoadContextInfo, contextKey); + + nsAutoCString entryKey; + CacheEntry::HashingKey(""_ns, aIdExtension, aURISpec, entryKey); + + RefPtr entry; + { + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) { + return false; + } + + CacheEntryTable* entries; + if (!sGlobalEntryTables->Get(contextKey, &entries)) { + return false; + } + + if (!entries->Get(entryKey, getter_AddRefs(entry))) { + return false; + } + } + + GetCacheEntryInfo(entry, aCallback); + return true; +} + +// static +void CacheStorageService::GetCacheEntryInfo(CacheEntry* aEntry, + EntryInfoCallback* aCallback) { + nsCString const uriSpec = aEntry->GetURI(); + nsCString const enhanceId = aEntry->GetEnhanceID(); + + nsAutoCString entryKey; + aEntry->HashingKeyWithStorage(entryKey); + + nsCOMPtr info = CacheFileUtils::ParseKey(entryKey); + + uint32_t dataSize; + if (NS_FAILED(aEntry->GetStorageDataSize(&dataSize))) { + dataSize = 0; + } + int64_t altDataSize; + if (NS_FAILED(aEntry->GetAltDataSize(&altDataSize))) { + altDataSize = 0; + } + uint32_t fetchCount; + if (NS_FAILED(aEntry->GetFetchCount(&fetchCount))) { + fetchCount = 0; + } + uint32_t lastModified; + if (NS_FAILED(aEntry->GetLastModified(&lastModified))) { + lastModified = 0; + } + uint32_t expirationTime; + if (NS_FAILED(aEntry->GetExpirationTime(&expirationTime))) { + expirationTime = 0; + } + + aCallback->OnEntryInfo(uriSpec, enhanceId, dataSize, altDataSize, fetchCount, + lastModified, expirationTime, aEntry->IsPinned(), + info); +} + +// static +uint32_t CacheStorageService::CacheQueueSize(bool highPriority) { + RefPtr thread = CacheFileIOManager::IOThread(); + // The thread will be null at shutdown. + if (!thread) { + return 0; + } + return thread->QueueSize(highPriority); +} + +// Telemetry collection + +namespace { + +bool TelemetryEntryKey(CacheEntry const* entry, nsAutoCString& key) { + nsAutoCString entryKey; + nsresult rv = entry->HashingKey(entryKey); + if (NS_FAILED(rv)) return false; + + if (entry->GetStorageID().IsEmpty()) { + // Hopefully this will be const-copied, saves some memory + key = entryKey; + } else { + key.Assign(entry->GetStorageID()); + key.Append(':'); + key.Append(entryKey); + } + + return true; +} + +} // namespace + +void CacheStorageService::TelemetryPrune(TimeStamp& now) { + static TimeDuration const oneMinute = TimeDuration::FromSeconds(60); + static TimeStamp dontPruneUntil = now + oneMinute; + if (now < dontPruneUntil) return; + + static TimeDuration const fifteenMinutes = TimeDuration::FromSeconds(900); + for (auto iter = mPurgeTimeStamps.Iter(); !iter.Done(); iter.Next()) { + if (now - iter.Data() > fifteenMinutes) { + // We are not interested in resurrection of entries after 15 minutes + // of time. This is also the limit for the telemetry. + iter.Remove(); + } + } + dontPruneUntil = now + oneMinute; +} + +void CacheStorageService::TelemetryRecordEntryCreation( + CacheEntry const* entry) { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + nsAutoCString key; + if (!TelemetryEntryKey(entry, key)) return; + + TimeStamp now = TimeStamp::NowLoRes(); + TelemetryPrune(now); + + // When an entry is craeted (registered actually) we check if there is + // a timestamp marked when this very same cache entry has been removed + // (deregistered) because of over-memory-limit purging. If there is such + // a timestamp found accumulate telemetry on how long the entry was away. + TimeStamp timeStamp; + if (!mPurgeTimeStamps.Get(key, &timeStamp)) return; + + mPurgeTimeStamps.Remove(key); + + Telemetry::AccumulateTimeDelta(Telemetry::HTTP_CACHE_ENTRY_RELOAD_TIME, + timeStamp, TimeStamp::NowLoRes()); +} + +void CacheStorageService::TelemetryRecordEntryRemoval(CacheEntry* entry) { + MOZ_ASSERT(CacheStorageService::IsOnManagementThread()); + + // Doomed entries must not be considered, we are only interested in purged + // entries. Note that the mIsDoomed flag is always set before deregistration + // happens. + if (entry->IsDoomed()) return; + + nsAutoCString key; + if (!TelemetryEntryKey(entry, key)) return; + + // When an entry is removed (deregistered actually) we put a timestamp for + // this entry to the hashtable so that when the entry is created (registered) + // again we know how long it was away. Also accumulate number of AsyncOpen + // calls on the entry, this tells us how efficiently the pool actually works. + + TimeStamp now = TimeStamp::NowLoRes(); + TelemetryPrune(now); + mPurgeTimeStamps.InsertOrUpdate(key, now); + + Telemetry::Accumulate(Telemetry::HTTP_CACHE_ENTRY_REUSE_COUNT, + entry->UseCount()); + Telemetry::AccumulateTimeDelta(Telemetry::HTTP_CACHE_ENTRY_ALIVE_TIME, + entry->LoadStart(), TimeStamp::NowLoRes()); +} + +// nsIMemoryReporter + +size_t CacheStorageService::SizeOfExcludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + + size_t n = 0; + // The elemets are referenced by sGlobalEntryTables and are reported from + // there + n += Pool(true).mFrecencyArray.ShallowSizeOfExcludingThis(mallocSizeOf); + n += Pool(true).mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf); + n += Pool(false).mFrecencyArray.ShallowSizeOfExcludingThis(mallocSizeOf); + n += Pool(false).mExpirationArray.ShallowSizeOfExcludingThis(mallocSizeOf); + // Entries reported manually in CacheStorageService::CollectReports callback + if (sGlobalEntryTables) { + n += sGlobalEntryTables->ShallowSizeOfIncludingThis(mallocSizeOf); + } + n += mPurgeTimeStamps.SizeOfExcludingThis(mallocSizeOf); + + return n; +} + +size_t CacheStorageService::SizeOfIncludingThis( + mozilla::MallocSizeOf mallocSizeOf) const { + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +NS_IMETHODIMP +CacheStorageService::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + MutexAutoLock lock(mLock); + MOZ_COLLECT_REPORT("explicit/network/cache2/io", KIND_HEAP, UNITS_BYTES, + CacheFileIOManager::SizeOfIncludingThis(MallocSizeOf), + "Memory used by the cache IO manager."); + + MOZ_COLLECT_REPORT("explicit/network/cache2/index", KIND_HEAP, UNITS_BYTES, + CacheIndex::SizeOfIncludingThis(MallocSizeOf), + "Memory used by the cache index."); + + // Report the service instance, this doesn't report entries, done lower + MOZ_COLLECT_REPORT("explicit/network/cache2/service", KIND_HEAP, UNITS_BYTES, + SizeOfIncludingThis(MallocSizeOf), + "Memory used by the cache storage service."); + + // Report all entries, each storage separately (by the context key) + // + // References are: + // sGlobalEntryTables to N CacheEntryTable + // CacheEntryTable to N CacheEntry + // CacheEntry to 1 CacheFile + // CacheFile to + // N CacheFileChunk (keeping the actual data) + // 1 CacheFileMetadata (keeping http headers etc.) + // 1 CacheFileOutputStream + // N CacheFileInputStream + if (sGlobalEntryTables) { + for (const auto& globalEntry : *sGlobalEntryTables) { + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + + CacheEntryTable* table = globalEntry.GetWeak(); + + size_t size = 0; + mozilla::MallocSizeOf mallocSizeOf = CacheStorageService::MallocSizeOf; + + size += table->ShallowSizeOfIncludingThis(mallocSizeOf); + for (const auto& tableEntry : *table) { + size += tableEntry.GetKey().SizeOfExcludingThisIfUnshared(mallocSizeOf); + + // Bypass memory-only entries, those will be reported when iterating the + // memory only table. Memory-only entries are stored in both ALL_ENTRIES + // and MEMORY_ONLY hashtables. + RefPtr const& entry = tableEntry.GetData(); + if (table->Type() == CacheEntryTable::MEMORY_ONLY || + entry->IsUsingDisk()) { + size += entry->SizeOfIncludingThis(mallocSizeOf); + } + } + + aHandleReport->Callback( + ""_ns, + nsPrintfCString( + "explicit/network/cache2/%s-storage(%s)", + table->Type() == CacheEntryTable::MEMORY_ONLY ? "memory" : "disk", + aAnonymize ? "" + : globalEntry.GetKey().BeginReading()), + nsIMemoryReporter::KIND_HEAP, nsIMemoryReporter::UNITS_BYTES, size, + "Memory used by the cache storage."_ns, aData); + } + } + + return NS_OK; +} + +// nsICacheTesting + +NS_IMETHODIMP +CacheStorageService::IOThreadSuspender::Run() { + MonitorAutoLock mon(mMon); + while (!mSignaled) { + mon.Wait(); + } + return NS_OK; +} + +void CacheStorageService::IOThreadSuspender::Notify() { + MonitorAutoLock mon(mMon); + mSignaled = true; + mon.Notify(); +} + +NS_IMETHODIMP +CacheStorageService::SuspendCacheIOThread(uint32_t aLevel) { + RefPtr thread = CacheFileIOManager::IOThread(); + if (!thread) { + return NS_ERROR_NOT_AVAILABLE; + } + + MOZ_ASSERT(!mActiveIOSuspender); + mActiveIOSuspender = new IOThreadSuspender(); + return thread->Dispatch(mActiveIOSuspender, aLevel); +} + +NS_IMETHODIMP +CacheStorageService::ResumeCacheIOThread() { + MOZ_ASSERT(mActiveIOSuspender); + + RefPtr suspender; + suspender.swap(mActiveIOSuspender); + suspender->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +CacheStorageService::Flush(nsIObserver* aObserver) { + RefPtr thread = CacheFileIOManager::IOThread(); + if (!thread) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (!observerService) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Adding as weak, the consumer is responsible to keep the reference + // until notified. + observerService->AddObserver(aObserver, "cacheservice:purge-memory-pools", + false); + + // This runnable will do the purging and when done, notifies the above + // observer. We dispatch it to the CLOSE level, so all data writes scheduled + // up to this time will be done before this purging happens. + RefPtr r = + new CacheStorageService::PurgeFromMemoryRunnable(this, + CacheEntry::PURGE_WHOLE); + + return thread->Dispatch(r, CacheIOThread::WRITE); +} + +} // namespace mozilla::net diff --git a/netwerk/cache2/CacheStorageService.h b/netwerk/cache2/CacheStorageService.h new file mode 100644 index 0000000000..674f674424 --- /dev/null +++ b/netwerk/cache2/CacheStorageService.h @@ -0,0 +1,451 @@ +/* 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/. */ + +#ifndef CacheStorageService__h__ +#define CacheStorageService__h__ + +#include "nsICacheStorageService.h" +#include "nsIMemoryReporter.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsICacheTesting.h" + +#include "nsClassHashtable.h" +#include "nsTHashMap.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Monitor.h" +#include "mozilla/Mutex.h" +#include "mozilla/AtomicBitfields.h" +#include "mozilla/Atomics.h" +#include "mozilla/TimeStamp.h" +#include "nsTArray.h" + +class nsIURI; +class nsICacheEntryDoomCallback; +class nsICacheStorageVisitor; +class nsIRunnable; +class nsIThread; +class nsIEventTarget; + +namespace mozilla { + +class OriginAttributes; + +namespace net { + +class CacheStorageService; +class CacheStorage; +class CacheEntry; +class CacheEntryHandle; + +class CacheMemoryConsumer { + private: + friend class CacheStorageService; + // clang-format off + MOZ_ATOMIC_BITFIELDS(mAtomicBitfields, 32, ( + (uint32_t, ReportedMemoryConsumption, 30), + (uint32_t, Flags, 2) + )) + // clang-format on + + private: + CacheMemoryConsumer() = delete; + + protected: + enum { + // No special treatment, reports always to the disk-entries pool. + NORMAL = 0, + // This consumer is belonging to a memory-only cache entry, used to decide + // which of the two disk and memory pools count this consumption at. + MEMORY_ONLY = 1 << 0, + // Prevent reports of this consumer at all, used for disk data chunks since + // we throw them away as soon as the entry is not used by any consumer and + // don't want to make them wipe the whole pool out during their short life. + DONT_REPORT = 1 << 1 + }; + + explicit CacheMemoryConsumer(uint32_t aFlags); + ~CacheMemoryConsumer() { DoMemoryReport(0); } + void DoMemoryReport(uint32_t aCurrentSize); +}; + +class CacheStorageService final : public nsICacheStorageService, + public nsIMemoryReporter, + public nsITimerCallback, + public nsICacheTesting, + public nsINamed { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHESTORAGESERVICE + NS_DECL_NSIMEMORYREPORTER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSICACHETESTING + NS_DECL_NSINAMED + + CacheStorageService(); + + void Shutdown(); + void DropPrivateBrowsingEntries(); + + static CacheStorageService* Self() { return sSelf; } + static nsISupports* SelfISupports() { + return static_cast(Self()); + } + nsresult Dispatch(nsIRunnable* aEvent); + static bool IsRunning() { return sSelf && !sSelf->mShutdown; } + static bool IsOnManagementThread(); + already_AddRefed Thread() const; + mozilla::Mutex& Lock() { return mLock; } + + // Tracks entries that may be forced valid in a pruned hashtable. + struct ForcedValidData { + // The timestamp is computed when the entry gets inserted into the map. + // It should never be null for an entry in the map. + TimeStamp validUntil; + // viewed gets set to true by a call to MarkForcedValidEntryUse() + bool viewed = false; + }; + nsTHashMap mForcedValidEntries; + void ForcedValidEntriesPrune(TimeStamp& now); + + // Helper thread-safe interface to pass entry info, only difference from + // nsICacheStorageVisitor is that instead of nsIURI only the uri spec is + // passed. + class EntryInfoCallback { + public: + virtual void OnEntryInfo(const nsACString& aURISpec, + const nsACString& aIdEnhance, int64_t aDataSize, + int64_t aAltDataSize, uint32_t aFetchCount, + uint32_t aLastModifiedTime, + uint32_t aExpirationTime, bool aPinned, + nsILoadContextInfo* aInfo) = 0; + }; + + // Invokes OnEntryInfo for the given aEntry, synchronously. + static void GetCacheEntryInfo(CacheEntry* aEntry, + EntryInfoCallback* aCallback); + + nsresult GetCacheIndexEntryAttrs(CacheStorage const* aStorage, + const nsACString& aURI, + const nsACString& aIdExtension, + bool* aHasAltData, uint32_t* aFileSizeKb); + + static uint32_t CacheQueueSize(bool highPriority); + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf) + + private: + virtual ~CacheStorageService(); + void ShutdownBackground(); + + private: + // The following methods may only be called on the management + // thread. + friend class CacheEntry; + + /** + * Registers the entry in management ordered arrays, a mechanism + * helping with weighted purge of entries. + * Management arrays keep hard reference to the entry. Entry is + * responsible to remove it self or the service is responsible to + * remove the entry when it's no longer needed. + */ + void RegisterEntry(CacheEntry* aEntry); + + /** + * Deregisters the entry from management arrays. References are + * then released. + */ + void UnregisterEntry(CacheEntry* aEntry); + + /** + * Removes the entry from the related entry hash table, if still present. + */ + bool RemoveEntry(CacheEntry* aEntry, bool aOnlyUnreferenced = false); + + /** + * Tells the storage service whether this entry is only to be stored in + * memory. + */ + void RecordMemoryOnlyEntry(CacheEntry* aEntry, bool aOnlyInMemory, + bool aOverwrite); + + /** + * Sets a cache entry valid (overrides the default loading behavior by loading + * directly from cache) for the given number of seconds + * See nsICacheEntry.idl for more details + */ + void ForceEntryValidFor(nsACString const& aContextKey, + nsACString const& aEntryKey, + uint32_t aSecondsToTheFuture); + + /** + * Remove the validity info + */ + void RemoveEntryForceValid(nsACString const& aContextKey, + nsACString const& aEntryKey); + + /** + * Retrieves the status of the cache entry to see if it has been forced valid + * (so it will loaded directly from cache without further validation) + */ + bool IsForcedValidEntry(nsACString const& aContextKey, + nsACString const& aEntryKey); + + // Marks the entry as used, so we may properly report when it gets evicted + // if the prefetched resource was used or not. + void MarkForcedValidEntryUse(nsACString const& aContextKey, + nsACString const& aEntryKey); + + private: + friend class CacheIndex; + + /** + * CacheIndex uses this to prevent a cache entry from being prememptively + * thrown away when forced valid + * See nsICacheEntry.idl for more details + */ + bool IsForcedValidEntry(nsACString const& aContextEntryKey); + + private: + // These are helpers for telemetry monitoring of the memory pools. + void TelemetryPrune(TimeStamp& now); + void TelemetryRecordEntryCreation(CacheEntry const* entry); + void TelemetryRecordEntryRemoval(CacheEntry* entry); + + private: + // Following methods are thread safe to call. + friend class CacheStorage; + + /** + * Get, or create when not existing and demanded, an entry for the storage + * and uri+id extension. + */ + nsresult AddStorageEntry(CacheStorage const* aStorage, const nsACString& aURI, + const nsACString& aIdExtension, uint32_t aFlags, + CacheEntryHandle** aResult); + + /** + * Check existance of an entry. This may throw NS_ERROR_NOT_AVAILABLE + * when the information cannot be obtained synchronously w/o blocking. + */ + nsresult CheckStorageEntry(CacheStorage const* aStorage, + const nsACString& aURI, + const nsACString& aIdExtension, bool* aResult); + + /** + * Removes the entry from the related entry hash table, if still present + * and returns it. + */ + nsresult DoomStorageEntry(CacheStorage const* aStorage, + const nsACString& aURI, + const nsACString& aIdExtension, + nsICacheEntryDoomCallback* aCallback); + + /** + * Removes and returns entry table for the storage. + */ + nsresult DoomStorageEntries(CacheStorage const* aStorage, + nsICacheEntryDoomCallback* aCallback); + + /** + * Walk all entiries beloging to the storage. + */ + nsresult WalkStorageEntries(CacheStorage const* aStorage, bool aVisitEntries, + nsICacheStorageVisitor* aVisitor); + + private: + friend class CacheFileIOManager; + + /** + * CacheFileIOManager uses this method to notify CacheStorageService that + * an active entry was removed. This method is called even if the entry + * removal was originated by CacheStorageService. + */ + void CacheFileDoomed(nsILoadContextInfo* aLoadContextInfo, + const nsACString& aIdExtension, + const nsACString& aURISpec); + + /** + * Tries to find an existing entry in the hashtables and synchronously call + * OnCacheEntryInfo of the aVisitor callback when found. + * @retuns + * true, when the entry has been found that also implies the callbacks has + * beem invoked + * false, when an entry has not been found + */ + bool GetCacheEntryInfo(nsILoadContextInfo* aLoadContextInfo, + const nsACString& aIdExtension, + const nsACString& aURISpec, + EntryInfoCallback* aCallback); + + private: + friend class CacheMemoryConsumer; + + /** + * When memory consumption of this entry radically changes, this method + * is called to reflect the size of allocated memory. This call may purge + * unspecified number of entries from memory (but not from disk). + */ + void OnMemoryConsumptionChange(CacheMemoryConsumer* aConsumer, + uint32_t aCurrentMemoryConsumption); + + /** + * If not already pending, it schedules mPurgeTimer that fires after 1 second + * and dispatches PurgeOverMemoryLimit(). + */ + void SchedulePurgeOverMemoryLimit(); + + /** + * Called on the management thread, removes all expired and then least used + * entries from the memory, first from the disk pool and then from the memory + * pool. + */ + void PurgeOverMemoryLimit(); + + private: + nsresult DoomStorageEntries(const nsACString& aContextKey, + nsILoadContextInfo* aContext, bool aDiskStorage, + bool aPin, nsICacheEntryDoomCallback* aCallback); + nsresult AddStorageEntry(const nsACString& aContextKey, + const nsACString& aURI, + const nsACString& aIdExtension, bool aWriteToDisk, + bool aSkipSizeCheck, bool aPin, uint32_t aFlags, + CacheEntryHandle** aResult); + + nsresult ClearOriginInternal( + const nsAString& aOrigin, + const mozilla::OriginAttributes& aOriginAttributes, bool aAnonymous); + + static CacheStorageService* sSelf; + + mozilla::Mutex mLock MOZ_UNANNOTATED{"CacheStorageService.mLock"}; + mozilla::Mutex mForcedValidEntriesLock{ + "CacheStorageService.mForcedValidEntriesLock"}; + + Atomic mShutdown{false}; + + // Accessible only on the service thread + class MemoryPool { + public: + enum EType { + DISK, + MEMORY, + } mType; + + explicit MemoryPool(EType aType); + ~MemoryPool(); + + nsTArray> mFrecencyArray; + nsTArray> mExpirationArray; + Atomic mMemorySize{0}; + + bool OnMemoryConsumptionChange(uint32_t aSavedMemorySize, + uint32_t aCurrentMemoryConsumption); + /** + * Purges entries from memory based on the frecency ordered array. + */ + void PurgeOverMemoryLimit(); + void PurgeExpired(); + void PurgeByFrecency(uint32_t aWhat); + void PurgeAll(uint32_t aWhat); + + private: + uint32_t Limit() const; + MemoryPool() = delete; + }; + + MemoryPool mDiskPool{MemoryPool::DISK}; + MemoryPool mMemoryPool{MemoryPool::MEMORY}; + TimeStamp mLastPurgeTime; + MemoryPool& Pool(bool aUsingDisk) { + return aUsingDisk ? mDiskPool : mMemoryPool; + } + MemoryPool const& Pool(bool aUsingDisk) const { + return aUsingDisk ? mDiskPool : mMemoryPool; + } + + nsCOMPtr mPurgeTimer; +#ifdef MOZ_TSAN + // In OnMemoryConsumptionChange() we check whether the timer exists, but we + // cannot grab the lock there (see comment 6 in bug 1614637) and TSan reports + // a data race. This data race is harmless, so we use this atomic flag only in + // TSan build to suppress it. + Atomic mPurgeTimerActive{false}; +#endif + + class PurgeFromMemoryRunnable : public Runnable { + public: + PurgeFromMemoryRunnable(CacheStorageService* aService, uint32_t aWhat) + : Runnable("net::CacheStorageService::PurgeFromMemoryRunnable"), + mService(aService), + mWhat(aWhat) {} + + private: + virtual ~PurgeFromMemoryRunnable() = default; + + NS_IMETHOD Run() override; + + RefPtr mService; + uint32_t mWhat; + }; + + // Used just for telemetry purposes, accessed only on the management thread. + // Note: not included in the memory reporter, this is not expected to be huge + // and also would be complicated to report since reporting happens on the main + // thread but this table is manipulated on the management thread. + nsTHashMap mPurgeTimeStamps; + + // nsICacheTesting + class IOThreadSuspender : public Runnable { + public: + IOThreadSuspender() + : Runnable("net::CacheStorageService::IOThreadSuspender"), + mMon("IOThreadSuspender") {} + void Notify(); + + private: + virtual ~IOThreadSuspender() = default; + NS_IMETHOD Run() override; + + Monitor mMon MOZ_UNANNOTATED; + bool mSignaled{false}; + }; + + RefPtr mActiveIOSuspender; +}; + +template +void ProxyRelease(const char* aName, nsCOMPtr& object, + nsIEventTarget* target) { + NS_ProxyRelease(aName, target, object.forget()); +} + +template +void ProxyReleaseMainThread(const char* aName, nsCOMPtr& object) { + ProxyRelease(aName, object, GetMainThreadSerialEventTarget()); +} + +} // namespace net +} // namespace mozilla + +#define NS_CACHE_STORAGE_SERVICE_CID \ + { \ + 0xea70b098, 0x5014, 0x4e21, { \ + 0xae, 0xe1, 0x75, 0xe6, 0xb2, 0xc4, 0xb8, 0xe0 \ + } \ + } + +#define NS_CACHE_STORAGE_SERVICE_CONTRACTID \ + "@mozilla.org/netwerk/cache-storage-service;1" + +#define NS_CACHE_STORAGE_SERVICE_CONTRACTID2 \ + "@mozilla.org/network/cache-storage-service;1" + +#endif diff --git a/netwerk/cache2/moz.build b/netwerk/cache2/moz.build new file mode 100644 index 0000000000..54afa11a09 --- /dev/null +++ b/netwerk/cache2/moz.build @@ -0,0 +1,66 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +with Files("**"): + BUG_COMPONENT = ("Core", "Networking: Cache") + +XPIDL_SOURCES += [ + "nsICacheEntry.idl", + "nsICacheEntryDoomCallback.idl", + "nsICacheEntryOpenCallback.idl", + "nsICachePurgeLock.idl", + "nsICacheStorage.idl", + "nsICacheStorageService.idl", + "nsICacheStorageVisitor.idl", + "nsICacheTesting.idl", +] + +XPIDL_MODULE = "necko_cache2" + +EXPORTS += [ + "CacheObserver.h", + "CacheStorageService.h", +] + +EXPORTS.mozilla.net += ["CachePurgeLock.h"] + +SOURCES += [ + "CacheStorage.cpp", +] + + +UNIFIED_SOURCES += [ + "CacheEntry.cpp", + "CacheFile.cpp", + "CacheFileChunk.cpp", + "CacheFileContextEvictor.cpp", + "CacheFileInputStream.cpp", + "CacheFileIOManager.cpp", + "CacheFileMetadata.cpp", + "CacheFileOutputStream.cpp", + "CacheFileUtils.cpp", + "CacheHashUtils.cpp", + "CacheIndex.cpp", + "CacheIndexContextIterator.cpp", + "CacheIndexIterator.cpp", + "CacheIOThread.cpp", + "CacheLog.cpp", + "CacheObserver.cpp", + "CacheStorageService.cpp", +] + +if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android": + UNIFIED_SOURCES += [ + "CachePurgeLock.cpp", + ] + +LOCAL_INCLUDES += [ + "/netwerk/base", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/netwerk/cache2/nsICacheEntry.idl b/netwerk/cache2/nsICacheEntry.idl new file mode 100644 index 0000000000..744c5a014f --- /dev/null +++ b/netwerk/cache2/nsICacheEntry.idl @@ -0,0 +1,369 @@ +/* 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 "nsISupports.idl" + +interface nsIAsyncOutputStream; +interface nsICacheEntryDoomCallback; +interface nsICacheEntryMetaDataVisitor; +interface nsIInputStream; +interface nsILoadContextInfo; +interface nsIOutputStream; +interface nsITransportSecurityInfo; + +[scriptable, uuid(607c2a2c-0a48-40b9-a956-8cf2bb9857cf)] +interface nsICacheEntry : nsISupports +{ + const unsigned long CONTENT_TYPE_UNKNOWN = 0; + const unsigned long CONTENT_TYPE_OTHER = 1; + const unsigned long CONTENT_TYPE_JAVASCRIPT = 2; + const unsigned long CONTENT_TYPE_IMAGE = 3; + const unsigned long CONTENT_TYPE_MEDIA = 4; + const unsigned long CONTENT_TYPE_STYLESHEET = 5; + const unsigned long CONTENT_TYPE_WASM = 6; + /** + * Content type that is used internally to check whether the value parsed + * from disk is within allowed limits. Don't pass CONTENT_TYPE_LAST to + * setContentType method. + */ + const unsigned long CONTENT_TYPE_LAST = 7; + + /** + * Placeholder for the initial value of expiration time. + */ + const unsigned long NO_EXPIRATION_TIME = 0xFFFFFFFF; + + /** + * Get the key identifying the cache entry. + */ + readonly attribute ACString key; + + /** + * The unique ID for every nsICacheEntry instance, which can be used to check + * whether two pieces of information are from the same nsICacheEntry instance. + */ + readonly attribute uint64_t cacheEntryId; + + /** + * Whether the entry is memory/only or persisted to disk. + * Note: private browsing entries are reported as persistent for consistency + * while are not actually persisted to disk. + */ + readonly attribute boolean persistent; + + /** + * Get the number of times the cache entry has been opened. + */ + readonly attribute uint32_t fetchCount; + + /** + * Get the last time the cache entry was opened (in seconds since the Epoch). + */ + readonly attribute uint32_t lastFetched; + + /** + * Get the last time the cache entry was modified (in seconds since the Epoch). + */ + readonly attribute uint32_t lastModified; + + /** + * Get the expiration time of the cache entry (in seconds since the Epoch). + */ + readonly attribute uint32_t expirationTime; + + /** + * Set the time at which the cache entry should be considered invalid (in + * seconds since the Epoch). + */ + void setExpirationTime(in uint32_t expirationTime); + + /** + * Get the last network response times for onStartReqeust/onStopRequest (in ms). + * @throws + * - NS_ERROR_NOT_AVAILABLE if onStartTime/onStopTime does not exist. + */ + readonly attribute uint64_t onStartTime; + readonly attribute uint64_t onStopTime; + + /** + * Set the network response times for onStartReqeust/onStopRequest (in ms). + */ + void setNetworkTimes(in uint64_t onStartTime, in uint64_t onStopTime); + + /** + * Set content type. Available types are defined at the begining of this file. + * The content type is used internally for cache partitioning and telemetry + * purposes so there is no getter. + */ + void setContentType(in uint8_t contentType); + + /** + * This method is intended to override the per-spec cache validation + * decisions for a duration specified in seconds. The current state can + * be examined with isForcedValid (see below). This value is not persisted, + * so it will not survive session restart. Cache entries that are forced valid + * will not be evicted from the cache for the duration of forced validity. + * This means that there is a potential problem if the number of forced valid + * entries grows to take up more space than the cache size allows. + * + * NOTE: entries that have been forced valid will STILL be ignored by HTTP + * channels if they have expired AND the resource in question requires + * validation after expiring. This is to avoid using known-stale content. + * + * @param aSecondsToTheFuture + * the number of seconds the default cache validation behavior will be + * overridden before it returns to normal + */ + void forceValidFor(in unsigned long aSecondsToTheFuture); + + /** + * The state variable for whether this entry is currently forced valid. + * Defaults to false for normal cache validation behavior, and will return + * true if the number of seconds set by forceValidFor() has yet to be reached. + */ + readonly attribute boolean isForcedValid; + + /** + * This method gets called to mark the actual use of the forced-valid entry. + * This is necessary for telemetry, so when the entry eventually gets + * evicted we can report whether it was ever used or not. + * If the entry was not forced-valid, then this operation has no effect. + */ + void markForcedValidUse(); + + /** + * Open blocking input stream to cache data. Use the stream transport + * service to asynchronously read this stream on a background thread. + * The returned stream MAY implement nsISeekableStream. + * + * @param offset + * read starting from this offset into the cached data. an offset + * beyond the end of the stream has undefined consequences. + * + * @return non-blocking, buffered input stream. + */ + nsIInputStream openInputStream(in long long offset); + + /** + * Open non-blocking output stream to cache data. The returned stream + * MAY implement nsISeekableStream. + * + * If opening an output stream to existing cached data, the data will be + * truncated to the specified offset. + * + * @param offset + * write starting from this offset into the cached data. an offset + * beyond the end of the stream has undefined consequences. + * @param predictedSize + * Predicted size of the data that will be written. It's used to decide + * whether the resulting entry would exceed size limit, in which case + * an error is thrown. If the size isn't known in advance, -1 should be + * passed. + * + * @return blocking, buffered output stream. + */ + nsIOutputStream openOutputStream(in long long offset, in long long predictedSize); + + /** + * Get/set security info on the cache entry for this descriptor. + */ + attribute nsITransportSecurityInfo securityInfo; + + /** + * Get the size of the cache entry data, as stored. This may differ + * from the entry's dataSize, if the entry is compressed. + */ + readonly attribute unsigned long storageDataSize; + + /** + * Asynchronously doom an entry. Listener will be notified about the status + * of the operation. Null may be passed if caller doesn't care about the + * result. + */ + void asyncDoom(in nsICacheEntryDoomCallback listener); + + /** + * Methods for accessing meta data. Meta data is a table of key/value + * string pairs. The strings do not have to conform to any particular + * charset, but they must be null terminated. + */ + string getMetaDataElement(in string key); + void setMetaDataElement(in string key, in string value); + + /** + * Obtain the list of metadata keys this entry keeps. + * + * NOTE: The callback is invoked under the CacheFile's lock. It means + * there should not be made any calls to the entry from the visitor and + * if the values need to be processed somehow, it's better to cache them + * and process outside the callback. + */ + void visitMetaData(in nsICacheEntryMetaDataVisitor visitor); + + /** + * Claims that all metadata on this entry are up-to-date and this entry + * now can be delivered to other waiting consumers. + * + * We need such method since metadata must be delivered synchronously. + */ + void metaDataReady(); + + /** + * Called by consumer upon 304/206 response from the server. This marks + * the entry content as positively revalidated. + * Consumer uses this method after the consumer has returned ENTRY_NEEDS_REVALIDATION + * result from onCacheEntryCheck and after successfull revalidation with the server. + */ + void setValid(); + + /** + * Explicitly tell the cache backend this consumer is no longer going to modify + * this cache entry data or metadata. In case the consumer was responsible to + * either of writing the cache entry or revalidating it, calling this method + * reverts the state to initial (as never written) or as not-validated and + * immediately notifies the next consumer in line waiting for this entry. + * This is the way to prevent deadlocks when someone else than the responsible + * channel references the cache entry being in a non-written or revalidating + * state. + */ + void dismiss(); + + /** + * Returns the size in kilobytes used to store the cache entry on disk. + */ + readonly attribute uint32_t diskStorageSizeInKB; + + /** + * Doom this entry and open a new, empty, entry for write. Consumer has + * to exchange the entry this method is called on for the newly created. + * Used on 200 responses to conditional requests. + * + * @param aMemoryOnly + * - whether the entry is to be created as memory/only regardless how + * the entry being recreated persistence is set + * @returns + * - an entry that can be used to write to + * @throws + * - NS_ERROR_NOT_AVAILABLE when the entry cannot be from some reason + * recreated for write + */ + nsICacheEntry recreate([optional] in boolean aMemoryOnly); + + /** + * Returns the length of data this entry holds. + * @throws + * NS_ERROR_IN_PROGRESS when the write is still in progress. + */ + readonly attribute long long dataSize; + + /** + * Returns the length of data this entry holds. + * @throws + * - NS_ERROR_IN_PROGRESS when a write is still in progress (either real + content or alt data). + * - NS_ERROR_NOT_AVAILABLE if alt data does not exist. + */ + readonly attribute long long altDataSize; + + /** + * Returns the type of the saved alt data. + * @throws + * - NS_ERROR_NOT_AVAILABLE if alt data does not exist. + */ + readonly attribute ACString altDataType; + + /** + * Opens and returns an output stream that a consumer may use to save an + * alternate representation of the data. + * + * @param type + * type of the alternative data representation + * @param predictedSize + * Predicted size of the data that will be written. It's used to decide + * whether the resulting entry would exceed size limit, in which case + * an error is thrown. If the size isn't known in advance, -1 should be + * passed. + * + * @throws + * - NS_ERROR_NOT_AVAILABLE if the real data hasn't been written. + * - NS_ERROR_IN_PROGRESS when the writing regular content or alt-data to + * the cache entry is still in progress. + * + * If there is alt-data already saved, it will be overwritten. + */ + nsIAsyncOutputStream openAlternativeOutputStream(in ACString type, in long long predictedSize); + + /** + * Opens and returns an input stream that can be used to read the alternative + * representation previously saved in the cache. + * If this call is made while writing alt-data is still in progress, it is + * still possible to read content from the input stream as it's being written. + * @throws + * - NS_ERROR_NOT_AVAILABLE if the alt-data representation doesn't exist at + * all or if alt-data of the given type doesn't exist. + */ + nsIInputStream openAlternativeInputStream(in ACString type); + + /** + * Get the nsILoadContextInfo of the cache entry + */ + readonly attribute nsILoadContextInfo loadContextInfo; + + /**************************************************************************** + * The following methods might be added to some nsICacheEntryInternal + * interface since we want to remove them as soon as the old cache backend is + * completely removed. + */ + + /** + * @deprecated + * FOR BACKWARD COMPATIBILITY ONLY + * When the old cache backend is eventually removed, this method + * can be removed too. + * + * In the new backend: this method is no-op + * In the old backend: this method delegates to nsICacheEntryDescriptor.close() + */ + void close(); + + /** + * @deprecated + * FOR BACKWARD COMPATIBILITY ONLY + * Marks the entry as valid so that others can use it and get only readonly + * access when the entry is held by the 1st writer. + */ + void markValid(); + + /** + * @deprecated + * FOR BACKWARD COMPATIBILITY ONLY + * Marks the entry as valid when write access is acquired. + */ + void maybeMarkValid(); + + /** + * @deprecated + * FOR BACKWARD COMPATIBILITY ONLY / KINDA HACK + * @param aWriteAllowed + * Consumer indicates whether write to the entry is allowed for it. + * Depends on implementation how the flag is handled. + * @returns + * true when write access is acquired for this entry, + * false otherwise + */ + boolean hasWriteAccess(in boolean aWriteAllowed); +}; + +/** + * Argument for nsICacheEntry.visitMetaData, provides access to all metadata + * keys and values stored on the entry. + */ +[scriptable, uuid(fea3e276-6ba5-4ceb-a581-807d1f43f6d0)] +interface nsICacheEntryMetaDataVisitor : nsISupports +{ + /** + * Called over each key / value pair. + */ + void onMetaDataElement(in string key, in string value); +}; diff --git a/netwerk/cache2/nsICacheEntryDoomCallback.idl b/netwerk/cache2/nsICacheEntryDoomCallback.idl new file mode 100644 index 0000000000..a16a738292 --- /dev/null +++ b/netwerk/cache2/nsICacheEntryDoomCallback.idl @@ -0,0 +1,15 @@ +/* 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 "nsISupports.idl" + +[scriptable, uuid(2f8896be-232f-4140-afb3-1faffb56f3c6)] +interface nsICacheEntryDoomCallback : nsISupports +{ + /** + * Callback invoked after an entry or entries has/have been + * doomed from the cache. + */ + void onCacheEntryDoomed(in nsresult aResult); +}; diff --git a/netwerk/cache2/nsICacheEntryOpenCallback.idl b/netwerk/cache2/nsICacheEntryOpenCallback.idl new file mode 100644 index 0000000000..0116d7cc57 --- /dev/null +++ b/netwerk/cache2/nsICacheEntryOpenCallback.idl @@ -0,0 +1,79 @@ +/* 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 "nsISupports.idl" + +interface nsICacheEntry; + +[scriptable, uuid(1fc9fe11-c6ac-4748-94bd-8555a5a12b94)] +interface nsICacheEntryOpenCallback : nsISupports +{ + /** + * State of the entry determined by onCacheEntryCheck. + * + * ENTRY_WANTED - the consumer is interested in the entry, we will pass it. + * RECHECK_AFTER_WRITE_FINISHED - the consumer cannot use the entry while data is + * still being written and wants to check it again after the current write is + * finished. This actually prevents concurrent read/write and is used with + * non-resumable HTTP responses. + * ENTRY_NEEDS_REVALIDATION - entry needs to be revalidated first with origin server, + * this means the loading channel will decide whether to use the entry content + * as is after it gets a positive response from the server about validity of the + * content ; when a new content needs to be loaded from the server, the loading + * channel opens a new entry with OPEN_TRUNCATE flag which dooms the one + * this check has been made for. + * ENTRY_NOT_WANTED - the consumer is not interested in the entry, we will not pass it. + */ + const unsigned long ENTRY_WANTED = 0; + const unsigned long RECHECK_AFTER_WRITE_FINISHED = 1; + const unsigned long ENTRY_NEEDS_REVALIDATION = 2; + const unsigned long ENTRY_NOT_WANTED = 3; + + /** + * Callback to perform any validity checks before the entry should be used. + * Called before onCacheEntryAvailable callback, depending on the result it + * may be called more then one time. + * + * This callback is ensured to be called on the same thread on which asyncOpenURI + * has been called, unless nsICacheStorage.CHECK_MULTITHREADED flag has been specified. + * In that case this callback can be invoked on any thread, usually it is the cache I/O + * or cache management thread. + * + * IMPORTANT NOTE: + * This callback may be invoked sooner then respective asyncOpenURI call exits. + * + * @param aEntry + * An entry to examine. Consumer has a chance to decide whether the + * entry is valid or not. + * @return + * State of the entry, see the constants just above. + */ + unsigned long onCacheEntryCheck(in nsICacheEntry aEntry); + + /** + * Callback giving actual result of asyncOpenURI. It may give consumer the cache + * entry or a failure result when it's not possible to open it from some reason. + * This callback is ensured to be called on the same thread on which asyncOpenURI + * has been called. + * + * IMPORTANT NOTE: + * This callback may be invoked sooner then respective asyncOpenURI call exits. + * + * @param aEntry + * The entry bound to the originally requested URI. + * @param aNew + * Whether no data so far has been stored for this entry, i.e. reading + * it will just fail. When aNew is true, a server request should be + * made and data stored to this new entry. + * @param aResult + * Result of the request. This may be a failure only when one of these + * issues occur: + * - the cache storage service could not be started due to some unexpected + * faulure + * - there is not enough disk space to create new entries + */ + void onCacheEntryAvailable(in nsICacheEntry aEntry, + in boolean aNew, + in nsresult aResult); +}; diff --git a/netwerk/cache2/nsICachePurgeLock.idl b/netwerk/cache2/nsICachePurgeLock.idl new file mode 100644 index 0000000000..a0937a8922 --- /dev/null +++ b/netwerk/cache2/nsICachePurgeLock.idl @@ -0,0 +1,40 @@ +/* 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 "nsISupports.idl" +interface nsIFile; + +/** + * This object is a wrapper of MultiInstanceLock. + * It's intended to be used to ensure exclusive access to folders being + * deleted by the purgeHTTPCache background task. + */ +[scriptable,uuid(8abb21e3-c6a0-4b4d-9333-cc0d72f2c23b)] +interface nsICachePurgeLock : nsISupports { + /** + * Initializes the lock using the profile name and the current process's + * path. + * Will throw if a lock was already acquired successfully. + */ + void lock(in AUTF8String profileName); + + /** + * Returns true if another instance also holds the lock. + * Throws if called before lock was called, or after unlock was called. + */ + bool isOtherInstanceRunning(); + + /** + * Releases the lock. + * This object may be locked again, potentially using a different path + * after unlocking. + */ + void unlock(); + + /** + * Returns the file used to guarantee single access to a resource. + * This method is used to remove the lock file when no longer necessary. + */ + nsIFile getLockFile(in AUTF8String profileName); +}; diff --git a/netwerk/cache2/nsICacheStorage.idl b/netwerk/cache2/nsICacheStorage.idl new file mode 100644 index 0000000000..8169c9b730 --- /dev/null +++ b/netwerk/cache2/nsICacheStorage.idl @@ -0,0 +1,145 @@ +/* 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 "nsISupports.idl" + +interface nsIURI; +interface nsICacheEntry; +interface nsICacheEntryOpenCallback; +interface nsICacheEntryDoomCallback; +interface nsICacheStorageVisitor; + +/** + * Representation of a cache storage. There can be just-in-mem, + * in-mem+on-disk, in-mem+on-disk+app-cache or just a specific + * app-cache storage. + */ +[scriptable, uuid(35d104a6-d252-4fd4-8a56-3c14657cad3b)] +interface nsICacheStorage : nsISupports +{ + /** + * Placeholder for specifying "no special flags" during open. + */ + const uint32_t OPEN_NORMALLY = 0; + + /** + * Rewrite any existing data when opening a URL. + */ + const uint32_t OPEN_TRUNCATE = 1 << 0; + + /** + * Only open an existing entry. Don't create a new one. + */ + const uint32_t OPEN_READONLY = 1 << 1; + + /** + * Use for first-paint blocking loads. + */ + const uint32_t OPEN_PRIORITY = 1 << 2; + + /** + * Bypass the cache load when write is still in progress. + */ + const uint32_t OPEN_BYPASS_IF_BUSY = 1 << 3; + + /** + * Perform the cache entry check (onCacheEntryCheck invocation) on any thread + * for optimal perfomance optimization. If this flag is not specified it is + * ensured that onCacheEntryCheck is called on the same thread as respective + * asyncOpen has been called. + */ + const uint32_t CHECK_MULTITHREADED = 1 << 4; + + /** + * Don't automatically update any 'last used' metadata of the entry. + */ + const uint32_t OPEN_SECRETLY = 1 << 5; + + /** + * Entry is being opened as part of a service worker interception. Do not + * allow the cache to be disabled in this case. + */ + const uint32_t OPEN_INTERCEPTED = 1 << 6; + + /** + * Asynchronously opens a cache entry for the specified URI. + * Result is fetched asynchronously via the callback. + * + * @param aURI + * The URI to search in cache or to open for writting. + * @param aIdExtension + * Any string that will extend (distinguish) the entry. Two entries + * with the same aURI but different aIdExtension will be comletely + * different entries. If you don't know what aIdExtension should be + * leave it empty. + * @param aFlags + * OPEN_NORMALLY - open cache entry normally for read and write + * OPEN_TRUNCATE - delete any existing entry before opening it + * OPEN_READONLY - don't create an entry if there is none + * OPEN_PRIORITY - give this request a priority over others + * OPEN_BYPASS_IF_BUSY - backward compatibility only, LOAD_BYPASS_LOCAL_CACHE_IF_BUSY + * CHECK_MULTITHREADED - onCacheEntryCheck may be called on any thread, consumer + * implementation is thread-safe + * @param aCallback + * The consumer that receives the result. + * IMPORTANT: The callback may be called sooner the method returns. + */ + void asyncOpenURI(in nsIURI aURI, in ACString aIdExtension, + in uint32_t aFlags, + in nsICacheEntryOpenCallback aCallback); + + /** + * Immediately opens a new and empty cache entry in the storage, any existing + * entries are immediately doomed. This is similar to the recreate() method + * on nsICacheEntry. + * + * Storage may not implement this method and throw NS_ERROR_NOT_IMPLEMENTED. + * In that case consumer must use asyncOpen with OPEN_TRUNCATE flag and get + * the new entry via a callback. + * + * @param aURI @see asyncOpenURI + * @param aIdExtension @see asyncOpenURI + */ + nsICacheEntry openTruncate(in nsIURI aURI, + in ACString aIdExtension); + + /** + * Synchronously check on existance of an entry. In case of disk entries + * this uses information from the cache index. When the index data are not + * up to date or index is still building, NS_ERROR_NOT_AVAILABLE is thrown. + * The same error may throw any storage implementation that cannot determine + * entry state without blocking the caller. + */ + boolean exists(in nsIURI aURI, in ACString aIdExtension); + + /** + * Synchronously check on existance of alternative data and size of the + * content. When the index data are not up to date or index is still building, + * NS_ERROR_NOT_AVAILABLE is thrown. The same error may throw any storage + * implementation that cannot determine entry state without blocking the caller. + */ + void getCacheIndexEntryAttrs(in nsIURI aURI, + in ACString aIdExtension, + out bool aHasAltData, + out uint32_t aSizeInKB); + /** + * Asynchronously removes an entry belonging to the URI from the cache. + */ + void asyncDoomURI(in nsIURI aURI, in ACString aIdExtension, + in nsICacheEntryDoomCallback aCallback); + + /** + * Asynchronously removes all cached entries under this storage. + * NOTE: Disk storage also evicts memory storage. + */ + void asyncEvictStorage(in nsICacheEntryDoomCallback aCallback); + + /** + * Visits the storage and its entries. + * NOTE: Disk storage also visits memory storage. + */ + void asyncVisitStorage(in nsICacheStorageVisitor aVisitor, + in boolean aVisitEntries); + +}; diff --git a/netwerk/cache2/nsICacheStorageService.idl b/netwerk/cache2/nsICacheStorageService.idl new file mode 100644 index 0000000000..f0a4cf5b65 --- /dev/null +++ b/netwerk/cache2/nsICacheStorageService.idl @@ -0,0 +1,138 @@ +/* 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 "nsISupports.idl" + +interface nsICacheStorage; +interface nsILoadContextInfo; +interface nsIEventTarget; +interface nsICacheStorageConsumptionObserver; +interface nsICacheStorageVisitor; +interface nsIPrincipal; + +/** + * Provides access to particual cache storages of the network URI cache. + */ +[scriptable, uuid(ae29c44b-fbc3-4552-afaf-0a157ce771e7)] +interface nsICacheStorageService : nsISupports +{ + /** + * Get storage where entries will only remain in memory, never written + * to the disk. + * + * NOTE: Any existing disk entry for [URL|id-extension] will be doomed + * prior opening an entry using this memory-only storage. Result of + * AsyncOpenURI will be a new and empty memory-only entry. Using + * OPEN_READONLY open flag has no effect on this behavior. + * + * @param aLoadContextInfo + * Information about the loading context, this focuses the storage JAR and + * respects separate storage for private browsing. + */ + nsICacheStorage memoryCacheStorage(in nsILoadContextInfo aLoadContextInfo); + + /** + * Get storage where entries will be written to disk when not forbidden by + * response headers. + */ + nsICacheStorage diskCacheStorage(in nsILoadContextInfo aLoadContextInfo); + + /** + * Get storage where entries will be written to disk and marked as pinned. + * These pinned entries are immune to over limit eviction and call of clear() + * on this service. + */ + nsICacheStorage pinningCacheStorage(in nsILoadContextInfo aLoadContextInfo); + + /** + * Evict any cache entry having the same origin of aPrincipal. + * + * @param aPrincipal + * The principal to compare the entries with. + */ + void clearOrigin(in nsIPrincipal aPrincipal); + + /** + * Evict any cache entry which belongs to a base domain. This includes entries + * partitioned under aBaseDomain and entries which belong to aBaseDomain, but + * are partitioned under other top level sites. + * @param aBaseDomain + * The base domain to clear cache for. + */ + void clearBaseDomain(in AString aBaseDomain); + + /** + * Evict any cache entry having the same originAttributes. + * + * @param aOriginAttributes + * The origin attributes in string format to compare the entries with. + */ + void clearOriginAttributes(in AString aOriginAttributes); + + /** + * Evict the whole cache. + */ + void clear(); + + /** + * Purge only data of disk backed entries. Metadata are left for + * performance purposes. + */ + const uint32_t PURGE_DISK_DATA_ONLY = 1; + /** + * Purge whole disk backed entries from memory. Disk files will + * be left unattended. + */ + const uint32_t PURGE_DISK_ALL = 2; + /** + * Purge all entries we keep in memory, including memory-storage + * entries. This may be dangerous to use. + */ + const uint32_t PURGE_EVERYTHING = 3; + /** + * Purges data we keep warmed in memory. Use for tests and for + * saving memory. + */ + void purgeFromMemory(in uint32_t aWhat); + + /** + * I/O thread target to use for any operations on disk + */ + readonly attribute nsIEventTarget ioTarget; + + /** + * Asynchronously determine how many bytes of the disk space the cache takes. + * @see nsICacheStorageConsumptionObserver + * @param aObserver + * A mandatory (weak referred) observer. Documented at + * nsICacheStorageConsumptionObserver. + * NOTE: the observer MUST implement nsISupportsWeakReference. + */ + void asyncGetDiskConsumption(in nsICacheStorageConsumptionObserver aObserver); + + /** + * Asynchronously visits all storages of the disk cache and memory cache. + * @see nsICacheStorageVisitor + * @param aVisitor + * A visitor callback. + * @param aVisitEntries + * A boolean indicates whether visits entries. + */ + void asyncVisitAllStorages(in nsICacheStorageVisitor aVisitor, + in boolean aVisitEntries); +}; + +[scriptable, uuid(7728ab5b-4c01-4483-a606-32bf5b8136cb)] +interface nsICacheStorageConsumptionObserver : nsISupports +{ + /** + * Callback invoked to answer asyncGetDiskConsumption call. Always triggered + * on the main thread. + * NOTE: implementers must also implement nsISupportsWeakReference. + * + * @param aDiskSize + * The disk consumption in bytes. + */ + void onNetworkCacheDiskConsumption(in int64_t aDiskSize); +}; diff --git a/netwerk/cache2/nsICacheStorageVisitor.idl b/netwerk/cache2/nsICacheStorageVisitor.idl new file mode 100644 index 0000000000..138de2c984 --- /dev/null +++ b/netwerk/cache2/nsICacheStorageVisitor.idl @@ -0,0 +1,36 @@ +/* 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 "nsISupports.idl" + +interface nsIURI; +interface nsIFile; +interface nsILoadContextInfo; + +[scriptable, uuid(6cc7c253-93b6-482b-8e9d-1e04d8e9d655)] +interface nsICacheStorageVisitor : nsISupports +{ + /** + */ + void onCacheStorageInfo(in uint32_t aEntryCount, + in uint64_t aConsumption, + in uint64_t aCapacity, + in nsIFile aDiskDirectory); + + /** + */ + void onCacheEntryInfo(in nsIURI aURI, + in ACString aIdEnhance, + in int64_t aDataSize, + in int64_t aAltDataSize, + in uint32_t aFetchCount, + in uint32_t aLastModifiedTime, + in uint32_t aExpirationTime, + in boolean aPinned, + in nsILoadContextInfo aInfo); + + /** + */ + void onCacheEntryVisitCompleted(); +}; diff --git a/netwerk/cache2/nsICacheTesting.idl b/netwerk/cache2/nsICacheTesting.idl new file mode 100644 index 0000000000..15704f7caa --- /dev/null +++ b/netwerk/cache2/nsICacheTesting.idl @@ -0,0 +1,20 @@ +/* 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 "nsISupports.idl" + +interface nsIObserver; + +/** + * This is an internal interface used only for testing purposes. + * + * THIS IS NOT AN API TO BE USED BY EXTENSIONS! ONLY USED BY MOZILLA TESTS. + */ +[scriptable, builtinclass, uuid(4e8ba935-92e1-4a74-944b-b1a2f02a7480)] +interface nsICacheTesting : nsISupports +{ + void suspendCacheIOThread(in uint32_t aLevel); + void resumeCacheIOThread(); + void flush(in nsIObserver aObserver); +}; -- cgit v1.2.3