CacheManager.cs source code in C# .NET

Source code for the .NET framework in C#

                        

Code:

/ 4.0 / 4.0 / untmp / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / xsp / System / Web / Hosting / CacheManager.cs / 1305376 / CacheManager.cs

                            using System; 
using System.Globalization;
using System.Web;
using System.Web.Util;
using System.Threading; 
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; 
using Debug = System.Web.Util.Debug; 

// 
// Welcome to the CacheManager class, CM for short.  CM monitors private bytes for the
// worker process.  If the Private Bytes limit is about to be exceeded, CM will trim
// the cache (as necessary), and induce a GC to prevent the process from recycling.
// 
// A timer thread is used to monitor Private Bytes.  The interval is adjusted depending
// on the current memory pressure.  The maximum interval is every 2 minutes, and the 
// minimum interval is every 5 seconds. 
//
 
namespace System.Web.Hosting {
    internal class CacheManager: IDisposable {
        const int      HIGH_FREQ_INTERVAL_S                     = 5;
        const int      HIGH_FREQ_INTERVAL_MS                    = 5 * Msec.ONE_SECOND; 
        const int      MEDIUM_FREQ_INTERVAL_S                   = 30;
        const int      MEDIUM_FREQ_INTERVAL_MS                  = 30 * Msec.ONE_SECOND; 
        const int      LOW_FREQ_INTERVAL_S                      = 120; 
        const int      LOW_FREQ_INTERVAL_MS                     = 120 * Msec.ONE_SECOND;
        const int      MEGABYTE_SHIFT                           = 20; 
        const long     MEGABYTE                                 = 1L << MEGABYTE_SHIFT; // 1048576
        const int      SAMPLE_COUNT                             = 2;
        const int      DELTA_SAMPLE_COUNT                       = 10;
 
        private        ApplicationManager _appManager;
 
        private        long       _totalCacheSize; 
        private        long       _trimDurationTicks;
        private        int        _lastTrimPercent              = 10; // starts at 10, but changes to fit workload 
        private        long       _inducedGCMinInterval         = TimeSpan.TicksPerSecond * 5; // starts at 5 seconds, but changes to fit workload
        private        DateTime   _inducedGCFinishTime          = DateTime.MinValue;
        private        long       _inducedGCDurationTicks;
        private        int        _inducedGCCount; 
        private        long       _inducedGCPostPrivateBytes;
        private        long       _inducedGCPrivateBytesChange; 
 
        private        int        _currentPollInterval          = MEDIUM_FREQ_INTERVAL_MS;
        private        DateTime   _timerSuspendTime             = DateTime.MinValue; 
        private        int        _inPBytesMonitorThread;
        private        Timer      _timer;
        private        Object     _timerLock                    = new object();
 
        private        long       _limit; // the "effective" worker process Private Bytes limit
        private        long       _highPressureMark; 
        private        long       _mediumPressureMark; 
        private        long       _lowPressureMark;
        private        long[]     _deltaSamples; // a history of the increase in private bytes per second 
        private        int        _idxDeltaSamples;
        private        long       _maxDelta; // the maximum expected increase in private bytes per second
        private        long       _minMaxDelta;  // _maxDelta must always be at least this large
        private        long[]     _samples; // a history of the sample values (private bytes for the process) 
        private        DateTime[] _sampleTimes; // time at which samples were taken
        private        int        _idx; 
 
        private        bool       _useGetProcessMemoryInfo;
        private        uint       _pid; 
        private        bool       _disposed;

        private CacheManager() {}
 
