/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- * The contents of this file are subject to the Netscape Public * License Version 1.1 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.mozilla.org/NPL/ * * Software distributed under the License is distributed on an "AS * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or * implied. See the License for the specific language governing * rights and limitations under the License. * * The Original Code is Mozilla Communicator client code, released * March 31, 1998. * * The Initial Developer of the Original Code is Netscape * Communications Corporation. Portions created by Netscape are * Copyright (C) 1998-1999 Netscape Communications Corporation. All * Rights Reserved. * * Contributor(s): * Scott Furman, fur@netscape.com */ #include "nsReplacementPolicy.h" #include "nsCachedNetData.h" #include "nsQuickSort.h" #include "nsMemory.h" #include "nsIEnumerator.h" #include "prtime.h" #include "prinrval.h" #include "prbit.h" #include "nsCOMPtr.h" #include // Constant used to estimate frequency of access to a document based on size #define CACHE_CONST_B 1.35 #define CACHE_LOW_NUM_ENTRIES(entries) ((PRUint32)(0.75 * (entries))) nsReplacementPolicy::nsReplacementPolicy() : mRankedEntries(0), mCaches(0), mRecordsRemovedSinceLastRanking(0), mNumEntries(0), mCapacityRankedEntriesArray(0), mLastRankTime(0), mLoadedAllDatabaseRecords( PR_FALSE ) {} nsReplacementPolicy::~nsReplacementPolicy() { #ifdef NS_BUILD_REFCNT_LOGGING // #ifdef since this is a no-op unless NS_LOG_RELEASE does something. // Tell the refcount logging tools that all non-recycled entries // in the arena are going to go away with the arena, even though // they have a refcount of 1. PRUint32 i; for (i = 0; i < mNumEntries; i++) { nsCachedNetData* entry = mRankedEntries[i]; if (entry && !entry->GetFlag(nsCachedNetData::RECYCLED)) { NS_LOG_RELEASE(entry, 0, "nsCachedNetData"); } } #endif if (mRankedEntries) nsMemory::Free(mRankedEntries); if (mMapRecordIdToEntry) nsMemory::Free(mMapRecordIdToEntry); while (mCaches) { CacheInfo* nextCacheInfo = mCaches->mNext; delete mCaches; mCaches = nextCacheInfo; } } nsresult nsReplacementPolicy::Init(PRUint32 aMaxCacheEntries) { nsresult rv; rv = NS_NewHeapArena(getter_AddRefs(mArena), sizeof(nsCachedNetData) * 32); if (NS_FAILED(rv)) return rv; mMaxEntries = aMaxCacheEntries; // Hash array length must be power-of-two mHashArrayLength = (1 << PR_CeilingLog2(aMaxCacheEntries)) >> 3; size_t numBytes = mHashArrayLength * sizeof(*mMapRecordIdToEntry); mMapRecordIdToEntry = (nsCachedNetData**)nsMemory::Alloc(numBytes); if (!mMapRecordIdToEntry) return NS_ERROR_OUT_OF_MEMORY; nsCRT::zero(mMapRecordIdToEntry, numBytes); return NS_OK; } nsresult nsReplacementPolicy::AddCache(nsINetDataCache *aCache) { CacheInfo *cacheInfo = new CacheInfo(aCache); if (!cacheInfo) return NS_ERROR_OUT_OF_MEMORY; cacheInfo->mNext = mCaches; mCaches = cacheInfo; return NS_OK; } PRUint32 nsReplacementPolicy::HashRecordID(PRInt32 aRecordID) { return ((aRecordID >> 16) ^ aRecordID) & (mHashArrayLength - 1); } nsCachedNetData* nsReplacementPolicy::FindCacheEntryByRecordID(PRInt32 aRecordID, nsINetDataCache *aCache) { nsresult rv; nsCachedNetData* cacheEntry; PRUint32 bucket = HashRecordID(aRecordID); cacheEntry = mMapRecordIdToEntry[bucket]; for (;cacheEntry; cacheEntry = cacheEntry->mNext) { PRInt32 recordID; rv = cacheEntry->GetRecordID(&recordID); if (NS_FAILED(rv)) continue; if ((recordID == aRecordID) && (cacheEntry->mCache == aCache)) return cacheEntry; } return 0; } // Add a cache entry to the hash table that maps record ID to entries void nsReplacementPolicy::AddCacheEntry(nsCachedNetData* aCacheEntry, PRInt32 aRecordID) { nsCachedNetData** cacheEntryp; PRUint32 bucket = HashRecordID(aRecordID); cacheEntryp = &mMapRecordIdToEntry[bucket]; while (*cacheEntryp) cacheEntryp = &(*cacheEntryp)->mNext; *cacheEntryp = aCacheEntry; aCacheEntry->mNext = 0; } // Delete a cache entry from the hash table that maps record ID to entries nsresult nsReplacementPolicy::DeleteCacheEntry(nsCachedNetData* aCacheEntry) { nsresult rv; PRInt32 recordID; rv = aCacheEntry->GetRecordID(&recordID); if (NS_FAILED(rv)) return rv; PRUint32 bucket = HashRecordID(recordID); nsCachedNetData** cacheEntryp; cacheEntryp = &mMapRecordIdToEntry[bucket]; while (*cacheEntryp) { if (*cacheEntryp == aCacheEntry) { *cacheEntryp = aCacheEntry->mNext; return NS_OK; } cacheEntryp = &(*cacheEntryp)->mNext; } NS_ASSERTION(0, "hash table inconsistency"); return NS_ERROR_FAILURE; } nsresult nsReplacementPolicy::AddAllRecordsInCache(nsINetDataCache *aCache) { nsresult rv; nsCOMPtr iterator; nsCOMPtr iSupports; nsCOMPtr record; rv = aCache->NewCacheEntryIterator(getter_AddRefs(iterator)); if (!NS_SUCCEEDED(rv)) return rv; while (1) { PRBool notDone; rv = iterator->HasMoreElements(¬Done); if (NS_FAILED(rv)) return rv; if (!notDone) break; rv = iterator->GetNext(getter_AddRefs(iSupports)); if (!NS_SUCCEEDED(rv)) return rv; record = do_QueryInterface(iSupports); rv = AssociateCacheEntryWithRecord(record, aCache, 0); if (!NS_SUCCEEDED(rv)) return rv; } return NS_OK; } // Get current time and convert to seconds since the epoch static PRUint32 now32() { double nowFP; PRInt64 now64 = PR_Now(); LL_L2D(nowFP, now64); PRUint32 now = (PRUint32)(nowFP * 1e-6); return now; } void nsCachedNetData::NoteDownloadTime(PRIntervalTime start, PRIntervalTime end) { double rate; PRUint32 duration; duration = PR_IntervalToMilliseconds(end - start); // If the data arrives so fast that it can not be timed due to insufficient // clock granularity, assume a data arrival duration of 5 ms if (!duration) duration = 5; // Compute download rate in kB/s rate = mLogicalLength / (duration * (1.0e-3 * 1024.0)); if (mDownloadRate) { // Exponentially smooth download rate const double alpha = 0.5; mDownloadRate = (float)(mDownloadRate * alpha + rate * (1.0 - alpha)); } else { mDownloadRate = (float)rate; } } // 1 hour #define MIN_HALFLIFE (60 * 60) // 1 week #define TYPICAL_HALFLIFE (7 * 24 * 60 * 60) /** * Estimate the profit that would be lost if the given cache entry was evicted * from the cache. Profit is defined as the future expected download delay per * byte of cached content. The profit computation is made based on projected * frequency of access, prior download performance and a heuristic staleness * criteria. The technique used is a variation of that described in the * following paper: * * "A Case for Delay-Conscious Caching of Web Documents" * http://www.bell-labs.com/user/rvingral/www97.html * * Briefly, expected profit is: * * (projected frequency of access) * (download time per byte) * (probability freshness) */ void nsCachedNetData::ComputeProfit(PRUint32 aNow) { PRUint32 K, now; if (aNow) now = aNow; else now = now32(); K = PR_MIN(MAX_K, mNumAccesses); if (!K) { mProfit = 0; return; } // Compute time, in seconds, since k'th most recent access double timeSinceKthAccess = now - mAccessTime[K - 1]; if (timeSinceKthAccess <= 0.0) // Sanity check timeSinceKthAccess = 1.0; // Estimate frequency of future document access based on past // access frequency double frequencyAccess = K / timeSinceKthAccess; // If we don't have much historical data on access frequency // use a heuristic based on document size as an estimate if (mLogicalLength) { if (K == 1) { frequencyAccess /= pow(mLogicalLength, CACHE_CONST_B); } else if (K == 2) { frequencyAccess /= pow(mLogicalLength, CACHE_CONST_B / 2); } } // Estimate likelihood that data in cache is fresh, i.e. // that it corresponds to the document on the server double probabilityFreshness; PRInt32 halfLife, age, docTime; PRBool potentiallyStale; docTime = GetFlag(LAST_MODIFIED_KNOWN) ? mLastModifiedTime : mLastUpdateTime; age = now - docTime; probabilityFreshness = 1.0; // Optimistic if (GetFlag(EXPIRATION_KNOWN)) { potentiallyStale = now > mExpirationTime; halfLife = mExpirationTime - mLastModifiedTime; } else if (GetFlag(STALE_TIME_KNOWN)) { potentiallyStale = PR_TRUE; halfLife = mStaleTime - docTime; } else { potentiallyStale = PR_TRUE; halfLife = TYPICAL_HALFLIFE; } if (potentiallyStale) { if (halfLife < MIN_HALFLIFE) halfLife = MIN_HALFLIFE; probabilityFreshness = pow(0.5, (double)age / (double)halfLife); } mProfit = (float)(frequencyAccess * probabilityFreshness); if (mDownloadRate) mProfit /= mDownloadRate; } // Number of entries to grow mRankedEntries array when it's full #define STATS_GROWTH_INCREMENT 256 // Sorting predicate for NS_Quicksort int nsCachedNetData::Compare(const void *a, const void *b, void *unused) { nsCachedNetData* entryA = *(nsCachedNetData**)a; nsCachedNetData* entryB = *(nsCachedNetData**)b; // Percolate deleted or empty entries to the end of the mRankedEntries // array, so that they can be recycled. if (!entryA || entryA->GetFlag(RECYCLED)) { if (!entryB || entryB->GetFlag(RECYCLED)) return 0; else return +1; } if (!entryB || entryB->GetFlag(RECYCLED)) return -1; // Evicted entries (those with no content data) and active entries (those // currently being updated) are collected towards the end of the sorted // array just prior to the deleted cache entries, since evicted entries // can't be re-evicted. if (entryA->GetFlag(UPDATE_IN_PROGRESS)) { if (entryB->GetFlag(UPDATE_IN_PROGRESS)) return 0; else return +1; } if (entryB->GetFlag(UPDATE_IN_PROGRESS)) return -1; PRUint16 Ka = PR_MIN(MAX_K, entryA->mNumAccesses); PRUint16 Kb = PR_MIN(MAX_K, entryB->mNumAccesses); // Order cache entries by the number of times they've been accessed if (Ka < Kb) return -1; if (Ka > Kb) return +1; /* * Among records that have been accessed an equal number of times, order * them by profit. */ if (entryA->mProfit > entryB->mProfit) return +1; if (entryA->mProfit < entryB->mProfit) return -1; return 0; } /** * Rank cache entries in terms of their elegibility for eviction. */ nsresult nsReplacementPolicy::RankRecords() { PRUint32 i, now; // Get current time and convert to seconds since the epoch now = now32(); // Recompute profit for every known cache record, except deleted ones for (i = 0; i < mNumEntries; i++) { nsCachedNetData* entry = mRankedEntries[i]; if (entry && !entry->GetFlag(nsCachedNetData::RECYCLED)) entry->ComputeProfit(now); } NS_QuickSort(mRankedEntries, mNumEntries, sizeof *mRankedEntries, nsCachedNetData::Compare, 0); mNumEntries -= mRecordsRemovedSinceLastRanking; mRecordsRemovedSinceLastRanking = 0; mLastRankTime = now; return NS_OK; } // A heuristic policy to avoid the cost of re-ranking cache records by // profitability every single time space must be made available in the cache. void nsReplacementPolicy::MaybeRerankRecords() { // Rank at most once per minute PRUint32 now = now32(); if ((now - mLastRankTime) >= 60) RankRecords(); } void nsReplacementPolicy::CompactRankedEntriesArray() { if (mRecordsRemovedSinceLastRanking || !mLastRankTime) RankRecords(); } nsresult nsReplacementPolicy::CheckForTooManyCacheEntries() { PRUint32 undeletedEntries; PRUint32 maxEntries = 0; PRUint32 numEntriesDeleted = 0; PRBool deleteEntries = PR_FALSE; nsresult rv; CacheInfo *cacheInfo; cacheInfo = mCaches; undeletedEntries = mNumEntries - mRecordsRemovedSinceLastRanking; while (cacheInfo) { rv = cacheInfo->mCache->GetMaxEntries(&maxEntries); if (NS_FAILED(rv)) return rv; if (undeletedEntries >= (mMaxEntries-1)) { deleteEntries = PR_TRUE; } else { PRUint32 numEntries = 0; rv = cacheInfo->mCache->GetNumEntries(&numEntries); if (NS_FAILED(rv)) return rv; if (numEntries == maxEntries) { deleteEntries = PR_TRUE; } } if (deleteEntries) { return DeleteAtleastOneEntry(cacheInfo->mCache, CACHE_LOW_NUM_ENTRIES(maxEntries), &numEntriesDeleted); } deleteEntries = PR_FALSE; cacheInfo = cacheInfo->mNext; } return NS_OK; } /** * Create a new association between a low-level cache database record and a * cache entry. Add the entry to the set of entries eligible for eviction from * the cache. This would typically be done when the cache entry is created. */ nsresult nsReplacementPolicy::AssociateCacheEntryWithRecord(nsINetDataCacheRecord *aRecord, nsINetDataCache* aCache, nsCachedNetData** aResult) { nsCachedNetData* cacheEntry; nsresult rv; // First, see if the record is already known to the replacement policy PRInt32 recordID; rv = aRecord->GetRecordID(&recordID); if (NS_FAILED(rv)) return rv; cacheEntry = FindCacheEntryByRecordID(recordID, aCache); if (cacheEntry) { if (aResult) { if (cacheEntry->GetFlag(nsCachedNetData::DORMANT)) cacheEntry->Resurrect(aRecord); NS_ADDREF(cacheEntry); *aResult = cacheEntry; } return NS_OK; } // Compact the array of cache entry statistics, so that free entries appear // at the end, for possible reuse. if (mNumEntries && (mNumEntries == mCapacityRankedEntriesArray)) CompactRankedEntriesArray(); // If compaction doesn't yield available entries in the // mRankedEntries array, then extend the array. if (mNumEntries == mCapacityRankedEntriesArray) { PRUint32 newCapacity; newCapacity = mCapacityRankedEntriesArray + STATS_GROWTH_INCREMENT; if (newCapacity > mMaxEntries) newCapacity = mMaxEntries; nsCachedNetData** newRankedEntriesArray; PRUint32 numBytes = sizeof(nsCachedNetData*) * newCapacity; newRankedEntriesArray = (nsCachedNetData**)nsMemory::Realloc(mRankedEntries, numBytes); if (!newRankedEntriesArray) return NS_ERROR_OUT_OF_MEMORY; mRankedEntries = newRankedEntriesArray; mCapacityRankedEntriesArray = newCapacity; PRUint32 i; for (i = mNumEntries; i < newCapacity; i++) mRankedEntries[i] = 0; } // We should never hit this condition, should return otherwise it will cause an ABR if (mNumEntries >= mMaxEntries) return NS_ERROR_FAILURE; // Recycle the record after the last in-use record in the array nsCachedNetData *entry = mRankedEntries[mNumEntries]; NS_ASSERTION(!entry || entry->GetFlag(nsCachedNetData::RECYCLED), "Only deleted cache entries should appear at end of array"); if (!entry) { entry = new(mArena) nsCachedNetData; if (!entry) return NS_ERROR_OUT_OF_MEMORY; mRankedEntries[mNumEntries] = entry; } else { // Clear out recycled data structure entry = new(entry) nsCachedNetData; } entry->Init(aRecord, aCache); AddCacheEntry(entry, recordID); // Add one reference to the cache entry from the cache manager NS_ADDREF(entry); if (aResult) { // And one reference from our caller NS_ADDREF(entry); *aResult = entry; } mNumEntries++; return NS_OK; } nsresult nsReplacementPolicy::GetCachedNetData(const char* cacheKey, PRUint32 cacheKeyLength, nsINetDataCache* aCache, nsCachedNetData** aResult) { nsresult rv; nsCOMPtr record; // If number of tracked cache entries exceeds limits, delete one rv = CheckForTooManyCacheEntries(); if (NS_FAILED(rv)) return rv; rv = aCache->GetCachedNetData(cacheKey, cacheKeyLength, getter_AddRefs(record)); if (NS_FAILED(rv)) return rv; return AssociateCacheEntryWithRecord(record, aCache, aResult); } /** * Delete the atleast one desirable record from the cache database. This is used * when the addition of another record would exceed either the cache manager or * the cache's maximum permitted number of records. This method tries to reduce the * number of records in the cache database to targetNumEntries. targetNumEntriesReached * is set to PR_TRUE, if the nember of records has been reduced to targetNumEntries. * It returns NS_OK, if atleast one record has been succesfully deleted. */ nsresult nsReplacementPolicy::DeleteAtleastOneEntry(nsINetDataCache *aCache, PRUint32 targetNumEntries, PRUint32* numEntriesDeleted) { PRUint32 i; nsresult rv; nsCachedNetData *entry; PRBool atleastOneEntryDeleted = PR_FALSE; PRUint32 numRecordEntries = 0; *numEntriesDeleted = 0; if (!aCache) return NS_ERROR_FAILURE; rv = aCache->GetNumEntries(&numRecordEntries); if (NS_FAILED(rv)) return rv; /* Figure out if we are our end condition is number of entries or records */ if (targetNumEntries >= numRecordEntries) { if (targetNumEntries < mNumEntries) { numRecordEntries = mNumEntries; } else return NS_ERROR_FAILURE; } // It's not possible to rank cache entries by their profitability // until all of them are known to the replacement policy. rv = LoadAllRecordsInAllCacheDatabases(); if(NS_FAILED(rv)) { return rv ; } i = 0; MaybeRerankRecords(); for (i = 0; i < mNumEntries; i++) { entry = mRankedEntries[i]; if (!entry || entry->GetFlag(nsCachedNetData::RECYCLED) || (entry->mRefCnt > 1)) continue; if (entry->mCache == aCache) { rv = entry->Delete(); if (NS_SUCCEEDED(rv)) { rv = DeleteCacheEntry(entry); mRecordsRemovedSinceLastRanking++; atleastOneEntryDeleted = PR_TRUE; numRecordEntries--; *numEntriesDeleted = mRecordsRemovedSinceLastRanking; if (numRecordEntries <= targetNumEntries) { return rv; } } } } // Report error if no record found to delete if (i == mNumEntries) { if (atleastOneEntryDeleted) return NS_OK; else return NS_ERROR_FAILURE; } return NS_ERROR_FAILURE; } nsresult nsReplacementPolicy::GetStorageInUse(PRUint32* aStorageInUse) { nsresult rv; CacheInfo *cacheInfo; *aStorageInUse = 0; cacheInfo = mCaches; while (cacheInfo) { PRUint32 cacheStorage; rv = cacheInfo->mCache->GetStorageInUse(&cacheStorage); if (NS_FAILED(rv)) return rv; *aStorageInUse += cacheStorage; cacheInfo = cacheInfo->mNext; } return NS_OK; } // When true, all cache database records have been loaded into the // mRankedEntries array. Until this occurs, it is not possible to rank // cache entries against each other to determine which is the best // candidate for eviction from the cache. nsresult nsReplacementPolicy::LoadAllRecordsInAllCacheDatabases() { // We been here before ? if (mLoadedAllDatabaseRecords) return NS_OK; nsresult rv; CacheInfo *cacheInfo; cacheInfo = mCaches; while (cacheInfo) { rv = AddAllRecordsInCache(cacheInfo->mCache); if (NS_FAILED(rv)) { mLoadedAllDatabaseRecords = PR_FALSE; return rv; } cacheInfo = cacheInfo->mNext; } mLoadedAllDatabaseRecords = PR_TRUE; return RankRecords(); } /** * Delete the least desirable records from the cache until the occupancy of the * cache has been reduced by the given number of KB. This is used when the * addition of more cache data would exceed the cache's capacity. */ nsresult nsReplacementPolicy::Evict(PRUint32 aTargetOccupancy) { PRUint32 i; nsCachedNetData *entry; nsresult rv; PRUint32 occupancy; PRInt32 truncatedLength; nsCOMPtr record; // It's not possible to rank cache entries by their profitability // until all of them are known to the replacement policy. rv = LoadAllRecordsInAllCacheDatabases(); if(NS_FAILED(rv)) { return rv ; } MaybeRerankRecords(); for (i = 0; i < mNumEntries; i++) { rv = GetStorageInUse(&occupancy); if (!NS_SUCCEEDED(rv)) return rv; if (occupancy <= aTargetOccupancy) return NS_OK; entry = mRankedEntries[i]; // Skip deleted/empty cache entries and ones that have already been evicted if (!entry || entry->GetFlag(nsCachedNetData::UNEVICTABLE)) continue; // If the entry is not DORMANT then it cannot be evicted, so skip it.. if (!(entry->GetFlag(nsCachedNetData::DORMANT))) continue; if (entry->GetFlag(nsCachedNetData::ALLOW_PARTIAL)) { rv = entry->GetRecord(getter_AddRefs(record)); if (NS_FAILED(rv)) continue; PRUint32 contentLength; rv = record->GetStoredContentLength(&contentLength); if (NS_FAILED(rv)) continue; // Additional cache storage required, in KB PRUint32 storageToReclaim = (occupancy - aTargetOccupancy) << 10; truncatedLength = (PRInt32)(contentLength - storageToReclaim); if (truncatedLength < 0) truncatedLength = 0; } else { truncatedLength = 0; } rv = entry->Evict(truncatedLength); } if (occupancy <= aTargetOccupancy) return NS_OK; else return NS_ERROR_FAILURE; }