diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index e7748124ba..6917c59910 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -73,6 +73,7 @@ what it is today. * Ewe Loon * Fly-Man * Flyte Xevious +* Imaze Rhiano * Intimidated * Jeremy Bongio (IBM) * jhurliman diff --git a/OpenSim/Framework/CnmMemoryCache.cs b/OpenSim/Framework/CnmMemoryCache.cs new file mode 100644 index 0000000000..6ca71ecbd8 --- /dev/null +++ b/OpenSim/Framework/CnmMemoryCache.cs @@ -0,0 +1,1852 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// +// +// +// +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace OpenSim.Framework +{ + /// + /// Cenome memory based cache to store key/value pairs (elements) limited time and/or limited size. + /// + /// + /// The type of keys in the cache. + /// + /// + /// The type of values in the dictionary. + /// + /// + /// + /// Cenome memory cache stores elements to hash table generations. When new element is being added to cache, and new size would exceed + /// maximal allowed size or maximal amount of allowed element count, then elements in oldest generation are deleted. Last access time + /// is also tracked in generation level - thus it is possible that some elements are staying in cache far beyond their expiration time. + /// If elements in older generations are accessed through method, they are moved to newest generation. + /// + /// + public class CnmMemoryCache : ICnmCache + { + /// + /// Default maximal count. + /// + /// + public const int DefaultMaxCount = 4096; + + /// + /// Default maximal size. + /// + /// + /// + /// 128MB = 128 * 1024^2 = 134 217 728 bytes. + /// + /// + /// + public const long DefaultMaxSize = 134217728; + + /// + /// How many operations between time checks. + /// + private const int DefaultOperationsBetweenTimeChecks = 40; + + /// + /// Default expiration time. + /// + /// + /// + /// 30 minutes. + /// + /// + public static readonly TimeSpan DefaultExpirationTime = TimeSpan.FromMinutes( 30.0 ); + + /// + /// Minimal allowed expiration time. + /// + /// + /// + /// 5 minutes. + /// + /// + public static readonly TimeSpan MinExpirationTime = TimeSpan.FromSeconds( 10.0 ); + + /// + /// Comparer used to compare element keys. + /// + /// + /// Comparer is initialized by constructor. + /// + /// + public readonly IEqualityComparer Comparer; + + /// + /// Expiration time. + /// + private TimeSpan m_expirationTime = DefaultExpirationTime; + + /// + /// Generation bucket count. + /// + private int m_generationBucketCount; + + /// + /// Generation entry count. + /// + private int m_generationElementCount; + + /// + /// Generation max size. + /// + private long m_generationMaxSize; + + /// + /// Maximal allowed count of elements. + /// + private int m_maxCount; + + /// + /// Maximal allowed total size of elements. + /// + private long m_maxElementSize; + + /// + /// Maximal size. + /// + private long m_maxSize; + + /// + /// New generation. + /// + private IGeneration m_newGeneration; + + /// + /// Old generation. + /// + private IGeneration m_oldGeneration; + + /// + /// Operations between time check. + /// + private int m_operationsBetweenTimeChecks = DefaultOperationsBetweenTimeChecks; + + /// + /// Synchronization root object, should always be private and exists always + /// + private readonly object m_syncRoot = new object(); + + /// + /// Version of cache. + /// + /// + /// + /// Updated every time when cache has been changed (element removed, expired, added, replaced). + /// + /// + private int m_version; + + /// + /// Initializes a new instance of the class. + /// + public CnmMemoryCache() + : this( DefaultMaxSize ) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Maximal cache size. + /// + public CnmMemoryCache( long maximalSize ) + : this( maximalSize, DefaultMaxCount ) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Maximal cache size. + /// + /// + /// Maximal element count. + /// + public CnmMemoryCache( long maximalSize, int maximalCount ) + : this( maximalSize, maximalCount, DefaultExpirationTime ) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Maximal cache size. + /// + /// + /// Maximal element count. + /// + /// + /// Elements expiration time. + /// + public CnmMemoryCache( long maximalSize, int maximalCount, TimeSpan expirationTime ) + : this( maximalSize, maximalCount, expirationTime, EqualityComparer.Default ) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// Maximal cache size. + /// + /// + /// Maximal element count. + /// + /// + /// Elements expiration time. + /// + /// + /// Comparer used for comparing elements. + /// + /// + /// is reference. + /// + public CnmMemoryCache( long maximalSize, + int maximalCount, + TimeSpan expirationTime, + IEqualityComparer comparer ) + { + if( comparer == null ) + throw new ArgumentNullException( "comparer" ); + + if( expirationTime < MinExpirationTime ) + expirationTime = MinExpirationTime; + if( maximalCount < 8 ) + maximalCount = 8; + if( maximalSize < 8 ) + maximalSize = 8; + if( maximalCount > maximalSize ) + maximalCount = (int) maximalSize; + + Comparer = comparer; + m_expirationTime = expirationTime; + m_maxSize = maximalSize; + m_maxCount = maximalCount; + + Initialize(); + } + + /// + /// Add element to new generation. + /// + /// + /// The bucket index. + /// + /// + /// The element's key. + /// + /// + /// The element's value. + /// + /// + /// The element's size. + /// + protected virtual void AddToNewGeneration( int bucketIndex, TKey key, TValue value, long size ) + { + // Add to newest generation + if( !m_newGeneration.Set( bucketIndex, key, value, size ) ) + { + // Failed to add new generation + RecycleGenerations(); + m_newGeneration.Set( bucketIndex, key, value, size ); + } + + m_version++; + } + + /// + /// + /// Get keys bucket index. + /// + /// + /// + /// + /// Key which bucket index is being retrieved. + /// + /// + /// + /// + /// Bucket index. + /// + /// + /// + /// + /// Method uses to calculate hash code. + /// + /// + /// Bucket index is remainder when element key's hash value is divided by bucket count. + /// + /// + /// For example: key's hash is 72, bucket count is 5, element's bucket index is 72 % 5 = 2. + /// + /// + protected virtual int GetBucketIndex( TKey key ) + { + return (Comparer.GetHashCode( key ) & 0x7FFFFFFF) % m_generationBucketCount; + } + + /// + /// Purge generation from the cache. + /// + /// + /// The generation that is purged. + /// + protected virtual void PurgeGeneration( IGeneration generation ) + { + generation.Clear(); + m_version++; + } + + /// + /// check expired. + /// + private void CheckExpired() + { + // Do this only one in every m_operationsBetweenTimeChecks + // Fetching time is using several millisecons - it is better not to do all time. + m_operationsBetweenTimeChecks--; + if( m_operationsBetweenTimeChecks <= 0 ) + PurgeExpired(); + } + + /// + /// Initialize cache. + /// + private void Initialize() + { + m_version++; + + m_generationMaxSize = MaxSize / 2; + MaxElementSize = MaxSize / 8; + m_generationElementCount = MaxCount / 2; + + // Buckets need to be prime number to get better spread of hash values + m_generationBucketCount = PrimeNumberHelper.GetPrime( m_generationElementCount ); + + m_newGeneration = new HashGeneration( this ); + m_oldGeneration = new HashGeneration( this ); + m_oldGeneration.MakeOld(); + } + + /// + /// Recycle generations. + /// + private void RecycleGenerations() + { + // Rotate old generation to new generation, new generation to old generation + IGeneration temp = m_newGeneration; + m_newGeneration = m_oldGeneration; + m_newGeneration.Clear(); + m_oldGeneration = temp; + m_oldGeneration.MakeOld(); + } + + #region Nested type: Enumerator + + /// + /// Key and value pair enumerator. + /// + private class Enumerator : IEnumerator> + { + /// + /// Current enumerator. + /// + private int m_currentEnumerator = -1; + + /// + /// Enumerators to different generations. + /// + private readonly IEnumerator>[] m_generationEnumerators = + new IEnumerator>[2]; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The cache. + /// + public Enumerator( CnmMemoryCache cache ) + { + m_generationEnumerators[ 0 ] = cache.m_newGeneration.GetEnumerator(); + m_generationEnumerators[ 1 ] = cache.m_oldGeneration.GetEnumerator(); + } + + #region IEnumerator> Members + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + /// + /// The element in the collection at the current position of the enumerator. + /// + /// + /// The enumerator has reach end of collection or is not called. + /// + public KeyValuePair Current + { + get + { + if( m_currentEnumerator == -1 || m_currentEnumerator >= m_generationEnumerators.Length ) + throw new InvalidOperationException(); + + return m_generationEnumerators[ m_currentEnumerator ].Current; + } + } + + /// + /// Gets the current element in the collection. + /// + /// + /// The current element in the collection. + /// + /// + /// The enumerator is positioned before the first element of the collection or after the last element. + /// 2 + object IEnumerator.Current + { + get { return Current; } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// 2 + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; if the enumerator has passed the end of the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + /// 2 + public bool MoveNext() + { + if( m_currentEnumerator == -1 ) + m_currentEnumerator = 0; + + while( m_currentEnumerator < m_generationEnumerators.Length ) + { + if( m_generationEnumerators[ m_currentEnumerator ].MoveNext() ) + return true; + + m_currentEnumerator++; + } + + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + /// 2 + public void Reset() + { + foreach( IEnumerator> enumerator in m_generationEnumerators ) + { + enumerator.Reset(); + } + + m_currentEnumerator = -1; + } + + #endregion + } + + #endregion + + #region Nested type: HashGeneration + + /// + /// Hash generation class + /// + /// + /// + /// Current implementation is based to separated chaining with move-to-front heuristics. Hash generations have fixed + /// amount of buckets and it is never rehashed. + /// + /// + /// Read more about hash tables from Wiki article. + /// + /// + /// + private class HashGeneration : IGeneration + { + /// + /// Value indicating whether generation was accessed since last time check. + /// + private bool m_accessedSinceLastTimeCheck; + + /// + /// Index of first element's in element chain. + /// + /// + /// -1 if there is no element in bucket; otherwise first element's index in the element chain. + /// + /// + /// Bucket index is remainder when element key's hash value is divided by bucket count. + /// For example: key's hash is 72, bucket count is 5, element's bucket index is 72 % 5 = 2. + /// + private readonly int[] m_buckets; + + /// + /// Cache object. + /// + private readonly CnmMemoryCache m_cache; + + /// + /// Generation's element array. + /// + /// + private readonly Element[] m_elements; + + /// + /// Generation's expiration time. + /// + private DateTime m_expirationTime1; + + /// + /// Index to first free element. + /// + private int m_firstFreeElement; + + /// + /// Free element count. + /// + /// + /// When generation is cleared or constructed, this is NOT set to element count. + /// This is only tracking elements that are removed and are currently free. + /// + private int m_freeCount; + + /// + /// Is this generation "new generation". + /// + private bool m_newGeneration; + + /// + /// Next unused entry. + /// + private int m_nextUnusedElement; + + /// + /// Size of data stored to generation. + /// + private long m_size; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The cache. + /// + public HashGeneration( CnmMemoryCache cache ) + { + m_cache = cache; + m_elements = new Element[m_cache.m_generationElementCount]; + m_buckets = new int[m_cache.m_generationBucketCount]; + Clear(); + } + + /// + /// Find element's index + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// Move element to front of elements. + /// + /// + /// The previous element's index. + /// + /// + /// Element's index, if found from the generation; -1 otherwise (if element is not found the generation). + /// + private int FindElementIndex( int bucketIndex, TKey key, bool moveToFront, out int previousIndex ) + { + previousIndex = -1; + int elementIndex = m_buckets[ bucketIndex ]; + while( elementIndex >= 0 ) + { + if( m_cache.Comparer.Equals( key, m_elements[ elementIndex ].Key ) ) + { + // Found match + if( moveToFront && previousIndex >= 0 ) + { + // Move entry to front + m_elements[ previousIndex ].Next = m_elements[ elementIndex ].Next; + m_elements[ elementIndex ].Next = m_buckets[ bucketIndex ]; + m_buckets[ bucketIndex ] = elementIndex; + previousIndex = 0; + } + + return elementIndex; + } + + previousIndex = elementIndex; + elementIndex = m_elements[ elementIndex ].Next; + } + + return -1; + } + + /// + /// Remove element front the generation. + /// + /// + /// The bucket index. + /// + /// + /// The element index. + /// + /// + /// The element's previous index. + /// + private void RemoveElement( int bucketIndex, int entryIndex, int previousIndex ) + { + if( previousIndex >= 0 ) + m_elements[ previousIndex ].Next = m_elements[ entryIndex ].Next; + else + m_buckets[ bucketIndex ] = m_elements[ entryIndex ].Next; + + Size -= m_elements[ entryIndex ].Size; + m_elements[ entryIndex ].Value = default(TValue); + m_elements[ entryIndex ].Key = default(TKey); + + // Add element to free elements list + m_elements[ entryIndex ].Next = m_firstFreeElement; + m_firstFreeElement = entryIndex; + m_freeCount++; + } + + #region Nested type: Element + + /// + /// Element that stores key, next element in chain, size and value. + /// + private struct Element + { + /// + /// Element's key. + /// + public TKey Key; + + /// + /// Next element in chain. + /// + /// + /// When element have value (something is stored to it), this is index of + /// next element with same bucket index. When element is free, this + /// is index of next element in free element's list. + /// + public int Next; + + /// + /// Size of element. + /// + /// + /// 0 if element is free; otherwise larger than 0. + /// + public long Size; + + /// + /// Element's value. + /// + /// + /// It is possible that this value is even when element + /// have value - element's value is then reference. + /// + public TValue Value; + + /// + /// Gets a value indicating whether element is free or have value. + /// + /// + /// when element is free; otherwise . + /// + public bool IsFree + { + get { return Size == 0; } + } + } + + #endregion + + #region Nested type: Enumerator + + /// + /// Key value pair enumerator for object. + /// + private class Enumerator : IEnumerator> + { + /// + /// Current element. + /// + private KeyValuePair m_current; + + /// + /// Current index. + /// + private int m_currentIndex; + + /// + /// Generation that is being enumerated. + /// + private readonly HashGeneration m_generation; + + /// + /// Cache version. + /// + /// + /// When cache is change, version number is changed. + /// + /// + private readonly int m_version; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The generation. + /// + public Enumerator( HashGeneration generation ) + { + m_generation = generation; + m_version = m_generation.m_cache.m_version; + } + + #region IEnumerator> Members + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + /// + /// The element in the collection at the current position of the enumerator. + /// + /// + /// The enumerator has reach end of collection or is not called. + /// + public KeyValuePair Current + { + get + { + if( m_currentIndex == 0 || m_currentIndex >= m_generation.Count ) + throw new InvalidOperationException(); + + return m_current; + } + } + + /// + /// Gets the current element in the collection. + /// + /// + /// The current element in the collection. + /// + /// + /// The enumerator is positioned before the first element of the collection or after the last element. + /// 2 + object IEnumerator.Current + { + get { return Current; } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// 2 + public void Dispose() + { + } + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public bool MoveNext() + { + if( m_version != m_generation.m_cache.m_version ) + throw new InvalidOperationException(); + + while( m_currentIndex < m_generation.Count ) + { + if( m_generation.m_elements[ m_currentIndex ].IsFree ) + { + m_currentIndex++; + continue; + } + + m_current = new KeyValuePair( m_generation.m_elements[ m_currentIndex ].Key, + m_generation.m_elements[ m_currentIndex ].Value ); + m_currentIndex++; + return true; + } + + m_current = new KeyValuePair(); + return false; + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + /// 2 + public void Reset() + { + if( m_version != m_generation.m_cache.m_version ) + throw new InvalidOperationException(); + + m_currentIndex = 0; + } + + #endregion + } + + #endregion + + #region IGeneration Members + + /// + /// Gets or sets a value indicating whether generation was accessed since last time check. + /// + public bool AccessedSinceLastTimeCheck + { + get { return m_accessedSinceLastTimeCheck; } + + set { m_accessedSinceLastTimeCheck = value; } + } + + /// + /// Gets element count in generation. + /// + public int Count + { + get { return m_nextUnusedElement - m_freeCount; } + } + + /// + /// Gets or sets generation's expiration time. + /// + public DateTime ExpirationTime + { + get { return m_expirationTime1; } + + set { m_expirationTime1 = value; } + } + + /// + /// Gets or sets size of data stored to generation. + /// + public long Size + { + get { return m_size; } + + private set { m_size = value; } + } + + /// + /// Clear all elements from the generation and make generation new again. + /// + /// + /// When generation is new, it is allowed to add new elements to it and + /// doesn't remove elements from it. + /// + /// + public void Clear() + { + for( int i = m_buckets.Length - 1 ; i >= 0 ; i-- ) + { + m_buckets[ i ] = -1; + } + + Array.Clear( m_elements, 0, m_elements.Length ); + Size = 0; + m_firstFreeElement = -1; + m_freeCount = 0; + m_nextUnusedElement = 0; + m_newGeneration = true; + ExpirationTime = DateTime.MaxValue; + } + + /// + /// Determines whether the contains an element with the specific key. + /// + /// + /// The bucket index for the to locate in . + /// + /// + /// The key to locate in the . + /// + /// + /// if the contains an element with the ; + /// otherwise . + /// + public bool Contains( int bucketIndex, TKey key ) + { + int previousIndex; + if( FindElementIndex( bucketIndex, key, true, out previousIndex ) == -1 ) + return false; + + AccessedSinceLastTimeCheck = true; + return true; + } + + /// + /// Returns an enumerator that iterates through the elements stored . + /// + /// + /// A that can be used to iterate through the . + /// + /// 1 + public IEnumerator> GetEnumerator() + { + return new Enumerator( this ); + } + + /// + /// Make from generation old generation. + /// + /// + /// When generation is old, hit removes element from the generation. + /// + /// + public void MakeOld() + { + m_newGeneration = false; + } + + /// + /// Remove element associated with the key from the generation. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// , if remove was successful; otherwise . + /// + public bool Remove( int bucketIndex, TKey key ) + { + int previousIndex; + int entryIndex = FindElementIndex( bucketIndex, key, false, out previousIndex ); + if( entryIndex != -1 ) + { + RemoveElement( bucketIndex, entryIndex, previousIndex ); + AccessedSinceLastTimeCheck = true; + return true; + } + + return false; + } + + /// + /// Set or add element to generation. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// The element's value. + /// + /// + /// The element's size. + /// + /// + /// , if setting or adding was successful; otherwise . + /// + /// + /// + /// If element was already existing in generation and new element size fits to collection limits, + /// then it's value is replaced with new one and size information is updated. If element didn't + /// exists in generation before, then generation must have empty space for a new element and + /// size must fit generation's limits, before element is added to generation. + /// + /// + public bool Set( int bucketIndex, TKey key, TValue value, long size ) + { + Debug.Assert( m_newGeneration, "It is possible to insert new elements only to newest generation." ); + Debug.Assert( size > 0, "New element size should be more than 0." ); + + int previousIndex; + int elementIndex = FindElementIndex( bucketIndex, key, true, out previousIndex ); + if( elementIndex == -1 ) + { + // New key + if( Size + size > m_cache.m_generationMaxSize || + (m_nextUnusedElement == m_cache.m_generationElementCount && m_freeCount == 0) ) + { + // Generation is full + return false; + } + + // Increase size of generation + Size += size; + + // Get first free entry and update free entry list + if( m_firstFreeElement != -1 ) + { + // There was entry that was removed + elementIndex = m_firstFreeElement; + m_firstFreeElement = m_elements[ elementIndex ].Next; + m_freeCount--; + } + else + { + // No entries removed so far - just take a last one + elementIndex = m_nextUnusedElement; + m_nextUnusedElement++; + } + + Debug.Assert( m_elements[ elementIndex ].IsFree, "Allocated element is not free." ); + + // Move new entry to front + m_elements[ elementIndex ].Next = m_buckets[ bucketIndex ]; + m_buckets[ bucketIndex ] = elementIndex; + + // Set key and update count + m_elements[ elementIndex ].Key = key; + } + else + { + // Existing key + if( Size - m_elements[ elementIndex ].Size + size > m_cache.m_generationMaxSize ) + { + // Generation is full + // Remove existing element, because generation is going to be recycled to + // old generation and element is stored to new generation + RemoveElement( bucketIndex, elementIndex, previousIndex ); + return false; + } + + // Update generation's size + Size = Size - m_elements[ elementIndex ].Size + size; + } + + // Finally set value and size + m_elements[ elementIndex ].Value = value; + m_elements[ elementIndex ].Size = size; + + // Success - key was inserterted to generation + AccessedSinceLastTimeCheck = true; + return true; + } + + /// + /// Try to get element associated with key. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// The element's value. + /// + /// + /// The element's size. + /// + /// + /// , if element was successful retrieved; otherwise . + /// + /// + /// + /// If element is not found from generation then and + /// are set to default value (default(TValue) and 0). + /// + /// + public bool TryGetValue( int bucketIndex, TKey key, out TValue value, out long size ) + { + // Find entry index, + int previousIndex; + int elementIndex = FindElementIndex( bucketIndex, key, m_newGeneration, out previousIndex ); + if( elementIndex == -1 ) + { + value = default(TValue); + size = 0; + return false; + } + + value = m_elements[ elementIndex ].Value; + size = m_elements[ elementIndex ].Size; + + if( !m_newGeneration ) + { + // Old generation - remove element, because it is moved to new generation + RemoveElement( bucketIndex, elementIndex, previousIndex ); + } + + AccessedSinceLastTimeCheck = true; + return true; + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } + + #endregion + + #region Nested type: IGeneration + + /// + /// Cache element generation interface + /// + /// + /// + /// Generation can hold limited count of elements and limited size of data. + /// + /// + /// There are two kind generations: "new generation" and "old generation(s)". All new elements + /// are added to "new generation". + /// + /// + protected interface IGeneration : IEnumerable> + { + /// + /// Gets or sets a value indicating whether generation was accessed since last time check. + /// + bool AccessedSinceLastTimeCheck { get; set; } + + /// + /// Gets element count in generation. + /// + int Count { get; } + + /// + /// Gets or sets generation's expiration time. + /// + DateTime ExpirationTime { get; set; } + + /// + /// Gets size of data stored to generation. + /// + long Size { get; } + + /// + /// Clear all elements from the generation and make generation new again. + /// + /// + /// When generation is new, it is allowed to add new elements to it and + /// doesn't remove elements from it. + /// + /// + void Clear(); + + /// + /// Determines whether the contains an element with the specific key. + /// + /// + /// The bucket index for the to locate in . + /// + /// + /// The key to locate in the . + /// + /// + /// if the contains an element with the ; + /// otherwise . + /// + bool Contains( int bucketIndex, TKey key ); + + /// + /// Make from generation old generation. + /// + /// + /// When generation is old, hit removes element from the generation. + /// + /// + void MakeOld(); + + /// + /// Remove element associated with the key from the generation. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// , if remove was successful; otherwise . + /// + bool Remove( int bucketIndex, TKey key ); + + /// + /// Set or add element to generation. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// The element's value. + /// + /// + /// The element's size. + /// + /// + /// , if setting or adding was successful; otherwise . + /// + /// + /// + /// If element was already existing in generation and new element size fits to collection limits, + /// then it's value is replaced with new one and size information is updated. If element didn't + /// exists in generation before, then generation must have empty space for a new element and + /// size must fit generation's limits, before element is added to generation. + /// + /// + bool Set( int bucketIndex, TKey key, TValue value, long size ); + + /// + /// Try to get element associated with key. + /// + /// + /// The element's bucket index. + /// + /// + /// The element's key. + /// + /// + /// The element's value. + /// + /// + /// The element's size. + /// + /// + /// , if element was successful retrieved; otherwise . + /// + /// + /// + /// If element is not found from generation then and + /// are set to default value (default(TValue) and 0). + /// + /// + bool TryGetValue( int bucketIndex, TKey key, out TValue value, out long size ); + } + + #endregion + + #region ICnmCache Members + + /// + /// Gets current count of elements stored to . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + public int Count + { + get { return m_newGeneration.Count + m_oldGeneration.Count; } + } + + /// + /// Gets or sets elements expiration time. + /// + /// + /// Elements expiration time. + /// + /// + /// + /// When element has been stored in longer than + /// and it is not accessed through method or element's value is + /// not replaced by method, then it is automatically removed from the + /// . + /// + /// + /// It is possible that implementation removes element before it's expiration time, + /// because total size or count of elements stored to cache is larger than or . + /// + /// + /// It is also possible that element stays in cache longer than . + /// + /// + /// Calling try to remove all elements that are expired. + /// + /// + /// To disable time limit in cache, set to . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TimeSpan ExpirationTime + { + get { return m_expirationTime; } + + set + { + if( value < MinExpirationTime ) + value = MinExpirationTime; + + if( m_expirationTime == value ) + return; + + m_newGeneration.ExpirationTime = (m_newGeneration.ExpirationTime - m_expirationTime) + value; + m_oldGeneration.ExpirationTime = (m_oldGeneration.ExpirationTime - m_expirationTime) + value; + m_expirationTime = value; + + PurgeExpired(); + } + } + + /// + /// Gets a value indicating whether is limiting count of elements. + /// + /// + /// if the count of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + public bool IsCountLimited + { + get { return true; } + } + + /// + /// Gets a value indicating whether is limiting size of elements. + /// + /// + /// if the total size of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + public bool IsSizeLimited + { + get { return true; } + } + + /// + /// Gets a value indicating whether or not access to the is synchronized (thread safe). + /// + /// + /// if access to the is synchronized (thread safe); + /// otherwise, . + /// + /// + /// + /// To get synchronized (thread safe) access to object, use + /// in class + /// to retrieve synchronized wrapper for object. + /// + /// + /// + /// + public bool IsSynchronized + { + get { return false; } + } + + /// + /// Gets a value indicating whether elements stored to have limited inactivity time. + /// + /// + /// if the has a fixed total size of elements; + /// otherwise, . + /// + /// + /// If have limited inactivity time and element is not accessed through + /// or methods in , then element is automatically removed from + /// the cache. Depending on implementation of the , some of the elements may + /// stay longer in cache. + /// + /// + /// + /// + /// + public bool IsTimeLimited + { + get { return ExpirationTime != TimeSpan.MaxValue; } + } + + /// + /// Gets or sets maximal allowed count of elements that can be stored to . + /// + /// + /// , if is not limited by count of elements; + /// otherwise maximal allowed count of elements. + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + public int MaxCount + { + get { return m_maxCount; } + + set + { + if( value < 8 ) + value = 8; + if( m_maxCount == value ) + return; + + m_maxCount = value; + Initialize(); + } + } + + /// + /// Gets maximal allowed element size. + /// + /// + /// Maximal allowed element size. + /// + /// + /// + /// If element's size is larger than , then element is + /// not added to the . + /// + /// + /// + /// + /// + /// + public long MaxElementSize + { + get { return m_maxElementSize; } + + private set { m_maxElementSize = value; } + } + + /// + /// Gets or sets maximal allowed total size for elements stored to . + /// + /// + /// Maximal allowed total size for elements stored to . + /// + /// + /// + /// Normally size is total bytes used by elements in the cache. But it can be any other suitable unit of measure. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + public long MaxSize + { + get { return m_maxSize; } + + set + { + if( value < 8 ) + value = 8; + if( m_maxSize == value ) + return; + + m_maxSize = value; + Initialize(); + } + } + + /// + /// Gets total size of elements stored to . + /// + /// + /// Total size of elements stored to . + /// + /// + /// + /// Normally bytes, but can be any suitable unit of measure. + /// + /// + /// Element's size is given when element is added or replaced by method. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + public long Size + { + get { return m_newGeneration.Size + m_oldGeneration.Size; } + } + + /// + /// Gets an object that can be used to synchronize access to the . + /// + /// + /// An object that can be used to synchronize access to the . + /// + /// + /// + /// To get synchronized (thread safe) access to , use + /// method to retrieve synchronized wrapper interface to + /// . + /// + /// + /// + /// + public object SyncRoot + { + get { return m_syncRoot; } + } + + /// + /// Removes all elements from the . + /// + /// + /// + /// + /// + /// + public void Clear() + { + m_newGeneration.Clear(); + m_oldGeneration.Clear(); + m_oldGeneration.MakeOld(); + m_version++; + } + + /// + /// Returns an enumerator that iterates through the elements stored to . + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator> GetEnumerator() + { + return new Enumerator( this ); + } + + /// + /// Purge expired elements from the . + /// + /// + /// + /// Element becomes expired when last access time to it has been longer time than . + /// + /// + /// Depending on implementation, some of expired elements + /// may stay longer than in the cache. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void PurgeExpired() + { + m_operationsBetweenTimeChecks = DefaultOperationsBetweenTimeChecks; + + if( !IsTimeLimited ) + return; + + DateTime now = DateTime.Now; + if( m_newGeneration.AccessedSinceLastTimeCheck ) + { + // New generation has been accessed since last check + // Update it's expiration time. + m_newGeneration.ExpirationTime = now + ExpirationTime; + m_newGeneration.AccessedSinceLastTimeCheck = false; + } + else if( m_newGeneration.ExpirationTime < now ) + { + // New generation has been expired. + // --> also old generation must be expired. + PurgeGeneration( m_newGeneration ); + PurgeGeneration( m_oldGeneration ); + return; + } + + if( m_oldGeneration.ExpirationTime < now ) + PurgeGeneration( m_oldGeneration ); + } + + /// + /// Removes element associated with from the . + /// + /// + /// The key that is associated with element to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public void Remove( TKey key ) + { + if( key == null ) + throw new ArgumentNullException( "key" ); + + int bucketIndex = GetBucketIndex( key ); + if( !m_newGeneration.Remove( bucketIndex, key ) ) + { + if( !m_oldGeneration.Remove( bucketIndex, key ) ) + { + CheckExpired(); + return; + } + } + + CheckExpired(); + m_version++; + } + + /// + /// Removes elements that are associated with one of from the . + /// + /// + /// The keys that are associated with elements to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public void RemoveRange( IEnumerable keys ) + { + if( keys == null ) + throw new ArgumentNullException( "keys" ); + + foreach( TKey key in keys ) + { + if( key == null ) + continue; + + int bucketIndex = GetBucketIndex( key ); + if( !m_newGeneration.Remove( bucketIndex, key ) ) + m_oldGeneration.Remove( bucketIndex, key ); + } + + CheckExpired(); + m_version++; + } + + /// + /// Add or replace an element with the provided , and to + /// . + /// + /// + /// The object used as the key of the element. Can't be reference. + /// + /// + /// The object used as the value of the element to add or replace. is allowed. + /// + /// + /// The element's size. Normally bytes, but can be any suitable unit of measure. + /// + /// + /// if element has been added successfully to the ; + /// otherwise . + /// + /// + /// is . + /// + /// + /// The element's is less than 0. + /// + /// + /// + /// If element's is larger than , then element is + /// not added to the , however - possible older element is + /// removed from the . + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public bool Set( TKey key, TValue value, long size ) + { + if( key == null ) + throw new ArgumentNullException( "key" ); + + if( size < 0 ) + throw new ArgumentOutOfRangeException( "size", size, "Value's size can't be less than 0." ); + + if( size > MaxElementSize ) + { + // Entry size is too big to fit cache - ignore it + Remove( key ); + return false; + } + + if( size == 0 ) + size = 1; + + int bucketIndex = GetBucketIndex( key ); + m_oldGeneration.Remove( bucketIndex, key ); + AddToNewGeneration( bucketIndex, key, value, size ); + CheckExpired(); + + return true; + } + + /// + /// Gets the associated with the specified . + /// + /// + /// if the contains an element with + /// the specified key; otherwise, . + /// + /// + /// The key whose to get. + /// + /// + /// When this method returns, the value associated with the specified , + /// if the is found; otherwise, the + /// default value for the type of the parameter. This parameter is passed uninitialized. + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public bool TryGetValue( TKey key, out TValue value ) + { + if( key == null ) + throw new ArgumentNullException( "key" ); + + int bucketIndex = GetBucketIndex( key ); + long size; + if( m_newGeneration.TryGetValue( bucketIndex, key, out value, out size ) ) + { + CheckExpired(); + return true; + } + + if( m_oldGeneration.TryGetValue( bucketIndex, key, out value, out size ) ) + { + // Move element to new generation + AddToNewGeneration( bucketIndex, key, value, size ); + CheckExpired(); + return true; + } + + CheckExpired(); + return false; + } + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// + /// An object that can be used to iterate through the collection. + /// + /// 2 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/OpenSim/Framework/CnmSynchronizedCache.cs b/OpenSim/Framework/CnmSynchronizedCache.cs new file mode 100644 index 0000000000..418a095e5d --- /dev/null +++ b/OpenSim/Framework/CnmSynchronizedCache.cs @@ -0,0 +1,746 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project 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 DEVELOPERS ``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 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 OpenSim.Framework +{ + /// + /// Synchronized Cenome cache wrapper. + /// + /// + /// The type of keys in the cache. + /// + /// + /// The type of values in the cache. + /// + /// + /// + /// Enumerator will block other threads, until enumerator's method is called. + /// "foreach" statement is automatically calling it. + /// + /// + public class CnmSynchronizedCache : ICnmCache + { + /// + /// The cache object. + /// + private readonly ICnmCache m_cache; + + /// + /// Synchronization root. + /// + private readonly object m_syncRoot; + + /// + /// Initializes a new instance of the class. + /// Initializes a new instance of the class. + /// + /// + /// The cache. + /// + private CnmSynchronizedCache( ICnmCache cache ) + { + m_cache = cache; + m_syncRoot = m_cache.SyncRoot; + } + + /// + /// Returns a wrapper that is synchronized (thread safe). + /// + /// + /// The to synchronize. + /// + /// + /// A wrapper that is synchronized (thread safe). + /// + /// + /// is null. + /// + public static ICnmCache Synchronized( ICnmCache cache ) + { + if( cache == null ) + throw new ArgumentNullException( "cache" ); + return cache.IsSynchronized ? cache : new CnmSynchronizedCache( cache ); + } + + #region Nested type: SynchronizedEnumerator + + /// + /// Synchronized enumerator. + /// + private class SynchronizedEnumerator : IEnumerator> + { + /// + /// Enumerator that is being synchronized. + /// + private readonly IEnumerator> m_enumerator; + + /// + /// Synchronization root. + /// + private object m_syncRoot; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The enumerator that is being synchronized. + /// + /// + /// The sync root. + /// + public SynchronizedEnumerator( IEnumerator> enumerator, object syncRoot ) + { + m_syncRoot = syncRoot; + m_enumerator = enumerator; + Monitor.Enter( m_syncRoot ); + } + + /// + /// Finalizes an instance of the class. + /// + ~SynchronizedEnumerator() + { + Dispose(); + } + + #region IEnumerator> Members + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + /// + /// The element in the collection at the current position of the enumerator. + /// + /// + /// The enumerator has reach end of collection or is not called. + /// + public KeyValuePair Current + { + get { return m_enumerator.Current; } + } + + /// + /// Gets the current element in the collection. + /// + /// + /// The current element in the collection. + /// + /// + /// The enumerator is positioned before the first element of the collection or after the last element. + /// 2 + object IEnumerator.Current + { + get { return Current; } + } + + /// + /// Releases synchronization lock. + /// + public void Dispose() + { + if( m_syncRoot != null ) + { + Monitor.Exit( m_syncRoot ); + m_syncRoot = null; + } + + m_enumerator.Dispose(); + GC.SuppressFinalize( this ); + } + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// true if the enumerator was successfully advanced to the next element; false if the enumerator has passed the end of the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public bool MoveNext() + { + return m_enumerator.MoveNext(); + } + + /// + /// Sets the enumerator to its initial position, which is before the first element in the collection. + /// + /// + /// The collection was modified after the enumerator was created. + /// + public void Reset() + { + m_enumerator.Reset(); + } + + #endregion + } + + #endregion + + #region ICnmCache Members + + /// + /// Gets current count of elements stored to . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + public int Count + { + get + { + lock( m_syncRoot ) + { + return m_cache.Count; + } + } + } + + /// + /// Gets or sets elements expiration time. + /// + /// + /// Elements expiration time. + /// + /// + /// + /// When element has been stored in longer than + /// and it is not accessed through method or element's value is + /// not replaced by method, then it is automatically removed from the + /// . + /// + /// + /// It is possible that implementation removes element before it's expiration time, + /// because total size or count of elements stored to cache is larger than or . + /// + /// + /// It is also possible that element stays in cache longer than . + /// + /// + /// Calling try to remove all elements that are expired. + /// + /// + /// To disable time limit in cache, set to . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public TimeSpan ExpirationTime + { + get + { + lock( m_syncRoot ) + { + return m_cache.ExpirationTime; + } + } + + set + { + lock( m_syncRoot ) + { + m_cache.ExpirationTime = value; + } + } + } + + /// + /// Gets a value indicating whether is limiting count of elements. + /// + /// + /// if the count of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + public bool IsCountLimited + { + get + { + lock( m_syncRoot ) + { + return m_cache.IsCountLimited; + } + } + } + + /// + /// Gets a value indicating whether is limiting size of elements. + /// + /// + /// if the total size of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + public bool IsSizeLimited + { + get + { + lock( m_syncRoot ) + { + return m_cache.IsSizeLimited; + } + } + } + + /// + /// Gets a value indicating whether or not access to the is synchronized (thread safe). + /// + /// + /// if access to the is synchronized (thread safe); + /// otherwise, . + /// + /// + /// + /// To get synchronized (thread safe) access to object, use + /// in class + /// to retrieve synchronized wrapper for object. + /// + /// + /// + /// + public bool IsSynchronized + { + get { return true; } + } + + /// + /// Gets a value indicating whether elements stored to have limited inactivity time. + /// + /// + /// if the has a fixed total size of elements; + /// otherwise, . + /// + /// + /// If have limited inactivity time and element is not accessed through + /// or methods in , then element is automatically removed from + /// the cache. Depending on implementation of the , some of the elements may + /// stay longer in cache. + /// + /// + /// + /// + /// + public bool IsTimeLimited + { + get + { + lock( m_syncRoot ) + { + return m_cache.IsTimeLimited; + } + } + } + + /// + /// Gets or sets maximal allowed count of elements that can be stored to . + /// + /// + /// , if is not limited by count of elements; + /// otherwise maximal allowed count of elements. + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + public int MaxCount + { + get + { + lock( m_syncRoot ) + { + return m_cache.MaxCount; + } + } + + set + { + lock( m_syncRoot ) + { + m_cache.MaxCount = value; + } + } + } + + /// + /// Gets maximal allowed element size. + /// + /// + /// Maximal allowed element size. + /// + /// + /// + /// If element's size is larger than , then element is + /// not added to the . + /// + /// + /// + /// + /// + /// + public long MaxElementSize + { + get + { + lock( m_syncRoot ) + { + return m_cache.MaxElementSize; + } + } + } + + /// + /// Gets or sets maximal allowed total size for elements stored to . + /// + /// + /// Maximal allowed total size for elements stored to . + /// + /// + /// + /// Normally size is total bytes used by elements in the cache. But it can be any other suitable unit of measure. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// value is less than 0. + /// + /// + /// + public long MaxSize + { + get + { + lock( m_syncRoot ) + { + return m_cache.MaxSize; + } + } + + set + { + lock( m_syncRoot ) + { + m_cache.MaxSize = value; + } + } + } + + /// + /// Gets total size of elements stored to . + /// + /// + /// Total size of elements stored to . + /// + /// + /// + /// Normally bytes, but can be any suitable unit of measure. + /// + /// + /// Element's size is given when element is added or replaced by method. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + public long Size + { + get + { + lock( m_syncRoot ) + { + return m_cache.Size; + } + } + } + + /// + /// Gets an object that can be used to synchronize access to the . + /// + /// + /// An object that can be used to synchronize access to the . + /// + /// + /// + /// To get synchronized (thread safe) access to , use + /// method to retrieve synchronized wrapper interface to + /// . + /// + /// + /// + /// + public object SyncRoot + { + get { return m_syncRoot; } + } + + /// + /// Removes all elements from the . + /// + /// + /// + /// + /// + /// + public void Clear() + { + lock( m_syncRoot ) + { + m_cache.Clear(); + } + } + + /// + /// Returns an enumerator that iterates through the elements stored to . + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + public IEnumerator> GetEnumerator() + { + lock( m_syncRoot ) + { + return new SynchronizedEnumerator( m_cache.GetEnumerator(), m_syncRoot ); + } + } + + /// + /// Purge expired elements from the . + /// + /// + /// + /// Element becomes expired when last access time to it has been longer time than . + /// + /// + /// Depending on implementation, some of expired elements + /// may stay longer than in the cache. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void PurgeExpired() + { + lock( m_syncRoot ) + { + m_cache.PurgeExpired(); + } + } + + /// + /// Removes element associated with from the . + /// + /// + /// The key that is associated with element to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public void Remove( TKey key ) + { + lock( m_syncRoot ) + { + m_cache.Remove( key ); + } + } + + /// + /// Removes elements that are associated with one of from the . + /// + /// + /// The keys that are associated with elements to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public void RemoveRange( IEnumerable keys ) + { + lock( m_syncRoot ) + { + m_cache.RemoveRange( keys ); + } + } + + /// + /// Add or replace an element with the provided , and to + /// . + /// + /// + /// The object used as the key of the element. Can't be reference. + /// + /// + /// The object used as the value of the element to add or replace. is allowed. + /// + /// + /// The element's size. Normally bytes, but can be any suitable unit of measure. + /// + /// + /// if element has been added successfully to the ; + /// otherwise . + /// + /// + /// is . + /// + /// + /// The element's is less than 0. + /// + /// + /// + /// If element's is larger than , then element is + /// not added to the , however - possible older element is + /// removed from the . + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + /// + /// + public bool Set( TKey key, TValue value, long size ) + { + lock( m_syncRoot ) + { + return m_cache.Set( key, value, size ); + } + } + + /// + /// Gets the associated with the specified . + /// + /// + /// if the contains an element with + /// the specified key; otherwise, . + /// + /// + /// The key whose to get. + /// + /// + /// When this method returns, the value associated with the specified , + /// if the is found; otherwise, the + /// default value for the type of the parameter. This parameter is passed uninitialized. + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + public bool TryGetValue( TKey key, out TValue value ) + { + lock( m_syncRoot ) + { + return m_cache.TryGetValue( key, out value ); + } + } + + /// + /// Returns an enumerator that iterates through the elements stored to . + /// + /// + /// A that can be used to iterate through the collection. + /// + /// 1 + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/OpenSim/Framework/ICnmCache.cs b/OpenSim/Framework/ICnmCache.cs new file mode 100644 index 0000000000..cba8a7f0f9 --- /dev/null +++ b/OpenSim/Framework/ICnmCache.cs @@ -0,0 +1,441 @@ +/* + * Copyright (c) Contributors, http://opensimulator.org/ + * See CONTRIBUTORS.TXT for a full list of copyright holders. + * + * 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. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the OpenSimulator Project 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 DEVELOPERS ``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 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.Generic; + +namespace OpenSim.Framework +{ + /// + /// Represent generic cache to store key/value pairs (elements) limited by time, size and count of elements. + /// + /// + /// The type of keys in the cache. + /// + /// + /// The type of values in the cache. + /// + /// + /// + /// Cache store limitations: + /// + /// + /// + /// Limitation + /// Description + /// + /// + /// Time + /// + /// Element that is not accessed through or in last are + /// removed from the cache automatically. Depending on implementation of the cache some of elements may stay longer in cache. + /// returns , if cache is limited by time. + /// + /// + /// + /// Count + /// + /// When adding an new element to cache that already have of elements, cache will remove less recently + /// used element(s) from the cache, until element fits to cache. + /// returns , if cache is limiting element count. + /// + /// + /// + /// Size + /// + /// + /// When adding an new element to cache that already have of elements, cache will remove less recently + /// used element(s) from the cache, until element fits to cache. + /// returns , if cache is limiting total size of elements. + /// Normally size is bytes used by element in the cache. But it can be any other suitable unit of measure. + /// + /// + /// + /// + /// + public interface ICnmCache : IEnumerable> + { + /// + /// Gets current count of elements stored to . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + int Count { get; } + + /// + /// Gets or sets elements expiration time. + /// + /// + /// Elements expiration time. + /// + /// + /// + /// When element has been stored in longer than + /// and it is not accessed through method or element's value is + /// not replaced by method, then it is automatically removed from the + /// . + /// + /// + /// It is possible that implementation removes element before it's expiration time, + /// because total size or count of elements stored to cache is larger than or . + /// + /// + /// It is also possible that element stays in cache longer than . + /// + /// + /// Calling try to remove all elements that are expired. + /// + /// + /// To disable time limit in cache, set to . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + TimeSpan ExpirationTime { get; set; } + + /// + /// Gets a value indicating whether or not access to the is synchronized (thread safe). + /// + /// + /// if access to the is synchronized (thread safe); + /// otherwise, . + /// + /// + /// + /// To get synchronized (thread safe) access to object, use + /// in class + /// to retrieve synchronized wrapper for object. + /// + /// + /// + /// + bool IsSynchronized { get; } + + /// + /// Gets a value indicating whether is limiting count of elements. + /// + /// + /// if the count of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + bool IsCountLimited { get; } + + /// + /// Gets a value indicating whether is limiting size of elements. + /// + /// + /// if the total size of elements is limited; + /// otherwise, . + /// + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + bool IsSizeLimited { get; } + + /// + /// Gets a value indicating whether elements stored to have limited inactivity time. + /// + /// + /// if the has a fixed total size of elements; + /// otherwise, . + /// + /// + /// If have limited inactivity time and element is not accessed through + /// or methods in , then element is automatically removed from + /// the cache. Depending on implementation of the , some of the elements may + /// stay longer in cache. + /// + /// + /// + /// + /// + bool IsTimeLimited { get; } + + /// + /// Gets or sets maximal allowed count of elements that can be stored to . + /// + /// + /// , if is not limited by count of elements; + /// otherwise maximal allowed count of elements. + /// + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + int MaxCount { get; set; } + + /// + /// Gets maximal allowed element size. + /// + /// + /// Maximal allowed element size. + /// + /// + /// + /// If element's size is larger than , then element is + /// not added to the . + /// + /// + /// + /// + /// + /// + long MaxElementSize { get; } + + /// + /// Gets or sets maximal allowed total size for elements stored to . + /// + /// + /// Maximal allowed total size for elements stored to . + /// + /// + /// + /// Normally size is total bytes used by elements in the cache. But it can be any other suitable unit of measure. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// value is less than 0. + /// + /// + /// + long MaxSize { get; set; } + + /// + /// Gets total size of elements stored to . + /// + /// + /// Total size of elements stored to . + /// + /// + /// + /// Normally bytes, but can be any suitable unit of measure. + /// + /// + /// Element's size is given when element is added or replaced by method. + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + long Size { get; } + + /// + /// Gets an object that can be used to synchronize access to the . + /// + /// + /// An object that can be used to synchronize access to the . + /// + /// + /// + /// To get synchronized (thread safe) access to , use + /// method to retrieve synchronized wrapper interface to + /// . + /// + /// + /// + /// + object SyncRoot { get; } + + /// + /// Removes all elements from the . + /// + /// + /// + /// + /// + /// + void Clear(); + + /// + /// Purge expired elements from the . + /// + /// + /// + /// Element becomes expired when last access time to it has been longer time than . + /// + /// + /// Depending on implementation, some of expired elements + /// may stay longer than in the cache. + /// + /// + /// + /// + /// + /// + /// + /// + /// + void PurgeExpired(); + + /// + /// Removes element associated with from the . + /// + /// + /// The key that is associated with element to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + void Remove( TKey key ); + + /// + /// Removes elements that are associated with one of from the . + /// + /// + /// The keys that are associated with elements to remove from the . + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + void RemoveRange( IEnumerable keys ); + + /// + /// Add or replace an element with the provided , and to + /// . + /// + /// + /// The object used as the key of the element. Can't be reference. + /// + /// + /// The object used as the value of the element to add or replace. is allowed. + /// + /// + /// The element's size. Normally bytes, but can be any suitable unit of measure. + /// + /// + /// if element has been added successfully to the ; + /// otherwise . + /// + /// + /// is . + /// + /// + /// The element's is less than 0. + /// + /// + /// + /// If element's is larger than , then element is + /// not added to the , however - possible older element is + /// removed from the . + /// + /// + /// When adding an new element to that is limiting total size of elements, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// When adding an new element to that is limiting element count, + /// will remove less recently used elements until it can fit an new element. + /// + /// + /// + /// + /// + /// + /// + /// + /// + bool Set( TKey key, TValue value, long size ); + + /// + /// Gets the associated with the specified . + /// + /// + /// if the contains an element with + /// the specified key; otherwise, . + /// + /// + /// The key whose to get. + /// + /// + /// When this method returns, the value associated with the specified , + /// if the is found; otherwise, the + /// default value for the type of the parameter. This parameter is passed uninitialized. + /// + /// + /// is . + /// + /// + /// + /// + /// + /// + bool TryGetValue( TKey key, out TValue value ); + } +} diff --git a/OpenSim/Framework/PrimeNumberHelper.cs b/OpenSim/Framework/PrimeNumberHelper.cs new file mode 100644 index 0000000000..e4bf615d4e --- /dev/null +++ b/OpenSim/Framework/PrimeNumberHelper.cs @@ -0,0 +1,98 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// +// +// +// +// +// +// -------------------------------------------------------------------------------------------------------------------- + +using System; + +namespace OpenSim.Framework +{ + /// + /// Utility class that is used to find small prime numbers and test is number prime number. + /// + public static class PrimeNumberHelper + { + /// + /// Precalculated prime numbers. + /// + private static readonly int[] Primes = new int[] + { + 3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, + 293, 353, 431, 521, 631, 761, 919, 1103, 1327, 1597, 1931, 2333, + 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591, + 17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, + 90523, 108631, 130363, 156437, 187751, 225307, 270371, 324449, + 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, + 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, + 5999471, 7199369 + }; + + /// + /// Get prime number that is equal or larger than . + /// + /// + /// Minimal returned prime number. + /// + /// + /// Primer number that is equal or larger than . If is too large, return -1. + /// + public static int GetPrime( int min ) + { + if( min <= 2 ) + return 2; + + if( Primes[ Primes.Length - 1 ] < min ) + { + for( int i = min | 1 ; i < 0x7FFFFFFF ; i += 2 ) + { + if( IsPrime( i ) ) + return i; + } + + return -1; + } + + for( int i = Primes.Length - 2 ; i >= 0 ; i-- ) + { + if( min == Primes[ i ] ) + return min; + + if( min > Primes[ i ] ) + return Primes[ i + 1 ]; + } + + return 2; + } + + /// + /// Just basic Sieve of Eratosthenes prime number test. + /// + /// + /// Number that is tested. + /// + /// + /// true, if is prime number; otherwise false. + /// + public static bool IsPrime( int candinate ) + { + if( (candinate & 1) == 0 ) + + // Even number - only prime if 2 + return candinate == 2; + + int upperBound = (int) Math.Sqrt( candinate ); + for( int i = 3 ; i < upperBound ; i += 2 ) + { + if( candinate % i == 0 ) + return false; + } + + return true; + } + } +} diff --git a/OpenSim/Region/CoreModules/Asset/CenomeAssetCache.cs b/OpenSim/Region/CoreModules/Asset/CenomeAssetCache.cs new file mode 100644 index 0000000000..00a8143fc7 --- /dev/null +++ b/OpenSim/Region/CoreModules/Asset/CenomeAssetCache.cs @@ -0,0 +1,382 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// +// +// +// +// +// +// -------------------------------------------------------------------------------------------------------------------- +using System; +using System.Reflection; +using log4net; +using Nini.Config; +using OpenSim.Framework; +using OpenSim.Region.Framework.Interfaces; +using OpenSim.Region.Framework.Scenes; + +namespace OpenSim.Region.CoreModules.Asset +{ + /// + /// Cenome memory asset cache. + /// + /// + /// + /// Cache is enabled by setting "AssetCaching" configuration to value "CenomeMemoryAssetCache". + /// When cache is successfully enable log should have message + /// "[ASSET CACHE]: Cenome asset cache enabled (MaxSize = XXX bytes, MaxCount = XXX, ExpirationTime = XXX)". + /// + /// + /// Cache's size is limited by two parameters: + /// maximal allowed size in bytes and maximal allowed asset count. When new asset + /// is added to cache that have achieved either size or count limitation, cache + /// will automatically remove less recently used assets from cache. Additionally + /// asset's lifetime is controlled by expiration time. + /// + /// + /// + /// + /// Configuration + /// Description + /// + /// + /// MaxSize + /// Maximal size of the cache in bytes. Default value: 128MB (134 217 728 bytes). + /// + /// + /// MaxCount + /// Maximal count of assets stored to cache. Default value: 4096 assets. + /// + /// + /// ExpirationTime + /// Asset's expiration time in minutes. Default value: 30 minutes. + /// + /// + /// + /// + /// + /// Enabling Cenome Asset Cache: + /// + /// [Modules] + /// AssetCaching = "CenomeMemoryAssetCache" + /// + /// Setting size and expiration time limitations: + /// + /// [AssetService] + /// ; 256 MB (default: 134217728) + /// MaxSize = 268435456 + /// ; How many assets it is possible to store cache (default: 4096) + /// MaxCount = 16384 + /// ; Expiration time - 1 hour (default: 30 minutes) + /// ExpirationTime = 60 + /// + /// + public class CenomeMemoryAssetCache : IImprovedAssetCache, ISharedRegionModule + { + /// + /// Cache's default maximal asset count. + /// + /// + /// + /// Assuming that average asset size is about 32768 bytes. + /// + /// + public const int DefaultMaxCount = 4096; + + /// + /// Default maximal size of the cache in bytes + /// + /// + /// + /// 128MB = 128 * 1024^2 = 134 217 728 bytes. + /// + /// + public const long DefaultMaxSize = 134217728; + + /// + /// Asset's default expiration time in the cache. + /// + public static readonly TimeSpan DefaultExpirationTime = TimeSpan.FromMinutes( 30.0 ); + + /// + /// Log manager instance. + /// + private static readonly ILog Log = LogManager.GetLogger( MethodBase.GetCurrentMethod().DeclaringType ); + + /// + /// Cache object. + /// + private ICnmCache m_cache; + + /// + /// Count of cache commands + /// + private int m_cachedCount; + + /// + /// How many gets before dumping statistics + /// + /// + /// If 0 or less, then disabled. + /// + private int m_debugEpoch; + + /// + /// Is Cenome asset cache enabled. + /// + private bool m_enabled; + + /// + /// Count of get requests + /// + private int m_getCount; + + /// + /// How many hits + /// + private int m_hitCount; + + /// + /// Initialize asset cache module with default parameters. + /// + public void Initialize() + { + Initialize( DefaultMaxSize, DefaultMaxCount, DefaultExpirationTime ); + } + + /// + /// Initialize asset cache module, with custom parameters. + /// + /// + /// Cache's maximal size in bytes. + /// + /// + /// Cache's maximal count of assets. + /// + /// + /// Asset's expiration time. + /// + public void Initialize( long maximalSize, int maximalCount, TimeSpan expirationTime ) + { + if( maximalSize <= 0 || maximalCount <= 0 ) + { + Log.Info( "[ASSET CACHE]: Cenome asset cache is not enabled." ); + m_enabled = false; + return; + } + + if( expirationTime <= TimeSpan.Zero ) + { + // Disable expiration time + expirationTime = TimeSpan.MaxValue; + } + + // Create cache and add synchronization wrapper over it + m_cache = + CnmSynchronizedCache.Synchronized( new CnmMemoryCache( + maximalSize, maximalCount, expirationTime ) ); + m_enabled = true; + Log.InfoFormat( + "[ASSET CACHE]: Cenome asset cache enabled (MaxSize = {0} bytes, MaxCount = {1}, ExpirationTime = {2})", + maximalSize, + maximalCount, + expirationTime ); + } + + #region IImprovedAssetCache Members + + /// + /// Cache asset. + /// + /// + /// The asset that is being cached. + /// + public void Cache( AssetBase asset ) + { + long size = asset.Data != null ? asset.Data.Length : 1; + m_cache.Set( asset.ID, asset, size ); + m_cachedCount++; + } + + /// + /// Clear asset cache. + /// + public void Clear() + { + m_cache.Clear(); + } + + /// + /// Expire (remove) asset stored to cache. + /// + /// + /// The expired asset's id. + /// + public void Expire( string id ) + { + m_cache.Remove( id ); + } + + /// + /// Get asset stored + /// + /// + /// The asset's id. + /// + /// + /// Asset if it is found from cache; otherwise . + /// + /// + /// + /// Caller should always check that is return value . + /// Cache doesn't guarantee in any situation that asset is stored to it. + /// + /// + public AssetBase Get( string id ) + { + m_getCount++; + AssetBase assetBase; + if( m_cache.TryGetValue( id, out assetBase ) ) + m_hitCount++; + + if( m_getCount == m_debugEpoch ) + { + Log.InfoFormat( + "[ASSET CACHE]: Cached = {0}, Get = {1}, Hits = {2}%, Size = {3} bytes, Avg. A. Size = {4} bytes", + m_cachedCount, + m_getCount, + ((double) m_hitCount / m_getCount) * 100.0, + m_cache.Size, + m_cache.Size / m_cache.Count ); + m_getCount = 0; + m_hitCount = 0; + m_cachedCount = 0; + } + + return assetBase; + } + + #endregion + + #region ISharedRegionModule Members + + /// + /// Gets region module's name. + /// + public string Name + { + get { return "CenomeMemoryAssetCache"; } + } + + /// + /// New region is being added to server. + /// + /// + /// Region's scene. + /// + public void AddRegion( Scene scene ) + { + if( m_enabled ) + scene.RegisterModuleInterface( this ); + } + + /// + /// Close region module. + /// + public void Close() + { + m_enabled = false; + m_cache.Clear(); + m_cache = null; + } + + /// + /// Initialize region module. + /// + /// + /// Configuration source. + /// + public void Initialise( IConfigSource source ) + { + m_cache = null; + m_enabled = false; + + IConfig moduleConfig = source.Configs[ "Modules" ]; + if( moduleConfig == null ) + return; + + string name = moduleConfig.GetString( "AssetCaching" ); + Log.DebugFormat( "[XXX] name = {0} (this module's name: {1}", name, Name ); + + if( name != Name ) + return; + + // This module is used + long maxSize = DefaultMaxSize; + int maxCount = DefaultMaxCount; + TimeSpan expirationTime = DefaultExpirationTime; + + IConfig assetConfig = source.Configs[ "AssetCache" ]; + if( assetConfig != null ) + { + // Get optional configurations + maxSize = assetConfig.GetLong( "MaxSize", DefaultMaxSize ); + maxCount = assetConfig.GetInt( "MaxCount", DefaultMaxCount ); + expirationTime = + TimeSpan.FromMinutes( assetConfig.GetInt( "ExpirationTime", (int) DefaultExpirationTime.TotalMinutes ) ); + + // Debugging purposes only + m_debugEpoch = assetConfig.GetInt( "DebugEpoch", 0 ); + } + + Initialize( maxSize, maxCount, expirationTime ); + } + + /// + /// Initialization post handling. + /// + /// + /// + /// Modules can use this to initialize connection with other modules. + /// + /// + public void PostInitialise() + { + } + + /// + /// Region has been loaded. + /// + /// + /// Region's scene. + /// + /// + /// + /// This is needed for all module types. Modules will register + /// Interfaces with scene in AddScene, and will also need a means + /// to access interfaces registered by other modules. Without + /// this extra method, a module attempting to use another modules' + /// interface would be successful only depending on load order, + /// which can't be depended upon, or modules would need to resort + /// to ugly kludges to attempt to request interfaces when needed + /// and unnecessary caching logic repeated in all modules. + /// The extra function stub is just that much cleaner. + /// + /// + public void RegionLoaded( Scene scene ) + { + } + + /// + /// Region is being removed. + /// + /// + /// Region scene that is being removed. + /// + public void RemoveRegion( Scene scene ) + { + } + + #endregion + } +} diff --git a/OpenSim/Region/CoreModules/Resources/CoreModulePlugin.addin.xml b/OpenSim/Region/CoreModules/Resources/CoreModulePlugin.addin.xml index 393f340445..9969ebe667 100644 --- a/OpenSim/Region/CoreModules/Resources/CoreModulePlugin.addin.xml +++ b/OpenSim/Region/CoreModules/Resources/CoreModulePlugin.addin.xml @@ -19,6 +19,7 @@ + diff --git a/bin/config-include/GridCommon.ini.example b/bin/config-include/GridCommon.ini.example index 8a957768f0..38253744e7 100644 --- a/bin/config-include/GridCommon.ini.example +++ b/bin/config-include/GridCommon.ini.example @@ -1,6 +1,7 @@ [Modules] ;AssetCaching = "CoreAssetCache" AssetCaching = "GlynnTuckerAssetCache" + ;AssetCaching = "CenomeMemoryAssetCache" [AssetCache] ; Number of buckets for assets @@ -16,3 +17,10 @@ ; AssetServerURI = "http://myassetserver.com:8003" + ; Optional configurations for CenomeMemoryAssetCache + ; Cache size 128 MB (default: 134217728) + ; MaxSize = 134217728 + ; Maximal asset count + ; MaxCount = 4096 + ; Asset's expiration time (minutes) + ; ExpirationTime = 30 \ No newline at end of file diff --git a/bin/config-include/StandaloneCommon.ini.example b/bin/config-include/StandaloneCommon.ini.example index d60709207c..d772256b7b 100644 --- a/bin/config-include/StandaloneCommon.ini.example +++ b/bin/config-include/StandaloneCommon.ini.example @@ -1,6 +1,7 @@ [Modules] ;AssetCaching = "CoreAssetCache" AssetCaching = "GlynnTuckerAssetCache" + ;AssetCaching = "CenomeMemoryAssetCache" [AssetCache] ; Number of buckets for assets @@ -20,3 +21,10 @@ AssetLoaderArgs = "assets/AssetSets.xml" + ; Optional configurations for CenomeMemoryAssetCache + ; Cache size 128 MB (default: 134217728) + ; MaxSize = 134217728 + ; Maximal asset count + ; MaxCount = 4096 + ; Asset's expiration time (minutes) + ; ExpirationTime = 30 \ No newline at end of file