corrade-vassal – Rev 1

Subversion Repositories:
Rev:
/*
 * 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<T> : IDisposable where T : class
    {
        private T _instance;
        internal readonly ObjectPoolSegment<T> _owningSegment;
        internal readonly ObjectPoolBase<T> _owningObjectPool;
        private bool _disposed = false;

        internal WrappedObject(ObjectPoolBase<T> owningPool, ObjectPoolSegment<T> 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);
        }

        /// <summary>
        /// Returns an instance of the class that has been checked out of the Object Pool.
        /// </summary>
        public T Instance
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException("WrappedObject");
                return _instance;
            }
        }

        /// <summary>
        /// Checks the instance back into the object pool
        /// </summary>
        public void Dispose()
        {
            if (_disposed)
                return;

            _disposed = true;
            _owningObjectPool.CheckIn(_owningSegment, _instance);
            GC.SuppressFinalize(this);
        }
    }

    public abstract class ObjectPoolBase<T> : 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<int, ObjectPoolSegment<T>> _segments = new Dictionary<int, ObjectPoolSegment<T>>();
        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;

        /// <summary>
        /// Creates a new instance of the ObjectPoolBase class. Initialize MUST be called
        /// after using this constructor.
        /// </summary>
        protected ObjectPoolBase()
        {
        }

        /// <summary>
        /// Creates a new instance of the ObjectPool Base class.        
        /// </summary>
        /// <param name="itemsPerSegment">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.
        /// </param>
        /// <param name="minimumSegmentCount">The minimun number of segments that may exist.</param>
        /// <param name="gcOnPoolGrowth">Perform a full GC.Collect whenever a segment is allocated, and then again after allocation to compact the heap.</param>
        /// <param name="cleanupFrequenceMS">The frequency which segments are checked to see if they're eligible for cleanup.</param>
        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<T> 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);
        }

        /// <summary>
        /// Forces the segment cleanup algorithm to be run. This method is intended 
        /// primarly for use from the Unit Test libraries.
        /// </summary>
        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<T> 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);
            }
        }

        /// <summary>
        /// Responsible for allocate 1 instance of an object that will be stored in a segment. 
        /// </summary>
        /// <returns>An instance of whatever objec the pool is pooling.</returns>
        protected abstract T GetObjectInstance();


        private ObjectPoolSegment<T> 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<T> buffers = new Queue<T>();
            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<T> segment = new ObjectPoolSegment<T>(segmentToAdd, buffers, cleanupTime);

            return segment;
        }


        /// <summary>
        /// Checks in an instance of T owned by the object pool. This method is only intended to be called
        /// by the <c>WrappedObject</c> class.
        /// </summary>
        /// <param name="owningSegment">The segment from which the instance is checked out.</param>
        /// <param name="instance">The instance of <c>T</c> to check back into the segment.</param>
        internal void CheckIn(ObjectPoolSegment<T> owningSegment, T instance)
        {
            lock (_syncRoot)
            {
                owningSegment.CheckInObject(instance);
            }
        }

        /// <summary>
        /// Checks an instance of <c>T</c> from the pool. If the pool is not sufficient to 
        /// allow the checkout, a new segment is created. 
        /// </summary>
        /// <returns>A <c>WrappedObject</c> around the instance of <c>T</c>. To check
        /// the instance back into the segment, be sureto dispose the WrappedObject 
        /// when finished. </returns>
        public WrappedObject<T> 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<T> targetSegment = null;

                // find the oldest segment that has items available for checkout
                for (int i = 0; i < _activeSegment; i++)
                {
                    ObjectPoolSegment<T> 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<T> obj = new WrappedObject<T>(this, targetSegment, targetSegment.CheckOutObject());
                return obj;
            }
        }

        /// <summary>
        /// The total number of segments created. Intended to be used by the Unit Tests.
        /// </summary>
        public int TotalSegments
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException("ObjectPoolBase");

                lock (_syncRoot)
                {
                    return _segments.Count;
                }
            }
        }

        /// <summary>
        /// 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. 
        /// </summary>
        public int ItemsPerSegment
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException("ObjectPoolBase");

                return _itemsPerSegment;
            }
        }

        /// <summary>
        /// The minimum number of segments. When segments are reclaimed, 
        /// this number of segments will always be left alone. These
        /// segments are allocated at startup.
        /// </summary>
        public int MinimumSegmentCount
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException("ObjectPoolBase");

                return _minimumSegmentCount;
            }
        }

        /// <summary>
        /// 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.
        /// </summary>
        public TimeSpan MinimumSegmentAgePriorToCleanup
        {
            get
            {
                if (_disposed)
                    throw new ObjectDisposedException("ObjectPoolBase");

                return _minimumAgeToCleanup;
            }
            set
            {
                if (_disposed)
                    throw new ObjectDisposedException("ObjectPoolBase");

                _minimumAgeToCleanup = value;
            }
        }

        /// <summary>
        /// The frequence which the cleanup thread runs. This is typically
        /// expected to be in the 5 minute range. 
        /// </summary>
        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<int, ObjectPoolSegment<T>> kvp in _segments)
                    {
                        try
                        {
                            kvp.Value.Dispose();
                        }
                        catch (Exception) { }
                    }

                    _segments.Clear();
                }
            }
        }

        #endregion
    }

    internal class ObjectPoolSegment<T> : IDisposable where T : class
    {
        private Queue<T> _liveInstances = new Queue<T>();
        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<T> 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;
        }
    }
}