michael@0: /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ michael@0: /* vim: set ts=8 sts=4 et sw=4 tw=80: */ 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 "nsCache.h" michael@0: #include "nsMemoryCacheDevice.h" michael@0: #include "nsCacheService.h" michael@0: #include "nsICacheService.h" michael@0: #include "nsICacheVisitor.h" michael@0: #include "nsIStorageStream.h" michael@0: #include "nsCRT.h" michael@0: #include "nsReadableUtils.h" michael@0: #include "mozilla/MathAlgorithms.h" michael@0: #include "mozilla/Telemetry.h" michael@0: #include michael@0: michael@0: // The memory cache implements the "LRU-SP" caching algorithm michael@0: // described in "LRU-SP: A Size-Adjusted and Popularity-Aware LRU Replacement michael@0: // Algorithm for Web Caching" by Kai Cheng and Yahiko Kambayashi. michael@0: michael@0: // We keep kQueueCount LRU queues, which should be about ceil(log2(mHardLimit)) michael@0: // The queues hold exponentially increasing ranges of floor(log2((size/nref))) michael@0: // values for entries. michael@0: // Entries larger than 2^(kQueueCount-1) go in the last queue. michael@0: // Entries with no expiration go in the first queue. michael@0: michael@0: const char *gMemoryDeviceID = "memory"; michael@0: michael@0: michael@0: nsMemoryCacheDevice::nsMemoryCacheDevice() michael@0: : mInitialized(false), michael@0: mHardLimit(4 * 1024 * 1024), // default, if no pref michael@0: mSoftLimit((mHardLimit * 9) / 10), // default, if no pref michael@0: mTotalSize(0), michael@0: mInactiveSize(0), michael@0: mEntryCount(0), michael@0: mMaxEntryCount(0), michael@0: mMaxEntrySize(-1) // -1 means "no limit" michael@0: { michael@0: for (int i=0; i= 0; --i) { michael@0: entry = (nsCacheEntry *)PR_LIST_HEAD(&mEvictionList[i]); michael@0: while (entry != &mEvictionList[i]) { michael@0: NS_ASSERTION(!entry->IsInUse(), "### shutting down with active entries"); michael@0: next = (nsCacheEntry *)PR_NEXT_LINK(entry); michael@0: PR_REMOVE_AND_INIT_LINK(entry); michael@0: michael@0: // update statistics michael@0: int32_t memoryRecovered = (int32_t)entry->DataSize(); michael@0: mTotalSize -= memoryRecovered; michael@0: mInactiveSize -= memoryRecovered; michael@0: --mEntryCount; michael@0: michael@0: delete entry; michael@0: entry = next; michael@0: } michael@0: } michael@0: michael@0: /* michael@0: * we're not factoring in changes to meta data yet... michael@0: * NS_ASSERTION(mTotalSize == 0, "### mem cache leaking entries?"); michael@0: */ michael@0: NS_ASSERTION(mInactiveSize == 0, "### mem cache leaking entries?"); michael@0: NS_ASSERTION(mEntryCount == 0, "### mem cache leaking entries?"); michael@0: michael@0: mInitialized = false; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: const char * michael@0: nsMemoryCacheDevice::GetDeviceID() michael@0: { michael@0: return gMemoryDeviceID; michael@0: } michael@0: michael@0: michael@0: nsCacheEntry * michael@0: nsMemoryCacheDevice::FindEntry(nsCString * key, bool *collision) michael@0: { michael@0: mozilla::Telemetry::AutoTimer timer; michael@0: nsCacheEntry * entry = mMemCacheEntries.GetEntry(key); michael@0: if (!entry) return nullptr; michael@0: michael@0: // move entry to the tail of an eviction list michael@0: PR_REMOVE_AND_INIT_LINK(entry); michael@0: PR_APPEND_LINK(entry, &mEvictionList[EvictionList(entry, 0)]); michael@0: michael@0: mInactiveSize -= entry->DataSize(); michael@0: michael@0: return entry; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::DeactivateEntry(nsCacheEntry * entry) michael@0: { michael@0: CACHE_LOG_DEBUG(("nsMemoryCacheDevice::DeactivateEntry for entry 0x%p\n", michael@0: entry)); michael@0: if (entry->IsDoomed()) { michael@0: #ifdef DEBUG michael@0: // XXX verify we've removed it from mMemCacheEntries & eviction list michael@0: #endif michael@0: delete entry; michael@0: CACHE_LOG_DEBUG(("deleted doomed entry 0x%p\n", entry)); michael@0: return NS_OK; michael@0: } michael@0: michael@0: #ifdef DEBUG michael@0: nsCacheEntry * ourEntry = mMemCacheEntries.GetEntry(entry->Key()); michael@0: NS_ASSERTION(ourEntry, "DeactivateEntry called for an entry we don't have!"); michael@0: NS_ASSERTION(entry == ourEntry, "entry doesn't match ourEntry"); michael@0: if (ourEntry != entry) michael@0: return NS_ERROR_INVALID_POINTER; michael@0: #endif michael@0: michael@0: mInactiveSize += entry->DataSize(); michael@0: EvictEntriesIfNecessary(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::BindEntry(nsCacheEntry * entry) michael@0: { michael@0: if (!entry->IsDoomed()) { michael@0: NS_ASSERTION(PR_CLIST_IS_EMPTY(entry),"entry is already on a list!"); michael@0: michael@0: // append entry to the eviction list michael@0: PR_APPEND_LINK(entry, &mEvictionList[EvictionList(entry, 0)]); michael@0: michael@0: // add entry to hashtable of mem cache entries michael@0: nsresult rv = mMemCacheEntries.AddEntry(entry); michael@0: if (NS_FAILED(rv)) { michael@0: PR_REMOVE_AND_INIT_LINK(entry); michael@0: return rv; michael@0: } michael@0: michael@0: // add size of entry to memory totals michael@0: ++mEntryCount; michael@0: if (mMaxEntryCount < mEntryCount) mMaxEntryCount = mEntryCount; michael@0: michael@0: mTotalSize += entry->DataSize(); michael@0: EvictEntriesIfNecessary(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: void michael@0: nsMemoryCacheDevice::DoomEntry(nsCacheEntry * entry) michael@0: { michael@0: #ifdef DEBUG michael@0: // debug code to verify we have entry michael@0: nsCacheEntry * hashEntry = mMemCacheEntries.GetEntry(entry->Key()); michael@0: if (!hashEntry) NS_WARNING("no entry for key"); michael@0: else if (entry != hashEntry) NS_WARNING("entry != hashEntry"); michael@0: #endif michael@0: CACHE_LOG_DEBUG(("Dooming entry 0x%p in memory cache\n", entry)); michael@0: EvictEntry(entry, DO_NOT_DELETE_ENTRY); michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::OpenInputStreamForEntry( nsCacheEntry * entry, michael@0: nsCacheAccessMode mode, michael@0: uint32_t offset, michael@0: nsIInputStream ** result) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(entry); michael@0: NS_ENSURE_ARG_POINTER(result); michael@0: michael@0: nsCOMPtr storage; michael@0: nsresult rv; michael@0: michael@0: nsISupports *data = entry->Data(); michael@0: if (data) { michael@0: storage = do_QueryInterface(data, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: else { michael@0: rv = NS_NewStorageStream(4096, uint32_t(-1), getter_AddRefs(storage)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: entry->SetData(storage); michael@0: } michael@0: michael@0: return storage->NewInputStream(offset, result); michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::OpenOutputStreamForEntry( nsCacheEntry * entry, michael@0: nsCacheAccessMode mode, michael@0: uint32_t offset, michael@0: nsIOutputStream ** result) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(entry); michael@0: NS_ENSURE_ARG_POINTER(result); michael@0: michael@0: nsCOMPtr storage; michael@0: nsresult rv; michael@0: michael@0: nsISupports *data = entry->Data(); michael@0: if (data) { michael@0: storage = do_QueryInterface(data, &rv); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: } michael@0: else { michael@0: rv = NS_NewStorageStream(4096, uint32_t(-1), getter_AddRefs(storage)); michael@0: if (NS_FAILED(rv)) michael@0: return rv; michael@0: entry->SetData(storage); michael@0: } michael@0: michael@0: return storage->GetOutputStream(offset, result); michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::GetFileForEntry( nsCacheEntry * entry, michael@0: nsIFile ** result ) michael@0: { michael@0: return NS_ERROR_NOT_IMPLEMENTED; michael@0: } michael@0: michael@0: bool michael@0: nsMemoryCacheDevice::EntryIsTooBig(int64_t entrySize) michael@0: { michael@0: CACHE_LOG_DEBUG(("nsMemoryCacheDevice::EntryIsTooBig " michael@0: "[size=%d max=%d soft=%d]\n", michael@0: entrySize, mMaxEntrySize, mSoftLimit)); michael@0: if (mMaxEntrySize == -1) michael@0: return entrySize > mSoftLimit; michael@0: else michael@0: return (entrySize > mSoftLimit || entrySize > mMaxEntrySize); michael@0: } michael@0: michael@0: size_t michael@0: nsMemoryCacheDevice::TotalSize() michael@0: { michael@0: return mTotalSize; michael@0: } michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::OnDataSizeChange( nsCacheEntry * entry, int32_t deltaSize) michael@0: { michael@0: if (entry->IsStreamData()) { michael@0: // we have the right to refuse or pre-evict michael@0: uint32_t newSize = entry->DataSize() + deltaSize; 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: michael@0: // adjust our totals michael@0: mTotalSize += deltaSize; michael@0: michael@0: if (!entry->IsDoomed()) { michael@0: // move entry to the tail of the appropriate eviction list michael@0: PR_REMOVE_AND_INIT_LINK(entry); michael@0: PR_APPEND_LINK(entry, &mEvictionList[EvictionList(entry, deltaSize)]); michael@0: } michael@0: michael@0: EvictEntriesIfNecessary(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: void michael@0: nsMemoryCacheDevice::AdjustMemoryLimits(int32_t softLimit, int32_t hardLimit) michael@0: { michael@0: mSoftLimit = softLimit; michael@0: mHardLimit = hardLimit; michael@0: michael@0: // First, evict entries that won't fit into the new cache size. michael@0: EvictEntriesIfNecessary(); michael@0: } michael@0: michael@0: michael@0: void michael@0: nsMemoryCacheDevice::EvictEntry(nsCacheEntry * entry, bool deleteEntry) michael@0: { michael@0: CACHE_LOG_DEBUG(("Evicting entry 0x%p from memory cache, deleting: %d\n", michael@0: entry, deleteEntry)); michael@0: // remove entry from our hashtable michael@0: mMemCacheEntries.RemoveEntry(entry); michael@0: michael@0: // remove entry from the eviction list michael@0: PR_REMOVE_AND_INIT_LINK(entry); michael@0: michael@0: // update statistics michael@0: int32_t memoryRecovered = (int32_t)entry->DataSize(); michael@0: mTotalSize -= memoryRecovered; michael@0: if (!entry->IsDoomed()) michael@0: mInactiveSize -= memoryRecovered; michael@0: --mEntryCount; michael@0: michael@0: if (deleteEntry) delete entry; michael@0: } michael@0: michael@0: michael@0: void michael@0: nsMemoryCacheDevice::EvictEntriesIfNecessary(void) michael@0: { michael@0: nsCacheEntry * entry; michael@0: nsCacheEntry * maxEntry; michael@0: CACHE_LOG_DEBUG(("EvictEntriesIfNecessary. mTotalSize: %d, mHardLimit: %d," michael@0: "mInactiveSize: %d, mSoftLimit: %d\n", michael@0: mTotalSize, mHardLimit, mInactiveSize, mSoftLimit)); michael@0: michael@0: if ((mTotalSize < mHardLimit) && (mInactiveSize < mSoftLimit)) michael@0: return; michael@0: michael@0: uint32_t now = SecondsFromPRTime(PR_Now()); michael@0: uint64_t entryCost = 0; michael@0: uint64_t maxCost = 0; michael@0: do { michael@0: // LRU-SP eviction selection: Check the head of each segment (each michael@0: // eviction list, kept in LRU order) and select the maximal-cost michael@0: // entry for eviction. Cost is time-since-accessed * size / nref. michael@0: maxEntry = 0; michael@0: for (int i = kQueueCount - 1; i >= 0; --i) { michael@0: entry = (nsCacheEntry *)PR_LIST_HEAD(&mEvictionList[i]); michael@0: michael@0: // If the head of a list is in use, check the next available entry michael@0: while ((entry != &mEvictionList[i]) && michael@0: (entry->IsInUse())) { michael@0: entry = (nsCacheEntry *)PR_NEXT_LINK(entry); michael@0: } michael@0: michael@0: if (entry != &mEvictionList[i]) { michael@0: entryCost = (uint64_t) michael@0: (now - entry->LastFetched()) * entry->DataSize() / michael@0: std::max(1, entry->FetchCount()); michael@0: if (!maxEntry || (entryCost > maxCost)) { michael@0: maxEntry = entry; michael@0: maxCost = entryCost; michael@0: } michael@0: } michael@0: } michael@0: if (maxEntry) { michael@0: EvictEntry(maxEntry, DELETE_ENTRY); michael@0: } else { michael@0: break; michael@0: } michael@0: } michael@0: while ((mTotalSize >= mHardLimit) || (mInactiveSize >= mSoftLimit)); michael@0: } michael@0: michael@0: michael@0: int michael@0: nsMemoryCacheDevice::EvictionList(nsCacheEntry * entry, int32_t deltaSize) michael@0: { michael@0: // favor items which never expire by putting them in the lowest-index queue michael@0: if (entry->ExpirationTime() == nsICache::NO_EXPIRATION_TIME) michael@0: return 0; michael@0: michael@0: // compute which eviction queue this entry should go into, michael@0: // based on floor(log2(size/nref)) michael@0: int32_t size = deltaSize + (int32_t)entry->DataSize(); michael@0: int32_t fetchCount = std::max(1, entry->FetchCount()); michael@0: michael@0: return std::min((int)mozilla::FloorLog2(size / fetchCount), kQueueCount - 1); michael@0: } michael@0: michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::Visit(nsICacheVisitor * visitor) michael@0: { michael@0: nsMemoryCacheDeviceInfo * deviceInfo = new nsMemoryCacheDeviceInfo(this); michael@0: nsCOMPtr deviceRef(deviceInfo); michael@0: if (!deviceInfo) return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: bool keepGoing; michael@0: nsresult rv = visitor->VisitDevice(gMemoryDeviceID, deviceInfo, &keepGoing); michael@0: if (NS_FAILED(rv)) return rv; michael@0: michael@0: if (!keepGoing) michael@0: return NS_OK; michael@0: michael@0: nsCacheEntry * entry; michael@0: nsCOMPtr entryRef; michael@0: michael@0: for (int i = kQueueCount - 1; i >= 0; --i) { michael@0: entry = (nsCacheEntry *)PR_LIST_HEAD(&mEvictionList[i]); michael@0: while (entry != &mEvictionList[i]) { michael@0: nsCacheEntryInfo * entryInfo = new nsCacheEntryInfo(entry); michael@0: if (!entryInfo) return NS_ERROR_OUT_OF_MEMORY; michael@0: entryRef = entryInfo; michael@0: michael@0: rv = visitor->VisitEntry(gMemoryDeviceID, entryInfo, &keepGoing); michael@0: entryInfo->DetachEntry(); michael@0: if (NS_FAILED(rv)) return rv; michael@0: if (!keepGoing) break; michael@0: michael@0: entry = (nsCacheEntry *)PR_NEXT_LINK(entry); michael@0: } michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: static bool michael@0: IsEntryPrivate(nsCacheEntry* entry, void* args) michael@0: { michael@0: return entry->IsPrivate(); michael@0: } michael@0: michael@0: struct ClientIDArgs { michael@0: const char* clientID; michael@0: uint32_t prefixLength; michael@0: }; michael@0: michael@0: static bool michael@0: EntryMatchesClientID(nsCacheEntry* entry, void* args) michael@0: { michael@0: const char * clientID = static_cast(args)->clientID; michael@0: uint32_t prefixLength = static_cast(args)->prefixLength; michael@0: const char * key = entry->Key()->get(); michael@0: return !clientID || nsCRT::strncmp(clientID, key, prefixLength) == 0; michael@0: } michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::DoEvictEntries(bool (*matchFn)(nsCacheEntry* entry, void* args), void* args) michael@0: { michael@0: nsCacheEntry * entry; michael@0: michael@0: for (int i = kQueueCount - 1; i >= 0; --i) { michael@0: PRCList * elem = PR_LIST_HEAD(&mEvictionList[i]); michael@0: while (elem != &mEvictionList[i]) { michael@0: entry = (nsCacheEntry *)elem; michael@0: elem = PR_NEXT_LINK(elem); michael@0: michael@0: if (!matchFn(entry, args)) michael@0: continue; michael@0: michael@0: if (entry->IsInUse()) { michael@0: nsresult rv = nsCacheService::DoomEntry(entry); michael@0: if (NS_FAILED(rv)) { michael@0: CACHE_LOG_WARNING(("memCache->DoEvictEntries() aborted: rv =%x", rv)); michael@0: return rv; michael@0: } michael@0: } else { michael@0: EvictEntry(entry, DELETE_ENTRY); michael@0: } michael@0: } michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::EvictEntries(const char * clientID) michael@0: { michael@0: ClientIDArgs args = {clientID, clientID ? uint32_t(strlen(clientID)) : 0}; michael@0: return DoEvictEntries(&EntryMatchesClientID, &args); michael@0: } michael@0: michael@0: nsresult michael@0: nsMemoryCacheDevice::EvictPrivateEntries() michael@0: { michael@0: return DoEvictEntries(&IsEntryPrivate, nullptr); michael@0: } michael@0: michael@0: michael@0: // WARNING: SetCapacity can get called before Init() michael@0: void michael@0: nsMemoryCacheDevice::SetCapacity(int32_t capacity) michael@0: { michael@0: int32_t hardLimit = capacity * 1024; // convert k into bytes michael@0: int32_t softLimit = (hardLimit * 9) / 10; michael@0: AdjustMemoryLimits(softLimit, hardLimit); michael@0: } michael@0: michael@0: void michael@0: nsMemoryCacheDevice::SetMaxEntrySize(int32_t maxSizeInKilobytes) michael@0: { michael@0: // Internal unit is 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: #ifdef DEBUG michael@0: static PLDHashOperator michael@0: CountEntry(PLDHashTable * table, PLDHashEntryHdr * hdr, uint32_t number, void * arg) michael@0: { michael@0: int32_t *entryCount = (int32_t *)arg; michael@0: ++(*entryCount); michael@0: return PL_DHASH_NEXT; michael@0: } michael@0: michael@0: void michael@0: nsMemoryCacheDevice::CheckEntryCount() michael@0: { michael@0: if (!mInitialized) return; michael@0: michael@0: int32_t evictionListCount = 0; michael@0: for (int i=0; i\n" michael@0: " Inactive storage:\n" michael@0: " "); michael@0: buffer.AppendInt(mDevice->mInactiveSize / 1024); michael@0: buffer.AppendLiteral(" KiB\n" michael@0: " \n"); michael@0: michael@0: *result = ToNewCString(buffer); michael@0: if (!*result) return NS_ERROR_OUT_OF_MEMORY; michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: NS_IMETHODIMP michael@0: nsMemoryCacheDeviceInfo::GetEntryCount(uint32_t * result) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(result); michael@0: // XXX compare calculated count vs. mEntryCount michael@0: *result = (uint32_t)mDevice->mEntryCount; michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: NS_IMETHODIMP michael@0: nsMemoryCacheDeviceInfo::GetTotalSize(uint32_t * result) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(result); michael@0: *result = (uint32_t)mDevice->mTotalSize; michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: NS_IMETHODIMP michael@0: nsMemoryCacheDeviceInfo::GetMaximumSize(uint32_t * result) michael@0: { michael@0: NS_ENSURE_ARG_POINTER(result); michael@0: *result = (uint32_t)mDevice->mHardLimit; michael@0: return NS_OK; michael@0: }