// // Copyright (c) 2009 Microsoft Corporation. All rights reserved. // using System.Collections.Specialized; using System.Configuration; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.Caching.Configuration; using System.Runtime.InteropServices; using System.Security; using System.Threading; namespace System.Runtime.Caching { internal sealed class MemoryCacheStatistics : IDisposable { const int MEMORYSTATUS_INTERVAL_5_SECONDS = 5 * 1000; const int MEMORYSTATUS_INTERVAL_30_SECONDS = 30 * 1000; private int _configCacheMemoryLimitMegabytes; private int _configPhysicalMemoryLimitPercentage; private int _configPollingInterval; private int _inCacheManagerThread; private int _disposed; private long _lastTrimCount; private long _lastTrimDurationTicks; // used only for debugging private int _lastTrimGen2Count; private int _lastTrimPercent; private DateTime _lastTrimTime; private int _pollingInterval; private Timer _timer; private Object _timerLock; private long _totalCountBeforeTrim; private CacheMemoryMonitor _cacheMemoryMonitor; private MemoryCache _memoryCache; private PhysicalMemoryMonitor _physicalMemoryMonitor; // private private MemoryCacheStatistics() { //hide default ctor } private void AdjustTimer() { lock (_timerLock) { if (_timer == null) return; // the order of these if statements is important // When above the high pressure mark, interval should be 5 seconds or less if (_physicalMemoryMonitor.IsAboveHighPressure() || _cacheMemoryMonitor.IsAboveHighPressure()) { if (_pollingInterval > MEMORYSTATUS_INTERVAL_5_SECONDS) { _pollingInterval = MEMORYSTATUS_INTERVAL_5_SECONDS; _timer.Change(_pollingInterval, _pollingInterval); } return; } // When above half the low pressure mark, interval should be 30 seconds or less if ((_cacheMemoryMonitor.PressureLast > _cacheMemoryMonitor.PressureLow / 2) || (_physicalMemoryMonitor.PressureLast > _physicalMemoryMonitor.PressureLow / 2)) { // DevDivBugs 104034: allow interval to fall back down when memory pressure goes away int newPollingInterval = Math.Min(_configPollingInterval, MEMORYSTATUS_INTERVAL_30_SECONDS); if (_pollingInterval != newPollingInterval) { _pollingInterval = newPollingInterval; _timer.Change(_pollingInterval, _pollingInterval); } return; } // there is no pressure, interval should be the value from config if (_pollingInterval != _configPollingInterval) { _pollingInterval = _configPollingInterval; _timer.Change(_pollingInterval, _pollingInterval); } } } // timer callback private void CacheManagerTimerCallback(object state) { CacheManagerThread(0); } private int GetPercentToTrim() { int gen2Count = GC.CollectionCount(2); // has there been a Gen 2 Collection since the last trim? if (gen2Count != _lastTrimGen2Count) { return Math.Max(_physicalMemoryMonitor.GetPercentToTrim(_lastTrimTime, _lastTrimPercent), _cacheMemoryMonitor.GetPercentToTrim(_lastTrimTime, _lastTrimPercent)); } else { return 0; } } private void InitializeConfiguration(NameValueCollection config) { MemoryCacheElement element = null; if (!_memoryCache.ConfigLess) { MemoryCacheSection section = ConfigurationManager.GetSection("system.runtime.caching/memoryCache") as MemoryCacheSection; if (section != null) { element = section.NamedCaches[_memoryCache.Name]; } } if (element != null) { _configCacheMemoryLimitMegabytes = element.CacheMemoryLimitMegabytes; _configPhysicalMemoryLimitPercentage = element.PhysicalMemoryLimitPercentage; double milliseconds = element.PollingInterval.TotalMilliseconds; _configPollingInterval = (milliseconds < (double)Int32.MaxValue) ? (int) milliseconds : Int32.MaxValue; } else { _configPollingInterval = ConfigUtil.DefaultPollingTimeMilliseconds; _configCacheMemoryLimitMegabytes = 0; _configPhysicalMemoryLimitPercentage = 0; } if (config != null) { _configPollingInterval = ConfigUtil.GetIntValueFromTimeSpan(config, ConfigUtil.PollingInterval, _configPollingInterval); _configCacheMemoryLimitMegabytes = ConfigUtil.GetIntValue(config, ConfigUtil.CacheMemoryLimitMegabytes, _configCacheMemoryLimitMegabytes, true, Int32.MaxValue); _configPhysicalMemoryLimitPercentage = ConfigUtil.GetIntValue(config, ConfigUtil.PhysicalMemoryLimitPercentage, _configPhysicalMemoryLimitPercentage, true, 100); } } private void InitDisposableMembers() { bool dispose = true; try { _cacheMemoryMonitor = new CacheMemoryMonitor(_memoryCache, _configCacheMemoryLimitMegabytes); _timer = new Timer(new TimerCallback(CacheManagerTimerCallback), null, _configPollingInterval, _configPollingInterval); dispose = false; } finally { if (dispose) { Dispose(); } } } private void SetTrimStats(long trimDurationTicks, long totalCountBeforeTrim, long trimCount) { _lastTrimDurationTicks = trimDurationTicks; int gen2Count = GC.CollectionCount(2); // has there been a Gen 2 Collection since the last trim? if (gen2Count != _lastTrimGen2Count) { _lastTrimTime = DateTime.UtcNow; _totalCountBeforeTrim = totalCountBeforeTrim; _lastTrimCount = trimCount; } else { // we've done multiple trims between Gen 2 collections, so only add to the trim count _lastTrimCount += trimCount; } _lastTrimGen2Count = gen2Count; _lastTrimPercent = (int)((_lastTrimCount * 100L) / _totalCountBeforeTrim); } private void Update() { _physicalMemoryMonitor.Update(); _cacheMemoryMonitor.Update(); } // public/internal internal long CacheMemoryLimit { get { return _cacheMemoryMonitor.MemoryLimit; } } internal long PhysicalMemoryLimit { get { return _physicalMemoryMonitor.MemoryLimit; } } internal TimeSpan PollingInterval { get { return TimeSpan.FromMilliseconds(_configPollingInterval); } } internal MemoryCacheStatistics(MemoryCache memoryCache, NameValueCollection config) { _memoryCache = memoryCache; _lastTrimGen2Count = -1; _lastTrimTime = DateTime.MinValue; _timerLock = new Object(); InitializeConfiguration(config); _pollingInterval = _configPollingInterval; _physicalMemoryMonitor = new PhysicalMemoryMonitor(_configPhysicalMemoryLimitPercentage); InitDisposableMembers(); } [SecuritySafeCritical] internal long CacheManagerThread(int minPercent) { if (Interlocked.Exchange(ref _inCacheManagerThread, 1) != 0) return 0; try { if (_disposed == 1) { return 0; } #if DBG Dbg.Trace("MemoryCacheStats", "**BEG** CacheManagerThread " + DateTime.Now.ToString("T", CultureInfo.InvariantCulture)); #endif // The timer thread must always call Update so that the CacheManager // knows the size of the cache. Update(); AdjustTimer(); int percent = Math.Max(minPercent, GetPercentToTrim()); long beginTotalCount = _memoryCache.GetCount(); Stopwatch sw = Stopwatch.StartNew(); long trimmedOrExpired = _memoryCache.Trim(percent); sw.Stop(); // 1) don't update stats if the trim happend because MAX_COUNT was exceeded // 2) don't update stats unless we removed at least one entry if (percent > 0 && trimmedOrExpired > 0) { SetTrimStats(sw.Elapsed.Ticks, beginTotalCount, trimmedOrExpired); } #if DBG Dbg.Trace("MemoryCacheStats", "**END** CacheManagerThread: " + ", percent=" + percent + ", beginTotalCount=" + beginTotalCount + ", trimmed=" + trimmedOrExpired + ", Milliseconds=" + sw.ElapsedMilliseconds); #endif #if PERF SafeNativeMethods.OutputDebugString("CacheCommon.CacheManagerThread:" + " minPercent= " + minPercent + ", percent= " + percent + ", beginTotalCount=" + beginTotalCount + ", trimmed=" + trimmedOrExpired + ", Milliseconds=" + sw.ElapsedMilliseconds + "\n"); #endif return trimmedOrExpired; } finally { Interlocked.Exchange(ref _inCacheManagerThread, 0); } } public void Dispose() { if (Interlocked.Exchange(ref _disposed, 1) == 0) { lock (_timerLock) { Timer timer = _timer; if (timer != null && Interlocked.CompareExchange(ref _timer, null, timer) == timer) { timer.Dispose(); Dbg.Trace("MemoryCacheStats", "Stopped CacheMemoryTimers"); } } while (_inCacheManagerThread != 0) { Thread.Sleep(100); } if (_cacheMemoryMonitor != null) { _cacheMemoryMonitor.Dispose(); } // Don't need to call GC.SuppressFinalize(this) for sealed types without finalizers. } } [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Grandfathered suppression from original caching code checkin")] internal void UpdateConfig(NameValueCollection config) { int pollingInterval = ConfigUtil.GetIntValueFromTimeSpan(config, ConfigUtil.PollingInterval, _configPollingInterval); int cacheMemoryLimitMegabytes = ConfigUtil.GetIntValue(config, ConfigUtil.CacheMemoryLimitMegabytes, _configCacheMemoryLimitMegabytes, true, Int32.MaxValue); int physicalMemoryLimitPercentage = ConfigUtil.GetIntValue(config, ConfigUtil.PhysicalMemoryLimitPercentage, _configPhysicalMemoryLimitPercentage, true, 100); if (pollingInterval != _configPollingInterval) { lock (_timerLock) { _configPollingInterval = pollingInterval; } } if (cacheMemoryLimitMegabytes == _configCacheMemoryLimitMegabytes && physicalMemoryLimitPercentage == _configPhysicalMemoryLimitPercentage) { return; } try { try { } finally { // prevent ThreadAbortEx from interrupting while (Interlocked.Exchange(ref _inCacheManagerThread, 1) != 0) { Thread.Sleep(100); } } if (_disposed == 0) { if (cacheMemoryLimitMegabytes != _configCacheMemoryLimitMegabytes) { _cacheMemoryMonitor.SetLimit(cacheMemoryLimitMegabytes); _configCacheMemoryLimitMegabytes = cacheMemoryLimitMegabytes; } if (physicalMemoryLimitPercentage != _configPhysicalMemoryLimitPercentage) { _physicalMemoryMonitor.SetLimit(physicalMemoryLimitPercentage); _configPhysicalMemoryLimitPercentage = physicalMemoryLimitPercentage; } } } finally { Interlocked.Exchange(ref _inCacheManagerThread, 0); } } } }