File: System\Caching\CacheMemoryMonitor.cs
Project: ndp\fx\src\Caching\System.Runtime.Caching.csproj (System.Runtime.Caching)
// <copyright file="CacheMemoryMonitor.cs" company="Microsoft">
//   Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
// </copyright>
using System;
using System.Runtime.Caching.Configuration;
using System.Runtime.Caching.Hosting;
using System.Diagnostics.CodeAnalysis;
using System.Security;
using System.Security.Permissions;
using System.Threading;
 
namespace System.Runtime.Caching {
    // CacheMemoryMonitor uses the internal System.SizedReference type to determine
    // the size of the cache itselt, and helps us know when to drop entries to avoid
    // exceeding the cache's memory limit.  The limit is configurable (see ConfigUtil.cs).
    internal sealed class CacheMemoryMonitor : MemoryMonitor, IDisposable {
        const long PRIVATE_BYTES_LIMIT_2GB = 800 * MEGABYTE;
        const long PRIVATE_BYTES_LIMIT_3GB = 1800 * MEGABYTE;
        const long PRIVATE_BYTES_LIMIT_64BIT = 1L * TERABYTE;
        const int SAMPLE_COUNT = 2;
 
        private static IMemoryCacheManager s_memoryCacheManager;
        private static long s_autoPrivateBytesLimit = -1;
        private static long s_effectiveProcessMemoryLimit = -1;
 
        private MemoryCache _memoryCache;
        private long[] _cacheSizeSamples;
        private DateTime[] _cacheSizeSampleTimes;
        private int _idx;
        private SRefMultiple _sizedRefMultiple;
        private int _gen2Count;
        private long _memoryLimit;
 
        internal long MemoryLimit {
            get { return _memoryLimit; }
        }
 
        private CacheMemoryMonitor() {
            // hide default ctor
        }
 
        internal CacheMemoryMonitor(MemoryCache memoryCache, int cacheMemoryLimitMegabytes) {
            _memoryCache = memoryCache;
            _gen2Count = GC.CollectionCount(2);
            _cacheSizeSamples = new long[SAMPLE_COUNT];
            _cacheSizeSampleTimes = new DateTime[SAMPLE_COUNT];
            if (memoryCache.UseMemoryCacheManager)
                InitMemoryCacheManager();   // This magic thing connects us to ObjectCacheHost magically. :/
            InitDisposableMembers(cacheMemoryLimitMegabytes);
        }
        
        private void InitDisposableMembers(int cacheMemoryLimitMegabytes) {
            bool dispose = true;
            try {
                _sizedRefMultiple = new SRefMultiple(_memoryCache.AllSRefTargets);
                SetLimit(cacheMemoryLimitMegabytes);
                InitHistory();
                dispose = false;
            }
            finally {
                if (dispose) {
                    Dispose();
                }
            }
        }
 
        // Auto-generate the private bytes limit:
        // - On 64bit, the auto value is MIN(60% physical_ram, 1 TB)
        // - On x86, for 2GB, the auto value is MIN(60% physical_ram, 800 MB)
        // - On x86, for 3GB, the auto value is MIN(60% physical_ram, 1800 MB)
        //
        // - If it's not a hosted environment (e.g. console app), the 60% in the above
        //   formulas will become 100% because in un-hosted environment we don't launch
        //   other processes such as compiler, etc.
        private static long AutoPrivateBytesLimit {
            get {
                long memoryLimit = s_autoPrivateBytesLimit;
                if (memoryLimit == -1) {
 
                    bool is64bit = (IntPtr.Size == 8);
 
                    long totalPhysical = TotalPhysical;
                    long totalVirtual = TotalVirtual;
                    if (totalPhysical != 0) {
                        long recommendedPrivateByteLimit;
                        if (is64bit) {
                            recommendedPrivateByteLimit = PRIVATE_BYTES_LIMIT_64BIT;
                        }
                        else {
                            // Figure out if it's 2GB or 3GB
 
                            if (totalVirtual > 2 * GIGABYTE) {
                                recommendedPrivateByteLimit = PRIVATE_BYTES_LIMIT_3GB;
                            }
                            else {
                                recommendedPrivateByteLimit = PRIVATE_BYTES_LIMIT_2GB;
                            }
                        }
 
                        // use 60% of physical RAM
                        long usableMemory = totalPhysical * 3 / 5;
                        memoryLimit = Math.Min(usableMemory, recommendedPrivateByteLimit);
                    }
                    else {
                        // If GlobalMemoryStatusEx fails, we'll use these as our auto-gen private bytes limit
                        memoryLimit = is64bit ? PRIVATE_BYTES_LIMIT_64BIT : PRIVATE_BYTES_LIMIT_2GB;
                    }
                    Interlocked.Exchange(ref s_autoPrivateBytesLimit, memoryLimit);
                }
 
                return memoryLimit;
            }
        }
 
        public void Dispose() {
            SRefMultiple sref = _sizedRefMultiple;
            if (sref != null && Interlocked.CompareExchange(ref _sizedRefMultiple, null, sref) == sref) {
                sref.Dispose();
            }
            IMemoryCacheManager memoryCacheManager = s_memoryCacheManager;
            if (memoryCacheManager != null) {
                memoryCacheManager.ReleaseCache(_memoryCache);
            }
        }
 
