diff options
Diffstat (limited to 'netwerk/cache/nsDiskCacheDevice.cpp')
-rw-r--r-- | netwerk/cache/nsDiskCacheDevice.cpp | 1149 |
1 files changed, 1149 insertions, 0 deletions
diff --git a/netwerk/cache/nsDiskCacheDevice.cpp b/netwerk/cache/nsDiskCacheDevice.cpp new file mode 100644 index 000000000..ac91534ff --- /dev/null +++ b/netwerk/cache/nsDiskCacheDevice.cpp @@ -0,0 +1,1149 @@ +/* -*- 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 Runnable { +public: + nsDiskCacheDeviceDeactivateEntryEvent(nsDiskCacheDevice *device, + nsCacheEntry * entry, + nsDiskCacheBinding * binding) + : mCanceled(false), + mEntry(entry), + mDevice(device), + mBinding(binding) + { + } + + NS_IMETHOD Run() override + { + nsCacheServiceAutoLock lock; + CACHE_LOG_DEBUG(("nsDiskCacheDeviceDeactivateEntryEvent[%p]\n", this)); + 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 Runnable { +public: + explicit nsEvictDiskCacheEntriesEvent(nsDiskCacheDevice *device) + : mDevice(device) {} + + NS_IMETHOD Run() override + { + nsCacheServiceAutoLock lock; + 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) + +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; +} + +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; +} + +NS_IMETHODIMP nsDiskCacheDeviceInfo::GetEntryCount(uint32_t *aEntryCount) +{ + NS_ENSURE_ARG_POINTER(aEntryCount); + *aEntryCount = mDevice->getEntryCount(); + return NS_OK; +} + +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; +} + +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); MOZ_FALLTHROUGH; + case 10: c += (uint32_t(k[9])<<16); MOZ_FALLTHROUGH; + case 9 : c += (uint32_t(k[8])<<8); MOZ_FALLTHROUGH; + /* the low-order byte of c is reserved for the length */ + case 8 : b += (uint32_t(k[7])<<24); MOZ_FALLTHROUGH; + case 7 : b += (uint32_t(k[6])<<16); MOZ_FALLTHROUGH; + case 6 : b += (uint32_t(k[5])<<8); MOZ_FALLTHROUGH; + case 5 : b += k[4]; MOZ_FALLTHROUGH; + case 4 : a += (uint32_t(k[3])<<24); MOZ_FALLTHROUGH; + case 3 : a += (uint32_t(k[2])<<16); MOZ_FALLTHROUGH; + case 2 : a += (uint32_t(k[1])<<8); MOZ_FALLTHROUGH; + 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; + + mBindery.Init(); + + // 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); + + if (rv == NS_ERROR_ALREADY_INITIALIZED) { + NS_WARNING("nsDiskCacheDevice::OpenDiskCache: already open!"); + } else if (NS_FAILED(rv)) { + // Consider cache corrupt: delete it + // 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(LogLevel::Info, "\ncreate cache directory: %s\n", mCacheDirectory); + CACHE_LOG_INFO(("mCacheDirectory->Create() = %x\n", rv)); + if (NS_FAILED(rv)) + return rv; + + // reopen the cache map + nsDiskCache::CorruptCacheInfo corruptInfo; + rv = mCacheMap.Open(mCacheDirectory, &corruptInfo); + 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; +} |