1 //------------------------------------------------------------------------------
2 // <copyright file="QueryCacheManager.cs" company="Microsoft">
3 // Copyright (c) Microsoft Corporation. All rights reserved.
7 // @backupOwner Microsoft
8 //------------------------------------------------------------------------------
10 namespace System
.Data
.Common
.QueryCache
13 using System
.Collections
.Generic
;
14 using System
.Data
.EntityClient
;
15 using System
.Data
.Metadata
.Edm
;
16 using System
.Data
.Objects
.Internal
;
17 using System
.Diagnostics
;
18 using System
.Threading
;
19 using System
.Data
.Common
.Internal
.Materialization
;
20 using System
.Data
.Entity
.Util
;
23 /// Provides Query Execution Plan Caching Service
27 /// Dispose <b>must</b> be called as there is no finalizer for this class
29 internal class QueryCacheManager
: IDisposable
31 #region Constants/Default values for configuration parameters
34 /// Default high mark for starting sweeping process
35 /// default value: 80% of MaxNumberOfEntries
37 const float DefaultHighMarkPercentageFactor
= 0.8f
; // 80% of MaxLimit
40 /// Recycler timer period
42 const int DefaultRecyclerPeriodInMilliseconds
= 60 * 1000;
51 private readonly object _cacheDataLock
= new object();
56 private readonly Dictionary
<QueryCacheKey
, QueryCacheEntry
> _cacheData
= new Dictionary
<QueryCacheKey
, QueryCacheEntry
>(32);
59 /// soft maximum number of entries in the cache
61 private readonly int _maxNumberOfEntries
;
64 /// high mark of the number of entries to trigger the sweeping process
66 private readonly int _sweepingTriggerHighMark
;
71 private readonly EvictionTimer _evictionTimer
;
75 #region Construction and Initialization
78 /// Constructs a new Query Cache Manager instance, with default values for all 'configurable' parameters.
80 /// <returns>A new instance of <see cref="QueryCacheManager"/> configured with default entry count, load factor and recycle period</returns>
81 internal static QueryCacheManager
Create()
83 return new QueryCacheManager(AppSettings
.QueryCacheSize
, DefaultHighMarkPercentageFactor
, DefaultRecyclerPeriodInMilliseconds
);
89 /// <param name="maximumSize">
90 /// Maximum number of entries that the cache should contain.
92 /// <param name="loadFactor">
93 /// The number of entries that must be present, as a percentage, before entries should be removed
94 /// according to the eviction policy.
95 /// Must be greater than 0 and less than or equal to 1.0
97 /// <param name="recycleMillis">
98 /// The interval, in milliseconds, at which the number of entries will be compared to the load factor
99 /// and eviction carried out if necessary.
101 private QueryCacheManager(int maximumSize
, float loadFactor
, int recycleMillis
)
103 Debug
.Assert(maximumSize
> 0, "Maximum size must be greater than zero");
104 Debug
.Assert(loadFactor
> 0 && loadFactor
<= 1, "Load factor must be greater than 0.0 and less than or equal to 1.0");
105 Debug
.Assert(recycleMillis
>= 0, "Recycle period in milliseconds must not be negative");
108 // Load hardcoded defaults
110 this._maxNumberOfEntries
= maximumSize
;
113 // set sweeping high mark trigger value
115 this._sweepingTriggerHighMark
= (int)(_maxNumberOfEntries
* loadFactor
);
118 // Initialize Recycler
120 this._evictionTimer
= new EvictionTimer(this, recycleMillis
);
125 #region 'External' interface
127 /// Adds new entry to the cache using "abstract" cache context and
128 /// value; returns an existing entry if the key is already in the
131 /// <param name="inQueryCacheEntry"></param>
132 /// <param name="outQueryCacheEntry">
133 /// The existing entry in the dicitionary if already there;
134 /// inQueryCacheEntry if none was found and inQueryCacheEntry
135 /// was added instead.
137 /// <returns>true if the output entry was already found; false if it had to be added.</returns>
138 internal bool TryLookupAndAdd(QueryCacheEntry inQueryCacheEntry
, out QueryCacheEntry outQueryCacheEntry
)
140 Debug
.Assert(null != inQueryCacheEntry
, "qEntry must not be null");
142 outQueryCacheEntry
= null;
144 lock (_cacheDataLock
)
146 if (!_cacheData
.TryGetValue(inQueryCacheEntry
.QueryCacheKey
, out outQueryCacheEntry
))
149 // add entry to cache data
151 _cacheData
.Add(inQueryCacheEntry
.QueryCacheKey
, inQueryCacheEntry
);
152 if (_cacheData
.Count
> _sweepingTriggerHighMark
)
154 _evictionTimer
.Start();
161 outQueryCacheEntry
.QueryCacheKey
.UpdateHit();
169 /// Lookup service for a cached value.
171 internal bool TryCacheLookup
<TK
, TE
>(TK key
, out TE
value)
172 where TK
: QueryCacheKey
174 Debug
.Assert(null != key
, "key must not be null");
179 // invoke internal lookup
181 QueryCacheEntry qEntry
= null;
182 bool bHit
= TryInternalCacheLookup(key
, out qEntry
);
185 // if it is a hit, 'extract' the entry strong type cache value
189 value = (TE
)qEntry
.GetTarget();
198 internal void Clear()
200 lock (_cacheDataLock
)
207 #region Private Members
212 /// <param name="queryCacheKey"></param>
213 /// <param name="queryCacheEntry"></param>
214 /// <returns>true if cache hit, false if cache miss</returns>
215 private bool TryInternalCacheLookup( QueryCacheKey queryCacheKey
, out QueryCacheEntry queryCacheEntry
)
217 Debug
.Assert(null != queryCacheKey
, "queryCacheKey must not be null");
219 queryCacheEntry
= null;
224 // lock the cache for the minimal possible period
226 lock (_cacheDataLock
)
228 bHit
= _cacheData
.TryGetValue(queryCacheKey
, out queryCacheEntry
);
237 // update hit mark in cache key
239 queryCacheEntry
.QueryCacheKey
.UpdateHit();
247 /// Recycler handler. This method is called directly by the eviction timer.
248 /// It should take no action beyond invoking the <see cref="SweepCache"/> method on the
249 /// cache manager instance passed as <paramref name="state"/>.
251 /// <param name="state">The cache manager instance on which the 'recycle' handler should be invoked</param>
252 private static void CacheRecyclerHandler(object state
)
254 ((QueryCacheManager
)state
).SweepCache();
260 private static readonly int[] _agingFactor
= {1,1,2,4,8,16}
;
261 private static readonly int AgingMaxIndex
= _agingFactor
.Length
- 1;
264 /// Sweeps the cache removing old unused entries.
265 /// This method implements the query cache eviction policy.
267 private void SweepCache()
269 if (!this._evictionTimer
.Suspend())
271 // Return of false from .Suspend means that the manager and timer have been disposed.
275 bool disabledEviction
= false;
276 lock (_cacheDataLock
)
279 // recycle only if entries exceeds the high mark factor
281 if (_cacheData
.Count
> _sweepingTriggerHighMark
)
286 uint evictedEntriesCount
= 0;
287 List
<QueryCacheKey
> cacheKeys
= new List
<QueryCacheKey
>(_cacheData
.Count
);
288 cacheKeys
.AddRange(_cacheData
.Keys
);
289 for (int i
= 0; i
< cacheKeys
.Count
; i
++)
292 // if entry was not used in the last time window, then evict the entry
294 if (0 == cacheKeys
[i
].HitCount
)
296 _cacheData
.Remove(cacheKeys
[i
]);
297 evictedEntriesCount
++;
300 // otherwise, age the entry in a progressive scheme
304 int agingIndex
= unchecked(cacheKeys
[i
].AgingIndex
+ 1);
305 if (agingIndex
> AgingMaxIndex
)
307 agingIndex
= AgingMaxIndex
;
309 cacheKeys
[i
].AgingIndex
= agingIndex
;
310 cacheKeys
[i
].HitCount
= cacheKeys
[i
].HitCount
>> _agingFactor
[agingIndex
];
316 _evictionTimer
.Stop();
317 disabledEviction
= true;
321 if (!disabledEviction
)
323 this._evictionTimer
.Resume();
329 #region IDisposable Members
334 /// <remarks>Dispose <b>must</b> be called as there are no finalizers for this class</remarks>
335 public void Dispose()
337 // Technically, calling GC.SuppressFinalize is not required because the class does not
338 // have a finalizer, but it does no harm, protects against the case where a finalizer is added
339 // in the future, and prevents an FxCop warning.
340 GC
.SuppressFinalize(this);
341 if (this._evictionTimer
.Stop())
350 /// Periodically invokes cache cleanup logic on a specified <see cref="QueryCacheManager"/> instance,
351 /// and allows this periodic callback to be suspended, resumed or stopped in a thread-safe way.
353 private sealed class EvictionTimer
355 /// <summary>Used to control multi-threaded accesses to this instance</summary>
356 private readonly object _sync
= new object();
358 /// <summary>The required interval between invocations of the cache cleanup logic</summary>
359 private readonly int _period
;
361 /// <summary>The underlying QueryCacheManger that the callback will act on</summary>
362 private readonly QueryCacheManager _cacheManager
;
364 /// <summary>The underlying <see cref="Timer"/> that implements the periodic callback</summary>
365 private Timer _timer
;
367 internal EvictionTimer(QueryCacheManager cacheManager
, int recyclePeriod
)
369 this._cacheManager
= cacheManager
;
370 this._period
= recyclePeriod
;
373 internal void Start()
379 this._timer
= new Timer(QueryCacheManager
.CacheRecyclerHandler
, _cacheManager
, _period
, _period
);
385 /// Permanently stops the eviction timer.
386 /// It will no longer generate periodic callbacks and further calls to <see cref="Suspend"/>, <see cref="Resume"/>, or <see cref="Stop"/>,
387 /// though thread-safe, will have no effect.
390 /// If this eviction timer has already been stopped (using the <see cref="Stop"/> method), returns <c>false</c>;
391 /// otherwise, returns <c>true</c> to indicate that the call successfully stopped and cleaned up the underlying timer instance.
394 /// Thread safe. May be called regardless of the current state of the eviction timer.
395 /// Once stopped, an eviction timer cannot be restarted with the <see cref="Resume"/> method.
401 if (this._timer
!= null)
403 this._timer
.Dispose();
415 /// Pauses the operation of the eviction timer.
418 /// If this eviction timer has already been stopped (using the <see cref="Stop"/> method), returns <c>false</c>;
419 /// otherwise, returns <c>true</c> to indicate that the call successfully suspended the inderlying <see cref="Timer"/>
420 /// and no further periodic callbacks will be generated until the <see cref="Resume"/> method is called.
423 /// Thread-safe. May be called regardless of the current state of the eviction timer.
424 /// Once suspended, an eviction timer may be resumed or stopped.
426 internal bool Suspend()
430 if (this._timer
!= null)
432 this._timer
.Change(Timeout
.Infinite
, Timeout
.Infinite
);
443 /// Causes this eviction timer to generate periodic callbacks, provided it has not been permanently stopped (using the <see cref="Stop"/> method).
446 /// Thread-safe. May be called regardless of the current state of the eviction timer.
448 internal void Resume()
452 if (this._timer
!= null)
454 this._timer
.Change(this._period
, this._period
);