/* * Copyright (c) 2006-2014, openmetaverse.org * All rights reserved. * * - Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * - Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * - Neither the name of the openmetaverse.org nor the names * of its contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ using System; using System.Collections; using System.Collections.Generic; using System.Threading; namespace OpenMetaverse { public sealed class WrappedObject : IDisposable where T : class { private T _instance; internal readonly ObjectPoolSegment _owningSegment; internal readonly ObjectPoolBase _owningObjectPool; private bool _disposed = false; internal WrappedObject(ObjectPoolBase owningPool, ObjectPoolSegment ownerSegment, T activeInstance) { _owningObjectPool = owningPool; _owningSegment = ownerSegment; _instance = activeInstance; } ~WrappedObject() { #if !PocketPC // If the AppDomain is being unloaded, or the CLR is // shutting down, just exit gracefully if (Environment.HasShutdownStarted) return; #endif // Object Resurrection in Action! GC.ReRegisterForFinalize(this); // Return this instance back to the owning queue _owningObjectPool.CheckIn(_owningSegment, _instance); } /// /// Returns an instance of the class that has been checked out of the Object Pool. /// public T Instance { get { if (_disposed) throw new ObjectDisposedException("WrappedObject"); return _instance; } } /// /// Checks the instance back into the object pool /// public void Dispose() { if (_disposed) return; _disposed = true; _owningObjectPool.CheckIn(_owningSegment, _instance); GC.SuppressFinalize(this); } } public abstract class ObjectPoolBase : IDisposable where T : class { private int _itemsPerSegment = 32; private int _minimumSegmentCount = 1; // A segment won't be eligible for cleanup unless it's at least this old... private TimeSpan _minimumAgeToCleanup = new TimeSpan(0, 5, 0); // ever increasing segment counter private int _activeSegment = 0; private bool _gc = true; private volatile bool _disposed = false; private Dictionary> _segments = new Dictionary>(); private object _syncRoot = new object(); private object _timerLock = new object(); // create a timer that starts in 5 minutes, and gets called every 5 minutes. System.Threading.Timer _timer; int _cleanupFrequency; /// /// Creates a new instance of the ObjectPoolBase class. Initialize MUST be called /// after using this constructor. /// protected ObjectPoolBase() { } /// /// Creates a new instance of the ObjectPool Base class. /// /// The object pool is composed of segments, which /// are allocated whenever the size of the pool is exceeded. The number of items /// in a segment should be large enough that allocating a new segmeng is a rare /// thing. For example, on a server that will have 10k people logged in at once, /// the receive buffer object pool should have segment sizes of at least 1000 /// byte arrays per segment. /// /// The minimun number of segments that may exist. /// Perform a full GC.Collect whenever a segment is allocated, and then again after allocation to compact the heap. /// The frequency which segments are checked to see if they're eligible for cleanup. protected ObjectPoolBase(int itemsPerSegment, int minimumSegmentCount, bool gcOnPoolGrowth, int cleanupFrequenceMS) { Initialize(itemsPerSegment, minimumSegmentCount, gcOnPoolGrowth, cleanupFrequenceMS); } protected void Initialize(int itemsPerSegment, int minimumSegmentCount, bool gcOnPoolGrowth, int cleanupFrequenceMS) { _itemsPerSegment = itemsPerSegment; _minimumSegmentCount = minimumSegmentCount; _gc = gcOnPoolGrowth; // force garbage collection to make sure these new long lived objects // cause as little fragmentation as possible if (_gc) System.GC.Collect(); lock (_syncRoot) { while (_segments.Count < this.MinimumSegmentCount) { ObjectPoolSegment segment = CreateSegment(false); _segments.Add(segment.SegmentNumber, segment); } } // This forces a compact, to make sure our objects fill in any holes in the heap. if (_gc) { System.GC.Collect(); } _timer = new Timer(CleanupThreadCallback, null, cleanupFrequenceMS, cleanupFrequenceMS); } /// /// Forces the segment cleanup algorithm to be run. This method is intended /// primarly for use from the Unit Test libraries. /// internal void ForceCleanup() { CleanupThreadCallback(null); } private void CleanupThreadCallback(object state) { if (_disposed) return; if (Monitor.TryEnter(_timerLock) == false) return; try { lock (_syncRoot) { // If we're below, or at, or minimum segment count threshold, // there's no point in going any further. if (_segments.Count <= _minimumSegmentCount) return; for (int i = _activeSegment; i > 0; i--) { ObjectPoolSegment segment; if (_segments.TryGetValue(i, out segment) == true) { // For the "old" segments that were allocated at startup, this will // always be false, as their expiration dates are set at infinity. if (segment.CanBeCleanedUp()) { _segments.Remove(i); segment.Dispose(); } } } } } finally { Monitor.Exit(_timerLock); } } /// /// Responsible for allocate 1 instance of an object that will be stored in a segment. /// /// An instance of whatever objec the pool is pooling. protected abstract T GetObjectInstance(); private ObjectPoolSegment CreateSegment(bool allowSegmentToBeCleanedUp) { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); if (allowSegmentToBeCleanedUp) Logger.Log("Creating new object pool segment", Helpers.LogLevel.Info); // This method is called inside a lock, so no interlocked stuff required. int segmentToAdd = _activeSegment; _activeSegment++; Queue buffers = new Queue(); for (int i = 1; i <= this._itemsPerSegment; i++) { T obj = GetObjectInstance(); buffers.Enqueue(obj); } // certain segments we don't want to ever be cleaned up (the initial segments) DateTime cleanupTime = (allowSegmentToBeCleanedUp) ? DateTime.Now.Add(this._minimumAgeToCleanup) : DateTime.MaxValue; ObjectPoolSegment segment = new ObjectPoolSegment(segmentToAdd, buffers, cleanupTime); return segment; } /// /// Checks in an instance of T owned by the object pool. This method is only intended to be called /// by the WrappedObject class. /// /// The segment from which the instance is checked out. /// The instance of T to check back into the segment. internal void CheckIn(ObjectPoolSegment owningSegment, T instance) { lock (_syncRoot) { owningSegment.CheckInObject(instance); } } /// /// Checks an instance of T from the pool. If the pool is not sufficient to /// allow the checkout, a new segment is created. /// /// A WrappedObject around the instance of T. To check /// the instance back into the segment, be sureto dispose the WrappedObject /// when finished. public WrappedObject CheckOut() { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); // It's key that this CheckOut always, always, uses a pooled object // from the oldest available segment. This will help keep the "newer" // segments from being used - which in turn, makes them eligible // for deletion. lock (_syncRoot) { ObjectPoolSegment targetSegment = null; // find the oldest segment that has items available for checkout for (int i = 0; i < _activeSegment; i++) { ObjectPoolSegment segment; if (_segments.TryGetValue(i, out segment) == true) { if (segment.AvailableItems > 0) { targetSegment = segment; break; } } } if (targetSegment == null) { // We couldn't find a sigment that had any available space in it, // so it's time to create a new segment. // Before creating the segment, do a GC to make sure the heap // is compacted. if (_gc) GC.Collect(); targetSegment = CreateSegment(true); if (_gc) GC.Collect(); _segments.Add(targetSegment.SegmentNumber, targetSegment); } WrappedObject obj = new WrappedObject(this, targetSegment, targetSegment.CheckOutObject()); return obj; } } /// /// The total number of segments created. Intended to be used by the Unit Tests. /// public int TotalSegments { get { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); lock (_syncRoot) { return _segments.Count; } } } /// /// The number of items that are in a segment. Items in a segment /// are all allocated at the same time, and are hopefully close to /// each other in the managed heap. /// public int ItemsPerSegment { get { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); return _itemsPerSegment; } } /// /// The minimum number of segments. When segments are reclaimed, /// this number of segments will always be left alone. These /// segments are allocated at startup. /// public int MinimumSegmentCount { get { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); return _minimumSegmentCount; } } /// /// The age a segment must be before it's eligible for cleanup. /// This is used to prevent thrash, and typical values are in /// the 5 minute range. /// public TimeSpan MinimumSegmentAgePriorToCleanup { get { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); return _minimumAgeToCleanup; } set { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); _minimumAgeToCleanup = value; } } /// /// The frequence which the cleanup thread runs. This is typically /// expected to be in the 5 minute range. /// public int CleanupFrequencyMilliseconds { get { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); return _cleanupFrequency; } set { if (_disposed) throw new ObjectDisposedException("ObjectPoolBase"); Interlocked.Exchange(ref _cleanupFrequency, value); _timer.Change(_cleanupFrequency, _cleanupFrequency); } } #region IDisposable Members public void Dispose() { if (_disposed) return; Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing) { lock (_syncRoot) { if (_disposed) return; _timer.Dispose(); _disposed = true; foreach (KeyValuePair> kvp in _segments) { try { kvp.Value.Dispose(); } catch (Exception) { } } _segments.Clear(); } } } #endregion } internal class ObjectPoolSegment : IDisposable where T : class { private Queue _liveInstances = new Queue(); private int _segmentNumber; private int _originalCount; private bool _isDisposed = false; private DateTime _eligibleForDeletionAt; public int SegmentNumber { get { return _segmentNumber; } } public int AvailableItems { get { return _liveInstances.Count; } } public DateTime DateEligibleForDeletion { get { return _eligibleForDeletionAt; } } public ObjectPoolSegment(int segmentNumber, Queue liveInstances, DateTime eligibleForDeletionAt) { _segmentNumber = segmentNumber; _liveInstances = liveInstances; _originalCount = liveInstances.Count; _eligibleForDeletionAt = eligibleForDeletionAt; } public bool CanBeCleanedUp() { if (_isDisposed == true) throw new ObjectDisposedException("ObjectPoolSegment"); return ((_originalCount == _liveInstances.Count) && (DateTime.Now > _eligibleForDeletionAt)); } public void Dispose() { if (_isDisposed) return; _isDisposed = true; bool shouldDispose = (typeof(T) is IDisposable); while (_liveInstances.Count != 0) { T instance = _liveInstances.Dequeue(); if (shouldDispose) { try { (instance as IDisposable).Dispose(); } catch (Exception) { } } } } internal void CheckInObject(T o) { if (_isDisposed == true) throw new ObjectDisposedException("ObjectPoolSegment"); _liveInstances.Enqueue(o); } internal T CheckOutObject() { if (_isDisposed == true) throw new ObjectDisposedException("ObjectPoolSegment"); if (0 == _liveInstances.Count) throw new InvalidOperationException("No Objects Available for Checkout"); T o = _liveInstances.Dequeue(); return o; } } }