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;
22 /// Provides Query Execution Plan Caching Service
26 /// Dispose <b>must</b> be called as there is no finalizer for this class
28 internal class QueryCacheManager : IDisposable
30 #region Constants/Default values for configuration parameters
33 /// Default Soft maximum number of entries in the cache
34 /// Default value: 1000
36 const int DefaultMaxNumberOfEntries = 1000;
39 /// Default high mark for starting sweeping process
40 /// default value: 80% of MaxNumberOfEntries
42 const float DefaultHighMarkPercentageFactor = 0.8f; // 80% of MaxLimit
45 /// Recycler timer period
47 const int DefaultRecyclerPeriodInMilliseconds = 60 * 1000;
56 private readonly object _cacheDataLock = new object();
61 private readonly Dictionary<QueryCacheKey, QueryCacheEntry> _cacheData = new Dictionary<QueryCacheKey, QueryCacheEntry>(32);
64 /// soft maximum number of entries in the cache
66 private readonly int _maxNumberOfEntries;
69 /// high mark of the number of entries to trigger the sweeping process
71 private readonly int _sweepingTriggerHighMark;
76 private readonly EvictionTimer _evictionTimer;
80 #region Construction and Initialization
83 /// Constructs a new Query Cache Manager instance, with default values for all 'configurable' parameters.
85 /// <returns>A new instance of <see cref="QueryCacheManager"/> configured with default entry count, load factor and recycle period</returns>
86 internal static QueryCacheManager Create()
88 return new QueryCacheManager(DefaultMaxNumberOfEntries, DefaultHighMarkPercentageFactor, DefaultRecyclerPeriodInMilliseconds);
94 /// <param name="maximumSize">
95 /// Maximum number of entries that the cache should contain.
97 /// <param name="loadFactor">
98 /// The number of entries that must be present, as a percentage, before entries should be removed
99 /// according to the eviction policy.
100 /// Must be greater than 0 and less than or equal to 1.0
102 /// <param name="recycleMillis">
103 /// The interval, in milliseconds, at which the number of entries will be compared to the load factor
104 /// and eviction carried out if necessary.
106 private QueryCacheManager(int maximumSize, float loadFactor, int recycleMillis)
108 Debug.Assert(maximumSize > 0, "Maximum size must be greater than zero");
109 Debug.Assert(loadFactor > 0 && loadFactor <= 1, "Load factor must be greater than 0.0 and less than or equal to 1.0");
110 Debug.Assert(recycleMillis >= 0, "Recycle period in milliseconds must not be negative");
113 // Load hardcoded defaults
115 this._maxNumberOfEntries = maximumSize;
118 // set sweeping high mark trigger value
120 this._sweepingTriggerHighMark = (int)(_maxNumberOfEntries * loadFactor);
123 // Initialize Recycler
125 this._evictionTimer = new EvictionTimer(this, recycleMillis);
130 #region 'External' interface
132 /// Adds new entry to the cache using "abstract" cache context and
133 /// value; returns an existing entry if the key is already in the
136 /// <param name="inQueryCacheEntry"></param>
137 /// <param name="outQueryCacheEntry">
138 /// The existing entry in the dicitionary if already there;
139 /// inQueryCacheEntry if none was found and inQueryCacheEntry
140 /// was added instead.
142 /// <returns>true if the output entry was already found; false if it had to be added.</returns>
143 internal bool TryLookupAndAdd(QueryCacheEntry inQueryCacheEntry, out QueryCacheEntry outQueryCacheEntry)
145 Debug.Assert(null != inQueryCacheEntry, "qEntry must not be null");
147 outQueryCacheEntry = null;
149 lock (_cacheDataLock)
151 if (!_cacheData.TryGetValue(inQueryCacheEntry.QueryCacheKey, out outQueryCacheEntry))
154 // add entry to cache data
156 _cacheData.Add(inQueryCacheEntry.QueryCacheKey, inQueryCacheEntry);
157 if (_cacheData.Count > _sweepingTriggerHighMark)
159 _evictionTimer.Start();
166 outQueryCacheEntry.QueryCacheKey.UpdateHit();
174 /// Lookup service for a cached value.
176 internal bool TryCacheLookup<TK, TE>(TK key, out TE value)
177 where TK : QueryCacheKey
179 Debug.Assert(null != key, "key must not be null");
184 // invoke internal lookup
186 QueryCacheEntry qEntry = null;
187 bool bHit = TryInternalCacheLookup(key, out qEntry);
190 // if it is a hit, 'extract' the entry strong type cache value
194 value = (TE)qEntry.GetTarget();
203 internal void Clear()
205 lock (_cacheDataLock)
212 #region Private Members
217 /// <param name="queryCacheKey"></param>
218 /// <param name="queryCacheEntry"></param>
219 /// <returns>true if cache hit, false if cache miss</returns>
220 private bool TryInternalCacheLookup( QueryCacheKey queryCacheKey, out QueryCacheEntry queryCacheEntry )
222 Debug.Assert(null != queryCacheKey, "queryCacheKey must not be null");
224 queryCacheEntry = null;
229 // lock the cache for the minimal possible period
231 lock (_cacheDataLock)
233 bHit = _cacheData.TryGetValue(queryCacheKey, out queryCacheEntry);
242 // update hit mark in cache key
244 queryCacheEntry.QueryCacheKey.UpdateHit();
252 /// Recycler handler. This method is called directly by the eviction timer.
253 /// It should take no action beyond invoking the <see cref="SweepCache"/> method on the
254 /// cache manager instance passed as <paramref name="state"/>.
256 /// <param name="state">The cache manager instance on which the 'recycle' handler should be invoked</param>
257 private static void CacheRecyclerHandler(object state)
259 ((QueryCacheManager)state).SweepCache();
265 private static readonly int[] _agingFactor = {1,1,2,4,8,16};
266 private static readonly int AgingMaxIndex = _agingFactor.Length - 1;
269 /// Sweeps the cache removing old unused entries.
270 /// This method implements the query cache eviction policy.
272 private void SweepCache()
274 if (!this._evictionTimer.Suspend())
276 // Return of false from .Suspend means that the manager and timer have been disposed.
280 bool disabledEviction = false;
281 lock (_cacheDataLock)
284 // recycle only if entries exceeds the high mark factor
286 if (_cacheData.Count > _sweepingTriggerHighMark)
291 uint evictedEntriesCount = 0;
292 List<QueryCacheKey> cacheKeys = new List<QueryCacheKey>(_cacheData.Count);
293 cacheKeys.AddRange(_cacheData.Keys);
294 for (int i = 0; i < cacheKeys.Count; i++)
297 // if entry was not used in the last time window, then evict the entry
299 if (0 == cacheKeys[i].HitCount)
301 _cacheData.Remove(cacheKeys[i]);
302 evictedEntriesCount++;
305 // otherwise, age the entry in a progressive scheme
309 int agingIndex = unchecked(cacheKeys[i].AgingIndex + 1);
310 if (agingIndex > AgingMaxIndex)
312 agingIndex = AgingMaxIndex;
314 cacheKeys[i].AgingIndex = agingIndex;
315 cacheKeys[i].HitCount = cacheKeys[i].HitCount >> _agingFactor[agingIndex];
321 _evictionTimer.Stop();
322 disabledEviction = true;
326 if (!disabledEviction)
328 this._evictionTimer.Resume();
334 #region IDisposable Members
339 /// <remarks>Dispose <b>must</b> be called as there are no finalizers for this class</remarks>
340 public void Dispose()
342 // Technically, calling GC.SuppressFinalize is not required because the class does not
343 // have a finalizer, but it does no harm, protects against the case where a finalizer is added
344 // in the future, and prevents an FxCop warning.
345 GC.SuppressFinalize(this);
346 if (this._evictionTimer.Stop())
355 /// Periodically invokes cache cleanup logic on a specified <see cref="QueryCacheManager"/> instance,
356 /// and allows this periodic callback to be suspended, resumed or stopped in a thread-safe way.
358 private sealed class EvictionTimer
360 /// <summary>Used to control multi-threaded accesses to this instance</summary>
361 private readonly object _sync = new object();
363 /// <summary>The required interval between invocations of the cache cleanup logic</summary>
364 private readonly int _period;
366 /// <summary>The underlying QueryCacheManger that the callback will act on</summary>
367 private readonly QueryCacheManager _cacheManager;
369 /// <summary>The underlying <see cref="Timer"/> that implements the periodic callback</summary>
370 private Timer _timer;
372 internal EvictionTimer(QueryCacheManager cacheManager, int recyclePeriod)
374 this._cacheManager = cacheManager;
375 this._period = recyclePeriod;
378 internal void Start()
384 this._timer = new Timer(QueryCacheManager.CacheRecyclerHandler, _cacheManager, _period, _period);
390 /// Permanently stops the eviction timer.
391 /// It will no longer generate periodic callbacks and further calls to <see cref="Suspend"/>, <see cref="Resume"/>, or <see cref="Stop"/>,
392 /// though thread-safe, will have no effect.
395 /// If this eviction timer has already been stopped (using the <see cref="Stop"/> method), returns <c>false</c>;
396 /// otherwise, returns <c>true</c> to indicate that the call successfully stopped and cleaned up the underlying timer instance.
399 /// Thread safe. May be called regardless of the current state of the eviction timer.
400 /// Once stopped, an eviction timer cannot be restarted with the <see cref="Resume"/> method.
406 if (this._timer != null)
408 this._timer.Dispose();
420 /// Pauses the operation of the eviction timer.
423 /// If this eviction timer has already been stopped (using the <see cref="Stop"/> method), returns <c>false</c>;
424 /// otherwise, returns <c>true</c> to indicate that the call successfully suspended the inderlying <see cref="Timer"/>
425 /// and no further periodic callbacks will be generated until the <see cref="Resume"/> method is called.
428 /// Thread-safe. May be called regardless of the current state of the eviction timer.
429 /// Once suspended, an eviction timer may be resumed or stopped.
431 internal bool Suspend()
435 if (this._timer != null)
437 this._timer.Change(Timeout.Infinite, Timeout.Infinite);
448 /// Causes this eviction timer to generate periodic callbacks, provided it has not been permanently stopped (using the <see cref="Stop"/> method).
451 /// Thread-safe. May be called regardless of the current state of the eviction timer.
453 internal void Resume()
457 if (this._timer != null)
459 this._timer.Change(this._period, this._period);