        internal CacheManager(ApplicationManager appManager, long privateBytesLimit) {
#if PERF 
            SafeNativeMethods.OutputDebugString(String.Format("Creating CacheManager with PrivateBytesLimit = {0:N}\n", privateBytesLimit)); 
#endif
            // don't create timer if there's no memory limit 
            if (privateBytesLimit <= 0) {
                return;
            }
 
            _appManager = appManager;
            _limit = privateBytesLimit; 
 
            _pid = (uint) SafeNativeMethods.GetCurrentProcessId();
 
            // the initial expected maximum increase in private bytes is 2MB per second per CPU
            _minMaxDelta = 2 * MEGABYTE * SystemInfo.GetNumProcessCPUs();
            AdjustMaxDeltaAndPressureMarks(_minMaxDelta);
 
            _samples = new long[SAMPLE_COUNT];
            _sampleTimes = new DateTime[SAMPLE_COUNT]; 
            _useGetProcessMemoryInfo = (VersionInfo.ExeName == "w3wp"); 
            _deltaSamples = new long[DELTA_SAMPLE_COUNT];
 
            // start timer with initial poll interval
            _timer = new Timer(new TimerCallback(this.PBytesMonitorThread), null, _currentPollInterval, _currentPollInterval);
        }
 

        void Adjust() { 
            // not thread-safe, only invoke from timer callback 
            Debug.Assert(_inPBytesMonitorThread == 1);
 
            Debug.Assert(SAMPLE_COUNT == 2);
            // current sample
            long s2 = _samples[_idx];
            // previous sample 
            long s1 = _samples[_idx ^ 1];
 
            // adjust _maxDelta and pressure marks 
            if (s2 > s1 && s1 > 0) {
                // current time 
                DateTime d2 = _sampleTimes[_idx];
                // previous time
                DateTime d1 = _sampleTimes[_idx ^ 1];
 
                long numBytes = s2 - s1;
                long numSeconds = (long)Math.Round(d2.Subtract(d1).TotalSeconds); 
                if (numSeconds > 0) { 
                    long delta = numBytes / numSeconds;
                    _deltaSamples[_idxDeltaSamples] = delta; 
                    _idxDeltaSamples = (_idxDeltaSamples + 1) % DELTA_SAMPLE_COUNT;
                    // update rate of change in private bytes and pressure marks
                    AdjustMaxDeltaAndPressureMarks(delta);
                } 
            }
 
            lock (_timerLock) { 
                if (_timer == null) {
                    return; 
                }

                // adjust timer frequency
                if (s2 > _mediumPressureMark) { 
                    if (_currentPollInterval > HIGH_FREQ_INTERVAL_MS) {
                        _currentPollInterval = HIGH_FREQ_INTERVAL_MS; 
                        _timer.Change(_currentPollInterval, _currentPollInterval); 
                    }
                } 
                else if (s2 > _lowPressureMark) {
                    if (_currentPollInterval > MEDIUM_FREQ_INTERVAL_MS) {
                        _currentPollInterval = MEDIUM_FREQ_INTERVAL_MS;
                        _timer.Change(_currentPollInterval, _currentPollInterval); 
                    }
                } 
                else { 
                    if (_currentPollInterval != LOW_FREQ_INTERVAL_MS) {
                        _currentPollInterval = LOW_FREQ_INTERVAL_MS; 
                        _timer.Change(_currentPollInterval, _currentPollInterval);
                    }
                }
            } 
        }
 