        internal static long EffectiveProcessMemoryLimit {
            get {
                long memoryLimit = s_effectiveProcessMemoryLimit;
                if (memoryLimit == -1) {
                    memoryLimit = AutoPrivateBytesLimit;
                    Interlocked.Exchange(ref s_effectiveProcessMemoryLimit, memoryLimit);
                }
                return memoryLimit;
            }
        }
 
        protected override int GetCurrentPressure() {
            // Call GetUpdatedTotalCacheSize to update the total
            // cache size, if there has been a recent Gen 2 Collection.
            // This update must happen, otherwise the CacheManager won't 
            // know the total cache size.
            int gen2Count = GC.CollectionCount(2);
            SRefMultiple sref = _sizedRefMultiple;
            if (gen2Count != _gen2Count && sref != null) {
                // update _gen2Count
                _gen2Count = gen2Count;
 
                // the SizedRef is only updated after a Gen2 Collection
 
                // increment the index (it's either 1 or 0)
                Dbg.Assert(SAMPLE_COUNT == 2);
                _idx = _idx ^ 1;
                // remember the sample time
                _cacheSizeSampleTimes[_idx] = DateTime.UtcNow;
                // remember the sample value
                _cacheSizeSamples[_idx] = sref.ApproximateSize;
#if DBG
                Dbg.Trace("MemoryCacheStats", "SizedRef.ApproximateSize=" + _cacheSizeSamples[_idx]);
#endif
                IMemoryCacheManager memoryCacheManager = s_memoryCacheManager;
                if (memoryCacheManager != null) {
                    memoryCacheManager.UpdateCacheSize(_cacheSizeSamples[_idx], _memoryCache);
                }
            }
 
            // if there's no memory limit, then there's nothing more to do
            if (_memoryLimit <= 0) {
                return 0;
            }
 
            long cacheSize = _cacheSizeSamples[_idx];
 
            // use _memoryLimit as an upper bound so that pressure is a percentage (between 0 and 100, inclusive).
            if (cacheSize > _memoryLimit) {
                cacheSize = _memoryLimit;
            }
 
            // PerfCounter: Cache Percentage Process Memory Limit Used
            //    = memory used by this process / process memory limit at pressureHigh
            // Set private bytes used in kilobytes because the counter is a DWORD
 
            // 
 
 
            int result = (int)(cacheSize * 100 / _memoryLimit);
            return result;
        }
 
        internal override int GetPercentToTrim(DateTime lastTrimTime, int lastTrimPercent) {
            int percent = 0;
            if (IsAboveHighPressure()) {
                long cacheSize = _cacheSizeSamples[_idx];
                if (cacheSize > _memoryLimit) {
                    percent = Math.Min(100, (int)((cacheSize - _memoryLimit) * 100L / cacheSize));
                }
 
#if PERF
                SafeNativeMethods.OutputDebugString(String.Format("CacheMemoryMonitor.GetPercentToTrim: percent={0:N}, lastTrimPercent={1:N}\n",
                                                    percent,
                                                    lastTrimPercent));
#endif
 
            }
            return percent;
        }
 
        internal void SetLimit(int cacheMemoryLimitMegabytes) {
            long cacheMemoryLimit = cacheMemoryLimitMegabytes;
            cacheMemoryLimit = cacheMemoryLimit << MEGABYTE_SHIFT;
 
            // 
            _memoryLimit = 0;
 
            // VSWhidbey 546381: never override what the user specifies as the limit;
            // only call AutoPrivateBytesLimit when the user does not specify one.
            if (cacheMemoryLimit == 0 && _memoryLimit == 0) {
                // Zero means we impose a limit
                _memoryLimit = EffectiveProcessMemoryLimit;
            }
            else if (cacheMemoryLimit != 0 && _memoryLimit != 0) {
                // Take the min of "cache memory limit" and the host's "process memory limit".
                _memoryLimit = Math.Min(_memoryLimit, cacheMemoryLimit);
            }
            else if (cacheMemoryLimit != 0) {
                // _memoryLimit is 0, but "cache memory limit" is non-zero, so use it as the limit
                _memoryLimit = cacheMemoryLimit;
            }
 
            Dbg.Trace("MemoryCacheStats", "CacheMemoryMonitor.SetLimit: _memoryLimit=" + (_memoryLimit >> MEGABYTE_SHIFT) + "Mb");
 
            if (_memoryLimit > 0) {
                _pressureHigh = 100;
                _pressureLow = 80;
            }
            else {
                _pressureHigh = 99;
                _pressureLow = 97;
            }
 
            Dbg.Trace("MemoryCacheStats", "CacheMemoryMonitor.SetLimit: _pressureHigh=" + _pressureHigh +
                        ", _pressureLow=" + _pressureLow);
        }
 
        [SecuritySafeCritical]
        [PermissionSet(SecurityAction.Assert, Unrestricted = true)]
        [SuppressMessage("Microsoft.Security", "CA2106:SecureAsserts", Justification = "Grandfathered suppression from original caching code checkin")]
        private static void InitMemoryCacheManager() {
            if (s_memoryCacheManager == null) {
                IMemoryCacheManager memoryCacheManager = null;
                IServiceProvider host = ObjectCache.Host;
                if (host != null) {
                    memoryCacheManager = host.GetService(typeof(IMemoryCacheManager)) as IMemoryCacheManager;
                }
                if (memoryCacheManager != null) {
                    Interlocked.CompareExchange(ref s_memoryCacheManager, memoryCacheManager, null);
                }
            }
        }
    }
}