1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /* vim: set ts=8 sts=4 et sw=4 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include "nsMemoryCacheDevice.h"
9 #include "nsCacheService.h"
10 #include "nsICacheService.h"
11 #include "nsICacheVisitor.h"
12 #include "nsIStorageStream.h"
14 #include "nsReadableUtils.h"
15 #include "mozilla/MathAlgorithms.h"
16 #include "mozilla/Telemetry.h"
19 // The memory cache implements the "LRU-SP" caching algorithm
20 // described in "LRU-SP: A Size-Adjusted and Popularity-Aware LRU Replacement
21 // Algorithm for Web Caching" by Kai Cheng and Yahiko Kambayashi.
23 // We keep kQueueCount LRU queues, which should be about ceil(log2(mHardLimit))
24 // The queues hold exponentially increasing ranges of floor(log2((size/nref)))
25 // values for entries.
26 // Entries larger than 2^(kQueueCount-1) go in the last queue.
27 // Entries with no expiration go in the first queue.
29 const char *gMemoryDeviceID
= "memory";
32 nsMemoryCacheDevice::nsMemoryCacheDevice()
33 : mInitialized(false),
34 mHardLimit(4 * 1024 * 1024), // default, if no pref
35 mSoftLimit((mHardLimit
* 9) / 10), // default, if no pref
40 mMaxEntrySize(-1) // -1 means "no limit"
42 for (int i
=0; i
<kQueueCount
; ++i
)
43 PR_INIT_CLIST(&mEvictionList
[i
]);
47 nsMemoryCacheDevice::~nsMemoryCacheDevice()
54 nsMemoryCacheDevice::Init()
56 if (mInitialized
) return NS_ERROR_ALREADY_INITIALIZED
;
58 nsresult rv
= mMemCacheEntries
.Init();
59 mInitialized
= NS_SUCCEEDED(rv
);
65 nsMemoryCacheDevice::Shutdown()
67 NS_ASSERTION(mInitialized
, "### attempting shutdown while not initialized");
68 NS_ENSURE_TRUE(mInitialized
, NS_ERROR_NOT_INITIALIZED
);
70 mMemCacheEntries
.Shutdown();
73 nsCacheEntry
* entry
, * next
;
75 for (int i
= kQueueCount
- 1; i
>= 0; --i
) {
76 entry
= (nsCacheEntry
*)PR_LIST_HEAD(&mEvictionList
[i
]);
77 while (entry
!= &mEvictionList
[i
]) {
78 NS_ASSERTION(!entry
->IsInUse(), "### shutting down with active entries");
79 next
= (nsCacheEntry
*)PR_NEXT_LINK(entry
);
80 PR_REMOVE_AND_INIT_LINK(entry
);
83 int32_t memoryRecovered
= (int32_t)entry
->DataSize();
84 mTotalSize
-= memoryRecovered
;
85 mInactiveSize
-= memoryRecovered
;
94 * we're not factoring in changes to meta data yet...
95 * NS_ASSERTION(mTotalSize == 0, "### mem cache leaking entries?");
97 NS_ASSERTION(mInactiveSize
== 0, "### mem cache leaking entries?");
98 NS_ASSERTION(mEntryCount
== 0, "### mem cache leaking entries?");
100 mInitialized
= false;
107 nsMemoryCacheDevice::GetDeviceID()
109 return gMemoryDeviceID
;
114 nsMemoryCacheDevice::FindEntry(nsCString
* key
, bool *collision
)
116 mozilla::Telemetry::AutoTimer
<mozilla::Telemetry::CACHE_MEMORY_SEARCH_2
> timer
;
117 nsCacheEntry
* entry
= mMemCacheEntries
.GetEntry(key
);
118 if (!entry
) return nullptr;
120 // move entry to the tail of an eviction list
121 PR_REMOVE_AND_INIT_LINK(entry
);
122 PR_APPEND_LINK(entry
, &mEvictionList
[EvictionList(entry
, 0)]);
124 mInactiveSize
-= entry
->DataSize();
131 nsMemoryCacheDevice::DeactivateEntry(nsCacheEntry
* entry
)
133 CACHE_LOG_DEBUG(("nsMemoryCacheDevice::DeactivateEntry for entry 0x%p\n",
135 if (entry
->IsDoomed()) {
137 // XXX verify we've removed it from mMemCacheEntries & eviction list
140 CACHE_LOG_DEBUG(("deleted doomed entry 0x%p\n", entry
));
145 nsCacheEntry
* ourEntry
= mMemCacheEntries
.GetEntry(entry
->Key());
146 NS_ASSERTION(ourEntry
, "DeactivateEntry called for an entry we don't have!");
147 NS_ASSERTION(entry
== ourEntry
, "entry doesn't match ourEntry");
148 if (ourEntry
!= entry
)
149 return NS_ERROR_INVALID_POINTER
;
152 mInactiveSize
+= entry
->DataSize();
153 EvictEntriesIfNecessary();
160 nsMemoryCacheDevice::BindEntry(nsCacheEntry
* entry
)
162 if (!entry
->IsDoomed()) {
163 NS_ASSERTION(PR_CLIST_IS_EMPTY(entry
),"entry is already on a list!");
165 // append entry to the eviction list
166 PR_APPEND_LINK(entry
, &mEvictionList
[EvictionList(entry
, 0)]);
168 // add entry to hashtable of mem cache entries
169 nsresult rv
= mMemCacheEntries
.AddEntry(entry
);
171 PR_REMOVE_AND_INIT_LINK(entry
);
175 // add size of entry to memory totals
177 if (mMaxEntryCount
< mEntryCount
) mMaxEntryCount
= mEntryCount
;
179 mTotalSize
+= entry
->DataSize();
180 EvictEntriesIfNecessary();
188 nsMemoryCacheDevice::DoomEntry(nsCacheEntry
* entry
)
191 // debug code to verify we have entry
192 nsCacheEntry
* hashEntry
= mMemCacheEntries
.GetEntry(entry
->Key());
193 if (!hashEntry
) NS_WARNING("no entry for key");
194 else if (entry
!= hashEntry
) NS_WARNING("entry != hashEntry");
196 CACHE_LOG_DEBUG(("Dooming entry 0x%p in memory cache\n", entry
));
197 EvictEntry(entry
, DO_NOT_DELETE_ENTRY
);
202 nsMemoryCacheDevice::OpenInputStreamForEntry( nsCacheEntry
* entry
,
203 nsCacheAccessMode mode
,
205 nsIInputStream
** result
)
207 NS_ENSURE_ARG_POINTER(entry
);
208 NS_ENSURE_ARG_POINTER(result
);
210 nsCOMPtr
<nsIStorageStream
> storage
;
213 nsISupports
*data
= entry
->Data();
215 storage
= do_QueryInterface(data
, &rv
);
220 rv
= NS_NewStorageStream(4096, uint32_t(-1), getter_AddRefs(storage
));
223 entry
->SetData(storage
);
226 return storage
->NewInputStream(offset
, result
);
231 nsMemoryCacheDevice::OpenOutputStreamForEntry( nsCacheEntry
* entry
,
232 nsCacheAccessMode mode
,
234 nsIOutputStream
** result
)
236 NS_ENSURE_ARG_POINTER(entry
);
237 NS_ENSURE_ARG_POINTER(result
);
239 nsCOMPtr
<nsIStorageStream
> storage
;
242 nsISupports
*data
= entry
->Data();
244 storage
= do_QueryInterface(data
, &rv
);
249 rv
= NS_NewStorageStream(4096, uint32_t(-1), getter_AddRefs(storage
));
252 entry
->SetData(storage
);
255 return storage
->GetOutputStream(offset
, result
);
260 nsMemoryCacheDevice::GetFileForEntry( nsCacheEntry
* entry
,
263 return NS_ERROR_NOT_IMPLEMENTED
;
267 nsMemoryCacheDevice::EntryIsTooBig(int64_t entrySize
)
269 CACHE_LOG_DEBUG(("nsMemoryCacheDevice::EntryIsTooBig "
270 "[size=%d max=%d soft=%d]\n",
271 entrySize
, mMaxEntrySize
, mSoftLimit
));
272 if (mMaxEntrySize
== -1)
273 return entrySize
> mSoftLimit
;
275 return (entrySize
> mSoftLimit
|| entrySize
> mMaxEntrySize
);
279 nsMemoryCacheDevice::TotalSize()
285 nsMemoryCacheDevice::OnDataSizeChange( nsCacheEntry
* entry
, int32_t deltaSize
)
287 if (entry
->IsStreamData()) {
288 // we have the right to refuse or pre-evict
289 uint32_t newSize
= entry
->DataSize() + deltaSize
;
290 if (EntryIsTooBig(newSize
)) {
294 nsCacheService::DoomEntry(entry
);
295 NS_ASSERTION(NS_SUCCEEDED(rv
),"DoomEntry() failed.");
296 return NS_ERROR_ABORT
;
301 mTotalSize
+= deltaSize
;
303 if (!entry
->IsDoomed()) {
304 // move entry to the tail of the appropriate eviction list
305 PR_REMOVE_AND_INIT_LINK(entry
);
306 PR_APPEND_LINK(entry
, &mEvictionList
[EvictionList(entry
, deltaSize
)]);
309 EvictEntriesIfNecessary();
315 nsMemoryCacheDevice::AdjustMemoryLimits(int32_t softLimit
, int32_t hardLimit
)
317 mSoftLimit
= softLimit
;
318 mHardLimit
= hardLimit
;
320 // First, evict entries that won't fit into the new cache size.
321 EvictEntriesIfNecessary();
326 nsMemoryCacheDevice::EvictEntry(nsCacheEntry
* entry
, bool deleteEntry
)
328 CACHE_LOG_DEBUG(("Evicting entry 0x%p from memory cache, deleting: %d\n",
329 entry
, deleteEntry
));
330 // remove entry from our hashtable
331 mMemCacheEntries
.RemoveEntry(entry
);
333 // remove entry from the eviction list
334 PR_REMOVE_AND_INIT_LINK(entry
);
337 int32_t memoryRecovered
= (int32_t)entry
->DataSize();
338 mTotalSize
-= memoryRecovered
;
339 if (!entry
->IsDoomed())
340 mInactiveSize
-= memoryRecovered
;
343 if (deleteEntry
) delete entry
;
348 nsMemoryCacheDevice::EvictEntriesIfNecessary(void)
350 nsCacheEntry
* entry
;
351 nsCacheEntry
* maxEntry
;
352 CACHE_LOG_DEBUG(("EvictEntriesIfNecessary. mTotalSize: %d, mHardLimit: %d,"
353 "mInactiveSize: %d, mSoftLimit: %d\n",
354 mTotalSize
, mHardLimit
, mInactiveSize
, mSoftLimit
));
356 if ((mTotalSize
< mHardLimit
) && (mInactiveSize
< mSoftLimit
))
359 uint32_t now
= SecondsFromPRTime(PR_Now());
360 uint64_t entryCost
= 0;
361 uint64_t maxCost
= 0;
363 // LRU-SP eviction selection: Check the head of each segment (each
364 // eviction list, kept in LRU order) and select the maximal-cost
365 // entry for eviction. Cost is time-since-accessed * size / nref.
367 for (int i
= kQueueCount
- 1; i
>= 0; --i
) {
368 entry
= (nsCacheEntry
*)PR_LIST_HEAD(&mEvictionList
[i
]);
370 // If the head of a list is in use, check the next available entry
371 while ((entry
!= &mEvictionList
[i
]) &&
372 (entry
->IsInUse())) {
373 entry
= (nsCacheEntry
*)PR_NEXT_LINK(entry
);
376 if (entry
!= &mEvictionList
[i
]) {
377 entryCost
= (uint64_t)
378 (now
- entry
->LastFetched()) * entry
->DataSize() /
379 std::max(1, entry
->FetchCount());
380 if (!maxEntry
|| (entryCost
> maxCost
)) {
387 EvictEntry(maxEntry
, DELETE_ENTRY
);
392 while ((mTotalSize
>= mHardLimit
) || (mInactiveSize
>= mSoftLimit
));
397 nsMemoryCacheDevice::EvictionList(nsCacheEntry
* entry
, int32_t deltaSize
)
399 // favor items which never expire by putting them in the lowest-index queue
400 if (entry
->ExpirationTime() == nsICache::NO_EXPIRATION_TIME
)
403 // compute which eviction queue this entry should go into,
404 // based on floor(log2(size/nref))
405 int32_t size
= deltaSize
+ (int32_t)entry
->DataSize();
406 int32_t fetchCount
= std::max(1, entry
->FetchCount());
408 return std::min((int)mozilla::FloorLog2(size
/ fetchCount
), kQueueCount
- 1);
413 nsMemoryCacheDevice::Visit(nsICacheVisitor
* visitor
)
415 nsMemoryCacheDeviceInfo
* deviceInfo
= new nsMemoryCacheDeviceInfo(this);
416 nsCOMPtr
<nsICacheDeviceInfo
> deviceRef(deviceInfo
);
417 if (!deviceInfo
) return NS_ERROR_OUT_OF_MEMORY
;
420 nsresult rv
= visitor
->VisitDevice(gMemoryDeviceID
, deviceInfo
, &keepGoing
);
421 if (NS_FAILED(rv
)) return rv
;
426 nsCacheEntry
* entry
;
427 nsCOMPtr
<nsICacheEntryInfo
> entryRef
;
429 for (int i
= kQueueCount
- 1; i
>= 0; --i
) {
430 entry
= (nsCacheEntry
*)PR_LIST_HEAD(&mEvictionList
[i
]);
431 while (entry
!= &mEvictionList
[i
]) {
432 nsCacheEntryInfo
* entryInfo
= new nsCacheEntryInfo(entry
);
433 if (!entryInfo
) return NS_ERROR_OUT_OF_MEMORY
;
434 entryRef
= entryInfo
;
436 rv
= visitor
->VisitEntry(gMemoryDeviceID
, entryInfo
, &keepGoing
);
437 entryInfo
->DetachEntry();
438 if (NS_FAILED(rv
)) return rv
;
439 if (!keepGoing
) break;
441 entry
= (nsCacheEntry
*)PR_NEXT_LINK(entry
);
449 IsEntryPrivate(nsCacheEntry
* entry
, void* args
)
451 return entry
->IsPrivate();
454 struct ClientIDArgs
{
455 const char* clientID
;
456 uint32_t prefixLength
;
460 EntryMatchesClientID(nsCacheEntry
* entry
, void* args
)
462 const char * clientID
= static_cast<ClientIDArgs
*>(args
)->clientID
;
463 uint32_t prefixLength
= static_cast<ClientIDArgs
*>(args
)->prefixLength
;
464 const char * key
= entry
->Key()->get();
465 return !clientID
|| nsCRT::strncmp(clientID
, key
, prefixLength
) == 0;
469 nsMemoryCacheDevice::DoEvictEntries(bool (*matchFn
)(nsCacheEntry
* entry
, void* args
), void* args
)
471 nsCacheEntry
* entry
;
473 for (int i
= kQueueCount
- 1; i
>= 0; --i
) {
474 PRCList
* elem
= PR_LIST_HEAD(&mEvictionList
[i
]);
475 while (elem
!= &mEvictionList
[i
]) {
476 entry
= (nsCacheEntry
*)elem
;
477 elem
= PR_NEXT_LINK(elem
);
479 if (!matchFn(entry
, args
))
482 if (entry
->IsInUse()) {
483 nsresult rv
= nsCacheService::DoomEntry(entry
);
485 CACHE_LOG_WARNING(("memCache->DoEvictEntries() aborted: rv =%x", rv
));
489 EvictEntry(entry
, DELETE_ENTRY
);
498 nsMemoryCacheDevice::EvictEntries(const char * clientID
)
500 ClientIDArgs args
= {clientID
, clientID
? uint32_t(strlen(clientID
)) : 0};
501 return DoEvictEntries(&EntryMatchesClientID
, &args
);
505 nsMemoryCacheDevice::EvictPrivateEntries()
507 return DoEvictEntries(&IsEntryPrivate
, nullptr);
511 // WARNING: SetCapacity can get called before Init()
513 nsMemoryCacheDevice::SetCapacity(int32_t capacity
)
515 int32_t hardLimit
= capacity
* 1024; // convert k into bytes
516 int32_t softLimit
= (hardLimit
* 9) / 10;
517 AdjustMemoryLimits(softLimit
, hardLimit
);
521 nsMemoryCacheDevice::SetMaxEntrySize(int32_t maxSizeInKilobytes
)
523 // Internal unit is bytes. Changing this only takes effect *after* the
524 // change and has no consequences for existing cache-entries
525 if (maxSizeInKilobytes
>= 0)
526 mMaxEntrySize
= maxSizeInKilobytes
* 1024;
532 static PLDHashOperator
533 CountEntry(PLDHashTable
* table
, PLDHashEntryHdr
* hdr
, uint32_t number
, void * arg
)
535 int32_t *entryCount
= (int32_t *)arg
;
537 return PL_DHASH_NEXT
;
541 nsMemoryCacheDevice::CheckEntryCount()
543 if (!mInitialized
) return;
545 int32_t evictionListCount
= 0;
546 for (int i
=0; i
<kQueueCount
; ++i
) {
547 PRCList
* elem
= PR_LIST_HEAD(&mEvictionList
[i
]);
548 while (elem
!= &mEvictionList
[i
]) {
549 elem
= PR_NEXT_LINK(elem
);
553 NS_ASSERTION(mEntryCount
== evictionListCount
, "### mem cache badness");
555 int32_t entryCount
= 0;
556 mMemCacheEntries
.VisitEntries(CountEntry
, &entryCount
);
557 NS_ASSERTION(mEntryCount
== entryCount
, "### mem cache badness");
561 /******************************************************************************
562 * nsMemoryCacheDeviceInfo - for implementing about:cache
563 *****************************************************************************/
566 NS_IMPL_ISUPPORTS(nsMemoryCacheDeviceInfo
, nsICacheDeviceInfo
)
570 nsMemoryCacheDeviceInfo::GetDescription(char ** result
)
572 NS_ENSURE_ARG_POINTER(result
);
573 *result
= NS_strdup("Memory cache device");
574 if (!*result
) return NS_ERROR_OUT_OF_MEMORY
;
580 nsMemoryCacheDeviceInfo::GetUsageReport(char ** result
)
582 NS_ENSURE_ARG_POINTER(result
);
585 buffer
.AssignLiteral(" <tr>\n"
586 " <th>Inactive storage:</th>\n"
588 buffer
.AppendInt(mDevice
->mInactiveSize
/ 1024);
589 buffer
.AppendLiteral(" KiB</td>\n"
592 *result
= ToNewCString(buffer
);
593 if (!*result
) return NS_ERROR_OUT_OF_MEMORY
;
599 nsMemoryCacheDeviceInfo::GetEntryCount(uint32_t * result
)
601 NS_ENSURE_ARG_POINTER(result
);
602 // XXX compare calculated count vs. mEntryCount
603 *result
= (uint32_t)mDevice
->mEntryCount
;
609 nsMemoryCacheDeviceInfo::GetTotalSize(uint32_t * result
)
611 NS_ENSURE_ARG_POINTER(result
);
612 *result
= (uint32_t)mDevice
->mTotalSize
;
618 nsMemoryCacheDeviceInfo::GetMaximumSize(uint32_t * result
)
620 NS_ENSURE_ARG_POINTER(result
);
621 *result
= (uint32_t)mDevice
->mHardLimit
;