        void AdjustMaxDeltaAndPressureMarks(long delta) { 
            // not thread-safe...only invoke from ctor or timer callback
            Debug.Assert(_inPBytesMonitorThread == 1 || _timer == null); 

            // The value of _maxDelta is the largest rate of change we've seen,
            // but it is reduced if the rate is now consistently less than what
            // it once was. 
            long newMaxDelta = _maxDelta;
            if (delta > newMaxDelta) { 
                // set maxDelta to the current rate of change 
                newMaxDelta = delta;
            } 
            else {
                // if _maxDelta is at least four times larger than every sample rate in the history,
                // then reduce _maxDelta
                bool reduce = true; 
                long maxDelta = _maxDelta / 4;
                foreach (long rate in _deltaSamples) { 
                    if (rate > maxDelta) { 
                        reduce = false;
                        break; 
                    }
                }
                if (reduce) {
                    newMaxDelta = maxDelta * 2; 
                }
            } 
 
            // ensure that maxDelta is sufficiently large so that the _highPressureMark is sufficiently
            // far away from the memory limit 
            newMaxDelta = Math.Max(newMaxDelta, _minMaxDelta);

            // Do we have a new maxDelta?  If so, adjust it and pressure marks.
            if (_maxDelta != newMaxDelta) { 
                // adjust _maxDelta
                _maxDelta = newMaxDelta; 
                // instead of using _maxDelta, use twice _maxDelta since recycling is 
                // expensive and the real delta fluctuates
                _highPressureMark = Math.Max(_limit * 9 / 10, _limit - (_maxDelta * 2 * HIGH_FREQ_INTERVAL_S)); 
                _lowPressureMark =  Math.Max(_limit * 6 / 10, _limit - (_maxDelta * 2 * LOW_FREQ_INTERVAL_S));
                _mediumPressureMark = Math.Max((_highPressureMark + _lowPressureMark) / 2 , _limit - (_maxDelta * 2 * MEDIUM_FREQ_INTERVAL_S));
                _mediumPressureMark = Math.Min(_highPressureMark , _mediumPressureMark);
 
#if PERF
                SafeNativeMethods.OutputDebugString(String.Format("CacheManager.AdjustMaxDeltaAndPressureMarks:  _highPressureMark={0:N}, _mediumPressureMark={1:N}, _lowPressureMark={2:N}, _maxDelta={3:N}\n", _highPressureMark, _mediumPressureMark, _lowPressureMark, _maxDelta)); 
#endif 

#if DBG 
                Debug.Trace("CacheMemory", "AdjustMaxDeltaAndPressureMarks "
                            + "delta=" + delta
                            + ", _maxDelta=" + _maxDelta
                            + ", _highPressureMark=" + _highPressureMark 
                            + ", _mediumPressureMark=" + _mediumPressureMark
                            + ", _lowPressureMark=" + _lowPressureMark); 
#endif 

            } 
        }

