gecko-dev/netwerk/cache/nsDiskCacheMap.cpp

1431 lines
46 KiB
C++

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