//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.Caching { using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Web; using System.Web.Util; using System.Collections; // ExpiresEntryRef defines a reference to a ExpiresEntry in the ExpiresBucket data structure. // An entry is identified by its index into the ExpiresBucket._pages array, and its // index into the ExpiresPage._entries array. // // Bytes 0-7 of the reference are for the index into the ExpiresPage._entries array. // Bytes 8-31 of the reference are for the index into the ExpiresBucket._pages array. // struct ExpiresEntryRef { // The invalid reference is 0. static internal readonly ExpiresEntryRef INVALID = new ExpiresEntryRef(0, 0); const uint ENTRY_MASK = 0x000000ffu; const uint PAGE_MASK = 0xffffff00u; const int PAGE_SHIFT = 8; uint _ref; internal ExpiresEntryRef(int pageIndex, int entryIndex) { Debug.Assert((pageIndex & 0x00ffffff) == pageIndex, "(pageIndex & 0x00ffffff) == pageIndex"); Debug.Assert((entryIndex & ENTRY_MASK) == entryIndex, "(entryIndex & ENTRY_MASK) == entryIndex"); Debug.Assert(entryIndex != 0 || pageIndex == 0, "entryIndex != 0 || pageIndex == 0"); _ref = ( (((uint)pageIndex) << PAGE_SHIFT) | (((uint)(entryIndex)) & ENTRY_MASK) ); } public override bool Equals(object value) { if (value is ExpiresEntryRef) { return _ref == ((ExpiresEntryRef)value)._ref; } return false; } #if NOT_USED public static bool Equals(ExpiresEntryRef r1, ExpiresEntryRef r2) { return r1._ref == r2._ref; } #endif public static bool operator !=(ExpiresEntryRef r1, ExpiresEntryRef r2) { return r1._ref != r2._ref; } public static bool operator ==(ExpiresEntryRef r1, ExpiresEntryRef r2) { return r1._ref == r2._ref; } public override int GetHashCode() { return (int) _ref; } #if DBG public override string ToString() { return PageIndex + ":" + Index; } #endif // The index into the ExpiresBucket._pages array. internal int PageIndex { get { int result = (int) (_ref >> PAGE_SHIFT); return result; } } // The index into the _entries array. internal int Index { get { int result = (int) (_ref & ENTRY_MASK); return result; } } // Is the reference invalid? internal bool IsInvalid { get { return _ref == 0; } } } // overhead is 12 bytes [StructLayout(LayoutKind.Explicit)] struct ExpiresEntry { // _utcExpires holds the expiration time of the item. // When an ExpiresEntry is not in use, _utcExpires is // no longer needed, so we can use its memory to hold // an index into the page's free list, and a count // of free entries and whether or not it is on an expires list. [FieldOffset(0)] internal DateTime _utcExpires; // expires [FieldOffset(0)] internal ExpiresEntryRef _next; // free list [FieldOffset(4)] internal int _cFree; // count of free entries in list at expires[0] // 1 if inFlush, 0 if free [FieldOffset(8)] internal CacheEntry _cacheEntry; // cache entry } // A page to hold the array of ExpiresEntry. struct ExpiresPage { internal ExpiresEntry[] _entries; // array of ExpiresEntry. internal int _pageNext; // next page on the free page or free entry list internal int _pagePrev; // prev page on the free page or free entry list } // A list of ExpiresPages. struct ExpiresPageList { internal int _head; // head of list internal int _tail; // tail of list } // // Class ExpiresBucket contains all the entries that expire within a given interval // every cycle of buckets. // // For example, if the inverval length is 20 seconds, and there are 60 buckets, // then bucket 4 will contain all items that expire between: // 00:01:20 and 00:01:40 // 00:21:20 and 00:21:40 // 00:41:20 and 00:41:40 // 01:01:20 and 01:01:40 // ... // // When we flush expired items, we need to examine every item in every bucket to // determine its expiration time. We potentially call FlushExpiredItems() every time // we need to trim items from the cache when there is memory pressure, which can // occur several times a second. // // To avoid a full scan on every call to FlushExpiredItems(), we maintain a count // of the items that expire within the first interval since the last full scan, and // only scan all entries in the bucket when there are items to flush. The first // interval is split into smaller intervals, and counts are kept of all items that need to // be flushed up to the time of that interval. // // Continuing with the example above, the first interval would be split into // 4 intervals of 5 seconds: // // If the last full scan occurred at 00:01:20 ... // _counts[0] contains the number of items expiring before 00:01:25 // _counts[1] contains the number of items expiring before 00:01:30 // _counts[2] contains the number of items expiring before 00:01:35 // _counts[3] contains the number of items expiring before 00:01:40 // // If there were 3 items in this bucket expiring at 00:01:27, 00:01:31, and 00:01:34 ... // _counts[0] == 0 // _counts[1] == 1 // _counts[2] == 3 // _counts[3] == 3 // // We further reduce the number of scans by keeping the minimum expiration time // of an item in the bucket. If FlushExpiredItems() is called before the minimum // expiration time, we do not need to perform the scan. Note that if the item with the // minimum expiration time is removed, we do not update the minimum expiration time, // so it is only a heuristic. // sealed class ExpiresBucket { // We use _entries[0] to hold the head of the free list, and the count of items in the free list. internal const int NUM_ENTRIES = 127; const int LENGTH_ENTRIES = 128; const int MIN_PAGES_INCREMENT = 10; const int MAX_PAGES_INCREMENT = 340; // (size of a page on x86) / (12 bytes per UsagePage) const double MIN_LOAD_FACTOR = 0.5; // minimum ratio of used to total entries before we will reduce const int COUNTS_LENGTH=4; // length of _counts array // timespan represented by each count entry static readonly TimeSpan COUNT_INTERVAL= new TimeSpan(CacheExpires._tsPerBucket.Ticks / COUNTS_LENGTH); readonly CacheExpires _cacheExpires; // parent CacheExpires object readonly byte _bucket; // index of this bucket ExpiresPage[] _pages; // page array #if IA64 volatile #endif int _cEntriesInUse; // count of ExpiresEntry's in use #if IA64 volatile #endif int _cPagesInUse; // count of ExpiresPage's in use #if IA64 volatile #endif int _cEntriesInFlush; // count of ExpiresEntry's in process of being flushed #if IA64 volatile #endif int _minEntriesInUse; // minimum number of entries in use before we reduce ExpiresPageList _freePageList; // list of free pages (_entries == null) ExpiresPageList _freeEntryList; // list of pages with free entries (entry.FreeCountCount > 0) #if IA64 volatile #endif bool _blockReduce; // block calls to Reduce() while in FlushExpiredItems // Minimum expiration date of the items in this bucket. // Note that it is only a heursitic - if an item that is // the minimum is removed, the minimum will not be updated. // The minimum is reset when the bucket is flushed. DateTime _utcMinExpires; // An exact count of the number of items that expire in the first period // since the last full scan. We use this to determine whether or not to // flush expired items when we are trimming items from the cache. int[] _counts; // The last time a flush occured, at which time we recalculate the counts. DateTime _utcLastCountReset; internal ExpiresBucket(CacheExpires cacheExpires, byte bucket, DateTime utcNow) { _cacheExpires = cacheExpires; _bucket = bucket; _counts = new int[COUNTS_LENGTH]; ResetCounts(utcNow); InitZeroPages(); Debug.Validate("CacheValidateExpires", this); } void InitZeroPages() { Debug.Assert(_cPagesInUse == 0, "_cPagesInUse == 0"); Debug.Assert(_cEntriesInUse == 0, "_cEntriesInUse == 0"); Debug.Assert(_cEntriesInFlush == 0, "_cEntriesInFlush == 0"); _pages = null; _minEntriesInUse = -1; _freePageList._head = -1; _freePageList._tail = -1; _freeEntryList._head = -1; _freeEntryList._tail = -1; } // Use macros so that the code is inlined in the function #define EntriesI(i) (_pages[(i)]._entries) #define EntriesR(entryRef) (_pages[(entryRef.PageIndex)]._entries) #define PagePrev(i) (_pages[(i)]._pagePrev) #define PageNext(i) (_pages[(i)]._pageNext) #define FreeEntryHead(entries) ((entries)[0]._next) #define FreeEntryCount(entries) ((entries)[0]._cFree) #if DBG bool EntryIsFree(ExpiresEntryRef entryRef) { return EntriesR(entryRef)[entryRef.Index]._cacheEntry == null; } #endif // Reset the counts array and min expiration time. void ResetCounts(DateTime utcNow) { _utcLastCountReset = utcNow; _utcMinExpires = DateTime.MaxValue; for (int i = 0; i < _counts.Length; i++) { _counts[i] = 0; } } // Return the index into the _counts array of all items that // expire by the given expiration time. Note that the index // may be larger than the length of the array. int GetCountIndex(DateTime utcExpires) { return Math.Max(0, (int) ((utcExpires - _utcLastCountReset).Ticks / COUNT_INTERVAL.Ticks)); } // Add counts for the expiration time. void AddCount(DateTime utcExpires) { int ci = GetCountIndex(utcExpires); for (int i = _counts.Length - 1; i >= ci; i--) { _counts[i]++; } if (utcExpires < _utcMinExpires) { _utcMinExpires = utcExpires; } } // Remove counts for the expiration time void RemoveCount(DateTime utcExpires) { int ci = GetCountIndex(utcExpires); for (int i = _counts.Length - 1; i >= ci; i--) { _counts[i]--; } } // Get the number of items that expire before utcExpires. int GetExpiresCount(DateTime utcExpires) { if (utcExpires < _utcMinExpires) return 0; int ci = GetCountIndex(utcExpires); if (ci >= _counts.Length) return _cEntriesInUse; return _counts[ci]; } // Add a page to the head of a list. void AddToListHead(int pageIndex, ref ExpiresPageList list) { Debug.Assert((list._head == -1) == (list._tail == -1), "(list._head == -1) == (list._tail == -1)"); PagePrev(pageIndex) = -1; PageNext(pageIndex) = list._head; if (list._head != -1) { Debug.Assert(PagePrev(list._head) == -1, "PagePrev(list._head) == -1"); PagePrev(list._head) = pageIndex; } else { list._tail = pageIndex; } list._head = pageIndex; } // Add a page to the tail of a list. void AddToListTail(int pageIndex, ref ExpiresPageList list) { Debug.Assert((list._head == -1) == (list._tail == -1), "(list._head == -1) == (list._tail == -1)"); PageNext(pageIndex) = -1; PagePrev(pageIndex) = list._tail; if (list._tail != -1) { Debug.Assert(PageNext(list._tail) == -1, "PageNext(list._tail) == -1"); PageNext(list._tail) = pageIndex; } else { list._head = pageIndex; } list._tail = pageIndex; } // Remove a page from the head of a list. int RemoveFromListHead(ref ExpiresPageList list) { Debug.Assert(list._head != -1, "list._head != -1"); int oldHead = list._head; RemoveFromList(oldHead, ref list); return oldHead; } // Remove a page from the list. void RemoveFromList(int pageIndex, ref ExpiresPageList list) { Debug.Assert((list._head == -1) == (list._tail == -1), "(list._head == -1) == (list._tail == -1)"); if (PagePrev(pageIndex) != -1) { Debug.Assert(PageNext(PagePrev(pageIndex)) == pageIndex, "PageNext(PagePrev(pageIndex)) == pageIndex"); PageNext(PagePrev(pageIndex)) = PageNext(pageIndex); } else { Debug.Assert(list._head == pageIndex, "list._head == pageIndex"); list._head = PageNext(pageIndex); } if (PageNext(pageIndex) != -1) { Debug.Assert(PagePrev(PageNext(pageIndex)) == pageIndex, "PagePrev(PageNext(pageIndex)) == pageIndex"); PagePrev(PageNext(pageIndex)) = PagePrev(pageIndex); } else { Debug.Assert(list._tail == pageIndex, "list._tail == pageIndex"); list._tail = PagePrev(pageIndex); } PagePrev(pageIndex) = -1; PageNext(pageIndex) = -1; } // Move a page to the head of the list void MoveToListHead(int pageIndex, ref ExpiresPageList list) { Debug.Assert(list._head != -1, "list._head != -1"); Debug.Assert(list._tail != -1, "list._tail != -1"); // already at head? if (list._head == pageIndex) return; // remove from list RemoveFromList(pageIndex, ref list); // add to head AddToListHead(pageIndex, ref list); } // Move to the tail of the list void MoveToListTail(int pageIndex, ref ExpiresPageList list) { Debug.Assert(list._head != -1, "list._head != -1"); Debug.Assert(list._tail != -1, "list._tail != -1"); // already at tail? if (list._tail == pageIndex) return; // remove from list RemoveFromList(pageIndex, ref list); // add to head AddToListTail(pageIndex, ref list); } // Update _minEntriesInUse when _cPagesInUse changes. // When _cEntriesInUse falls below _minEntriesInUse, // a call to Reduce() will consolidate entries onto fewer pages. // If _minEntries == -1, then a call to Reduce() will never reduce the number of pages. void UpdateMinEntries() { if (_cPagesInUse <= 1) { _minEntriesInUse = -1; } else { int capacity = _cPagesInUse * NUM_ENTRIES; Debug.Assert(capacity > 0, "capacity > 0"); Debug.Assert(MIN_LOAD_FACTOR < 1.0, "MIN_LOAD_FACTOR < 1.0"); _minEntriesInUse = (int) (capacity * MIN_LOAD_FACTOR); // Don't allow a reduce if there are not enough free entries to // remove a page. if ((_minEntriesInUse - 1) > ((_cPagesInUse - 1) * NUM_ENTRIES)) { _minEntriesInUse = -1; } } #if DBG if (Debug.IsTagPresent("CacheExpiresNoReduce") && Debug.IsTagEnabled("CacheExpiresNoReduce")) { _minEntriesInUse = -1; } #endif } // Remove a ExpiresPage that is in use, and put in on the list of free pages. void RemovePage(int pageIndex) { Debug.Assert(FreeEntryCount(EntriesI(pageIndex)) == NUM_ENTRIES, "FreeEntryCount(EntriesI(pageIndex)) == NUM_ENTRIES"); // release the page from the free entries list RemoveFromList(pageIndex, ref _freeEntryList); // Add the page to the free pages list AddToListHead(pageIndex, ref _freePageList); // remove reference to page Debug.Assert(EntriesI(pageIndex) != null, "EntriesI(pageIndex) != null"); EntriesI(pageIndex) = null; // decrement count of pages and update _cMinEntriesInUse _cPagesInUse--; if (_cPagesInUse == 0) { InitZeroPages(); } else { UpdateMinEntries(); } } // Get a free ExpiresEntry. ExpiresEntryRef GetFreeExpiresEntry() { // get the page of the free entry Debug.Assert(_freeEntryList._head >= 0, "_freeEntryList._head >= 0"); int pageIndex = _freeEntryList._head; // get a free entry from _entries ExpiresEntry[] entries = EntriesI(pageIndex); int entryIndex = FreeEntryHead(entries).Index; // fixup free list and count FreeEntryHead(entries) = entries[entryIndex]._next; FreeEntryCount(entries)--; if (FreeEntryCount(entries) == 0) { // remove page from list of free pages Debug.Assert(FreeEntryHead(entries).IsInvalid, "FreeEntryHead(entries).IsInvalid"); RemoveFromList(pageIndex, ref _freeEntryList); } #if DBG Debug.Assert(EntryIsFree(new ExpiresEntryRef(pageIndex, entryIndex)), "EntryIsFree(new ExpiresEntryRef(pageIndex, entryIndex))"); if (!FreeEntryHead(entries).IsInvalid) { Debug.Assert(FreeEntryHead(entries).Index != entryIndex, "FreeEntryHead(entries).Index != entryIndex"); Debug.Assert(EntryIsFree(new ExpiresEntryRef(pageIndex, FreeEntryHead(entries).Index)), "EntryIsFree(new ExpiresEntryRef(pageIndex, FreeEntryHead(entries).Index))"); } #endif return new ExpiresEntryRef(pageIndex, entryIndex); } // Add a ExpiresEntry to the free entry list. void AddExpiresEntryToFreeList(ExpiresEntryRef entryRef) { ExpiresEntry[] entries = EntriesR(entryRef); int entryIndex = entryRef.Index; Debug.Assert(entries[entryIndex]._cacheEntry == null, "entries[entryIndex]._cacheEntry == null"); entries[entryIndex]._cFree = 0; entries[entryIndex]._next = FreeEntryHead(entries); FreeEntryHead(entries) = entryRef; _cEntriesInUse--; int pageIndex = entryRef.PageIndex; FreeEntryCount(entries)++; if (FreeEntryCount(entries) == 1) { // add page to head of list of free pages AddToListHead(pageIndex, ref _freeEntryList); } else if (FreeEntryCount(entries) == NUM_ENTRIES) { RemovePage(pageIndex); } } // Expand the capacity of the ExpiresBucket to hold more CacheEntry's. // We will need to allocate a new page, and perhaps expand the _pages array. // Note that we never collapse the _pages array. void Expand() { Debug.Assert(_cPagesInUse * NUM_ENTRIES == _cEntriesInUse, "_cPagesInUse * NUM_ENTRIES == _cEntriesInUse"); Debug.Assert(_freeEntryList._head == -1, "_freeEntryList._head == -1"); Debug.Assert(_freeEntryList._tail == -1, "_freeEntryList._tail == -1"); // exapnd _pages if there are no more if (_freePageList._head == -1) { // alloc new pages array int oldLength; if (_pages == null) { oldLength = 0; } else { oldLength = _pages.Length; } Debug.Assert(_cPagesInUse == oldLength, "_cPagesInUse == oldLength"); Debug.Assert(_cEntriesInUse == oldLength * NUM_ENTRIES, "_cEntriesInUse == oldLength * ExpiresEntryRef.NUM_ENTRIES"); int newLength = oldLength * 2; newLength = Math.Max(oldLength + MIN_PAGES_INCREMENT, newLength); newLength = Math.Min(newLength, oldLength + MAX_PAGES_INCREMENT); Debug.Assert(newLength > oldLength, "newLength > oldLength"); ExpiresPage[] newPages = new ExpiresPage[newLength]; // copy original pages for (int i = 0; i < oldLength; i++) { newPages[i] = _pages[i]; } // setup free list of new pages for (int i = oldLength; i < newPages.Length; i++) { newPages[i]._pagePrev = i - 1; newPages[i]._pageNext = i + 1; } newPages[oldLength]._pagePrev = -1; newPages[newPages.Length - 1]._pageNext = -1; // use new pages array _freePageList._head = oldLength; _freePageList._tail = newPages.Length - 1; _pages = newPages; } // move from free page list to free entries list int pageIndex = RemoveFromListHead(ref _freePageList); AddToListHead(pageIndex, ref _freeEntryList); // create the entries ExpiresEntry[] entries = new ExpiresEntry[LENGTH_ENTRIES]; FreeEntryCount(entries) = NUM_ENTRIES; // init free list for (int i = 0; i < entries.Length - 1; i++) { entries[i]._next = new ExpiresEntryRef(pageIndex, i + 1); } entries[entries.Length - 1]._next = ExpiresEntryRef.INVALID; EntriesI(pageIndex) = entries; // increment count of pages and update _minEntriesInUse _cPagesInUse++; UpdateMinEntries(); } // Consolidate ExpiresEntry's onto fewer pages when there are too many // free entries. void Reduce() { // Test if we need to consolidate. if (_cEntriesInUse >= _minEntriesInUse || _blockReduce) return; Debug.Assert(_freeEntryList._head != -1, "_freeEntryList._head != -1"); Debug.Assert(_freeEntryList._tail != -1, "_freeEntryList._tail != -1"); Debug.Assert(_freeEntryList._head != _freeEntryList._tail, "_freeEntryList._head != _freeEntryList._tail"); // Rearrange free page list to put pages with more free entries at the tail int meanFree = (int) (NUM_ENTRIES - (NUM_ENTRIES * MIN_LOAD_FACTOR)); int pageIndexLast = _freeEntryList._tail; int pageIndexCurrent = _freeEntryList._head; int pageIndexNext; ExpiresEntry[] entries; for (;;) { pageIndexNext = PageNext(pageIndexCurrent); // move pages with greater than mean number // of free items to tail, move the others to head if (FreeEntryCount(EntriesI(pageIndexCurrent)) > meanFree) { MoveToListTail(pageIndexCurrent, ref _freeEntryList); } else { MoveToListHead(pageIndexCurrent, ref _freeEntryList); } // check if entire list has been examined if (pageIndexCurrent == pageIndexLast) break; // iterate pageIndexCurrent = pageIndexNext; } // Move entries from the free pages at the tail to the // free pages at the front, and release the free pages at the tail. for (;;) { // See if there is room left to move entries used by the page. if (_freeEntryList._tail == -1) break; entries = EntriesI(_freeEntryList._tail); Debug.Assert(FreeEntryCount(entries) > 0, "FreeEntryCount(entries) > 0"); int availableFreeEntries = (_cPagesInUse * NUM_ENTRIES) - FreeEntryCount(entries) - _cEntriesInUse; if (availableFreeEntries < (NUM_ENTRIES - FreeEntryCount(entries))) break; // Move each entry from the page at the tail to a page at the head. for (int i = 1; i < entries.Length; i++) { // skip the free entries if (entries[i]._cacheEntry == null) continue; // get a free ExpiresEntry from the head of the list. Debug.Assert(_freeEntryList._head != _freeEntryList._tail, "_freeEntryList._head != _freeEntryList._tail"); ExpiresEntryRef newRef = GetFreeExpiresEntry(); Debug.Assert(newRef.PageIndex != _freeEntryList._tail, "newRef.PageIndex != _freeEntryList._tail"); // update the CacheEntry CacheEntry cacheEntry = entries[i]._cacheEntry; #if DBG ExpiresEntryRef oldRef = new ExpiresEntryRef(_freeEntryList._tail, i); Debug.Assert(cacheEntry.ExpiresEntryRef == oldRef, "cacheEntry.ExpiresEntryRef == oldRef"); #endif cacheEntry.ExpiresEntryRef = newRef; // copy old entry to new entry ExpiresEntry[] newEntries = EntriesR(newRef); newEntries[newRef.Index] = entries[i]; // Update free entry count for debugging. We don't bother // to fix up the free entry list for this page as we are // going to release the page. FreeEntryCount(entries)++; } // now the page is free - release its memory RemovePage(_freeEntryList._tail); Debug.Validate("CacheValidateExpires", this); } } // Add an entry to the bucket. This may occur the first time an item is added to the cache, // or when the CacheEntry has a sliding expiration and is moved from one bucket to another. internal void AddCacheEntry(CacheEntry cacheEntry) { lock (this) { // Test if the item is still added to the cache. When CacheExpires.UtcUpdate() is called, // the item is removed from CacheExpires, and the CacheEntry could be removed by Cache.Remove() // on another thread while we are in this function. if ((cacheEntry.State & (CacheEntry.EntryState.AddedToCache | CacheEntry.EntryState.AddingToCache)) == 0) return; // If item is already added to a bucket, do nothing. ExpiresEntryRef entryRef = cacheEntry.ExpiresEntryRef; Debug.Assert((cacheEntry.ExpiresBucket == 0xff) == entryRef.IsInvalid, "(cacheEntry.ExpiresBucket == 0xff) == entryRef.IsInvalid"); if (cacheEntry.ExpiresBucket != 0xff || !entryRef.IsInvalid) return; // Expand if there are no free ExpiresEntry's available. if (_freeEntryList._head == -1) { Expand(); } // get the free entry ExpiresEntryRef freeRef = GetFreeExpiresEntry(); Debug.Assert(cacheEntry.ExpiresBucket == 0xff, "cacheEntry.ExpiresBucket == 0xff"); Debug.Assert(cacheEntry.ExpiresEntryRef.IsInvalid, "cacheEntry.ExpiresEntryRef.IsInvalid"); cacheEntry.ExpiresBucket = _bucket; cacheEntry.ExpiresEntryRef = freeRef; // initialize index ExpiresEntry[] entries = EntriesR(freeRef); int entryIndex = freeRef.Index; entries[entryIndex]._cacheEntry = cacheEntry; entries[entryIndex]._utcExpires = cacheEntry.UtcExpires; // update the count AddCount(cacheEntry.UtcExpires); _cEntriesInUse++; #if DBG { Debug.Trace("CacheExpiresAdd", "Added item=" + cacheEntry.Key + ",_bucket=" + _bucket + ",_ref=" + freeRef + ",now=" + Debug.FormatLocalDate(DateTime.Now) + ",expires=" + DateTimeUtil.ConvertToLocalTime(cacheEntry.UtcExpires)); Debug.Validate("CacheValidateExpires", this); Debug.Dump("CacheExpiresAdd", this); } #endif // Test again if the item is still added to the cache. When an update occurs, // the item is removed from CacheExpires, and the CacheEntry could be removed // on another thread while we are in this function. // Since we don't know whether or not CacheSingle.UpdateCache has called CacheExpires.Remove(), // we remove the item ourselves. RemoveCacheEntryNoLock protects itself against more than // one remove of the cache item. if ((cacheEntry.State & (CacheEntry.EntryState.AddedToCache | CacheEntry.EntryState.AddingToCache)) == 0) { RemoveCacheEntryNoLock(cacheEntry); } } } // Remove an item from the bucket. The caller must have a lock. void RemoveCacheEntryNoLock(CacheEntry cacheEntry) { ExpiresEntryRef entryRef = cacheEntry.ExpiresEntryRef; if (cacheEntry.ExpiresBucket != _bucket || entryRef.IsInvalid) return; ExpiresEntry[] entries = EntriesR(entryRef); int entryIndex = entryRef.Index; #if DBG Debug.Assert(cacheEntry == entries[entryIndex]._cacheEntry, "cacheEntry == entries[entryIndex]._cacheEntry"); #endif // update Count RemoveCount(entries[entryIndex]._utcExpires); // update the cache entry cacheEntry.ExpiresBucket = 0xff; cacheEntry.ExpiresEntryRef = ExpiresEntryRef.INVALID; entries[entryIndex]._cacheEntry = null; // add to free list AddExpiresEntryToFreeList(entryRef); // reset count if able if (_cEntriesInUse == 0) { ResetCounts(DateTime.UtcNow); } // remove pages if necessary Reduce(); Debug.Trace("CacheExpiresRemove", "Removed item=" + cacheEntry.Key + ",_bucket=" + _bucket + ",ref=" + entryRef + ",now=" + Debug.FormatLocalDate(DateTime.Now) + ",expires=" + DateTimeUtil.ConvertToLocalTime(cacheEntry.UtcExpires)); Debug.Validate("CacheValidateExpires", this); Debug.Dump("CacheExpiresRemove", this); } // Remove an item from the bucket. internal void RemoveCacheEntry(CacheEntry cacheEntry) { lock (this) { RemoveCacheEntryNoLock(cacheEntry); } } // Update the expiration time of a cache entry, and the count. internal void UtcUpdateCacheEntry(CacheEntry cacheEntry, DateTime utcExpires) { lock (this) { ExpiresEntryRef entryRef = cacheEntry.ExpiresEntryRef; if (cacheEntry.ExpiresBucket != _bucket || entryRef.IsInvalid) return; ExpiresEntry[] entries = EntriesR(entryRef); int entryIndex = entryRef.Index; Debug.Assert(cacheEntry == entries[entryIndex]._cacheEntry); // update count RemoveCount(entries[entryIndex]._utcExpires); AddCount(utcExpires); // update expires entry entries[entryIndex]._utcExpires = utcExpires; // update the cache entry cacheEntry.UtcExpires = utcExpires; Debug.Validate("CacheValidateExpires", this); Debug.Trace("CacheExpiresUpdate", "Updated item " + cacheEntry.Key + " in bucket " + _bucket); } } // Flush expired items from the cache. internal int FlushExpiredItems(DateTime utcNow, bool useInsertBlock) { // Check if there is something to flush if (_cEntriesInUse == 0 || GetExpiresCount(utcNow) == 0) return 0; Debug.Assert(_cEntriesInFlush == 0, "_cEntriesInFlush == 0"); // We create a list of ExpiresEntry's that we wish to flush. These entries // are not considered free, so the page that holds them will not be removed. ExpiresEntryRef inFlushHead = ExpiresEntryRef.INVALID; ExpiresEntry[] entries; int entryIndex; CacheEntry cacheEntry; int flushed = 0; try { if (useInsertBlock) { // Block insertion into the Cache if we're under high memory pressure _cacheExpires.CacheSingle.BlockInsertIfNeeded(); } lock (this) { Debug.Assert(_blockReduce == false, "_blockReduce == false"); // Recheck if there is something to flush. if (_cEntriesInUse == 0 || GetExpiresCount(utcNow) == 0) return 0; #if DBG Debug.Trace("CacheExpiresFlush", "FlushExpiredItems ExpiresCount=" + GetExpiresCount(utcNow) + " bucket=" + _bucket + "; Time=" + Debug.FormatLocalDate(DateTime.Now)); #endif // Walk through all ExpiresEntries, create a list of expired items, // and recalculate count heuristics. ResetCounts(utcNow); int cPages = _cPagesInUse; for (int i = 0; i < _pages.Length; i++) { entries = _pages[i]._entries; if (entries != null) { int cEntries = NUM_ENTRIES - FreeEntryCount(entries); for (int j = 1; j < entries.Length; j++) { cacheEntry = entries[j]._cacheEntry; if (cacheEntry != null) { if (entries[j]._utcExpires > utcNow) { AddCount(entries[j]._utcExpires); } else { // Remove reference from CacheEntry. We must do this before we // release the lock, otherwise the item would be corrupted if // UpdateCacheEntry or RemoveCacheEntry were called. cacheEntry.ExpiresBucket = 0xff; cacheEntry.ExpiresEntryRef = ExpiresEntryRef.INVALID; // distinguish between items on free list and inflush list for debugging entries[j]._cFree = 1; // add it to the inFlush list entries[j]._next = inFlushHead; inFlushHead = new ExpiresEntryRef(i, j); flushed++; _cEntriesInFlush++; } cEntries--; if (cEntries == 0) break; } } cPages--; if (cPages == 0) break; } } if (flushed == 0) { Debug.Trace("CacheExpiresFlushTotal", "FlushExpiredItems flushed " + flushed + " expired items, bucket=" + _bucket + "; Time=" + Debug.FormatLocalDate(DateTime.Now)); return 0; } // prevent pages from being moved by blocking Reduce(). _blockReduce = true; } } finally { if (useInsertBlock) { // Don't hold any insertblock before we remove Cache items. If not, the following // deadlock scenario may happen: // - 3rd party code hold lock A, call Cache.Insert, which wait for the Cache insertblock // - FlushExpiredItems holds the Cache insertBlock, call Cache.Remove, which call // 3rd party CacheItemRemovedCallback, which then try to get lock A _cacheExpires.CacheSingle.UnblockInsert(); } } Debug.Assert(!inFlushHead.IsInvalid, "!inFlushHead.IsInvalid"); // Remove items on the inFlush list from the rest of the cache. CacheSingle cacheSingle = _cacheExpires.CacheSingle; ExpiresEntryRef current = inFlushHead; ExpiresEntryRef next; while (!current.IsInvalid) { entries = EntriesR(current); entryIndex = current.Index; next = entries[entryIndex]._next; // remove the entry cacheEntry = entries[entryIndex]._cacheEntry; entries[entryIndex]._cacheEntry = null; Debug.Assert(cacheEntry.ExpiresEntryRef.IsInvalid, "cacheEntry.ExpiresEntryRef.IsInvalid"); cacheSingle.Remove(cacheEntry, CacheItemRemovedReason.Expired); //iterate current = next; } try { if (useInsertBlock) { // Block insertion into the Cache if we're under high memory pressure _cacheExpires.CacheSingle.BlockInsertIfNeeded(); } lock (this) { // add each ExpiresEntry to the free list current = inFlushHead; while (!current.IsInvalid) { entries = EntriesR(current); entryIndex = current.Index; next = entries[entryIndex]._next; _cEntriesInFlush--; AddExpiresEntryToFreeList(current); //iterate current = next; } // try to reduce Debug.Assert(_cEntriesInFlush == 0, "_cEntriesInFlush == 0"); _blockReduce = false; Reduce(); Debug.Trace("CacheExpiresFlushTotal", "FlushExpiredItems flushed " + flushed + " expired items, bucket=" + _bucket + "; Time=" + Debug.FormatLocalDate(DateTime.Now)); Debug.Validate("CacheValidateExpires", this); Debug.Dump("CacheExpiresFlush", this); } } finally { if (useInsertBlock) { _cacheExpires.CacheSingle.UnblockInsert(); } } return flushed; } #if DBG internal void DebugValidate() { int cFree = 0; int cEntriesInUse = 0; int cPagesInUse = 0; int pagesLength; int[] counts = new int[COUNTS_LENGTH]; if (_pages == null) { pagesLength = 0; } else { pagesLength = _pages.Length; } Debug.CheckValid(-1 <= _freePageList._head && _freePageList._head <= pagesLength, "-1 <= _freePageList._head && _freePageList._head <= pagesLength"); Debug.CheckValid(-1 <= _freeEntryList._head && _freeEntryList._head <= pagesLength, "-1 <= _freeEntryList._head && _freeEntryList._head <= pagesLength"); Debug.CheckValid(-1 <= _freeEntryList._tail && _freeEntryList._tail <= pagesLength, "-1 <= _freeEntryList._tail && _freeEntryList._tail <= pagesLength"); Debug.CheckValid((_freeEntryList._head == -1) == (_freeEntryList._tail == -1), "(_freeEntryList._head == -1) == (_freeEntryList._tail == -1)"); Debug.CheckValid(_minEntriesInUse >= -1, "_minEntriesInUse >= -1"); Debug.CheckValid(_cEntriesInFlush >= 0, "_cEntriesInFlush >= 0"); // check counts for (int i = 0; i < pagesLength; i++) { ExpiresEntry[] entries = _pages[i]._entries; if (entries != null) { cPagesInUse++; cFree = 0; Debug.CheckValid(entries[0]._cacheEntry == null, "entries[0]._cacheEntry == null"); for (int j = 1; j < entries.Length; j++) { if (entries[j]._cacheEntry == null && entries[j]._cFree == 0) { cFree++; } else { cEntriesInUse++; } if (entries[j]._cacheEntry != null && entries[j]._cFree != 1) { int ci = GetCountIndex(entries[j]._utcExpires); for (int k = _counts.Length - 1; k >= ci; k--) { counts[k]++; } } } Debug.CheckValid(cFree == FreeEntryCount(entries), "cFree == FreeEntryCount(entries)"); // walk the free list cFree = 0; if (!FreeEntryHead(entries).IsInvalid) { int j = FreeEntryHead(entries).Index; for (;;) { cFree++; Debug.CheckValid(cFree <= FreeEntryCount(entries), "cFree <= FreeEntryCount(entries)"); if (entries[j]._next.IsInvalid) break; j = entries[j]._next.Index; } } Debug.CheckValid(cFree == FreeEntryCount(entries), "cFree == FreeEntryCount(entries)"); } } Debug.CheckValid(cPagesInUse == _cPagesInUse, "cPagesInUse == _cPagesInUse"); Debug.CheckValid(cEntriesInUse == _cEntriesInUse, "cEntriesInUse == _cEntriesInUse"); for (int i = 0; i < _counts.Length; i++) { Debug.CheckValid(_counts[i] == counts[i], "_counts[i] == counts[i]"); } // walk the free slot list int cFreeSlots = 0; if (_freePageList._head != -1) { for (int i = _freePageList._head; i != -1; i = _pages[i]._pageNext) { cFreeSlots++; Debug.CheckValid(cFreeSlots <= pagesLength, "cFreeSlots <= pagesLength"); Debug.CheckValid(_pages[i]._entries == null, "_pages[i]._entries == null"); if (_freePageList._head != i) { Debug.CheckValid(PageNext(PagePrev(i)) == i, "PageNext(PagePrev(i)) == i"); } if (_freePageList._tail != i) { Debug.CheckValid(PagePrev(PageNext(i)) == i, "PagePrev(PageNext(i)) == i"); } } } Debug.CheckValid(cFreeSlots == pagesLength - _cPagesInUse, "cFreeSlots == pagesLength - _cPagesInUse"); // walk the free page list int cFreeEntries = 0; int cFreePages = 0; if (_freeEntryList._head != -1) { for (int i = _freeEntryList._head; i != -1; i = _pages[i]._pageNext) { cFreePages++; Debug.CheckValid(cFreePages <= pagesLength, "cFreePages < pagesLength"); ExpiresEntry[] entries = _pages[i]._entries; Debug.CheckValid(entries != null, "entries != null"); cFreeEntries += FreeEntryCount(entries); if (_freeEntryList._head != i) { Debug.CheckValid(PageNext(PagePrev(i)) == i, "PageNext(PagePrev(i)) == i"); } if (_freeEntryList._tail != i) { Debug.CheckValid(PagePrev(PageNext(i)) == i, "PagePrev(PageNext(i)) == i"); } } } Debug.CheckValid(cFreeEntries == (_cPagesInUse * NUM_ENTRIES) - _cEntriesInUse, "cFreeEntries == (_cPagesInUse * NUM_ENTRIES) - _cEntriesInUse"); } internal string DebugDescription(string indent) { StringBuilder sb = new StringBuilder(); string i2 = indent + " "; sb.Append(indent + "_bucket=" + _bucket + ",_cEntriesInUse=" + _cEntriesInUse + ",_cPagesInUse=" + _cPagesInUse + ",_pages is " + (_pages == null ? "null" : "non-null") + ",_minEntriesInUse=" + _minEntriesInUse + ",_freePageList._head=" + _freePageList._head + ",_freeEntryList._head=" + _freeEntryList._head + ",_freeEntryList._tail=" + _freeEntryList._tail + "\n"); return sb.ToString(); } #endif } /* * Provides an expiration service for entries in the cache. * Items with expiration times are placed into a configurable * number of buckets. Each minute a bucket is examined for * expired items. */ sealed class CacheExpires { internal static readonly TimeSpan MIN_UPDATE_DELTA = new TimeSpan(0, 0, 1); internal static readonly TimeSpan MIN_FLUSH_INTERVAL = new TimeSpan(0, 0, 1); internal static readonly TimeSpan _tsPerBucket = new TimeSpan(0, 0, 20); const int NUMBUCKETS = 30; static readonly TimeSpan _tsPerCycle = new TimeSpan(NUMBUCKETS * _tsPerBucket.Ticks); readonly CacheSingle _cacheSingle; readonly ExpiresBucket[] _buckets; DisposableGCHandleRef _timerHandleRef; DateTime _utcLastFlush; int _inFlush; internal CacheExpires(CacheSingle cacheSingle) { Debug.Assert(NUMBUCKETS < Byte.MaxValue); DateTime utcNow = DateTime.UtcNow; _cacheSingle = cacheSingle; _buckets = new ExpiresBucket[NUMBUCKETS]; for (byte b = 0; b < _buckets.Length; b++) { _buckets[b] = new ExpiresBucket(this, b, utcNow); } } int UtcCalcExpiresBucket(DateTime utcDate) { long ticksFromCycleStart = utcDate.Ticks % _tsPerCycle.Ticks; int bucket = (int) (((ticksFromCycleStart / _tsPerBucket.Ticks) + 1) % NUMBUCKETS); return bucket; } int FlushExpiredItems(bool checkDelta, bool useInsertBlock) { int flushed = 0; if (Interlocked.Exchange(ref _inFlush, 1) == 0) { try { // if the timer was disposed, return without doing anything if (_timerHandleRef == null) { return 0; } DateTime utcNow = DateTime.UtcNow; if (!checkDelta || utcNow - _utcLastFlush >= MIN_FLUSH_INTERVAL || utcNow < _utcLastFlush) { _utcLastFlush = utcNow; foreach (ExpiresBucket bucket in _buckets) { flushed += bucket.FlushExpiredItems(utcNow, useInsertBlock); } Debug.Trace("CacheExpiresFlushTotal", "FlushExpiredItems flushed a total of " + flushed + " items; Time=" + Debug.FormatLocalDate(DateTime.Now)); Debug.Dump("CacheExpiresFlush", this); } } finally { Interlocked.Exchange(ref _inFlush, 0); } } return flushed; } internal int FlushExpiredItems(bool useInsertBlock) { return FlushExpiredItems(true, useInsertBlock); } void TimerCallback(object state) { FlushExpiredItems(false, false); } internal void EnableExpirationTimer(bool enable) { #if DBG if (Debug.IsTagPresent("Timer") && !Debug.IsTagEnabled("Timer")) { enable = false; } #endif if (enable) { if (_timerHandleRef == null) { DateTime utcNow = DateTime.UtcNow; TimeSpan due = _tsPerBucket - (new TimeSpan(utcNow.Ticks % _tsPerBucket.Ticks)); Timer timer = new Timer(new TimerCallback(this.TimerCallback), null, due.Ticks / TimeSpan.TicksPerMillisecond, _tsPerBucket.Ticks / TimeSpan.TicksPerMillisecond); _timerHandleRef = new DisposableGCHandleRef(timer); Debug.Trace("Cache", "Cache expiration timer created."); } } else { DisposableGCHandleRef timerHandleRef = _timerHandleRef; if (timerHandleRef != null && Interlocked.CompareExchange(ref _timerHandleRef, null, timerHandleRef) == timerHandleRef) { timerHandleRef.Dispose(); Debug.Trace("Cache", "Cache expiration timer disposed."); while (_inFlush != 0) { Thread.Sleep(100); } } } } internal CacheSingle CacheSingle { get { return _cacheSingle; } } /* * Adds an entry to the expires list. * * @param entry The cache entry to add. */ internal void Add(CacheEntry cacheEntry) { DateTime utcNow = DateTime.UtcNow; if (utcNow > cacheEntry.UtcExpires) { cacheEntry.UtcExpires = utcNow; } int bucket = UtcCalcExpiresBucket(cacheEntry.UtcExpires); _buckets[bucket].AddCacheEntry(cacheEntry); } /* * Removes an entry from the expires list. * * @param entry The cache entry to remove. */ internal void Remove(CacheEntry cacheEntry) { byte bucket = cacheEntry.ExpiresBucket; if (bucket != 0xff) { _buckets[bucket].RemoveCacheEntry(cacheEntry); } } /* * Updates an entry. * */ internal void UtcUpdate(CacheEntry cacheEntry, DateTime utcNewExpires) { int oldBucket = cacheEntry.ExpiresBucket; int newBucket = UtcCalcExpiresBucket(utcNewExpires); if (oldBucket != newBucket) { Debug.Trace("CacheExpiresUpdate", "Updating item " + cacheEntry.Key + " from bucket " + oldBucket + " to new bucket " + newBucket); if (oldBucket != 0xff) { _buckets[oldBucket].RemoveCacheEntry(cacheEntry); cacheEntry.UtcExpires = utcNewExpires; _buckets[newBucket].AddCacheEntry(cacheEntry); } } else { if (oldBucket != 0xff) { _buckets[oldBucket].UtcUpdateCacheEntry(cacheEntry, utcNewExpires); } } } #if DBG internal void DebugValidate() { int i; for (i = 0; i < _buckets.Length; i++) { _buckets[i].DebugValidate(); } } internal string DebugDescription(string indent) { int i; StringBuilder sb = new StringBuilder(); string i2 = indent + " "; sb.Append(indent); sb.Append("Cache expires\n"); for (i = 0; i < _buckets.Length; i++) { sb.Append(_buckets[i].DebugDescription(i2)); } return sb.ToString(); } #endif } }