        [SuppressMessage("Microsoft.Reliability", "CA2001:AvoidCallingProblematicMethods", Justification="Need to call GC.Collect.")]
        private void CollectInfrequently(long privateBytes) { 
            // not thread-safe, only invoke from timer callback
            Debug.Assert(_inPBytesMonitorThread == 1); 
 
            // The Server GC on x86 can traverse ~200mb per CPU per second, and the maximum heap size
            // is about 3400mb, so the worst case scenario on x86 would take about 8 seconds to collect 
            // on a dual CPU box.
            //
            // The Server GC on x64 can traverse ~300mb per CPU per second, so a 6000 MB heap will take
            // about 10 seconds to collect on a dual CPU box.  The worst case scenario on x64 would make 
            // you want to return your hardware for a refund.
 
            long timeSinceInducedGC = DateTime.UtcNow.Subtract(_inducedGCFinishTime).Ticks; 
            bool infrequent = (timeSinceInducedGC > _inducedGCMinInterval);
 
            // if we haven't collected recently, or if the trim percent is low (less than 50%),
            // we need to collect again
            if (infrequent || _lastTrimPercent < 50) {
 
                // if we're inducing GC too frequently, increase the trim percentage, but don't go above 50%
                if (!infrequent) { 
                    _lastTrimPercent = Math.Min(50, _lastTrimPercent + 10); 
                }
                // if we're inducing GC infrequently, we may want to decrease the trim percentage 
                else if (_lastTrimPercent > 10 && timeSinceInducedGC > 2 * _inducedGCMinInterval) {
                    _lastTrimPercent = Math.Max(10, _lastTrimPercent - 10);
                }
                int percent = (_totalCacheSize > 0) ? _lastTrimPercent : 0; 
                long trimmedOrExpired = 0;
                if (percent > 0) { 
                    Stopwatch sw1 = Stopwatch.StartNew(); 
                    trimmedOrExpired = _appManager.TrimCaches(percent);
                    sw1.Stop(); 
                    _trimDurationTicks = sw1.Elapsed.Ticks;
                }

                // 

                if (trimmedOrExpired == 0 || _appManager.ShutdownInProgress) { 
                    return; 
                }
 
                // collect and record statistics
                Stopwatch sw2 = Stopwatch.StartNew();
                GC.Collect();
                sw2.Stop(); 

                _inducedGCCount++; // only used for debugging 
                _inducedGCFinishTime = DateTime.UtcNow; 
                _inducedGCDurationTicks = sw2.Elapsed.Ticks;
                _inducedGCPostPrivateBytes = NextSample(); 
                _inducedGCPrivateBytesChange = privateBytes - _inducedGCPostPrivateBytes;
                // target 3.3% Time in GC, but don't induce a GC more than once every 5 seconds
                // Notes on calculation below:  If G is duration of garbage collection and T is duration
                // between starting the next collection, then G/T is % Time in GC.  If we target 3.3%, 
                // then G/T = 3.3% = 33/1000, so T = G * 1000/33.
                _inducedGCMinInterval = Math.Max(_inducedGCDurationTicks * 1000 / 33, 5 * TimeSpan.TicksPerSecond); 
                // no more frequently than every 60 seconds if change is less than 1% 
                if (_inducedGCPrivateBytesChange * 100 <= privateBytes) {
                    _inducedGCMinInterval = Math.Max(_inducedGCMinInterval, 60 * TimeSpan.TicksPerSecond); 
                }
#if DBG
                Debug.Trace("CacheMemory", "GC.COLLECT STATS "
                            + "TrimCaches(" + percent + ")" 
                            + ", trimDurationSeconds=" + (_trimDurationTicks/TimeSpan.TicksPerSecond)
                            + ", trimmedOrExpired=" + trimmedOrExpired 
                            + ", #secondsSinceInducedGC=" + (timeSinceInducedGC/TimeSpan.TicksPerSecond) 
                            + ", InducedGCCount=" + _inducedGCCount
                            + ", gcDurationSeconds=" + (_inducedGCDurationTicks/TimeSpan.TicksPerSecond) 
                            + ", PrePrivateBytes=" + privateBytes
                            + ", PostPrivateBytes=" + _inducedGCPostPrivateBytes
                            + ", PrivateBytesChange=" + _inducedGCPrivateBytesChange
                            + ", gcMinIntervalSeconds=" + (_inducedGCMinInterval/TimeSpan.TicksPerSecond)); 
#endif
 
#if PERF 
                SafeNativeMethods.OutputDebugString("  ** COLLECT **: "
                            + percent + "%, " 
                            + (_trimDurationTicks/TimeSpan.TicksPerSecond) + " seconds"
                            + ", infrequent=" + infrequent
                            + ", removed=" + trimmedOrExpired
                            + ", sinceIGC=" + (timeSinceInducedGC/TimeSpan.TicksPerSecond) 
                            + ", IGCCount=" + _inducedGCCount
                            + ", IGCDuration=" + (_inducedGCDurationTicks/TimeSpan.TicksPerSecond) 
                            + ", preBytes=" + privateBytes 
                            + ", postBytes=" + _inducedGCPostPrivateBytes
                            + ", byteChange=" + _inducedGCPrivateBytesChange 
                            + ", IGCMinInterval=" + (_inducedGCMinInterval/TimeSpan.TicksPerSecond) + "\n");
#endif

            } 
        }
 
