mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-26 18:31:29 +00:00
1165 lines
34 KiB
C++
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;
|
|
}
|
|
|