gecko-dev/netwerk/cache/nsDiskCacheDevice.cpp

1165 lines
34 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
*
* 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 <limits.h>
#include "mozilla/DebugOnly.h"
#include "nsCache.h"
#include "nsIMemoryReporter.h"
// include files for ftruncate (or equivalent)
#if defined(XP_UNIX)
#include <unistd.h>
#elif defined(XP_WIN)
#include <windows.h>
#else
// XXX add necessary include file for ftruncate (or equivalent)
#endif
#include "prthread.h"
#include "private/pprio.h"
#include "nsDiskCacheDevice.h"
#include "nsDiskCacheEntry.h"
#include "nsDiskCacheMap.h"
#include "nsDiskCacheStreams.h"
#include "nsDiskCache.h"
#include "nsCacheService.h"
#include "nsDeleteDir.h"
#include "nsICacheVisitor.h"
#include "nsReadableUtils.h"
#include "nsIInputStream.h"
#include "nsIOutputStream.h"
#include "nsCRT.h"
#include "nsCOMArray.h"
#include "nsISimpleEnumerator.h"
#include "nsThreadUtils.h"
#include "mozilla/MemoryReporting.h"
#include "mozilla/Telemetry.h"
static const char DISK_CACHE_DEVICE_ID[] = { "disk" };
using namespace mozilla;
class nsDiskCacheDeviceDeactivateEntryEvent : public nsRunnable {
public:
nsDiskCacheDeviceDeactivateEntryEvent(nsDiskCacheDevice *device,
nsCacheEntry * entry,
nsDiskCacheBinding * binding)
: mCanceled(false),
mEntry(entry),
mDevice(device),
mBinding(binding)
{
}
NS_IMETHOD Run()
{
nsCacheServiceAutoLock lock(LOCK_TELEM(NSDISKCACHEDEVICEDEACTIVATEENTRYEVENT_RUN));
#ifdef PR_LOGGING
CACHE_LOG_DEBUG(("nsDiskCacheDeviceDeactivateEntryEvent[%p]\n", this));
#endif
if (!mCanceled) {
(void) mDevice->DeactivateEntry_Private(mEntry, mBinding);
}
return NS_OK;
}
void CancelEvent() { mCanceled = true; }
private:
bool mCanceled;
nsCacheEntry *mEntry;
nsDiskCacheDevice *mDevice;
nsDiskCacheBinding *mBinding;
};
class nsEvictDiskCacheEntriesEvent : public nsRunnable {
public:
explicit nsEvictDiskCacheEntriesEvent(nsDiskCacheDevice *device)
: mDevice(device) {}
NS_IMETHOD Run()
{
nsCacheServiceAutoLock lock(LOCK_TELEM(NSEVICTDISKCACHEENTRIESEVENT_RUN));
mDevice->EvictDiskCacheEntries(mDevice->mCacheCapacity);
return NS_OK;
}
private:
nsDiskCacheDevice *mDevice;
};
/******************************************************************************
* nsDiskCacheEvictor
*
* Helper class for nsDiskCacheDevice.
*
*****************************************************************************/
class nsDiskCacheEvictor : public nsDiskCacheRecordVisitor
{
public:
nsDiskCacheEvictor( nsDiskCacheMap * cacheMap,
nsDiskCacheBindery * cacheBindery,
uint32_t targetSize,
const char * clientID)
: mCacheMap(cacheMap)
, mBindery(cacheBindery)
, mTargetSize(targetSize)
, mClientID(clientID)
{
mClientIDSize = clientID ? strlen(clientID) : 0;
}
virtual int32_t VisitRecord(nsDiskCacheRecord * mapRecord);
private:
nsDiskCacheMap * mCacheMap;
nsDiskCacheBindery * mBindery;
uint32_t mTargetSize;
const char * mClientID;
uint32_t mClientIDSize;
};
int32_t
nsDiskCacheEvictor::VisitRecord(nsDiskCacheRecord * mapRecord)
{
if (mCacheMap->TotalSize() < mTargetSize)
return kStopVisitingRecords;
if (mClientID) {
// we're just evicting records for a specific client
nsDiskCacheEntry * diskEntry = mCacheMap->ReadDiskCacheEntry(mapRecord);
if (!diskEntry)
return kVisitNextRecord; // XXX or delete record?
// Compare clientID's without malloc
if ((diskEntry->mKeySize <= mClientIDSize) ||
(diskEntry->Key()[mClientIDSize] != ':') ||
(memcmp(diskEntry->Key(), mClientID, mClientIDSize) != 0)) {
return kVisitNextRecord; // clientID doesn't match, skip it
}
}
nsDiskCacheBinding * binding = mBindery->FindActiveBinding(mapRecord->HashNumber());
if (binding) {
// If the entry is pending deactivation, cancel deactivation and doom
// the entry
if (binding->mDeactivateEvent) {
binding->mDeactivateEvent->CancelEvent();
binding->mDeactivateEvent = nullptr;
}
// We are currently using this entry, so all we can do is doom it.
// Since we're enumerating the records, we don't want to call
// DeleteRecord when nsCacheService::DoomEntry() calls us back.
binding->mDoomed = true; // mark binding record as 'deleted'
nsCacheService::DoomEntry(binding->mCacheEntry);
} else {
// entry not in use, just delete storage because we're enumerating the records
(void) mCacheMap->DeleteStorage(mapRecord);
}
return kDeleteRecordAndContinue; // this will REALLY delete the record
}
/******************************************************************************
* nsDiskCacheDeviceInfo
*****************************************************************************/
class nsDiskCacheDeviceInfo : public nsICacheDeviceInfo {
public:
NS_DECL_ISUPPORTS
NS_DECL_NSICACHEDEVICEINFO
explicit nsDiskCacheDeviceInfo(nsDiskCacheDevice* device)
: mDevice(device)
{
}
private:
virtual ~nsDiskCacheDeviceInfo() {}
nsDiskCacheDevice* mDevice;
};
NS_IMPL_ISUPPORTS(nsDiskCacheDeviceInfo, nsICacheDeviceInfo)
/* readonly attribute string description; */
NS_IMETHODIMP nsDiskCacheDeviceInfo::GetDescription(char ** aDescription)
{
NS_ENSURE_ARG_POINTER(aDescription);
*aDescription = NS_strdup("Disk cache device");
return *aDescription ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}
/* readonly attribute string usageReport; */
NS_IMETHODIMP nsDiskCacheDeviceInfo::GetUsageReport(char ** usageReport)
{
NS_ENSURE_ARG_POINTER(usageReport);
nsCString buffer;
buffer.AssignLiteral(" <tr>\n"
" <th>Cache Directory:</th>\n"
" <td>");
nsCOMPtr<nsIFile> cacheDir;
nsAutoString path;
mDevice->getCacheDirectory(getter_AddRefs(cacheDir));
nsresult rv = cacheDir->GetPath(path);
if (NS_SUCCEEDED(rv)) {
AppendUTF16toUTF8(path, buffer);
} else {
buffer.AppendLiteral("directory unavailable");
}
buffer.AppendLiteral("</td>\n"
" </tr>\n");
*usageReport = ToNewCString(buffer);
if (!*usageReport) return NS_ERROR_OUT_OF_MEMORY;
return NS_OK;
}
/* readonly attribute unsigned long entryCount; */
NS_IMETHODIMP nsDiskCacheDeviceInfo::GetEntryCount(uint32_t *aEntryCount)
{
NS_ENSURE_ARG_POINTER(aEntryCount);
*aEntryCount = mDevice->getEntryCount();
return NS_OK;
}
/* readonly attribute unsigned long totalSize; */
NS_IMETHODIMP nsDiskCacheDeviceInfo::GetTotalSize(uint32_t *aTotalSize)
{
NS_ENSURE_ARG_POINTER(aTotalSize);
// Returned unit's are in bytes
*aTotalSize = mDevice->getCacheSize() * 1024;
return NS_OK;
}
/* readonly attribute unsigned long maximumSize; */
NS_IMETHODIMP nsDiskCacheDeviceInfo::GetMaximumSize(uint32_t *aMaximumSize)
{
NS_ENSURE_ARG_POINTER(aMaximumSize);
// Returned unit's are in bytes
*aMaximumSize = mDevice->getCacheCapacity() * 1024;
return NS_OK;
}
/******************************************************************************
* nsDiskCache
*****************************************************************************/
/**
* nsDiskCache::Hash(const char * key, PLDHashNumber initval)
*
* See http://burtleburtle.net/bob/hash/evahash.html for more information
* about this hash function.
*
* This algorithm of this method implies nsDiskCacheRecords will be stored
* in a certain order on disk. If the algorithm changes, existing cache
* map files may become invalid, and therefore the kCurrentVersion needs
* to be revised.
*/
static inline void hashmix(uint32_t& a, uint32_t& b, uint32_t& c)
{
a -= b; a -= c; a ^= (c>>13);
b -= c; b -= a; b ^= (a<<8);
c -= a; c -= b; c ^= (b>>13);
a -= b; a -= c; a ^= (c>>12);
b -= c; b -= a; b ^= (a<<16);
c -= a; c -= b; c ^= (b>>5);
a -= b; a -= c; a ^= (c>>3);
b -= c; b -= a; b ^= (a<<10);
c -= a; c -= b; c ^= (b>>15);
}
PLDHashNumber
nsDiskCache::Hash(const char * key, PLDHashNumber initval)
{
const uint8_t *k = reinterpret_cast<const uint8_t*>(key);
uint32_t a, b, c, len, length;
length = strlen(key);
/* Set up the internal state */
len = length;
a = b = 0x9e3779b9; /* the golden ratio; an arbitrary value */
c = initval; /* variable initialization of internal state */
/*---------------------------------------- handle most of the key */
while (len >= 12)
{
a += k[0] + (uint32_t(k[1])<<8) + (uint32_t(k[2])<<16) + (uint32_t(k[3])<<24);
b += k[4] + (uint32_t(k[5])<<8) + (uint32_t(k[6])<<16) + (uint32_t(k[7])<<24);
c += k[8] + (uint32_t(k[9])<<8) + (uint32_t(k[10])<<16) + (uint32_t(k[11])<<24);
hashmix(a, b, c);
k += 12; len -= 12;
}
/*------------------------------------- handle the last 11 bytes */
c += length;
switch(len) { /* all the case statements fall through */
case 11: c += (uint32_t(k[10])<<24);
case 10: c += (uint32_t(k[9])<<16);
case 9 : c += (uint32_t(k[8])<<8);
/* the low-order byte of c is reserved for the length */
case 8 : b += (uint32_t(k[7])<<24);
case 7 : b += (uint32_t(k[6])<<16);
case 6 : b += (uint32_t(k[5])<<8);
case 5 : b += k[4];
case 4 : a += (uint32_t(k[3])<<24);
case 3 : a += (uint32_t(k[2])<<16);
case 2 : a += (uint32_t(k[1])<<8);
case 1 : a += k[0];
/* case 0: nothing left to add */
}
hashmix(a, b, c);
return c;
}
nsresult
nsDiskCache::Truncate(PRFileDesc * fd, uint32_t newEOF)
{
// use modified SetEOF from nsFileStreams::SetEOF()
#if defined(XP_UNIX)
if (ftruncate(PR_FileDesc2NativeHandle(fd), newEOF) != 0) {
NS_ERROR("ftruncate failed");
return NS_ERROR_FAILURE;
}
#elif defined(XP_WIN)
int32_t cnt = PR_Seek(fd, newEOF, PR_SEEK_SET);
if (cnt == -1) return NS_ERROR_FAILURE;
if (!SetEndOfFile((HANDLE) PR_FileDesc2NativeHandle(fd))) {
NS_ERROR("SetEndOfFile failed");
return NS_ERROR_FAILURE;
}
#else
// add implementations for other platforms here
#endif
return NS_OK;
}
/******************************************************************************
* nsDiskCacheDevice
*****************************************************************************/
nsDiskCacheDevice::nsDiskCacheDevice()
: mCacheCapacity(0)
, mMaxEntrySize(-1) // -1 means "no limit"
, mInitialized(false)
, mClearingDiskCache(false)
{
}
nsDiskCacheDevice::~nsDiskCacheDevice()
{
Shutdown();
}
/**
* methods of nsCacheDevice
*/
nsresult
nsDiskCacheDevice::Init()
{
nsresult rv;
if (Initialized()) {
NS_ERROR("Disk cache already initialized!");
return NS_ERROR_UNEXPECTED;
}
if (!mCacheDirectory)
return NS_ERROR_FAILURE;
rv = mBindery.Init();
if (NS_FAILED(rv))
return rv;
// Open Disk Cache
rv = OpenDiskCache();
if (NS_FAILED(rv)) {
(void) mCacheMap.Close(false);
return rv;
}
mInitialized = true;
return NS_OK;
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::Shutdown()
{
nsCacheService::AssertOwnsLock();
nsresult rv = Shutdown_Private(true);
if (NS_FAILED(rv))
return rv;
return NS_OK;
}
nsresult
nsDiskCacheDevice::Shutdown_Private(bool flush)
{
CACHE_LOG_DEBUG(("CACHE: disk Shutdown_Private [%u]\n", flush));
if (Initialized()) {
// check cache limits in case we need to evict.
EvictDiskCacheEntries(mCacheCapacity);
// At this point there may be a number of pending cache-requests on the
// cache-io thread. Wait for all these to run before we wipe out our
// datastructures (see bug #620660)
(void) nsCacheService::SyncWithCacheIOThread();
// write out persistent information about the cache.
(void) mCacheMap.Close(flush);
mBindery.Reset();
mInitialized = false;
}
return NS_OK;
}
const char *
nsDiskCacheDevice::GetDeviceID()
{
return DISK_CACHE_DEVICE_ID;
}
/**
* FindEntry -
*
* cases: key not in disk cache, hash number free
* key not in disk cache, hash number used
* key in disk cache
*
* NOTE: called while holding the cache service lock
*/
nsCacheEntry *
nsDiskCacheDevice::FindEntry(nsCString * key, bool *collision)
{
Telemetry::AutoTimer<Telemetry::CACHE_DISK_SEARCH_2> timer;
if (!Initialized()) return nullptr; // NS_ERROR_NOT_INITIALIZED
if (mClearingDiskCache) return nullptr;
nsDiskCacheRecord record;
nsDiskCacheBinding * binding = nullptr;
PLDHashNumber hashNumber = nsDiskCache::Hash(key->get());
*collision = false;
binding = mBindery.FindActiveBinding(hashNumber);
if (binding && !binding->mCacheEntry->Key()->Equals(*key)) {
*collision = true;
return nullptr;
} else if (binding && binding->mDeactivateEvent) {
binding->mDeactivateEvent->CancelEvent();
binding->mDeactivateEvent = nullptr;
CACHE_LOG_DEBUG(("CACHE: reusing deactivated entry %p " \
"req-key=%s entry-key=%s\n",
binding->mCacheEntry, key, binding->mCacheEntry->Key()));
return binding->mCacheEntry; // just return this one, observing that
// FindActiveBinding() does not return
// bindings to doomed entries
}
binding = nullptr;
// lookup hash number in cache map
nsresult rv = mCacheMap.FindRecord(hashNumber, &record);
if (NS_FAILED(rv)) return nullptr; // XXX log error?
nsDiskCacheEntry * diskEntry = mCacheMap.ReadDiskCacheEntry(&record);
if (!diskEntry) return nullptr;
// compare key to be sure
if (!key->Equals(diskEntry->Key())) {
*collision = true;
return nullptr;
}
nsCacheEntry * entry = diskEntry->CreateCacheEntry(this);
if (entry) {
binding = mBindery.CreateBinding(entry, &record);
if (!binding) {
delete entry;
entry = nullptr;
}
}
if (!entry) {
(void) mCacheMap.DeleteStorage(&record);
(void) mCacheMap.DeleteRecord(&record);
}
return entry;
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::DeactivateEntry(nsCacheEntry * entry)
{
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
if (!IsValidBinding(binding))
return NS_ERROR_UNEXPECTED;
CACHE_LOG_DEBUG(("CACHE: disk DeactivateEntry [%p %x]\n",
entry, binding->mRecord.HashNumber()));
nsDiskCacheDeviceDeactivateEntryEvent *event =
new nsDiskCacheDeviceDeactivateEntryEvent(this, entry, binding);
// ensure we can cancel the event via the binding later if necessary
binding->mDeactivateEvent = event;
DebugOnly<nsresult> rv = nsCacheService::DispatchToCacheIOThread(event);
NS_ASSERTION(NS_SUCCEEDED(rv), "DeactivateEntry: Failed dispatching "
"deactivation event");
return NS_OK;
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::DeactivateEntry_Private(nsCacheEntry * entry,
nsDiskCacheBinding * binding)
{
nsresult rv = NS_OK;
if (entry->IsDoomed()) {
// delete data, entry, record from disk for entry
rv = mCacheMap.DeleteStorage(&binding->mRecord);
} else {
// save stuff to disk for entry
rv = mCacheMap.WriteDiskCacheEntry(binding);
if (NS_FAILED(rv)) {
// clean up as best we can
(void) mCacheMap.DeleteStorage(&binding->mRecord);
(void) mCacheMap.DeleteRecord(&binding->mRecord);
binding->mDoomed = true; // record is no longer in cache map
}
}
mBindery.RemoveBinding(binding); // extract binding from collision detection stuff
delete entry; // which will release binding
return rv;
}
/**
* BindEntry()
* no hash number collision -> no problem
* collision
* record not active -> evict, no problem
* record is active
* record is already doomed -> record shouldn't have been in map, no problem
* record is not doomed -> doom, and replace record in map
*
* walk matching hashnumber list to find lowest generation number
* take generation number from other (data/meta) location,
* or walk active list
*
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::BindEntry(nsCacheEntry * entry)
{
if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
if (mClearingDiskCache) return NS_ERROR_NOT_AVAILABLE;
nsresult rv = NS_OK;
nsDiskCacheRecord record, oldRecord;
nsDiskCacheBinding *binding;
PLDHashNumber hashNumber = nsDiskCache::Hash(entry->Key()->get());
// Find out if there is already an active binding for this hash. If yes it
// should have another key since BindEntry() shouldn't be called twice for
// the same entry. Doom the old entry, the new one will get another
// generation number so files won't collide.
binding = mBindery.FindActiveBinding(hashNumber);
if (binding) {
NS_ASSERTION(!binding->mCacheEntry->Key()->Equals(*entry->Key()),
"BindEntry called for already bound entry!");
// If the entry is pending deactivation, cancel deactivation
if (binding->mDeactivateEvent) {
binding->mDeactivateEvent->CancelEvent();
binding->mDeactivateEvent = nullptr;
}
nsCacheService::DoomEntry(binding->mCacheEntry);
binding = nullptr;
}
// Lookup hash number in cache map. There can be a colliding inactive entry.
// See bug #321361 comment 21 for the scenario. If there is such entry,
// delete it.
rv = mCacheMap.FindRecord(hashNumber, &record);
if (NS_SUCCEEDED(rv)) {
nsDiskCacheEntry * diskEntry = mCacheMap.ReadDiskCacheEntry(&record);
if (diskEntry) {
// compare key to be sure
if (!entry->Key()->Equals(diskEntry->Key())) {
mCacheMap.DeleteStorage(&record);
rv = mCacheMap.DeleteRecord(&record);
if (NS_FAILED(rv)) return rv;
}
}
record = nsDiskCacheRecord();
}
// create a new record for this entry
record.SetHashNumber(nsDiskCache::Hash(entry->Key()->get()));
record.SetEvictionRank(ULONG_MAX - SecondsFromPRTime(PR_Now()));
CACHE_LOG_DEBUG(("CACHE: disk BindEntry [%p %x]\n",
entry, record.HashNumber()));
if (!entry->IsDoomed()) {
// if entry isn't doomed, add it to the cache map
rv = mCacheMap.AddRecord(&record, &oldRecord); // deletes old record, if any
if (NS_FAILED(rv)) return rv;
uint32_t oldHashNumber = oldRecord.HashNumber();
if (oldHashNumber) {
// gotta evict this one first
nsDiskCacheBinding * oldBinding = mBindery.FindActiveBinding(oldHashNumber);
if (oldBinding) {
// XXX if debug : compare keys for hashNumber collision
if (!oldBinding->mCacheEntry->IsDoomed()) {
// If the old entry is pending deactivation, cancel deactivation
if (oldBinding->mDeactivateEvent) {
oldBinding->mDeactivateEvent->CancelEvent();
oldBinding->mDeactivateEvent = nullptr;
}
// we've got a live one!
nsCacheService::DoomEntry(oldBinding->mCacheEntry);
// storage will be delete when oldBinding->mCacheEntry is Deactivated
}
} else {
// delete storage
// XXX if debug : compare keys for hashNumber collision
rv = mCacheMap.DeleteStorage(&oldRecord);
if (NS_FAILED(rv)) return rv; // XXX delete record we just added?
}
}
}
// Make sure this entry has its associated nsDiskCacheBinding attached.
binding = mBindery.CreateBinding(entry, &record);
NS_ASSERTION(binding, "nsDiskCacheDevice::BindEntry");
if (!binding) return NS_ERROR_OUT_OF_MEMORY;
NS_ASSERTION(binding->mRecord.ValidRecord(), "bad cache map record");
return NS_OK;
}
/**
* NOTE: called while holding the cache service lock
*/
void
nsDiskCacheDevice::DoomEntry(nsCacheEntry * entry)
{
CACHE_LOG_DEBUG(("CACHE: disk DoomEntry [%p]\n", entry));
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
NS_ASSERTION(binding, "DoomEntry: binding == nullptr");
if (!binding)
return;
if (!binding->mDoomed) {
// so it can't be seen by FindEntry() ever again.
#ifdef DEBUG
nsresult rv =
#endif
mCacheMap.DeleteRecord(&binding->mRecord);
NS_ASSERTION(NS_SUCCEEDED(rv),"DeleteRecord failed.");
binding->mDoomed = true; // record in no longer in cache map
}
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::OpenInputStreamForEntry(nsCacheEntry * entry,
nsCacheAccessMode mode,
uint32_t offset,
nsIInputStream ** result)
{
CACHE_LOG_DEBUG(("CACHE: disk OpenInputStreamForEntry [%p %x %u]\n",
entry, mode, offset));
NS_ENSURE_ARG_POINTER(entry);
NS_ENSURE_ARG_POINTER(result);
nsresult rv;
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
if (!IsValidBinding(binding))
return NS_ERROR_UNEXPECTED;
NS_ASSERTION(binding->mCacheEntry == entry, "binding & entry don't point to each other");
rv = binding->EnsureStreamIO();
if (NS_FAILED(rv)) return rv;
return binding->mStreamIO->GetInputStream(offset, result);
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::OpenOutputStreamForEntry(nsCacheEntry * entry,
nsCacheAccessMode mode,
uint32_t offset,
nsIOutputStream ** result)
{
CACHE_LOG_DEBUG(("CACHE: disk OpenOutputStreamForEntry [%p %x %u]\n",
entry, mode, offset));
NS_ENSURE_ARG_POINTER(entry);
NS_ENSURE_ARG_POINTER(result);
nsresult rv;
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
if (!IsValidBinding(binding))
return NS_ERROR_UNEXPECTED;
NS_ASSERTION(binding->mCacheEntry == entry, "binding & entry don't point to each other");
rv = binding->EnsureStreamIO();
if (NS_FAILED(rv)) return rv;
return binding->mStreamIO->GetOutputStream(offset, result);
}
/**
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::GetFileForEntry(nsCacheEntry * entry,
nsIFile ** result)
{
NS_ENSURE_ARG_POINTER(result);
*result = nullptr;
nsresult rv;
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
if (!IsValidBinding(binding))
return NS_ERROR_UNEXPECTED;
// check/set binding->mRecord for separate file, sync w/mCacheMap
if (binding->mRecord.DataLocationInitialized()) {
if (binding->mRecord.DataFile() != 0)
return NS_ERROR_NOT_AVAILABLE; // data not stored as separate file
NS_ASSERTION(binding->mRecord.DataFileGeneration() == binding->mGeneration, "error generations out of sync");
} else {
binding->mRecord.SetDataFileGeneration(binding->mGeneration);
binding->mRecord.SetDataFileSize(0); // 1k minimum
if (!binding->mDoomed) {
// record stored in cache map, so update it
rv = mCacheMap.UpdateRecord(&binding->mRecord);
if (NS_FAILED(rv)) return rv;
}
}
nsCOMPtr<nsIFile> file;
rv = mCacheMap.GetFileForDiskCacheRecord(&binding->mRecord,
nsDiskCache::kData,
false,
getter_AddRefs(file));
if (NS_FAILED(rv)) return rv;
NS_IF_ADDREF(*result = file);
return NS_OK;
}
/**
* This routine will get called every time an open descriptor is written to.
*
* NOTE: called while holding the cache service lock
*/
nsresult
nsDiskCacheDevice::OnDataSizeChange(nsCacheEntry * entry, int32_t deltaSize)
{
CACHE_LOG_DEBUG(("CACHE: disk OnDataSizeChange [%p %d]\n",
entry, deltaSize));
// If passed a negative value, then there's nothing to do.
if (deltaSize < 0)
return NS_OK;
nsDiskCacheBinding * binding = GetCacheEntryBinding(entry);
if (!IsValidBinding(binding))
return NS_ERROR_UNEXPECTED;
NS_ASSERTION(binding->mRecord.ValidRecord(), "bad record");
uint32_t newSize = entry->DataSize() + deltaSize;
uint32_t newSizeK = ((newSize + 0x3FF) >> 10);
// If the new size is larger than max. file size or larger than
// 1/8 the cache capacity (which is in KiB's), doom the entry and abort.
if (EntryIsTooBig(newSize)) {
#ifdef DEBUG
nsresult rv =
#endif
nsCacheService::DoomEntry(entry);
NS_ASSERTION(NS_SUCCEEDED(rv),"DoomEntry() failed.");
return NS_ERROR_ABORT;
}
uint32_t sizeK = ((entry->DataSize() + 0x03FF) >> 10); // round up to next 1k
// In total count we ignore anything over kMaxDataSizeK (bug #651100), so
// the target capacity should be calculated the same way.
if (sizeK > kMaxDataSizeK) sizeK = kMaxDataSizeK;
if (newSizeK > kMaxDataSizeK) newSizeK = kMaxDataSizeK;
// pre-evict entries to make space for new data
uint32_t targetCapacity = mCacheCapacity > (newSizeK - sizeK)
? mCacheCapacity - (newSizeK - sizeK)
: 0;
EvictDiskCacheEntries(targetCapacity);
return NS_OK;
}
/******************************************************************************
* EntryInfoVisitor
*****************************************************************************/
class EntryInfoVisitor : public nsDiskCacheRecordVisitor
{
public:
EntryInfoVisitor(nsDiskCacheMap * cacheMap,
nsICacheVisitor * visitor)
: mCacheMap(cacheMap)
, mVisitor(visitor)
{}
virtual int32_t VisitRecord(nsDiskCacheRecord * mapRecord)
{
// XXX optimization: do we have this record in memory?
// read in the entry (metadata)
nsDiskCacheEntry * diskEntry = mCacheMap->ReadDiskCacheEntry(mapRecord);
if (!diskEntry) {
return kVisitNextRecord;
}
// create nsICacheEntryInfo
nsDiskCacheEntryInfo * entryInfo = new nsDiskCacheEntryInfo(DISK_CACHE_DEVICE_ID, diskEntry);
if (!entryInfo) {
return kStopVisitingRecords;
}
nsCOMPtr<nsICacheEntryInfo> ref(entryInfo);
bool keepGoing;
(void)mVisitor->VisitEntry(DISK_CACHE_DEVICE_ID, entryInfo, &keepGoing);
return keepGoing ? kVisitNextRecord : kStopVisitingRecords;
}
private:
nsDiskCacheMap * mCacheMap;
nsICacheVisitor * mVisitor;
};
nsresult
nsDiskCacheDevice::Visit(nsICacheVisitor * visitor)
{
if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
nsDiskCacheDeviceInfo* deviceInfo = new nsDiskCacheDeviceInfo(this);
nsCOMPtr<nsICacheDeviceInfo> ref(deviceInfo);
bool keepGoing;
nsresult rv = visitor->VisitDevice(DISK_CACHE_DEVICE_ID, deviceInfo, &keepGoing);
if (NS_FAILED(rv)) return rv;
if (keepGoing) {
EntryInfoVisitor infoVisitor(&mCacheMap, visitor);
return mCacheMap.VisitRecords(&infoVisitor);
}
return NS_OK;
}
// Max allowed size for an entry is currently MIN(mMaxEntrySize, 1/8 CacheCapacity)
bool
nsDiskCacheDevice::EntryIsTooBig(int64_t entrySize)
{
if (mMaxEntrySize == -1) // no limit
return entrySize > (static_cast<int64_t>(mCacheCapacity) * 1024 / 8);
else
return entrySize > mMaxEntrySize ||
entrySize > (static_cast<int64_t>(mCacheCapacity) * 1024 / 8);
}
nsresult
nsDiskCacheDevice::EvictEntries(const char * clientID)
{
CACHE_LOG_DEBUG(("CACHE: disk EvictEntries [%s]\n", clientID));
if (!Initialized()) return NS_ERROR_NOT_INITIALIZED;
nsresult rv;
if (clientID == nullptr) {
// we're clearing the entire disk cache
rv = ClearDiskCache();
if (rv != NS_ERROR_CACHE_IN_USE)
return rv;
}
nsDiskCacheEvictor evictor(&mCacheMap, &mBindery, 0, clientID);
rv = mCacheMap.VisitRecords(&evictor);
if (clientID == nullptr) // we tried to clear the entire cache
rv = mCacheMap.Trim(); // so trim cache block files (if possible)
return rv;
}
/**
* private methods
*/
nsresult
nsDiskCacheDevice::OpenDiskCache()
{
Telemetry::AutoTimer<Telemetry::NETWORK_DISK_CACHE_OPEN> timer;
// if we don't have a cache directory, create one and open it
bool exists;
nsresult rv = mCacheDirectory->Exists(&exists);
if (NS_FAILED(rv))
return rv;
if (exists) {
// Try opening cache map file.
nsDiskCache::CorruptCacheInfo corruptInfo;
rv = mCacheMap.Open(mCacheDirectory, &corruptInfo, true);
if (NS_SUCCEEDED(rv)) {
Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT_DETAILS,
corruptInfo);
} else if (rv == NS_ERROR_ALREADY_INITIALIZED) {
NS_WARNING("nsDiskCacheDevice::OpenDiskCache: already open!");
} else {
// Consider cache corrupt: delete it
Telemetry::Accumulate(Telemetry::DISK_CACHE_CORRUPT_DETAILS,
corruptInfo);
// delay delete by 1 minute to avoid IO thrash at startup
rv = nsDeleteDir::DeleteDir(mCacheDirectory, true, 60000);
if (NS_FAILED(rv))
return rv;
exists = false;
}
}
// if we don't have a cache directory, create one and open it
if (!exists) {
nsCacheService::MarkStartingFresh();
rv = mCacheDirectory->Create(nsIFile::DIRECTORY_TYPE, 0777);
CACHE_LOG_PATH(PR_LOG_ALWAYS, "\ncreate cache directory: %s\n", mCacheDirectory);
CACHE_LOG_ALWAYS(("mCacheDirectory->Create() = %x\n", rv));
if (NS_FAILED(rv))
return rv;
// reopen the cache map
nsDiskCache::CorruptCacheInfo corruptInfo;
rv = mCacheMap.Open(mCacheDirectory, &corruptInfo, false);
if (NS_FAILED(rv))
return rv;
}
return NS_OK;
}
nsresult
nsDiskCacheDevice::ClearDiskCache()
{
if (mBindery.ActiveBindings())
return NS_ERROR_CACHE_IN_USE;
mClearingDiskCache = true;
nsresult rv = Shutdown_Private(false); // false: don't bother flushing
if (NS_FAILED(rv))
return rv;
mClearingDiskCache = false;
// If the disk cache directory is already gone, then it's not an error if
// we fail to delete it ;-)
rv = nsDeleteDir::DeleteDir(mCacheDirectory, true);
if (NS_FAILED(rv) && rv != NS_ERROR_FILE_TARGET_DOES_NOT_EXIST)
return rv;
return Init();
}
nsresult
nsDiskCacheDevice::EvictDiskCacheEntries(uint32_t targetCapacity)
{
CACHE_LOG_DEBUG(("CACHE: disk EvictDiskCacheEntries [%u]\n",
targetCapacity));
NS_ASSERTION(targetCapacity > 0, "oops");
if (mCacheMap.TotalSize() < targetCapacity)
return NS_OK;
// targetCapacity is in KiB's
nsDiskCacheEvictor evictor(&mCacheMap, &mBindery, targetCapacity, nullptr);
return mCacheMap.EvictRecords(&evictor);
}
/**
* methods for prefs
*/
void
nsDiskCacheDevice::SetCacheParentDirectory(nsIFile * parentDir)
{
nsresult rv;
bool exists;
if (Initialized()) {
NS_ASSERTION(false, "Cannot switch cache directory when initialized");
return;
}
if (!parentDir) {
mCacheDirectory = nullptr;
return;
}
// ensure parent directory exists
rv = parentDir->Exists(&exists);
if (NS_SUCCEEDED(rv) && !exists)
rv = parentDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
if (NS_FAILED(rv)) return;
// ensure cache directory exists
nsCOMPtr<nsIFile> directory;
rv = parentDir->Clone(getter_AddRefs(directory));
if (NS_FAILED(rv)) return;
rv = directory->AppendNative(NS_LITERAL_CSTRING("Cache"));
if (NS_FAILED(rv)) return;
mCacheDirectory = do_QueryInterface(directory);
}
void
nsDiskCacheDevice::getCacheDirectory(nsIFile ** result)
{
*result = mCacheDirectory;
NS_IF_ADDREF(*result);
}
/**
* NOTE: called while holding the cache service lock
*/
void
nsDiskCacheDevice::SetCapacity(uint32_t capacity)
{
// Units are KiB's
mCacheCapacity = capacity;
if (Initialized()) {
if (NS_IsMainThread()) {
// Do not evict entries on the main thread
nsCacheService::DispatchToCacheIOThread(
new nsEvictDiskCacheEntriesEvent(this));
} else {
// start evicting entries if the new size is smaller!
EvictDiskCacheEntries(mCacheCapacity);
}
}
// Let cache map know of the new capacity
mCacheMap.NotifyCapacityChange(capacity);
}
uint32_t nsDiskCacheDevice::getCacheCapacity()
{
return mCacheCapacity;
}
uint32_t nsDiskCacheDevice::getCacheSize()
{
return mCacheMap.TotalSize();
}
uint32_t nsDiskCacheDevice::getEntryCount()
{
return mCacheMap.EntryCount();
}
void
nsDiskCacheDevice::SetMaxEntrySize(int32_t maxSizeInKilobytes)
{
// Internal units are bytes. Changing this only takes effect *after* the
// change and has no consequences for existing cache-entries
if (maxSizeInKilobytes >= 0)
mMaxEntrySize = maxSizeInKilobytes * 1024;
else
mMaxEntrySize = -1;
}
size_t
nsDiskCacheDevice::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf)
{
size_t usage = aMallocSizeOf(this);
usage += mCacheMap.SizeOfExcludingThis(aMallocSizeOf);
usage += mBindery.SizeOfExcludingThis(aMallocSizeOf);
return usage;
}