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