diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /netwerk/cache2 | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | uxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz |
Add m-esr52 at 52.6.0
Diffstat (limited to 'netwerk/cache2')
48 files changed, 27600 insertions, 0 deletions
diff --git a/netwerk/cache2/AppCacheStorage.cpp b/netwerk/cache2/AppCacheStorage.cpp new file mode 100644 index 0000000000..c550b272d4 --- /dev/null +++ b/netwerk/cache2/AppCacheStorage.cpp @@ -0,0 +1,177 @@ +/* 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 "AppCacheStorage.h" +#include "CacheStorageService.h" + +#include "OldWrappers.h" + +#include "nsICacheEntryDoomCallback.h" + +#include "nsCacheService.h" +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheService.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS_INHERITED0(AppCacheStorage, CacheStorage) + +AppCacheStorage::AppCacheStorage(nsILoadContextInfo* aInfo, + nsIApplicationCache* aAppCache) +: CacheStorage(aInfo, true /* disk */, false /* lookup app cache */, false /* skip size check */, false /* pin */) +, mAppCache(aAppCache) +{ + MOZ_COUNT_CTOR(AppCacheStorage); +} + +AppCacheStorage::~AppCacheStorage() +{ + ProxyReleaseMainThread(mAppCache); + MOZ_COUNT_DTOR(AppCacheStorage); +} + +NS_IMETHODIMP AppCacheStorage::AsyncOpenURI(nsIURI *aURI, + const nsACString & aIdExtension, + uint32_t aFlags, + nsICacheEntryOpenCallback *aCallback) +{ + if (!CacheStorageService::Self()) + return NS_ERROR_NOT_INITIALIZED; + + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv; + + nsCOMPtr<nsIApplicationCache> appCache = mAppCache; + + if (!appCache) { + rv = ChooseApplicationCache(aURI, getter_AddRefs(appCache)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (!appCache) { + LOG(("AppCacheStorage::AsyncOpenURI entry not found in any appcache, giving up")); + aCallback->OnCacheEntryAvailable(nullptr, false, nullptr, NS_ERROR_CACHE_KEY_NOT_FOUND); + return NS_OK; + } + + nsCOMPtr<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString cacheKey; + rv = noRefURI->GetAsciiSpec(cacheKey); + NS_ENSURE_SUCCESS(rv, rv); + + // This is the only way how to recognize appcache data by the anonymous + // flag. There is no way to switch to e.g. a different session, because + // there is just a single session for an appcache version (identified + // by the client id). + if (LoadInfo()->IsAnonymous()) { + cacheKey = NS_LITERAL_CSTRING("anon&") + cacheKey; + } + + nsAutoCString scheme; + rv = noRefURI->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<_OldCacheLoad> appCacheLoad = + new _OldCacheLoad(scheme, cacheKey, aCallback, appCache, + LoadInfo(), WriteToDisk(), aFlags); + rv = appCacheLoad->Start(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP AppCacheStorage::OpenTruncate(nsIURI *aURI, const nsACString & aIdExtension, + nsICacheEntry **aCacheEntry) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP AppCacheStorage::Exists(nsIURI *aURI, const nsACString & aIdExtension, + bool *aResult) +{ + *aResult = false; + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP AppCacheStorage::AsyncDoomURI(nsIURI *aURI, const nsACString & aIdExtension, + nsICacheEntryDoomCallback* aCallback) +{ + if (!CacheStorageService::Self()) + return NS_ERROR_NOT_INITIALIZED; + + if (!mAppCache) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<_OldStorage> old = new _OldStorage( + LoadInfo(), WriteToDisk(), LookupAppCache(), true, mAppCache); + return old->AsyncDoomURI(aURI, aIdExtension, aCallback); +} + +NS_IMETHODIMP AppCacheStorage::AsyncEvictStorage(nsICacheEntryDoomCallback* aCallback) +{ + if (!CacheStorageService::Self()) + return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + if (!mAppCache) { + // Discard everything under this storage context + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appCacheService->Evict(LoadInfo()); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Discard the group + RefPtr<_OldStorage> old = new _OldStorage( + LoadInfo(), WriteToDisk(), LookupAppCache(), true, mAppCache); + rv = old->AsyncEvictStorage(aCallback); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + if (aCallback) + aCallback->OnCacheEntryDoomed(NS_OK); + + return NS_OK; +} + +NS_IMETHODIMP AppCacheStorage::AsyncVisitStorage(nsICacheStorageVisitor* aVisitor, + bool aVisitEntries) +{ + if (!CacheStorageService::Self()) + return NS_ERROR_NOT_INITIALIZED; + + LOG(("AppCacheStorage::AsyncVisitStorage [this=%p, cb=%p]", this, aVisitor)); + + nsresult rv; + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<_OldVisitCallbackWrapper> cb = new _OldVisitCallbackWrapper( + "offline", aVisitor, aVisitEntries, LoadInfo()); + rv = nsCacheService::GlobalInstance()->VisitEntriesInternal(cb); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/AppCacheStorage.h b/netwerk/cache2/AppCacheStorage.h new file mode 100644 index 0000000000..eef70dea08 --- /dev/null +++ b/netwerk/cache2/AppCacheStorage.h @@ -0,0 +1,37 @@ +/* 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 AppCacheStorage__h__ +#define AppCacheStorage__h__ + +#include "CacheStorage.h" + +#include "nsCOMPtr.h" +#include "nsILoadContextInfo.h" +#include "nsIApplicationCache.h" + +class nsIApplicationCache; + +namespace mozilla { +namespace net { + +class AppCacheStorage : public CacheStorage +{ + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICACHESTORAGE + +public: + AppCacheStorage(nsILoadContextInfo* aInfo, + nsIApplicationCache* aAppCache); + +private: + virtual ~AppCacheStorage(); + + nsCOMPtr<nsIApplicationCache> mAppCache; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheEntry.cpp b/netwerk/cache2/CacheEntry.cpp new file mode 100644 index 0000000000..b79bf73734 --- /dev/null +++ b/netwerk/cache2/CacheEntry.cpp @@ -0,0 +1,1920 @@ +/* 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 "CacheEntry.h" +#include "CacheStorageService.h" +#include "CacheObserver.h" +#include "CacheFileUtils.h" +#include "CacheIndex.h" + +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISeekableStream.h" +#include "nsIURI.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheStorage.h" +#include "nsISerializable.h" +#include "nsIStreamTransportService.h" +#include "nsISizeOf.h" + +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsString.h" +#include "nsProxyRelease.h" +#include "nsSerializationHelper.h" +#include "nsThreadUtils.h" +#include "mozilla/Telemetry.h" +#include <math.h> +#include <algorithm> + +namespace mozilla { +namespace 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) +{ + MOZ_COUNT_CTOR(CacheEntryHandle); + +#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)); +} + +CacheEntryHandle::~CacheEntryHandle() +{ + mEntry->ReleaseHandleRef(); + mEntry->OnHandleClosed(this); + + MOZ_COUNT_DTOR(CacheEntryHandle); +} + +// CacheEntry::Callback + +CacheEntry::Callback::Callback(CacheEntry* aEntry, + nsICacheEntryOpenCallback *aCallback, + bool aReadOnly, bool aCheckOnAnyThread, + bool aSecret) +: mEntry(aEntry) +, mCallback(aCallback) +, mTargetThread(do_GetCurrentThread()) +, 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 == true) +, mDoomWhenFoundNonPinned(aDoomWhenFoundInPinStatus == false) +{ + MOZ_COUNT_CTOR(CacheEntry::Callback); + MOZ_ASSERT(mEntry->HandlesCount()); + mEntry->AddHandleRef(); +} + +CacheEntry::Callback::Callback(CacheEntry::Callback const &aThat) +: mEntry(aThat.mEntry) +, mCallback(aThat.mCallback) +, mTargetThread(aThat.mTargetThread) +, 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(mCallback, mTargetThread); + + mEntry->ReleaseHandleRef(); + MOZ_COUNT_DTOR(CacheEntry::Callback); +} + +void CacheEntry::Callback::ExchangeEntry(CacheEntry* aEntry) +{ + 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; +} + +bool CacheEntry::Callback::DeferDoom(bool *aDoom) const +{ + 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 mTargetThread->IsOnCurrentThread(aOnCheckThread); + } + + // We can invoke check anywhere + *aOnCheckThread = true; + return NS_OK; +} + +nsresult CacheEntry::Callback::OnAvailThread(bool *aOnAvailThread) const +{ + return mTargetThread->IsOnCurrentThread(aOnAvailThread); +} + +// CacheEntry + +NS_IMPL_ISUPPORTS(CacheEntry, + nsICacheEntry, + nsIRunnable, + CacheFileListener) + +CacheEntry::CacheEntry(const nsACString& aStorageID, + const nsACString& aURI, + const nsACString& aEnhanceID, + bool aUseDisk, + bool aSkipSizeCheck, + bool aPin) +: mFrecency(0) +, mSortingExpirationTime(uint32_t(-1)) +, mLock("CacheEntry") +, mFileStatus(NS_ERROR_NOT_INITIALIZED) +, mURI(aURI) +, mEnhanceID(aEnhanceID) +, mStorageID(aStorageID) +, mUseDisk(aUseDisk) +, mSkipSizeCheck(aSkipSizeCheck) +, mIsDoomed(false) +, mSecurityInfoLoaded(false) +, mPreventCallbacks(false) +, mHasData(false) +, mPinned(aPin) +, mPinningKnown(false) +, mState(NOTLOADED) +, mRegistration(NEVERREGISTERED) +, mWriter(nullptr) +, mPredictedDataSize(0) +, mUseCount(0) +{ + 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(EmptyCString(), mEnhanceID, mURI, aResult); +} + +// static +nsresult CacheEntry::HashingKey(nsCSubstring const& aStorageID, + nsCSubstring const& 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(nsCSubstring const& aStorageID, + nsCSubstring const& aEnhanceID, + nsCSubstring const& 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) +{ + LOG(("CacheEntry::AsyncOpen [this=%p, state=%s, flags=%d, callback=%p]", + this, StateString(mState), aFlags, aCallback)); + + 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; + + MOZ_ASSERT(!readonly || !truncate, "Bad flags combination"); + MOZ_ASSERT(!(truncate && mState > LOADING), "Must not call truncate on already loaded entry"); + + 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%08x, new=%d]", + this, 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", 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<DoomCallbackRunnable> event = + new DoomCallbackRunnable(this, aResult); + NS_DispatchToMainThread(event); + } + + return NS_OK; +} + +already_AddRefed<CacheEntryHandle> 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<CacheEntryHandle> handle; + RefPtr<CacheEntry> 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, + true, // truncate existing (this one) + getter_AddRefs(handle)); + + if (NS_SUCCEEDED(rv)) { + newEntry = handle->Entry(); + LOG((" exchanged entry %p by entry %p, rv=0x%08x", this, newEntry.get(), rv)); + newEntry->AsyncOpen(aCallback, nsICacheStorage::OPEN_TRUNCATE); + } else { + LOG((" exchanged of entry %p failed, rv=0x%08x", this, 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<CacheEntryHandle> writeHandle = + newEntry->NewWriteHandle(); + return writeHandle.forget(); +} + +void CacheEntry::TransferCallbacks(CacheEntry & aFromEntry) +{ + mozilla::MutexAutoLock lock(mLock); + + 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 (unfortunatelly, 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<CacheEntryHandle> 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].mTargetThread->Dispatch(NewRunnableMethod(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<size_t>(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) +{ + LOG(("CacheEntry::InvokeCallback [this=%p, state=%s, cb=%p]", + this, StateString(mState), aCallback.mCallback.get())); + + mLock.AssertCurrentThreadOwns(); + + // 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); + + nsresult rv = aCallback.mCallback->OnCacheEntryCheck( + this, nullptr, &checkResult); + LOG((" OnCacheEntryCheck: rv=0x%08x, result=%d", rv, 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) +{ + 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)); + + nsresult rv; + + uint32_t const state = mState; + + // 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<AvailableCallbackRunnable> event = + new AvailableCallbackRunnable(this, aCallback); + + rv = aCallback.mTargetThread->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); + LOG((" redispatched, (rv = 0x%08x)", 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, nullptr, 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<CacheEntryHandle> handle = NewHandle(); + aCallback.mCallback->OnCacheEntryAvailable( + handle, false, nullptr, 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, nullptr, 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<CacheEntryHandle> handle = NewWriteHandle(); + rv = aCallback.mCallback->OnCacheEntryAvailable( + handle, state == WRITING, nullptr, NS_OK); + + if (NS_FAILED(rv)) { + LOG((" writing/revalidating failed (0x%08x)", 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) +{ + LOG(("CacheEntry::OnHandleClosed [this=%p, state=%s, handle=%p]", this, StateString(mState), aHandle)); + + mozilla::MutexAutoLock lock(mLock); + + if (IsDoomed() && 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 + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP CacheEntry::GetKey(nsACString & aKey) +{ + aKey.Assign(mURI); + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::GetFetchCount(int32_t *aFetchCount) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetFetchCount(reinterpret_cast<uint32_t*>(aFetchCount)); +} + +NS_IMETHODIMP CacheEntry::GetLastFetched(uint32_t *aLastFetched) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastFetched(aLastFetched); +} + +NS_IMETHODIMP CacheEntry::GetLastModified(uint32_t *aLastModified) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetLastModified(aLastModified); +} + +NS_IMETHODIMP CacheEntry::GetExpirationTime(uint32_t *aExpirationTime) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetExpirationTime(aExpirationTime); +} + +NS_IMETHODIMP CacheEntry::GetIsForcedValid(bool *aIsForcedValid) +{ + NS_ENSURE_ARG(aIsForcedValid); + + MOZ_ASSERT(mState > LOADING); + + 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; +} + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP CacheEntry::OpenInputStream(int64_t offset, nsIInputStream * *_retval) +{ + LOG(("CacheEntry::OpenInputStream [this=%p]", this)); + return OpenInputStreamInternal(offset, nullptr, _retval); +} + +NS_IMETHODIMP 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<CacheEntryHandle> selfHandle = NewHandle(); + + nsCOMPtr<nsIInputStream> 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<nsISeekableStream> 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; +} + +NS_IMETHODIMP CacheEntry::OpenOutputStream(int64_t offset, nsIOutputStream * *_retval) +{ + LOG(("CacheEntry::OpenOutputStream [this=%p]", this)); + + nsresult rv; + + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState > EMPTY); + + 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; +} + +NS_IMETHODIMP CacheEntry::OpenAlternativeOutputStream(const nsACString & type, nsIOutputStream * *_retval) +{ + LOG(("CacheEntry::OpenAlternativeOutputStream [this=%p, type=%s]", this, + PromiseFlatCString(type).get())); + + nsresult rv; + + mozilla::MutexAutoLock lock(mLock); + + if (!mHasData || mState < READY || mOutputStream || mIsDoomed) { + LOG((" entry not in state to write alt-data")); + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIOutputStream> 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<CacheOutputCloseListener> listener = + new CacheOutputCloseListener(this); + + nsCOMPtr<nsIOutputStream> stream; + rv = mFile->OpenOutputStream(listener, getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISeekableStream> 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; +} + +NS_IMETHODIMP CacheEntry::GetPredictedDataSize(int64_t *aPredictedDataSize) +{ + *aPredictedDataSize = mPredictedDataSize; + return NS_OK; +} +NS_IMETHODIMP CacheEntry::SetPredictedDataSize(int64_t aPredictedDataSize) +{ + mPredictedDataSize = aPredictedDataSize; + + if (!mSkipSizeCheck && CacheObserver::EntryIsTooBig(mPredictedDataSize, mUseDisk)) { + LOG(("CacheEntry::SetPredictedDataSize [this=%p] too big, dooming", this)); + AsyncDoom(nullptr); + + return NS_ERROR_FILE_TOO_BIG; + } + + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::GetSecurityInfo(nsISupports * *aSecurityInfo) +{ + { + mozilla::MutexAutoLock lock(mLock); + if (mSecurityInfoLoaded) { + NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo); + return NS_OK; + } + } + + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + nsXPIDLCString info; + nsCOMPtr<nsISupports> secInfo; + nsresult rv; + + rv = mFile->GetElement("security-info", getter_Copies(info)); + NS_ENSURE_SUCCESS(rv, rv); + + if (info) { + rv = NS_DeserializeObject(info, getter_AddRefs(secInfo)); + NS_ENSURE_SUCCESS(rv, rv); + } + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo.swap(secInfo); + mSecurityInfoLoaded = true; + + NS_IF_ADDREF(*aSecurityInfo = mSecurityInfo); + } + + return NS_OK; +} +NS_IMETHODIMP CacheEntry::SetSecurityInfo(nsISupports *aSecurityInfo) +{ + nsresult rv; + + NS_ENSURE_SUCCESS(mFileStatus, mFileStatus); + + { + mozilla::MutexAutoLock lock(mLock); + + mSecurityInfo = aSecurityInfo; + mSecurityInfoLoaded = true; + } + + nsCOMPtr<nsISerializable> serializable = + do_QueryInterface(aSecurityInfo); + if (aSecurityInfo && !serializable) + return NS_ERROR_UNEXPECTED; + + nsCString info; + if (serializable) { + rv = NS_SerializeToString(serializable, info); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mFile->SetElement("security-info", info.Length() ? info.get() : nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP CacheEntry::GetMetaDataElement(const char * aKey, char * *aRetval) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->GetElement(aKey, aRetval); +} + +NS_IMETHODIMP CacheEntry::SetMetaDataElement(const char * aKey, const char * aValue) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->SetElement(aKey, aValue); +} + +NS_IMETHODIMP CacheEntry::VisitMetaData(nsICacheEntryMetaDataVisitor *aVisitor) +{ + NS_ENSURE_SUCCESS(mFileStatus, NS_ERROR_NOT_AVAILABLE); + + return mFile->VisitMetaData(aVisitor); +} + +NS_IMETHODIMP 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; +} + +NS_IMETHODIMP CacheEntry::SetValid() +{ + LOG(("CacheEntry::SetValid [this=%p, state=%s]", this, StateString(mState))); + + nsCOMPtr<nsIOutputStream> outputStream; + + { + mozilla::MutexAutoLock lock(mLock); + + MOZ_ASSERT(mState > EMPTY); + + mState = READY; + mHasData = true; + + InvokeCallbacks(); + + outputStream.swap(mOutputStream); + } + + if (outputStream) { + LOG((" abandoning phantom output stream")); + outputStream->Close(); + } + + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::Recreate(bool aMemoryOnly, + nsICacheEntry **_retval) +{ + LOG(("CacheEntry::Recreate [this=%p, state=%s]", this, StateString(mState))); + + mozilla::MutexAutoLock lock(mLock); + + RefPtr<CacheEntryHandle> handle = ReopenTruncated(aMemoryOnly, nullptr); + if (handle) { + handle.forget(_retval); + return NS_OK; + } + + BackgroundOp(Ops::CALLBACKS, true); + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP 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=%lld", *aDataSize)); + return NS_OK; +} + + +NS_IMETHODIMP CacheEntry::GetAltDataSize(int64_t *aDataSize) +{ + LOG(("CacheEntry::GetAltDataSize [this=%p]", this)); + if (NS_FAILED(mFileStatus)) { + return mFileStatus; + } + return mFile->GetAltDataSize(aDataSize); +} + + +NS_IMETHODIMP CacheEntry::MarkValid() +{ + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::MaybeMarkValid() +{ + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::HasWriteAccess(bool aWriteAllowed, bool *aWriteAccess) +{ + *aWriteAccess = aWriteAllowed; + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::Close() +{ + // NOT IMPLEMENTED ACTUALLY + return NS_OK; +} + +NS_IMETHODIMP CacheEntry::GetDiskStorageSizeInKB(uint32_t *aDiskStorageSize) +{ + if (NS_FAILED(mFileStatus)) { + return NS_ERROR_NOT_AVAILABLE; + } + + return mFile->GetDiskStorageSizeInKB(aDiskStorageSize); +} + +// 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", 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; + } + } + + 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<double>(PR_USEC_PER_SEC); + + double now_decay = static_cast<double>(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<double>(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) +: mEntry(aEntry) +{ + MOZ_COUNT_CTOR(CacheOutputCloseListener); +} + +CacheOutputCloseListener::~CacheOutputCloseListener() +{ + MOZ_COUNT_DTOR(CacheOutputCloseListener); +} + +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. + NS_DispatchToCurrentThread(this); +} + +NS_IMETHODIMP CacheOutputCloseListener::Run() +{ + mEntry->OnOutputClosed(); + return NS_OK; +} + +// Memory reporting + +size_t CacheEntry::SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + size_t n = 0; + + 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 unfortunatelly cannot be reported. Handles are + // small, though. + // mSecurityInfo doesn't impl nsISizeOf. + + return n; +} + +size_t CacheEntry::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheEntry.h b/netwerk/cache2/CacheEntry.h new file mode 100644 index 0000000000..7331be2a4f --- /dev/null +++ b/netwerk/cache2/CacheEntry.h @@ -0,0 +1,418 @@ +/* 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 "nsCOMPtr.h" +#include "nsRefPtrHashtable.h" +#include "nsDataHashtable.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) +{ + PRTime usec_per_sec = PR_USEC_PER_SEC; + return uint32_t(t_usec /= 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 nsICacheEntry + , public nsIRunnable + , public CacheFileListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHEENTRY + NS_DECL_NSIRUNNABLE + + 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(); + +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; + 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(nsCSubstring const& aStorageID, + nsCSubstring const& aEnhanceID, + nsIURI* aURI, + nsACString &aResult); + + static nsresult HashingKey(nsCSubstring const& aStorageID, + nsCSubstring const& aEnhanceID, + nsCSubstring const& aURISpec, + nsACString &aResult); + + // Accessed only on the service management thread + double mFrecency; + ::mozilla::Atomic<uint32_t, ::mozilla::Relaxed> mSortingExpirationTime; + + // Memory reporting + size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const; + +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<CacheStorageService> 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); + + // Returns true when an entry is about to be "defer" doomed and this is + // a "defer" callback. + 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<CacheEntry> mEntry; + nsCOMPtr<nsICacheEntryOpenCallback> mCallback; + nsCOMPtr<nsIThread> mTargetThread; + 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) + : mEntry(aEntry) + , mCallback(aCallback) + {} + + private: + NS_IMETHOD Run() override + { + mEntry->InvokeAvailableCallback(mCallback); + return NS_OK; + } + + RefPtr<CacheEntry> 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) + : mEntry(aEntry), mRv(aRv) {} + + private: + NS_IMETHOD Run() override + { + nsCOMPtr<nsICacheEntryDoomCallback> callback; + { + mozilla::MutexAutoLock lock(mEntry->mLock); + mEntry->mDoomCallback.swap(callback); + } + + if (callback) + callback->OnCacheEntryDoomed(mRv); + return NS_OK; + } + + RefPtr<CacheEntry> 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); + 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() { ++mHandlesCount; } + void ReleaseHandleRef() { --mHandlesCount; } + // Current number of handles keeping this entry. + uint32_t HandlesCount() const { 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(); + // When this entry is doomed the first time, this method removes + // any force-valid timing info for this entry. + void RemoveForcedValidity(); + + already_AddRefed<CacheEntryHandle> ReopenTruncated(bool aMemoryOnly, + nsICacheEntryOpenCallback* aCallback); + void TransferCallbacks(CacheEntry & aFromEntry); + + mozilla::Mutex mLock; + + // Reflects the number of existing handles for this entry + ::mozilla::ThreadSafeAutoRefCnt mHandlesCount; + + nsTArray<Callback> mCallbacks; + nsCOMPtr<nsICacheEntryDoomCallback> mDoomCallback; + + RefPtr<CacheFile> 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(). + ::mozilla::Atomic<nsresult, ::mozilla::ReleaseAcquire> mFileStatus; + nsCString mURI; + nsCString mEnhanceID; + nsCString 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(). + bool mIsDoomed; + + // Following flags are all synchronized with the cache entry lock. + + // Whether security info has already been looked up in metadata. + bool mSecurityInfoLoaded : 1; + // Prevents any callback invocation + bool mPreventCallbacks : 1; + // 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; + // The indication of pinning this entry was open with + bool mPinned : 1; + // Whether the pinning state of the entry is known (equals to the actual state + // of the cache file) + bool mPinningKnown : 1; + + 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; + + 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; + + // 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<nsIOutputStream> mOutputStream; + + // 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; + + // 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() : mFlags(0) { } + 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; + } mBackgroundOperations; + + nsCOMPtr<nsISupports> mSecurityInfo; + int64_t mPredictedDataSize; + mozilla::TimeStamp mLoadStart; + uint32_t mUseCount; +}; + + +class CacheEntryHandle : public nsICacheEntry +{ +public: + explicit CacheEntryHandle(CacheEntry* aEntry); + CacheEntry* Entry() const { return mEntry; } + + NS_DECL_THREADSAFE_ISUPPORTS + NS_FORWARD_NSICACHEENTRY(mEntry->) +private: + virtual ~CacheEntryHandle(); + RefPtr<CacheEntry> mEntry; +}; + + +class CacheOutputCloseListener final : public Runnable +{ +public: + void OnOutputClosed(); + +private: + friend class CacheEntry; + + virtual ~CacheOutputCloseListener(); + + NS_DECL_NSIRUNNABLE + explicit CacheOutputCloseListener(CacheEntry* aEntry); + +private: + RefPtr<CacheEntry> mEntry; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFile.cpp b/netwerk/cache2/CacheFile.cpp new file mode 100644 index 0000000000..fa0a893823 --- /dev/null +++ b/netwerk/cache2/CacheFile.cpp @@ -0,0 +1,2377 @@ +/* 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 "CacheFile.h" + +#include "CacheFileChunk.h" +#include "CacheFileInputStream.h" +#include "CacheFileOutputStream.h" +#include "nsThreadUtils.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Move.h" +#include <algorithm> +#include "nsComponentManagerUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Telemetry.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 { +namespace net { + +class NotifyCacheFileListenerEvent : public Runnable { +public: + NotifyCacheFileListenerEvent(CacheFileListener *aCallback, + nsresult aResult, + bool aIsNew) + : mCallback(aCallback) + , mRV(aResult) + , mIsNew(aIsNew) + { + LOG(("NotifyCacheFileListenerEvent::NotifyCacheFileListenerEvent() " + "[this=%p]", this)); + MOZ_COUNT_CTOR(NotifyCacheFileListenerEvent); + } + +protected: + ~NotifyCacheFileListenerEvent() + { + LOG(("NotifyCacheFileListenerEvent::~NotifyCacheFileListenerEvent() " + "[this=%p]", this)); + MOZ_COUNT_DTOR(NotifyCacheFileListenerEvent); + } + +public: + NS_IMETHOD Run() override + { + LOG(("NotifyCacheFileListenerEvent::Run() [this=%p]", this)); + + mCallback->OnFileReady(mRV, mIsNew); + return NS_OK; + } + +protected: + nsCOMPtr<CacheFileListener> mCallback; + nsresult mRV; + bool mIsNew; +}; + +class NotifyChunkListenerEvent : public Runnable { +public: + NotifyChunkListenerEvent(CacheFileChunkListener *aCallback, + nsresult aResult, + uint32_t aChunkIdx, + CacheFileChunk *aChunk) + : mCallback(aCallback) + , mRV(aResult) + , mChunkIdx(aChunkIdx) + , mChunk(aChunk) + { + LOG(("NotifyChunkListenerEvent::NotifyChunkListenerEvent() [this=%p]", + this)); + MOZ_COUNT_CTOR(NotifyChunkListenerEvent); + } + +protected: + ~NotifyChunkListenerEvent() + { + LOG(("NotifyChunkListenerEvent::~NotifyChunkListenerEvent() [this=%p]", + this)); + MOZ_COUNT_DTOR(NotifyChunkListenerEvent); + } + +public: + NS_IMETHOD Run() override + { + LOG(("NotifyChunkListenerEvent::Run() [this=%p]", this)); + + mCallback->OnChunkAvailable(mRV, mChunkIdx, mChunk); + return NS_OK; + } + +protected: + nsCOMPtr<CacheFileChunkListener> mCallback; + nsresult mRV; + uint32_t mChunkIdx; + RefPtr<CacheFileChunk> mChunk; +}; + + +class DoomFileHelper : public CacheFileIOListener +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit DoomFileHelper(CacheFileListener *aListener) + : mListener(aListener) + { + MOZ_COUNT_CTOR(DoomFileHelper); + } + + + 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() + { + MOZ_COUNT_DTOR(DoomFileHelper); + } + + nsCOMPtr<CacheFileListener> 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_THREADSAFE + +CacheFile::CacheFile() + : mLock("CacheFile.mLock") + , mOpeningFile(false) + , mReady(false) + , mMemoryOnly(false) + , mSkipSizeCheck(false) + , mOpenAsMemoryOnly(false) + , mPinned(false) + , mPriority(false) + , mDataAccessed(false) + , mDataIsDirty(false) + , mWritingMetadata(false) + , mPreloadWithoutInputStreams(true) + , mPreloadChunkCount(0) + , mStatus(NS_OK) + , mDataSize(-1) + , mAltDataOffset(-1) + , mKill(false) + , mOutput(nullptr) +{ + LOG(("CacheFile::CacheFile() [this=%p]", this)); +} + +CacheFile::~CacheFile() +{ + LOG(("CacheFile::~CacheFile() [this=%p]", this)); + + MutexAutoLock lock(mLock); + 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_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); + mReady = true; + mDataSize = mMetadata->Offset(); + return NS_OK; + } + else { + 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); + 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); + mReady = true; + mDataSize = mMetadata->Offset(); + + RefPtr<NotifyCacheFileListenerEvent> 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; +} + +nsresult +CacheFile::OnChunkRead(nsresult aResult, CacheFileChunk *aChunk) +{ + CacheFileAutoLock lock(this); + + nsresult rv; + + uint32_t index = aChunk->Index(); + + LOG(("CacheFile::OnChunkRead() [this=%p, rv=0x%08x, chunk=%p, idx=%u]", + this, aResult, aChunk, index)); + + 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<CacheFileChunk> deactivateChunkAgain; + + CacheFileAutoLock lock(this); + + nsresult rv; + + LOG(("CacheFile::OnChunkWritten() [this=%p, rv=0x%08x, chunk=%p, idx=%u]", + this, aResult, aChunk, aChunk->Index())); + + MOZ_ASSERT(!mMemoryOnly); + MOZ_ASSERT(!mOpeningFile); + MOZ_ASSERT(mHandle); + + 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=%d]", 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) +{ + nsresult rv; + + // 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<CacheFileIOListener> mListener; + bool mAlreadyDoomed; + } autoDoom(aHandle); + + nsCOMPtr<CacheFileListener> 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%08x, handle=%p]", + this, 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); + 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(); + const RefPtr<CacheFileChunk>& chunk = iter.Data(); + + LOG(("CacheFile::OnFileOpened() - write [this=%p, idx=%u, chunk=%p]", + this, idx, chunk.get())); + + mChunks.Put(idx, 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<Derived>. + ReleaseOutsideLock(RefPtr<nsISupports>(chunk)); + + iter.Remove(); + } + + return NS_OK; + } + } + } + + if (listener) { + listener->OnFileReady(retval, isNew); + return NS_OK; + } + + MOZ_ASSERT(NS_SUCCEEDED(aResult)); + MOZ_ASSERT(!mMetadata); + MOZ_ASSERT(mListener); + + mMetadata = new CacheFileMetadata(mHandle, mKey); + + rv = mMetadata->ReadMetadata(this); + if (NS_FAILED(rv)) { + mListener.swap(listener); + listener->OnFileReady(rv, false); + } + + 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) +{ + MOZ_ASSERT(mListener); + + LOG(("CacheFile::OnMetadataRead() [this=%p, rv=0x%08x]", this, aResult)); + + bool isNew = false; + 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, nullptr)) || + (mAltDataOffset > mDataSize))) { + // alt-metadata cannot be parsed or alt-data offset is invalid + mMetadata->InitEmptyMetadata(); + isNew = true; + mAltDataOffset = -1; + mDataSize = 0; + } else { + CacheFileAutoLock lock(this); + PreloadChunks(0); + } + } + + InitIndexEntry(); + } + + nsCOMPtr<CacheFileListener> listener; + 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%08x]", this, 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<CacheFileListener> listener; + + { + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mListener); + + LOG(("CacheFile::OnFileDoomed() [this=%p, rv=0x%08x, handle=%p]", + this, 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%08x]", this, 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; + NS_ADDREF(*_retval = input); + return NS_OK; +} + +nsresult +CacheFile::OpenAlternativeInputStream(nsICacheEntry *aEntryHandle, + const char *aAltDataType, + nsIInputStream **_retval) +{ + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + nsresult rv; + + 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%08x]", this, 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; + } + + const char *altData = mMetadata->GetElement(CacheFileUtils::kAltDataKey); + MOZ_ASSERT(altData, "alt-metadata should exist but was not found!"); + if (NS_WARN_IF(!altData)) { + LOG(("CacheFile::OpenAlternativeInputStream() - alt-metadata not found but " + "alt-data exists according to mAltDataOffset! [this=%p, ]", this)); + return NS_ERROR_NOT_AVAILABLE; + } + + int64_t offset; + nsCString availableAltData; + rv = CacheFileUtils::ParseAlternativeDataInfo(altData, &offset, + &availableAltData); + if (NS_WARN_IF(NS_FAILED(rv))) { + MOZ_ASSERT(false, "alt-metadata unexpectedly failed to parse"); + LOG(("CacheFile::OpenAlternativeInputStream() - Cannot parse alternative " + "metadata! [this=%p]", this)); + return rv; + } + + if (availableAltData != aAltDataType) { + LOG(("CacheFile::OpenAlternativeInputStream() - Alternative data is of a " + "different type than requested [this=%p, availableType=%s, " + "requestedType=%s]", this, availableAltData.get(), aAltDataType)); + return NS_ERROR_NOT_AVAILABLE; + } + + // mAltDataOffset must be in sync with what is stored in metadata + MOZ_ASSERT(mAltDataOffset == offset); + + // 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; + NS_ADDREF(*_retval = input); + 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; + } + + // 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)) { + return rv; + } + mMetadata->SetElement(CacheFileUtils::kAltDataKey, nullptr); + mAltDataOffset = -1; + } + + // 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; + NS_ADDREF(*_retval = mOutput); + return NS_OK; +} + +nsresult +CacheFile::OpenAlternativeOutputStream(CacheOutputCloseListener *aCloseListener, + const char *aAltDataType, + nsIOutputStream **_retval) +{ + CacheFileAutoLock lock(this); + + MOZ_ASSERT(mHandle || mMemoryOnly || mOpeningFile); + + nsresult rv; + + 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; + } + + // 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) { + // Truncate old alt-data + rv = Truncate(mAltDataOffset); + if (NS_FAILED(rv)) { + return rv; + } + } else { + mAltDataOffset = mDataSize; + } + + nsAutoCString altMetadata; + CacheFileUtils::BuildAlternativeDataInfo(aAltDataType, mAltDataOffset, + altMetadata); + rv = mMetadata->SetElement(CacheFileUtils::kAltDataKey, altMetadata.get()); + if (NS_FAILED(rv)) { + // Removing element shouldn't fail because it doesn't allocate memory. + mMetadata->SetElement(CacheFileUtils::kAltDataKey, nullptr); + + mAltDataOffset = -1; + 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; + NS_ADDREF(*_retval = mOutput); + return NS_OK; +} + +nsresult +CacheFile::SetMemoryOnly() +{ + 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) +{ + 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<CacheFileIOListener> 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_strdup(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); + + return mMetadata->Visit(aVisitor); +} + +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(); + + if (mHandle && !mHandle->IsDoomed()) + CacheFileIOManager::UpdateIndexEntry(mHandle, nullptr, &aExpirationTime); + + return mMetadata->SetExpirationTime(aExpirationTime); +} + +nsresult +CacheFile::GetExpirationTime(uint32_t *_retval) +{ + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + return mMetadata->GetExpirationTime(_retval); +} + +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); + + return mMetadata->SetFrecency(aFrecency); +} + +nsresult +CacheFile::GetFrecency(uint32_t *_retval) +{ + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + return mMetadata->GetFrecency(_retval); +} + +nsresult +CacheFile::GetLastModified(uint32_t *_retval) +{ + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + return mMetadata->GetLastModified(_retval); +} + +nsresult +CacheFile::GetLastFetched(uint32_t *_retval) +{ + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + return mMetadata->GetLastFetched(_retval); +} + +nsresult +CacheFile::GetFetchCount(uint32_t *_retval) +{ + CacheFileAutoLock lock(this); + MOZ_ASSERT(mMetadata); + NS_ENSURE_TRUE(mMetadata, NS_ERROR_UNEXPECTED); + + return mMetadata->GetFetchCount(_retval); +} + +nsresult +CacheFile::GetDiskStorageSizeInKB(uint32_t *aDiskStorageSize) +{ + 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(); + + return mMetadata->OnFetched(); +} + +void +CacheFile::Lock() +{ + mLock.Lock(); +} + +void +CacheFile::Unlock() +{ + // move the elements out of mObjsToRelease + // so that they can be released after we unlock + nsTArray<RefPtr<nsISupports>> objs; + objs.SwapElements(mObjsToRelease); + + mLock.Unlock(); + +} + +void +CacheFile::AssertOwnsLock() const +{ + mLock.AssertCurrentThreadOwns(); +} + +void +CacheFile::ReleaseOutsideLock(RefPtr<nsISupports> aObject) +{ + AssertOwnsLock(); + + mObjsToRelease.AppendElement(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<CacheFileChunk> 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 { + rv = QueueChunkListener(aIndex, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + } + + 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.Put(aIndex, 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<int64_t>(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=%lld, mDataSize=%lld]", + this, off, mDataSize)); + + return NS_ERROR_UNEXPECTED; + } + + chunk = new CacheFileChunk(this, aIndex, aCaller == WRITER); + mChunks.Put(aIndex, 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<uint32_t>(mDataSize - off), + static_cast<uint32_t>(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) { + rv = QueueChunkListener(aIndex, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (preload) { + PreloadChunks(aIndex + 1); + } + + return NS_OK; + } else if (off == mDataSize) { + if (aCaller == WRITER) { + // this listener is going to write to the chunk + chunk = new CacheFileChunk(this, aIndex, true); + mChunks.Put(aIndex, 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 + rv = QueueChunkListener(aIndex, aCallback); + NS_ENSURE_SUCCESS(rv, rv); + } 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<int64_t>(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<CacheFileChunk> 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<int64_t>(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<int64_t>(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<CacheFileChunk> 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=%d]", 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<CacheFileChunkListener>(aChunk->mFile.forget()).forget()); + + DebugOnly<bool> 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<CacheFileChunk> 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%08x]", this, chunk.get(), 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%08x]", + this, chunk.get(), 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<CacheFileChunkListener>(aChunk->mFile.forget()).forget()); + + if (aCacheChunk) { + mCachedChunks.Put(aChunk->Index(), 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(); + + nsresult rv; + + MOZ_ASSERT(aOffset <= mDataSize); + MOZ_ASSERT(mReady); + + uint32_t lastChunk = 0; + + if (aOffset > 0) { + lastChunk = (mDataSize - 1) / kChunkSize; + } + + uint32_t bytesInLastChunk = aOffset - lastChunk * kChunkSize; + + for (auto iter = mCachedChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + + if (idx > lastChunk) { + // This is unused chunk, simply remove it. + iter.Remove(); + continue; + } + + if (idx == lastChunk) { + RefPtr<CacheFileChunk>& chunk = iter.Data(); + + rv = chunk->Truncate(bytesInLastChunk); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + for (auto iter = mChunks.Iter(); !iter.Done(); iter.Next()) { + uint32_t idx = iter.Key(); + RefPtr<CacheFileChunk>& chunk = iter.Data(); + + if (idx > lastChunk) { + chunk->mDiscardedChunk = true; + mDiscardedChunks.AppendElement(chunk); + iter.Remove(); + continue; + } + + if (idx == lastChunk) { + RefPtr<CacheFileChunk>& chunk = iter.Data(); + + rv = chunk->Truncate(bytesInLastChunk); + if (NS_FAILED(rv)) { + return rv; + } + } + } + + 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_DISK_FULL: + 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 + } + + NS_NOTREACHED("We should never get here"); +} + +nsresult +CacheFile::RemoveInput(CacheFileInputStream *aInput, nsresult aStatus) +{ + CacheFileAutoLock lock(this); + + LOG(("CacheFile::RemoveInput() [this=%p, input=%p, status=0x%08x]", this, + aInput, aStatus)); + + DebugOnly<bool> found; + found = mInputs.RemoveElement(aInput); + MOZ_ASSERT(found); + + ReleaseOutsideLock(already_AddRefed<nsIInputStream>(static_cast<nsIInputStream*>(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)); + + return NS_OK; +} + +nsresult +CacheFile::RemoveOutput(CacheFileOutputStream *aOutput, nsresult aStatus) +{ + AssertOwnsLock(); + + LOG(("CacheFile::RemoveOutput() [this=%p, output=%p, status=0x%08x]", this, + aOutput, aStatus)); + + if (mOutput != aOutput) { + LOG(("CacheFile::RemoveOutput() - This output was already removed, ignoring" + " call [this=%p]", this)); + return NS_OK; + } + + 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) { + mStatus = aStatus; + } + + // Notify close listener as the last action + aOutput->NotifyCloseListener(); + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS, + StatusToTelemetryEnum(aStatus)); + + return NS_OK; +} + +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%08x, idx=%u, chunk=%p]", this, aCallback, aTarget, aResult, + aChunkIdx, aChunk)); + + nsresult rv; + RefPtr<NotifyChunkListenerEvent> ev; + ev = new NotifyChunkListenerEvent(aCallback, aResult, aChunkIdx, aChunk); + if (aTarget) + rv = aTarget->Dispatch(ev, NS_DISPATCH_NORMAL); + else + rv = NS_DispatchToCurrentThread(ev); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +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 = do_GetMainThread(); + } + item->mCallback = aCallback; + + ChunkListeners *listeners; + if (!mChunkListeners.Get(aIndex, &listeners)) { + listeners = new ChunkListeners(); + mChunkListeners.Put(aIndex, listeners); + } + + listeners->mItems.AppendElement(item); + return NS_OK; +} + +nsresult +CacheFile::NotifyChunkListeners(uint32_t aIndex, nsresult aResult, + CacheFileChunk *aChunk) +{ + LOG(("CacheFile::NotifyChunkListeners() [this=%p, idx=%u, rv=0x%08x, " + "chunk=%p]", this, aIndex, 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) +{ + 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(); + nsAutoPtr<ChunkListeners>& listeners = iter.Data(); + + LOG(("CacheFile::NotifyListenersAboutOutputRemoval() - fail " + "[this=%p, idx=%u]", this, idx)); + + RefPtr<CacheFileChunk> chunk; + mChunks.Get(idx, getter_AddRefs(chunk)); + if (chunk) { + MOZ_ASSERT(!chunk->IsReady()); + 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 (auto iter = mChunks.Iter(); !iter.Done(); iter.Next()) { + const RefPtr<CacheFileChunk>& chunk = iter.Data(); + LOG(("CacheFile::NotifyListenersAboutOutputRemoval() - fail2 " + "[this=%p, idx=%u]", this, iter.Key())); + + 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; +} + +bool +CacheFile::IsDoomed() +{ + CacheFileAutoLock lock(this); + + if (!mHandle) + return false; + + return mHandle->IsDoomed(); +} + +bool +CacheFile::IsWriteInProgress() +{ + // Returns true when there is a potentially unfinished write operation. + // Not using lock for performance reasons. mMetadata is never released + // during life time of CacheFile. + + bool result = false; + + if (!mMemoryOnly) { + result = mDataIsDirty || + (mMetadata && mMetadata->IsDirty()) || + mWritingMetadata; + } + + result = result || + mOpeningFile || + mOutput || + mChunks.Count(); + + return result; +} + +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<CacheFileChunk>& 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<CacheFileChunk> 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(chunk.forget()); + 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(chunk.forget()); + + return NS_OK; +} + +void +CacheFile::SetError(nsresult aStatus) +{ + AssertOwnsLock(); + + if (NS_SUCCEEDED(mStatus)) { + mStatus = aStatus; + if (mHandle) { + CacheFileIOManager::DoomFile(mHandle, nullptr); + } + } +} + +nsresult +CacheFile::InitIndexEntry() +{ + 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 expTime; + mMetadata->GetExpirationTime(&expTime); + + uint32_t frecency; + mMetadata->GetFrecency(&frecency); + + rv = CacheFileIOManager::UpdateIndexEntry(mHandle, &frecency, &expTime); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +size_t +CacheFile::SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + CacheFileAutoLock lock(const_cast<CacheFile*>(this)); + + size_t n = 0; + n += mKey.SizeOfExcludingThisIfUnshared(mallocSizeOf); + n += mChunks.ShallowSizeOfExcludingThis(mallocSizeOf); + for (auto iter = mChunks.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Data()->SizeOfIncludingThis(mallocSizeOf); + } + n += mCachedChunks.ShallowSizeOfExcludingThis(mallocSizeOf); + for (auto iter = mCachedChunks.ConstIter(); !iter.Done(); iter.Next()) { + n += iter.Data()->SizeOfIncludingThis(mallocSizeOf); + } + if (mMetadata) { + 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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFile.h b/netwerk/cache2/CacheFile.h new file mode 100644 index 0000000000..3c8f7e97d6 --- /dev/null +++ b/netwerk/cache2/CacheFile.h @@ -0,0 +1,272 @@ +/* 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 nsIInputStream; +class nsIOutputStream; +class nsICacheEntryMetaDataVisitor; + +namespace mozilla { +namespace net { + +class CacheFileInputStream; +class CacheFileOutputStream; +class CacheOutputCloseListener; +class MetadataWriteTimer; + +#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 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, nsIOutputStream **_retval); + NS_IMETHOD SetMemoryOnly(); + NS_IMETHOD Doom(CacheFileListener *aCallback); + + void Kill() { mKill = true; } + nsresult ThrowMemoryCachedData(); + + nsresult GetAltDataSize(int64_t *aSize); + + // 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 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) { aKey = mKey; } + bool IsDoomed(); + bool IsPinned() const { return mPinned; } + bool IsWriteInProgress(); + + // 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(); + void Unlock(); + void AssertOwnsLock() const; + void ReleaseOutsideLock(RefPtr<nsISupports> 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); + + nsresult RemoveInput(CacheFileInputStream *aInput, nsresult aStatus); + nsresult RemoveOutput(CacheFileOutputStream *aOutput, nsresult aStatus); + nsresult NotifyChunkListener(CacheFileChunkListener *aCallback, + nsIEventTarget *aTarget, + nsresult aResult, + uint32_t aChunkIdx, + CacheFileChunk *aChunk); + nsresult QueueChunkListener(uint32_t aIndex, + CacheFileChunkListener *aCallback); + nsresult NotifyChunkListeners(uint32_t aIndex, nsresult aResult, + CacheFileChunk *aChunk); + bool HaveChunkListeners(uint32_t aIndex); + void NotifyListenersAboutOutputRemoval(); + + bool IsDirty(); + void WriteMetadataIfNeeded(); + void WriteMetadataIfNeededLocked(bool aFireAndForget = false); + void PostWriteTimer(); + + void CleanUpCachedChunks(); + + nsresult PadChunkWithZeroes(uint32_t aChunkIdx); + + void SetError(nsresult aStatus); + + nsresult InitIndexEntry(); + + mozilla::Mutex mLock; + bool mOpeningFile; + bool mReady; + bool mMemoryOnly; + bool mSkipSizeCheck; + bool mOpenAsMemoryOnly; + bool mPinned; + bool mPriority; + bool mDataAccessed; + bool mDataIsDirty; + bool mWritingMetadata; + bool mPreloadWithoutInputStreams; + uint32_t mPreloadChunkCount; + nsresult mStatus; + int64_t mDataSize; // Size of the whole data including eventual + // alternative data represenation. + int64_t mAltDataOffset; // If there is alternative data present, it + // contains size of the original data, i.e. + // offset where alternative data starts. + // Otherwise it is -1. + nsCString mKey; + + RefPtr<CacheFileHandle> mHandle; + RefPtr<CacheFileMetadata> mMetadata; + nsCOMPtr<CacheFileListener> mListener; + nsCOMPtr<CacheFileIOListener> mDoomAfterOpenListener; + Atomic<bool, Relaxed> mKill; + + nsRefPtrHashtable<nsUint32HashKey, CacheFileChunk> mChunks; + nsClassHashtable<nsUint32HashKey, ChunkListeners> mChunkListeners; + nsRefPtrHashtable<nsUint32HashKey, CacheFileChunk> mCachedChunks; + // 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<RefPtr<CacheFileChunk> > mDiscardedChunks; + + nsTArray<CacheFileInputStream*> mInputs; + CacheFileOutputStream *mOutput; + + nsTArray<RefPtr<nsISupports>> mObjsToRelease; +}; + +class CacheFileAutoLock { +public: + explicit CacheFileAutoLock(CacheFile *aFile) + : mFile(aFile) + , mLocked(true) + { + mFile->Lock(); + } + ~CacheFileAutoLock() + { + if (mLocked) + mFile->Unlock(); + } + void Lock() + { + MOZ_ASSERT(!mLocked); + mFile->Lock(); + mLocked = true; + } + void Unlock() + { + MOZ_ASSERT(mLocked); + mFile->Unlock(); + mLocked = false; + } + +private: + RefPtr<CacheFile> 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..83d79f79fc --- /dev/null +++ b/netwerk/cache2/CacheFileChunk.cpp @@ -0,0 +1,932 @@ +/* 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" + +namespace mozilla { +namespace 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->mBufSize); + memcpy(mBuf + invalidOffset, aOther->mBuf + invalidOffset, invalidLength); + } + invalidOffset = validOffset + validLength; + } + + if (invalidOffset < aOther->mBufSize) { + invalidLength = aOther->mBufSize - invalidOffset; + memcpy(mBuf + invalidOffset, aOther->mBuf + invalidOffset, invalidLength); + } + + return NS_OK; +} + +MOZ_MUST_USE 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<char *>(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. + (mDataSize == 0 && 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<bool> 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) + : mCallback(aCallback) + , mChunk(aChunk) + { + LOG(("NotifyUpdateListenerEvent::NotifyUpdateListenerEvent() [this=%p]", + this)); + MOZ_COUNT_CTOR(NotifyUpdateListenerEvent); + } + +protected: + ~NotifyUpdateListenerEvent() + { + LOG(("NotifyUpdateListenerEvent::~NotifyUpdateListenerEvent() [this=%p]", + this)); + MOZ_COUNT_DTOR(NotifyUpdateListenerEvent); + } + +public: + NS_IMETHOD Run() override + { + LOG(("NotifyUpdateListenerEvent::Run() [this=%p]", this)); + + mCallback->OnChunkUpdated(mChunk); + return NS_OK; + } + +protected: + nsCOMPtr<CacheFileChunkListener> mCallback; + RefPtr<CacheFileChunk> mChunk; +}; + +bool +CacheFileChunk::DispatchRelease() +{ + if (NS_IsMainThread()) { + return false; + } + + NS_DispatchToMainThread(NewNonOwningRunnableMethod(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; + } + + NS_PRECONDITION(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_THREADSAFE + +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)); + MOZ_COUNT_CTOR(CacheFileChunk); + + mBuf = new CacheFileChunkBuffer(this); +} + +CacheFileChunk::~CacheFileChunk() +{ + LOG(("CacheFileChunk::~CacheFileChunk() [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileChunk); +} + +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<CacheFileChunkBuffer> 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 = new CacheFileChunkReadHandle(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(); + + 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 = do_GetMainThread(); + } + item->mCallback = aCallback; + MOZ_ASSERT(item->mTarget); + item->mCallback = aCallback; + + mUpdateListeners.AppendElement(item); +} + +nsresult +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 + + return NS_OK; +} + +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<NotifyUpdateListenerEvent> 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 +{ + AssertOwnsLock(); + + MOZ_ASSERT(!mListener); + 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(); + + // 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<int64_t>(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(); +} + +nsresult +CacheFileChunk::Truncate(uint32_t aOffset) +{ + mBuf->SetDataSize(aOffset); + return NS_OK; +} + +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%08x]", + this, aHandle, aResult)); + + nsCOMPtr<CacheFileChunkListener> 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%08x]", + this, aHandle, aResult)); + + nsCOMPtr<CacheFileChunkListener> 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<CacheFileChunkBuffer> 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->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 +{ + AssertOwnsLock(); + + return (NS_SUCCEEDED(mStatus) && (mState == READY || mState == WRITING)); +} + +bool +CacheFileChunk::IsDirty() const +{ + AssertOwnsLock(); + + return mIsDirty; +} + +nsresult +CacheFileChunk::GetStatus() +{ + AssertOwnsLock(); + + return mStatus; +} + +void +CacheFileChunk::SetError(nsresult aStatus) +{ + LOG(("CacheFileChunk::SetError() [this=%p, status=0x%08x]", this, 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<CacheFileChunkBuffer> 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)); + + uint32_t limit = CacheObserver::MaxDiskChunksMemoryUsage(mIsPriority); + if (limit == 0) { + return true; + } + + uint32_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<uint32_t>(ChunksMemoryUsage()), this)); +} + +mozilla::Atomic<uint32_t, ReleaseAcquire>& CacheFileChunk::ChunksMemoryUsage() const +{ + static mozilla::Atomic<uint32_t, ReleaseAcquire> chunksMemoryUsage(0); + static mozilla::Atomic<uint32_t, ReleaseAcquire> prioChunksMemoryUsage(0); + return mIsPriority ? prioChunksMemoryUsage : chunksMemoryUsage; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileChunk.h b/netwerk/cache2/CacheFileChunk.h new file mode 100644 index 0000000000..09acb62c5a --- /dev/null +++ b/netwerk/cache2/CacheFileChunk.h @@ -0,0 +1,252 @@ +/* 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 "nsAutoPtr.h" +#include "mozilla/Mutex.h" + +namespace mozilla { +namespace net { + +#define kChunkSize (256 * 1024) +#define kEmptyChunkHash 0x1826 + +class CacheFileChunk; +class CacheFile; + +class CacheFileChunkBuffer +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheFileChunkBuffer) + + explicit CacheFileChunkBuffer(CacheFileChunk *aChunk); + + nsresult EnsureBufSize(uint32_t aSize); + 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<CacheFileChunkBuffer> 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: + ChunkListenerItem() { MOZ_COUNT_CTOR(ChunkListenerItem); } + ~ChunkListenerItem() { MOZ_COUNT_DTOR(ChunkListenerItem); } + + nsCOMPtr<nsIEventTarget> mTarget; + nsCOMPtr<CacheFileChunkListener> mCallback; +}; + +class ChunkListeners { +public: + ChunkListeners() { MOZ_COUNT_CTOR(ChunkListeners); } + ~ChunkListeners() { MOZ_COUNT_DTOR(ChunkListeners); } + + nsTArray<ChunkListenerItem *> mItems; +}; + +class CacheFileChunk : 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); + nsresult 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); + nsresult Truncate(uint32_t aOffset); + + bool CanAllocate(uint32_t aSize) const; + void BuffersAllocationChanged(uint32_t aFreed, uint32_t aAllocated); + + mozilla::Atomic<uint32_t, ReleaseAcquire>& ChunksMemoryUsage() const; + + enum EState { + INITIAL = 0, + READING = 1, + WRITING = 2, + READY = 3 + }; + + uint32_t mIndex; + EState mState; + nsresult mStatus; + + Atomic<bool> 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<CacheFileChunkBuffer> mBuf; + + // We need to keep pointers of the old buffers for memory reporting. + nsTArray<RefPtr<CacheFileChunkBuffer>> mOldBufs; + + // Read handle that is used during writing the chunk to the disk. + nsAutoPtr<CacheFileChunkReadHandle> 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<CacheFileChunkBuffer> mReadingStateBuf; + CacheHash::Hash16_t mExpectedHash; + + RefPtr<CacheFile> mFile; // is null if chunk is cached to + // prevent reference cycles + nsCOMPtr<CacheFileChunkListener> mListener; + nsTArray<ChunkListenerItem *> 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..acf089b797 --- /dev/null +++ b/netwerk/cache2/CacheFileContextEvictor.cpp @@ -0,0 +1,663 @@ +/* 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 "CacheIndex.h" +#include "CacheIndexIterator.h" +#include "CacheFileUtils.h" +#include "nsIFile.h" +#include "LoadContextInfo.h" +#include "nsThreadUtils.h" +#include "nsString.h" +#include "nsISimpleEnumerator.h" +#include "nsIDirectoryEnumerator.h" +#include "mozilla/Base64.h" + + +namespace mozilla { +namespace net { + +#define CONTEXT_EVICTION_PREFIX "ce_" +const uint32_t kContextEvictionPrefixLength = + sizeof(CONTEXT_EVICTION_PREFIX) - 1; + +bool CacheFileContextEvictor::sDiskAlreadySearched = false; + +CacheFileContextEvictor::CacheFileContextEvictor() + : mEvicting(false) + , mIndexIsUpToDate(false) +{ + 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(NS_LITERAL_CSTRING(ENTRIES_DIR)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (!sDiskAlreadySearched) { + LoadEvictInfoFromDisk(); + if ((mEntries.Length() != 0) && mIndexIsUpToDate) { + CreateIterators(); + StartEvicting(); + } + } + + return NS_OK; +} + +uint32_t +CacheFileContextEvictor::ContextsCount() +{ + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + return mEntries.Length(); +} + +nsresult +CacheFileContextEvictor::AddContext(nsILoadContextInfo *aLoadContextInfo, + bool aPinned) +{ + 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) { + entry = mEntries[i]; + 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.RemoveElementAt(i); + } + } + } + + if (!entry) { + entry = new CacheFileContextEvictorEntry(); + entry->mInfo = aLoadContextInfo; + entry->mPinned = aPinned; + mEntries.AppendElement(entry); + } + + entry->mTimeStamp = PR_Now() / PR_USEC_PER_MSEC; + + PersistEvictionInfoToDisk(aLoadContextInfo, aPinned); + + 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%08x]", rv)); + mEntries.RemoveElement(entry); + return rv; + } + + StartEvicting(); + } + + return NS_OK; +} + +nsresult +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 NS_OK; + } + + if (!isUpToDate && !mIndexIsUpToDate) { + // Index is outdated and status has not changed, nothing to do. + return NS_OK; + } + + if (isUpToDate && mIndexIsUpToDate) { + // Status has not changed, but make sure the eviction is running. + if (mEvicting) { + return NS_OK; + } + + // 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(); + } + + return NS_OK; +} + +nsresult +CacheFileContextEvictor::WasEvicted(const nsACString &aKey, nsIFile *aFile, + bool *aEvictedAsPinned, bool *aEvictedAsNonPinned) +{ + LOG(("CacheFileContextEvictor::WasEvicted() [key=%s]", + PromiseFlatCString(aKey).get())); + + nsresult rv; + + *aEvictedAsPinned = false; + *aEvictedAsNonPinned = false; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr<nsILoadContextInfo> info = CacheFileUtils::ParseKey(aKey); + MOZ_ASSERT(info); + if (!info) { + LOG(("CacheFileContextEvictor::WasEvicted() - Cannot parse key!")); + return NS_OK; + } + + for (uint32_t i = 0; i < mEntries.Length(); ++i) { + CacheFileContextEvictorEntry *entry = mEntries[i]; + + if (entry->mInfo && !info->Equals(entry->mInfo)) { + continue; + } + + PRTime lastModifiedTime; + rv = aFile->GetLastModifiedTime(&lastModifiedTime); + if (NS_FAILED(rv)) { + LOG(("CacheFileContextEvictor::WasEvicted() - Cannot get last modified time" + ", returning false.")); + return NS_OK; + } + + if (lastModifiedTime > entry->mTimeStamp) { + // File has been modified since context eviction. + continue; + } + + LOG(("CacheFileContextEvictor::WasEvicted() - evicted [pinning=%d, " + "mTimeStamp=%lld, lastModifiedTime=%lld]", + entry->mPinned, entry->mTimeStamp, lastModifiedTime)); + + if (entry->mPinned) { + *aEvictedAsPinned = true; + } else { + *aEvictedAsNonPinned = true; + } + } + + return NS_OK; +} + +nsresult +CacheFileContextEvictor::PersistEvictionInfoToDisk( + nsILoadContextInfo *aLoadContextInfo, bool aPinned) +{ + LOG(("CacheFileContextEvictor::PersistEvictionInfoToDisk() [this=%p, " + "loadContextInfo=%p]", this, aLoadContextInfo)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr<nsIFile> file; + rv = GetContextFile(aLoadContextInfo, aPinned, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString path; + file->GetNativePath(path); + + 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%08x]", path.get(), 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) +{ + LOG(("CacheFileContextEvictor::RemoveEvictInfoFromDisk() [this=%p, " + "loadContextInfo=%p]", this, aLoadContextInfo)); + + nsresult rv; + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr<nsIFile> file; + rv = GetContextFile(aLoadContextInfo, aPinned, getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString path; + file->GetNativePath(path); + + rv = file->Remove(false); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("CacheFileContextEvictor::RemoveEvictionInfoFromDisk() - Removing file" + " failed! [path=%s, rv=0x%08x]", path.get(), 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<nsISimpleEnumerator> enumerator; + rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(enumerator)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIDirectoryEnumerator> dirEnum = do_QueryInterface(enumerator, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + while (true) { + nsCOMPtr<nsIFile> 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, NS_LITERAL_CSTRING(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); + } + + nsCOMPtr<nsILoadContextInfo> info; + if (!NS_LITERAL_CSTRING("*").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; + entry->mTimeStamp = lastModifiedTime; + mEntries.AppendElement(entry); + } + + return NS_OK; +} + +nsresult +CacheFileContextEvictor::GetContextFile(nsILoadContextInfo *aLoadContextInfo, + bool aPinned, + nsIFile **_retval) +{ + nsresult rv; + + nsAutoCString leafName; + leafName.AssignLiteral(CONTEXT_EVICTION_PREFIX); + + 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('*'); + } + + nsAutoCString data64; + rv = Base64Encode(keyPrefix, data64); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + // Replace '/' with '-' since '/' cannot be part of the filename. + data64.ReplaceChar('/', '-'); + + leafName.Append(data64); + + nsCOMPtr<nsIFile> 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%08x]", 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 evicintg.")); + return; + } + + if (mEntries.Length() == 0) { + LOG(("CacheFileContextEvictor::StartEvicting() - no context to evict.")); + return; + } + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod(this, &CacheFileContextEvictor::EvictEntries); + + RefPtr<CacheIOThread> 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%08x]", rv)); + } + + mEvicting = true; +} + +nsresult +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 NS_OK; + } + + 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 NS_OK; + } + + if (CacheIOThread::YieldAndRerun()) { + LOG(("CacheFileContextEvictor::EvictEntries() - Breaking loop for higher " + "level events.")); + mEvicting = true; + return NS_OK; + } + + 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 NS_OK; + } + + 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.RemoveElementAt(0); + continue; + } else 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<CacheFileHandle> 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; + rv = CacheIndex::HasEntry(hash, &status, &pinned); + // 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; + } + + nsAutoCString leafName; + CacheFileIOManager::HashToStr(&hash, leafName); + + PRTime lastModifiedTime; + nsCOMPtr<nsIFile> 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=%lld, lastModifiedTime=%lld]", mEntries[0]->mTimeStamp, + lastModifiedTime)); + continue; + } + + LOG(("CacheFileContextEvictor::EvictEntries - Removing entry.")); + file->Remove(false); + CacheIndex::RemoveEntry(&hash); + } + + NS_NOTREACHED("We should never get here"); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileContextEvictor.h b/netwerk/cache2/CacheFileContextEvictor.h new file mode 100644 index 0000000000..f5fd49ecae --- /dev/null +++ b/netwerk/cache2/CacheFileContextEvictor.h @@ -0,0 +1,96 @@ +/* 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 "nsTArray.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" + +class nsIFile; +class nsILoadContextInfo; + +namespace mozilla { +namespace net { + +class CacheIndexIterator; + +struct CacheFileContextEvictorEntry +{ + nsCOMPtr<nsILoadContextInfo> mInfo; + bool mPinned; + PRTime mTimeStamp; // in milliseconds + RefPtr<CacheIndexIterator> mIterator; +}; + +class CacheFileContextEvictor +{ +public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheFileContextEvictor) + + CacheFileContextEvictor(); + +private: + virtual ~CacheFileContextEvictor(); + +public: + nsresult Init(nsIFile *aCacheDirectory); + + // Returns number of contexts that are being evicted. + uint32_t ContextsCount(); + // Start evicting given context. + nsresult AddContext(nsILoadContextInfo *aLoadContextInfo, bool aPinned); + // 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. + nsresult 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. + nsresult 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); + // Once we are done with eviction for the given context, the eviction info is + // removed from the disk. + nsresult RemoveEvictInfoFromDisk(nsILoadContextInfo *aLoadContextInfo, bool aPinned); + // Tries to load all contexts from the disk. This method is called just once + // after startup. + nsresult LoadEvictInfoFromDisk(); + nsresult GetContextFile(nsILoadContextInfo *aLoadContextInfo, bool aPinned, + nsIFile **_retval); + + void CreateIterators(); + void CloseIterators(); + void StartEvicting(); + nsresult EvictEntries(); + + // Whether eviction is in progress + bool mEvicting; + // 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; + // Whether we already tried to restore unfinished jobs from previous run after + // startup. + static bool sDiskAlreadySearched; + // Array of contexts being evicted. + nsTArray<nsAutoPtr<CacheFileContextEvictorEntry> > mEntries; + nsCOMPtr<nsIFile> mCacheDirectory; + nsCOMPtr<nsIFile> mEntriesDir; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileIOManager.cpp b/netwerk/cache2/CacheFileIOManager.cpp new file mode 100644 index 0000000000..1d0d576355 --- /dev/null +++ b/netwerk/cache2/CacheFileIOManager.cpp @@ -0,0 +1,4274 @@ +/* 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 "CacheFileIOManager.h" + +#include "../cache/nsCacheUtils.h" +#include "CacheHashUtils.h" +#include "CacheStorageService.h" +#include "CacheIndex.h" +#include "CacheFileUtils.h" +#include "nsThreadUtils.h" +#include "CacheFile.h" +#include "CacheObserver.h" +#include "nsIFile.h" +#include "CacheFileContextEvictor.h" +#include "nsITimer.h" +#include "nsISimpleEnumerator.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIObserverService.h" +#include "nsICacheStorageVisitor.h" +#include "nsISizeOf.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Services.h" +#include "nsDirectoryServiceUtils.h" +#include "nsAppDirectoryServiceDefs.h" +#include "private/pprio.h" +#include "mozilla/Preferences.h" +#include "nsNetUtil.h" + +// include files for ftruncate (or equivalent) +#if defined(XP_UNIX) +#include <unistd.h> +#elif defined(XP_WIN) +#include <windows.h> +#undef CreateFile +#undef CREATE_NEW +#else +// XXX add necessary include file for ftruncate (or equivalent) +#endif + + +namespace mozilla { +namespace net { + +#define kOpenHandlesLimit 128 +#define kMetadataWriteDelay 5000 +#define kRemoveTrashStartDelay 60000 // in milliseconds +#define kSmartSizeUpdateInterval 60000 // in milliseconds + +#ifdef ANDROID +const uint32_t kMaxCacheSizeKB = 200*1024; // 200 MB +#else +const uint32_t kMaxCacheSizeKB = 350*1024; // 350 MB +#endif + +bool +CacheFileHandle::DispatchRelease() +{ + if (CacheFileIOManager::IsOnIOThreadOrCeased()) { + return false; + } + + nsCOMPtr<nsIEventTarget> ioTarget = CacheFileIOManager::IOTarget(); + if (!ioTarget) { + return false; + } + + nsresult rv = + ioTarget->Dispatch(NewNonOwningRunnableMethod(this, + &CacheFileHandle::Release), + nsIEventTarget::DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + return false; + } + + return true; +} + +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=%d]", this, mRefCnt.get())); + NS_PRECONDITION(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_THREADSAFE + +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<CacheFileIOManager> 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=%d, fileExists=%d, fileSize=%lld, leafName=%s, key=%s]", + this, + bool(mIsDoomed), bool(mPriority), bool(mClosed), bool(mInvalid), + mPinning, bool(mFileExists), 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=%d, fileExists=%d, fileSize=%lld, leafName=%s, key=%s]", + this, LOGSHA1(mHash), + bool(mIsDoomed), bool(mPriority), bool(mClosed), bool(mInvalid), + mPinning, bool(mFileExists), 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<uint32_t>(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<nsISizeOf> 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<bool> found; + found = mHandles.RemoveElement(aHandle); + MOZ_ASSERT(found); +} + +already_AddRefed<CacheFileHandle> +CacheFileHandles::HandleHashKey::GetNewestHandle() +{ + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + + RefPtr<CacheFileHandle> handle; + if (mHandles.Length()) { + handle = mHandles[0]; + } + + return handle.forget(); +} + +void +CacheFileHandles::HandleHashKey::GetHandles(nsTArray<RefPtr<CacheFileHandle> > &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<CacheFileHandle> 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; +} + + +nsresult +CacheFileHandles::NewHandle(const SHA1Sum::Hash *aHash, + bool aPriority, CacheFileHandle::PinningStatus aPinning, + CacheFileHandle **_retval) +{ + 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<CacheFileHandle> 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)); + + handle.forget(_retval); + return NS_OK; +} + +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->Hash()); + } +} + +void +CacheFileHandles::GetAllHandles(nsTArray<RefPtr<CacheFileHandle> > *_retval) +{ + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetHandles(*_retval); + } +} + +void +CacheFileHandles::GetActiveHandles( + nsTArray<RefPtr<CacheFileHandle> > *_retval) +{ + MOZ_ASSERT(CacheFileIOManager::IsOnIOThreadOrCeased()); + for (auto iter = mTable.Iter(); !iter.Done(); iter.Next()) { + RefPtr<CacheFileHandle> 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<RefPtr<CacheFileHandle> > 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 { +public: + ShutdownEvent() + : mMonitor("ShutdownEvent.mMonitor") + , mNotified(false) + { + MOZ_COUNT_CTOR(ShutdownEvent); + } + +protected: + ~ShutdownEvent() + { + MOZ_COUNT_DTOR(ShutdownEvent); + } + +public: + NS_IMETHOD Run() override + { + MonitorAutoLock mon(mMonitor); + + CacheFileIOManager::gInstance->ShutdownInternal(); + + mNotified = true; + mon.Notify(); + + return NS_OK; + } + + void PostAndWait() + { + MonitorAutoLock mon(mMonitor); + + DebugOnly<nsresult> rv; + rv = CacheFileIOManager::gInstance->mIOThread->Dispatch( + this, CacheIOThread::WRITE); // When writes and closing of handles is done + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + PRIntervalTime const waitTime = PR_MillisecondsToInterval(1000); + while (!mNotified) { + mon.Wait(waitTime); + if (!mNotified) { + // If there is any IO blocking on the IO thread, this will + // try to cancel it. Returns no later than after two seconds. + MonitorAutoUnlock unmon(mMonitor); // Prevent delays + CacheFileIOManager::gInstance->mIOThread->CancelBlockingIO(); + } + } + } + +protected: + mozilla::Monitor mMonitor; + bool mNotified; +}; + +class OpenFileEvent : public Runnable { +public: + OpenFileEvent(const nsACString &aKey, uint32_t aFlags, + CacheFileIOListener *aCallback) + : mFlags(aFlags) + , mCallback(aCallback) + , mKey(aKey) + { + MOZ_COUNT_CTOR(OpenFileEvent); + mIOMan = CacheFileIOManager::gInstance; + } + +protected: + ~OpenFileEvent() + { + MOZ_COUNT_DTOR(OpenFileEvent); + } + +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)); + } + 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<CacheFileIOListener> mCallback; + RefPtr<CacheFileIOManager> mIOMan; + RefPtr<CacheFileHandle> mHandle; + nsCString mKey; +}; + +class ReadEvent : public Runnable { +public: + ReadEvent(CacheFileHandle *aHandle, int64_t aOffset, char *aBuf, + int32_t aCount, CacheFileIOListener *aCallback) + : mHandle(aHandle) + , mOffset(aOffset) + , mBuf(aBuf) + , mCount(aCount) + , mCallback(aCallback) + { + MOZ_COUNT_CTOR(ReadEvent); + } + +protected: + ~ReadEvent() + { + MOZ_COUNT_DTOR(ReadEvent); + } + +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); + } + + mCallback->OnDataRead(mHandle, mBuf, rv); + return NS_OK; + } + +protected: + RefPtr<CacheFileHandle> mHandle; + int64_t mOffset; + char *mBuf; + int32_t mCount; + nsCOMPtr<CacheFileIOListener> mCallback; +}; + +class WriteEvent : public Runnable { +public: + WriteEvent(CacheFileHandle *aHandle, int64_t aOffset, const char *aBuf, + int32_t aCount, bool aValidate, bool aTruncate, + CacheFileIOListener *aCallback) + : mHandle(aHandle) + , mOffset(aOffset) + , mBuf(aBuf) + , mCount(aCount) + , mValidate(aValidate) + , mTruncate(aTruncate) + , mCallback(aCallback) + { + MOZ_COUNT_CTOR(WriteEvent); + } + +protected: + ~WriteEvent() + { + MOZ_COUNT_DTOR(WriteEvent); + + if (!mCallback && mBuf) { + free(const_cast<char *>(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_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<char *>(mBuf)); + mBuf = nullptr; + } + + return NS_OK; + } + +protected: + RefPtr<CacheFileHandle> mHandle; + int64_t mOffset; + const char *mBuf; + int32_t mCount; + bool mValidate : 1; + bool mTruncate : 1; + nsCOMPtr<CacheFileIOListener> mCallback; +}; + +class DoomFileEvent : public Runnable { +public: + DoomFileEvent(CacheFileHandle *aHandle, + CacheFileIOListener *aCallback) + : mCallback(aCallback) + , mHandle(aHandle) + { + MOZ_COUNT_CTOR(DoomFileEvent); + } + +protected: + ~DoomFileEvent() + { + MOZ_COUNT_DTOR(DoomFileEvent); + } + +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<CacheFileIOListener> mCallback; + nsCOMPtr<nsIEventTarget> mTarget; + RefPtr<CacheFileHandle> mHandle; +}; + +class DoomFileByKeyEvent : public Runnable { +public: + DoomFileByKeyEvent(const nsACString &aKey, + CacheFileIOListener *aCallback) + : mCallback(aCallback) + { + MOZ_COUNT_CTOR(DoomFileByKeyEvent); + + SHA1Sum sum; + sum.update(aKey.BeginReading(), aKey.Length()); + sum.finish(mHash); + + mIOMan = CacheFileIOManager::gInstance; + } + +protected: + ~DoomFileByKeyEvent() + { + MOZ_COUNT_DTOR(DoomFileByKeyEvent); + } + +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<CacheFileIOListener> mCallback; + RefPtr<CacheFileIOManager> mIOMan; +}; + +class ReleaseNSPRHandleEvent : public Runnable { +public: + explicit ReleaseNSPRHandleEvent(CacheFileHandle *aHandle) + : mHandle(aHandle) + { + MOZ_COUNT_CTOR(ReleaseNSPRHandleEvent); + } + +protected: + ~ReleaseNSPRHandleEvent() + { + MOZ_COUNT_DTOR(ReleaseNSPRHandleEvent); + } + +public: + NS_IMETHOD Run() override + { + if (!mHandle->IsClosed()) { + CacheFileIOManager::gInstance->MaybeReleaseNSPRHandleInternal(mHandle); + } + + return NS_OK; + } + +protected: + RefPtr<CacheFileHandle> mHandle; +}; + +class TruncateSeekSetEOFEvent : public Runnable { +public: + TruncateSeekSetEOFEvent(CacheFileHandle *aHandle, int64_t aTruncatePos, + int64_t aEOFPos, CacheFileIOListener *aCallback) + : mHandle(aHandle) + , mTruncatePos(aTruncatePos) + , mEOFPos(aEOFPos) + , mCallback(aCallback) + { + MOZ_COUNT_CTOR(TruncateSeekSetEOFEvent); + } + +protected: + ~TruncateSeekSetEOFEvent() + { + MOZ_COUNT_DTOR(TruncateSeekSetEOFEvent); + } + +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<CacheFileHandle> mHandle; + int64_t mTruncatePos; + int64_t mEOFPos; + nsCOMPtr<CacheFileIOListener> mCallback; +}; + +class RenameFileEvent : public Runnable { +public: + RenameFileEvent(CacheFileHandle *aHandle, const nsACString &aNewName, + CacheFileIOListener *aCallback) + : mHandle(aHandle) + , mNewName(aNewName) + , mCallback(aCallback) + { + MOZ_COUNT_CTOR(RenameFileEvent); + } + +protected: + ~RenameFileEvent() + { + MOZ_COUNT_DTOR(RenameFileEvent); + } + +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<CacheFileHandle> mHandle; + nsCString mNewName; + nsCOMPtr<CacheFileIOListener> mCallback; +}; + +class InitIndexEntryEvent : public Runnable { +public: + InitIndexEntryEvent(CacheFileHandle *aHandle, + OriginAttrsHash aOriginAttrsHash, bool aAnonymous, + bool aPinning) + : mHandle(aHandle) + , mOriginAttrsHash(aOriginAttrsHash) + , mAnonymous(aAnonymous) + , mPinning(aPinning) + { + MOZ_COUNT_CTOR(InitIndexEntryEvent); + } + +protected: + ~InitIndexEntryEvent() + { + MOZ_COUNT_DTOR(InitIndexEntryEvent); + } + +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 and expiration time 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, &sizeInK); + + return NS_OK; + } + +protected: + RefPtr<CacheFileHandle> mHandle; + OriginAttrsHash mOriginAttrsHash; + bool mAnonymous; + bool mPinning; +}; + +class UpdateIndexEntryEvent : public Runnable { +public: + UpdateIndexEntryEvent(CacheFileHandle *aHandle, const uint32_t *aFrecency, + const uint32_t *aExpirationTime) + : mHandle(aHandle) + , mHasFrecency(false) + , mHasExpirationTime(false) + { + MOZ_COUNT_CTOR(UpdateIndexEntryEvent); + if (aFrecency) { + mHasFrecency = true; + mFrecency = *aFrecency; + } + if (aExpirationTime) { + mHasExpirationTime = true; + mExpirationTime = *aExpirationTime; + } + } + +protected: + ~UpdateIndexEntryEvent() + { + MOZ_COUNT_DTOR(UpdateIndexEntryEvent); + } + +public: + NS_IMETHOD Run() override + { + if (mHandle->IsClosed() || mHandle->IsDoomed()) { + return NS_OK; + } + + CacheIndex::UpdateEntry(mHandle->Hash(), + mHasFrecency ? &mFrecency : nullptr, + mHasExpirationTime ? &mExpirationTime : nullptr, + nullptr); + return NS_OK; + } + +protected: + RefPtr<CacheFileHandle> mHandle; + bool mHasFrecency; + bool mHasExpirationTime; + uint32_t mFrecency; + uint32_t mExpirationTime; +}; + +class MetadataWriteScheduleEvent : public Runnable +{ +public: + enum EMode { + SCHEDULE, + UNSCHEDULE, + SHUTDOWN + } mMode; + + RefPtr<CacheFile> mFile; + RefPtr<CacheFileIOManager> mIOMan; + + MetadataWriteScheduleEvent(CacheFileIOManager * aManager, + CacheFile * aFile, + EMode aMode) + : mMode(aMode) + , mFile(aFile) + , mIOMan(aManager) + { } + + virtual ~MetadataWriteScheduleEvent() { } + + NS_IMETHOD Run() override + { + RefPtr<CacheFileIOManager> 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> CacheFileIOManager::gInstance; + +NS_IMPL_ISUPPORTS(CacheFileIOManager, nsITimerCallback) + +CacheFileIOManager::CacheFileIOManager() + : mShuttingDown(false) + , mTreeCreated(false) + , mTreeCreationFailed(false) + , mOverLimitEvicting(false) + , mRemovingTrashDirs(false) +{ + LOG(("CacheFileIOManager::CacheFileIOManager [this=%p]", this)); + MOZ_COUNT_CTOR(CacheFileIOManager); + MOZ_ASSERT(!gInstance, "multiple CacheFileIOManager instances!"); +} + +CacheFileIOManager::~CacheFileIOManager() +{ + LOG(("CacheFileIOManager::~CacheFileIOManager [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileIOManager); +} + +// static +nsresult +CacheFileIOManager::Init() +{ + LOG(("CacheFileIOManager::Init()")); + + MOZ_ASSERT(NS_IsMainThread()); + + if (gInstance) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + RefPtr<CacheFileIOManager> ioMan = new CacheFileIOManager(); + + nsresult rv = ioMan->InitInternal(); + NS_ENSURE_SUCCESS(rv, rv); + + gInstance = ioMan.forget(); + 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<Telemetry::NETWORK_DISK_CACHE_SHUTDOWN_V2> shutdownTimer; + + CacheIndex::PreShutdown(); + + ShutdownMetadataWriteScheduling(); + + RefPtr<ShutdownEvent> 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<Telemetry::NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE> totalTimer; + gInstance->SyncRemoveAllCacheFiles(); + } + + gInstance = nullptr; + + return NS_OK; +} + +nsresult +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; + + // close all handles and delete all associated files + nsTArray<RefPtr<CacheFileHandle> > 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; + } + + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::OnProfile() +{ + LOG(("CacheFileIOManager::OnProfile() [gInstance=%p]", gInstance.get())); + + RefPtr<CacheFileIOManager> 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<nsIFile> directory; + + CacheObserver::ParentDirOverride(getter_AddRefs(directory)); + +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr<nsIFile> 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<nsIFile> 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(NS_LITERAL_STRING("cache2")); + NS_ENSURE_SUCCESS(rv, rv); + } + + // All functions return a clone. + ioMan->mCacheDirectory.swap(directory); + +#if defined(MOZ_WIDGET_ANDROID) + if (profilelessDirectory) { + rv = profilelessDirectory->Append(NS_LITERAL_STRING("cache2")); + NS_ENSURE_SUCCESS(rv, rv); + } + + ioMan->mCacheProfilelessDirectory.swap(profilelessDirectory); +#endif + + if (ioMan->mCacheDirectory) { + CacheIndex::Init(ioMan->mCacheDirectory); + } + + return NS_OK; +} + +// static +already_AddRefed<nsIEventTarget> +CacheFileIOManager::IOTarget() +{ + nsCOMPtr<nsIEventTarget> target; + if (gInstance && gInstance->mIOThread) { + target = gInstance->mIOThread->Target(); + } + + return target.forget(); +} + +// static +already_AddRefed<CacheIOThread> +CacheFileIOManager::IOThread() +{ + RefPtr<CacheIOThread> thread; + if (gInstance) { + thread = gInstance->mIOThread; + } + + return thread.forget(); +} + +// static +bool +CacheFileIOManager::IsOnIOThread() +{ + RefPtr<CacheFileIOManager> ioMan = gInstance; + if (ioMan && ioMan->mIOThread) { + return ioMan->mIOThread->IsCurrentThread(); + } + + return false; +} + +// static +bool +CacheFileIOManager::IsOnIOThreadOrCeased() +{ + RefPtr<CacheFileIOManager> 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<CacheFileIOManager> ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr<MetadataWriteScheduleEvent> event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::SCHEDULE); + nsCOMPtr<nsIEventTarget> 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) { + mMetadataWritesTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mMetadataWritesTimer->InitWithCallback( + this, kMetadataWriteDelay, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mScheduledMetadataWrites.IndexOf(aFile) != + mScheduledMetadataWrites.NoIndex) { + return NS_OK; + } + + mScheduledMetadataWrites.AppendElement(aFile); + + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::UnscheduleMetadataWrite(CacheFile * aFile) +{ + RefPtr<CacheFileIOManager> ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + NS_ENSURE_TRUE(!ioMan->mShuttingDown, NS_ERROR_NOT_INITIALIZED); + + RefPtr<MetadataWriteScheduleEvent> event = new MetadataWriteScheduleEvent( + ioMan, aFile, MetadataWriteScheduleEvent::UNSCHEDULE); + nsCOMPtr<nsIEventTarget> target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +nsresult +CacheFileIOManager::UnscheduleMetadataWriteInternal(CacheFile * aFile) +{ + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + mScheduledMetadataWrites.RemoveElement(aFile); + + if (mScheduledMetadataWrites.Length() == 0 && + mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } + + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::ShutdownMetadataWriteScheduling() +{ + RefPtr<CacheFileIOManager> ioMan = gInstance; + NS_ENSURE_TRUE(ioMan, NS_ERROR_NOT_INITIALIZED); + + RefPtr<MetadataWriteScheduleEvent> event = new MetadataWriteScheduleEvent( + ioMan, nullptr, MetadataWriteScheduleEvent::SHUTDOWN); + nsCOMPtr<nsIEventTarget> target = ioMan->IOTarget(); + NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED); + return target->Dispatch(event, nsIEventTarget::DISPATCH_NORMAL); +} + +nsresult +CacheFileIOManager::ShutdownMetadataWriteSchedulingInternal() +{ + MOZ_ASSERT(IsOnIOThreadOrCeased()); + + nsTArray<RefPtr<CacheFile> > files; + files.SwapElements(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile * file = files[i]; + file->WriteMetadataIfNeeded(); + } + + if (mMetadataWritesTimer) { + mMetadataWritesTimer->Cancel(); + mMetadataWritesTimer = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +CacheFileIOManager::Notify(nsITimer * aTimer) +{ + MOZ_ASSERT(IsOnIOThreadOrCeased()); + MOZ_ASSERT(mMetadataWritesTimer == aTimer); + + mMetadataWritesTimer = nullptr; + + nsTArray<RefPtr<CacheFile> > files; + files.SwapElements(mScheduledMetadataWrites); + for (uint32_t i = 0; i < files.Length(); ++i) { + CacheFile * file = files[i]; + file->WriteMetadataIfNeeded(); + } + + 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<CacheFileIOManager> ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + bool priority = aFlags & CacheFileIOManager::PRIORITY; + RefPtr<OpenFileEvent> 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<nsIFile> file; + rv = GetFile(aHash, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CacheFileHandle> 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; + } + + rv = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning, getter_AddRefs(handle)); + NS_ENSURE_SUCCESS(rv, rv); + + 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%08x]", 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; + } + + rv = mHandles.NewHandle(aHash, aFlags & PRIORITY, pinning, getter_AddRefs(handle)); + NS_ENSURE_SUCCESS(rv, rv); + + 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)); + } + + rv = file->GetFileSize(&handle->mFileSize); + NS_ENSURE_SUCCESS(rv, rv); + + 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<nsIFile> file; + rv = GetSpecialFile(aKey, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CacheFileHandle> 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%08x]", 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) { + rv = file->GetFileSize(&handle->mFileSize); + NS_ENSURE_SUCCESS(rv, rv); + + handle->mFileExists = true; + } else { + handle->mFileSize = 0; + } + + handle->mFile.swap(file); + handle.swap(*_retval); + return NS_OK; +} + +nsresult +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) && NS_SUCCEEDED(rv)) { + LOG(("CacheFileIOManager::CloseHandleInternal() - Removing file from " + "disk")); + + aHandle->mFile->Remove(false); + } + + 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); + } + } + + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::Read(CacheFileHandle *aHandle, int64_t aOffset, + char *aBuf, int32_t aCount, + CacheFileIOListener *aCallback) +{ + LOG(("CacheFileIOManager::Read() [handle=%p, offset=%lld, count=%d, " + "listener=%p]", aHandle, aOffset, aCount, aCallback)); + + if (CacheObserver::ShuttingDown()) { + LOG((" no reads after shutdown")); + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<ReadEvent> 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=%lld, 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=%lld, count=%d, " + "validate=%d, truncate=%d, listener=%p]", aHandle, aOffset, aCount, + aValidate, aTruncate, aCallback)); + + nsresult rv; + RefPtr<CacheFileIOManager> 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<char *>(aBuf)); + } + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<WriteEvent> 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=%lld, 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; + } + + // Check whether this write would cause critical low disk space. + if (aHandle->mFileSize < aOffset + aCount) { + int64_t freeSpace = -1; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("CacheFileIOManager::WriteInternal() - GetDiskSpaceAvailable() " + "failed! [rv=0x%08x]", rv)); + } else { + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aOffset - aCount + aHandle->mFileSize < limit) { + LOG(("CacheFileIOManager::WriteInternal() - Low free space, refusing " + "to write! [freeSpace=%lld, limit=%u]", freeSpace, limit)); + return NS_ERROR_FILE_DISK_FULL; + } + } + } + + // 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, &newSizeInK); + + if (oldSizeInK < newSizeInK) { + EvictIfOverLimitInternal(); + } + } + } + + 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<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<DoomFileEvent> 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<nsIFile> file; + rv = GetDoomedFile(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> 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 || NS_ERROR_FILE_TARGET_DOES_NOT_EXIST == 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<CacheStorageService> storageService = CacheStorageService::Self(); + if (storageService) { + nsAutoCString idExtension, url; + nsCOMPtr<nsILoadContextInfo> 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<CacheFileIOManager> ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<DoomFileByKeyEvent> 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<CacheFileHandle> 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<nsIFile> 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%08x]", rv)); + } + + CacheIndex::RemoveEntry(aHash); + + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::ReleaseNSPRHandle(CacheFileHandle *aHandle) +{ + LOG(("CacheFileIOManager::ReleaseNSPRHandle() [handle=%p]", aHandle)); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<ReleaseNSPRHandleEvent> 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<bool> 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=%lld, " + "EOFPos=%lld, listener=%p]", aHandle, aTruncatePos, aEOFPos, aCallback)); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || (aCallback && aCallback->IsKilled()) || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + RefPtr<TruncateSeekSetEOFEvent> 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<CacheFileIOManager> 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<CacheFileIOManager> 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<CacheFileIOManager> ioMan = gInstance; + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsAutoCString enhanceId; + nsAutoCString uriSpec; + + RefPtr<CacheFileHandle> handle; + ioMan->mHandles.GetHandle(aHash, getter_AddRefs(handle)); + if (handle) { + RefPtr<nsILoadContextInfo> info = + CacheFileUtils::ParseKey(handle->Key(), &enhanceId, &uriSpec); + + MOZ_ASSERT(info); + if (!info) { + return NS_OK; // ignore + } + + RefPtr<CacheStorageService> 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<nsIFile> file; + ioMan->GetFile(aHash, getter_AddRefs(file)); + + // Read metadata from the file synchronously + RefPtr<CacheFileMetadata> metadata = new CacheFileMetadata(); + rv = metadata->SyncReadMetadata(file); + if (NS_FAILED(rv)) { + return NS_OK; + } + + // Now get the context + enhance id + URL from the key. + nsAutoCString key; + metadata->GetKey(key); + + RefPtr<nsILoadContextInfo> info = + CacheFileUtils::ParseKey(key, &enhanceId, &uriSpec); + MOZ_ASSERT(info); + if (!info) { + return NS_OK; + } + + // Pick all data to pass to the callback. + int64_t dataSize = metadata->Offset(); + uint32_t fetchCount; + if (NS_FAILED(metadata->GetFetchCount(&fetchCount))) { + fetchCount = 0; + } + uint32_t expirationTime; + if (NS_FAILED(metadata->GetExpirationTime(&expirationTime))) { + expirationTime = 0; + } + uint32_t lastModified; + if (NS_FAILED(metadata->GetLastModified(&lastModified))) { + lastModified = 0; + } + + // Call directly on the callback. + aCallback->OnEntryInfo(uriSpec, enhanceId, dataSize, fetchCount, + lastModified, expirationTime, metadata->Pinned()); + + return NS_OK; +} + +nsresult +CacheFileIOManager::TruncateSeekSetEOFInternal(CacheFileHandle *aHandle, + int64_t aTruncatePos, + int64_t aEOFPos) +{ + LOG(("CacheFileIOManager::TruncateSeekSetEOFInternal() [handle=%p, " + "truncatePos=%lld, EOFPos=%lld]", 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; + } + + // Check whether this operation would cause critical low disk space. + if (aHandle->mFileSize < aEOFPos) { + int64_t freeSpace = -1; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("CacheFileIOManager::TruncateSeekSetEOFInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08x]", rv)); + } else { + uint32_t limit = CacheObserver::DiskFreeSpaceHardLimit(); + if (freeSpace - aEOFPos + aHandle->mFileSize < limit) { + LOG(("CacheFileIOManager::TruncateSeekSetEOFInternal() - Low free space" + ", refusing to write! [freeSpace=%lld, limit=%u]", freeSpace, + limit)); + return NS_ERROR_FILE_DISK_FULL; + } + } + } + + // 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, &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<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<RenameFileEvent> 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<nsIFile> 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%08x]", 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<CacheFileIOManager> ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod(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%08x]", rv)); + } else { + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity() >> 10; + 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=%lld, freeSpaceLimit=%u]", cacheUsage, cacheLimit, + freeSpace, freeSpaceLimit)); + return NS_OK; + } + + LOG(("CacheFileIOManager::EvictIfOverLimitInternal() - Cache size exceeded " + "limit. Starting overlimit eviction. [cacheSize=%u, limit=%u]", + cacheUsage, cacheLimit)); + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod(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 = -1; + rv = mCacheDirectory->GetDiskSpaceAvailable(&freeSpace); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Do not change smart size. + LOG(("CacheFileIOManager::EvictIfOverLimitInternal() - " + "GetDiskSpaceAvailable() failed! [rv=0x%08x]", rv)); + } else { + UpdateSmartCacheSize(freeSpace); + } + + uint32_t cacheUsage; + rv = CacheIndex::GetCacheSize(&cacheUsage); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t cacheLimit = CacheObserver::DiskCacheCapacity() >> 10; + uint32_t freeSpaceLimit = CacheObserver::DiskFreeSpaceSoftLimit(); + + if (cacheUsage > cacheLimit) { + LOG(("CacheFileIOManager::OverLimitEvictionInternal() - Cache size over " + "limit. [cacheSize=%u, limit=%u]", cacheUsage, cacheLimit)); + } else if (freeSpace != 1 && freeSpace < freeSpaceLimit) { + LOG(("CacheFileIOManager::OverLimitEvictionInternal() - Free space under " + "limit. [freeSpace=%lld, freeSpaceLimit=%u]", freeSpace, + freeSpaceLimit)); + } else { + LOG(("CacheFileIOManager::OverLimitEvictionInternal() - Cache size and " + "free space in limits. [cacheSize=%ukB, cacheSizeLimit=%ukB, " + "freeSpace=%lld, freeSpaceLimit=%u]", cacheUsage, cacheLimit, + freeSpace, freeSpaceLimit)); + 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%08x]", 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%08x]", 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; + uint32_t expTime = nsICacheEntry::NO_EXPIRATION_TIME; + rv = CacheIndex::UpdateEntry(&hash, &frecency, &expTime, 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; + } + } + } + + NS_NOTREACHED("We should never get here"); + return NS_OK; +} + +// static +nsresult +CacheFileIOManager::EvictAll() +{ + LOG(("CacheFileIOManager::EvictAll()")); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod(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: + NS_DECL_NSIRUNNABLE +}; + +NS_IMETHODIMP +EvictionNotifierRunnable::Run() +{ + nsCOMPtr<nsIObserverService> 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<EvictionNotifierRunnable> 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<RefPtr<CacheFileHandle> > 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<nsIFile> file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = file->AppendNative(NS_LITERAL_CSTRING(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) +{ + LOG(("CacheFileIOManager::EvictByContext() [loadContextInfo=%p]", + aLoadContextInfo)); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (!ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod<nsCOMPtr<nsILoadContextInfo>, bool> + (ioMan, &CacheFileIOManager::EvictByContextInternal, aLoadContextInfo, aPinned); + + rv = ioMan->mIOThread->DispatchAfterPendingOpens(ev); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +CacheFileIOManager::EvictByContextInternal(nsILoadContextInfo *aLoadContextInfo, bool aPinned) +{ + 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<EvictionNotifierRunnable> 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; + } + } + + // Doom all active handles that matches the load context + nsTArray<RefPtr<CacheFileHandle> > handles; + mHandles.GetActiveHandles(&handles); + + for (uint32_t i = 0; i < handles.Length(); ++i) { + CacheFileHandle* handle = handles[i]; + + if (aLoadContextInfo) { + bool equals; + rv = CacheFileUtils::KeyMatchesLoadContextInfo(handle->Key(), + aLoadContextInfo, + &equals); + if (NS_FAILED(rv)) { + LOG(("CacheFileIOManager::EvictByContextInternal() - Cannot parse key in " + "handle! [handle=%p, key=%s]", handle, handle->Key().get())); + MOZ_CRASH("Unexpected error!"); + } + + if (!equals) { + 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<EvictionNotifierRunnable> r = new EvictionNotifierRunnable(); + NS_DispatchToMainThread(r); + } + + if (!mContextEvictor) { + mContextEvictor = new CacheFileContextEvictor(); + mContextEvictor->Init(mCacheDirectory); + } + + mContextEvictor->AddContext(aLoadContextInfo, aPinned); + + 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<nsIRunnable> ev; + ev = NewRunnableMethod( + gInstance.get(), &CacheFileIOManager::CacheIndexStateChangedInternal); + + nsCOMPtr<nsIEventTarget> ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + rv = ioTarget->Dispatch(ev, nsIEventTarget::DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + return NS_OK; +} + +nsresult +CacheFileIOManager::CacheIndexStateChangedInternal() +{ + if (mShuttingDown) { + // ignore notification during shutdown + return NS_OK; + } + + if (!mContextEvictor) { + return NS_OK; + } + + mContextEvictor->CacheIndexStateChanged(); + return NS_OK; +} + +nsresult +CacheFileIOManager::TrashDirectory(nsIFile *aFile) +{ + nsAutoCString path; + aFile->GetNativePath(path); + LOG(("CacheFileIOManager::TrashDirectory() [file=%s]", path.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%08x]", + rv)); + return rv; + } + +#ifdef DEBUG + nsCOMPtr<nsIFile> 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<nsIFile> 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<unsigned>(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<CacheFileIOManager> 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<nsITimer> timer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEventTarget> ioTarget = IOTarget(); + MOZ_ASSERT(ioTarget); + + rv = timer->SetTarget(ioTarget); + NS_ENSURE_SUCCESS(rv, rv); + + rv = timer->InitWithFuncCallback(CacheFileIOManager::OnTrashTimer, nullptr, + kRemoveTrashStartDelay - elapsed, + nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, rv); + + mTrashTimer.swap(timer); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> ev; + ev = NewRunnableMethod(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); + + nsCOMPtr<nsISimpleEnumerator> enumerator; + rv = mTrashDir->GetDirectoryEntries(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv)) { + mTrashDirEnumerator = do_QueryInterface(enumerator, &rv); + 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<nsIFile> file; + rv = mTrashDirEnumerator->GetNextFile(getter_AddRefs(file)); + if (!file) { + mTrashDirEnumerator->Close(); + mTrashDirEnumerator = nullptr; + continue; // check elapsed time + } else { + 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()) { + nsAutoCString path; + file->GetNativePath(path); + 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]", path.get())); + } + } + file->Remove(isDir); + } + } + + NS_NOTREACHED("We should never get here"); + return NS_OK; +} + +nsresult +CacheFileIOManager::FindTrashDirToRemove() +{ + LOG(("CacheFileIOManager::FindTrashDirToRemove()")); + + nsresult rv; + + // We call this method on the main thread during shutdown when user wants to + // remove all cache files. + MOZ_ASSERT(mIOThread->IsCurrentThread() || mShuttingDown); + + nsCOMPtr<nsISimpleEnumerator> iter; + rv = mCacheDirectory->GetDirectoryEntries(getter_AddRefs(iter)); + NS_ENSURE_SUCCESS(rv, rv); + + bool more; + nsCOMPtr<nsISupports> elem; + + while (NS_SUCCEEDED(iter->HasMoreElements(&more)) && more) { + rv = iter->GetNext(getter_AddRefs(elem)); + if (NS_FAILED(rv)) { + continue; + } + + nsCOMPtr<nsIFile> file = do_QueryInterface(elem); + if (!file) { + continue; + } + + 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, NS_LITERAL_CSTRING(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=%llx, " + "anonymous=%d, pinning=%d]", aHandle, aOriginAttrsHash, aAnonymous, + aPinning)); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<InitIndexEntryEvent> 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 uint32_t *aExpirationTime) +{ + LOG(("CacheFileIOManager::UpdateIndexEntry() [handle=%p, frecency=%s, " + "expirationTime=%s]", aHandle, + aFrecency ? nsPrintfCString("%u", *aFrecency).get() : "", + aExpirationTime ? nsPrintfCString("%u", *aExpirationTime).get() : "")); + + nsresult rv; + RefPtr<CacheFileIOManager> ioMan = gInstance; + + if (aHandle->IsClosed() || !ioMan) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (aHandle->IsSpecialFile()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr<UpdateIndexEntryEvent> ev = + new UpdateIndexEntryEvent(aHandle, aFrecency, aExpirationTime); + 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<nsIFile> 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<uint8_t *>(_retval))[i/2] = value << 4; + } else { + (reinterpret_cast<uint8_t *>(_retval))[i/2] += value; + } + } + + return NS_OK; +} + +nsresult +CacheFileIOManager::GetFile(const SHA1Sum::Hash *aHash, nsIFile **_retval) +{ + nsresult rv; + nsCOMPtr<nsIFile> file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(NS_LITERAL_CSTRING(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<nsIFile> 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<nsIFile> file; + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(NS_LITERAL_CSTRING(DOOMED_DIR)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(NS_LITERAL_CSTRING("dummyleaf")); + NS_ENSURE_SUCCESS(rv, rv); + + const int32_t kMaxTries = 64; + srand(static_cast<unsigned>(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<nsISimpleEnumerator> 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<nsIFile> 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<nsIFile> 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<CacheFileContextEvictor> 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(); + + if (!CacheObserver::CacheFSReported()) { + uint32_t fsType = 4; // Other OS + +#ifdef XP_WIN + nsAutoString target; + nsresult rv = mCacheDirectory->GetTarget(target); + if (NS_FAILED(rv)) { + return NS_OK; + } + + wchar_t volume_path[MAX_PATH + 1] = { 0 }; + if (!::GetVolumePathNameW(target.get(), + volume_path, + mozilla::ArrayLength(volume_path))) { + return NS_OK; + } + + wchar_t fsName[6] = { 0 }; + if (!::GetVolumeInformationW(volume_path, nullptr, 0, nullptr, nullptr, + nullptr, fsName, + mozilla::ArrayLength(fsName))) { + return NS_OK; + } + + if (wcscmp(fsName, L"NTFS") == 0) { + fsType = 0; + } else if (wcscmp(fsName, L"FAT32") == 0) { + fsType = 1; + } else if (wcscmp(fsName, L"FAT") == 0) { + fsType = 2; + } else { + fsType = 3; + } +#endif + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_FS_TYPE, fsType); + CacheObserver::SetCacheFSReported(); + } + + 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%08x", 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%08x", 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<bool> found; + found = mHandlesByLastUsed.RemoveElement(aHandle); + MOZ_ASSERT(found); + + mHandlesByLastUsed.AppendElement(aHandle); +} + +nsresult +CacheFileIOManager::SyncRemoveDir(nsIFile *aFile, const char *aDir) +{ + nsresult rv; + nsCOMPtr<nsIFile> file; + + 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()) { + nsAutoCString path; + file->GetNativePath(path); + LOG(("CacheFileIOManager::SyncRemoveDir() - Removing directory %s", + path.get())); + } + + rv = file->Remove(true); + if (NS_WARN_IF(NS_FAILED(rv))) { + LOG(("CacheFileIOManager::SyncRemoveDir() - Removing failed! [rv=0x%08x]", + rv)); + } + + return rv; +} + +void +CacheFileIOManager::SyncRemoveAllCacheFiles() +{ + LOG(("CacheFileIOManager::SyncRemoveAllCacheFiles()")); + + nsresult rv; + + 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%08x]", + 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 uint32_t availKB) +{ + uint32_t maxSize = kMaxCacheSizeKB; + + if (availKB > 100 * 1024 * 1024) { + return maxSize; // skip computing if we're over 100 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); + + // .5% of space above 25 GB + if (avail10MBs > 2500) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 2500)*.005); + avail10MBs = 2500; + } + // 1% of space between 7GB -> 25 GB + if (avail10MBs > 700) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 700)*.01); + avail10MBs = 700; + } + // 5% of space between 500 MB -> 7 GB + if (avail10MBs > 50) { + sz10MBs += static_cast<uint32_t>((avail10MBs - 50)*.05); + 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. + + // 20% of space up to 500 MB (10 MB min) + sz10MBs += std::max<uint32_t>(1, static_cast<uint32_t>(avail10MBs * .2)); +#else + // 40% of space up to 500 MB (50 MB min) + sz10MBs += std::max<uint32_t>(5, static_cast<uint32_t>(avail10MBs * .4)); +#endif + + return std::min<uint32_t>(maxSize, sz10MBs * 10 * 1024); +} + +nsresult +CacheFileIOManager::UpdateSmartCacheSize(int64_t aFreeSpace) +{ + MOZ_ASSERT(mIOThread->IsCurrentThread()); + + nsresult rv; + + if (!CacheObserver::UseNewCache()) { + return NS_ERROR_NOT_AVAILABLE; + } + + 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%08x]", rv)); + return rv; + } + + mLastSmartSizeTime = TimeStamp::NowLoRes(); + + uint32_t smartSize = SmartCacheSize(static_cast<uint32_t>(aFreeSpace / 1024) + + cacheUsage); + + if (smartSize == (CacheObserver::DiskCacheCapacity() >> 10)) { + // Smart size has not changed. + return NS_OK; + } + + CacheObserver::SetDiskCacheCapacity(smartSize << 10); + + 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<CacheFileHandle *> const &specialHandles) + : mMonitor("SizeOfHandlesRunnable.mMonitor") + , mMallocSizeOf(mallocSizeOf) + , mHandles(handles) + , mSpecialHandles(specialHandles) + { + } + + size_t Get(CacheIOThread* thread) + { + nsCOMPtr<nsIEventTarget> 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; + bool mMonitorNotified; + mozilla::MallocSizeOf mMallocSizeOf; + CacheFileHandles const &mHandles; + nsTArray<CacheFileHandle *> const &mSpecialHandles; + size_t mSize; +}; + +} // namespace + +size_t +CacheFileIOManager::SizeOfExcludingThisInternal(mozilla::MallocSizeOf mallocSizeOf) const +{ + size_t n = 0; + nsCOMPtr<nsISizeOf> sizeOf; + + if (mIOThread) { + n += mIOThread->SizeOfIncludingThis(mallocSizeOf); + + // mHandles and mSpecialHandles must be accessed only on the I/O thread, + // must sync dispatch. + RefPtr<SizeOfHandlesRunnable> 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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileIOManager.h b/netwerk/cache2/CacheFileIOManager.h new file mode 100644 index 0000000000..5ac812da5d --- /dev/null +++ b/netwerk/cache2/CacheFileIOManager.h @@ -0,0 +1,489 @@ +/* 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 "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 : 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<bool, ReleaseAcquire> mIsDoomed; + mozilla::Atomic<bool, ReleaseAcquire> 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<bool, Relaxed> 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<nsIFile> mFile; + int64_t 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); + nsresult NewHandle(const SHA1Sum::Hash *aHash, bool aPriority, + CacheFileHandle::PinningStatus aPinning, CacheFileHandle **_retval); + void RemoveHandle(CacheFileHandle *aHandlle); + void GetAllHandles(nsTArray<RefPtr<CacheFileHandle> > *_retval); + void GetActiveHandles(nsTArray<RefPtr<CacheFileHandle> > *_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: + typedef const SHA1Sum::Hash& KeyType; + typedef const SHA1Sum::Hash* KeyTypePointer; + + explicit HandleHashKey(KeyTypePointer aKey) + { + MOZ_COUNT_CTOR(HandleHashKey); + mHash = MakeUnique<uint8_t[]>(SHA1Sum::kHashSize); + memcpy(mHash.get(), aKey, sizeof(SHA1Sum::Hash)); + } + HandleHashKey(const HandleHashKey& aOther) + { + NS_NOTREACHED("HandleHashKey copy constructor is forbidden!"); + } + ~HandleHashKey() + { + MOZ_COUNT_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<const uint32_t *>(aKey))[0]; + } + + void AddHandle(CacheFileHandle* aHandle); + void RemoveHandle(CacheFileHandle* aHandle); + already_AddRefed<CacheFileHandle> GetNewestHandle(); + void GetHandles(nsTArray<RefPtr<CacheFileHandle> > &aResult); + + SHA1Sum::Hash *Hash() const + { + return reinterpret_cast<SHA1Sum::Hash*>(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<SHA1Sum::Hash>, 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<uint8_t[]> 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<CacheFileHandle*> mHandles; + }; + +private: + nsTHashtable<HandleHashKey> 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 : public nsITimerCallback +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITIMERCALLBACK + + 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 already_AddRefed<nsIEventTarget> IOTarget(); + static already_AddRefed<CacheIOThread> 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 aPinning); + + static nsresult InitIndexEntry(CacheFileHandle *aHandle, + OriginAttrsHash aOriginAttrsHash, + bool aAnonymous, + bool aPinning); + static nsresult UpdateIndexEntry(CacheFileHandle *aHandle, + const uint32_t *aFrecency, + const uint32_t *aExpirationTime); + + 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(); + nsresult 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); + nsresult 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 aPinningStatusRestriction = 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 aPinning); + + 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); + nsresult UnscheduleMetadataWriteInternal(CacheFile * aFile); + nsresult ShutdownMetadataWriteSchedulingInternal(); + + static nsresult CacheIndexStateChanged(); + nsresult CacheIndexStateChangedInternal(); + + // 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<CacheFileIOManager> gInstance; + + TimeStamp mStartTime; + // Set true on the IO thread, CLOSE level as part of the internal shutdown + // procedure. + bool mShuttingDown; + RefPtr<CacheIOThread> mIOThread; + nsCOMPtr<nsIFile> 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<nsIFile> mCacheProfilelessDirectory; +#endif + bool mTreeCreated; + bool mTreeCreationFailed; + CacheFileHandles mHandles; + nsTArray<CacheFileHandle *> mHandlesByLastUsed; + nsTArray<CacheFileHandle *> mSpecialHandles; + nsTArray<RefPtr<CacheFile> > mScheduledMetadataWrites; + nsCOMPtr<nsITimer> mMetadataWritesTimer; + bool mOverLimitEvicting; + bool mRemovingTrashDirs; + nsCOMPtr<nsITimer> mTrashTimer; + nsCOMPtr<nsIFile> mTrashDir; + nsCOMPtr<nsIDirectoryEnumerator> mTrashDirEnumerator; + nsTArray<nsCString> mFailedTrashDirs; + RefPtr<CacheFileContextEvictor> 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..26ca575370 --- /dev/null +++ b/netwerk/cache2/CacheFileInputStream.cpp @@ -0,0 +1,723 @@ +/* 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 <algorithm> + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(CacheFileInputStream) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileInputStream::Release() +{ + NS_PRECONDITION(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) { + 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(mozilla::net::CacheFileChunkListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream) +NS_INTERFACE_MAP_END_THREADSAFE + +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)); + MOZ_COUNT_CTOR(CacheFileInputStream); + + if (mAlternativeData) { + mPos = mFile->mAltDataOffset; + } +} + +CacheFileInputStream::~CacheFileInputStream() +{ + LOG(("CacheFileInputStream::~CacheFileInputStream() [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileInputStream); + 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%08x]", this, 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%08x]", this, 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=%lld, rv=0x%08x]", + this, *_retval, rv)); + + return rv; +} + +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; + + *_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%08x]", this, mStatus)); + + if NS_FAILED(mStatus) + return mStatus; + + return NS_OK; + } + + EnsureCorrectChunk(false); + + while (true) { + if (NS_FAILED(mStatus)) + return mStatus; + + if (!mChunk) { + if (mListeningForChunk == -1) { + return NS_OK; + } else { + 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<uint32_t>(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) { + if (hnd.DataSize() != mChunk->DataSize()) { + // New data was written to this chunk while the lock was released. + continue; + } + + // The last chunk is released after the caller closes this stream. + EnsureCorrectChunk(false); + + if (mChunk && aCount) { + // We have the next chunk! Go on. + continue; + } + } + } + + if (mClosed) { + // The stream was closed from aWriter, do the cleanup. + CleanUp(); + } + + rv = NS_OK; + } else { + if (mFile->OutputStreamExists(mAlternativeData)) { + rv = NS_BASE_STREAM_WOULD_BLOCK; + } else { + rv = NS_OK; + } + } + + break; + } + + LOG(("CacheFileInputStream::ReadSegments() [this=%p, rv=0x%08x, retval=%d]", + this, 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%08x]", + this, aStatus)); + + return CloseWithStatusLocked(aStatus); +} + +nsresult +CacheFileInputStream::CloseWithStatusLocked(nsresult aStatus) +{ + LOG(("CacheFileInputStream::CloseWithStatusLocked() [this=%p, " + "aStatus=0x%08x]", this, 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 NS_OK; + } + + mClosed = true; + mStatus = NS_FAILED(aStatus) ? aStatus : NS_BASE_STREAM_CLOSED; + + if (!mInReadSegments) { + CleanUp(); + } + + return NS_OK; +} + +void +CacheFileInputStream::CleanUp() +{ + MOZ_ASSERT(!mInReadSegments); + MOZ_ASSERT(mClosed); + + if (mChunk) { + ReleaseChunk(); + } + + // TODO propagate error from input stream to other streams ??? + + MaybeNotifyListener(); + + mFile->ReleaseOutsideLock(mCacheEntryHandle.forget()); +} + +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); + + LOG(("CacheFileInputStream::Seek() [this=%p, whence=%d, offset=%lld]", + 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=%lld]", this, mPos)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileInputStream::Tell(int64_t *_retval) +{ + CacheFileAutoLock lock(mFile); + + 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=%lld]", this, *_retval)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileInputStream::SetEOF() +{ + MOZ_ASSERT(false, "Don't call SetEOF on cache input stream"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +// 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%08x, " + "idx=%d, chunk=%p]", this, aResult, aChunkIdx, aChunk)); + + MOZ_ASSERT(mListeningForChunk != -1); + + if (mListeningForChunk != static_cast<int64_t>(aChunkIdx)) { + // This is not a chunk that we're waiting for + LOG(("CacheFileInputStream::OnChunkAvailable() - Notification is for a " + "different chunk. [this=%p, listeningForChunk=%lld]", + 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; + } + else { + 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(mChunk.forget()); +} + +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; + } else { + ReleaseChunk(); + } + } + + MOZ_ASSERT(!mWaitingForUpdate); + + if (aReleaseOnly) + return; + + if (mListeningForChunk == static_cast<int64_t>(chunkIdx)) { + // We're already waiting for this chunk + LOG(("CacheFileInputStream::EnsureCorrectChunk() - Already listening for " + "chunk %lld [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%08x]", this, chunkIdx, 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<int64_t>(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() - mPos; + if (retval <= 0 && NS_FAILED(mChunk->GetStatus())) { + CloseWithStatusLocked(mChunk->GetStatus()); + } + + LOG(("CacheFileInputStream::CanRead() [this=%p, canRead=%lld]", + 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 = do_GetMainThread(); + } + } + + nsCOMPtr<nsIInputStreamCallback> asyncCallback = + NS_NewInputStreamReadyEvent(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%08x, mChunk=%p, mListeningForChunk=%lld, " + "mWaitingForUpdate=%d]", this, mCallback.get(), mClosed, 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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileInputStream.h b/netwerk/cache2/CacheFileInputStream.h new file mode 100644 index 0000000000..ab632f3687 --- /dev/null +++ b/netwerk/cache2/CacheFileInputStream.h @@ -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/. */ + +#ifndef CacheFileInputStream__h__ +#define CacheFileInputStream__h__ + +#include "nsIAsyncInputStream.h" +#include "nsISeekableStream.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.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 + +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; }; + +private: + virtual ~CacheFileInputStream(); + + nsresult 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<CacheFile> mFile; + RefPtr<CacheFileChunk> mChunk; + int64_t mPos; + nsresult mStatus; + bool mClosed : 1; + bool mInReadSegments : 1; + bool mWaitingForUpdate : 1; + bool const mAlternativeData : 1; + int64_t mListeningForChunk; + + nsCOMPtr<nsIInputStreamCallback> mCallback; + uint32_t mCallbackFlags; + nsCOMPtr<nsIEventTarget> mCallbackTarget; + // Held purely for referencing purposes + RefPtr<nsISupports> mCacheEntryHandle; +}; + + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileMetadata.cpp b/netwerk/cache2/CacheFileMetadata.cpp new file mode 100644 index 0000000000..3814b4c878 --- /dev/null +++ b/netwerk/cache2/CacheFileMetadata.cpp @@ -0,0 +1,1095 @@ +/* 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 "../cache/nsCacheUtils.h" +#include "nsIFile.h" +#include "mozilla/Telemetry.h" +#include "mozilla/DebugOnly.h" +#include "prnetdb.h" + + +namespace mozilla { +namespace 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) + : CacheMemoryConsumer(NORMAL) + , mHandle(aHandle) + , mHashArray(nullptr) + , mHashArraySize(0) + , mHashCount(0) + , mOffset(-1) + , mBuf(nullptr) + , mBufSize(0) + , mWriteBuf(nullptr) + , mElementsSize(0) + , mIsDirty(false) + , mAnonymous(false) + , mAllocExactSize(false) + , mFirstRead(true) +{ + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p, handle=%p, key=%s]", + this, aHandle, PromiseFlatCString(aKey).get())); + + MOZ_COUNT_CTOR(CacheFileMetadata); + memset(&mMetaHdr, 0, sizeof(CacheFileMetadataHeader)); + mMetaHdr.mVersion = kCacheEntryVersion; + mMetaHdr.mExpirationTime = nsICacheEntry::NO_EXPIRATION_TIME; + mKey = aKey; + + DebugOnly<nsresult> rv; + rv = ParseKey(aKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +CacheFileMetadata::CacheFileMetadata(bool aMemoryOnly, bool aPinned, const nsACString &aKey) + : CacheMemoryConsumer(aMemoryOnly ? MEMORY_ONLY : NORMAL) + , mHandle(nullptr) + , mHashArray(nullptr) + , mHashArraySize(0) + , mHashCount(0) + , mOffset(0) + , mBuf(nullptr) + , mBufSize(0) + , mWriteBuf(nullptr) + , mElementsSize(0) + , mIsDirty(true) + , mAnonymous(false) + , mAllocExactSize(false) + , mFirstRead(true) +{ + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p, key=%s]", + this, PromiseFlatCString(aKey).get())); + + MOZ_COUNT_CTOR(CacheFileMetadata); + 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<nsresult> rv; + rv = ParseKey(aKey); + MOZ_ASSERT(NS_SUCCEEDED(rv)); +} + +CacheFileMetadata::CacheFileMetadata() + : CacheMemoryConsumer(DONT_REPORT /* This is a helper class */) + , mHandle(nullptr) + , mHashArray(nullptr) + , mHashArraySize(0) + , mHashCount(0) + , mOffset(0) + , mBuf(nullptr) + , mBufSize(0) + , mWriteBuf(nullptr) + , mElementsSize(0) + , mIsDirty(false) + , mAnonymous(false) + , mAllocExactSize(false) + , mFirstRead(true) +{ + LOG(("CacheFileMetadata::CacheFileMetadata() [this=%p]", this)); + + MOZ_COUNT_CTOR(CacheFileMetadata); + memset(&mMetaHdr, 0, sizeof(CacheFileMetadataHeader)); +} + +CacheFileMetadata::~CacheFileMetadata() +{ + LOG(("CacheFileMetadata::~CacheFileMetadata() [this=%p]", this)); + + MOZ_COUNT_DTOR(CacheFileMetadata); + 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; +} + +nsresult +CacheFileMetadata::GetKey(nsACString &_retval) +{ + _retval = mKey; + return NS_OK; +} + +nsresult +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 NS_OK; + } + + 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=%lld]", this, size)); + + InitEmptyMetadata(); + aListener->OnMetadataRead(NS_OK); + return NS_OK; + } + + // 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<char *>(moz_xmalloc(mBufSize)); + + DoMemoryReport(MemoryUsage()); + + LOG(("CacheFileMetadata::ReadMetadata() - Reading metadata from disk, trying " + "offset=%lld, filesize=%lld [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%08x]", + this, rv)); + + mListener = nullptr; + InitEmptyMetadata(); + aListener->OnMetadataRead(NS_OK); + return NS_OK; + } + + return 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<char *>(malloc(CalcMetadataSize(mElementsSize, + mHashCount))); + if (!mWriteBuf) { + return NS_ERROR_OUT_OF_MEMORY; + } + + char *p = mWriteBuf + sizeof(uint32_t); + 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++; + 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%08x]", this, 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<char *>(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<int32_t>(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) { + // Point to the value part + const char *value = data + strlen(data) + 1; + MOZ_ASSERT(value < limit, "Metadata elements corrupted"); + if (strcmp(data, aKey) == 0) { + LOG(("CacheFileMetadata::GetElement() - Key found [this=%p, key=%s]", + this, aKey)); + return value; + } + + // Skip value part + data = value + strlen(value) + 1; + } + MOZ_ASSERT(data == limit, "Metadata elements corrupted"); + 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)); + + MarkDirty(); + + nsresult rv; + + const uint32_t keySize = strlen(aKey) + 1; + char *pos = const_cast<char *>(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; +} + +nsresult +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"); + + return NS_OK; +} + +CacheHash::Hash16_t +CacheFileMetadata::GetHash(uint32_t aIndex) +{ + 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)); + + MarkDirty(); + + MOZ_ASSERT(aIndex <= mHashCount); + + if (aIndex > mHashCount) { + return NS_ERROR_INVALID_ARG; + } else 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<CacheHash::Hash16_t *>( + moz_xrealloc(mHashArray, mHashArraySize)); + } + + mHashCount++; + } + + NetworkEndian::writeUint16(&mHashArray[aIndex], aHash); + + DoMemoryReport(MemoryUsage()); + + return NS_OK; +} + +nsresult +CacheFileMetadata::AddFlags(uint32_t aFlags) +{ + MarkDirty(false); + mMetaHdr.mFlags |= aFlags; + return NS_OK; +} + +nsresult +CacheFileMetadata::RemoveFlags(uint32_t aFlags) +{ + MarkDirty(false); + mMetaHdr.mFlags &= ~aFlags; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetFlags(uint32_t *_retval) +{ + *_retval = mMetaHdr.mFlags; + return NS_OK; +} + +nsresult +CacheFileMetadata::SetExpirationTime(uint32_t aExpirationTime) +{ + LOG(("CacheFileMetadata::SetExpirationTime() [this=%p, expirationTime=%d]", + this, aExpirationTime)); + + MarkDirty(false); + mMetaHdr.mExpirationTime = aExpirationTime; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetExpirationTime(uint32_t *_retval) +{ + *_retval = mMetaHdr.mExpirationTime; + return NS_OK; +} + +nsresult +CacheFileMetadata::SetFrecency(uint32_t aFrecency) +{ + LOG(("CacheFileMetadata::SetFrecency() [this=%p, frecency=%f]", + this, (double)aFrecency)); + + MarkDirty(false); + mMetaHdr.mFrecency = aFrecency; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetFrecency(uint32_t *_retval) +{ + *_retval = mMetaHdr.mFrecency; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetLastModified(uint32_t *_retval) +{ + *_retval = mMetaHdr.mLastModified; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetLastFetched(uint32_t *_retval) +{ + *_retval = mMetaHdr.mLastFetched; + return NS_OK; +} + +nsresult +CacheFileMetadata::GetFetchCount(uint32_t *_retval) +{ + *_retval = mMetaHdr.mFetchCount; + return NS_OK; +} + +nsresult +CacheFileMetadata::OnFetched() +{ + MarkDirty(false); + + mMetaHdr.mLastFetched = NOW_SECONDS(); + ++mMetaHdr.mFetchCount; + return NS_OK; +} + +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%08x]", + this, aHandle, aResult)); + + MOZ_ASSERT(mListener); + MOZ_ASSERT(mWriteBuf); + + CacheFileUtils::FreeBuffer(mWriteBuf); + mWriteBuf = nullptr; + + nsCOMPtr<CacheFileMetadataListener> listener; + + mListener.swap(listener); + listener->OnMetadataWritten(aResult); + + DoMemoryReport(MemoryUsage()); + + return NS_OK; +} + +nsresult +CacheFileMetadata::OnDataRead(CacheFileHandle *aHandle, char *aBuf, + nsresult aResult) +{ + LOG(("CacheFileMetadata::OnDataRead() [this=%p, handle=%p, result=0x%08x]", + this, aHandle, aResult)); + + MOZ_ASSERT(mListener); + + nsresult rv; + nsCOMPtr<CacheFileMetadataListener> listener; + + if (NS_FAILED(aResult)) { + LOG(("CacheFileMetadata::OnDataRead() - CacheFileIOManager::Read() failed" + ", creating empty metadata. [this=%p, rv=0x%08x]", this, aResult)); + + InitEmptyMetadata(); + + mListener.swap(listener); + listener->OnMetadataRead(NS_OK); + return NS_OK; + } + + if (mFirstRead) { + Telemetry::AccumulateTimeDelta( + Telemetry::NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS, mReadStart); + Telemetry::Accumulate( + Telemetry::NETWORK_CACHE_METADATA_FIRST_READ_SIZE, mBufSize); + } 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=%lld]", this, + realOffset, size)); + + InitEmptyMetadata(); + + mListener.swap(listener); + listener->OnMetadataRead(NS_OK); + 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=%lld]", this, realOffset, maxMetadataSize, + size)); + + InitEmptyMetadata(); + + mListener.swap(listener); + listener->OnMetadataRead(NS_OK); + 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<char *>(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); + listener->OnMetadataRead(NS_OK); + 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%08x]", this, rv)); + + InitEmptyMetadata(); + + mListener.swap(listener); + listener->OnMetadataRead(NS_OK); + return NS_OK; + } + + return NS_OK; + } + + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_METADATA_SIZE, + 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<char *>(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); + listener->OnMetadataRead(NS_OK); + + 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<CacheHash::Hash16_t *>( + 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<char *>(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<nsILoadContextInfo> 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); + n += mallocSizeOf(mWriteBuf); + // mListener is usually the owning CacheFile. + + return n; +} + +size_t +CacheFileMetadata::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const +{ + return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileMetadata.h b/netwerk/cache2/CacheFileMetadata.h new file mode 100644 index 0000000000..97a0fff575 --- /dev/null +++ b/netwerk/cache2/CacheFileMetadata.h @@ -0,0 +1,227 @@ +/* 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 "nsAutoPtr.h" +#include "nsString.h" + +class nsICacheEntryMetaDataVisitor; + +namespace mozilla { +namespace net { + +// 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<uint8_t*>(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<const uint8_t*>(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 : public CacheFileIOListener + , public CacheMemoryConsumer +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + CacheFileMetadata(CacheFileHandle *aHandle, + const nsACString &aKey); + CacheFileMetadata(bool aMemoryOnly, + bool aPinned, + const nsACString &aKey); + CacheFileMetadata(); + + void SetHandle(CacheFileHandle *aHandle); + + nsresult GetKey(nsACString &_retval); + + nsresult 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::NeckoOriginAttributes 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); + nsresult Visit(nsICacheEntryMetaDataVisitor *aVisitor); + + CacheHash::Hash16_t GetHash(uint32_t aIndex); + nsresult SetHash(uint32_t aIndex, CacheHash::Hash16_t aHash); + + nsresult AddFlags(uint32_t aFlags); + nsresult RemoveFlags(uint32_t aFlags); + nsresult GetFlags(uint32_t *_retval); + nsresult SetExpirationTime(uint32_t aExpirationTime); + nsresult GetExpirationTime(uint32_t *_retval); + nsresult SetFrecency(uint32_t aFrecency); + nsresult GetFrecency(uint32_t *_retval); + nsresult GetLastModified(uint32_t *_retval); + nsresult GetLastFetched(uint32_t *_retval); + nsresult GetFetchCount(uint32_t *_retval); + // Called by upper layers to indicate the entry this metadata belongs + // with has been fetched, i.e. delivered to the consumer. + nsresult 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<CacheFileHandle> mHandle; + nsCString mKey; + CacheHash::Hash16_t *mHashArray; + uint32_t mHashArraySize; + uint32_t mHashCount; + int64_t mOffset; + char *mBuf; // used for parsing, then points + // to elements + uint32_t mBufSize; + char *mWriteBuf; + CacheFileMetadataHeader mMetaHdr; + uint32_t mElementsSize; + bool mIsDirty : 1; + bool mAnonymous : 1; + bool mAllocExactSize : 1; + bool mFirstRead : 1; + mozilla::NeckoOriginAttributes mOriginAttributes; + mozilla::TimeStamp mReadStart; + nsCOMPtr<CacheFileMetadataListener> mListener; +}; + + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileOutputStream.cpp b/netwerk/cache2/CacheFileOutputStream.cpp new file mode 100644 index 0000000000..a3d414b8fa --- /dev/null +++ b/netwerk/cache2/CacheFileOutputStream.cpp @@ -0,0 +1,483 @@ +/* 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 <algorithm> + +namespace mozilla { +namespace net { + +NS_IMPL_ADDREF(CacheFileOutputStream) +NS_IMETHODIMP_(MozExternalRefCountType) +CacheFileOutputStream::Release() +{ + NS_PRECONDITION(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(mozilla::net::CacheFileChunkListener) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIOutputStream) +NS_INTERFACE_MAP_END_THREADSAFE + +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)); + MOZ_COUNT_CTOR(CacheFileOutputStream); + + if (mAlternativeData) { + mPos = mFile->mAltDataOffset; + } +} + +CacheFileOutputStream::~CacheFileOutputStream() +{ + LOG(("CacheFileOutputStream::~CacheFileOutputStream() [this=%p]", this)); + MOZ_COUNT_DTOR(CacheFileOutputStream); +} + +// 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::Write(const char * aBuf, uint32_t aCount, + uint32_t *_retval) +{ + CacheFileAutoLock lock(mFile); + + LOG(("CacheFileOutputStream::Write() [this=%p, count=%d]", this, aCount)); + + if (mClosed) { + LOG(("CacheFileOutputStream::Write() - Stream is closed. [this=%p, " + "status=0x%08x]", this, 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, failing and " + "dooming the entry. [this=%p]", this)); + + mFile->DoomLocked(nullptr); + CloseWithStatusLocked(NS_ERROR_FILE_TOO_BIG); + return NS_ERROR_FILE_TOO_BIG; + } + + // We use 64-bit offset when accessing the file, unfortunatelly 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 while it " + "isn't too big according to CacheObserver::EntryIsTooBig(). Failing " + "and dooming the entry. [this=%p]", this)); + + mFile->DoomLocked(nullptr); + 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<uint32_t>(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%08x]", + this, aStatus)); + + return CloseWithStatusLocked(aStatus); +} + +nsresult +CacheFileOutputStream::CloseWithStatusLocked(nsresult aStatus) +{ + LOG(("CacheFileOutputStream::CloseWithStatusLocked() [this=%p, " + "aStatus=0x%08x]", this, 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); + + LOG(("CacheFileOutputStream::Seek() [this=%p, whence=%d, offset=%lld]", + 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=%lld]", this, mPos)); + return NS_OK; +} + +NS_IMETHODIMP +CacheFileOutputStream::Tell(int64_t *_retval) +{ + CacheFileAutoLock lock(mFile); + + 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=%lld]", this, *_retval)); + 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; +} + +// 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<CacheOutputCloseListener> listener; + listener.swap(mCloseListener); + if (!listener) + return; + + listener->OnOutputClosed(); +} + +void +CacheFileOutputStream::ReleaseChunk() +{ + LOG(("CacheFileOutputStream::ReleaseChunk() [this=%p, idx=%d]", + this, mChunk->Index())); + + mFile->ReleaseOutsideLock(mChunk.forget()); +} + +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; + } + else { + 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%08x]", this, chunkIdx, 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 = do_GetMainThread(); + } + } + + nsCOMPtr<nsIOutputStreamCallback> 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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileOutputStream.h b/netwerk/cache2/CacheFileOutputStream.h new file mode 100644 index 0000000000..c283d702b6 --- /dev/null +++ b/netwerk/cache2/CacheFileOutputStream.h @@ -0,0 +1,73 @@ +/* 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 "nsAutoPtr.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 + +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<CacheFile> mFile; + RefPtr<CacheFileChunk> mChunk; + RefPtr<CacheOutputCloseListener> mCloseListener; + int64_t mPos; + bool mClosed : 1; + bool const mAlternativeData : 1; + nsresult mStatus; + + nsCOMPtr<nsIOutputStreamCallback> mCallback; + uint32_t mCallbackFlags; + nsCOMPtr<nsIEventTarget> mCallbackTarget; +}; + + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheFileUtils.cpp b/netwerk/cache2/CacheFileUtils.cpp new file mode 100644 index 0000000000..d43e958bff --- /dev/null +++ b/netwerk/cache2/CacheFileUtils.cpp @@ -0,0 +1,575 @@ +/* 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 "LoadContextInfo.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/Telemetry.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "nsString.h" +#include <algorithm> +#include "mozilla/Unused.h" + + +namespace mozilla { +namespace net { +namespace 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) + // Initialize attributes to their default values + , originAttribs(0, false) + , isAnonymous(false) + // Initialize the cache key to a zero length by default + , lastTag(0) + { + } + +private: + // Results + NeckoOriginAttributes 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 + static bool TagChar(const char aChar) + { + return aChar >= ' ' && aChar <= '~'; + } + + 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 + if (!ReadInteger(&originAttribs.mAppId)) { + 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<LoadContextInfo> Parse() + { + RefPtr<LoadContextInfo> 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<nsILoadContextInfo> +ParseKey(const nsCSubstring &aKey, + nsCSubstring *aIdEnhance, + nsCSubstring *aURISpec) +{ + KeyParser parser(aKey); + RefPtr<LoadContextInfo> 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. + */ + + NeckoOriginAttributes 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, nsCSubstring const & 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_LITERAL_CSTRING(","), NS_LITERAL_CSTRING(",,")); + aTarget.Append(escapedValue); + } + } + + aTarget.Append(','); +} + +nsresult +KeyMatchesLoadContextInfo(const nsACString &aKey, nsILoadContextInfo *aInfo, + bool *_retval) +{ + nsCOMPtr<nsILoadContextInfo> 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) +{} + +ValidityPair& +ValidityPair::operator=(const ValidityPair& aOther) +{ + mOffset = aOther.mOffset; + mLen = aOther.mLen; + return *this; +} + +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: %u", 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(); + } + } +} + +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" + // <version>;<offset>,<type> + 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; + } + + *_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 CacheFileUtils +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheFileUtils.h b/netwerk/cache2/CacheFileUtils.h new file mode 100644 index 0000000000..3371c3eb56 --- /dev/null +++ b/netwerk/cache2/CacheFileUtils.h @@ -0,0 +1,164 @@ +/* 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/StaticMutex.h" +#include "mozilla/TimeStamp.h" + +class nsILoadContextInfo; +class nsACString; + +namespace mozilla { +namespace net { +namespace CacheFileUtils { + +extern const char *kAltDataKey; + +already_AddRefed<nsILoadContextInfo> +ParseKey(const nsCSubstring &aKey, + nsCSubstring *aIdEnhance = nullptr, + nsCSubstring *aURISpec = nullptr); + +void +AppendKeyPrefix(nsILoadContextInfo *aInfo, nsACString &_retval); + +void +AppendTagWithValue(nsACString & aTarget, char const aTag, nsCSubstring const & 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); + + // 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<ValidityPair> 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; + uint32_t mMissCnt; + }; + + // 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, sHitStats and Telemetry::Accumulated() calls. + static StaticMutex sLock; + + // Counter of samples that is compared against kTotalSamplesReportLimit. + static uint32_t sRecordCnt; + + // Hit rate statistics for every cache size range. + static HitRate sHRStats[kNumOfRanges]; +}; + +void +FreeBuffer(void *aBuf); + +nsresult +ParseAlternativeDataInfo(const char *aInfo, int64_t *_offset, nsACString *_type); + +void +BuildAlternativeDataInfo(const char *aInfo, int64_t aOffset, nsACString &_retval); + +} // 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..1f816e3471 --- /dev/null +++ b/netwerk/cache2/CacheHashUtils.cpp @@ -0,0 +1,206 @@ +/* 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 "plstr.h" + +namespace mozilla { +namespace 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<const uint8_t*>(aData); + uint32_t a, b, c, len; + +// length = PL_strlen(key); + /* 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); MOZ_FALLTHROUGH; + case 10: c += (uint32_t(k[9])<<16); MOZ_FALLTHROUGH; + case 9 : c += (uint32_t(k[8])<<8); MOZ_FALLTHROUGH; + /* the low-order byte of c is reserved for the length */ + case 8 : b += (uint32_t(k[7])<<24); MOZ_FALLTHROUGH; + case 7 : b += (uint32_t(k[6])<<16); MOZ_FALLTHROUGH; + case 6 : b += (uint32_t(k[5])<<8); MOZ_FALLTHROUGH; + case 5 : b += k[4]; MOZ_FALLTHROUGH; + case 4 : a += (uint32_t(k[3])<<24); MOZ_FALLTHROUGH; + case 3 : a += (uint32_t(k[2])<<16); MOZ_FALLTHROUGH; + case 2 : a += (uint32_t(k[1])<<8); MOZ_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) + : mA(0x9e3779b9) + , mB(0x9e3779b9) + , mC(aInitval) + , mPos(0) + , mBuf(0) + , mBufPos(0) + , mLength(0) + , mFinalized(false) +{} + +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<const uint8_t*>(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; MOZ_FALLTHROUGH; + case 2: mBuf += data[1] << 8; MOZ_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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheHashUtils.h b/netwerk/cache2/CacheHashUtils.h new file mode 100644 index 0000000000..2af3fed808 --- /dev/null +++ b/netwerk/cache2/CacheHashUtils.h @@ -0,0 +1,67 @@ +/* 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<const uint32_t *>(x))[0]), \ + PR_htonl((reinterpret_cast<const uint32_t *>(x))[1]), \ + PR_htonl((reinterpret_cast<const uint32_t *>(x))[2]), \ + PR_htonl((reinterpret_cast<const uint32_t *>(x))[3]), \ + PR_htonl((reinterpret_cast<const uint32_t *>(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 + + typedef uint16_t Hash16_t; + typedef uint32_t Hash32_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() {} + + void Feed(uint32_t aVal, uint8_t aLen = 4); + + uint32_t mA, mB, mC; + uint8_t mPos; + uint32_t mBuf; + uint8_t mBufPos; + uint32_t mLength; + bool mFinalized; +}; + +typedef uint64_t OriginAttrsHash; + +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..b96f03216c --- /dev/null +++ b/netwerk/cache2/CacheIOThread.cpp @@ -0,0 +1,646 @@ +/* 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 "nsIRunnable.h" +#include "nsISupportsImpl.h" +#include "nsPrintfCString.h" +#include "nsThreadUtils.h" +#include "mozilla/IOInterposer.h" + +#ifdef XP_WIN +#include <windows.h> +#endif + +namespace mozilla { +namespace net { + +namespace { // anon + +class CacheIOTelemetry +{ +public: + typedef CacheIOThread::EventQueue::size_type 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::ID 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<size_type>(aLength, 10); + + Telemetry::Accumulate(telemetryID[aLevel], aLength - 1); // counted from 0 +} + +} // anon + +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 BlockingIOWatcher +{ +#ifdef XP_WIN + typedef BOOL(WINAPI* TCancelSynchronousIo)(HANDLE hThread); + TCancelSynchronousIo mCancelSynchronousIo; + // The native handle to the thread + HANDLE mThread; + // Event signaling back to the main thread, see NotifyOperationDone. + HANDLE mEvent; +#endif + +public: + // Created and destroyed on the main thread only + BlockingIOWatcher(); + ~BlockingIOWatcher(); + + // 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. + // Waits for notification from the IO thread for up to two seconds. + // If that times out, it attempts to cancel the IO operation. + void WatchAndCancel(Monitor& aMonitor); + // Called by the IO thread after each operation has been + // finished (after each Run() call). This wakes the main + // thread up and makes WatchAndCancel() early exit and become + // a no-op. + void NotifyOperationDone(); +}; + +#ifdef XP_WIN + +BlockingIOWatcher::BlockingIOWatcher() + : mCancelSynchronousIo(NULL) + , mThread(NULL) + , mEvent(NULL) +{ + HMODULE kernel32_dll = GetModuleHandle("kernel32.dll"); + if (!kernel32_dll) { + return; + } + + FARPROC ptr = GetProcAddress(kernel32_dll, "CancelSynchronousIo"); + if (!ptr) { + return; + } + + mCancelSynchronousIo = reinterpret_cast<TCancelSynchronousIo>(ptr); + + mEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL); +} + +BlockingIOWatcher::~BlockingIOWatcher() +{ + if (mEvent) { + CloseHandle(mEvent); + } + if (mThread) { + CloseHandle(mThread); + } +} + +void BlockingIOWatcher::InitThread() +{ + // GetCurrentThread() only returns a pseudo handle, hence DuplicateHandle + BOOL result = ::DuplicateHandle( + GetCurrentProcess(), + GetCurrentThread(), + GetCurrentProcess(), + &mThread, + 0, + FALSE, + DUPLICATE_SAME_ACCESS); +} + +void BlockingIOWatcher::WatchAndCancel(Monitor& aMonitor) +{ + if (!mEvent) { + return; + } + + // Reset before we enter the monitor to raise the chance we catch + // the currently pending IO op completion. + ::ResetEvent(mEvent); + + HANDLE thread; + { + MonitorAutoLock lock(aMonitor); + thread = mThread; + + if (!thread) { + return; + } + } + + LOG(("Blocking IO operation pending on IO thread, waiting...")); + + // It seems wise to use the I/O lag time as a maximum time to wait + // for an operation to finish. When that times out and cancelation + // succeeds, there will be no other IO operation permitted. By default + // this is two seconds. + uint32_t maxLag = std::min<uint32_t>(5, CacheObserver::MaxShutdownIOLag()) * 1000; + + DWORD result = ::WaitForSingleObject(mEvent, maxLag); + if (result == WAIT_TIMEOUT) { + LOG(("CacheIOThread: Attempting to cancel a long blocking IO operation")); + BOOL result = mCancelSynchronousIo(thread); + if (result) { + LOG((" cancelation signal succeeded")); + } else { + DWORD error = GetLastError(); + LOG((" cancelation signal failed with GetLastError=%u", error)); + } + } +} + +void BlockingIOWatcher::NotifyOperationDone() +{ + if (mEvent) { + ::SetEvent(mEvent); + } +} + +#else // WIN + +// Stub code only (we don't implement IO cancelation for this platform) + +BlockingIOWatcher::BlockingIOWatcher() { } +BlockingIOWatcher::~BlockingIOWatcher() { } +void BlockingIOWatcher::InitThread() { } +void BlockingIOWatcher::WatchAndCancel(Monitor&) { } +void BlockingIOWatcher::NotifyOperationDone() { } + +#endif + +} // detail + +CacheIOThread* CacheIOThread::sSelf = nullptr; + +NS_IMPL_ISUPPORTS(CacheIOThread, nsIThreadObserver) + +CacheIOThread::CacheIOThread() +: mMonitor("CacheIOThread") +, mThread(nullptr) +, mXPCOMThread(nullptr) +, mLowestLevelWaiting(LAST_LEVEL) +, mCurrentlyExecutingLevel(0) +, mHasXPCOMEvents(false) +, mRerunCurrentEvent(false) +, mShutdown(false) +, mIOCancelableEvents(0) +#ifdef DEBUG +, mInsideLoop(true) +#endif +{ + for (uint32_t i = 0; i < LAST_LEVEL; ++i) { + mQueueLength[i] = 0; + } + + sSelf = this; +} + +CacheIOThread::~CacheIOThread() +{ + if (mXPCOMThread) { + nsIThread *thread = mXPCOMThread; + thread->Release(); + } + + sSelf = nullptr; +#ifdef DEBUG + for (uint32_t level = 0; level < LAST_LEVEL; ++level) { + MOZ_ASSERT(!mEventQueue[level].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. + mBlockingIOWatcher = MakeUnique<detail::BlockingIOWatcher>(); + } + + 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; + } + + return NS_OK; +} + +nsresult CacheIOThread::Dispatch(nsIRunnable* aRunnable, uint32_t aLevel) +{ + return Dispatch(do_AddRef(aRunnable), aLevel); +} + +nsresult CacheIOThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable, + uint32_t aLevel) +{ + NS_ENSURE_ARG(aLevel < LAST_LEVEL); + + nsCOMPtr<nsIRunnable> 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<nsIRunnable> aRunnable, + uint32_t aLevel) +{ + nsCOMPtr<nsIRunnable> runnable(aRunnable); + + 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 (!mBlockingIOWatcher) { + 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. + mBlockingIOWatcher->WatchAndCancel(mMonitor); +} + +already_AddRefed<nsIEventTarget> CacheIOThread::Target() +{ + nsCOMPtr<nsIEventTarget> target; + + target = mXPCOMThread; + if (!target && mThread) + { + MonitorAutoLock lock(mMonitor); + while (!mXPCOMThread) { + lock.Wait(); + } + + target = mXPCOMThread; + } + + return target.forget(); +} + +// static +void CacheIOThread::ThreadFunc(void* aClosure) +{ + PR_SetCurrentThreadName("Cache2 I/O"); + mozilla::IOInterposer::RegisterCurrentThread(); + CacheIOThread* thread = static_cast<CacheIOThread*>(aClosure); + thread->ThreadFunc(); + mozilla::IOInterposer::UnregisterCurrentThread(); +} + +void CacheIOThread::ThreadFunc() +{ + nsCOMPtr<nsIThreadInternal> threadInternal; + + { + MonitorAutoLock lock(mMonitor); + + MOZ_ASSERT(mBlockingIOWatcher); + mBlockingIOWatcher->InitThread(); + + // This creates nsThread for this PRThread + nsCOMPtr<nsIThread> xpcomThread = NS_GetCurrentThread(); + + threadInternal = do_QueryInterface(xpcomThread); + if (threadInternal) + threadInternal->SetObserver(this); + + mXPCOMThread = xpcomThread.forget().take(); + + 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 { + nsIThread *thread = mXPCOMThread; + rv = thread->ProcessNextEvent(false, &processedEvent); + + MOZ_ASSERT(mBlockingIOWatcher); + mBlockingIOWatcher->NotifyOperationDone(); + } 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; + } + + lock.Wait(PR_INTERVAL_NO_TIMEOUT); + + } 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) +{ + EventQueue events; + events.SwapElements(mEventQueue[aLevel]); + EventQueue::size_type length = events.Length(); + + mCurrentlyExecutingLevel = aLevel; + + bool returnEvents = false; + bool reportTelementry = 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 (reportTelementry) { + reportTelementry = false; + CacheIOTelemetry::Report(aLevel, length); + } + + // Drop any previous flagging, only an event on the current level may set + // this flag. + mRerunCurrentEvent = false; + + events[index]->Run(); + + MOZ_ASSERT(mBlockingIOWatcher); + mBlockingIOWatcher->NotifyOperationDone(); + + if (mRerunCurrentEvent) { + // The event handler yields to higher priority events and wants to rerun. + returnEvents = true; + break; + } + + --mQueueLength[aLevel]; + + // Release outside the lock. + events[index] = nullptr; + } + } + + if (returnEvents) + mEventQueue[aLevel].InsertElementsAt(0, events.Elements() + index, length - index); +} + +bool CacheIOThread::EventsPending(uint32_t aLastLevel) +{ + return mLowestLevelWaiting < aLastLevel || mHasXPCOMEvents; +} + +NS_IMETHODIMP CacheIOThread::OnDispatchedEvent(nsIThreadInternal *thread) +{ + 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<CacheIOThread*>(this)->mMonitor); + + size_t n = 0; + n += mallocSizeOf(mThread); + for (uint32_t level = 0; level < LAST_LEVEL; ++level) { + n += mEventQueue[level].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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheIOThread.h b/netwerk/cache2/CacheIOThread.h new file mode 100644 index 0000000000..ea71f7df0a --- /dev/null +++ b/netwerk/cache2/CacheIOThread.h @@ -0,0 +1,147 @@ +/* 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 "nsAutoPtr.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 watch and +// cancel any long blocking synchronous IO. Must be predeclared here +// since including windows.h breaks stuff with number of macro definition +// conflicts. +class BlockingIOWatcher; +} + +class CacheIOThread : public nsIThreadObserver +{ + virtual ~CacheIOThread(); + +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + CacheIOThread(); + + typedef nsTArray<nsCOMPtr<nsIRunnable>> EventQueue; + + 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<nsIRunnable>, 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); + + /** + * 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<nsIEventTarget> 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); + bool EventsPending(uint32_t aLastLevel = LAST_LEVEL); + nsresult DispatchInternal(already_AddRefed<nsIRunnable> aRunnable, uint32_t aLevel); + bool YieldInternal(); + + static CacheIOThread* sSelf; + + mozilla::Monitor mMonitor; + PRThread* mThread; + UniquePtr<detail::BlockingIOWatcher> mBlockingIOWatcher; + Atomic<nsIThread *> mXPCOMThread; + Atomic<uint32_t, Relaxed> mLowestLevelWaiting; + uint32_t mCurrentlyExecutingLevel; + + // Keeps the length of the each event queue, since LoopOneLevel moves all + // events into a local array. + Atomic<int32_t> mQueueLength[LAST_LEVEL]; + + EventQueue mEventQueue[LAST_LEVEL]; + // Raised when nsIEventTarget.Dispatch() is called on this thread + Atomic<bool, Relaxed> mHasXPCOMEvents; + // See YieldAndRerun() above + bool mRerunCurrentEvent; + // Signal to process all pending events and then shutdown + // Synchronized by mMonitor + bool mShutdown; + // 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<uint32_t, Relaxed> mIOCancelableEvents; +#ifdef DEBUG + bool mInsideLoop; +#endif +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndex.cpp b/netwerk/cache2/CacheIndex.cpp new file mode 100644 index 0000000000..4525bbe6d6 --- /dev/null +++ b/netwerk/cache2/CacheIndex.cpp @@ -0,0 +1,3810 @@ +/* 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 "CacheIndexIterator.h" +#include "CacheIndexContextIterator.h" +#include "nsThreadUtils.h" +#include "nsISimpleEnumerator.h" +#include "nsIDirectoryEnumerator.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 <algorithm> +#include "mozilla/Telemetry.h" +#include "mozilla/Unused.h" + + +#define kMinUnwrittenChanges 300 +#define kMinDumpInterval 20000 // in milliseconds +#define kMaxBufSize 16384 +#define kIndexVersion 0x00000003 +#define kUpdateIndexStartDelay 50000 // in milliseconds + +#define INDEX_NAME "index" +#define TEMP_INDEX_NAME "index.tmp" +#define JOURNAL_NAME "index.log" + +namespace mozilla { +namespace net { + +namespace { + +class FrecencyComparator +{ +public: + bool Equals(CacheIndexRecord* a, CacheIndexRecord* b) const { + if (!a || !b) { + return false; + } + + return a->mFrecency == b->mFrecency; + } + bool LessThan(CacheIndexRecord* a, CacheIndexRecord* 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->mFrecency == 0) { + return false; + } + if (b->mFrecency == 0) { + return true; + } + + return a->mFrecency < b->mFrecency; + } +}; + +} // namespace + +/** + * This helper class is responsible for keeping CacheIndex::mIndexStats and + * CacheIndex::mFrecencyArray up to date. + */ +class CacheIndexEntryAutoManage +{ +public: + CacheIndexEntryAutoManage(const SHA1Sum::Hash *aHash, CacheIndex *aIndex) + : mIndex(aIndex) + , mOldRecord(nullptr) + , mOldFrecency(0) + , mDoNotSearchInIndex(false) + , mDoNotSearchInUpdates(false) + { + CacheIndex::sLock.AssertCurrentThreadOwns(); + + mHash = aHash; + const CacheIndexEntry *entry = FindEntry(); + mIndex->mIndexStats.BeforeChange(entry); + if (entry && entry->IsInitialized() && !entry->IsRemoved()) { + mOldRecord = entry->mRec; + mOldFrecency = entry->mRec->mFrecency; + } + } + + ~CacheIndexEntryAutoManage() + { + CacheIndex::sLock.AssertCurrentThreadOwns(); + + const CacheIndexEntry *entry = FindEntry(); + mIndex->mIndexStats.AfterChange(entry); + if (!entry || !entry->IsInitialized() || entry->IsRemoved()) { + entry = nullptr; + } + + if (entry && !mOldRecord) { + mIndex->mFrecencyArray.AppendRecord(entry->mRec); + mIndex->AddRecordToIterators(entry->mRec); + } else if (!entry && mOldRecord) { + mIndex->mFrecencyArray.RemoveRecord(mOldRecord); + mIndex->RemoveRecordFromIterators(mOldRecord); + } else if (entry && mOldRecord) { + if (entry->mRec != mOldRecord) { + // record has a different address, we have to replace it + mIndex->ReplaceRecordInIterators(mOldRecord, entry->mRec); + + if (entry->mRec->mFrecency == mOldFrecency) { + // If frecency hasn't changed simply replace the pointer + mIndex->mFrecencyArray.ReplaceRecord(mOldRecord, entry->mRec); + } else { + // Remove old pointer and insert the new one at the end of the array + mIndex->mFrecencyArray.RemoveRecord(mOldRecord); + mIndex->mFrecencyArray.AppendRecord(entry->mRec); + } + } else if (entry->mRec->mFrecency != mOldFrecency) { + // Move the element at the end of the array + mIndex->mFrecencyArray.RemoveRecord(entry->mRec); + mIndex->mFrecencyArray.AppendRecord(entry->mRec); + } + } 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() + { + const CacheIndexEntry *entry = nullptr; + + switch (mIndex->mState) { + case CacheIndex::READING: + case CacheIndex::WRITING: + if (!mDoNotSearchInUpdates) { + entry = mIndex->mPendingUpdates.GetEntry(*mHash); + } + MOZ_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<CacheIndex> mIndex; + CacheIndexRecord *mOldRecord; + uint32_t mOldFrecency; + bool mDoNotSearchInIndex; + bool mDoNotSearchInUpdates; +}; + +class FileOpenHelper : 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() {} + + 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<CacheIndex> 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); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(FileOpenHelper, CacheFileIOListener); + + +StaticRefPtr<CacheIndex> 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_THREADSAFE + + +CacheIndex::CacheIndex() + : mState(INITIAL) + , mShuttingDown(false) + , mIndexNeedsUpdate(false) + , mRemovingAll(false) + , mIndexOnDiskIsValid(false) + , mDontMarkIndexClean(false) + , mIndexTimeStamp(0) + , mUpdateEventPending(false) + , mSkipEntries(0) + , mProcessEntries(0) + , mRWBuf(nullptr) + , mRWBufSize(0) + , mRWBufPos(0) + , mRWPending(false) + , mJournalReadSuccessfully(false) + , mAsyncGetDiskConsumptionBlocked(false) +{ + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::CacheIndex [this=%p]", this)); + MOZ_COUNT_CTOR(CacheIndex); + MOZ_ASSERT(!gInstance, "multiple CacheIndex instances!"); +} + +CacheIndex::~CacheIndex() +{ + sLock.AssertCurrentThreadOwns(); + LOG(("CacheIndex::~CacheIndex [this=%p]", this)); + MOZ_COUNT_DTOR(CacheIndex); + + 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<CacheIndex> idx = new CacheIndex(); + + nsresult rv = idx->InitInternal(aCacheDirectory); + NS_ENSURE_SUCCESS(rv, rv); + + gInstance = idx.forget(); + return NS_OK; +} + +nsresult +CacheIndex::InitInternal(nsIFile *aCacheDirectory) +{ + nsresult rv; + + rv = aCacheDirectory->Clone(getter_AddRefs(mCacheDirectory)); + NS_ENSURE_SUCCESS(rv, rv); + + mStartTime = TimeStamp::NowLoRes(); + + ReadIndexFromDisk(); + + return NS_OK; +} + +// static +nsresult +CacheIndex::PreShutdown() +{ + MOZ_ASSERT(NS_IsMainThread()); + + StaticMutexAutoLock lock(sLock); + + LOG(("CacheIndex::PreShutdown() [gInstance=%p]", gInstance.get())); + + nsresult rv; + RefPtr<CacheIndex> 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%08x]", rv)); + i++; + } + } + + index->mShuttingDown = true; + + if (index->mState == READY) { + return NS_OK; // nothing to do + } + + nsCOMPtr<nsIRunnable> event; + event = NewRunnableMethod(index, &CacheIndex::PreShutdownInternal); + + nsCOMPtr<nsIEventTarget> 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 = nullptr; + } + + switch (mState) { + case WRITING: + FinishWrite(false); + break; + case READY: + // nothing to do, write the journal in Shutdown() + break; + case READING: + FinishRead(false); + break; + case BUILDING: + case UPDATING: + FinishUpdate(false); + 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<CacheIndex> 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); + + if (oldState != READY) { + LOG(("CacheIndex::Shutdown() - Unexpected state. Did posting of " + "PreShutdownInternal() fail?")); + } + + switch (oldState) { + case WRITING: + index->FinishWrite(false); + MOZ_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); + break; + case BUILDING: + case UPDATING: + index->FinishUpdate(false); + 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<CacheIndex> 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); + + 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(); + index->WriteIndexToDiskIfNeeded(); + + 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<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index); + + 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(); + index->WriteIndexToDiskIfNeeded(); + + 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=%llx, anonymous=%d, pinned=%d]", LOGSHA1(aHash), + aOriginAttrsHash, aAnonymous, aPinned)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index); + + 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 (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<bool> removed = updated && updated->IsRemoved(); + + MOZ_ASSERT(updated || !removed); + MOZ_ASSERT(updated || entry); + + 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(); + index->WriteIndexToDiskIfNeeded(); + + 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<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index); + + 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(); + index->WriteIndexToDiskIfNeeded(); + + return NS_OK; +} + +// static +nsresult +CacheIndex::UpdateEntry(const SHA1Sum::Hash *aHash, + const uint32_t *aFrecency, + const uint32_t *aExpirationTime, + const uint32_t *aSize) +{ + LOG(("CacheIndex::UpdateEntry() [hash=%08x%08x%08x%08x%08x, " + "frecency=%s, expirationTime=%s, size=%s]", LOGSHA1(aHash), + aFrecency ? nsPrintfCString("%u", *aFrecency).get() : "", + aExpirationTime ? nsPrintfCString("%u", *aExpirationTime).get() : "", + aSize ? nsPrintfCString("%u", *aSize).get() : "")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + { + CacheIndexEntryAutoManage entryMng(aHash, index); + + 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 (!HasEntryChanged(entry, aFrecency, aExpirationTime, aSize)) { + return NS_OK; + } + + MOZ_ASSERT(entry->IsFresh()); + MOZ_ASSERT(entry->IsInitialized()); + entry->MarkDirty(); + + if (aFrecency) { + entry->SetFrecency(*aFrecency); + } + + if (aExpirationTime) { + entry->SetExpirationTime(*aExpirationTime); + } + + if (aSize) { + entry->SetFileSize(*aSize); + } + } else { + CacheIndexEntryUpdate *updated = index->mPendingUpdates.GetEntry(*aHash); + DebugOnly<bool> 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_NOT_AVAILABLE; + } + + // 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 (aExpirationTime) { + updated->SetExpirationTime(*aExpirationTime); + } + + if (aSize) { + updated->SetFileSize(*aSize); + } + } + } + + index->WriteIndexToDiskIfNeeded(); + + return NS_OK; +} + +// static +nsresult +CacheIndex::RemoveAll() +{ + LOG(("CacheIndex::RemoveAll()")); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsCOMPtr<nsIFile> file; + + { + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + MOZ_ASSERT(!index->mRemovingAll); + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + AutoRestore<bool> 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(NS_LITERAL_CSTRING(INDEX_NAME), getter_AddRefs(file)); + } + + if (index->mJournalHandle) { + CacheFileIOManager::DoomFile(index->mJournalHandle, nullptr); + } + + switch (index->mState) { + case WRITING: + index->FinishWrite(false); + break; + case READY: + // nothing to do + break; + case READING: + index->FinishRead(false); + break; + case BUILDING: + case UPDATING: + index->FinishUpdate(false); + 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(); + 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%08x]", 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, bool *_pinned) +{ + 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, _pinned); +} + +// static +nsresult +CacheIndex::HasEntry(const SHA1Sum::Hash &hash, EntryStatus *_retval, bool *_pinned) +{ + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (_pinned) { + *_pinned = false; + } + + const CacheIndexEntry *entry = nullptr; + + switch (index->mState) { + case READING: + case WRITING: + entry = index->mPendingUpdates.GetEntry(hash); + MOZ_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 (_pinned && entry->IsPinned()) { + *_pinned = true; + } + } + } + + 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<CacheIndex> index = gInstance; + + if (!index) + return NS_ERROR_NOT_INITIALIZED; + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + 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(); + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + CacheIndexRecord *rec = iter.Get(); + + memcpy(&hash, rec->mHash, sizeof(SHA1Sum::Hash)); + + ++skipped; + + 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 from frecency " + "array [hash=%08x%08x%08x%08x%08x, cnt=%u, frecency=%u]", + LOGSHA1(&hash), *aCnt, foundRecord->mFrecency)); + + memcpy(aHash, &hash, sizeof(SHA1Sum::Hash)); + + return NS_OK; +} + + +// static +bool CacheIndex::IsForcedValidEntry(const SHA1Sum::Hash *aHash) +{ + RefPtr<CacheFileHandle> 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<CacheIndex> 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<CacheIndex> 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<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + if (!aInfo) { + return NS_ERROR_INVALID_ARG; + } + + *aSize = 0; + *aCount = 0; + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + CacheIndexRecord *record = iter.Get(); + if (!CacheIndexEntry::RecordMatchesLoadContextInfo(record, aInfo)) + continue; + + *aSize += CacheIndexEntry::GetFileSize(record); + ++*aCount; + } + + return NS_OK; +} + +// static +nsresult +CacheIndex::AsyncGetDiskConsumption(nsICacheStorageConsumptionObserver* aObserver) +{ + LOG(("CacheIndex::AsyncGetDiskConsumption()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<DiskConsumptionObserver> 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<CacheIOThread> ioThread = CacheFileIOManager::IOThread(); + if (ioThread) { + ioThread->Dispatch(NS_NewRunnableFunction([]() -> void { + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> index = gInstance; + if (index && index->mUpdateTimer) { + index->mUpdateTimer->Cancel(); + index->DelayedUpdateLocked(); + } + }), 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<CacheIndex> index = gInstance; + + if (!index) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (!index->IsIndexUsable()) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr<CacheIndexIterator> idxIter; + if (aInfo) { + idxIter = new CacheIndexContextIterator(index, aAddNew, aInfo); + } else { + idxIter = new CacheIndexIterator(index, aAddNew); + } + + index->mFrecencyArray.SortIfNeeded(); + + for (auto iter = index->mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + idxIter->AddRecord(iter.Get()); + } + + index->mIterators.AppendElement(idxIter); + idxIter.swap(*_retval); + return NS_OK; +} + +// static +nsresult +CacheIndex::IsUpToDate(bool *_retval) +{ + LOG(("CacheIndex::IsUpToDate()")); + + StaticMutexAutoLock lock(sLock); + + RefPtr<CacheIndex> 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 %p", *_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=%llx, " + "anonymous=%d; actual values: originAttrsHash=%llx, 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 uint32_t *aExpirationTime, + const uint32_t *aSize) +{ + if (aFrecency && *aFrecency != aEntry->GetFrecency()) { + return true; + } + + if (aExpirationTime && *aExpirationTime != aEntry->GetExpirationTime()) { + return true; + } + + if (aSize && + (*aSize & CacheIndexEntry::kFileSizeMask) != aEntry->GetFileSize()) { + return true; + } + + return false; +} + +void +CacheIndex::ProcessPendingOperations() +{ + LOG(("CacheIndex::ProcessPendingOperations()")); + + sLock.AssertCurrentThreadOwns(); + + 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); + 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(*update->Hash()); + 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() +{ + if (mState != READY || mShuttingDown || mRWPending) { + return false; + } + + if (!mLastDumpTime.IsNull() && + (TimeStamp::NowLoRes() - mLastDumpTime).ToMilliseconds() < + kMinDumpInterval) { + return false; + } + + if (mIndexStats.Dirty() < kMinUnwrittenChanges) { + return false; + } + + WriteIndexToDisk(); + return true; +} + +void +CacheIndex::WriteIndexToDisk() +{ + LOG(("CacheIndex::WriteIndexToDisk()")); + mIndexStats.Log(); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(mState == READY); + MOZ_ASSERT(!mRWBuf); + MOZ_ASSERT(!mRWHash); + MOZ_ASSERT(!mRWPending); + + ChangeState(WRITING); + + mProcessEntries = mIndexStats.ActiveEntriesCount(); + + mIndexFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile(NS_LITERAL_CSTRING(TEMP_INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | + CacheFileIOManager::CREATE, + mIndexFileOpener); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::WriteIndexToDisk() - Can't open file [rv=0x%08x]", rv)); + FinishWrite(false); + 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<uint32_t>(PR_Now() / PR_USEC_PER_SEC)); + mRWBufPos += sizeof(uint32_t); + // dirty flag + NetworkEndian::writeUint32(mRWBuf + mRWBufPos, 1); + mRWBufPos += sizeof(uint32_t); + + mSkipEntries = 0; +} + +void +CacheIndex::WriteRecords() +{ + LOG(("CacheIndex::WriteRecords()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + 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<uint32_t>(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<char *>(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%08x]", rv)); + FinishWrite(false); + } else { + mRWPending = true; + } + + mRWBufPos = 0; +} + +void +CacheIndex::FinishWrite(bool aSucceeded) +{ + LOG(("CacheIndex::FinishWrite() [succeeded=%d]", aSucceeded)); + + MOZ_ASSERT((!aSucceeded && mState == SHUTDOWN) || mState == WRITING); + + sLock.AssertCurrentThreadOwns(); + + // 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); + + 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(); + mIndexStats.Log(); + + if (mState == WRITING) { + ChangeState(READY); + mLastDumpTime = TimeStamp::NowLoRes(); + } +} + +nsresult +CacheIndex::GetFile(const nsACString &aName, nsIFile **_retval) +{ + nsresult rv; + + nsCOMPtr<nsIFile> 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; +} + +nsresult +CacheIndex::RemoveFile(const nsACString &aName) +{ + MOZ_ASSERT(mState == SHUTDOWN); + + nsresult rv; + + nsCOMPtr<nsIFile> file; + rv = GetFile(aName, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + bool exists; + rv = file->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (exists) { + rv = file->Remove(false); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::RemoveFile() - Cannot remove old entry file from disk." + "[name=%s]", PromiseFlatCString(aName).get())); + NS_WARNING("Cannot remove old entry file from the disk"); + return rv; + } + } + + return NS_OK; +} + +void +CacheIndex::RemoveAllIndexFiles() +{ + LOG(("CacheIndex::RemoveAllIndexFiles()")); + RemoveFile(NS_LITERAL_CSTRING(INDEX_NAME)); + RemoveJournalAndTempFile(); +} + +void +CacheIndex::RemoveJournalAndTempFile() +{ + LOG(("CacheIndex::RemoveJournalAndTempFile()")); + RemoveFile(NS_LITERAL_CSTRING(TEMP_INDEX_NAME)); + RemoveFile(NS_LITERAL_CSTRING(JOURNAL_NAME)); +} + +class WriteLogHelper +{ +public: + explicit WriteLogHelper(PRFileDesc *aFD) + : mFD(aFD) + , mBufSize(kMaxBufSize) + , mBufPos(0) + { + mHash = new CacheHash(); + mBuf = static_cast<char *>(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<CacheHash> 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(NS_LITERAL_CSTRING(TEMP_INDEX_NAME)); + + nsCOMPtr<nsIFile> indexFile; + rv = GetFile(NS_LITERAL_CSTRING(INDEX_NAME), getter_AddRefs(indexFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> logFile; + rv = GetFile(NS_LITERAL_CSTRING(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() +{ + LOG(("CacheIndex::ReadIndexFromDisk()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + MOZ_ASSERT(mState == INITIAL); + + ChangeState(READING); + + mIndexFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile(NS_LITERAL_CSTRING(INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | + CacheFileIOManager::OPEN, + mIndexFileOpener); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08x, file=%s]", rv, INDEX_NAME)); + FinishRead(false); + return; + } + + mJournalFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile(NS_LITERAL_CSTRING(JOURNAL_NAME), + CacheFileIOManager::SPECIAL_FILE | + CacheFileIOManager::OPEN, + mJournalFileOpener); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08x, file=%s]", rv, JOURNAL_NAME)); + FinishRead(false); + } + + mTmpFileOpener = new FileOpenHelper(this); + rv = CacheFileIOManager::OpenFile(NS_LITERAL_CSTRING(TEMP_INDEX_NAME), + CacheFileIOManager::SPECIAL_FILE | + CacheFileIOManager::OPEN, + mTmpFileOpener); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::ReadIndexFromDisk() - CacheFileIOManager::OpenFile() " + "failed [rv=0x%08x, file=%s]", rv, TEMP_INDEX_NAME)); + FinishRead(false); + } +} + +void +CacheIndex::StartReadingIndex() +{ + LOG(("CacheIndex::StartReadingIndex()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + + 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); + return; + } + + AllocBuffer(); + mSkipEntries = 0; + mRWHash = new CacheHash(); + + mRWBufPos = std::min(mRWBufSize, + static_cast<uint32_t>(mIndexHandle->FileSize())); + + rv = CacheFileIOManager::Read(mIndexHandle, 0, mRWBuf, mRWBufPos, this); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::StartReadingIndex() - CacheFileIOManager::Read() failed " + "synchronously [rv=0x%08x]", rv)); + FinishRead(false); + } else { + mRWPending = true; + } +} + +void +CacheIndex::ParseRecords() +{ + LOG(("CacheIndex::ParseRecords()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + + 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); + 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<uint32_t *>( + 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<char *>(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); + } + + uint32_t hashOffset = pos; + + while (pos + sizeof(CacheIndexRecord) <= mRWBufPos && + mSkipEntries != entryCnt) { + CacheIndexRecord *rec = reinterpret_cast<CacheIndexRecord *>(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); + return; + } + + CacheIndexEntryAutoManage emng(tmpEntry.Hash(), this); + + 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); + return; + } + + mIndexOnDiskIsValid = true; + mJournalReadSuccessfully = false; + + if (mJournalHandle) { + StartReadingJournal(); + } else { + FinishRead(false); + } + + return; + } + + pos = mRWBufPos; + uint32_t toRead = std::min(mRWBufSize - pos, + static_cast<uint32_t>(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%08x]", rv)); + FinishRead(false); + return; + } else { + mRWPending = true; + } +} + +void +CacheIndex::StartReadingJournal() +{ + LOG(("CacheIndex::StartReadingJournal()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + + 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); + return; + } + + mSkipEntries = 0; + mRWHash = new CacheHash(); + + mRWBufPos = std::min(mRWBufSize, + static_cast<uint32_t>(mJournalHandle->FileSize())); + + rv = CacheFileIOManager::Read(mJournalHandle, 0, mRWBuf, mRWBufPos, this); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::StartReadingJournal() - CacheFileIOManager::Read() failed" + " synchronously [rv=0x%08x]", rv)); + FinishRead(false); + } else { + mRWPending = true; + } +} + +void +CacheIndex::ParseJournal() +{ + LOG(("CacheIndex::ParseJournal()")); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + + 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<SHA1Sum::Hash *>(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); + 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); + return; + } + + mJournalReadSuccessfully = true; + FinishRead(true); + return; + } + + pos = mRWBufPos; + uint32_t toRead = std::min(mRWBufSize - pos, + static_cast<uint32_t>(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%08x]", rv)); + FinishRead(false); + return; + } else { + mRWPending = true; + } +} + +void +CacheIndex::MergeJournal() +{ + LOG(("CacheIndex::MergeJournal()")); + + sLock.AssertCurrentThreadOwns(); + + 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); + 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) +{ + LOG(("CacheIndex::FinishRead() [succeeded=%d]", aSucceeded)); + sLock.AssertCurrentThreadOwns(); + + 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(NS_LITERAL_CSTRING(TEMP_INDEX_NAME)); + RemoveFile(NS_LITERAL_CSTRING(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(); + // Remove all entries that we haven't seen during this session + RemoveNonFreshEntries(); + StartUpdatingIndex(true); + return; + } + + if (!mJournalReadSuccessfully) { + mTmpJournal.Clear(); + EnsureNoFreshEntry(); + ProcessPendingOperations(); + StartUpdatingIndex(false); + return; + } + + MergeJournal(); + EnsureNoFreshEntry(); + ProcessPendingOperations(); + mIndexStats.Log(); + + ChangeState(READY); + mLastDumpTime = TimeStamp::NowLoRes(); // Do not dump new index immediately +} + +// static +void +CacheIndex::DelayedUpdate(nsITimer *aTimer, void *aClosure) +{ + LOG(("CacheIndex::DelayedUpdate()")); + + StaticMutexAutoLock lock(sLock); + RefPtr<CacheIndex> index = gInstance; + + if (!index) { + return; + } + + index->DelayedUpdateLocked(); +} + +// static +void +CacheIndex::DelayedUpdateLocked() +{ + LOG(("CacheIndex::DelayedUpdateLocked()")); + + sLock.AssertCurrentThreadOwns(); + + 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<CacheIOThread> 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); + } +} + +nsresult +CacheIndex::ScheduleUpdateTimer(uint32_t aDelay) +{ + LOG(("CacheIndex::ScheduleUpdateTimer() [delay=%u]", aDelay)); + + MOZ_ASSERT(!mUpdateTimer); + + nsresult rv; + + nsCOMPtr<nsITimer> timer = do_CreateInstance("@mozilla.org/timer;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEventTarget> ioTarget = CacheFileIOManager::IOTarget(); + MOZ_ASSERT(ioTarget); + + rv = timer->SetTarget(ioTarget); + NS_ENSURE_SUCCESS(rv, rv); + + rv = timer->InitWithFuncCallback(CacheIndex::DelayedUpdate, nullptr, + aDelay, nsITimer::TYPE_ONE_SHOT); + NS_ENSURE_SUCCESS(rv, rv); + + mUpdateTimer.swap(timer); + return NS_OK; +} + +nsresult +CacheIndex::SetupDirectoryEnumerator() +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_ASSERT(!mDirEnumerator); + + nsresult rv; + nsCOMPtr<nsIFile> file; + + rv = mCacheDirectory->Clone(getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = file->AppendNative(NS_LITERAL_CSTRING(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; + } + + nsCOMPtr<nsISimpleEnumerator> enumerator; + rv = file->GetDirectoryEntries(getter_AddRefs(enumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + mDirEnumerator = do_QueryInterface(enumerator, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +void +CacheIndex::InitEntryFromDiskData(CacheIndexEntry *aEntry, + CacheFileMetadata *aMetaData, + int64_t aFileSize) +{ + aEntry->InitNew(); + aEntry->MarkDirty(); + aEntry->MarkFresh(); + + aEntry->Init(GetOriginAttrsHash(aMetaData->OriginAttributes()), + aMetaData->IsAnonymous(), + aMetaData->Pinned()); + + uint32_t expirationTime; + aMetaData->GetExpirationTime(&expirationTime); + aEntry->SetExpirationTime(expirationTime); + + uint32_t frecency; + aMetaData->GetFrecency(&frecency); + aEntry->SetFrecency(frecency); + + aEntry->SetFileSize(static_cast<uint32_t>( + std::min(static_cast<int64_t>(PR_UINT32_MAX), + (aFileSize + 0x3FF) >> 10))); +} + +bool +CacheIndex::IsUpdatePending() +{ + sLock.AssertCurrentThreadOwns(); + + if (mUpdateTimer || mUpdateEventPending) { + return true; + } + + return false; +} + +void +CacheIndex::BuildIndex() +{ + LOG(("CacheIndex::BuildIndex()")); + + sLock.AssertCurrentThreadOwns(); + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + + nsresult rv; + + if (!mDirEnumerator) { + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + 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); + return; + } + } + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG(("CacheIndex::BuildIndex() - Breaking loop for higher level events.")); + mUpdateEventPending = true; + return; + } + + nsCOMPtr<nsIFile> file; + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + rv = mDirEnumerator->GetNextFile(getter_AddRefs(file)); + } + if (mState == SHUTDOWN) { + return; + } + if (!file) { + FinishUpdate(NS_SUCCEEDED(rv)); + return; + } + + nsAutoCString leaf; + rv = file->GetNativeLeafName(leaf); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::BuildIndex() - GetNativeLeafName() failed! Skipping " + "file.")); + mDontMarkIndexClean = true; + 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<CacheFileHandle> 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<CacheFileMetadata> 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); + entry = mIndex.PutEntry(hash); + InitEntryFromDiskData(entry, meta, size); + LOG(("CacheIndex::BuildIndex() - Added entry to index. [hash=%s]", + leaf.get())); + entry->Log(); + } + } + + NS_NOTREACHED("We should never get here"); +} + +bool +CacheIndex::StartUpdatingIndexIfNeeded(bool aSwitchingToReadyState) +{ + // 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); + return true; + } + + return false; +} + +void +CacheIndex::StartUpdatingIndex(bool aRebuild) +{ + LOG(("CacheIndex::StartUpdatingIndex() [rebuild=%d]", aRebuild)); + + sLock.AssertCurrentThreadOwns(); + + nsresult rv; + + mIndexStats.Log(); + + ChangeState(aRebuild ? BUILDING : UPDATING); + mDontMarkIndexClean = false; + + if (mShuttingDown || mRemovingAll) { + FinishUpdate(false); + 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<CacheIOThread> 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); + } +} + +void +CacheIndex::UpdateIndex() +{ + LOG(("CacheIndex::UpdateIndex()")); + + sLock.AssertCurrentThreadOwns(); + + MOZ_ASSERT(mPendingUpdates.Count() == 0); + + nsresult rv; + + if (!mDirEnumerator) { + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + 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); + return; + } + } + + while (true) { + if (CacheIOThread::YieldAndRerun()) { + LOG(("CacheIndex::UpdateIndex() - Breaking loop for higher level " + "events.")); + mUpdateEventPending = true; + return; + } + + nsCOMPtr<nsIFile> file; + { + // Do not do IO under the lock. + StaticMutexAutoUnlock unlock(sLock); + rv = mDirEnumerator->GetNextFile(getter_AddRefs(file)); + } + if (mState == SHUTDOWN) { + return; + } + if (!file) { + FinishUpdate(NS_SUCCEEDED(rv)); + return; + } + + nsAutoCString leaf; + rv = file->GetNativeLeafName(leaf); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::UpdateIndex() - GetNativeLeafName() failed! Skipping " + "file.")); + mDontMarkIndexClean = true; + 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<CacheFileHandle> 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=%u, " + "lastModifiedTime=%u]", leaf.get(), mIndexTimeStamp, + lastModifiedTime / PR_MSEC_PER_SEC)); + + CacheIndexEntryAutoManage entryMng(&hash, this); + entry->MarkFresh(); + continue; + } + } + } + + RefPtr<CacheFileMetadata> 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); + + if (NS_FAILED(rv)) { + LOG(("CacheIndex::UpdateIndex() - CacheFileMetadata::SyncReadMetadata() " + "failed, removing file. [name=%s]", leaf.get())); + file->Remove(false); + if (entry) { + entry->MarkRemoved(); + entry->MarkFresh(); + entry->MarkDirty(); + } + } else { + entry = mIndex.PutEntry(hash); + InitEntryFromDiskData(entry, meta, size); + LOG(("CacheIndex::UpdateIndex() - Added/updated entry to/in index. " + "[hash=%s]", leaf.get())); + entry->Log(); + } + } + + NS_NOTREACHED("We should never get here"); +} + +void +CacheIndex::FinishUpdate(bool aSucceeded) +{ + LOG(("CacheIndex::FinishUpdate() [succeeded=%d]", aSucceeded)); + + MOZ_ASSERT(mState == UPDATING || mState == BUILDING || + (!aSucceeded && mState == SHUTDOWN)); + + sLock.AssertCurrentThreadOwns(); + + 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(); + } + + // 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); + mLastDumpTime = TimeStamp::NowLoRes(); // Do not dump new index immediately +} + +void +CacheIndex::RemoveNonFreshEntries() +{ + 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); + 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) +{ + 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(true)) { + return; + } + + if ((mState == READING || mState == BUILDING || mState == UPDATING) && + aNewState == READY) { + ReportHashStats(); + } + + // 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<char *>(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(CacheIndexRecord *aRecord) +{ + LOG(("CacheIndex::FrecencyArray::AppendRecord() [record=%p, hash=%08x%08x%08x" + "%08x%08x]", aRecord, LOGSHA1(aRecord->mHash))); + + MOZ_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->mFrecency != 0) { + ++mUnsortedElements; + } +} + +void +CacheIndex::FrecencyArray::RemoveRecord(CacheIndexRecord *aRecord) +{ + LOG(("CacheIndex::FrecencyArray::RemoveRecord() [record=%p]", aRecord)); + + decltype(mRecs)::index_type idx; + idx = mRecs.IndexOf(aRecord); + MOZ_RELEASE_ASSERT(idx != mRecs.NoIndex); + mRecs[idx] = nullptr; + ++mRemovedElements; + + // Calling SortIfNeeded ensures that we get rid of removed elements in the + // array once we hit the limit. + SortIfNeeded(); +} + +void +CacheIndex::FrecencyArray::ReplaceRecord(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord) +{ + 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); + mRecs[idx] = aNewRecord; +} + +void +CacheIndex::FrecencyArray::SortIfNeeded() +{ + const uint32_t kMaxUnsortedCount = 512; + const uint32_t kMaxUnsortedPercent = 10; + const uint32_t kMaxRemovedCount = 512; + + uint32_t unsortedLimit = + std::min<uint32_t>(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) { +#ifdef DEBUG + for (uint32_t i = Length(); i < mRecs.Length(); ++i) { + MOZ_ASSERT(!mRecs[i]); + } +#endif + // Removed elements are at the end after sorting. + mRecs.RemoveElementsAt(Length(), mRemovedElements); + mRemovedElements = 0; + } + } +} + +void +CacheIndex::AddRecordToIterators(CacheIndexRecord *aRecord) +{ + 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); + } + } +} + +void +CacheIndex::RemoveRecordFromIterators(CacheIndexRecord *aRecord) +{ + 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); + } +} + +void +CacheIndex::ReplaceRecordInIterators(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord) +{ + 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); + } +} + +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(); + break; + case UPDATING: + UpdateIndex(); + break; + default: + LOG(("CacheIndex::Run() - Update/Build was canceled")); + } + + return NS_OK; +} + +nsresult +CacheIndex::OnFileOpenedInternal(FileOpenHelper *aOpener, + CacheFileHandle *aHandle, nsresult aResult) +{ + LOG(("CacheIndex::OnFileOpenedInternal() [opener=%p, handle=%p, " + "result=0x%08x]", aOpener, aHandle, aResult)); + + MOZ_ASSERT(CacheFileIOManager::IsOnIOThread()); + + nsresult rv; + + sLock.AssertCurrentThreadOwns(); + + MOZ_RELEASE_ASSERT(IsIndexUsable()); + + if (mState == READY && mShuttingDown) { + return NS_OK; + } + + 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%08x]", aResult)); + FinishWrite(false); + } else { + mIndexHandle = aHandle; + WriteRecords(); + } + break; + case READING: + if (aOpener == mIndexFileOpener) { + mIndexFileOpener = nullptr; + + if (NS_SUCCEEDED(aResult)) { + if (aHandle->FileSize() == 0) { + FinishRead(false); + CacheFileIOManager::DoomFile(aHandle, nullptr); + break; + } else { + mIndexHandle = aHandle; + } + } else { + FinishRead(false); + 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); + break; + } + } + + if (mJournalHandle) { + // Rename journal to make sure we update index on next start in case + // firefox crashes + rv = CacheFileIOManager::RenameFile( + mJournalHandle, NS_LITERAL_CSTRING(TEMP_INDEX_NAME), this); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::OnFileOpenedInternal() - CacheFileIOManager::" + "RenameFile() failed synchronously [rv=0x%08x]", rv)); + FinishRead(false); + break; + } + } else { + StartReadingIndex(); + } + + break; + default: + MOZ_ASSERT(false, "Unexpected state!"); + } + + return NS_OK; +} + +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%08x]", aHandle, + 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); + } else { + if (mSkipEntries == mProcessEntries) { + rv = CacheFileIOManager::RenameFile(mIndexHandle, + NS_LITERAL_CSTRING(INDEX_NAME), + this); + if (NS_FAILED(rv)) { + LOG(("CacheIndex::OnDataWritten() - CacheFileIOManager::" + "RenameFile() failed synchronously [rv=0x%08x]", rv)); + FinishWrite(false); + } + } else { + WriteRecords(); + } + } + 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%08x]", aHandle, + 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); + } else { + if (!mIndexOnDiskIsValid) { + ParseRecords(); + } else { + ParseJournal(); + } + } + 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%08x]", aHandle, + 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)); + 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); + } else { + StartReadingIndex(); + } + 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<nsISizeOf> 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) +{ + sLock.AssertCurrentThreadOwns(); + + if (!gInstance) + return 0; + + return gInstance->SizeOfExcludingThisInternal(mallocSizeOf); +} + +// static +size_t +CacheIndex::SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) +{ + StaticMutexAutoLock lock(sLock); + + return mallocSizeOf(gInstance) + SizeOfExcludingThis(mallocSizeOf); +} + +namespace { + +class HashComparator +{ +public: + bool Equals(CacheIndexRecord* a, CacheIndexRecord* b) const { + return memcmp(&a->mHash, &b->mHash, sizeof(SHA1Sum::Hash)) == 0; + } + bool LessThan(CacheIndexRecord* a, CacheIndexRecord* b) const { + return memcmp(&a->mHash, &b->mHash, sizeof(SHA1Sum::Hash)) < 0; + } +}; + +void +ReportHashSizeMatch(const SHA1Sum::Hash *aHash1, const SHA1Sum::Hash *aHash2) +{ + const uint32_t *h1 = reinterpret_cast<const uint32_t *>(aHash1); + const uint32_t *h2 = reinterpret_cast<const uint32_t *>(aHash2); + + for (uint32_t i = 0; i < 5; ++i) { + if (h1[i] != h2[i]) { + uint32_t bitsDiff = h1[i] ^ h2[i]; + bitsDiff = NetworkEndian::readUint32(&bitsDiff); + + // count leading zeros in bitsDiff + static const uint8_t debruijn32[32] = + { 0, 31, 9, 30, 3, 8, 13, 29, 2, 5, 7, 21, 12, 24, 28, 19, + 1, 10, 4, 14, 6, 22, 25, 20, 11, 15, 23, 26, 16, 27, 17, 18}; + + bitsDiff |= bitsDiff>>1; + bitsDiff |= bitsDiff>>2; + bitsDiff |= bitsDiff>>4; + bitsDiff |= bitsDiff>>8; + bitsDiff |= bitsDiff>>16; + bitsDiff++; + + uint8_t hashSizeMatch = debruijn32[bitsDiff*0x076be629>>27] + (i<<5); + Telemetry::Accumulate(Telemetry::NETWORK_CACHE_HASH_STATS, hashSizeMatch); + + return; + } + } + + MOZ_ASSERT(false, "Found a collision in the index!"); +} + +} // namespace + +void +CacheIndex::ReportHashStats() +{ + // We're gathering the hash stats only once, exclude too small caches. + if (CacheObserver::HashStatsReported() || mFrecencyArray.Length() < 15000) { + return; + } + + nsTArray<CacheIndexRecord *> records; + for (auto iter = mFrecencyArray.Iter(); !iter.Done(); iter.Next()) { + records.AppendElement(iter.Get()); + } + + records.Sort(HashComparator()); + + for (uint32_t i = 1; i < records.Length(); i++) { + ReportHashSizeMatch(&records[i-1]->mHash, &records[i]->mHash); + } + + CacheObserver::SetHashStatsReported(); +} + +// static +void +CacheIndex::OnAsyncEviction(bool aEvicting) +{ + RefPtr<CacheIndex> index = gInstance; + if (!index) { + return; + } + + StaticMutexAutoLock lock(sLock); + index->mAsyncGetDiskConsumptionBlocked = aEvicting; + if (!aEvicting) { + index->NotifyAsyncGetDiskConsumptionCallbacks(); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheIndex.h b/netwerk/cache2/CacheIndex.h new file mode 100644 index 0000000000..dc72c346f7 --- /dev/null +++ b/netwerk/cache2/CacheIndex.h @@ -0,0 +1,1153 @@ +/* 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 "nsTHashtable.h" +#include "nsThreadUtils.h" +#include "nsWeakReference.h" +#include "mozilla/SHA1.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/EndianUtils.h" +#include "mozilla/TimeStamp.h" + +class nsIFile; +class nsIDirectoryEnumerator; +class nsITimer; + + +#ifdef DEBUG +#define DEBUG_STATS 1 +#endif + +namespace mozilla { +namespace net { + +class CacheFileMetadata; +class FileOpenHelper; +class CacheIndexIterator; + +typedef 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; +} CacheIndexHeader; + +static_assert( + sizeof(CacheIndexHeader::mVersion) + sizeof(CacheIndexHeader::mTimeStamp) + + sizeof(CacheIndexHeader::mIsDirty) == sizeof(CacheIndexHeader), + "Unexpected sizeof(CacheIndexHeader)!"); + +struct CacheIndexRecord { + SHA1Sum::Hash mHash; + uint32_t mFrecency; + OriginAttrsHash mOriginAttrsHash; + uint32_t mExpirationTime; + + /* + * 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 0011 0000 0000 0000 0000 0000 0000 : reserved + * 0000 0000 1111 1111 1111 1111 1111 1111 : file size (in kB) + */ + uint32_t mFlags; + + CacheIndexRecord() + : mFrecency(0) + , mOriginAttrsHash(0) + , mExpirationTime(nsICacheEntry::NO_EXPIRATION_TIME) + , mFlags(0) + {} +}; + +static_assert( + sizeof(CacheIndexRecord::mHash) + sizeof(CacheIndexRecord::mFrecency) + + sizeof(CacheIndexRecord::mOriginAttrsHash) + sizeof(CacheIndexRecord::mExpirationTime) + + sizeof(CacheIndexRecord::mFlags) == sizeof(CacheIndexRecord), + "Unexpected sizeof(CacheIndexRecord)!"); + +class CacheIndexEntry : public PLDHashEntryHdr +{ +public: + typedef const SHA1Sum::Hash& KeyType; + typedef const SHA1Sum::Hash* KeyTypePointer; + + explicit CacheIndexEntry(KeyTypePointer aKey) + { + MOZ_COUNT_CTOR(CacheIndexEntry); + mRec = new CacheIndexRecord(); + LOG(("CacheIndexEntry::CacheIndexEntry() - Created record [rec=%p]", mRec.get())); + memcpy(&mRec->mHash, aKey, sizeof(SHA1Sum::Hash)); + } + CacheIndexEntry(const CacheIndexEntry& aOther) + { + NS_NOTREACHED("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->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<const uint32_t *>(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->mHash); + } + + CacheIndexEntry& operator=(const CacheIndexEntry& aOther) + { + MOZ_ASSERT(memcmp(&mRec->mHash, &aOther.mRec->mHash, + sizeof(SHA1Sum::Hash)) == 0); + mRec->mFrecency = aOther.mRec->mFrecency; + mRec->mExpirationTime = aOther.mRec->mExpirationTime; + mRec->mOriginAttrsHash = aOther.mRec->mOriginAttrsHash; + mRec->mFlags = aOther.mRec->mFlags; + return *this; + } + + void InitNew() + { + mRec->mFrecency = 0; + mRec->mExpirationTime = nsICacheEntry::NO_EXPIRATION_TIME; + mRec->mOriginAttrsHash = 0; + mRec->mFlags = 0; + } + + void Init(OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned) + { + MOZ_ASSERT(mRec->mFrecency == 0); + MOZ_ASSERT(mRec->mExpirationTime == nsICacheEntry::NO_EXPIRATION_TIME); + MOZ_ASSERT(mRec->mOriginAttrsHash == 0); + // When we init the entry it must be fresh and may be dirty + MOZ_ASSERT((mRec->mFlags & ~kDirtyMask) == kFreshMask); + + mRec->mOriginAttrsHash = aOriginAttrsHash; + mRec->mFlags |= kInitializedMask; + if (aAnonymous) { + mRec->mFlags |= kAnonymousMask; + } + if (aPinned) { + mRec->mFlags |= kPinnedMask; + } + } + + const SHA1Sum::Hash * Hash() const { return &mRec->mHash; } + + bool IsInitialized() const { return !!(mRec->mFlags & kInitializedMask); } + + mozilla::net::OriginAttrsHash OriginAttrsHash() const { return mRec->mOriginAttrsHash; } + + bool Anonymous() const { return !!(mRec->mFlags & kAnonymousMask); } + + bool IsRemoved() const { return !!(mRec->mFlags & kRemovedMask); } + void MarkRemoved() { mRec->mFlags |= kRemovedMask; } + + bool IsDirty() const { return !!(mRec->mFlags & kDirtyMask); } + void MarkDirty() { mRec->mFlags |= kDirtyMask; } + void ClearDirty() { mRec->mFlags &= ~kDirtyMask; } + + bool IsFresh() const { return !!(mRec->mFlags & kFreshMask); } + void MarkFresh() { mRec->mFlags |= kFreshMask; } + + bool IsPinned() const { return !!(mRec->mFlags & kPinnedMask); } + + void SetFrecency(uint32_t aFrecency) { mRec->mFrecency = aFrecency; } + uint32_t GetFrecency() const { return mRec->mFrecency; } + + void SetExpirationTime(uint32_t aExpirationTime) + { + mRec->mExpirationTime = aExpirationTime; + } + uint32_t GetExpirationTime() const { return mRec->mExpirationTime; } + + // 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->mFlags &= ~kFileSizeMask; + mRec->mFlags |= aFileSize; + } + // Returns filesize in kilobytes. + uint32_t GetFileSize() const { return GetFileSize(mRec); } + static uint32_t GetFileSize(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<uint8_t*>(aBuf); + memcpy(ptr, mRec->mHash, sizeof(SHA1Sum::Hash)); ptr += sizeof(SHA1Sum::Hash); + NetworkEndian::writeUint32(ptr, mRec->mFrecency); ptr += sizeof(uint32_t); + NetworkEndian::writeUint64(ptr, mRec->mOriginAttrsHash); ptr += sizeof(uint64_t); + NetworkEndian::writeUint32(ptr, mRec->mExpirationTime); ptr += sizeof(uint32_t); + // Dirty and fresh flags should never go to disk, since they make sense only + // during current session. + NetworkEndian::writeUint32(ptr, mRec->mFlags & ~(kDirtyMask | kFreshMask)); + } + + void ReadFromBuf(void *aBuf) + { + const uint8_t* ptr = static_cast<const uint8_t*>(aBuf); + MOZ_ASSERT(memcmp(&mRec->mHash, ptr, sizeof(SHA1Sum::Hash)) == 0); ptr += sizeof(SHA1Sum::Hash); + mRec->mFrecency = NetworkEndian::readUint32(ptr); ptr += sizeof(uint32_t); + mRec->mOriginAttrsHash = NetworkEndian::readUint64(ptr); ptr += sizeof(uint64_t); + mRec->mExpirationTime = NetworkEndian::readUint32(ptr); ptr += sizeof(uint32_t); + mRec->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=%llx, frecency=%u, expirationTime=%u, size=%u]", + this, LOGSHA1(mRec->mHash), IsFresh(), IsInitialized(), IsRemoved(), + IsDirty(), Anonymous(), OriginAttrsHash(), GetFrecency(), + GetExpirationTime(), GetFileSize())); + } + + static bool RecordMatchesLoadContextInfo(CacheIndexRecord *aRec, + nsILoadContextInfo *aInfo) + { + if (!aInfo->IsPrivate() && + GetOriginAttrsHash(*aInfo->OriginAttributesPtr()) == aRec->mOriginAttrsHash && + aInfo->IsAnonymous() == !!(aRec->mFlags & kAnonymousMask)) { + return true; + } + + return false; + } + + // 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; + + 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; + + static const uint32_t kReservedMask = 0x03000000; + + // FileSize in kilobytes + static const uint32_t kFileSizeMask = 0x00FFFFFF; + + nsAutoPtr<CacheIndexRecord> 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->mHash, &aOther.mRec->mHash, + sizeof(SHA1Sum::Hash)) == 0); + mUpdateFlags = 0; + *(static_cast<CacheIndexEntry *>(this)) = aOther; + return *this; + } + + void InitNew() + { + mUpdateFlags = kFrecencyUpdatedMask | kExpirationUpdatedMask | + kFileSizeUpdatedMask; + CacheIndexEntry::InitNew(); + } + + void SetFrecency(uint32_t aFrecency) + { + mUpdateFlags |= kFrecencyUpdatedMask; + CacheIndexEntry::SetFrecency(aFrecency); + } + + void SetExpirationTime(uint32_t aExpirationTime) + { + mUpdateFlags |= kExpirationUpdatedMask; + CacheIndexEntry::SetExpirationTime(aExpirationTime); + } + + void SetFileSize(uint32_t aFileSize) + { + mUpdateFlags |= kFileSizeUpdatedMask; + CacheIndexEntry::SetFileSize(aFileSize); + } + + void ApplyUpdate(CacheIndexEntry *aDst) { + MOZ_ASSERT(memcmp(&mRec->mHash, &aDst->mRec->mHash, + sizeof(SHA1Sum::Hash)) == 0); + if (mUpdateFlags & kFrecencyUpdatedMask) { + aDst->mRec->mFrecency = mRec->mFrecency; + } + if (mUpdateFlags & kExpirationUpdatedMask) { + aDst->mRec->mExpirationTime = mRec->mExpirationTime; + } + aDst->mRec->mOriginAttrsHash = mRec->mOriginAttrsHash; + if (mUpdateFlags & kFileSizeUpdatedMask) { + aDst->mRec->mFlags = mRec->mFlags; + } else { + // Copy all flags except file size. + aDst->mRec->mFlags &= kFileSizeMask; + aDst->mRec->mFlags |= (mRec->mFlags & ~kFileSizeMask); + } + } + +private: + static const uint32_t kFrecencyUpdatedMask = 0x00000001; + static const uint32_t kExpirationUpdatedMask = 0x00000002; + static const uint32_t kFileSizeUpdatedMask = 0x00000004; + + uint32_t mUpdateFlags; +}; + +class CacheIndexStats +{ +public: + CacheIndexStats() + : mCount(0) + , mNotInitialized(0) + , mRemoved(0) + , mDirty(0) + , mFresh(0) + , mEmpty(0) + , mSize(0) +#ifdef DEBUG + , mStateLogged(false) + , mDisableLogging(false) +#endif + { + } + + bool operator==(const CacheIndexStats& aOther) const + { + 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; + } + +#ifdef DEBUG + bool StateLogged() { + return mStateLogged; + } +#endif + + uint32_t Count() { + MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Count() - state logged!"); + return mCount; + } + + 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; + } + + 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); + mCount--; + 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(); + } + } + } + } + } + + void AfterChange(const CacheIndexEntry *aEntry) { + MOZ_ASSERT(mStateLogged, "CacheIndexStats::AfterChange() - state not " + "logged!"); +#ifdef DEBUG + mStateLogged = false; +#endif + if (aEntry) { + ++mCount; + 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(); + } + } + } + } + +#ifdef DEBUG_STATS + if (!mDisableLogging) { + LOG(("CacheIndexStats::AfterChange()")); + Log(); + } +#endif + } + +private: + uint32_t mCount; + uint32_t mNotInitialized; + uint32_t mRemoved; + uint32_t mDirty; + uint32_t mFresh; + uint32_t mEmpty; + uint32_t mSize; +#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; + + // Disables logging in this instance of CacheIndexStats + bool mDisableLogging; +#endif +}; + +class CacheIndex : 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 uint32_t *aExpirationTime, + 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 _pinned is non-null, it's filled with pinning status of the entry. + static nsresult HasEntry(const nsACString &aKey, EntryStatus *_retval, + bool *_pinned = nullptr); + static nsresult HasEntry(const SHA1Sum::Hash &hash, EntryStatus *_retval, + bool *_pinned = 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. + 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); + + // 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; + + virtual ~CacheIndex(); + + NS_IMETHOD OnFileOpened(CacheFileHandle *aHandle, nsresult aResult) override; + nsresult OnFileOpenedInternal(FileOpenHelper *aOpener, + CacheFileHandle *aHandle, nsresult aResult); + 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); + void PreShutdownInternal(); + + // This method returns false when index is not initialized or is shut down. + bool IsIndexUsable(); + + // 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 uint32_t *aExpirationTime, + const uint32_t *aSize); + + // Merge all pending operations from mPendingUpdates into mIndex. + void ProcessPendingOperations(); + + // 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(); + // Starts writing of index file. + void WriteIndexToDisk(); + // Serializes part of mIndex hashtable to the write buffer a writes the buffer + // to the file. + void WriteRecords(); + // Finalizes writing process. + void FinishWrite(bool aSucceeded); + + // 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); + nsresult RemoveFile(const nsACString &aName); + void RemoveAllIndexFiles(); + void RemoveJournalAndTempFile(); + // Writes journal to the disk and clears dirty flag in index header. + nsresult WriteLogToDisk(); + + // 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(); + // Starts reading data from index file. + void StartReadingIndex(); + // Parses data read from index file. + void ParseRecords(); + // Starts reading data from journal file. + void StartReadingJournal(); + // Parses data read from journal file. + void ParseJournal(); + // Merges entries from journal into mIndex. + void MergeJournal(); + // 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(); + // In debug build this method is called after processing pending operations + // to make sure mIndexStats contains correct information. + void EnsureCorrectStats(); + // Finalizes reading process. + void FinishRead(bool aSucceeded); + + // 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(); + // Posts timer event that start update or build process. + nsresult ScheduleUpdateTimer(uint32_t aDelay); + nsresult SetupDirectoryEnumerator(); + void InitEntryFromDiskData(CacheIndexEntry *aEntry, + CacheFileMetadata *aMetaData, + int64_t aFileSize); + // Returns true when either a timer is scheduled or event is posted. + bool IsUpdatePending(); + // 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(); + + bool StartUpdatingIndexIfNeeded(bool aSwitchingToReadyState = false); + // Starts update or build process or fires a timer when it is too early after + // startup. + void StartUpdatingIndex(bool aRebuild); + // 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(); + // Finalizes update or build process. + void FinishUpdate(bool aSucceeded); + + void RemoveNonFreshEntries(); + + 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); + void NotifyAsyncGetDiskConsumptionCallbacks(); + + // Allocates and releases buffer used for reading and writing index. + void AllocBuffer(); + void ReleaseBuffer(); + + // Methods used by CacheIndexEntryAutoManage to keep the iterators up to date. + void AddRecordToIterators(CacheIndexRecord *aRecord); + void RemoveRecordFromIterators(CacheIndexRecord *aRecord); + void ReplaceRecordInIterators(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord); + + // Memory reporting (private part) + size_t SizeOfExcludingThisInternal(mozilla::MallocSizeOf mallocSizeOf) const; + + void ReportHashStats(); + + static mozilla::StaticRefPtr<CacheIndex> gInstance; + static StaticMutex sLock; + + nsCOMPtr<nsIFile> mCacheDirectory; + + EState mState; + // Timestamp of time when the index was initialized. We use it to delay + // initial update or build of index. + TimeStamp mStartTime; + // Set to true in PreShutdown(), it is checked on variaous places to prevent + // starting any process (write, update, etc.) during shutdown. + bool mShuttingDown; + // 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; + // 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; + // Whether the index file on disk exists and is valid. + bool mIndexOnDiskIsValid; + // 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; + // Timestamp value from index file. It is used during update process to skip + // entries that were last modified before this timestamp. + uint32_t mIndexTimeStamp; + // 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; + + // Timer of delayed update/build. + nsCOMPtr<nsITimer> mUpdateTimer; + // True when build or update event is posted + bool mUpdateEventPending; + + // 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; + // 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; + char *mRWBuf; + uint32_t mRWBufSize; + uint32_t mRWBufPos; + RefPtr<CacheHash> mRWHash; + + // 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; + + // Reading of journal succeeded if true. + bool mJournalReadSuccessfully; + + // Handle used for writing and reading index file. + RefPtr<CacheFileHandle> mIndexHandle; + // Handle used for reading journal file. + RefPtr<CacheFileHandle> mJournalHandle; + // Used to check the existence of the file during reading process. + RefPtr<CacheFileHandle> mTmpHandle; + + RefPtr<FileOpenHelper> mIndexFileOpener; + RefPtr<FileOpenHelper> mJournalFileOpener; + RefPtr<FileOpenHelper> mTmpFileOpener; + + // Directory enumerator used when building and updating index. + nsCOMPtr<nsIDirectoryEnumerator> mDirEnumerator; + + // Main index hashtable. + nsTHashtable<CacheIndexEntry> mIndex; + + // 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<CacheIndexEntryUpdate> mPendingUpdates; + + // Contains information statistics for mIndex + mPendingUpdates. + CacheIndexStats mIndexStats; + + // 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<CacheIndexEntry> mTmpJournal; + + // 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<CacheIndexRecord *> *aRecs) + : mRecs(aRecs) + , mIdx(0) + { + while (!Done() && !(*mRecs)[mIdx]) { + mIdx++; + } + } + + bool Done() const { return mIdx == mRecs->Length(); } + + CacheIndexRecord* Get() const + { + MOZ_ASSERT(!Done()); + return (*mRecs)[mIdx]; + } + + void Next() + { + MOZ_ASSERT(!Done()); + ++mIdx; + while (!Done() && !(*mRecs)[mIdx]) { + mIdx++; + } + } + + private: + nsTArray<CacheIndexRecord *> *mRecs; + uint32_t mIdx; + }; + + public: + Iterator Iter() { return Iterator(&mRecs); } + + FrecencyArray() : mUnsortedElements(0) + , mRemovedElements(0) {} + + // Methods used by CacheIndexEntryAutoManage to keep the array up to date. + void AppendRecord(CacheIndexRecord *aRecord); + void RemoveRecord(CacheIndexRecord *aRecord); + void ReplaceRecord(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord); + void SortIfNeeded(); + + size_t Length() const { return mRecs.Length() - mRemovedElements; } + void Clear() { mRecs.Clear(); } + + private: + friend class CacheIndex; + + nsTArray<CacheIndexRecord *> mRecs; + uint32_t mUnsortedElements; + // 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; + }; + + FrecencyArray mFrecencyArray; + + nsTArray<CacheIndexIterator *> mIterators; + + // 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; + + 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) + : mObserver(aWeakObserver) { } + virtual ~DiskConsumptionObserver() { + if (mObserver && !NS_IsMainThread()) { + NS_ReleaseOnMainThread(mObserver.forget()); + } + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsICacheStorageConsumptionObserver> 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<RefPtr<DiskConsumptionObserver> > mDiskConsumptionObservers; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndexContextIterator.cpp b/netwerk/cache2/CacheIndexContextIterator.cpp new file mode 100644 index 0000000000..5f3cb7bd7c --- /dev/null +++ b/netwerk/cache2/CacheIndexContextIterator.cpp @@ -0,0 +1,45 @@ +/* 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 { +namespace net { + +CacheIndexContextIterator::CacheIndexContextIterator(CacheIndex *aIndex, + bool aAddNew, + nsILoadContextInfo *aInfo) + : CacheIndexIterator(aIndex, aAddNew) + , mInfo(aInfo) +{ +} + +CacheIndexContextIterator::~CacheIndexContextIterator() +{ +} + +void +CacheIndexContextIterator::AddRecord(CacheIndexRecord *aRecord) +{ + if (CacheIndexEntry::RecordMatchesLoadContextInfo(aRecord, mInfo)) { + CacheIndexIterator::AddRecord(aRecord); + } +} + +void +CacheIndexContextIterator::AddRecords( + const nsTArray<CacheIndexRecord *> &aRecords) +{ + // We need to add one by one so that those with wrong context are ignored. + for (uint32_t i = 0; i < aRecords.Length(); ++i) { + AddRecord(aRecords[i]); + } +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheIndexContextIterator.h b/netwerk/cache2/CacheIndexContextIterator.h new file mode 100644 index 0000000000..32eb9c4789 --- /dev/null +++ b/netwerk/cache2/CacheIndexContextIterator.h @@ -0,0 +1,32 @@ +/* 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(); + +private: + virtual void AddRecord(CacheIndexRecord *aRecord); + virtual void AddRecords(const nsTArray<CacheIndexRecord *> &aRecords); + + nsCOMPtr<nsILoadContextInfo> mInfo; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheIndexIterator.cpp b/netwerk/cache2/CacheIndexIterator.cpp new file mode 100644 index 0000000000..0d56ec81f5 --- /dev/null +++ b/netwerk/cache2/CacheIndexIterator.cpp @@ -0,0 +1,118 @@ +/* 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 { +namespace 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)); + + Close(); +} + +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[mRecords.Length() - 1]->mHash, sizeof(SHA1Sum::Hash)); + mRecords.RemoveElementAt(mRecords.Length() - 1); + + 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%08x]", this, + 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; + } + + DebugOnly<bool> removed = mIndex->mIterators.RemoveElement(this); + MOZ_ASSERT(removed); + mStatus = aStatus; + + return NS_OK; +} + +void +CacheIndexIterator::AddRecord(CacheIndexRecord *aRecord) +{ + LOG(("CacheIndexIterator::AddRecord() [this=%p, record=%p]", this, aRecord)); + + mRecords.AppendElement(aRecord); +} + +bool +CacheIndexIterator::RemoveRecord(CacheIndexRecord *aRecord) +{ + LOG(("CacheIndexIterator::RemoveRecord() [this=%p, record=%p]", this, + aRecord)); + + return mRecords.RemoveElement(aRecord); +} + +bool +CacheIndexIterator::ReplaceRecord(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord) +{ + LOG(("CacheIndexIterator::ReplaceRecord() [this=%p, oldRecord=%p, " + "newRecord=%p]", this, aOldRecord, aNewRecord)); + + if (RemoveRecord(aOldRecord)) { + AddRecord(aNewRecord); + return true; + } + + return false; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheIndexIterator.h b/netwerk/cache2/CacheIndexIterator.h new file mode 100644 index 0000000000..9fe96989ec --- /dev/null +++ b/netwerk/cache2/CacheIndexIterator.h @@ -0,0 +1,59 @@ +/* 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 "nsAutoPtr.h" +#include "mozilla/SHA1.h" + +namespace mozilla { +namespace net { + +class CacheIndex; +struct CacheIndexRecord; + +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(CacheIndexRecord *aRecord); + bool RemoveRecord(CacheIndexRecord *aRecord); + bool ReplaceRecord(CacheIndexRecord *aOldRecord, + CacheIndexRecord *aNewRecord); + + nsresult mStatus; + RefPtr<CacheIndex> mIndex; + nsTArray<CacheIndexRecord *> 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..862c3316ff --- /dev/null +++ b/netwerk/cache2/CacheLog.cpp @@ -0,0 +1,22 @@ +/* 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 { +namespace 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 net +} // namespace mozilla diff --git a/netwerk/cache2/CacheLog.h b/netwerk/cache2/CacheLog.h new file mode 100644 index 0000000000..7191d47926 --- /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..1eb76e8c5b --- /dev/null +++ b/netwerk/cache2/CacheObserver.cpp @@ -0,0 +1,581 @@ +/* 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 <time.h> +#include <math.h> + +namespace mozilla { +namespace net { + +CacheObserver* CacheObserver::sSelf = nullptr; + +static uint32_t const kDefaultUseNewCache = 1; // Use the new cache by default +uint32_t CacheObserver::sUseNewCache = kDefaultUseNewCache; + +static bool sUseNewCacheTemp = false; // Temp trigger to not lose early adopters + +static int32_t const kAutoDeleteCacheVersion = -1; // Auto-delete off by default +static int32_t sAutoDeleteCacheVersion = kAutoDeleteCacheVersion; + +static int32_t const kDefaultHalfLifeExperiment = -1; // Disabled +int32_t CacheObserver::sHalfLifeExperiment = kDefaultHalfLifeExperiment; + +static float const kDefaultHalfLifeHours = 1.0F; // 1 hour +float CacheObserver::sHalfLifeHours = kDefaultHalfLifeHours; + +static bool const kDefaultUseDiskCache = true; +bool CacheObserver::sUseDiskCache = kDefaultUseDiskCache; + +static bool const kDefaultUseMemoryCache = true; +bool CacheObserver::sUseMemoryCache = kDefaultUseMemoryCache; + +static uint32_t const kDefaultMetadataMemoryLimit = 250; // 0.25 MB +uint32_t CacheObserver::sMetadataMemoryLimit = kDefaultMetadataMemoryLimit; + +static int32_t const kDefaultMemoryCacheCapacity = -1; // autodetect +int32_t CacheObserver::sMemoryCacheCapacity = kDefaultMemoryCacheCapacity; +// Cache of the calculated memory capacity based on the system memory size +int32_t CacheObserver::sAutoMemoryCacheCapacity = -1; + +static uint32_t const kDefaultDiskCacheCapacity = 250 * 1024; // 250 MB +Atomic<uint32_t,Relaxed> CacheObserver::sDiskCacheCapacity + (kDefaultDiskCacheCapacity); + +static uint32_t const kDefaultDiskFreeSpaceSoftLimit = 5 * 1024; // 5MB +uint32_t CacheObserver::sDiskFreeSpaceSoftLimit = kDefaultDiskFreeSpaceSoftLimit; + +static uint32_t const kDefaultDiskFreeSpaceHardLimit = 1024; // 1MB +uint32_t CacheObserver::sDiskFreeSpaceHardLimit = kDefaultDiskFreeSpaceHardLimit; + +static bool const kDefaultSmartCacheSizeEnabled = false; +bool CacheObserver::sSmartCacheSizeEnabled = kDefaultSmartCacheSizeEnabled; + +static uint32_t const kDefaultPreloadChunkCount = 4; +uint32_t CacheObserver::sPreloadChunkCount = kDefaultPreloadChunkCount; + +static int32_t const kDefaultMaxMemoryEntrySize = 4 * 1024; // 4 MB +int32_t CacheObserver::sMaxMemoryEntrySize = kDefaultMaxMemoryEntrySize; + +static int32_t const kDefaultMaxDiskEntrySize = 50 * 1024; // 50 MB +int32_t CacheObserver::sMaxDiskEntrySize = kDefaultMaxDiskEntrySize; + +static uint32_t const kDefaultMaxDiskChunksMemoryUsage = 10 * 1024; // 10MB +uint32_t CacheObserver::sMaxDiskChunksMemoryUsage = kDefaultMaxDiskChunksMemoryUsage; + +static uint32_t const kDefaultMaxDiskPriorityChunksMemoryUsage = 10 * 1024; // 10MB +uint32_t CacheObserver::sMaxDiskPriorityChunksMemoryUsage = kDefaultMaxDiskPriorityChunksMemoryUsage; + +static uint32_t const kDefaultCompressionLevel = 1; +uint32_t CacheObserver::sCompressionLevel = kDefaultCompressionLevel; + +static bool kDefaultSanitizeOnShutdown = false; +bool CacheObserver::sSanitizeOnShutdown = kDefaultSanitizeOnShutdown; + +static bool kDefaultClearCacheOnShutdown = false; +bool CacheObserver::sClearCacheOnShutdown = kDefaultClearCacheOnShutdown; + +static bool kDefaultCacheFSReported = false; +bool CacheObserver::sCacheFSReported = kDefaultCacheFSReported; + +static bool kDefaultHashStatsReported = false; +bool CacheObserver::sHashStatsReported = kDefaultHashStatsReported; + +static uint32_t const kDefaultMaxShutdownIOLag = 2; // seconds +Atomic<uint32_t, Relaxed> CacheObserver::sMaxShutdownIOLag(kDefaultMaxShutdownIOLag); + +Atomic<PRIntervalTime> 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<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (!obs) { + return NS_ERROR_UNEXPECTED; + } + + sSelf = new CacheObserver(); + NS_ADDREF(sSelf); + + obs->AddObserver(sSelf, "prefservice:after-app-defaults", true); + obs->AddObserver(sSelf, "profile-do-change", true); + obs->AddObserver(sSelf, "browser-delayed-startup-finished", 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, "clear-origin-attributes-data", true); + obs->AddObserver(sSelf, "memory-pressure", true); + + return NS_OK; +} + +// static +nsresult +CacheObserver::Shutdown() +{ + if (!sSelf) { + return NS_ERROR_NOT_INITIALIZED; + } + + NS_RELEASE(sSelf); + return NS_OK; +} + +void +CacheObserver::AttachToPreferences() +{ + sAutoDeleteCacheVersion = mozilla::Preferences::GetInt( + "browser.cache.auto_delete_cache_version", kAutoDeleteCacheVersion); + + mozilla::Preferences::AddUintVarCache( + &sUseNewCache, "browser.cache.use_new_backend", kDefaultUseNewCache); + mozilla::Preferences::AddBoolVarCache( + &sUseNewCacheTemp, "browser.cache.use_new_backend_temp", false); + + mozilla::Preferences::AddBoolVarCache( + &sUseDiskCache, "browser.cache.disk.enable", kDefaultUseDiskCache); + mozilla::Preferences::AddBoolVarCache( + &sUseMemoryCache, "browser.cache.memory.enable", kDefaultUseMemoryCache); + + mozilla::Preferences::AddUintVarCache( + &sMetadataMemoryLimit, "browser.cache.disk.metadata_memory_limit", kDefaultMetadataMemoryLimit); + + mozilla::Preferences::AddAtomicUintVarCache( + &sDiskCacheCapacity, "browser.cache.disk.capacity", kDefaultDiskCacheCapacity); + mozilla::Preferences::AddBoolVarCache( + &sSmartCacheSizeEnabled, "browser.cache.disk.smart_size.enabled", kDefaultSmartCacheSizeEnabled); + mozilla::Preferences::AddIntVarCache( + &sMemoryCacheCapacity, "browser.cache.memory.capacity", kDefaultMemoryCacheCapacity); + + mozilla::Preferences::AddUintVarCache( + &sDiskFreeSpaceSoftLimit, "browser.cache.disk.free_space_soft_limit", kDefaultDiskFreeSpaceSoftLimit); + mozilla::Preferences::AddUintVarCache( + &sDiskFreeSpaceHardLimit, "browser.cache.disk.free_space_hard_limit", kDefaultDiskFreeSpaceHardLimit); + + mozilla::Preferences::AddUintVarCache( + &sPreloadChunkCount, "browser.cache.disk.preload_chunk_count", kDefaultPreloadChunkCount); + + mozilla::Preferences::AddIntVarCache( + &sMaxDiskEntrySize, "browser.cache.disk.max_entry_size", kDefaultMaxDiskEntrySize); + mozilla::Preferences::AddIntVarCache( + &sMaxMemoryEntrySize, "browser.cache.memory.max_entry_size", kDefaultMaxMemoryEntrySize); + + mozilla::Preferences::AddUintVarCache( + &sMaxDiskChunksMemoryUsage, "browser.cache.disk.max_chunks_memory_usage", kDefaultMaxDiskChunksMemoryUsage); + mozilla::Preferences::AddUintVarCache( + &sMaxDiskPriorityChunksMemoryUsage, "browser.cache.disk.max_priority_chunks_memory_usage", kDefaultMaxDiskPriorityChunksMemoryUsage); + + // http://mxr.mozilla.org/mozilla-central/source/netwerk/cache/nsCacheEntryDescriptor.cpp#367 + mozilla::Preferences::AddUintVarCache( + &sCompressionLevel, "browser.cache.compression_level", kDefaultCompressionLevel); + + mozilla::Preferences::GetComplex( + "browser.cache.disk.parent_directory", NS_GET_IID(nsIFile), + getter_AddRefs(mCacheParentDirectoryOverride)); + + // First check the default value. If it is at -1, the experient + // is turned off. If it is at 0, then use the user pref value + // instead. + sHalfLifeExperiment = mozilla::Preferences::GetDefaultInt( + "browser.cache.frecency_experiment", kDefaultHalfLifeExperiment); + + if (sHalfLifeExperiment == 0) { + // Default preferences indicate we want to run the experiment, + // hence read the user value. + sHalfLifeExperiment = mozilla::Preferences::GetInt( + "browser.cache.frecency_experiment", sHalfLifeExperiment); + } + + if (sHalfLifeExperiment == 0) { + // The experiment has not yet been initialized but is engaged, do + // the initialization now. + srand(time(NULL)); + sHalfLifeExperiment = (rand() % 4) + 1; + // Store the experiemnt value, since we need it not to change between + // browser sessions. + mozilla::Preferences::SetInt( + "browser.cache.frecency_experiment", sHalfLifeExperiment); + } + + switch (sHalfLifeExperiment) { + case 1: // The experiment is engaged + sHalfLifeHours = 0.083F; // ~5 mintues + break; + case 2: + sHalfLifeHours = 0.25F; // 15 mintues + break; + case 3: + sHalfLifeHours = 1.0F; + break; + case 4: + sHalfLifeHours = 6.0F; + break; + + case -1: + default: // The experiment is off or broken + sHalfLifeExperiment = -1; + sHalfLifeHours = std::max(0.01F, std::min(1440.0F, mozilla::Preferences::GetFloat( + "browser.cache.frecency_half_life_hours", kDefaultHalfLifeHours))); + break; + } + + mozilla::Preferences::AddBoolVarCache( + &sSanitizeOnShutdown, "privacy.sanitize.sanitizeOnShutdown", kDefaultSanitizeOnShutdown); + mozilla::Preferences::AddBoolVarCache( + &sClearCacheOnShutdown, "privacy.clearOnShutdown.cache", kDefaultClearCacheOnShutdown); + + mozilla::Preferences::AddAtomicUintVarCache( + &sMaxShutdownIOLag, "browser.cache.max_shutdown_io_lag", kDefaultMaxShutdownIOLag); +} + +// static +uint32_t CacheObserver::MemoryCacheCapacity() +{ + if (sMemoryCacheCapacity >= 0) + return sMemoryCacheCapacity << 10; + + if (sAutoMemoryCacheCapacity != -1) + return sAutoMemoryCacheCapacity; + + static 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) { + capacity = (int32_t)(x * x / 3.0 + x + 2.0 / 3 + 0.1); // 0.1 for rounding + if (capacity > 32) + capacity = 32; + capacity <<= 20; + } + + // Result is in bytes. + return sAutoMemoryCacheCapacity = capacity; +} + +// static +bool CacheObserver::UseNewCache() +{ + uint32_t useNewCache = sUseNewCache; + + if (sUseNewCacheTemp) + useNewCache = 1; + + switch (useNewCache) { + case 0: // use the old cache backend + return false; + + case 1: // use the new cache backend + return true; + } + + return true; +} + +// static +void +CacheObserver::SetDiskCacheCapacity(uint32_t aCapacity) +{ + sDiskCacheCapacity = aCapacity >> 10; + + if (!sSelf) { + return; + } + + if (NS_IsMainThread()) { + sSelf->StoreDiskCacheCapacity(); + } else { + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod(sSelf, &CacheObserver::StoreDiskCacheCapacity); + NS_DispatchToMainThread(event); + } +} + +void +CacheObserver::StoreDiskCacheCapacity() +{ + mozilla::Preferences::SetInt("browser.cache.disk.capacity", + sDiskCacheCapacity); +} + +// static +void +CacheObserver::SetCacheFSReported() +{ + sCacheFSReported = true; + + if (!sSelf) { + return; + } + + if (NS_IsMainThread()) { + sSelf->StoreCacheFSReported(); + } else { + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod(sSelf, &CacheObserver::StoreCacheFSReported); + NS_DispatchToMainThread(event); + } +} + +void +CacheObserver::StoreCacheFSReported() +{ + mozilla::Preferences::SetInt("browser.cache.disk.filesystem_reported", + sCacheFSReported); +} + +// static +void +CacheObserver::SetHashStatsReported() +{ + sHashStatsReported = true; + + if (!sSelf) { + return; + } + + if (NS_IsMainThread()) { + sSelf->StoreHashStatsReported(); + } else { + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod(sSelf, &CacheObserver::StoreHashStatsReported); + NS_DispatchToMainThread(event); + } +} + +void +CacheObserver::StoreHashStatsReported() +{ + mozilla::Preferences::SetInt("browser.cache.disk.hashstats_reported", + sHashStatsReported); +} + +// 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); +} + +namespace { +namespace CacheStorageEvictHelper { + +nsresult ClearStorage(bool const aPrivate, + bool const aAnonymous, + NeckoOriginAttributes &aOa) +{ + nsresult rv; + + aOa.SyncAttributesWithPrivateBrowsing(aPrivate); + RefPtr<LoadContextInfo> info = GetLoadContextInfo(aAnonymous, aOa); + + nsCOMPtr<nsICacheStorage> storage; + RefPtr<CacheStorageService> service = CacheStorageService::Self(); + NS_ENSURE_TRUE(service, NS_ERROR_FAILURE); + + // Clear disk storage + rv = service->DiskCacheStorage(info, false, 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(NeckoOriginAttributes &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; +} + +} // CacheStorageEvictHelper +} // anon + +// static +bool CacheObserver::EntryIsTooBig(int64_t aSize, bool aUsingDisk) +{ + // If custom limit is set, check it. + int64_t preferredLimit = aUsingDisk ? sMaxDiskEntrySize : sMaxMemoryEntrySize; + + // 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 (>> 3) of the respective capacity. + int64_t derivedLimit = aUsingDisk + ? (static_cast<int64_t>(DiskCacheCapacity() >> 3)) + : (static_cast<int64_t>(MemoryCacheCapacity() >> 3)); + + if (aSize > derivedLimit) + return true; + + return false; +} + +// static +bool CacheObserver::IsPastShutdownIOLag() +{ +#ifdef DEBUG + return false; +#endif + + if (sShutdownDemandedTime == PR_INTERVAL_NO_TIMEOUT || + sMaxShutdownIOLag == UINT32_MAX) { + return false; + } + + static const PRIntervalTime kMaxShutdownIOLag = + PR_SecondsToInterval(sMaxShutdownIOLag); + + if ((PR_IntervalNow() - sShutdownDemandedTime) > kMaxShutdownIOLag) { + return true; + } + + return false; +} + +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, "browser-delayed-startup-finished")) { + uint32_t activeVersion = UseNewCache() ? 1 : 0; + CacheStorageService::CleaupCacheDirectories(sAutoDeleteCacheVersion, activeVersion); + 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<CacheStorageService> service = CacheStorageService::Self(); + if (service) { + service->Shutdown(); + } + + CacheFileIOManager::Shutdown(); + return NS_OK; + } + + if (!strcmp(aTopic, "last-pb-context-exited")) { + RefPtr<CacheStorageService> service = CacheStorageService::Self(); + if (service) { + service->DropPrivateBrowsingEntries(); + } + + return NS_OK; + } + + if (!strcmp(aTopic, "clear-origin-attributes-data")) { + NeckoOriginAttributes oa; + if (!oa.Init(nsDependentString(aData))) { + NS_ERROR("Could not parse NeckoOriginAttributes JSON in clear-origin-attributes-data notification"); + return NS_OK; + } + + nsresult rv = CacheStorageEvictHelper::Run(oa); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; + } + + if (!strcmp(aTopic, "memory-pressure")) { + RefPtr<CacheStorageService> service = CacheStorageService::Self(); + if (service) + service->PurgeFromMemory(nsICacheStorageService::PURGE_EVERYTHING); + + return NS_OK; + } + + MOZ_ASSERT(false, "Missing observer handler"); + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheObserver.h b/netwerk/cache2/CacheObserver.h new file mode 100644 index 0000000000..62e5bbc6ec --- /dev/null +++ b/netwerk/cache2/CacheObserver.h @@ -0,0 +1,121 @@ +/* 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 <algorithm> + +namespace mozilla { +namespace net { + +class CacheObserver : public nsIObserver + , public nsSupportsWeakReference +{ + virtual ~CacheObserver() {} + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOBSERVER + + static nsresult Init(); + static nsresult Shutdown(); + static CacheObserver* Self() { return sSelf; } + + // Access to preferences + static bool UseNewCache(); + static bool UseDiskCache() + { return sUseDiskCache; } + static bool UseMemoryCache() + { return sUseMemoryCache; } + static uint32_t MetadataMemoryLimit() // result in bytes. + { return sMetadataMemoryLimit << 10; } + static uint32_t MemoryCacheCapacity(); // result in bytes. + static uint32_t DiskCacheCapacity() // result in bytes. + { return sDiskCacheCapacity << 10; } + static void SetDiskCacheCapacity(uint32_t); // parameter in bytes. + static uint32_t DiskFreeSpaceSoftLimit() // result in bytes. + { return sDiskFreeSpaceSoftLimit << 10; } + static uint32_t DiskFreeSpaceHardLimit() // result in bytes. + { return sDiskFreeSpaceHardLimit << 10; } + static bool SmartCacheSizeEnabled() + { return sSmartCacheSizeEnabled; } + static uint32_t PreloadChunkCount() + { return sPreloadChunkCount; } + static uint32_t MaxMemoryEntrySize() // result in bytes. + { return sMaxMemoryEntrySize << 10; } + static uint32_t MaxDiskEntrySize() // result in bytes. + { return sMaxDiskEntrySize << 10; } + static uint32_t MaxDiskChunksMemoryUsage(bool aPriority) // result in bytes. + { return aPriority ? sMaxDiskPriorityChunksMemoryUsage << 10 + : sMaxDiskChunksMemoryUsage << 10; } + static uint32_t CompressionLevel() + { return sCompressionLevel; } + static uint32_t HalfLifeSeconds() + { return sHalfLifeHours * 60.0F * 60.0F; } + static int32_t HalfLifeExperiment() + { return sHalfLifeExperiment; } + static bool ClearCacheOnShutdown() + { return sSanitizeOnShutdown && sClearCacheOnShutdown; } + static bool CacheFSReported() + { return sCacheFSReported; } + static void SetCacheFSReported(); + static bool HashStatsReported() + { return sHashStatsReported; } + static void SetHashStatsReported(); + static void ParentDirOverride(nsIFile ** aDir); + + static bool EntryIsTooBig(int64_t aSize, bool aUsingDisk); + + static uint32_t MaxShutdownIOLag() + { return sMaxShutdownIOLag; } + static bool IsPastShutdownIOLag(); + + static bool ShuttingDown() + { return sShutdownDemandedTime != PR_INTERVAL_NO_TIMEOUT; } + +private: + static CacheObserver* sSelf; + + void StoreDiskCacheCapacity(); + void StoreCacheFSReported(); + void StoreHashStatsReported(); + void AttachToPreferences(); + + static uint32_t sUseNewCache; + static bool sUseMemoryCache; + static bool sUseDiskCache; + static uint32_t sMetadataMemoryLimit; + static int32_t sMemoryCacheCapacity; + static int32_t sAutoMemoryCacheCapacity; + static Atomic<uint32_t, Relaxed> sDiskCacheCapacity; + static uint32_t sDiskFreeSpaceSoftLimit; + static uint32_t sDiskFreeSpaceHardLimit; + static bool sSmartCacheSizeEnabled; + static uint32_t sPreloadChunkCount; + static int32_t sMaxMemoryEntrySize; + static int32_t sMaxDiskEntrySize; + static uint32_t sMaxDiskChunksMemoryUsage; + static uint32_t sMaxDiskPriorityChunksMemoryUsage; + static uint32_t sCompressionLevel; + static float sHalfLifeHours; + static int32_t sHalfLifeExperiment; + static bool sSanitizeOnShutdown; + static bool sClearCacheOnShutdown; + static bool sCacheFSReported; + static bool sHashStatsReported; + static Atomic<uint32_t, Relaxed> sMaxShutdownIOLag; + static Atomic<PRIntervalTime> sShutdownDemandedTime; + + // Non static properties, accessible via sSelf + nsCOMPtr<nsIFile> mCacheParentDirectoryOverride; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/CacheStorage.cpp b/netwerk/cache2/CacheStorage.cpp new file mode 100644 index 0000000000..1d84195a40 --- /dev/null +++ b/netwerk/cache2/CacheStorage.cpp @@ -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/. */ + +#include "CacheLog.h" +#include "CacheStorage.h" +#include "CacheStorageService.h" +#include "CacheEntry.h" +#include "CacheObserver.h" + +#include "OldWrappers.h" + +#include "nsICacheEntryDoomCallback.h" + +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheService.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsServiceManagerUtils.h" + +namespace mozilla { +namespace net { + +NS_IMPL_ISUPPORTS(CacheStorage, nsICacheStorage) + +CacheStorage::CacheStorage(nsILoadContextInfo* aInfo, + bool aAllowDisk, + bool aLookupAppCache, + bool aSkipSizeCheck, + bool aPinning) +: mLoadContextInfo(GetLoadContextInfo(aInfo)) +, mWriteToDisk(aAllowDisk) +, mLookupAppCache(aLookupAppCache) +, mSkipSizeCheck(aSkipSizeCheck) +, mPinning(aPinning) +{ +} + +CacheStorage::~CacheStorage() +{ +} + +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, nullptr, NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + + if (MOZ_UNLIKELY(!CacheObserver::UseMemoryCache()) && !mWriteToDisk && + !(aFlags & OPEN_INTERCEPTED)) { + aCallback->OnCacheEntryAvailable(nullptr, false, nullptr, NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aCallback); + + nsresult rv; + + bool truncate = aFlags & nsICacheStorage::OPEN_TRUNCATE; + + nsCOMPtr<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIApplicationCache> appCache; + if (LookupAppCache()) { + rv = ChooseApplicationCache(noRefURI, getter_AddRefs(appCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (appCache) { + // From a chosen appcache open only as readonly + aFlags &= ~nsICacheStorage::OPEN_TRUNCATE; + } + } + + if (appCache) { + nsAutoCString scheme; + rv = noRefURI->GetScheme(scheme); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<_OldCacheLoad> appCacheLoad = + new _OldCacheLoad(scheme, asciiSpec, aCallback, appCache, + LoadInfo(), WriteToDisk(), aFlags); + rv = appCacheLoad->Start(); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("CacheStorage::AsyncOpenURI loading from appcache")); + return NS_OK; + } + + RefPtr<CacheEntryHandle> entry; + rv = CacheStorageService::Self()->AddStorageEntry( + this, asciiSpec, aIdExtension, + truncate, // replace any existing one? + getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + // 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<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString asciiSpec; + rv = noRefURI->GetAsciiSpec(asciiSpec); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CacheEntryHandle> handle; + rv = CacheStorageService::Self()->AddStorageEntry( + this, asciiSpec, aIdExtension, + true, // 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<CacheEntryHandle> 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<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(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::AsyncDoomURI(nsIURI *aURI, const nsACString & aIdExtension, + nsICacheEntryDoomCallback* aCallback) +{ + if (!CacheStorageService::Self()) + return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + nsCOMPtr<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(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; +} + +// Internal + +nsresult CacheStorage::ChooseApplicationCache(nsIURI* aURI, + nsIApplicationCache** aCache) +{ + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString cacheKey; + rv = aURI->GetAsciiSpec(cacheKey); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appCacheService->ChooseApplicationCache(cacheKey, LoadInfo(), aCache); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheStorage.h b/netwerk/cache2/CacheStorage.h new file mode 100644 index 0000000000..85c5bccdbb --- /dev/null +++ b/netwerk/cache2/CacheStorage.h @@ -0,0 +1,81 @@ +/* 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 "nsRefPtrHashtable.h" +#include "nsThreadUtils.h" +#include "nsCOMPtr.h" +#include "nsILoadContextInfo.h" +#include "nsIApplicationCache.h" +#include "nsICacheEntryDoomCallback.h" + +class nsIURI; +class nsIApplicationCache; + +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. +typedef nsRefPtrHashtable<nsCStringHashKey, CacheEntry> TCacheEntryTable; +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 aLookupAppCache, + bool aSkipSizeCheck, + bool aPinning); + +protected: + virtual ~CacheStorage(); + + nsresult ChooseApplicationCache(nsIURI* aURI, nsIApplicationCache** aCache); + + RefPtr<LoadContextInfo> mLoadContextInfo; + bool mWriteToDisk : 1; + bool mLookupAppCache : 1; + bool mSkipSizeCheck: 1; + bool mPinning : 1; + +public: + nsILoadContextInfo* LoadInfo() const { return mLoadContextInfo; } + bool WriteToDisk() const { return mWriteToDisk && !mLoadContextInfo->IsPrivate(); } + bool LookupAppCache() const { return mLookupAppCache; } + 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..67ef4c5267 --- /dev/null +++ b/netwerk/cache2/CacheStorageService.cpp @@ -0,0 +1,2290 @@ +/* -*- 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 "AppCacheStorage.h" +#include "CacheEntry.h" +#include "CacheFileUtils.h" + +#include "OldWrappers.h" +#include "nsCacheService.h" +#include "nsDeleteDir.h" + +#include "nsICacheStorageVisitor.h" +#include "nsIObserverService.h" +#include "nsIFile.h" +#include "nsIURI.h" +#include "nsCOMPtr.h" +#include "nsAutoPtr.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsServiceManagerUtils.h" +#include "nsWeakReference.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Services.h" + +namespace mozilla { +namespace net { + +namespace { + +void AppendMemoryStorageID(nsAutoCString &key) +{ + key.Append('/'); + key.Append('M'); +} + +} // 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. +typedef nsClassHashtable<nsCStringHashKey, CacheEntryTable> + GlobalEntryTables; + +/** + * 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: + * <isPrivate|isAnon|appId|inIsolatedMozBrowser> which builds a mapping key. + * + * Thread-safe to access, protected by the service mutex. + */ +static GlobalEntryTables* sGlobalEntryTables; + +CacheMemoryConsumer::CacheMemoryConsumer(uint32_t aFlags) +: mReportedMemoryConsumption(0) +, mFlags(aFlags) +{ +} + +void +CacheMemoryConsumer::DoMemoryReport(uint32_t aCurrentSize) +{ + if (!(mFlags & DONT_REPORT) && CacheStorageService::Self()) { + CacheStorageService::Self()->OnMemoryConsumptionChange(this, aCurrentSize); + } +} + +CacheStorageService::MemoryPool::MemoryPool(EType aType) +: mType(aType) +, mMemorySize(0) +{ +} + +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 +{ + switch (mType) { + case DISK: + return CacheObserver::MetadataMemoryLimit(); + case MEMORY: + return CacheObserver::MemoryCacheCapacity(); + } + + MOZ_CRASH("Bad pool type"); + return 0; +} + +NS_IMPL_ISUPPORTS(CacheStorageService, + nsICacheStorageService, + nsIMemoryReporter, + nsITimerCallback, + nsICacheTesting) + +CacheStorageService* CacheStorageService::sSelf = nullptr; + +CacheStorageService::CacheStorageService() +: mLock("CacheStorageService.mLock") +, mForcedValidEntriesLock("CacheStorageService.mForcedValidEntriesLock") +, mShutdown(false) +, mDiskPool(MemoryPool::DISK) +, mMemoryPool(MemoryPool::MEMORY) +{ + CacheFileIOManager::Init(); + + 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<nsIRunnable> event = + NewRunnableMethod(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) + : mService(CacheStorageService::Self()) + , mCallback(aVisitor) + , mSize(0) + , mNotifyStorage(true) + , mVisitEntries(aVisitEntries) + , mCancel(false) + { + MOZ_ASSERT(NS_IsMainThread()); + } + + virtual ~WalkCacheRunnable() + { + if (mCallback) { + ProxyReleaseMainThread(mCallback); + } + } + + RefPtr<CacheStorageService> mService; + nsCOMPtr<nsICacheStorageVisitor> mCallback; + + uint64_t mSize; + + bool mNotifyStorage : 1; + bool mVisitEntries : 1; + + Atomic<bool> mCancel; +}; + +// 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; + + CacheEntryTable* entries; + if (sGlobalEntryTables->Get(mContextKey, &entries)) { + for (auto iter = entries->Iter(); !iter.Done(); iter.Next()) { + CacheEntry* entry = iter.UserData(); + + // Ignore disk entries + if (entry->IsUsingDisk()) { + continue; + } + + 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 (mNotifyStorage) { + LOG((" storage")); + + // Second, notify overall storage info + mCallback->OnCacheStorageInfo(mEntryArray.Length(), mSize, + CacheObserver::MemoryCacheCapacity(), nullptr); + if (!mVisitEntries) + return NS_OK; // done + + mNotifyStorage = false; + + } else { + LOG((" entry [left=%d, 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<CacheEntry> 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(mCallback); + } + + virtual void OnEntryInfo(const nsACString & aURISpec, const nsACString & aIdEnhance, + int64_t aDataSize, int32_t aFetchCount, + uint32_t aLastModifiedTime, uint32_t aExpirationTime, + bool aPinned) override + { + nsresult rv; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), aURISpec); + if (NS_FAILED(rv)) { + return; + } + + rv = mCallback->OnCacheEntryInfo(uri, aIdEnhance, aDataSize, aFetchCount, + aLastModifiedTime, aExpirationTime, aPinned); + if (NS_FAILED(rv)) { + LOG((" callback failed, canceling the walk")); + mCancel = true; + } + } + +private: + nsCString mContextKey; + nsTArray<RefPtr<CacheEntry> > 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) + { + } + + 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<CacheIOThread> 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) + : mWalker(aWalker) + { + } + + NS_IMETHOD Run() override + { + MOZ_ASSERT(NS_IsMainThread()); + + nsresult rv; + + nsCOMPtr<nsIURI> uri; + rv = NS_NewURI(getter_AddRefs(uri), mURISpec); + if (NS_FAILED(rv)) { + return NS_OK; + } + + rv = mWalker->mCallback->OnCacheEntryInfo( + uri, mIdEnhance, mDataSize, mFetchCount, + mLastModifiedTime, mExpirationTime, mPinned); + if (NS_FAILED(rv)) { + mWalker->mCancel = true; + } + + return NS_OK; + } + + RefPtr<WalkDiskCacheRunnable> mWalker; + + nsCString mURISpec; + nsCString mIdEnhance; + int64_t mDataSize; + int32_t mFetchCount; + uint32_t mLastModifiedTime; + uint32_t mExpirationTime; + bool mPinned; + }; + + 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 (mVisitEntries) { + // both onStorageInfo and onCompleted are expected + NS_DispatchToMainThread(this); + } + return NS_DispatchToMainThread(this); + } + + mSize = size << 10; + + // Invoke onCacheStorageInfo with valid information. + NS_DispatchToMainThread(this); + + if (!mVisitEntries) { + return NS_OK; // done + } + + mPass = ITERATE_METADATA; + MOZ_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 (mNotifyStorage) { + nsCOMPtr<nsIFile> dir; + CacheFileIOManager::GetCacheDirectory(getter_AddRefs(dir)); + mCallback->OnCacheStorageInfo(mCount, mSize, CacheObserver::DiskCacheCapacity(), dir); + mNotifyStorage = 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, int32_t aFetchCount, + uint32_t aLastModifiedTime, uint32_t aExpirationTime, + bool aPinned) override + { + // Called directly from CacheFileIOManager::GetEntryInfo. + + // Invoke onCacheEntryInfo on the main thread for this entry. + RefPtr<OnCacheEntryInfoRunnable> info = new OnCacheEntryInfoRunnable(this); + info->mURISpec = aURISpec; + info->mIdEnhance = aIdEnhance; + info->mDataSize = aDataSize; + info->mFetchCount = aFetchCount; + info->mLastModifiedTime = aLastModifiedTime; + info->mExpirationTime = aExpirationTime; + info->mPinned = aPinned; + + NS_DispatchToMainThread(info); + } + + RefPtr<nsILoadContextInfo> 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<CacheIndexIterator> mIter; + uint32_t mCount; +}; + +} // namespace + +void CacheStorageService::DropPrivateBrowsingEntries() +{ + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) + return; + + nsTArray<nsCString> keys; + for (auto iter = sGlobalEntryTables->Iter(); !iter.Done(); iter.Next()) { + const nsACString& key = iter.Key(); + nsCOMPtr<nsILoadContextInfo> 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); + } +} + +namespace { + +class CleaupCacheDirectoriesRunnable : public Runnable +{ +public: + NS_DECL_NSIRUNNABLE + static bool Post(uint32_t aVersion, uint32_t aActive); + +private: + CleaupCacheDirectoriesRunnable(uint32_t aVersion, uint32_t aActive) + : mVersion(aVersion), mActive(aActive) + { + nsCacheService::GetDiskCacheDirectory(getter_AddRefs(mCache1Dir)); + CacheFileIOManager::GetCacheDirectory(getter_AddRefs(mCache2Dir)); +#if defined(MOZ_WIDGET_ANDROID) + CacheFileIOManager::GetProfilelessCacheDirectory(getter_AddRefs(mCache2Profileless)); +#endif + } + + virtual ~CleaupCacheDirectoriesRunnable() {} + uint32_t mVersion, mActive; + nsCOMPtr<nsIFile> mCache1Dir, mCache2Dir; +#if defined(MOZ_WIDGET_ANDROID) + nsCOMPtr<nsIFile> mCache2Profileless; +#endif +}; + +// static +bool CleaupCacheDirectoriesRunnable::Post(uint32_t aVersion, uint32_t aActive) +{ + // CleaupCacheDirectories is called regardless what cache version is set up to use. + // To obtain the cache1 directory we must unfortunatelly instantiate the old cache + // service despite it may not be used at all... This also initialize nsDeleteDir. + nsCOMPtr<nsICacheService> service = do_GetService(NS_CACHESERVICE_CONTRACTID); + if (!service) + return false; + + nsCOMPtr<nsIEventTarget> thread; + service->GetCacheIOTarget(getter_AddRefs(thread)); + if (!thread) + return false; + + RefPtr<CleaupCacheDirectoriesRunnable> r = + new CleaupCacheDirectoriesRunnable(aVersion, aActive); + thread->Dispatch(r, NS_DISPATCH_NORMAL); + return true; +} + +NS_IMETHODIMP CleaupCacheDirectoriesRunnable::Run() +{ + MOZ_ASSERT(!NS_IsMainThread()); + + if (mCache1Dir) { + nsDeleteDir::RemoveOldTrashes(mCache1Dir); + } + if (mCache2Dir) { + nsDeleteDir::RemoveOldTrashes(mCache2Dir); + } +#if defined(MOZ_WIDGET_ANDROID) + if (mCache2Profileless) { + nsDeleteDir::RemoveOldTrashes(mCache2Profileless); + // Always delete the profileless cache on Android + nsDeleteDir::DeleteDir(mCache2Profileless, true, 30000); + } +#endif + + // Delete the non-active version cache data right now + if (mVersion == mActive) { + return NS_OK; + } + + switch (mVersion) { + case 0: + if (mCache1Dir) { + nsDeleteDir::DeleteDir(mCache1Dir, true, 30000); + } + break; + case 1: + if (mCache2Dir) { + nsDeleteDir::DeleteDir(mCache2Dir, true, 30000); + } + break; + } + + return NS_OK; +} + +} // namespace + +// static +void CacheStorageService::CleaupCacheDirectories(uint32_t aVersion, uint32_t aActive) +{ + // Make sure we schedule just once in case CleaupCacheDirectories gets called + // multiple times from some reason. + static bool runOnce = CleaupCacheDirectoriesRunnable::Post(aVersion, aActive); + if (!runOnce) { + NS_WARNING("Could not start cache trashes cleanup"); + } +} + +// Helper methods + +// static +bool CacheStorageService::IsOnManagementThread() +{ + RefPtr<CacheStorageService> service = Self(); + if (!service) + return false; + + nsCOMPtr<nsIEventTarget> target = service->Thread(); + if (!target) + return false; + + bool currentThread; + nsresult rv = target->IsOnCurrentThread(¤tThread); + return NS_SUCCEEDED(rv) && currentThread; +} + +already_AddRefed<nsIEventTarget> CacheStorageService::Thread() const +{ + return CacheFileIOManager::IOTarget(); +} + +nsresult CacheStorageService::Dispatch(nsIRunnable* aEvent) +{ + RefPtr<CacheIOThread> cacheIOThread = CacheFileIOManager::IOThread(); + if (!cacheIOThread) + return NS_ERROR_NOT_AVAILABLE; + + return cacheIOThread->Dispatch(aEvent, CacheIOThread::MANAGEMENT); +} + +// nsICacheStorageService + +NS_IMETHODIMP CacheStorageService::MemoryCacheStorage(nsILoadContextInfo *aLoadContextInfo, + nsICacheStorage * *_retval) +{ + NS_ENSURE_ARG(aLoadContextInfo); + NS_ENSURE_ARG(_retval); + + nsCOMPtr<nsICacheStorage> storage; + if (CacheObserver::UseNewCache()) { + storage = new CacheStorage(aLoadContextInfo, false, false, false, false); + } + else { + storage = new _OldStorage(aLoadContextInfo, false, false, false, nullptr); + } + + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::DiskCacheStorage(nsILoadContextInfo *aLoadContextInfo, + bool aLookupAppCache, + nsICacheStorage * *_retval) +{ + NS_ENSURE_ARG(aLoadContextInfo); + 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<nsICacheStorage> storage; + if (CacheObserver::UseNewCache()) { + storage = new CacheStorage(aLoadContextInfo, useDisk, aLookupAppCache, false /* size limit */, false /* don't pin */); + } + else { + storage = new _OldStorage(aLoadContextInfo, useDisk, aLookupAppCache, false, nullptr); + } + + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::PinningCacheStorage(nsILoadContextInfo *aLoadContextInfo, + nsICacheStorage * *_retval) +{ + NS_ENSURE_ARG(aLoadContextInfo); + NS_ENSURE_ARG(_retval); + + if (!CacheObserver::UseNewCache()) { + return NS_ERROR_NOT_IMPLEMENTED; + } + + // When disk cache is disabled don't pretend we cache. + if (!CacheObserver::UseDiskCache()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsICacheStorage> storage = new CacheStorage( + aLoadContextInfo, true /* use disk */, false /* no appcache */, true /* ignore size checks */, true /* pin */); + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::AppCacheStorage(nsILoadContextInfo *aLoadContextInfo, + nsIApplicationCache *aApplicationCache, + nsICacheStorage * *_retval) +{ + NS_ENSURE_ARG(aLoadContextInfo); + NS_ENSURE_ARG(_retval); + + nsCOMPtr<nsICacheStorage> storage; + if (CacheObserver::UseNewCache()) { + // Using classification since cl believes we want to instantiate this method + // having the same name as the desired class... + storage = new mozilla::net::AppCacheStorage(aLoadContextInfo, aApplicationCache); + } + else { + storage = new _OldStorage(aLoadContextInfo, true, false, true, aApplicationCache); + } + + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::SynthesizedCacheStorage(nsILoadContextInfo *aLoadContextInfo, + nsICacheStorage * *_retval) +{ + NS_ENSURE_ARG(aLoadContextInfo); + NS_ENSURE_ARG(_retval); + + nsCOMPtr<nsICacheStorage> storage; + if (CacheObserver::UseNewCache()) { + storage = new CacheStorage(aLoadContextInfo, false, false, true /* skip size checks for synthesized cache */, false /* no pinning */); + } + else { + storage = new _OldStorage(aLoadContextInfo, false, false, false, nullptr); + } + + storage.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::Clear() +{ + nsresult rv; + + if (CacheObserver::UseNewCache()) { + // 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); + + nsTArray<nsCString> keys; + for (auto iter = sGlobalEntryTables->Iter(); !iter.Done(); iter.Next()) { + keys.AppendElement(iter.Key()); + } + + for (uint32_t i = 0; i < keys.Length(); ++i) { + DoomStorageEntries(keys[i], 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); + NS_ENSURE_SUCCESS(rv, rv); + } + } else { + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = serv->EvictEntries(nsICache::STORE_ANYWHERE); + 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<nsIRunnable> event = + new PurgeFromMemoryRunnable(this, what); + + return Dispatch(event); +} + +NS_IMETHODIMP CacheStorageService::PurgeFromMemoryRunnable::Run() +{ + if (NS_IsMainThread()) { + nsCOMPtr<nsIObserverService> 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; + + if (CacheObserver::UseNewCache()) { + rv = CacheIndex::AsyncGetDiskConsumption(aObserver); + NS_ENSURE_SUCCESS(rv, rv); + } else { + rv = _OldGetDiskConsumption::Get(aObserver); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP CacheStorageService::GetIoTarget(nsIEventTarget** aEventTarget) +{ + NS_ENSURE_ARG(aEventTarget); + + if (CacheObserver::UseNewCache()) { + nsCOMPtr<nsIEventTarget> ioTarget = CacheFileIOManager::IOTarget(); + ioTarget.forget(aEventTarget); + } + else { + nsresult rv; + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = serv->GetCacheIOTarget(aEventTarget); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +// 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 { + 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<bool> removedFrecency = pool.mFrecencyArray.RemoveElement(aEntry); + mozilla::DebugOnly<bool> 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<CacheEntry> 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->Put(aKey, aEntry); + return true; +} + +static bool +RemoveExactEntry(CacheEntryTable* aEntries, + nsACString const& aKey, + CacheEntry* aEntry, + bool aOverwrite) +{ + RefPtr<CacheEntry> 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; +} + +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()); + AppendMemoryStorageID(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()); + AppendMemoryStorageID(memoryStorageID); + + if (!sGlobalEntryTables->Get(memoryStorageID, &entries)) { + if (!aOnlyInMemory) { + LOG((" not recorded as memory only")); + return; + } + + entries = new CacheEntryTable(CacheEntryTable::MEMORY_ONLY); + sGlobalEntryTables->Put(memoryStorageID, entries); + 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); + + TimeStamp validUntil; + + if (!mForcedValidEntries.Get(aContextEntryKey, &validUntil)) { + return false; + } + + if (validUntil.IsNull()) { + return false; + } + + // Entry timeout not reached yet + if (TimeStamp::NowLoRes() <= validUntil) { + return true; + } + + // Entry timeout has been reached + mForcedValidEntries.Remove(aContextEntryKey); + return false; +} + +// 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); + + // This will be the timeout + TimeStamp validUntil = now + TimeDuration::FromSeconds(aSecondsToTheFuture); + + mForcedValidEntries.Put(aContextKey + aEntryKey, validUntil); +} + +void CacheStorageService::RemoveEntryForceValid(nsACString const &aContextKey, + nsACString const &aEntryKey) +{ + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + LOG(("CacheStorageService::RemoveEntryForceValid context='%s' entryKey=%s", + aContextKey.BeginReading(), aEntryKey.BeginReading())); + 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() < now) { + 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->mReportedMemoryConsumption; + if (savedMemorySize == aCurrentMemoryConsumption) + return; + + // Exchange saved size with current one. + aConsumer->mReportedMemoryConsumption = aCurrentMemoryConsumption; + + bool usingDisk = !(aConsumer->mFlags & 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. + if (mPurgeTimer) + return; + + // We don't know if this is called under the service lock or not, + // hence rather dispatch. + RefPtr<nsIEventTarget> 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<nsIRunnable> event = + NewRunnableMethod(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 = do_CreateInstance(NS_TIMER_CONTRACTID); + if (mPurgeTimer) { + nsresult rv; + rv = mPurgeTimer->InitWithCallback(this, 1000, nsITimer::TYPE_ONE_SHOT); + LOG((" timer init rv=0x%08x", rv)); + } +} + +NS_IMETHODIMP +CacheStorageService::Notify(nsITimer* aTimer) +{ + LOG(("CacheStorageService::Notify")); + + mozilla::MutexAutoLock lock(mLock); + + if (aTimer == mPurgeTimer) { + mPurgeTimer = nullptr; + + nsCOMPtr<nsIRunnable> event = + NewRunnableMethod(this, &CacheStorageService::PurgeOverMemoryLimit); + Dispatch(event); + } + + 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(); + } + + bool frecencyNeedsSort = true; + + // 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(frecencyNeedsSort, CacheEntry::PURGE_DATA_ONLY_DISK_BACKED); + } + + if (mMemorySize > memoryLimit) { + LOG((" metadata consumtion over the limit, abandon disk backed entries")); + PurgeByFrecency(frecencyNeedsSort, CacheEntry::PURGE_WHOLE_ONLY_DISK_BACKED); + } +#endif + + if (mMemorySize > memoryLimit) { + LOG((" memory data consumption over the limit, abandon any entry")); + PurgeByFrecency(frecencyNeedsSort, 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<CacheEntry> 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(bool &aFrecencyNeedsSort, uint32_t aWhat) +{ + MOZ_ASSERT(IsOnManagementThread()); + + if (aFrecencyNeedsSort) { + mFrecencyArray.Sort(FrecencyComparator()); + aFrecencyNeedsSort = false; + } + + uint32_t const memoryLimit = Limit(); + + for (uint32_t i = 0; mMemorySize > memoryLimit && i < mFrecencyArray.Length();) { + if (CacheIOThread::YieldAndRerun()) + return; + + RefPtr<CacheEntry> 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; + } +} + +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<CacheEntry> 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, + bool aReplace, + 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(), + aReplace, + aResult); +} + +nsresult +CacheStorageService::AddStorageEntry(nsCSubstring const& aContextKey, + const nsACString & aURI, + const nsACString & aIdExtension, + bool aWriteToDisk, + bool aSkipSizeCheck, + bool aPin, + bool aReplace, + CacheEntryHandle** aResult) +{ + nsresult rv; + + nsAutoCString entryKey; + rv = CacheEntry::HashingKey(EmptyCString(), aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + LOG(("CacheStorageService::AddStorageEntry [entryKey=%s, contextKey=%s]", + entryKey.get(), aContextKey.BeginReading())); + + RefPtr<CacheEntry> entry; + RefPtr<CacheEntryHandle> handle; + + { + mozilla::MutexAutoLock lock(mLock); + + NS_ENSURE_FALSE(mShutdown, NS_ERROR_NOT_INITIALIZED); + + // Ensure storage table + CacheEntryTable* entries; + if (!sGlobalEntryTables->Get(aContextKey, &entries)) { + entries = new CacheEntryTable(CacheEntryTable::ALL_ENTRIES); + sGlobalEntryTables->Put(aContextKey, entries); + LOG((" new storage entries table for context '%s'", aContextKey.BeginReading())); + } + + bool entryExists = entries->Get(entryKey, getter_AddRefs(entry)); + + if (entryExists && !aReplace) { + // 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")); + aReplace = true; + } + } + + // If truncate is demanded, delete and doom the current entry + if (entryExists && aReplace) { + 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. + aReplace = 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 (aReplace) { + RemoveEntryForceValid(aContextKey, entryKey); + } + + // Entry is not in the hashtable or has just been truncated... + entry = new CacheEntry(aContextKey, aURI, aIdExtension, aWriteToDisk, aSkipSizeCheck, aPin); + entries->Put(entryKey, 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()) { + AppendMemoryStorageID(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(EmptyCString(), 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%08x", rv)); + return NS_ERROR_NOT_AVAILABLE; + } + + *aResult = status == CacheIndex::EXISTS; + LOG((" %sfound in index", *aResult ? "" : "not ")); + return NS_OK; +} + +namespace { + +class CacheEntryDoomByKeyCallback : public CacheFileIOListener + , public nsIRunnable +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE + + explicit CacheEntryDoomByKeyCallback(nsICacheEntryDoomCallback* aCallback) + : mCallback(aCallback) { } + +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<nsICacheEntryDoomCallback> mCallback; + nsresult mResult; +}; + +CacheEntryDoomByKeyCallback::~CacheEntryDoomByKeyCallback() +{ + if (mCallback) + ProxyReleaseMainThread(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(EmptyCString(), aIdExtension, aURI, entryKey); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<CacheEntry> 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<CacheEntryDoomByKeyCallback> 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) : mCallback(aCallback) { } + NS_IMETHOD Run() override + { + mCallback->OnCacheEntryDoomed(NS_ERROR_NOT_AVAILABLE); + return NS_OK; + } + nsCOMPtr<nsICacheEntryDoomCallback> mCallback; + }; + + if (aCallback) { + RefPtr<Runnable> 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(nsCSubstring const& 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); + AppendMemoryStorageID(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); + } + } 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. + nsAutoPtr<CacheEntryTable> memoryEntries; + sGlobalEntryTables->RemoveAndForget(memoryStorageID, memoryEntries); + + CacheEntryTable* diskEntries; + if (memoryEntries && sGlobalEntryTables->Get(aContextKey, &diskEntries)) { + for (auto iter = memoryEntries->Iter(); !iter.Done(); iter.Next()) { + auto entry = iter.Data(); + RemoveExactEntry(diskEntries, iter.Key(), entry, false); + } + } + } + + { + mozilla::MutexAutoLock lock(mForcedValidEntriesLock); + + if (aContext) { + for (auto iter = mForcedValidEntries.Iter(); !iter.Done(); iter.Next()) { + bool matches; + DebugOnly<nsresult> 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) : mCallback(aCallback) { } + NS_IMETHOD Run() override + { + mCallback->OnCacheEntryDoomed(NS_OK); + return NS_OK; + } + nsCOMPtr<nsICacheEntryDoomCallback> mCallback; + }; + + if (aCallback) { + RefPtr<Runnable> 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<WalkDiskCacheRunnable> event = + new WalkDiskCacheRunnable(aStorage->LoadInfo(), aVisitEntries, aVisitor); + return event->Walk(); + } + + RefPtr<WalkMemoryCacheRunnable> 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(EmptyCString(), aIdExtension, aURISpec, entryKey); + + mozilla::MutexAutoLock lock(mLock); + + if (mShutdown) { + return; + } + + CacheEntryTable* entries; + RefPtr<CacheEntry> 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(EmptyCString(), aIdExtension, aURISpec, entryKey); + + RefPtr<CacheEntry> 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(); + + uint32_t dataSize; + if (NS_FAILED(aEntry->GetStorageDataSize(&dataSize))) { + dataSize = 0; + } + int32_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, + fetchCount, lastModified, expirationTime, + aEntry->IsPinned()); +} + +// static +uint32_t CacheStorageService::CacheQueueSize(bool highPriority) +{ + RefPtr<CacheIOThread> thread = CacheFileIOManager::IOThread(); + MOZ_ASSERT(thread); + return thread->QueueSize(highPriority); +} + +// Telementry 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 const* 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.Put(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); + } + + 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) +{ + 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."); + + MutexAutoLock lock(mLock); + + // 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 (auto iter1 = sGlobalEntryTables->Iter(); !iter1.Done(); iter1.Next()) { + CacheStorageService::Self()->Lock().AssertCurrentThreadOwns(); + + CacheEntryTable* table = iter1.UserData(); + + size_t size = 0; + mozilla::MallocSizeOf mallocSizeOf = CacheStorageService::MallocSizeOf; + + size += table->ShallowSizeOfIncludingThis(mallocSizeOf); + for (auto iter2 = table->Iter(); !iter2.Done(); iter2.Next()) { + size += iter2.Key().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<mozilla::net::CacheEntry> const& entry = iter2.Data(); + if (table->Type() == CacheEntryTable::MEMORY_ONLY || + entry->IsUsingDisk()) { + size += entry->SizeOfIncludingThis(mallocSizeOf); + } + } + + // These key names are not privacy-sensitive. + aHandleReport->Callback( + EmptyCString(), + nsPrintfCString("explicit/network/cache2/%s-storage(%s)", + table->Type() == CacheEntryTable::MEMORY_ONLY ? "memory" : "disk", + iter1.Key().BeginReading()), + nsIMemoryReporter::KIND_HEAP, nsIMemoryReporter::UNITS_BYTES, size, + NS_LITERAL_CSTRING("Memory used by the cache storage."), + 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<CacheIOThread> 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<IOThreadSuspender> suspender; + suspender.swap(mActiveIOSuspender); + suspender->Notify(); + return NS_OK; +} + +NS_IMETHODIMP +CacheStorageService::Flush(nsIObserver* aObserver) +{ + RefPtr<CacheIOThread> thread = CacheFileIOManager::IOThread(); + if (!thread) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsCOMPtr<nsIObserverService> 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<CacheStorageService::PurgeFromMemoryRunnable> r = + new CacheStorageService::PurgeFromMemoryRunnable(this, CacheEntry::PURGE_WHOLE); + + return thread->Dispatch(r, CacheIOThread::WRITE); +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/CacheStorageService.h b/netwerk/cache2/CacheStorageService.h new file mode 100644 index 0000000000..f40459d842 --- /dev/null +++ b/netwerk/cache2/CacheStorageService.h @@ -0,0 +1,422 @@ +/* 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 "nsITimer.h" +#include "nsICacheTesting.h" + +#include "nsClassHashtable.h" +#include "nsDataHashtable.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "nsProxyRelease.h" +#include "mozilla/Monitor.h" +#include "mozilla/Mutex.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 { +namespace net { + +class CacheStorageService; +class CacheStorage; +class CacheEntry; +class CacheEntryHandle; + +class CacheMemoryConsumer +{ +private: + friend class CacheStorageService; + uint32_t mReportedMemoryConsumption : 30; + uint32_t mFlags : 2; + +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: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHESTORAGESERVICE + NS_DECL_NSIMEMORYREPORTER + NS_DECL_NSITIMERCALLBACK + NS_DECL_NSICACHETESTING + + CacheStorageService(); + + void Shutdown(); + void DropPrivateBrowsingEntries(); + + // Takes care of deleting any pending trashes for both cache1 and cache2 + // as well as the cache directory of an inactive cache version when requested. + static void CleaupCacheDirectories(uint32_t aVersion, uint32_t aActive); + + static CacheStorageService* Self() { return sSelf; } + static nsISupports* SelfISupports() { return static_cast<nsICacheStorageService*>(Self()); } + nsresult Dispatch(nsIRunnable* aEvent); + static bool IsRunning() { return sSelf && !sSelf->mShutdown; } + static bool IsOnManagementThread(); + already_AddRefed<nsIEventTarget> Thread() const; + mozilla::Mutex& Lock() { return mLock; } + + // Tracks entries that may be forced valid in a pruned hashtable. + nsDataHashtable<nsCStringHashKey, TimeStamp> 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, int32_t aFetchCount, + uint32_t aLastModifiedTime, uint32_t aExpirationTime, + bool aPinned) = 0; + }; + + // Invokes OnEntryInfo for the given aEntry, synchronously. + static void GetCacheEntryInfo(CacheEntry* aEntry, EntryInfoCallback *aVisitor); + + 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); + +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 &aEntryKeyWithContext); + +private: + // These are helpers for telemetry monitoring of the memory pools. + void TelemetryPrune(TimeStamp &now); + void TelemetryRecordEntryCreation(CacheEntry const* entry); + void TelemetryRecordEntryRemoval(CacheEntry const* 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, + bool aReplace, + 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(nsCSubstring const& aContextKey, + nsILoadContextInfo* aContext, + bool aDiskStorage, + bool aPin, + nsICacheEntryDoomCallback* aCallback); + nsresult AddStorageEntry(nsCSubstring const& aContextKey, + const nsACString & aURI, + const nsACString & aIdExtension, + bool aWriteToDisk, + bool aSkipSizeCheck, + bool aPin, + bool aReplace, + CacheEntryHandle** aResult); + + static CacheStorageService* sSelf; + + mozilla::Mutex mLock; + mozilla::Mutex mForcedValidEntriesLock; + + bool mShutdown; + + // Accessible only on the service thread + class MemoryPool + { + public: + enum EType + { + DISK, + MEMORY, + } mType; + + explicit MemoryPool(EType aType); + ~MemoryPool(); + + nsTArray<RefPtr<CacheEntry> > mFrecencyArray; + nsTArray<RefPtr<CacheEntry> > mExpirationArray; + Atomic<uint32_t, Relaxed> mMemorySize; + + bool OnMemoryConsumptionChange(uint32_t aSavedMemorySize, + uint32_t aCurrentMemoryConsumption); + /** + * Purges entries from memory based on the frecency ordered array. + */ + void PurgeOverMemoryLimit(); + void PurgeExpired(); + void PurgeByFrecency(bool &aFrecencyNeedsSort, uint32_t aWhat); + void PurgeAll(uint32_t aWhat); + + private: + uint32_t Limit() const; + MemoryPool() = delete; + }; + + MemoryPool mDiskPool; + MemoryPool mMemoryPool; + TimeStamp mLastPurgeTime; + MemoryPool& Pool(bool aUsingDisk) + { + return aUsingDisk ? mDiskPool : mMemoryPool; + } + MemoryPool const& Pool(bool aUsingDisk) const + { + return aUsingDisk ? mDiskPool : mMemoryPool; + } + + nsCOMPtr<nsITimer> mPurgeTimer; + + class PurgeFromMemoryRunnable : public Runnable + { + public: + PurgeFromMemoryRunnable(CacheStorageService* aService, uint32_t aWhat) + : mService(aService), mWhat(aWhat) { } + + private: + virtual ~PurgeFromMemoryRunnable() { } + + NS_IMETHOD Run() override; + + RefPtr<CacheStorageService> 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. + nsDataHashtable<nsCStringHashKey, mozilla::TimeStamp> mPurgeTimeStamps; + + // nsICacheTesting + class IOThreadSuspender : public Runnable + { + public: + IOThreadSuspender() : mMon("IOThreadSuspender"), mSignaled(false) { } + void Notify(); + private: + virtual ~IOThreadSuspender() { } + NS_IMETHOD Run() override; + + Monitor mMon; + bool mSignaled; + }; + + RefPtr<IOThreadSuspender> mActiveIOSuspender; +}; + +template<class T> +void ProxyRelease(nsCOMPtr<T> &object, nsIThread* thread) +{ + NS_ProxyRelease(thread, object.forget()); +} + +template<class T> +void ProxyReleaseMainThread(nsCOMPtr<T> &object) +{ + nsCOMPtr<nsIThread> mainThread = do_GetMainThread(); + ProxyRelease(object, mainThread); +} + +} // 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/OldWrappers.cpp b/netwerk/cache2/OldWrappers.cpp new file mode 100644 index 0000000000..81df88df0e --- /dev/null +++ b/netwerk/cache2/OldWrappers.cpp @@ -0,0 +1,1155 @@ +// Stuff to link the old imp to the new api - will go away! + +#include "CacheLog.h" +#include "OldWrappers.h" +#include "CacheStorage.h" +#include "CacheStorageService.h" +#include "LoadContextInfo.h" +#include "nsCacheService.h" + +#include "nsIURI.h" +#include "nsICacheSession.h" +#include "nsIApplicationCache.h" +#include "nsIApplicationCacheService.h" +#include "nsIStreamTransportService.h" +#include "nsIFile.h" +#include "nsICacheEntryDoomCallback.h" +#include "nsICacheListener.h" +#include "nsICacheStorageVisitor.h" + +#include "nsServiceManagerUtils.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsProxyRelease.h" +#include "mozilla/Telemetry.h" + +static NS_DEFINE_CID(kStreamTransportServiceCID, + NS_STREAMTRANSPORTSERVICE_CID); + +static uint32_t const CHECK_MULTITHREADED = nsICacheStorage::CHECK_MULTITHREADED; + +namespace mozilla { +namespace net { + +namespace { + +// Fires the doom callback back on the main thread +// after the cache I/O thread is looped. + +class DoomCallbackSynchronizer : public Runnable +{ +public: + explicit DoomCallbackSynchronizer(nsICacheEntryDoomCallback* cb) : mCB(cb) + { + MOZ_COUNT_CTOR(DoomCallbackSynchronizer); + } + nsresult Dispatch(); + +private: + virtual ~DoomCallbackSynchronizer() + { + MOZ_COUNT_DTOR(DoomCallbackSynchronizer); + } + + NS_DECL_NSIRUNNABLE + nsCOMPtr<nsICacheEntryDoomCallback> mCB; +}; + +nsresult DoomCallbackSynchronizer::Dispatch() +{ + nsresult rv; + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIEventTarget> eventTarget; + rv = serv->GetCacheIOTarget(getter_AddRefs(eventTarget)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = eventTarget->Dispatch(this, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP DoomCallbackSynchronizer::Run() +{ + if (!NS_IsMainThread()) { + NS_DispatchToMainThread(this); + } + else { + if (mCB) + mCB->OnCacheEntryDoomed(NS_OK); + } + return NS_OK; +} + +// Receives doom callback from the old API and forwards to the new API + +class DoomCallbackWrapper : public nsICacheListener +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHELISTENER + + explicit DoomCallbackWrapper(nsICacheEntryDoomCallback* cb) : mCB(cb) + { + MOZ_COUNT_CTOR(DoomCallbackWrapper); + } + +private: + virtual ~DoomCallbackWrapper() + { + MOZ_COUNT_DTOR(DoomCallbackWrapper); + } + + nsCOMPtr<nsICacheEntryDoomCallback> mCB; +}; + +NS_IMPL_ISUPPORTS(DoomCallbackWrapper, nsICacheListener); + +NS_IMETHODIMP DoomCallbackWrapper::OnCacheEntryAvailable(nsICacheEntryDescriptor *descriptor, + nsCacheAccessMode accessGranted, + nsresult status) +{ + return NS_OK; +} + +NS_IMETHODIMP DoomCallbackWrapper::OnCacheEntryDoomed(nsresult status) +{ + if (!mCB) + return NS_ERROR_NULL_POINTER; + + mCB->OnCacheEntryDoomed(status); + mCB = nullptr; + return NS_OK; +} + +} // namespace + +// _OldVisitCallbackWrapper +// Receives visit callbacks from the old API and forwards it to the new API + +NS_IMPL_ISUPPORTS(_OldVisitCallbackWrapper, nsICacheVisitor) + +_OldVisitCallbackWrapper::~_OldVisitCallbackWrapper() +{ + if (!mHit) { + // The device has not been found, to not break the chain, simulate + // storage info callback. + mCB->OnCacheStorageInfo(0, 0, 0, nullptr); + } + + if (mVisitEntries) { + mCB->OnCacheEntryVisitCompleted(); + } + + MOZ_COUNT_DTOR(_OldVisitCallbackWrapper); +} + +NS_IMETHODIMP _OldVisitCallbackWrapper::VisitDevice(const char * deviceID, + nsICacheDeviceInfo *deviceInfo, + bool *_retval) +{ + if (!mCB) + return NS_ERROR_NULL_POINTER; + + *_retval = false; + if (strcmp(deviceID, mDeviceID)) { + // Not the device we want to visit + return NS_OK; + } + + mHit = true; + + nsresult rv; + + uint32_t capacity; + rv = deviceInfo->GetMaximumSize(&capacity); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> dir; + if (!strcmp(mDeviceID, "disk")) { + nsCacheService::GetDiskCacheDirectory(getter_AddRefs(dir)); + } else if (!strcmp(mDeviceID, "offline")) { + nsCacheService::GetAppCacheDirectory(getter_AddRefs(dir)); + } + + if (mLoadInfo->IsAnonymous()) { + // Anonymous visiting reports 0, 0 since we cannot count that + // early the number of anon entries. + mCB->OnCacheStorageInfo(0, 0, capacity, dir); + } else { + // Non-anon visitor counts all non-anon + ALL ANON entries, + // there is no way to determine the number of entries when + // using the old cache APIs - there is no concept of anonymous + // storage. + uint32_t entryCount; + rv = deviceInfo->GetEntryCount(&entryCount); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t totalSize; + rv = deviceInfo->GetTotalSize(&totalSize); + NS_ENSURE_SUCCESS(rv, rv); + + mCB->OnCacheStorageInfo(entryCount, totalSize, capacity, dir); + } + + *_retval = mVisitEntries; + return NS_OK; +} + +NS_IMETHODIMP _OldVisitCallbackWrapper::VisitEntry(const char * deviceID, + nsICacheEntryInfo *entryInfo, + bool *_retval) +{ + MOZ_ASSERT(!strcmp(deviceID, mDeviceID)); + + nsresult rv; + + *_retval = true; + + // Read all informative properties from the entry. + nsXPIDLCString clientId; + rv = entryInfo->GetClientID(getter_Copies(clientId)); + if (NS_FAILED(rv)) + return NS_OK; + + if (mLoadInfo->IsPrivate() != + StringBeginsWith(clientId, NS_LITERAL_CSTRING("HTTP-memory-only-PB"))) { + return NS_OK; + } + + nsAutoCString cacheKey, enhanceId; + rv = entryInfo->GetKey(cacheKey); + if (NS_FAILED(rv)) + return NS_OK; + + if (StringBeginsWith(cacheKey, NS_LITERAL_CSTRING("anon&"))) { + if (!mLoadInfo->IsAnonymous()) + return NS_OK; + + cacheKey = Substring(cacheKey, 5, cacheKey.Length()); + } else if (mLoadInfo->IsAnonymous()) { + return NS_OK; + } + + if (StringBeginsWith(cacheKey, NS_LITERAL_CSTRING("id="))) { + int32_t uriSpecEnd = cacheKey.Find("&uri="); + if (uriSpecEnd == kNotFound) // Corrupted, ignore + return NS_OK; + + enhanceId = Substring(cacheKey, 3, uriSpecEnd - 3); + cacheKey = Substring(cacheKey, uriSpecEnd + 1, cacheKey.Length()); + } + + if (StringBeginsWith(cacheKey, NS_LITERAL_CSTRING("uri="))) { + cacheKey = Substring(cacheKey, 4, cacheKey.Length()); + } + + nsCOMPtr<nsIURI> uri; + // cacheKey is strip of any prefixes + rv = NS_NewURI(getter_AddRefs(uri), cacheKey); + if (NS_FAILED(rv)) + return NS_OK; + + uint32_t dataSize; + if (NS_FAILED(entryInfo->GetDataSize(&dataSize))) + dataSize = 0; + int32_t fetchCount; + if (NS_FAILED(entryInfo->GetFetchCount(&fetchCount))) + fetchCount = 0; + uint32_t expirationTime; + if (NS_FAILED(entryInfo->GetExpirationTime(&expirationTime))) + expirationTime = 0; + uint32_t lastModified; + if (NS_FAILED(entryInfo->GetLastModified(&lastModified))) + lastModified = 0; + + // Send them to the consumer. + rv = mCB->OnCacheEntryInfo( + uri, enhanceId, (int64_t)dataSize, fetchCount, lastModified, expirationTime, false); + + *_retval = NS_SUCCEEDED(rv); + return NS_OK; +} + +// _OldGetDiskConsumption + +//static +nsresult _OldGetDiskConsumption::Get(nsICacheStorageConsumptionObserver* aCallback) +{ + nsresult rv; + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<_OldGetDiskConsumption> cb = new _OldGetDiskConsumption(aCallback); + + // _OldGetDiskConsumption stores the found size value, but until dispatched + // to the main thread it doesn't call on the consupmtion observer. See bellow. + rv = serv->VisitEntries(cb); + NS_ENSURE_SUCCESS(rv, rv); + + // We are called from CacheStorageService::AsyncGetDiskConsumption whose IDL + // documentation claims the callback is always delievered asynchronously + // back to the main thread. Despite we know the result synchronosusly when + // querying the old cache, we need to stand the word and dispatch the result + // to the main thread asynchronously. Hence the dispatch here. + return NS_DispatchToMainThread(cb); +} + +NS_IMPL_ISUPPORTS_INHERITED(_OldGetDiskConsumption, + Runnable, + nsICacheVisitor) + +_OldGetDiskConsumption::_OldGetDiskConsumption( + nsICacheStorageConsumptionObserver* aCallback) + : mCallback(aCallback) + , mSize(0) +{ +} + +NS_IMETHODIMP +_OldGetDiskConsumption::Run() +{ + mCallback->OnNetworkCacheDiskConsumption(mSize); + return NS_OK; +} + +NS_IMETHODIMP +_OldGetDiskConsumption::VisitDevice(const char * deviceID, + nsICacheDeviceInfo *deviceInfo, + bool *_retval) +{ + if (!strcmp(deviceID, "disk")) { + uint32_t size; + nsresult rv = deviceInfo->GetTotalSize(&size); + if (NS_SUCCEEDED(rv)) + mSize = (int64_t)size; + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +_OldGetDiskConsumption::VisitEntry(const char * deviceID, + nsICacheEntryInfo *entryInfo, + bool *_retval) +{ + MOZ_CRASH("Unexpected"); + return NS_OK; +} + + +// _OldCacheEntryWrapper + +_OldCacheEntryWrapper::_OldCacheEntryWrapper(nsICacheEntryDescriptor* desc) +: mOldDesc(desc), mOldInfo(desc) +{ + MOZ_COUNT_CTOR(_OldCacheEntryWrapper); + LOG(("Creating _OldCacheEntryWrapper %p for descriptor %p", this, desc)); +} + +_OldCacheEntryWrapper::_OldCacheEntryWrapper(nsICacheEntryInfo* info) +: mOldDesc(nullptr), mOldInfo(info) +{ + MOZ_COUNT_CTOR(_OldCacheEntryWrapper); + LOG(("Creating _OldCacheEntryWrapper %p for info %p", this, info)); +} + +_OldCacheEntryWrapper::~_OldCacheEntryWrapper() +{ + MOZ_COUNT_DTOR(_OldCacheEntryWrapper); + LOG(("Destroying _OldCacheEntryWrapper %p for descriptor %p", this, mOldInfo.get())); +} + +NS_IMETHODIMP _OldCacheEntryWrapper::GetIsForcedValid(bool *aIsForcedValid) +{ + // Unused stub + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::ForceValidFor(uint32_t aSecondsToTheFuture) +{ + // Unused stub + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(_OldCacheEntryWrapper, nsICacheEntry) + +NS_IMETHODIMP _OldCacheEntryWrapper::AsyncDoom(nsICacheEntryDoomCallback* listener) +{ + RefPtr<DoomCallbackWrapper> cb = listener + ? new DoomCallbackWrapper(listener) + : nullptr; + return AsyncDoom(cb); +} + +NS_IMETHODIMP _OldCacheEntryWrapper::GetDataSize(int64_t *aSize) +{ + uint32_t size; + nsresult rv = GetDataSize(&size); + if (NS_FAILED(rv)) + return rv; + + *aSize = size; + return NS_OK; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::GetAltDataSize(int64_t *aSize) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::GetPersistent(bool *aPersistToDisk) +{ + if (!mOldDesc) { + return NS_ERROR_NULL_POINTER; + } + + nsresult rv; + + nsCacheStoragePolicy policy; + rv = mOldDesc->GetStoragePolicy(&policy); + NS_ENSURE_SUCCESS(rv, rv); + + *aPersistToDisk = policy != nsICache::STORE_IN_MEMORY; + + return NS_OK; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::Recreate(bool aMemoryOnly, + nsICacheEntry** aResult) +{ + NS_ENSURE_TRUE(mOldDesc, NS_ERROR_NOT_AVAILABLE); + + nsCacheAccessMode mode; + nsresult rv = mOldDesc->GetAccessGranted(&mode); + NS_ENSURE_SUCCESS(rv, rv); + + if (!(mode & nsICache::ACCESS_WRITE)) + return NS_ERROR_NOT_AVAILABLE; + + LOG(("_OldCacheEntryWrapper::Recreate [this=%p]", this)); + + if (aMemoryOnly) + mOldDesc->SetStoragePolicy(nsICache::STORE_IN_MEMORY); + + nsCOMPtr<nsICacheEntry> self(this); + self.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::OpenInputStream(int64_t offset, + nsIInputStream * *_retval) +{ + if (offset > PR_UINT32_MAX) + return NS_ERROR_INVALID_ARG; + + return OpenInputStream(uint32_t(offset), _retval); +} +NS_IMETHODIMP _OldCacheEntryWrapper::OpenOutputStream(int64_t offset, + nsIOutputStream * *_retval) +{ + if (offset > PR_UINT32_MAX) + return NS_ERROR_INVALID_ARG; + + return OpenOutputStream(uint32_t(offset), _retval); +} + +NS_IMETHODIMP _OldCacheEntryWrapper::MaybeMarkValid() +{ + LOG(("_OldCacheEntryWrapper::MaybeMarkValid [this=%p]", this)); + + NS_ENSURE_TRUE(mOldDesc, NS_ERROR_NULL_POINTER); + + nsCacheAccessMode mode; + nsresult rv = mOldDesc->GetAccessGranted(&mode); + NS_ENSURE_SUCCESS(rv, rv); + + if (mode & nsICache::ACCESS_WRITE) { + LOG(("Marking cache entry valid [entry=%p, descr=%p]", this, mOldDesc)); + return mOldDesc->MarkValid(); + } + + LOG(("Not marking read-only cache entry valid [entry=%p, descr=%p]", this, mOldDesc)); + return NS_OK; +} + +NS_IMETHODIMP _OldCacheEntryWrapper::HasWriteAccess(bool aWriteAllowed_unused, bool *aWriteAccess) +{ + NS_ENSURE_TRUE(mOldDesc, NS_ERROR_NULL_POINTER); + NS_ENSURE_ARG(aWriteAccess); + + nsCacheAccessMode mode; + nsresult rv = mOldDesc->GetAccessGranted(&mode); + NS_ENSURE_SUCCESS(rv, rv); + + *aWriteAccess = !!(mode & nsICache::ACCESS_WRITE); + + LOG(("_OldCacheEntryWrapper::HasWriteAccess [this=%p, write-access=%d]", this, *aWriteAccess)); + + return NS_OK; +} + +namespace { + +class MetaDataVisitorWrapper : public nsICacheMetaDataVisitor +{ + virtual ~MetaDataVisitorWrapper() {} + + NS_DECL_ISUPPORTS + NS_DECL_NSICACHEMETADATAVISITOR + explicit MetaDataVisitorWrapper(nsICacheEntryMetaDataVisitor* cb) : mCB(cb) {} + nsCOMPtr<nsICacheEntryMetaDataVisitor> mCB; +}; + +NS_IMPL_ISUPPORTS(MetaDataVisitorWrapper, nsICacheMetaDataVisitor) + +NS_IMETHODIMP +MetaDataVisitorWrapper::VisitMetaDataElement(char const * key, + char const * value, + bool *goon) +{ + *goon = true; + return mCB->OnMetaDataElement(key, value); +} + +} // namespace + +NS_IMETHODIMP _OldCacheEntryWrapper::VisitMetaData(nsICacheEntryMetaDataVisitor* cb) +{ + RefPtr<MetaDataVisitorWrapper> w = new MetaDataVisitorWrapper(cb); + return mOldDesc->VisitMetaData(w); +} + +namespace { + +nsresult +GetCacheSessionNameForStoragePolicy( + nsCSubstring const &scheme, + nsCacheStoragePolicy storagePolicy, + bool isPrivate, + NeckoOriginAttributes const *originAttribs, + nsACString& sessionName) +{ + MOZ_ASSERT(!isPrivate || storagePolicy == nsICache::STORE_IN_MEMORY); + + // HTTP + if (scheme.EqualsLiteral("http") || + scheme.EqualsLiteral("https")) { + switch (storagePolicy) { + case nsICache::STORE_IN_MEMORY: + if (isPrivate) + sessionName.AssignLiteral("HTTP-memory-only-PB"); + else + sessionName.AssignLiteral("HTTP-memory-only"); + break; + case nsICache::STORE_OFFLINE: + // XXX This is actually never used, only added to prevent + // any compatibility damage. + sessionName.AssignLiteral("HTTP-offline"); + break; + default: + sessionName.AssignLiteral("HTTP"); + break; + } + } + // WYCIWYG + else if (scheme.EqualsLiteral("wyciwyg")) { + if (isPrivate) + sessionName.AssignLiteral("wyciwyg-private"); + else + sessionName.AssignLiteral("wyciwyg"); + } + // FTP + else if (scheme.EqualsLiteral("ftp")) { + if (isPrivate) + sessionName.AssignLiteral("FTP-private"); + else + sessionName.AssignLiteral("FTP"); + } + // all remaining URL scheme + else { + // Since with the new API a consumer cannot specify its own session name + // and partitioning of the cache is handled stricly only by the cache + // back-end internally, we will use a separate session name to pretend + // functionality of the new API wrapping the Darin's cache for all other + // URL schemes. + // Deliberately omitting |anonymous| since other session types don't + // recognize it too. + sessionName.AssignLiteral("other"); + if (isPrivate) + sessionName.AppendLiteral("-private"); + } + + nsAutoCString suffix; + originAttribs->CreateSuffix(suffix); + sessionName.Append(suffix); + + return NS_OK; +} + +nsresult +GetCacheSession(nsCSubstring const &aScheme, + bool aWriteToDisk, + nsILoadContextInfo* aLoadInfo, + nsIApplicationCache* aAppCache, + nsICacheSession** _result) +{ + nsresult rv; + + nsCacheStoragePolicy storagePolicy; + if (aAppCache) + storagePolicy = nsICache::STORE_OFFLINE; + else if (!aWriteToDisk || aLoadInfo->IsPrivate()) + storagePolicy = nsICache::STORE_IN_MEMORY; + else + storagePolicy = nsICache::STORE_ANYWHERE; + + nsAutoCString clientId; + if (aAppCache) { + aAppCache->GetClientID(clientId); + } + else { + rv = GetCacheSessionNameForStoragePolicy( + aScheme, + storagePolicy, + aLoadInfo->IsPrivate(), + aLoadInfo->OriginAttributesPtr(), + clientId); + NS_ENSURE_SUCCESS(rv, rv); + } + + LOG((" GetCacheSession for client=%s, policy=%d", clientId.get(), storagePolicy)); + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICacheSession> session; + rv = nsCacheService::GlobalInstance()->CreateSessionInternal(clientId.get(), + storagePolicy, + nsICache::STREAM_BASED, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->SetIsPrivate(aLoadInfo->IsPrivate()); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->SetDoomEntriesIfExpired(false); + NS_ENSURE_SUCCESS(rv, rv); + + if (aAppCache) { + nsCOMPtr<nsIFile> profileDirectory; + aAppCache->GetProfileDirectory(getter_AddRefs(profileDirectory)); + if (profileDirectory) + rv = session->SetProfileDirectory(profileDirectory); + NS_ENSURE_SUCCESS(rv, rv); + } + + session.forget(_result); + return NS_OK; +} + +} // namespace + + +NS_IMPL_ISUPPORTS_INHERITED(_OldCacheLoad, Runnable, nsICacheListener) + +_OldCacheLoad::_OldCacheLoad(nsCSubstring const& aScheme, + nsCSubstring const& aCacheKey, + nsICacheEntryOpenCallback* aCallback, + nsIApplicationCache* aAppCache, + nsILoadContextInfo* aLoadInfo, + bool aWriteToDisk, + uint32_t aFlags) + : mScheme(aScheme) + , mCacheKey(aCacheKey) + , mCallback(aCallback) + , mLoadInfo(GetLoadContextInfo(aLoadInfo)) + , mFlags(aFlags) + , mWriteToDisk(aWriteToDisk) + , mNew(true) + , mOpening(true) + , mSync(false) + , mStatus(NS_ERROR_UNEXPECTED) + , mRunCount(0) + , mAppCache(aAppCache) +{ + MOZ_COUNT_CTOR(_OldCacheLoad); +} + +_OldCacheLoad::~_OldCacheLoad() +{ + ProxyReleaseMainThread(mAppCache); + MOZ_COUNT_DTOR(_OldCacheLoad); +} + +nsresult _OldCacheLoad::Start() +{ + LOG(("_OldCacheLoad::Start [this=%p, key=%s]", this, mCacheKey.get())); + + mLoadStart = mozilla::TimeStamp::Now(); + + nsresult rv; + + // Consumers that can invoke this code as first and off the main thread + // are responsible for initiating these two services on the main thread. + // Currently this is only nsWyciwygChannel. + + // XXX: Start the cache service; otherwise DispatchToCacheIOThread will + // fail. + nsCOMPtr<nsICacheService> service = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + + // Ensure the stream transport service gets initialized on the main thread + if (NS_SUCCEEDED(rv) && NS_IsMainThread()) { + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(kStreamTransportServiceCID, &rv); + } + + if (NS_SUCCEEDED(rv)) { + rv = service->GetCacheIOTarget(getter_AddRefs(mCacheThread)); + } + + if (NS_SUCCEEDED(rv)) { + bool onCacheTarget; + rv = mCacheThread->IsOnCurrentThread(&onCacheTarget); + if (NS_SUCCEEDED(rv) && onCacheTarget) { + mSync = true; + } + } + + if (NS_SUCCEEDED(rv)) { + if (mSync) { + rv = Run(); + } + else { + rv = mCacheThread->Dispatch(this, NS_DISPATCH_NORMAL); + } + } + + return rv; +} + +NS_IMETHODIMP +_OldCacheLoad::Run() +{ + LOG(("_OldCacheLoad::Run [this=%p, key=%s, cb=%p]", this, mCacheKey.get(), mCallback.get())); + + nsresult rv; + + if (mOpening) { + mOpening = false; + nsCOMPtr<nsICacheSession> session; + rv = GetCacheSession(mScheme, mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + if (NS_SUCCEEDED(rv)) { + // AsyncOpenCacheEntry isn't really async when its called on the + // cache service thread. + + nsCacheAccessMode cacheAccess; + if (mFlags & nsICacheStorage::OPEN_TRUNCATE) + cacheAccess = nsICache::ACCESS_WRITE; + else if ((mFlags & nsICacheStorage::OPEN_READONLY) || mAppCache) + cacheAccess = nsICache::ACCESS_READ; + else + cacheAccess = nsICache::ACCESS_READ_WRITE; + + LOG((" session->AsyncOpenCacheEntry with access=%d", cacheAccess)); + + bool bypassBusy = mFlags & nsICacheStorage::OPEN_BYPASS_IF_BUSY; + + if (mSync && cacheAccess == nsICache::ACCESS_WRITE) { + nsCOMPtr<nsICacheEntryDescriptor> entry; + rv = session->OpenCacheEntry(mCacheKey, cacheAccess, bypassBusy, + getter_AddRefs(entry)); + + nsCacheAccessMode grantedAccess = 0; + if (NS_SUCCEEDED(rv)) { + entry->GetAccessGranted(&grantedAccess); + } + + return OnCacheEntryAvailable(entry, grantedAccess, rv); + } + + rv = session->AsyncOpenCacheEntry(mCacheKey, cacheAccess, this, bypassBusy); + if (NS_SUCCEEDED(rv)) + return NS_OK; + } + + // Opening failed, propagate the error to the consumer + LOG((" Opening cache entry failed with rv=0x%08x", rv)); + mStatus = rv; + mNew = false; + NS_DispatchToMainThread(this); + } else { + if (!mCallback) { + LOG((" duplicate call, bypassed")); + return NS_OK; + } + + if (NS_SUCCEEDED(mStatus)) { + if (mFlags & nsICacheStorage::OPEN_TRUNCATE) { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::NETWORK_CACHE_V1_TRUNCATE_TIME_MS, + mLoadStart); + } + else if (mNew) { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::NETWORK_CACHE_V1_MISS_TIME_MS, + mLoadStart); + } + else { + mozilla::Telemetry::AccumulateTimeDelta( + mozilla::Telemetry::NETWORK_CACHE_V1_HIT_TIME_MS, + mLoadStart); + } + } + + if (!(mFlags & CHECK_MULTITHREADED)) + Check(); + + // break cycles + nsCOMPtr<nsICacheEntryOpenCallback> cb = mCallback.forget(); + mCacheThread = nullptr; + nsCOMPtr<nsICacheEntry> entry = mCacheEntry.forget(); + + rv = cb->OnCacheEntryAvailable(entry, mNew, mAppCache, mStatus); + + if (NS_FAILED(rv) && entry) { + LOG((" cb->OnCacheEntryAvailable failed with rv=0x%08x", rv)); + if (mNew) + entry->AsyncDoom(nullptr); + else + entry->Close(); + } + } + + return rv; +} + +NS_IMETHODIMP +_OldCacheLoad::OnCacheEntryAvailable(nsICacheEntryDescriptor *entry, + nsCacheAccessMode access, + nsresult status) +{ + LOG(("_OldCacheLoad::OnCacheEntryAvailable [this=%p, ent=%p, cb=%p, appcache=%p, access=%x]", + this, entry, mCallback.get(), mAppCache.get(), access)); + + // XXX Bug 759805: Sometimes we will call this method directly from + // HttpCacheQuery::Run when AsyncOpenCacheEntry fails, but + // AsyncOpenCacheEntry will also call this method. As a workaround, we just + // ensure we only execute this code once. + NS_ENSURE_TRUE(mRunCount == 0, NS_ERROR_UNEXPECTED); + ++mRunCount; + + mCacheEntry = entry ? new _OldCacheEntryWrapper(entry) : nullptr; + mStatus = status; + mNew = access == nsICache::ACCESS_WRITE; + + if (mFlags & CHECK_MULTITHREADED) + Check(); + + if (mSync) + return Run(); + + return NS_DispatchToMainThread(this); +} + +void +_OldCacheLoad::Check() +{ + if (!mCacheEntry) + return; + + if (mNew) + return; + + uint32_t result; + nsresult rv = mCallback->OnCacheEntryCheck(mCacheEntry, mAppCache, &result); + LOG((" OnCacheEntryCheck result ent=%p, cb=%p, appcache=%p, rv=0x%08x, result=%d", + mCacheEntry.get(), mCallback.get(), mAppCache.get(), rv, result)); + + if (NS_FAILED(rv)) { + NS_WARNING("cache check failed"); + } + + if (NS_FAILED(rv) || result == nsICacheEntryOpenCallback::ENTRY_NOT_WANTED) { + mCacheEntry->Close(); + mCacheEntry = nullptr; + mStatus = NS_ERROR_CACHE_KEY_NOT_FOUND; + } +} + +NS_IMETHODIMP +_OldCacheLoad::OnCacheEntryDoomed(nsresult) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +// nsICacheStorage old cache wrapper + +NS_IMPL_ISUPPORTS(_OldStorage, nsICacheStorage) + +_OldStorage::_OldStorage(nsILoadContextInfo* aInfo, + bool aAllowDisk, + bool aLookupAppCache, + bool aOfflineStorage, + nsIApplicationCache* aAppCache) +: mLoadInfo(GetLoadContextInfo(aInfo)) +, mAppCache(aAppCache) +, mWriteToDisk(aAllowDisk) +, mLookupAppCache(aLookupAppCache) +, mOfflineStorage(aOfflineStorage) +{ + MOZ_COUNT_CTOR(_OldStorage); +} + +_OldStorage::~_OldStorage() +{ + MOZ_COUNT_DTOR(_OldStorage); +} + +NS_IMETHODIMP _OldStorage::AsyncOpenURI(nsIURI *aURI, + const nsACString & aIdExtension, + uint32_t aFlags, + nsICacheEntryOpenCallback *aCallback) +{ + NS_ENSURE_ARG(aURI); + NS_ENSURE_ARG(aCallback); + +#ifdef MOZ_LOGGING + nsAutoCString uriSpec; + aURI->GetAsciiSpec(uriSpec); + LOG(("_OldStorage::AsyncOpenURI [this=%p, uri=%s, ide=%s, flags=%x]", + this, uriSpec.get(), aIdExtension.BeginReading(), aFlags)); +#endif + + nsresult rv; + + nsAutoCString cacheKey, scheme; + rv = AssembleCacheKey(aURI, aIdExtension, cacheKey, scheme); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mAppCache && (mLookupAppCache || mOfflineStorage)) { + rv = ChooseApplicationCache(cacheKey, getter_AddRefs(mAppCache)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mAppCache) { + // From a chosen appcache open only as readonly + aFlags &= ~nsICacheStorage::OPEN_TRUNCATE; + } + } + + RefPtr<_OldCacheLoad> cacheLoad = + new _OldCacheLoad(scheme, cacheKey, aCallback, mAppCache, + mLoadInfo, mWriteToDisk, aFlags); + + rv = cacheLoad->Start(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP _OldStorage::OpenTruncate(nsIURI *aURI, const nsACString & aIdExtension, + nsICacheEntry **aCacheEntry) +{ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP _OldStorage::Exists(nsIURI *aURI, const nsACString & aIdExtension, + bool *aResult) +{ + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP _OldStorage::AsyncDoomURI(nsIURI *aURI, const nsACString & aIdExtension, + nsICacheEntryDoomCallback* aCallback) +{ + LOG(("_OldStorage::AsyncDoomURI")); + + nsresult rv; + + nsAutoCString cacheKey, scheme; + rv = AssembleCacheKey(aURI, aIdExtension, cacheKey, scheme); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsICacheSession> session; + rv = GetCacheSession(scheme, mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<DoomCallbackWrapper> cb = aCallback + ? new DoomCallbackWrapper(aCallback) + : nullptr; + rv = session->DoomEntry(cacheKey, cb); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP _OldStorage::AsyncEvictStorage(nsICacheEntryDoomCallback* aCallback) +{ + LOG(("_OldStorage::AsyncEvictStorage")); + + nsresult rv; + + if (!mAppCache && mOfflineStorage) { + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appCacheService->Evict(mLoadInfo); + NS_ENSURE_SUCCESS(rv, rv); + } else if (mAppCache) { + nsCOMPtr<nsICacheSession> session; + rv = GetCacheSession(EmptyCString(), + mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->EvictEntries(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Oh, I'll be so happy when session names are gone... + nsCOMPtr<nsICacheSession> session; + rv = GetCacheSession(NS_LITERAL_CSTRING("http"), + mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->EvictEntries(); + NS_ENSURE_SUCCESS(rv, rv); + + rv = GetCacheSession(NS_LITERAL_CSTRING("wyciwyg"), + mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->EvictEntries(); + NS_ENSURE_SUCCESS(rv, rv); + + // This clears any data from scheme other then http, wyciwyg or ftp + rv = GetCacheSession(EmptyCString(), + mWriteToDisk, mLoadInfo, mAppCache, + getter_AddRefs(session)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = session->EvictEntries(); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (aCallback) { + RefPtr<DoomCallbackSynchronizer> sync = + new DoomCallbackSynchronizer(aCallback); + rv = sync->Dispatch(); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP _OldStorage::AsyncVisitStorage(nsICacheStorageVisitor* aVisitor, + bool aVisitEntries) +{ + LOG(("_OldStorage::AsyncVisitStorage")); + + NS_ENSURE_ARG(aVisitor); + + nsresult rv; + + nsCOMPtr<nsICacheService> serv = + do_GetService(NS_CACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + char* deviceID; + if (mAppCache || mOfflineStorage) { + deviceID = const_cast<char*>("offline"); + } else if (!mWriteToDisk || mLoadInfo->IsPrivate()) { + deviceID = const_cast<char*>("memory"); + } else { + deviceID = const_cast<char*>("disk"); + } + + RefPtr<_OldVisitCallbackWrapper> cb = new _OldVisitCallbackWrapper( + deviceID, aVisitor, aVisitEntries, mLoadInfo); + rv = nsCacheService::GlobalInstance()->VisitEntriesInternal(cb); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// Internal + +nsresult _OldStorage::AssembleCacheKey(nsIURI *aURI, + nsACString const & aIdExtension, + nsACString & aCacheKey, + nsACString & aScheme) +{ + // Copied from nsHttpChannel::AssembleCacheKey + + aCacheKey.Truncate(); + + nsresult rv; + + rv = aURI->GetScheme(aScheme); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriSpec; + if (aScheme.EqualsLiteral("http") || + aScheme.EqualsLiteral("https")) { + if (mLoadInfo->IsAnonymous()) { + aCacheKey.AssignLiteral("anon&"); + } + + if (!aIdExtension.IsEmpty()) { + aCacheKey.AppendPrintf("id=%s&", aIdExtension.BeginReading()); + } + + nsCOMPtr<nsIURI> noRefURI; + rv = aURI->CloneIgnoringRef(getter_AddRefs(noRefURI)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = noRefURI->GetAsciiSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + + if (!aCacheKey.IsEmpty()) { + aCacheKey.AppendLiteral("uri="); + } + } + else if (aScheme.EqualsLiteral("wyciwyg")) { + rv = aURI->GetSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + } + else { + rv = aURI->GetAsciiSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + } + + aCacheKey.Append(uriSpec); + + return NS_OK; +} + +nsresult _OldStorage::ChooseApplicationCache(nsCSubstring const &cacheKey, + nsIApplicationCache** aCache) +{ + nsresult rv; + + nsCOMPtr<nsIApplicationCacheService> appCacheService = + do_GetService(NS_APPLICATIONCACHESERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = appCacheService->ChooseApplicationCache(cacheKey, mLoadInfo, aCache); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace net +} // namespace mozilla diff --git a/netwerk/cache2/OldWrappers.h b/netwerk/cache2/OldWrappers.h new file mode 100644 index 0000000000..f85b0741ac --- /dev/null +++ b/netwerk/cache2/OldWrappers.h @@ -0,0 +1,284 @@ +// Stuff to link the old imp to the new api - will go away! + +#ifndef OLDWRAPPERS__H__ +#define OLDWRAPPERS__H__ + +#include "nsICacheEntry.h" +#include "nsICacheListener.h" +#include "nsICacheStorage.h" + +#include "nsCOMPtr.h" +#include "nsICacheEntryOpenCallback.h" +#include "nsICacheEntryDescriptor.h" +#include "nsICacheStorageVisitor.h" +#include "nsThreadUtils.h" +#include "mozilla/TimeStamp.h" + +class nsIURI; +class nsICacheEntryOpenCallback; +class nsICacheStorageConsumptionObserver; +class nsIApplicationCache; +class nsILoadContextInfo; + +namespace mozilla { namespace net { + +class CacheStorage; + +class _OldCacheEntryWrapper : public nsICacheEntry +{ +public: + NS_DECL_THREADSAFE_ISUPPORTS + + // nsICacheEntryDescriptor + NS_IMETHOD SetExpirationTime(uint32_t expirationTime) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->SetExpirationTime(expirationTime); + } + nsresult OpenInputStream(uint32_t offset, nsIInputStream * *_retval) + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->OpenInputStream(offset, _retval); + } + nsresult OpenOutputStream(uint32_t offset, nsIOutputStream * *_retval) + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->OpenOutputStream(offset, _retval); + } + NS_IMETHOD OpenAlternativeOutputStream(const nsACString & type, nsIOutputStream * *_retval) override + { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD OpenAlternativeInputStream(const nsACString & type, nsIInputStream * *_retval) override + { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD GetPredictedDataSize(int64_t *aPredictedDataSize) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->GetPredictedDataSize(aPredictedDataSize); + } + NS_IMETHOD SetPredictedDataSize(int64_t aPredictedDataSize) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->SetPredictedDataSize(aPredictedDataSize); + } + NS_IMETHOD GetSecurityInfo(nsISupports * *aSecurityInfo) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->GetSecurityInfo(aSecurityInfo); + } + NS_IMETHOD SetSecurityInfo(nsISupports *aSecurityInfo) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->SetSecurityInfo(aSecurityInfo); + } + NS_IMETHOD GetStorageDataSize(uint32_t *aStorageDataSize) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->GetStorageDataSize(aStorageDataSize); + } + nsresult AsyncDoom(nsICacheListener *listener) + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->AsyncDoom(listener); + } + NS_IMETHOD MarkValid(void) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->MarkValid(); + } + NS_IMETHOD Close(void) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->Close(); + } + NS_IMETHOD GetMetaDataElement(const char * key, char * *_retval) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->GetMetaDataElement(key, _retval); + } + NS_IMETHOD SetMetaDataElement(const char * key, const char * value) override + { + return !mOldDesc ? NS_ERROR_NULL_POINTER : + mOldDesc->SetMetaDataElement(key, value); + } + + NS_IMETHOD GetDiskStorageSizeInKB(uint32_t *aDiskStorageSize) override + { + return NS_ERROR_NOT_IMPLEMENTED; + } + + // nsICacheEntryInfo + NS_IMETHOD GetKey(nsACString & aKey) override + { + return mOldInfo->GetKey(aKey); + } + NS_IMETHOD GetFetchCount(int32_t *aFetchCount) override + { + return mOldInfo->GetFetchCount(aFetchCount); + } + NS_IMETHOD GetLastFetched(uint32_t *aLastFetched) override + { + return mOldInfo->GetLastFetched(aLastFetched); + } + NS_IMETHOD GetLastModified(uint32_t *aLastModified) override + { + return mOldInfo->GetLastModified(aLastModified); + } + NS_IMETHOD GetExpirationTime(uint32_t *aExpirationTime) override + { + return mOldInfo->GetExpirationTime(aExpirationTime); + } + nsresult GetDataSize(uint32_t *aDataSize) + { + return mOldInfo->GetDataSize(aDataSize); + } + + NS_IMETHOD AsyncDoom(nsICacheEntryDoomCallback* listener) override; + NS_IMETHOD GetPersistent(bool *aPersistToDisk) override; + NS_IMETHOD GetIsForcedValid(bool *aIsForcedValid) override; + NS_IMETHOD ForceValidFor(uint32_t aSecondsToTheFuture) override; + NS_IMETHOD SetValid() override { return NS_OK; } + NS_IMETHOD MetaDataReady() override { return NS_OK; } + NS_IMETHOD Recreate(bool, nsICacheEntry**) override; + NS_IMETHOD GetDataSize(int64_t *size) override; + NS_IMETHOD GetAltDataSize(int64_t *size) override; + NS_IMETHOD OpenInputStream(int64_t offset, nsIInputStream * *_retval) override; + NS_IMETHOD OpenOutputStream(int64_t offset, nsIOutputStream * *_retval) override; + NS_IMETHOD MaybeMarkValid() override; + NS_IMETHOD HasWriteAccess(bool aWriteOnly, bool *aWriteAccess) override; + NS_IMETHOD VisitMetaData(nsICacheEntryMetaDataVisitor*) override; + + explicit _OldCacheEntryWrapper(nsICacheEntryDescriptor* desc); + explicit _OldCacheEntryWrapper(nsICacheEntryInfo* info); + +private: + virtual ~_OldCacheEntryWrapper(); + + _OldCacheEntryWrapper() = delete; + nsICacheEntryDescriptor* mOldDesc; // ref holded in mOldInfo + nsCOMPtr<nsICacheEntryInfo> mOldInfo; +}; + + +class _OldCacheLoad : public Runnable + , public nsICacheListener +{ +public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSICACHELISTENER + + _OldCacheLoad(nsCSubstring const& aScheme, + nsCSubstring const& aCacheKey, + nsICacheEntryOpenCallback* aCallback, + nsIApplicationCache* aAppCache, + nsILoadContextInfo* aLoadInfo, + bool aWriteToDisk, + uint32_t aFlags); + + nsresult Start(); + +protected: + virtual ~_OldCacheLoad(); + +private: + void Check(); + + nsCOMPtr<nsIEventTarget> mCacheThread; + + nsCString const mScheme; + nsCString const mCacheKey; + nsCOMPtr<nsICacheEntryOpenCallback> mCallback; + nsCOMPtr<nsILoadContextInfo> mLoadInfo; + uint32_t const mFlags; + + bool const mWriteToDisk : 1; + bool mNew : 1; + bool mOpening : 1; + bool mSync : 1; + + nsCOMPtr<nsICacheEntry> mCacheEntry; + nsresult mStatus; + uint32_t mRunCount; + nsCOMPtr<nsIApplicationCache> mAppCache; + + mozilla::TimeStamp mLoadStart; +}; + + +class _OldStorage : public nsICacheStorage +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHESTORAGE + +public: + _OldStorage(nsILoadContextInfo* aInfo, + bool aAllowDisk, + bool aLookupAppCache, + bool aOfflineStorage, + nsIApplicationCache* aAppCache); + +private: + virtual ~_OldStorage(); + nsresult AssembleCacheKey(nsIURI *aURI, nsACString const & aIdExtension, + nsACString & aCacheKey, nsACString & aScheme); + nsresult ChooseApplicationCache(nsCSubstring const &cacheKey, nsIApplicationCache** aCache); + + nsCOMPtr<nsILoadContextInfo> mLoadInfo; + nsCOMPtr<nsIApplicationCache> mAppCache; + bool const mWriteToDisk : 1; + bool const mLookupAppCache : 1; + bool const mOfflineStorage : 1; +}; + +class _OldVisitCallbackWrapper : public nsICacheVisitor +{ + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICACHEVISITOR + + _OldVisitCallbackWrapper(char const * deviceID, + nsICacheStorageVisitor * cb, + bool visitEntries, + nsILoadContextInfo * aInfo) + : mCB(cb) + , mVisitEntries(visitEntries) + , mDeviceID(deviceID) + , mLoadInfo(aInfo) + , mHit(false) + { + MOZ_COUNT_CTOR(_OldVisitCallbackWrapper); + } + +private: + virtual ~_OldVisitCallbackWrapper(); + nsCOMPtr<nsICacheStorageVisitor> mCB; + bool mVisitEntries; + char const * mDeviceID; + nsCOMPtr<nsILoadContextInfo> mLoadInfo; + bool mHit; // set to true when the device was found +}; + +class _OldGetDiskConsumption : public Runnable, + public nsICacheVisitor +{ +public: + static nsresult Get(nsICacheStorageConsumptionObserver* aCallback); + +private: + explicit _OldGetDiskConsumption(nsICacheStorageConsumptionObserver* aCallback); + virtual ~_OldGetDiskConsumption() {} + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSICACHEVISITOR + NS_DECL_NSIRUNNABLE + + nsCOMPtr<nsICacheStorageConsumptionObserver> mCallback; + int64_t mSize; +}; + +} // namespace net +} // namespace mozilla + +#endif diff --git a/netwerk/cache2/moz.build b/netwerk/cache2/moz.build new file mode 100644 index 0000000000..4fc6db59de --- /dev/null +++ b/netwerk/cache2/moz.build @@ -0,0 +1,59 @@ +# -*- 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/. + +XPIDL_SOURCES += [ + 'nsICacheEntry.idl', + 'nsICacheEntryDoomCallback.idl', + 'nsICacheEntryOpenCallback.idl', + 'nsICacheStorage.idl', + 'nsICacheStorageService.idl', + 'nsICacheStorageVisitor.idl', + 'nsICacheTesting.idl', +] + +XPIDL_MODULE = 'necko_cache2' + +EXPORTS += [ + 'CacheObserver.h', + 'CacheStorageService.h', +] + +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', + 'CacheStorage.cpp', + 'CacheStorageService.cpp', + 'OldWrappers.cpp', +] + +# AppCacheStorage.cpp cannot be built in unified mode because it uses plarena.h. +SOURCES += [ + 'AppCacheStorage.cpp', +] + +LOCAL_INCLUDES += [ + '/netwerk/base', + '/netwerk/cache', +] + +FINAL_LIBRARY = 'xul' + +if CONFIG['GNU_CXX']: + CXXFLAGS += ['-Wno-error=shadow'] diff --git a/netwerk/cache2/nsICacheEntry.idl b/netwerk/cache2/nsICacheEntry.idl new file mode 100644 index 0000000000..1885b9423c --- /dev/null +++ b/netwerk/cache2/nsICacheEntry.idl @@ -0,0 +1,292 @@ +/* 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" +#include "nsICache.idl" + +interface nsIInputStream; +interface nsIOutputStream; +interface nsICacheEntryDoomCallback; + +interface nsICacheListener; +interface nsIFile; +interface nsICacheEntryMetaDataVisitor; + +[scriptable, uuid(607c2a2c-0a48-40b9-a956-8cf2bb9857cf)] +interface nsICacheEntry : nsISupports +{ + /** + * 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; + + /** + * 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 long 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); + + /** + * 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; + + /** + * 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. + * + * @return blocking, buffered output stream. + */ + nsIOutputStream openOutputStream(in long long offset); + + /** + * Stores the Content-Length specified in the HTTP header for this + * entry. Checked before we write to the cache entry, to prevent ever + * taking up space in the cache for an entry that we know up front + * is going to have to be evicted anyway. See bug 588507. + */ + attribute int64_t predictedDataSize; + + /** + * Get/set security info on the cache entry for this descriptor. + */ + attribute nsISupports 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(); + + /** + * 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; + + /** + * Opens and returns an output stream that a consumer may use to save an + * alternate representation of the data. + * @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. + */ + nsIOutputStream openAlternativeOutputStream(in ACString type); + + /** + * 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); + + /**************************************************************************** + * 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..68409bb7eb --- /dev/null +++ b/netwerk/cache2/nsICacheEntryOpenCallback.idl @@ -0,0 +1,91 @@ +/* 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; +interface nsIApplicationCache; + +[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. + * @param aApplicationCache + * Optional, application cache the entry has been found in, if any. + * @return + * State of the entry, see the constants just above. + */ + unsigned long onCacheEntryCheck(in nsICacheEntry aEntry, + in nsIApplicationCache aApplicationCache); + + /** + * 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. May be null when + * loading from a particular application cache and the URI has not + * been found in that application cache. + * @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 aApplicationCache + * When an entry had been found in an application cache, this is the + * given application cache. It should be associated with the loading + * channel. + * @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 + * - cache entry was not found in a given application cache + */ + void onCacheEntryAvailable(in nsICacheEntry aEntry, + in boolean aNew, + in nsIApplicationCache aApplicationCache, + in nsresult aResult); +}; diff --git a/netwerk/cache2/nsICacheStorage.idl b/netwerk/cache2/nsICacheStorage.idl new file mode 100644 index 0000000000..f500a300b9 --- /dev/null +++ b/netwerk/cache2/nsICacheStorage.idl @@ -0,0 +1,134 @@ +/* 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); + + /** + * 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..2be9b1bab0 --- /dev/null +++ b/netwerk/cache2/nsICacheStorageService.idl @@ -0,0 +1,126 @@ +/* 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 nsIApplicationCache; +interface nsIEventTarget; +interface nsICacheStorageConsumptionObserver; + +/** + * 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. + * + * @param aLookupAppCache + * When set true (for top level document loading channels) app cache will + * be first to check on to find entries in. + */ + nsICacheStorage diskCacheStorage(in nsILoadContextInfo aLoadContextInfo, + in bool aLookupAppCache); + + /** + * 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); + + /** + * Get storage for a specified application cache obtained using some different + * mechanism. + * + * @param aLoadContextInfo + * Mandatory reference to a load context information. + * @param aApplicationCache + * Optional reference to an existing appcache. When left null, this will + * work with offline cache as a whole. + */ + nsICacheStorage appCacheStorage(in nsILoadContextInfo aLoadContextInfo, + in nsIApplicationCache aApplicationCache); + + /** + * Get storage for synthesized cache entries that we currently use for ServiceWorker interception in non-e10s mode. + * + * This cache storage has no limits on file size to allow the ServiceWorker to intercept large files. + */ + nsICacheStorage synthesizedCacheStorage(in nsILoadContextInfo aLoadContextInfo); + + /** + * 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); +}; + +[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..d6720acce7 --- /dev/null +++ b/netwerk/cache2/nsICacheStorageVisitor.idl @@ -0,0 +1,33 @@ +/* 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; + +[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 long aFetchCount, + in uint32_t aLastModifiedTime, + in uint32_t aExpirationTime, + in boolean aPinned); + + /** + */ + 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); +}; |