You've already forked linux-packaging-mono
							
							
		
			
				
	
	
		
			421 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
			
		
		
	
	
			421 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C#
		
	
	
	
	
	
| 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;
 | |
|         }
 | |
|     }
 | |
| }
 |