        internal long GetUpdatedTotalCacheSize(long sizeUpdate) { 
            if (sizeUpdate != 0) {
                long totalSize = Interlocked.Add(ref _totalCacheSize, sizeUpdate); 
#if PERF
                SafeNativeMethods.OutputDebugString("CacheManager.GetUpdatedTotalCacheSize:"
                                                    + " _totalCacheSize= " + totalSize
                                                    + ", sizeUpdate=" + sizeUpdate + "\n"); 
#endif
 
                return totalSize; 
            }
            else { 
                return _totalCacheSize;
            }
        }
 
        public void Dispose() {
            _disposed = true; 
            Dispose(true); 
            GC.SuppressFinalize(this);
        } 

        protected virtual void Dispose(bool disposing) {
            if (disposing) {
                // managed and unmanaged resources can be touched/released 
                DisposeTimer();
            } 
            else { 
                // the finalizer is calling, so don't touch managed state
            } 
        }

        private void DisposeTimer() {
            lock (_timerLock) { 
                if (_timer != null) {
                    _timer.Dispose(); 
                    _timer = null; 
                }
            } 
        }

        private void PBytesMonitorThread(object state) {
            // callbacks are queued and can unleash all at once, so concurrent invocations must be prevented 
            if (Interlocked.Exchange(ref _inPBytesMonitorThread, 1) != 0)
                return; 
 
            try {
                if (_disposed) { 
                    return;
                }

#if DBG 
                Debug.Trace("CacheMemory", "\r\n\r\n***BEG** PBytesMonitorThread " + DateTime.Now.ToString("T", CultureInfo.InvariantCulture));
#endif 
                // get another sample 
                long privateBytes = NextSample();
 
                // adjust frequency of timer and pressure marks after the sample is captured
                Adjust();

                if (privateBytes > _highPressureMark) { 
                    // induce a GC if necessary
                    CollectInfrequently(privateBytes); 
                } 

#if DBG 
                Debug.Trace("CacheMemory", "**END** PBytesMonitorThread "
                            + "privateBytes=" + privateBytes
                            + ", _highPressureMark=" + _highPressureMark);
#endif 

            } 
            finally { 
                Interlocked.Exchange(ref _inPBytesMonitorThread, 0);
            } 
        }

        private long NextSample() {
            // not thread-safe, only invoke from timer callback 
            Debug.Assert(_inPBytesMonitorThread == 1);
 
            // NtQuerySystemInformation is a very expensive call. A new function 
            // exists on XP Pro and later versions of the OS and it performs much
            // better. The name of that function is GetProcessMemoryInfo. For hosting 
            // scenarios where a larger number of w3wp.exe instances are running, we
            // want to use the new API (VSWhidbey 417366).
            long privateBytes;
            if (_useGetProcessMemoryInfo) { 
                long privatePageCount;
                UnsafeNativeMethods.GetPrivateBytesIIS6(out privatePageCount, true /*nocache*/); 
                privateBytes = privatePageCount; 
            }
            else { 
                uint    dummy;
                uint    privatePageCount = 0;
                // this is a very expensive call
                UnsafeNativeMethods.GetProcessMemoryInformation(_pid, out privatePageCount, out dummy, true /*nocache*/); 
                privateBytes = (long)privatePageCount << MEGABYTE_SHIFT;
            } 
 
            // increment the index (it's either 1 or 0)
            Debug.Assert(SAMPLE_COUNT == 2); 
            _idx = _idx ^ 1;
            // remember the sample time
            _sampleTimes[_idx] = DateTime.UtcNow;
            // remember the sample value 
            _samples[_idx] = privateBytes;
 
#if PERF 
            SafeNativeMethods.OutputDebugString(String.Format("CacheManager.NextSample:  privateBytes={0:N}, _highPresureMark={1:N}\n", privateBytes, _highPressureMark));
#endif 

            return privateBytes;
        }
    } 
}

// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
// Copyright (c) Microsoft Corporation. All rights reserved.
                        

Link Menu

Network programming in C#, Network Programming in VB.NET, Network Programming in .NET
This book is available now!
Buy at Amazon US or
Buy at Amazon UK