michael@0: /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* vim:set ts=4 sw=4 sts=4 cin et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsCache.h" michael@0: #include "nsDiskCacheMap.h" michael@0: #include "nsDiskCacheBinding.h" michael@0: #include "nsDiskCacheEntry.h" michael@0: #include "nsDiskCacheDevice.h" michael@0: #include "nsCacheService.h" michael@0: michael@0: #include michael@0: #include "nsPrintfCString.h" michael@0: michael@0: #include "nsISerializable.h" michael@0: #include "nsSerializationHelper.h" michael@0: michael@0: #include "mozilla/MemoryReporting.h" michael@0: #include "mozilla/Telemetry.h" michael@0: #include "mozilla/VisualEventTracer.h" michael@0: #include michael@0: michael@0: using namespace mozilla; michael@0: michael@0: /****************************************************************************** michael@0: * nsDiskCacheMap michael@0: *****************************************************************************/ michael@0: michael@0: /** michael@0: * File operations michael@0: */ michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::Open(nsIFile * cacheDirectory, michael@0: nsDiskCache::CorruptCacheInfo * corruptInfo, michael@0: bool reportCacheCleanTelemetryData) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(corruptInfo); michael@0: michael@0: // Assume we have an unexpected error until we find otherwise. michael@0: *corruptInfo = nsDiskCache::kUnexpectedError; michael@0: NS_ENSURE_ARG_POINTER(cacheDirectory); michael@0: if (mMapFD) return NS_ERROR_ALREADY_INITIALIZED; michael@0: michael@0: mCacheDirectory = cacheDirectory; // save a reference for ourselves michael@0: michael@0: // create nsIFile for _CACHE_MAP_ michael@0: nsresult rv; michael@0: nsCOMPtr file; michael@0: rv = cacheDirectory->Clone(getter_AddRefs(file)); michael@0: rv = file->AppendNative(NS_LITERAL_CSTRING("_CACHE_MAP_")); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // open the file - restricted to user, the data could be confidential michael@0: rv = file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 00600, &mMapFD); michael@0: if (NS_FAILED(rv)) { michael@0: *corruptInfo = nsDiskCache::kOpenCacheMapError; michael@0: NS_WARNING("Could not open cache map file"); michael@0: return NS_ERROR_FILE_CORRUPTED; michael@0: } michael@0: michael@0: bool cacheFilesExist = CacheFilesExist(); michael@0: rv = NS_ERROR_FILE_CORRUPTED; // presume the worst michael@0: uint32_t mapSize = PR_Available(mMapFD); michael@0: michael@0: if (NS_FAILED(InitCacheClean(cacheDirectory, michael@0: corruptInfo, michael@0: reportCacheCleanTelemetryData))) { michael@0: // corruptInfo is set in the call to InitCacheClean michael@0: goto error_exit; michael@0: } michael@0: michael@0: // check size of map file michael@0: if (mapSize == 0) { // creating a new _CACHE_MAP_ michael@0: michael@0: // block files shouldn't exist if we're creating the _CACHE_MAP_ michael@0: if (cacheFilesExist) { michael@0: *corruptInfo = nsDiskCache::kBlockFilesShouldNotExist; michael@0: goto error_exit; michael@0: } michael@0: michael@0: if (NS_FAILED(CreateCacheSubDirectories())) { michael@0: *corruptInfo = nsDiskCache::kCreateCacheSubdirectories; michael@0: goto error_exit; michael@0: } michael@0: michael@0: // create the file - initialize in memory michael@0: memset(&mHeader, 0, sizeof(nsDiskCacheHeader)); michael@0: mHeader.mVersion = nsDiskCache::kCurrentVersion; michael@0: mHeader.mRecordCount = kMinRecordCount; michael@0: mRecordArray = (nsDiskCacheRecord *) michael@0: PR_CALLOC(mHeader.mRecordCount * sizeof(nsDiskCacheRecord)); michael@0: if (!mRecordArray) { michael@0: *corruptInfo = nsDiskCache::kOutOfMemory; michael@0: rv = NS_ERROR_OUT_OF_MEMORY; michael@0: goto error_exit; michael@0: } michael@0: } else if (mapSize >= sizeof(nsDiskCacheHeader)) { // read existing _CACHE_MAP_ michael@0: michael@0: // if _CACHE_MAP_ exists, so should the block files michael@0: if (!cacheFilesExist) { michael@0: *corruptInfo = nsDiskCache::kBlockFilesShouldExist; michael@0: goto error_exit; michael@0: } michael@0: michael@0: CACHE_LOG_DEBUG(("CACHE: nsDiskCacheMap::Open [this=%p] reading map", this)); michael@0: michael@0: // read the header michael@0: uint32_t bytesRead = PR_Read(mMapFD, &mHeader, sizeof(nsDiskCacheHeader)); michael@0: if (sizeof(nsDiskCacheHeader) != bytesRead) { michael@0: *corruptInfo = nsDiskCache::kHeaderSizeNotRead; michael@0: goto error_exit; michael@0: } michael@0: mHeader.Unswap(); michael@0: michael@0: if (mHeader.mIsDirty) { michael@0: *corruptInfo = nsDiskCache::kHeaderIsDirty; michael@0: goto error_exit; michael@0: } michael@0: michael@0: if (mHeader.mVersion != nsDiskCache::kCurrentVersion) { michael@0: *corruptInfo = nsDiskCache::kVersionMismatch; michael@0: goto error_exit; michael@0: } michael@0: michael@0: uint32_t recordArraySize = michael@0: mHeader.mRecordCount * sizeof(nsDiskCacheRecord); michael@0: if (mapSize < recordArraySize + sizeof(nsDiskCacheHeader)) { michael@0: *corruptInfo = nsDiskCache::kRecordsIncomplete; michael@0: goto error_exit; michael@0: } michael@0: michael@0: // Get the space for the records michael@0: mRecordArray = (nsDiskCacheRecord *) PR_MALLOC(recordArraySize); michael@0: if (!mRecordArray) { michael@0: *corruptInfo = nsDiskCache::kOutOfMemory; michael@0: rv = NS_ERROR_OUT_OF_MEMORY; michael@0: goto error_exit; michael@0: } michael@0: michael@0: // Read the records michael@0: bytesRead = PR_Read(mMapFD, mRecordArray, recordArraySize); michael@0: if (bytesRead < recordArraySize) { michael@0: *corruptInfo = nsDiskCache::kNotEnoughToRead; michael@0: goto error_exit; michael@0: } michael@0: michael@0: // Unswap each record michael@0: int32_t total = 0; michael@0: for (int32_t i = 0; i < mHeader.mRecordCount; ++i) { michael@0: if (mRecordArray[i].HashNumber()) { michael@0: #if defined(IS_LITTLE_ENDIAN) michael@0: mRecordArray[i].Unswap(); michael@0: #endif michael@0: total ++; michael@0: } michael@0: } michael@0: michael@0: // verify entry count michael@0: if (total != mHeader.mEntryCount) { michael@0: *corruptInfo = nsDiskCache::kEntryCountIncorrect; michael@0: goto error_exit; michael@0: } michael@0: michael@0: } else { michael@0: *corruptInfo = nsDiskCache::kHeaderIncomplete; michael@0: goto error_exit; michael@0: } michael@0: michael@0: rv = OpenBlockFiles(corruptInfo); michael@0: if (NS_FAILED(rv)) { michael@0: // corruptInfo is set in the call to OpenBlockFiles michael@0: goto error_exit; michael@0: } michael@0: michael@0: // set dirty bit and flush header michael@0: mHeader.mIsDirty = true; michael@0: rv = FlushHeader(); michael@0: if (NS_FAILED(rv)) { michael@0: *corruptInfo = nsDiskCache::kFlushHeaderError; michael@0: goto error_exit; michael@0: } michael@0: michael@0: Telemetry::Accumulate(Telemetry::HTTP_DISK_CACHE_OVERHEAD, michael@0: (uint32_t)SizeOfExcludingThis(moz_malloc_size_of)); michael@0: michael@0: *corruptInfo = nsDiskCache::kNotCorrupt; michael@0: return NS_OK; michael@0: michael@0: error_exit: michael@0: (void) Close(false); michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::Close(bool flush) michael@0: { michael@0: nsCacheService::AssertOwnsLock(); michael@0: nsresult rv = NS_OK; michael@0: michael@0: // Cancel any pending cache validation event, the FlushRecords call below michael@0: // will validate the cache. michael@0: if (mCleanCacheTimer) { michael@0: mCleanCacheTimer->Cancel(); michael@0: } michael@0: michael@0: // If cache map file and its block files are still open, close them michael@0: if (mMapFD) { michael@0: // close block files michael@0: rv = CloseBlockFiles(flush); michael@0: if (NS_SUCCEEDED(rv) && flush && mRecordArray) { michael@0: // write the map records michael@0: rv = FlushRecords(false); // don't bother swapping buckets back michael@0: if (NS_SUCCEEDED(rv)) { michael@0: // clear dirty bit michael@0: mHeader.mIsDirty = false; michael@0: rv = FlushHeader(); michael@0: } michael@0: } michael@0: if ((PR_Close(mMapFD) != PR_SUCCESS) && (NS_SUCCEEDED(rv))) michael@0: rv = NS_ERROR_UNEXPECTED; michael@0: michael@0: mMapFD = nullptr; michael@0: } michael@0: michael@0: if (mCleanFD) { michael@0: PR_Close(mCleanFD); michael@0: mCleanFD = nullptr; michael@0: } michael@0: michael@0: PR_FREEIF(mRecordArray); michael@0: PR_FREEIF(mBuffer); michael@0: mBufferSize = 0; michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::Trim() michael@0: { michael@0: nsresult rv, rv2 = NS_OK; michael@0: for (int i=0; i < kNumBlockFiles; ++i) { michael@0: rv = mBlockFile[i].Trim(); michael@0: if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one michael@0: } michael@0: // Try to shrink the records array michael@0: rv = ShrinkRecords(); michael@0: if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one michael@0: return rv2; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::FlushHeader() michael@0: { michael@0: if (!mMapFD) return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: // seek to beginning of cache map michael@0: int32_t filePos = PR_Seek(mMapFD, 0, PR_SEEK_SET); michael@0: if (filePos != 0) return NS_ERROR_UNEXPECTED; michael@0: michael@0: // write the header michael@0: mHeader.Swap(); michael@0: int32_t bytesWritten = PR_Write(mMapFD, &mHeader, sizeof(nsDiskCacheHeader)); michael@0: mHeader.Unswap(); michael@0: if (sizeof(nsDiskCacheHeader) != bytesWritten) { michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: PRStatus err = PR_Sync(mMapFD); michael@0: if (err != PR_SUCCESS) return NS_ERROR_UNEXPECTED; michael@0: michael@0: // If we have a clean header then revalidate the cache clean file michael@0: if (!mHeader.mIsDirty) { michael@0: RevalidateCache(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::FlushRecords(bool unswap) michael@0: { michael@0: if (!mMapFD) return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: // seek to beginning of buckets michael@0: int32_t filePos = PR_Seek(mMapFD, sizeof(nsDiskCacheHeader), PR_SEEK_SET); michael@0: if (filePos != sizeof(nsDiskCacheHeader)) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: #if defined(IS_LITTLE_ENDIAN) michael@0: // Swap each record michael@0: for (int32_t i = 0; i < mHeader.mRecordCount; ++i) { michael@0: if (mRecordArray[i].HashNumber()) michael@0: mRecordArray[i].Swap(); michael@0: } michael@0: #endif michael@0: michael@0: int32_t recordArraySize = sizeof(nsDiskCacheRecord) * mHeader.mRecordCount; michael@0: michael@0: int32_t bytesWritten = PR_Write(mMapFD, mRecordArray, recordArraySize); michael@0: if (bytesWritten != recordArraySize) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: #if defined(IS_LITTLE_ENDIAN) michael@0: if (unswap) { michael@0: // Unswap each record michael@0: for (int32_t i = 0; i < mHeader.mRecordCount; ++i) { michael@0: if (mRecordArray[i].HashNumber()) michael@0: mRecordArray[i].Unswap(); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Record operations michael@0: */ michael@0: michael@0: uint32_t michael@0: nsDiskCacheMap::GetBucketRank(uint32_t bucketIndex, uint32_t targetRank) michael@0: { michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: uint32_t rank = 0; michael@0: michael@0: for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) { michael@0: if ((rank < records[i].EvictionRank()) && michael@0: ((targetRank == 0) || (records[i].EvictionRank() < targetRank))) michael@0: rank = records[i].EvictionRank(); michael@0: } michael@0: return rank; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::GrowRecords() michael@0: { michael@0: if (mHeader.mRecordCount >= mMaxRecordCount) michael@0: return NS_OK; michael@0: CACHE_LOG_DEBUG(("CACHE: GrowRecords\n")); michael@0: michael@0: // Resize the record array michael@0: int32_t newCount = mHeader.mRecordCount << 1; michael@0: if (newCount > mMaxRecordCount) michael@0: newCount = mMaxRecordCount; michael@0: nsDiskCacheRecord *newArray = (nsDiskCacheRecord *) michael@0: PR_REALLOC(mRecordArray, newCount * sizeof(nsDiskCacheRecord)); michael@0: if (!newArray) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: // Space out the buckets michael@0: uint32_t oldRecordsPerBucket = GetRecordsPerBucket(); michael@0: uint32_t newRecordsPerBucket = newCount / kBuckets; michael@0: // Work from back to space out each bucket to the new array michael@0: for (int bucketIndex = kBuckets - 1; bucketIndex >= 0; --bucketIndex) { michael@0: // Move bucket michael@0: nsDiskCacheRecord *newRecords = newArray + bucketIndex * newRecordsPerBucket; michael@0: const uint32_t count = mHeader.mBucketUsage[bucketIndex]; michael@0: memmove(newRecords, michael@0: newArray + bucketIndex * oldRecordsPerBucket, michael@0: count * sizeof(nsDiskCacheRecord)); michael@0: // clear unused records michael@0: memset(newRecords + count, 0, michael@0: (newRecordsPerBucket - count) * sizeof(nsDiskCacheRecord)); michael@0: } michael@0: michael@0: // Set as the new record array michael@0: mRecordArray = newArray; michael@0: mHeader.mRecordCount = newCount; michael@0: michael@0: InvalidateCache(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::ShrinkRecords() michael@0: { michael@0: if (mHeader.mRecordCount <= kMinRecordCount) michael@0: return NS_OK; michael@0: CACHE_LOG_DEBUG(("CACHE: ShrinkRecords\n")); michael@0: michael@0: // Verify if we can shrink the record array: all buckets must be less than michael@0: // 1/2 filled michael@0: uint32_t maxUsage = 0, bucketIndex; michael@0: for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) { michael@0: if (maxUsage < mHeader.mBucketUsage[bucketIndex]) michael@0: maxUsage = mHeader.mBucketUsage[bucketIndex]; michael@0: } michael@0: // Determine new bucket size, halve size until maxUsage michael@0: uint32_t oldRecordsPerBucket = GetRecordsPerBucket(); michael@0: uint32_t newRecordsPerBucket = oldRecordsPerBucket; michael@0: while (maxUsage < (newRecordsPerBucket >> 1)) michael@0: newRecordsPerBucket >>= 1; michael@0: if (newRecordsPerBucket < (kMinRecordCount / kBuckets)) michael@0: newRecordsPerBucket = (kMinRecordCount / kBuckets); michael@0: NS_ASSERTION(newRecordsPerBucket <= oldRecordsPerBucket, michael@0: "ShrinkRecords() can't grow records!"); michael@0: if (newRecordsPerBucket == oldRecordsPerBucket) michael@0: return NS_OK; michael@0: // Move the buckets close to each other michael@0: for (bucketIndex = 1; bucketIndex < kBuckets; ++bucketIndex) { michael@0: // Move bucket michael@0: memmove(mRecordArray + bucketIndex * newRecordsPerBucket, michael@0: mRecordArray + bucketIndex * oldRecordsPerBucket, michael@0: newRecordsPerBucket * sizeof(nsDiskCacheRecord)); michael@0: } michael@0: michael@0: // Shrink the record array memory block itself michael@0: uint32_t newCount = newRecordsPerBucket * kBuckets; michael@0: nsDiskCacheRecord* newArray = (nsDiskCacheRecord *) michael@0: PR_REALLOC(mRecordArray, newCount * sizeof(nsDiskCacheRecord)); michael@0: if (!newArray) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: // Set as the new record array michael@0: mRecordArray = newArray; michael@0: mHeader.mRecordCount = newCount; michael@0: michael@0: InvalidateCache(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::AddRecord( nsDiskCacheRecord * mapRecord, michael@0: nsDiskCacheRecord * oldRecord) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: AddRecord [%x]\n", mapRecord->HashNumber())); michael@0: michael@0: const uint32_t hashNumber = mapRecord->HashNumber(); michael@0: const uint32_t bucketIndex = GetBucketIndex(hashNumber); michael@0: const uint32_t count = mHeader.mBucketUsage[bucketIndex]; michael@0: michael@0: oldRecord->SetHashNumber(0); // signify no record michael@0: michael@0: if (count == GetRecordsPerBucket()) { michael@0: // Ignore failure to grow the record space, we will then reuse old records michael@0: GrowRecords(); michael@0: } michael@0: michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: if (count < GetRecordsPerBucket()) { michael@0: // stick the new record at the end michael@0: records[count] = *mapRecord; michael@0: mHeader.mEntryCount++; michael@0: mHeader.mBucketUsage[bucketIndex]++; michael@0: if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank()) michael@0: mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank(); michael@0: InvalidateCache(); michael@0: } else { michael@0: // Find the record with the highest eviction rank michael@0: nsDiskCacheRecord * mostEvictable = &records[0]; michael@0: for (int i = count-1; i > 0; i--) { michael@0: if (records[i].EvictionRank() > mostEvictable->EvictionRank()) michael@0: mostEvictable = &records[i]; michael@0: } michael@0: *oldRecord = *mostEvictable; // i == GetRecordsPerBucket(), so michael@0: // evict the mostEvictable michael@0: *mostEvictable = *mapRecord; // replace it with the new record michael@0: // check if we need to update mostEvictable entry in header michael@0: if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank()) michael@0: mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank(); michael@0: if (oldRecord->EvictionRank() >= mHeader.mEvictionRank[bucketIndex]) michael@0: mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0); michael@0: InvalidateCache(); michael@0: } michael@0: michael@0: NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0), michael@0: "eviction rank out of sync"); michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::UpdateRecord( nsDiskCacheRecord * mapRecord) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: UpdateRecord [%x]\n", mapRecord->HashNumber())); michael@0: michael@0: const uint32_t hashNumber = mapRecord->HashNumber(); michael@0: const uint32_t bucketIndex = GetBucketIndex(hashNumber); michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: michael@0: for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) { michael@0: if (records[i].HashNumber() == hashNumber) { michael@0: const uint32_t oldRank = records[i].EvictionRank(); michael@0: michael@0: // stick the new record here michael@0: records[i] = *mapRecord; michael@0: michael@0: // update eviction rank in header if necessary michael@0: if (mHeader.mEvictionRank[bucketIndex] < mapRecord->EvictionRank()) michael@0: mHeader.mEvictionRank[bucketIndex] = mapRecord->EvictionRank(); michael@0: else if (mHeader.mEvictionRank[bucketIndex] == oldRank) michael@0: mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0); michael@0: michael@0: InvalidateCache(); michael@0: michael@0: NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == GetBucketRank(bucketIndex, 0), michael@0: "eviction rank out of sync"); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: NS_NOTREACHED("record not found"); michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::FindRecord( uint32_t hashNumber, nsDiskCacheRecord * result) michael@0: { michael@0: const uint32_t bucketIndex = GetBucketIndex(hashNumber); michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: michael@0: for (int i = mHeader.mBucketUsage[bucketIndex]-1; i >= 0; i--) { michael@0: if (records[i].HashNumber() == hashNumber) { michael@0: *result = records[i]; // copy the record michael@0: NS_ASSERTION(result->ValidRecord(), "bad cache map record"); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: return NS_ERROR_CACHE_KEY_NOT_FOUND; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::DeleteRecord( nsDiskCacheRecord * mapRecord) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: DeleteRecord [%x]\n", mapRecord->HashNumber())); michael@0: michael@0: const uint32_t hashNumber = mapRecord->HashNumber(); michael@0: const uint32_t bucketIndex = GetBucketIndex(hashNumber); michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: uint32_t last = mHeader.mBucketUsage[bucketIndex]-1; michael@0: michael@0: for (int i = last; i >= 0; i--) { michael@0: if (records[i].HashNumber() == hashNumber) { michael@0: // found it, now delete it. michael@0: uint32_t evictionRank = records[i].EvictionRank(); michael@0: NS_ASSERTION(evictionRank == mapRecord->EvictionRank(), michael@0: "evictionRank out of sync"); michael@0: // if not the last record, shift last record into opening michael@0: records[i] = records[last]; michael@0: records[last].SetHashNumber(0); // clear last record michael@0: mHeader.mBucketUsage[bucketIndex] = last; michael@0: mHeader.mEntryCount--; michael@0: michael@0: // update eviction rank michael@0: uint32_t bucketIndex = GetBucketIndex(mapRecord->HashNumber()); michael@0: if (mHeader.mEvictionRank[bucketIndex] <= evictionRank) { michael@0: mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0); michael@0: } michael@0: michael@0: InvalidateCache(); michael@0: michael@0: NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == michael@0: GetBucketRank(bucketIndex, 0), "eviction rank out of sync"); michael@0: return NS_OK; michael@0: } michael@0: } michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: michael@0: int32_t michael@0: nsDiskCacheMap::VisitEachRecord(uint32_t bucketIndex, michael@0: nsDiskCacheRecordVisitor * visitor, michael@0: uint32_t evictionRank) michael@0: { michael@0: int32_t rv = kVisitNextRecord; michael@0: uint32_t count = mHeader.mBucketUsage[bucketIndex]; michael@0: nsDiskCacheRecord * records = GetFirstRecordInBucket(bucketIndex); michael@0: michael@0: // call visitor for each entry (matching any eviction rank) michael@0: for (int i = count-1; i >= 0; i--) { michael@0: if (evictionRank > records[i].EvictionRank()) continue; michael@0: michael@0: rv = visitor->VisitRecord(&records[i]); michael@0: if (rv == kStopVisitingRecords) michael@0: break; // Stop visiting records michael@0: michael@0: if (rv == kDeleteRecordAndContinue) { michael@0: --count; michael@0: records[i] = records[count]; michael@0: records[count].SetHashNumber(0); michael@0: InvalidateCache(); michael@0: } michael@0: } michael@0: michael@0: if (mHeader.mBucketUsage[bucketIndex] - count != 0) { michael@0: mHeader.mEntryCount -= mHeader.mBucketUsage[bucketIndex] - count; michael@0: mHeader.mBucketUsage[bucketIndex] = count; michael@0: // recalc eviction rank michael@0: mHeader.mEvictionRank[bucketIndex] = GetBucketRank(bucketIndex, 0); michael@0: } michael@0: NS_ASSERTION(mHeader.mEvictionRank[bucketIndex] == michael@0: GetBucketRank(bucketIndex, 0), "eviction rank out of sync"); michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * VisitRecords michael@0: * michael@0: * Visit every record in cache map in the most convenient order michael@0: */ michael@0: nsresult michael@0: nsDiskCacheMap::VisitRecords( nsDiskCacheRecordVisitor * visitor) michael@0: { michael@0: for (int bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) { michael@0: if (VisitEachRecord(bucketIndex, visitor, 0) == kStopVisitingRecords) michael@0: break; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * EvictRecords michael@0: * michael@0: * Just like VisitRecords, but visits the records in order of their eviction rank michael@0: */ michael@0: nsresult michael@0: nsDiskCacheMap::EvictRecords( nsDiskCacheRecordVisitor * visitor) michael@0: { michael@0: uint32_t tempRank[kBuckets]; michael@0: int bucketIndex = 0; michael@0: michael@0: // copy eviction rank array michael@0: for (bucketIndex = 0; bucketIndex < kBuckets; ++bucketIndex) michael@0: tempRank[bucketIndex] = mHeader.mEvictionRank[bucketIndex]; michael@0: michael@0: // Maximum number of iterations determined by number of records michael@0: // as a safety limiter for the loop. Use a copy of mHeader.mEntryCount since michael@0: // the value could decrease if some entry is evicted. michael@0: int32_t entryCount = mHeader.mEntryCount; michael@0: for (int n = 0; n < entryCount; ++n) { michael@0: michael@0: // find bucket with highest eviction rank michael@0: uint32_t rank = 0; michael@0: for (int i = 0; i < kBuckets; ++i) { michael@0: if (rank < tempRank[i]) { michael@0: rank = tempRank[i]; michael@0: bucketIndex = i; michael@0: } michael@0: } michael@0: michael@0: if (rank == 0) break; // we've examined all the records michael@0: michael@0: // visit records in bucket with eviction ranks >= target eviction rank michael@0: if (VisitEachRecord(bucketIndex, visitor, rank) == kStopVisitingRecords) michael@0: break; michael@0: michael@0: // find greatest rank less than 'rank' michael@0: tempRank[bucketIndex] = GetBucketRank(bucketIndex, rank); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::OpenBlockFiles(nsDiskCache::CorruptCacheInfo * corruptInfo) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(corruptInfo); michael@0: michael@0: // create nsIFile for block file michael@0: nsCOMPtr blockFile; michael@0: nsresult rv = NS_OK; michael@0: *corruptInfo = nsDiskCache::kUnexpectedError; michael@0: michael@0: for (int i = 0; i < kNumBlockFiles; ++i) { michael@0: rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile)); michael@0: if (NS_FAILED(rv)) { michael@0: *corruptInfo = nsDiskCache::kCouldNotGetBlockFileForIndex; michael@0: break; michael@0: } michael@0: michael@0: uint32_t blockSize = GetBlockSizeForIndex(i+1); // +1 to match file selectors 1,2,3 michael@0: uint32_t bitMapSize = GetBitMapSizeForIndex(i+1); michael@0: rv = mBlockFile[i].Open(blockFile, blockSize, bitMapSize, corruptInfo); michael@0: if (NS_FAILED(rv)) { michael@0: // corruptInfo was set inside the call to mBlockFile[i].Open michael@0: break; michael@0: } michael@0: } michael@0: // close all files in case of any error michael@0: if (NS_FAILED(rv)) michael@0: (void)CloseBlockFiles(false); // we already have an error to report michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::CloseBlockFiles(bool flush) michael@0: { michael@0: nsresult rv, rv2 = NS_OK; michael@0: for (int i=0; i < kNumBlockFiles; ++i) { michael@0: rv = mBlockFile[i].Close(flush); michael@0: if (NS_FAILED(rv)) rv2 = rv; // if one or more errors, report at least one michael@0: } michael@0: return rv2; michael@0: } michael@0: michael@0: michael@0: bool michael@0: nsDiskCacheMap::CacheFilesExist() michael@0: { michael@0: nsCOMPtr blockFile; michael@0: nsresult rv; michael@0: michael@0: for (int i = 0; i < kNumBlockFiles; ++i) { michael@0: bool exists; michael@0: rv = GetBlockFileForIndex(i, getter_AddRefs(blockFile)); michael@0: if (NS_FAILED(rv)) return false; michael@0: michael@0: rv = blockFile->Exists(&exists); michael@0: if (NS_FAILED(rv) || !exists) return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::CreateCacheSubDirectories() michael@0: { michael@0: if (!mCacheDirectory) michael@0: return NS_ERROR_UNEXPECTED; michael@0: michael@0: for (int32_t index = 0 ; index < 16 ; index++) { michael@0: nsCOMPtr file; michael@0: nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = file->AppendNative(nsPrintfCString("%X", index)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: michael@0: rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: nsDiskCacheEntry * michael@0: nsDiskCacheMap::ReadDiskCacheEntry(nsDiskCacheRecord * record) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: ReadDiskCacheEntry [%x]\n", record->HashNumber())); michael@0: michael@0: nsresult rv = NS_ERROR_UNEXPECTED; michael@0: nsDiskCacheEntry * diskEntry = nullptr; michael@0: uint32_t metaFile = record->MetaFile(); michael@0: int32_t bytesRead = 0; michael@0: michael@0: if (!record->MetaLocationInitialized()) return nullptr; michael@0: michael@0: if (metaFile == 0) { // entry/metadata stored in separate file michael@0: // open and read the file michael@0: nsCOMPtr file; michael@0: rv = GetLocalFileForDiskCacheRecord(record, michael@0: nsDiskCache::kMetaData, michael@0: false, michael@0: getter_AddRefs(file)); michael@0: NS_ENSURE_SUCCESS(rv, nullptr); michael@0: michael@0: CACHE_LOG_DEBUG(("CACHE: nsDiskCacheMap::ReadDiskCacheEntry" michael@0: "[this=%p] reading disk cache entry", this)); michael@0: michael@0: PRFileDesc * fd = nullptr; michael@0: michael@0: // open the file - restricted to user, the data could be confidential michael@0: rv = file->OpenNSPRFileDesc(PR_RDONLY, 00600, &fd); michael@0: NS_ENSURE_SUCCESS(rv, nullptr); michael@0: michael@0: int32_t fileSize = PR_Available(fd); michael@0: if (fileSize < 0) { michael@0: // an error occurred. We could call PR_GetError(), but how would that help? michael@0: rv = NS_ERROR_UNEXPECTED; michael@0: } else { michael@0: rv = EnsureBuffer(fileSize); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: bytesRead = PR_Read(fd, mBuffer, fileSize); michael@0: if (bytesRead < fileSize) { michael@0: rv = NS_ERROR_UNEXPECTED; michael@0: } michael@0: } michael@0: } michael@0: PR_Close(fd); michael@0: NS_ENSURE_SUCCESS(rv, nullptr); michael@0: michael@0: } else if (metaFile < (kNumBlockFiles + 1)) { michael@0: // entry/metadata stored in cache block file michael@0: michael@0: // allocate buffer michael@0: uint32_t blockCount = record->MetaBlockCount(); michael@0: bytesRead = blockCount * GetBlockSizeForIndex(metaFile); michael@0: michael@0: rv = EnsureBuffer(bytesRead); michael@0: NS_ENSURE_SUCCESS(rv, nullptr); michael@0: michael@0: // read diskEntry, note when the blocks are at the end of file, michael@0: // bytesRead may be less than blockSize*blockCount. michael@0: // But the bytesRead should at least agree with the real disk entry size. michael@0: rv = mBlockFile[metaFile - 1].ReadBlocks(mBuffer, michael@0: record->MetaStartBlock(), michael@0: blockCount, michael@0: &bytesRead); michael@0: NS_ENSURE_SUCCESS(rv, nullptr); michael@0: } michael@0: diskEntry = (nsDiskCacheEntry *)mBuffer; michael@0: diskEntry->Unswap(); // disk to memory michael@0: // Check if calculated size agrees with bytesRead michael@0: if (bytesRead < 0 || (uint32_t)bytesRead < diskEntry->Size()) michael@0: return nullptr; michael@0: michael@0: // Return the buffer containing the diskEntry structure michael@0: return diskEntry; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * CreateDiskCacheEntry(nsCacheEntry * entry) michael@0: * michael@0: * Prepare an nsCacheEntry for writing to disk michael@0: */ michael@0: nsDiskCacheEntry * michael@0: nsDiskCacheMap::CreateDiskCacheEntry(nsDiskCacheBinding * binding, michael@0: uint32_t * aSize) michael@0: { michael@0: nsCacheEntry * entry = binding->mCacheEntry; michael@0: if (!entry) return nullptr; michael@0: michael@0: // Store security info, if it is serializable michael@0: nsCOMPtr infoObj = entry->SecurityInfo(); michael@0: nsCOMPtr serializable = do_QueryInterface(infoObj); michael@0: if (infoObj && !serializable) return nullptr; michael@0: if (serializable) { michael@0: nsCString info; michael@0: nsresult rv = NS_SerializeToString(serializable, info); michael@0: if (NS_FAILED(rv)) return nullptr; michael@0: rv = entry->SetMetaDataElement("security-info", info.get()); michael@0: if (NS_FAILED(rv)) return nullptr; michael@0: } michael@0: michael@0: uint32_t keySize = entry->Key()->Length() + 1; michael@0: uint32_t metaSize = entry->MetaDataSize(); michael@0: uint32_t size = sizeof(nsDiskCacheEntry) + keySize + metaSize; michael@0: michael@0: if (aSize) *aSize = size; michael@0: michael@0: nsresult rv = EnsureBuffer(size); michael@0: if (NS_FAILED(rv)) return nullptr; michael@0: michael@0: nsDiskCacheEntry *diskEntry = (nsDiskCacheEntry *)mBuffer; michael@0: diskEntry->mHeaderVersion = nsDiskCache::kCurrentVersion; michael@0: diskEntry->mMetaLocation = binding->mRecord.MetaLocation(); michael@0: diskEntry->mFetchCount = entry->FetchCount(); michael@0: diskEntry->mLastFetched = entry->LastFetched(); michael@0: diskEntry->mLastModified = entry->LastModified(); michael@0: diskEntry->mExpirationTime = entry->ExpirationTime(); michael@0: diskEntry->mDataSize = entry->DataSize(); michael@0: diskEntry->mKeySize = keySize; michael@0: diskEntry->mMetaDataSize = metaSize; michael@0: michael@0: memcpy(diskEntry->Key(), entry->Key()->get(), keySize); michael@0: michael@0: rv = entry->FlattenMetaData(diskEntry->MetaData(), metaSize); michael@0: if (NS_FAILED(rv)) return nullptr; michael@0: michael@0: return diskEntry; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::WriteDiskCacheEntry(nsDiskCacheBinding * binding) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: WriteDiskCacheEntry [%x]\n", michael@0: binding->mRecord.HashNumber())); michael@0: michael@0: mozilla::eventtracer::AutoEventTracer writeDiskCacheEntry( michael@0: binding->mCacheEntry, michael@0: mozilla::eventtracer::eExec, michael@0: mozilla::eventtracer::eDone, michael@0: "net::cache::WriteDiskCacheEntry"); michael@0: michael@0: nsresult rv = NS_OK; michael@0: uint32_t size; michael@0: nsDiskCacheEntry * diskEntry = CreateDiskCacheEntry(binding, &size); michael@0: if (!diskEntry) return NS_ERROR_UNEXPECTED; michael@0: michael@0: uint32_t fileIndex = CalculateFileIndex(size); michael@0: michael@0: // Deallocate old storage if necessary michael@0: if (binding->mRecord.MetaLocationInitialized()) { michael@0: // we have existing storage michael@0: michael@0: if ((binding->mRecord.MetaFile() == 0) && michael@0: (fileIndex == 0)) { // keeping the separate file michael@0: // just decrement total michael@0: DecrementTotalSize(binding->mRecord.MetaFileSize()); michael@0: NS_ASSERTION(binding->mRecord.MetaFileGeneration() == binding->mGeneration, michael@0: "generations out of sync"); michael@0: } else { michael@0: rv = DeleteStorage(&binding->mRecord, nsDiskCache::kMetaData); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: } michael@0: michael@0: binding->mRecord.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now())); michael@0: // write entry data to disk cache block file michael@0: diskEntry->Swap(); michael@0: michael@0: if (fileIndex != 0) { michael@0: while (1) { michael@0: uint32_t blockSize = GetBlockSizeForIndex(fileIndex); michael@0: uint32_t blocks = ((size - 1) / blockSize) + 1; michael@0: michael@0: int32_t startBlock; michael@0: rv = mBlockFile[fileIndex - 1].WriteBlocks(diskEntry, size, blocks, michael@0: &startBlock); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: // update binding and cache map record michael@0: binding->mRecord.SetMetaBlocks(fileIndex, startBlock, blocks); michael@0: michael@0: rv = UpdateRecord(&binding->mRecord); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // XXX we should probably write out bucket ourselves michael@0: michael@0: IncrementTotalSize(blocks, blockSize); michael@0: break; michael@0: } michael@0: michael@0: if (fileIndex == kNumBlockFiles) { michael@0: fileIndex = 0; // write data to separate file michael@0: break; michael@0: } michael@0: michael@0: // try next block file michael@0: fileIndex++; michael@0: } michael@0: } michael@0: michael@0: if (fileIndex == 0) { michael@0: // Write entry data to separate file michael@0: uint32_t metaFileSizeK = ((size + 0x03FF) >> 10); // round up to nearest 1k michael@0: if (metaFileSizeK > kMaxDataSizeK) michael@0: metaFileSizeK = kMaxDataSizeK; michael@0: michael@0: binding->mRecord.SetMetaFileGeneration(binding->mGeneration); michael@0: binding->mRecord.SetMetaFileSize(metaFileSizeK); michael@0: rv = UpdateRecord(&binding->mRecord); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr localFile; michael@0: rv = GetLocalFileForDiskCacheRecord(&binding->mRecord, michael@0: nsDiskCache::kMetaData, michael@0: true, michael@0: getter_AddRefs(localFile)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // open the file michael@0: PRFileDesc * fd; michael@0: // open the file - restricted to user, the data could be confidential michael@0: rv = localFile->OpenNSPRFileDesc(PR_RDWR | PR_TRUNCATE | PR_CREATE_FILE, 00600, &fd); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // write the file michael@0: int32_t bytesWritten = PR_Write(fd, diskEntry, size); michael@0: michael@0: PRStatus err = PR_Close(fd); michael@0: if ((bytesWritten != (int32_t)size) || (err != PR_SUCCESS)) { michael@0: return NS_ERROR_UNEXPECTED; michael@0: } michael@0: michael@0: IncrementTotalSize(metaFileSizeK); michael@0: } michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::ReadDataCacheBlocks(nsDiskCacheBinding * binding, char * buffer, uint32_t size) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: ReadDataCacheBlocks [%x size=%u]\n", michael@0: binding->mRecord.HashNumber(), size)); michael@0: michael@0: uint32_t fileIndex = binding->mRecord.DataFile(); michael@0: int32_t readSize = size; michael@0: michael@0: nsresult rv = mBlockFile[fileIndex - 1].ReadBlocks(buffer, michael@0: binding->mRecord.DataStartBlock(), michael@0: binding->mRecord.DataBlockCount(), michael@0: &readSize); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: if (readSize < (int32_t)size) { michael@0: rv = NS_ERROR_UNEXPECTED; michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::WriteDataCacheBlocks(nsDiskCacheBinding * binding, char * buffer, uint32_t size) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: WriteDataCacheBlocks [%x size=%u]\n", michael@0: binding->mRecord.HashNumber(), size)); michael@0: michael@0: mozilla::eventtracer::AutoEventTracer writeDataCacheBlocks( michael@0: binding->mCacheEntry, michael@0: mozilla::eventtracer::eExec, michael@0: mozilla::eventtracer::eDone, michael@0: "net::cache::WriteDataCacheBlocks"); michael@0: michael@0: nsresult rv = NS_OK; michael@0: michael@0: // determine block file & number of blocks michael@0: uint32_t fileIndex = CalculateFileIndex(size); michael@0: uint32_t blockCount = 0; michael@0: int32_t startBlock = 0; michael@0: michael@0: if (size > 0) { michael@0: // if fileIndex is 0, bad things happen below, which makes gcc 4.7 michael@0: // complain, but it's not supposed to happen. See bug 854105. michael@0: MOZ_ASSERT(fileIndex); michael@0: while (fileIndex) { michael@0: uint32_t blockSize = GetBlockSizeForIndex(fileIndex); michael@0: blockCount = ((size - 1) / blockSize) + 1; michael@0: michael@0: rv = mBlockFile[fileIndex - 1].WriteBlocks(buffer, size, blockCount, michael@0: &startBlock); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: IncrementTotalSize(blockCount, blockSize); michael@0: break; michael@0: } michael@0: michael@0: if (fileIndex == kNumBlockFiles) michael@0: return rv; michael@0: michael@0: fileIndex++; michael@0: } michael@0: } michael@0: michael@0: // update binding and cache map record michael@0: binding->mRecord.SetDataBlocks(fileIndex, startBlock, blockCount); michael@0: if (!binding->mDoomed) { michael@0: rv = UpdateRecord(&binding->mRecord); michael@0: } michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record) michael@0: { michael@0: nsresult rv1 = DeleteStorage(record, nsDiskCache::kData); michael@0: nsresult rv2 = DeleteStorage(record, nsDiskCache::kMetaData); michael@0: return NS_FAILED(rv1) ? rv1 : rv2; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::DeleteStorage(nsDiskCacheRecord * record, bool metaData) michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: DeleteStorage [%x %u]\n", record->HashNumber(), michael@0: metaData)); michael@0: michael@0: nsresult rv = NS_ERROR_UNEXPECTED; michael@0: uint32_t fileIndex = metaData ? record->MetaFile() : record->DataFile(); michael@0: nsCOMPtr file; michael@0: michael@0: if (fileIndex == 0) { michael@0: // delete the file michael@0: uint32_t sizeK = metaData ? record->MetaFileSize() : record->DataFileSize(); michael@0: // XXX if sizeK == USHRT_MAX, stat file for actual size michael@0: michael@0: rv = GetFileForDiskCacheRecord(record, metaData, false, getter_AddRefs(file)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: rv = file->Remove(false); // false == non-recursive michael@0: } michael@0: DecrementTotalSize(sizeK); michael@0: michael@0: } else if (fileIndex < (kNumBlockFiles + 1)) { michael@0: // deallocate blocks michael@0: uint32_t startBlock = metaData ? record->MetaStartBlock() : record->DataStartBlock(); michael@0: uint32_t blockCount = metaData ? record->MetaBlockCount() : record->DataBlockCount(); michael@0: michael@0: rv = mBlockFile[fileIndex - 1].DeallocateBlocks(startBlock, blockCount); michael@0: DecrementTotalSize(blockCount, GetBlockSizeForIndex(fileIndex)); michael@0: } michael@0: if (metaData) record->ClearMetaLocation(); michael@0: else record->ClearDataLocation(); michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::GetFileForDiskCacheRecord(nsDiskCacheRecord * record, michael@0: bool meta, michael@0: bool createPath, michael@0: nsIFile ** result) michael@0: { michael@0: if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: nsCOMPtr file; michael@0: nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: uint32_t hash = record->HashNumber(); michael@0: michael@0: // The file is stored under subdirectories according to the hash number: michael@0: // 0x01234567 -> 0/12/ michael@0: rv = file->AppendNative(nsPrintfCString("%X", hash >> 28)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: rv = file->AppendNative(nsPrintfCString("%02X", (hash >> 20) & 0xFF)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: bool exists; michael@0: if (createPath && (NS_FAILED(file->Exists(&exists)) || !exists)) { michael@0: rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700); michael@0: if (NS_FAILED(rv)) return rv; michael@0: } michael@0: michael@0: int16_t generation = record->Generation(); michael@0: char name[32]; michael@0: // Cut the beginning of the hash that was used in the path michael@0: ::sprintf(name, "%05X%c%02X", hash & 0xFFFFF, (meta ? 'm' : 'd'), michael@0: generation); michael@0: rv = file->AppendNative(nsDependentCString(name)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: NS_IF_ADDREF(*result = file); michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::GetLocalFileForDiskCacheRecord(nsDiskCacheRecord * record, michael@0: bool meta, michael@0: bool createPath, michael@0: nsIFile ** result) michael@0: { michael@0: nsCOMPtr file; michael@0: nsresult rv = GetFileForDiskCacheRecord(record, michael@0: meta, michael@0: createPath, michael@0: getter_AddRefs(file)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: NS_IF_ADDREF(*result = file); michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::GetBlockFileForIndex(uint32_t index, nsIFile ** result) michael@0: { michael@0: if (!mCacheDirectory) return NS_ERROR_NOT_AVAILABLE; michael@0: michael@0: nsCOMPtr file; michael@0: nsresult rv = mCacheDirectory->Clone(getter_AddRefs(file)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: char name[32]; michael@0: ::sprintf(name, "_CACHE_%03d_", index + 1); michael@0: rv = file->AppendNative(nsDependentCString(name)); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: NS_IF_ADDREF(*result = file); michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: michael@0: uint32_t michael@0: nsDiskCacheMap::CalculateFileIndex(uint32_t size) michael@0: { michael@0: // We prefer to use block file with larger block if the wasted space would michael@0: // be the same. E.g. store entry with size of 3073 bytes in 1 4K-block michael@0: // instead of in 4 1K-blocks. michael@0: michael@0: if (size <= 3 * BLOCK_SIZE_FOR_INDEX(1)) return 1; michael@0: if (size <= 3 * BLOCK_SIZE_FOR_INDEX(2)) return 2; michael@0: if (size <= 4 * BLOCK_SIZE_FOR_INDEX(3)) return 3; michael@0: return 0; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::EnsureBuffer(uint32_t bufSize) michael@0: { michael@0: if (mBufferSize < bufSize) { michael@0: char * buf = (char *)PR_REALLOC(mBuffer, bufSize); michael@0: if (!buf) { michael@0: mBufferSize = 0; michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: } michael@0: mBuffer = buf; michael@0: mBufferSize = bufSize; michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: void michael@0: nsDiskCacheMap::NotifyCapacityChange(uint32_t capacity) michael@0: { michael@0: // Heuristic 1. average cache entry size is probably around 1KB michael@0: // Heuristic 2. we don't want more than 32MB reserved to store the record michael@0: // map in memory. michael@0: const int32_t RECORD_COUNT_LIMIT = 32 * 1024 * 1024 / sizeof(nsDiskCacheRecord); michael@0: int32_t maxRecordCount = std::min(int32_t(capacity), RECORD_COUNT_LIMIT); michael@0: if (mMaxRecordCount < maxRecordCount) { michael@0: // We can only grow michael@0: mMaxRecordCount = maxRecordCount; michael@0: } michael@0: } michael@0: michael@0: size_t michael@0: nsDiskCacheMap::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) michael@0: { michael@0: size_t usage = aMallocSizeOf(mRecordArray); michael@0: michael@0: usage += aMallocSizeOf(mBuffer); michael@0: usage += aMallocSizeOf(mMapFD); michael@0: usage += aMallocSizeOf(mCleanFD); michael@0: usage += aMallocSizeOf(mCacheDirectory); michael@0: usage += aMallocSizeOf(mCleanCacheTimer); michael@0: michael@0: for (int i = 0; i < kNumBlockFiles; i++) { michael@0: usage += mBlockFile[i].SizeOfExcludingThis(aMallocSizeOf); michael@0: } michael@0: michael@0: return usage; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::InitCacheClean(nsIFile * cacheDirectory, michael@0: nsDiskCache::CorruptCacheInfo * corruptInfo, michael@0: bool reportCacheCleanTelemetryData) michael@0: { michael@0: // The _CACHE_CLEAN_ file will be used in the future to determine michael@0: // if the cache is clean or not. michael@0: bool cacheCleanFileExists = false; michael@0: nsCOMPtr cacheCleanFile; michael@0: nsresult rv = cacheDirectory->GetParent(getter_AddRefs(cacheCleanFile)); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: rv = cacheCleanFile->AppendNative( michael@0: NS_LITERAL_CSTRING("_CACHE_CLEAN_")); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: // Check if the file already exists, if it does, we will later read the michael@0: // value and report it to telemetry. michael@0: cacheCleanFile->Exists(&cacheCleanFileExists); michael@0: } michael@0: } michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Could not build cache clean file path"); michael@0: *corruptInfo = nsDiskCache::kCacheCleanFilePathError; michael@0: return rv; michael@0: } michael@0: michael@0: // Make sure the _CACHE_CLEAN_ file exists michael@0: rv = cacheCleanFile->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, michael@0: 00600, &mCleanFD); michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Could not open cache clean file"); michael@0: *corruptInfo = nsDiskCache::kCacheCleanOpenFileError; michael@0: return rv; michael@0: } michael@0: michael@0: if (cacheCleanFileExists) { michael@0: char clean = '0'; michael@0: int32_t bytesRead = PR_Read(mCleanFD, &clean, 1); michael@0: if (bytesRead != 1) { michael@0: NS_WARNING("Could not read _CACHE_CLEAN_ file contents"); michael@0: } else if (reportCacheCleanTelemetryData) { michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_REDUCTION_TRIAL, michael@0: clean == '1' ? 1 : 0); michael@0: } michael@0: } michael@0: michael@0: // Create a timer that will be used to validate the cache michael@0: // as long as an activity threshold was met michael@0: mCleanCacheTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: mCleanCacheTimer->SetTarget(nsCacheService::GlobalInstance()->mCacheIOThread); michael@0: rv = ResetCacheTimer(); michael@0: } michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Could not create cache clean timer"); michael@0: mCleanCacheTimer = nullptr; michael@0: *corruptInfo = nsDiskCache::kCacheCleanTimerError; michael@0: return rv; michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::WriteCacheClean(bool clean) michael@0: { michael@0: nsCacheService::AssertOwnsLock(); michael@0: if (!mCleanFD) { michael@0: NS_WARNING("Cache clean file is not open!"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: michael@0: CACHE_LOG_DEBUG(("CACHE: WriteCacheClean: %d\n", clean? 1 : 0)); michael@0: // I'm using a simple '1' or '0' to denote cache clean michael@0: // since it can be edited easily by any text editor for testing. michael@0: char data = clean? '1' : '0'; michael@0: int32_t filePos = PR_Seek(mCleanFD, 0, PR_SEEK_SET); michael@0: if (filePos != 0) { michael@0: NS_WARNING("Could not seek in cache clean file!"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: int32_t bytesWritten = PR_Write(mCleanFD, &data, 1); michael@0: if (bytesWritten != 1) { michael@0: NS_WARNING("Could not write cache clean file!"); michael@0: return NS_ERROR_FAILURE; michael@0: } michael@0: PRStatus err = PR_Sync(mCleanFD); michael@0: if (err != PR_SUCCESS) { michael@0: NS_WARNING("Could not flush cache clean file!"); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::InvalidateCache() michael@0: { michael@0: nsCacheService::AssertOwnsLock(); michael@0: CACHE_LOG_DEBUG(("CACHE: InvalidateCache\n")); michael@0: nsresult rv; michael@0: michael@0: if (!mIsDirtyCacheFlushed) { michael@0: rv = WriteCacheClean(false); michael@0: if (NS_FAILED(rv)) { michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_INVALIDATION_SUCCESS, 0); michael@0: return rv; michael@0: } michael@0: michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_INVALIDATION_SUCCESS, 1); michael@0: mIsDirtyCacheFlushed = true; michael@0: } michael@0: michael@0: rv = ResetCacheTimer(); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::ResetCacheTimer(int32_t timeout) michael@0: { michael@0: mCleanCacheTimer->Cancel(); michael@0: nsresult rv = michael@0: mCleanCacheTimer->InitWithFuncCallback(RevalidateTimerCallback, michael@0: nullptr, timeout, michael@0: nsITimer::TYPE_ONE_SHOT); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: mLastInvalidateTime = PR_IntervalNow(); michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: void michael@0: nsDiskCacheMap::RevalidateTimerCallback(nsITimer *aTimer, void *arg) michael@0: { michael@0: nsCacheServiceAutoLock lock(LOCK_TELEM(NSDISKCACHEMAP_REVALIDATION)); michael@0: if (!nsCacheService::gService->mDiskDevice || michael@0: !nsCacheService::gService->mDiskDevice->Initialized()) { michael@0: return; michael@0: } michael@0: michael@0: nsDiskCacheMap *diskCacheMap = michael@0: &nsCacheService::gService->mDiskDevice->mCacheMap; michael@0: michael@0: // If we have less than kRevalidateCacheTimeout since the last timer was michael@0: // issued then another thread called InvalidateCache. This won't catch michael@0: // all cases where we wanted to cancel the timer, but under the lock it michael@0: // is always OK to revalidate as long as IsCacheInSafeState() returns michael@0: // true. We just want to avoid revalidating when we can to reduce IO michael@0: // and this check will do that. michael@0: uint32_t delta = michael@0: PR_IntervalToMilliseconds(PR_IntervalNow() - michael@0: diskCacheMap->mLastInvalidateTime) + michael@0: kRevalidateCacheTimeoutTolerance; michael@0: if (delta < kRevalidateCacheTimeout) { michael@0: diskCacheMap->ResetCacheTimer(); michael@0: return; michael@0: } michael@0: michael@0: nsresult rv = diskCacheMap->RevalidateCache(); michael@0: if (NS_FAILED(rv)) { michael@0: diskCacheMap->ResetCacheTimer(kRevalidateCacheErrorTimeout); michael@0: } michael@0: } michael@0: michael@0: bool michael@0: nsDiskCacheMap::IsCacheInSafeState() michael@0: { michael@0: return nsCacheService::GlobalInstance()->IsDoomListEmpty(); michael@0: } michael@0: michael@0: nsresult michael@0: nsDiskCacheMap::RevalidateCache() michael@0: { michael@0: CACHE_LOG_DEBUG(("CACHE: RevalidateCache\n")); michael@0: nsresult rv; michael@0: michael@0: if (!IsCacheInSafeState()) { michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_REVALIDATION_SAFE, 0); michael@0: CACHE_LOG_DEBUG(("CACHE: Revalidation should not performed because " michael@0: "cache not in a safe state\n")); michael@0: // Normally we would return an error here, but there is a bug where michael@0: // the doom list sometimes gets an entry 'stuck' and doens't clear it michael@0: // until browser shutdown. So we allow revalidation for the time being michael@0: // to get proper telemetry data of how much the cache corruption plan michael@0: // would help. michael@0: } else { michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_REVALIDATION_SAFE, 1); michael@0: } michael@0: michael@0: // We want this after the lock to prove that flushing a file isn't that expensive michael@0: Telemetry::AutoTimer totalTimer; michael@0: michael@0: // If telemetry data shows it is worth it, we'll be flushing headers and michael@0: // records before flushing the clean cache file. michael@0: michael@0: // Write out the _CACHE_CLEAN_ file with '1' michael@0: rv = WriteCacheClean(true); michael@0: if (NS_FAILED(rv)) { michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_REVALIDATION_SUCCESS, 0); michael@0: return rv; michael@0: } michael@0: michael@0: Telemetry::Accumulate(Telemetry::DISK_CACHE_REVALIDATION_SUCCESS, 1); michael@0: mIsDirtyCacheFlushed = false; michael@0: michael@0: return NS_OK; michael@0: }