1329 lines
54 KiB
Plaintext
1329 lines
54 KiB
Plaintext
|
//------------------------------------------------------------------------------
|
||
|
// <copyright file="CacheExpires.cs" company="Microsoft">
|
||
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||
|
// </copyright>
|
||
|
//------------------------------------------------------------------------------
|
||
|
|
||
|
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<Timer> _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>(timer);
|
||
|
|
||
|
Debug.Trace("Cache", "Cache expiration timer created.");
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
DisposableGCHandleRef<Timer> 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
|
||
|
}
|
||
|
}
|
||
|
|