/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef CacheIndex__h__ #define CacheIndex__h__ #include "CacheLog.h" #include "CacheFileIOManager.h" #include "nsIRunnable.h" #include "CacheHashUtils.h" #include "nsICacheStorageService.h" #include "nsICacheEntry.h" #include "nsILoadContextInfo.h" #include "nsIWeakReferenceUtils.h" #include "nsTHashtable.h" #include "nsThreadUtils.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/SHA1.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPtr.h" #include "mozilla/EndianUtils.h" #include "mozilla/TimeStamp.h" #include "mozilla/UniquePtr.h" class nsIFile; class nsIDirectoryEnumerator; class nsITimer; #ifdef DEBUG # define DEBUG_STATS 1 #endif namespace mozilla { namespace net { class CacheFileMetadata; class FileOpenHelper; class CacheIndexIterator; const uint16_t kIndexTimeNotAvailable = 0xFFFFU; const uint16_t kIndexTimeOutOfBound = 0xFFFEU; using CacheIndexHeader = struct { // Version of the index. The index must be ignored and deleted when the file // on disk was written with a newer version. uint32_t mVersion; // Timestamp of time when the last successful write of the index started. // During update process we use this timestamp for a quick validation of entry // files. If last modified time of the file is lower than this timestamp, we // skip parsing of such file since the information in index should be up to // date. uint32_t mTimeStamp; // We set this flag as soon as possible after parsing index during startup // and clean it after we write journal to disk during shutdown. We ignore the // journal and start update process whenever this flag is set during index // parsing. uint32_t mIsDirty; // The amount of data written to the cache. When it reaches // kTelemetryReportBytesLimit a telemetry report is sent and the counter is // reset. uint32_t mKBWritten; }; static_assert(sizeof(CacheIndexHeader::mVersion) + sizeof(CacheIndexHeader::mTimeStamp) + sizeof(CacheIndexHeader::mIsDirty) + sizeof(CacheIndexHeader::mKBWritten) == sizeof(CacheIndexHeader), "Unexpected sizeof(CacheIndexHeader)!"); #pragma pack(push, 1) struct CacheIndexRecord { SHA1Sum::Hash mHash{}; uint32_t mFrecency{0}; OriginAttrsHash mOriginAttrsHash{0}; uint16_t mOnStartTime{kIndexTimeNotAvailable}; uint16_t mOnStopTime{kIndexTimeNotAvailable}; uint8_t mContentType{nsICacheEntry::CONTENT_TYPE_UNKNOWN}; /* * 1000 0000 0000 0000 0000 0000 0000 0000 : initialized * 0100 0000 0000 0000 0000 0000 0000 0000 : anonymous * 0010 0000 0000 0000 0000 0000 0000 0000 : removed * 0001 0000 0000 0000 0000 0000 0000 0000 : dirty * 0000 1000 0000 0000 0000 0000 0000 0000 : fresh * 0000 0100 0000 0000 0000 0000 0000 0000 : pinned * 0000 0010 0000 0000 0000 0000 0000 0000 : has cached alt data * 0000 0001 0000 0000 0000 0000 0000 0000 : reserved * 0000 0000 1111 1111 1111 1111 1111 1111 : file size (in kB) */ uint32_t mFlags{0}; CacheIndexRecord() = default; }; #pragma pack(pop) static_assert(sizeof(CacheIndexRecord::mHash) + sizeof(CacheIndexRecord::mFrecency) + sizeof(CacheIndexRecord::mOriginAttrsHash) + sizeof(CacheIndexRecord::mOnStartTime) + sizeof(CacheIndexRecord::mOnStopTime) + sizeof(CacheIndexRecord::mContentType) + sizeof(CacheIndexRecord::mFlags) == sizeof(CacheIndexRecord), "Unexpected sizeof(CacheIndexRecord)!"); class CacheIndexRecordWrapper final { public: NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CacheIndexRecordWrapper) CacheIndexRecordWrapper() : mRec(MakeUnique()) {} CacheIndexRecord* Get() { return mRec.get(); } private: ~CacheIndexRecordWrapper(); UniquePtr mRec; }; class CacheIndexEntry : public PLDHashEntryHdr { public: using KeyType = const SHA1Sum::Hash&; using KeyTypePointer = const SHA1Sum::Hash*; explicit CacheIndexEntry(KeyTypePointer aKey) { MOZ_COUNT_CTOR(CacheIndexEntry); mRec = new CacheIndexRecordWrapper(); LOG(("CacheIndexEntry::CacheIndexEntry() - Created record [rec=%p]", mRec->Get())); memcpy(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash)); } CacheIndexEntry(const CacheIndexEntry& aOther) { MOZ_ASSERT_UNREACHABLE("CacheIndexEntry copy constructor is forbidden!"); } ~CacheIndexEntry() { MOZ_COUNT_DTOR(CacheIndexEntry); LOG(("CacheIndexEntry::~CacheIndexEntry() - Deleting record [rec=%p]", mRec->Get())); } // KeyEquals(): does this entry match this key? bool KeyEquals(KeyTypePointer aKey) const { return memcmp(&mRec->Get()->mHash, aKey, sizeof(SHA1Sum::Hash)) == 0; } // KeyToPointer(): Convert KeyType to KeyTypePointer static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; } // HashKey(): calculate the hash number static PLDHashNumber HashKey(KeyTypePointer aKey) { return (reinterpret_cast(aKey))[0]; } // ALLOW_MEMMOVE can we move this class with memmove(), or do we have // to use the copy constructor? enum { ALLOW_MEMMOVE = true }; bool operator==(const CacheIndexEntry& aOther) const { return KeyEquals(&aOther.mRec->Get()->mHash); } CacheIndexEntry& operator=(const CacheIndexEntry& aOther) { MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aOther.mRec->Get()->mHash, sizeof(SHA1Sum::Hash)) == 0); mRec->Get()->mFrecency = aOther.mRec->Get()->mFrecency; mRec->Get()->mOriginAttrsHash = aOther.mRec->Get()->mOriginAttrsHash; mRec->Get()->mOnStartTime = aOther.mRec->Get()->mOnStartTime; mRec->Get()->mOnStopTime = aOther.mRec->Get()->mOnStopTime; mRec->Get()->mContentType = aOther.mRec->Get()->mContentType; mRec->Get()->mFlags = aOther.mRec->Get()->mFlags; return *this; } void InitNew() { mRec->Get()->mFrecency = 0; mRec->Get()->mOriginAttrsHash = 0; mRec->Get()->mOnStartTime = kIndexTimeNotAvailable; mRec->Get()->mOnStopTime = kIndexTimeNotAvailable; mRec->Get()->mContentType = nsICacheEntry::CONTENT_TYPE_UNKNOWN; mRec->Get()->mFlags = 0; } void Init(OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned) { MOZ_ASSERT(mRec->Get()->mFrecency == 0); MOZ_ASSERT(mRec->Get()->mOriginAttrsHash == 0); MOZ_ASSERT(mRec->Get()->mOnStartTime == kIndexTimeNotAvailable); MOZ_ASSERT(mRec->Get()->mOnStopTime == kIndexTimeNotAvailable); MOZ_ASSERT(mRec->Get()->mContentType == nsICacheEntry::CONTENT_TYPE_UNKNOWN); // When we init the entry it must be fresh and may be dirty MOZ_ASSERT((mRec->Get()->mFlags & ~kDirtyMask) == kFreshMask); mRec->Get()->mOriginAttrsHash = aOriginAttrsHash; mRec->Get()->mFlags |= kInitializedMask; if (aAnonymous) { mRec->Get()->mFlags |= kAnonymousMask; } if (aPinned) { mRec->Get()->mFlags |= kPinnedMask; } } const SHA1Sum::Hash* Hash() const { return &mRec->Get()->mHash; } bool IsInitialized() const { return !!(mRec->Get()->mFlags & kInitializedMask); } mozilla::net::OriginAttrsHash OriginAttrsHash() const { return mRec->Get()->mOriginAttrsHash; } bool Anonymous() const { return !!(mRec->Get()->mFlags & kAnonymousMask); } bool IsRemoved() const { return !!(mRec->Get()->mFlags & kRemovedMask); } void MarkRemoved() { mRec->Get()->mFlags |= kRemovedMask; } bool IsDirty() const { return !!(mRec->Get()->mFlags & kDirtyMask); } void MarkDirty() { mRec->Get()->mFlags |= kDirtyMask; } void ClearDirty() { mRec->Get()->mFlags &= ~kDirtyMask; } bool IsFresh() const { return !!(mRec->Get()->mFlags & kFreshMask); } void MarkFresh() { mRec->Get()->mFlags |= kFreshMask; } bool IsPinned() const { return !!(mRec->Get()->mFlags & kPinnedMask); } void SetFrecency(uint32_t aFrecency) { mRec->Get()->mFrecency = aFrecency; } uint32_t GetFrecency() const { return mRec->Get()->mFrecency; } void SetHasAltData(bool aHasAltData) { aHasAltData ? mRec->Get()->mFlags |= kHasAltDataMask : mRec->Get()->mFlags &= ~kHasAltDataMask; } bool GetHasAltData() const { return !!(mRec->Get()->mFlags & kHasAltDataMask); } void SetOnStartTime(uint16_t aTime) { mRec->Get()->mOnStartTime = aTime; } uint16_t GetOnStartTime() const { return mRec->Get()->mOnStartTime; } void SetOnStopTime(uint16_t aTime) { mRec->Get()->mOnStopTime = aTime; } uint16_t GetOnStopTime() const { return mRec->Get()->mOnStopTime; } void SetContentType(uint8_t aType) { mRec->Get()->mContentType = aType; } uint8_t GetContentType() const { return GetContentType(mRec->Get()); } static uint8_t GetContentType(CacheIndexRecord* aRec) { if (aRec->mContentType >= nsICacheEntry::CONTENT_TYPE_LAST) { LOG( ("CacheIndexEntry::GetContentType() - Found invalid content type " "[hash=%08x%08x%08x%08x%08x, contentType=%u]", LOGSHA1(aRec->mHash), aRec->mContentType)); return nsICacheEntry::CONTENT_TYPE_UNKNOWN; } return aRec->mContentType; } // Sets filesize in kilobytes. void SetFileSize(uint32_t aFileSize) { if (aFileSize > kFileSizeMask) { LOG( ("CacheIndexEntry::SetFileSize() - FileSize is too large, " "truncating to %u", kFileSizeMask)); aFileSize = kFileSizeMask; } mRec->Get()->mFlags &= ~kFileSizeMask; mRec->Get()->mFlags |= aFileSize; } // Returns filesize in kilobytes. uint32_t GetFileSize() const { return GetFileSize(*(mRec->Get())); } static uint32_t GetFileSize(const CacheIndexRecord& aRec) { return aRec.mFlags & kFileSizeMask; } static uint32_t IsPinned(CacheIndexRecord* aRec) { return aRec->mFlags & kPinnedMask; } bool IsFileEmpty() const { return GetFileSize() == 0; } void WriteToBuf(void* aBuf) { uint8_t* ptr = static_cast(aBuf); memcpy(ptr, mRec->Get()->mHash, sizeof(SHA1Sum::Hash)); ptr += sizeof(SHA1Sum::Hash); NetworkEndian::writeUint32(ptr, mRec->Get()->mFrecency); ptr += sizeof(uint32_t); NetworkEndian::writeUint64(ptr, mRec->Get()->mOriginAttrsHash); ptr += sizeof(uint64_t); NetworkEndian::writeUint16(ptr, mRec->Get()->mOnStartTime); ptr += sizeof(uint16_t); NetworkEndian::writeUint16(ptr, mRec->Get()->mOnStopTime); ptr += sizeof(uint16_t); *ptr = mRec->Get()->mContentType; ptr += sizeof(uint8_t); // Dirty and fresh flags should never go to disk, since they make sense only // during current session. NetworkEndian::writeUint32( ptr, mRec->Get()->mFlags & ~(kDirtyMask | kFreshMask)); } void ReadFromBuf(void* aBuf) { const uint8_t* ptr = static_cast(aBuf); MOZ_ASSERT(memcmp(&mRec->Get()->mHash, ptr, sizeof(SHA1Sum::Hash)) == 0); ptr += sizeof(SHA1Sum::Hash); mRec->Get()->mFrecency = NetworkEndian::readUint32(ptr); ptr += sizeof(uint32_t); mRec->Get()->mOriginAttrsHash = NetworkEndian::readUint64(ptr); ptr += sizeof(uint64_t); mRec->Get()->mOnStartTime = NetworkEndian::readUint16(ptr); ptr += sizeof(uint16_t); mRec->Get()->mOnStopTime = NetworkEndian::readUint16(ptr); ptr += sizeof(uint16_t); mRec->Get()->mContentType = *ptr; ptr += sizeof(uint8_t); mRec->Get()->mFlags = NetworkEndian::readUint32(ptr); } void Log() const { LOG( ("CacheIndexEntry::Log() [this=%p, hash=%08x%08x%08x%08x%08x, fresh=%u," " initialized=%u, removed=%u, dirty=%u, anonymous=%u, " "originAttrsHash=%" PRIx64 ", frecency=%u, hasAltData=%u, " "onStartTime=%u, onStopTime=%u, contentType=%u, size=%u]", this, LOGSHA1(mRec->Get()->mHash), IsFresh(), IsInitialized(), IsRemoved(), IsDirty(), Anonymous(), OriginAttrsHash(), GetFrecency(), GetHasAltData(), GetOnStartTime(), GetOnStopTime(), GetContentType(), GetFileSize())); } static bool RecordMatchesLoadContextInfo(CacheIndexRecordWrapper* aRec, nsILoadContextInfo* aInfo) { MOZ_ASSERT(aInfo); return !aInfo->IsPrivate() && GetOriginAttrsHash(*aInfo->OriginAttributesPtr()) == aRec->Get()->mOriginAttrsHash && aInfo->IsAnonymous() == !!(aRec->Get()->mFlags & kAnonymousMask); } // Memory reporting size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf) const { return mallocSizeOf(mRec->Get()); } size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf) const { return mallocSizeOf(this) + SizeOfExcludingThis(mallocSizeOf); } private: friend class CacheIndexEntryUpdate; friend class CacheIndex; friend class CacheIndexEntryAutoManage; friend struct CacheIndexRecord; static const uint32_t kInitializedMask = 0x80000000; static const uint32_t kAnonymousMask = 0x40000000; // This flag is set when the entry was removed. We need to keep this // information in memory until we write the index file. static const uint32_t kRemovedMask = 0x20000000; // This flag is set when the information in memory is not in sync with the // information in index file on disk. static const uint32_t kDirtyMask = 0x10000000; // This flag is set when the information about the entry is fresh, i.e. // we've created or opened this entry during this session, or we've seen // this entry during update or build process. static const uint32_t kFreshMask = 0x08000000; // Indicates a pinned entry. static const uint32_t kPinnedMask = 0x04000000; // Indicates there is cached alternative data in the entry. static const uint32_t kHasAltDataMask = 0x02000000; static const uint32_t kReservedMask = 0x01000000; // FileSize in kilobytes static const uint32_t kFileSizeMask = 0x00FFFFFF; RefPtr mRec; }; class CacheIndexEntryUpdate : public CacheIndexEntry { public: explicit CacheIndexEntryUpdate(CacheIndexEntry::KeyTypePointer aKey) : CacheIndexEntry(aKey), mUpdateFlags(0) { MOZ_COUNT_CTOR(CacheIndexEntryUpdate); LOG(("CacheIndexEntryUpdate::CacheIndexEntryUpdate()")); } ~CacheIndexEntryUpdate() { MOZ_COUNT_DTOR(CacheIndexEntryUpdate); LOG(("CacheIndexEntryUpdate::~CacheIndexEntryUpdate()")); } CacheIndexEntryUpdate& operator=(const CacheIndexEntry& aOther) { MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aOther.mRec->Get()->mHash, sizeof(SHA1Sum::Hash)) == 0); mUpdateFlags = 0; *(static_cast(this)) = aOther; return *this; } void InitNew() { mUpdateFlags = kFrecencyUpdatedMask | kHasAltDataUpdatedMask | kOnStartTimeUpdatedMask | kOnStopTimeUpdatedMask | kContentTypeUpdatedMask | kFileSizeUpdatedMask; CacheIndexEntry::InitNew(); } void SetFrecency(uint32_t aFrecency) { mUpdateFlags |= kFrecencyUpdatedMask; CacheIndexEntry::SetFrecency(aFrecency); } void SetHasAltData(bool aHasAltData) { mUpdateFlags |= kHasAltDataUpdatedMask; CacheIndexEntry::SetHasAltData(aHasAltData); } void SetOnStartTime(uint16_t aTime) { mUpdateFlags |= kOnStartTimeUpdatedMask; CacheIndexEntry::SetOnStartTime(aTime); } void SetOnStopTime(uint16_t aTime) { mUpdateFlags |= kOnStopTimeUpdatedMask; CacheIndexEntry::SetOnStopTime(aTime); } void SetContentType(uint8_t aType) { mUpdateFlags |= kContentTypeUpdatedMask; CacheIndexEntry::SetContentType(aType); } void SetFileSize(uint32_t aFileSize) { mUpdateFlags |= kFileSizeUpdatedMask; CacheIndexEntry::SetFileSize(aFileSize); } void ApplyUpdate(CacheIndexEntry* aDst) { MOZ_ASSERT(memcmp(&mRec->Get()->mHash, &aDst->mRec->Get()->mHash, sizeof(SHA1Sum::Hash)) == 0); if (mUpdateFlags & kFrecencyUpdatedMask) { aDst->mRec->Get()->mFrecency = mRec->Get()->mFrecency; } aDst->mRec->Get()->mOriginAttrsHash = mRec->Get()->mOriginAttrsHash; if (mUpdateFlags & kOnStartTimeUpdatedMask) { aDst->mRec->Get()->mOnStartTime = mRec->Get()->mOnStartTime; } if (mUpdateFlags & kOnStopTimeUpdatedMask) { aDst->mRec->Get()->mOnStopTime = mRec->Get()->mOnStopTime; } if (mUpdateFlags & kContentTypeUpdatedMask) { aDst->mRec->Get()->mContentType = mRec->Get()->mContentType; } if (mUpdateFlags & kHasAltDataUpdatedMask && ((aDst->mRec->Get()->mFlags ^ mRec->Get()->mFlags) & kHasAltDataMask)) { // Toggle the bit if we need to. aDst->mRec->Get()->mFlags ^= kHasAltDataMask; } if (mUpdateFlags & kFileSizeUpdatedMask) { // Copy all flags except |HasAltData|. aDst->mRec->Get()->mFlags |= (mRec->Get()->mFlags & ~kHasAltDataMask); } else { // Copy all flags except |HasAltData| and file size. aDst->mRec->Get()->mFlags &= kFileSizeMask; aDst->mRec->Get()->mFlags |= (mRec->Get()->mFlags & ~kHasAltDataMask & ~kFileSizeMask); } } private: static const uint32_t kFrecencyUpdatedMask = 0x00000001; static const uint32_t kContentTypeUpdatedMask = 0x00000002; static const uint32_t kFileSizeUpdatedMask = 0x00000004; static const uint32_t kHasAltDataUpdatedMask = 0x00000008; static const uint32_t kOnStartTimeUpdatedMask = 0x00000010; static const uint32_t kOnStopTimeUpdatedMask = 0x00000020; uint32_t mUpdateFlags; }; class CacheIndexStats { public: CacheIndexStats() { for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { mCountByType[i] = 0; mSizeByType[i] = 0; } } bool operator==(const CacheIndexStats& aOther) const { for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { if (mCountByType[i] != aOther.mCountByType[i] || mSizeByType[i] != aOther.mSizeByType[i]) { return false; } } return #ifdef DEBUG aOther.mStateLogged == mStateLogged && #endif aOther.mCount == mCount && aOther.mNotInitialized == mNotInitialized && aOther.mRemoved == mRemoved && aOther.mDirty == mDirty && aOther.mFresh == mFresh && aOther.mEmpty == mEmpty && aOther.mSize == mSize; } #ifdef DEBUG void DisableLogging() { mDisableLogging = true; } #endif void Log() { LOG( ("CacheIndexStats::Log() [count=%u, notInitialized=%u, removed=%u, " "dirty=%u, fresh=%u, empty=%u, size=%u]", mCount, mNotInitialized, mRemoved, mDirty, mFresh, mEmpty, mSize)); } void Clear() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Clear() - state logged!"); mCount = 0; mNotInitialized = 0; mRemoved = 0; mDirty = 0; mFresh = 0; mEmpty = 0; mSize = 0; for (uint32_t i = 0; i < nsICacheEntry::CONTENT_TYPE_LAST; ++i) { mCountByType[i] = 0; mSizeByType[i] = 0; } } #ifdef DEBUG bool StateLogged() { return mStateLogged; } #endif uint32_t Count() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Count() - state logged!"); return mCount; } uint32_t CountByType(uint8_t aContentType) { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::CountByType() - state logged!"); MOZ_RELEASE_ASSERT(aContentType < nsICacheEntry::CONTENT_TYPE_LAST); return mCountByType[aContentType]; } uint32_t Dirty() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Dirty() - state logged!"); return mDirty; } uint32_t Fresh() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Fresh() - state logged!"); return mFresh; } uint32_t ActiveEntriesCount() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::ActiveEntriesCount() - state " "logged!"); return mCount - mRemoved - mNotInitialized - mEmpty; } uint32_t Size() { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::Size() - state logged!"); return mSize; } uint32_t SizeByType(uint8_t aContentType) { MOZ_ASSERT(!mStateLogged, "CacheIndexStats::SizeByType() - state logged!"); MOZ_RELEASE_ASSERT(aContentType < nsICacheEntry::CONTENT_TYPE_LAST); return mSizeByType[aContentType]; } void BeforeChange(const CacheIndexEntry* aEntry) { #ifdef DEBUG_STATS if (!mDisableLogging) { LOG(("CacheIndexStats::BeforeChange()")); Log(); } #endif MOZ_ASSERT(!mStateLogged, "CacheIndexStats::BeforeChange() - state " "logged!"); #ifdef DEBUG mStateLogged = true; #endif if (aEntry) { MOZ_ASSERT(mCount); uint8_t contentType = aEntry->GetContentType(); mCount--; mCountByType[contentType]--; if (aEntry->IsDirty()) { MOZ_ASSERT(mDirty); mDirty--; } if (aEntry->IsFresh()) { MOZ_ASSERT(mFresh); mFresh--; } if (aEntry->IsRemoved()) { MOZ_ASSERT(mRemoved); mRemoved--; } else { if (!aEntry->IsInitialized()) { MOZ_ASSERT(mNotInitialized); mNotInitialized--; } else { if (aEntry->IsFileEmpty()) { MOZ_ASSERT(mEmpty); mEmpty--; } else { MOZ_ASSERT(mSize >= aEntry->GetFileSize()); mSize -= aEntry->GetFileSize(); mSizeByType[contentType] -= aEntry->GetFileSize(); } } } } } void AfterChange(const CacheIndexEntry* aEntry) { MOZ_ASSERT(mStateLogged, "CacheIndexStats::AfterChange() - state not " "logged!"); #ifdef DEBUG mStateLogged = false; #endif if (aEntry) { uint8_t contentType = aEntry->GetContentType(); ++mCount; ++mCountByType[contentType]; if (aEntry->IsDirty()) { mDirty++; } if (aEntry->IsFresh()) { mFresh++; } if (aEntry->IsRemoved()) { mRemoved++; } else { if (!aEntry->IsInitialized()) { mNotInitialized++; } else { if (aEntry->IsFileEmpty()) { mEmpty++; } else { mSize += aEntry->GetFileSize(); mSizeByType[contentType] += aEntry->GetFileSize(); } } } } #ifdef DEBUG_STATS if (!mDisableLogging) { LOG(("CacheIndexStats::AfterChange()")); Log(); } #endif } private: uint32_t mCount{0}; uint32_t mCountByType[nsICacheEntry::CONTENT_TYPE_LAST]{0}; uint32_t mNotInitialized{0}; uint32_t mRemoved{0}; uint32_t mDirty{0}; uint32_t mFresh{0}; uint32_t mEmpty{0}; uint32_t mSize{0}; uint32_t mSizeByType[nsICacheEntry::CONTENT_TYPE_LAST]{0}; #ifdef DEBUG // We completely remove the data about an entry from the stats in // BeforeChange() and set this flag to true. The entry is then modified, // deleted or created and the data is again put into the stats and this flag // set to false. Statistics must not be read during this time since the // information is not correct. bool mStateLogged{false}; // Disables logging in this instance of CacheIndexStats bool mDisableLogging{false}; #endif }; class CacheIndex final : public CacheFileIOListener, public nsIRunnable { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIRUNNABLE CacheIndex(); static nsresult Init(nsIFile* aCacheDirectory); static nsresult PreShutdown(); static nsresult Shutdown(); // Following methods can be called only on IO thread. // Add entry to the index. The entry shouldn't be present in index. This // method is called whenever a new handle for a new entry file is created. The // newly created entry is not initialized and it must be either initialized // with InitEntry() or removed with RemoveEntry(). static nsresult AddEntry(const SHA1Sum::Hash* aHash); // Inform index about an existing entry that should be present in index. This // method is called whenever a new handle for an existing entry file is // created. Like in case of AddEntry(), either InitEntry() or RemoveEntry() // must be called on the entry, since the entry is not initizlized if the // index is outdated. static nsresult EnsureEntryExists(const SHA1Sum::Hash* aHash); // Initialize the entry. It MUST be present in index. Call to AddEntry() or // EnsureEntryExists() must precede the call to this method. static nsresult InitEntry(const SHA1Sum::Hash* aHash, OriginAttrsHash aOriginAttrsHash, bool aAnonymous, bool aPinned); // Remove entry from index. The entry should be present in index. static nsresult RemoveEntry(const SHA1Sum::Hash* aHash); // Update some information in entry. The entry MUST be present in index and // MUST be initialized. Call to AddEntry() or EnsureEntryExists() and to // InitEntry() must precede the call to this method. // Pass nullptr if the value didn't change. static nsresult UpdateEntry(const SHA1Sum::Hash* aHash, const uint32_t* aFrecency, const bool* aHasAltData, const uint16_t* aOnStartTime, const uint16_t* aOnStopTime, const uint8_t* aContentType, const uint32_t* aSize); // Remove all entries from the index. Called when clearing the whole cache. static nsresult RemoveAll(); enum EntryStatus { EXISTS = 0, DOES_NOT_EXIST = 1, DO_NOT_KNOW = 2 }; // Returns status of the entry in index for the given key. It can be called // on any thread. // If the optional aCB callback is given, the it will be called with a // CacheIndexEntry only if _retval is EXISTS when the method returns. static nsresult HasEntry( const nsACString& aKey, EntryStatus* _retval, const std::function& aCB = nullptr); static nsresult HasEntry( const SHA1Sum::Hash& hash, EntryStatus* _retval, const std::function& aCB = nullptr); // Returns a hash of the least important entry that should be evicted if the // cache size is over limit and also returns a total number of all entries in // the index minus the number of forced valid entries and unpinned entries // that we encounter when searching (see below) static nsresult GetEntryForEviction(bool aIgnoreEmptyEntries, SHA1Sum::Hash* aHash, uint32_t* aCnt); // Checks if a cache entry is currently forced valid. Used to prevent an entry // (that has been forced valid) from being evicted when the cache size reaches // its limit. static bool IsForcedValidEntry(const SHA1Sum::Hash* aHash); // Returns cache size in kB. static nsresult GetCacheSize(uint32_t* _retval); // Returns number of entry files in the cache static nsresult GetEntryFileCount(uint32_t* _retval); // Synchronously returns the disk occupation and number of entries // per-context. Callable on any thread. It will ignore loadContextInfo and get // stats for all entries if the aInfo is a nullptr. static nsresult GetCacheStats(nsILoadContextInfo* aInfo, uint32_t* aSize, uint32_t* aCount); // Asynchronously gets the disk cache size, used for display in the UI. static nsresult AsyncGetDiskConsumption( nsICacheStorageConsumptionObserver* aObserver); // Returns an iterator that returns entries matching a given context that were // present in the index at the time this method was called. If aAddNew is true // then the iterator will also return entries created after this call. // NOTE: When some entry is removed from index it is removed also from the // iterator regardless what aAddNew was passed. static nsresult GetIterator(nsILoadContextInfo* aInfo, bool aAddNew, CacheIndexIterator** _retval); // Returns true if we _think_ that the index is up to date. I.e. the state is // READY or WRITING and mIndexNeedsUpdate as well as mShuttingDown is false. static nsresult IsUpToDate(bool* _retval); // Called from CacheStorageService::Clear() and // CacheFileContextEvictor::EvictEntries(), sets a flag that blocks // notification to AsyncGetDiskConsumption. static void OnAsyncEviction(bool aEvicting); // We keep track of total bytes written to the cache to be able to do // a telemetry report after writting certain amount of data to the cache. static void UpdateTotalBytesWritten(uint32_t aBytesWritten); // Memory reporting static size_t SizeOfExcludingThis(mozilla::MallocSizeOf mallocSizeOf); static size_t SizeOfIncludingThis(mozilla::MallocSizeOf mallocSizeOf); private: friend class CacheIndexEntryAutoManage; friend class FileOpenHelper; friend class CacheIndexIterator; friend class CacheIndexRecordWrapper; virtual ~CacheIndex(); NS_IMETHOD OnFileOpened(CacheFileHandle* aHandle, nsresult aResult) override; void OnFileOpenedInternal(FileOpenHelper* aOpener, CacheFileHandle* aHandle, nsresult aResult, const StaticMutexAutoLock& aProofOfLock); NS_IMETHOD OnDataWritten(CacheFileHandle* aHandle, const char* aBuf, nsresult aResult) override; NS_IMETHOD OnDataRead(CacheFileHandle* aHandle, char* aBuf, nsresult aResult) override; NS_IMETHOD OnFileDoomed(CacheFileHandle* aHandle, nsresult aResult) override; NS_IMETHOD OnEOFSet(CacheFileHandle* aHandle, nsresult aResult) override; NS_IMETHOD OnFileRenamed(CacheFileHandle* aHandle, nsresult aResult) override; nsresult InitInternal(nsIFile* aCacheDirectory, const StaticMutexAutoLock& aProofOfLock); void PreShutdownInternal(); // This method returns false when index is not initialized or is shut down. bool IsIndexUsable(); // This method checks whether the entry has the same values of // originAttributes and isAnonymous. We don't expect to find a collision // since these values are part of the key that we hash and we use a strong // hash function. static bool IsCollision(CacheIndexEntry* aEntry, OriginAttrsHash aOriginAttrsHash, bool aAnonymous); // Checks whether any of the information about the entry has changed. static bool HasEntryChanged(CacheIndexEntry* aEntry, const uint32_t* aFrecency, const bool* aHasAltData, const uint16_t* aOnStartTime, const uint16_t* aOnStopTime, const uint8_t* aContentType, const uint32_t* aSize); // Merge all pending operations from mPendingUpdates into mIndex. void ProcessPendingOperations(const StaticMutexAutoLock& aProofOfLock); // Following methods perform writing of the index file. // // The index is written periodically, but not earlier than once in // kMinDumpInterval and there must be at least kMinUnwrittenChanges // differences between index on disk and in memory. Index is always first // written to a temporary file and the old index file is replaced when the // writing process succeeds. // // Starts writing of index when both limits (minimal delay between writes and // minimum number of changes in index) were exceeded. bool WriteIndexToDiskIfNeeded(const StaticMutexAutoLock& aProofOfLock); // Starts writing of index file. void WriteIndexToDisk(const StaticMutexAutoLock& aProofOfLock); // Serializes part of mIndex hashtable to the write buffer a writes the buffer // to the file. void WriteRecords(const StaticMutexAutoLock& aProofOfLock); // Finalizes writing process. void FinishWrite(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock); // Following methods perform writing of the journal during shutdown. All these // methods must be called only during shutdown since they write/delete files // directly on the main thread instead of using CacheFileIOManager that does // it asynchronously on IO thread. Journal contains only entries that are // dirty, i.e. changes that are not present in the index file on the disk. // When the log is written successfully, the dirty flag in index file is // cleared. nsresult GetFile(const nsACString& aName, nsIFile** _retval); void RemoveFile(const nsACString& aName); 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(const StaticMutexAutoLock& aProofOfLock); // Starts reading data from index file. void StartReadingIndex(const StaticMutexAutoLock& aProofOfLock); // Parses data read from index file. void ParseRecords(const StaticMutexAutoLock& aProofOfLock); // Starts reading data from journal file. void StartReadingJournal(const StaticMutexAutoLock& aProofOfLock); // Parses data read from journal file. void ParseJournal(const StaticMutexAutoLock& aProofOfLock); // Merges entries from journal into mIndex. void MergeJournal(const StaticMutexAutoLock& aProofOfLock); // 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, const StaticMutexAutoLock& aProofOfLock); // Following methods perform updating and building of the index. // Timer callback that starts update or build process. static void DelayedUpdate(nsITimer* aTimer, void* aClosure); void DelayedUpdateLocked(const StaticMutexAutoLock& aProofOfLock); // Posts timer event that start update or build process. nsresult ScheduleUpdateTimer(uint32_t aDelay); nsresult SetupDirectoryEnumerator(); nsresult 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(const StaticMutexAutoLock& aProofOfLock); bool StartUpdatingIndexIfNeeded(const StaticMutexAutoLock& aProofOfLock, bool aSwitchingToReadyState = false); // Starts update or build process or fires a timer when it is too early after // startup. void StartUpdatingIndex(bool aRebuild, const StaticMutexAutoLock& aProofOfLock); // Iterates through all files in entries directory that we didn't create/open // during this session and theirs last modified time is newer than timestamp // in the index header. Parses the files and adds the entries to the index. void UpdateIndex(const StaticMutexAutoLock& aProofOfLock); // Finalizes update or build process. void FinishUpdate(bool aSucceeded, const StaticMutexAutoLock& aProofOfLock); void RemoveNonFreshEntries(const StaticMutexAutoLock& aProofOfLock); enum EState { // Initial state in which the index is not usable // Possible transitions: // -> READING INITIAL = 0, // Index is being read from the disk. // Possible transitions: // -> INITIAL - We failed to dispatch a read event. // -> BUILDING - No or corrupted index file was found. // -> UPDATING - No or corrupted journal file was found. // - Dirty flag was set in index header. // -> READY - Index was read successfully or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. READING = 1, // Index is being written to the disk. // Possible transitions: // -> READY - Writing of index finished or was interrupted by // pre-shutdown.. // -> UPDATING - Writing of index finished, but index was found outdated // during writing. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. WRITING = 2, // Index is being build. // Possible transitions: // -> READY - Building of index finished or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. BUILDING = 3, // Index is being updated. // Possible transitions: // -> READY - Updating of index finished or was interrupted by // pre-shutdown. // -> SHUTDOWN - This could happen only in case of pre-shutdown failure. UPDATING = 4, // Index is ready. // Possible transitions: // -> UPDATING - Index was found outdated. // -> SHUTDOWN - Index is shutting down. READY = 5, // Index is shutting down. SHUTDOWN = 6 }; static char const* StateString(EState aState); void ChangeState(EState aNewState, const StaticMutexAutoLock& aProofOfLock); void NotifyAsyncGetDiskConsumptionCallbacks(); // 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(CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock); void RemoveRecordFromIterators(CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock); void ReplaceRecordInIterators(CacheIndexRecordWrapper* aOldRecord, CacheIndexRecordWrapper* aNewRecord, const StaticMutexAutoLock& aProofOfLock); // Memory reporting (private part) size_t SizeOfExcludingThisInternal(mozilla::MallocSizeOf mallocSizeOf) const; // Reports telemetry about cache, i.e. size, entry count and content type // stats. void DoTelemetryReport(); static mozilla::StaticRefPtr gInstance; static StaticMutex sLock MOZ_UNANNOTATED; nsCOMPtr mCacheDirectory; EState mState{INITIAL}; // 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{false}; // When set to true, update process should start as soon as possible. This // flag is set whenever we find some inconsistency which would be fixed by // update process. The flag is checked always when switching to READY state. // To make sure we start the update process as soon as possible, methods that // set this flag should also call StartUpdatingIndexIfNeeded() to cover the // case when we are currently in READY state. bool mIndexNeedsUpdate{false}; // Set at the beginning of RemoveAll() which clears the whole index. When // removing all entries we must stop any pending reading, writing, updating or // building operation. This flag is checked at various places and it prevents // we won't start another operation (e.g. canceling reading of the index would // normally start update or build process) bool mRemovingAll{false}; // Whether the index file on disk exists and is valid. bool mIndexOnDiskIsValid{false}; // When something goes wrong during updating or building process, we don't // mark index clean (and also don't write journal) to ensure that update or // build will be initiated on the next start. bool mDontMarkIndexClean{false}; // Timestamp value from index file. It is used during update process to skip // entries that were last modified before this timestamp. uint32_t mIndexTimeStamp{0}; // Timestamp of last time the index was dumped to disk. // NOTE: The index might not be necessarily dumped at this time. The value // is used to schedule next dump of the index. TimeStamp mLastDumpTime; // Timer of delayed update/build. nsCOMPtr mUpdateTimer; // True when build or update event is posted bool mUpdateEventPending{false}; // Helper members used when reading/writing index from/to disk. // Contains number of entries that should be skipped: // - in hashtable when writing index because they were already written // - in index file when reading index because they were already read uint32_t mSkipEntries{0}; // Number of entries that should be written to disk. This is number of entries // in hashtable that are initialized and are not marked as removed when // writing begins. uint32_t mProcessEntries{0}; char* mRWBuf{nullptr}; uint32_t mRWBufSize{0}; uint32_t mRWBufPos{0}; RefPtr 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{false}; // Reading of journal succeeded if true. bool mJournalReadSuccessfully{false}; // Handle used for writing and reading index file. RefPtr mIndexHandle; // Handle used for reading journal file. RefPtr mJournalHandle; // Used to check the existence of the file during reading process. RefPtr mTmpHandle; RefPtr mIndexFileOpener; RefPtr mJournalFileOpener; RefPtr mTmpFileOpener; // Directory enumerator used when building and updating index. nsCOMPtr mDirEnumerator; // Main index hashtable. nsTHashtable 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 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 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>* aRecs) : mRecs(aRecs), mIdx(0) { while (!Done() && !(*mRecs)[mIdx]) { mIdx++; } } bool Done() const { return mIdx == mRecs->Length(); } CacheIndexRecordWrapper* Get() const { MOZ_ASSERT(!Done()); return (*mRecs)[mIdx]; } void Next() { MOZ_ASSERT(!Done()); ++mIdx; while (!Done() && !(*mRecs)[mIdx]) { mIdx++; } } private: nsTArray>* mRecs; uint32_t mIdx; }; public: Iterator Iter() { return Iterator(&mRecs); } FrecencyArray() = default; // Methods used by CacheIndexEntryAutoManage to keep the array up to date. void AppendRecord(CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock); void RemoveRecord(CacheIndexRecordWrapper* aRecord, const StaticMutexAutoLock& aProofOfLock); void ReplaceRecord(CacheIndexRecordWrapper* aOldRecord, CacheIndexRecordWrapper* aNewRecord, const StaticMutexAutoLock& aProofOfLock); void SortIfNeeded(const StaticMutexAutoLock& aProofOfLock); bool RecordExistedUnlocked(CacheIndexRecordWrapper* aRecord); size_t Length() const { return mRecs.Length() - mRemovedElements; } void Clear(const StaticMutexAutoLock& aProofOfLock) { mRecs.Clear(); } private: friend class CacheIndex; nsTArray> mRecs; uint32_t mUnsortedElements{0}; // Instead of removing elements from the array immediately, we null them out // and the iterator skips them when accessing the array. The null pointers // are placed at the end during sorting and we strip them out all at once. // This saves moving a lot of memory in nsTArray::RemoveElementsAt. uint32_t mRemovedElements{0}; }; FrecencyArray mFrecencyArray; nsTArray 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{false}; class DiskConsumptionObserver : public Runnable { public: static DiskConsumptionObserver* Init( nsICacheStorageConsumptionObserver* aObserver) { nsWeakPtr observer = do_GetWeakReference(aObserver); if (!observer) return nullptr; return new DiskConsumptionObserver(observer); } void OnDiskConsumption(int64_t aSize) { mSize = aSize; NS_DispatchToMainThread(this); } private: explicit DiskConsumptionObserver(nsWeakPtr const& aWeakObserver) : Runnable("net::CacheIndex::DiskConsumptionObserver"), mObserver(aWeakObserver), mSize(0) {} virtual ~DiskConsumptionObserver() { if (mObserver && !NS_IsMainThread()) { NS_ReleaseOnMainThread("DiskConsumptionObserver::mObserver", mObserver.forget()); } } NS_IMETHOD Run() override { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr observer = do_QueryReferent(mObserver); mObserver = nullptr; if (observer) { observer->OnNetworkCacheDiskConsumption(mSize); } return NS_OK; } nsWeakPtr mObserver; int64_t mSize; }; // List of async observers that want to get disk consumption information nsTArray> mDiskConsumptionObservers; // Number of bytes written to the cache since the last telemetry report uint64_t mTotalBytesWritten{0}; }; } // namespace net } // namespace mozilla #endif