* MemoryCacheTest.cs: added tests for LRU removal of entries.
2010-04-26 Marek Habersack <mhabersack@novell.com>
* MemoryCacheLRU.cs: added
* MemoryCacheContainer.cs: added LRU entry cache.
* MemoryCache.cs: added option to emulate one CPU on SMP machines
(for testing purposes) - "__MonoEmulateOneCPU"
svn path=/trunk/mcs/; revision=156125
System.Runtime.Caching/MemoryCacheContainer.cs
System.Runtime.Caching/MemoryCacheEntry.cs
System.Runtime.Caching/MemoryCacheEntryChangeMonitor.cs
+System.Runtime.Caching/MemoryCacheLRU.cs
System.Runtime.Caching/MemoryCachePerformanceCounters.cs
System.Runtime.Caching/MemoryCacheEntryPriorityQueue.cs
System.Runtime.Caching/ObjectCache.cs
+2010-04-26 Marek Habersack <mhabersack@novell.com>
+
+ * MemoryCacheLRU.cs: added
+
+ * MemoryCacheContainer.cs: added LRU entry cache.
+
+ * MemoryCache.cs: added option to emulate one CPU on SMP machines
+ (for testing purposes) - "__MonoEmulateOneCPU"
+
2010-04-24 Marek Habersack <mhabersack@novell.com>
* ObjectCache.cs: implemented all the non-abstract methods.
DefaultCacheCapabilities defaultCaps;
MemoryCachePerformanceCounters perfCounters;
bool noPerformanceCounters;
-
+ bool emulateOneCPU;
+
static ulong TotalPhysicalMemory {
get {
if (totalPhysicalMemory == 0)
Interlocked.CompareExchange (ref totalPhysicalMemory, memBytes, 0);
}
+
+ bool ParseBoolConfigValue (string paramName, string name, NameValueCollection config, bool doTrow)
+ {
+ string value = config [name];
+ if (String.IsNullOrEmpty (value))
+ return false;
+
+ try {
+ return Boolean.Parse (value);
+ } catch {
+ return false;
+ }
+ }
bool ParseInt32ConfigValue (string paramName, string name, NameValueCollection config, int maxValue, bool doThrow, out int parsed)
{
return true;
}
-
+
bool ParseTimeSpanConfigValue (string paramName, string name, NameValueCollection config, out TimeSpan parsed)
{
string value = config [name];
TimerPeriod = (long)(parsed * 1000);
else
TimerPeriod = DEFAULT_TIMER_PERIOD;
+
+ if (ParseBoolConfigValue ("config", "__MonoEmulateOneCPU", config, false))
+ emulateOneCPU = true;
} else
TimerPeriod = DEFAULT_TIMER_PERIOD;
{
if (key == null)
throw new ArgumentNullException ("key");
-
- if (numCPUs == 1) {
+
+ if (emulateOneCPU || numCPUs == 1) {
if (containers [0] == null)
containers [0] = new MemoryCacheContainer (this, 0, perfCounters);
{
sealed class MemoryCacheContainer : IDisposable
{
+ const int DEFAULT_LRU_LOWER_BOUND = 10;
+
ReaderWriterLockSlim cache_lock = new ReaderWriterLockSlim ();
SortedDictionary <string, MemoryCacheEntry> cache;
MemoryCache owner;
MemoryCachePerformanceCounters perfCounters;
MemoryCacheEntryPriorityQueue timedItems;
+ MemoryCacheLRU lru;
Timer expirationTimer;
public int ID {
this.ID = id;
this.perfCounters = perfCounters;
cache = new SortedDictionary <string, MemoryCacheEntry> ();
+ lru = new MemoryCacheLRU (this, DEFAULT_LRU_LOWER_BOUND);
}
bool ExpireIfNeeded (string key, MemoryCacheEntry entry, bool needsLock = true, CacheEntryRemovedReason reason = CacheEntryRemovedReason.Expired)
cache [key] = entry;
else
cache.Add (key, entry);
+ lru.Update (entry);
entry.Added ();
if (!update)
perfCounters.Increment (MemoryCachePerformanceCounters.CACHE_ENTRIES);
-
+
if (entry.IsExpirable)
UpdateExpirable (entry);
}
timedItems.Dequeue ();
count++;
- DoRemoveEntry (entry, entry.Key, CacheEntryRemovedReason.Expired);
+ DoRemoveEntry (entry, true, entry.Key, CacheEntryRemovedReason.Expired);
entry = timedItems.Peek ();
}
}
}
- public object Remove (string key)
+ public object Remove (string key, bool needLock = true, bool updateLRU = true)
{
bool writeLocked = false, readLocked = false;
try {
- cache_lock.EnterUpgradeableReadLock ();
- readLocked = true;
+ if (needLock) {
+ cache_lock.EnterUpgradeableReadLock ();
+ readLocked = true;
+ }
MemoryCacheEntry entry;
if (!cache.TryGetValue (key, out entry))
return null;
+
+ if (needLock) {
+ cache_lock.EnterWriteLock ();
+ writeLocked = true;
+ }
- cache_lock.EnterWriteLock ();
- writeLocked = true;
object ret = entry.Value;
- DoRemoveEntry (entry, key);
+ DoRemoveEntry (entry, updateLRU, key);
return ret;
} finally {
if (writeLocked)
cache_lock.ExitUpgradeableReadLock ();
}
}
-
+
// NOTE: this must be called with the write lock held
- void DoRemoveEntry (MemoryCacheEntry entry, string key = null, CacheEntryRemovedReason reason = CacheEntryRemovedReason.Removed)
+ void DoRemoveEntry (MemoryCacheEntry entry, bool updateLRU = true, string key = null, CacheEntryRemovedReason reason = CacheEntryRemovedReason.Removed)
{
if (key == null)
key = entry.Key;
cache.Remove (key);
+ if (updateLRU)
+ lru.Remove (entry);
perfCounters.Decrement (MemoryCachePerformanceCounters.CACHE_ENTRIES);
entry.Removed (owner, reason);
}
mce.SetPolicy (policy);
if (mce.IsExpirable)
UpdateExpirable (mce);
+ lru.Update (mce);
return;
}
ret = DoRemoveExpiredItems (false);
goal -= ret;
- if (goal > 0) {
- // TODO: perform LRU removal
- }
+ if (goal > 0)
+ ret += lru.Trim (goal);
} finally {
if (locked)
cache_lock.ExitWriteLock ();
monitor.NotifyOnChanged (OnMonitorChanged);
}
+ public override int GetHashCode ()
+ {
+ return Key.GetHashCode ();
+ }
+
void OnMonitorChanged (object state)
{
owner.Remove (this);
public void Removed (MemoryCache owner, CacheEntryRemovedReason reason)
{
- Disabled = true;
-
- if (removedCallback == null)
+ if (removedCallback == null) {
+ Disabled = true;
return;
+ }
try {
removedCallback (new CacheEntryRemovedArguments (owner, reason, new CacheItem (Key, Value)));
} catch {
// ignore - we don't care about the exceptions thrown inside the
// handler
+ } finally {
+ Disabled = true;
}
}
--- /dev/null
+//
+// MemoryCacheLRU.cs
+//
+// Authors:
+// Marek Habersack <mhabersack@novell.com>
+//
+// Copyright (C) 2010 Novell, Inc. (http://novell.com/)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Collections.Generic;
+
+namespace System.Runtime.Caching
+{
+ // NOTE: all the public methods in this assume that the owner's write lock is held
+ sealed class MemoryCacheLRU
+ {
+ int trimLowerBound;
+ Dictionary <int, LinkedListNode <MemoryCacheEntry>> index;
+ LinkedList <MemoryCacheEntry> lru;
+ MemoryCacheContainer owner;
+
+ public MemoryCacheLRU (MemoryCacheContainer owner, int trimLowerBound)
+ {
+ this.trimLowerBound = trimLowerBound;
+ index = new Dictionary <int, LinkedListNode <MemoryCacheEntry>> ();
+ lru = new LinkedList <MemoryCacheEntry> ();
+ this.owner = owner;
+ }
+
+ public void Update (MemoryCacheEntry entry)
+ {
+ if (entry == null)
+ return;
+
+ int hash = entry.GetHashCode ();
+ LinkedListNode <MemoryCacheEntry> node;
+
+ if (!index.TryGetValue (hash, out node)) {
+ node = new LinkedListNode <MemoryCacheEntry> (entry);
+ index.Add (hash, node);
+ } else {
+ lru.Remove (node);
+ node.Value = entry;
+ }
+
+ lru.AddLast (node);
+ }
+
+ public void Remove (MemoryCacheEntry entry)
+ {
+ if (entry == null)
+ return;
+
+ int hash = entry.GetHashCode ();
+ LinkedListNode <MemoryCacheEntry> node;
+
+ if (index.TryGetValue (hash, out node)) {
+ lru.Remove (node);
+ index.Remove (hash);
+ }
+ }
+
+ public long Trim (long upTo)
+ {
+ int count = index.Count;
+ if (count <= 10)
+ return 0;
+
+ // The list is used below to reproduce .NET's behavior which selects the
+ // entries using the LRU order, but it removes them from the cache in the
+ // MRU order
+ var toremove = new List <MemoryCacheEntry> ((int)upTo);
+ long removed = 0;
+ MemoryCacheEntry entry;
+
+ while (upTo > removed && count > 10) {
+ entry = lru.First.Value;
+ toremove.Insert (0, entry);
+ Remove (entry);
+ removed++;
+ count--;
+ }
+
+ foreach (MemoryCacheEntry e in toremove)
+ owner.Remove (e.Key, false);
+
+ return removed;
+ }
+ }
+}
+2010-04-26 Marek Habersack <mhabersack@novell.com>
+
+ * MemoryCacheTest.cs: added tests for LRU removal of entries.
+
2010-04-24 Marek Habersack <mhabersack@novell.com>
* MemoryCacheTest.cs, ObjectCacheTest.cs: added
}, "#A3");
}
+ // NOTE: on Windows with 2 or more CPUs this test will most probably fail.
[Test]
public void Trim ()
{
- var mc = new MemoryCache ("MyCache");
+ var config = new NameValueCollection ();
+ config ["__MonoEmulateOneCPU"] = "true";
+ var mc = new MemoryCache ("MyCache", config);
for (int i = 0; i < 10; i++)
mc.Set ("key" + i.ToString (), "value" + i.ToString (), null);
- Assert.AreEqual (10, mc.GetCount (), "#A1");
+ // .NET doesn't touch the freshest 10 entries
+ Assert.AreEqual (10, mc.GetCount (), "#A1-1");
long trimmed = mc.Trim (50);
- Assert.AreEqual (0, trimmed, "#A2-1");
- Assert.AreEqual (10, mc.GetCount (), "#A2-2");
+ Assert.AreEqual (0, trimmed, "#A1-2");
+ Assert.AreEqual (10, mc.GetCount (), "#A1-3");
-
+ mc = new MemoryCache ("MyCache", config);
+ // Only entries 11- are considered for removal
+ for (int i = 0; i < 11; i++)
+ mc.Set ("key" + i.ToString (), "value" + i.ToString (), null);
+
+ Assert.AreEqual (11, mc.GetCount (), "#A2-1");
+ trimmed = mc.Trim (50);
+ Assert.AreEqual (1, trimmed, "#A2-2");
+ Assert.AreEqual (10, mc.GetCount (), "#A2-3");
+
+ mc = new MemoryCache ("MyCache", config);
+ // Only entries 11- are considered for removal
+ for (int i = 0; i < 125; i++)
+ mc.Set ("key" + i.ToString (), "value" + i.ToString (), null);
+
+ Assert.AreEqual (125, mc.GetCount (), "#A3-1");
+ trimmed = mc.Trim (50);
+ Assert.AreEqual (62, trimmed, "#A3-2");
+ Assert.AreEqual (63, mc.GetCount (), "#A3-3");
+
+ // Testing the removal order
+ mc = new MemoryCache ("MyCache", config);
+ var removed = new List <string> ();
+ var cip = new CacheItemPolicy ();
+ cip.RemovedCallback = (CacheEntryRemovedArguments args) => {
+ removed.Add (args.CacheItem.Key);
+ };
+
+ for (int i = 0; i < 50; i++)
+ mc.Set ("key" + i.ToString (), "value" + i.ToString (), cip);
+
+ object value;
+ for (int i = 0; i < 50; i++)
+ value = mc.Get ("key" + i.ToString ());
+
+ trimmed = mc.Trim (50);
+ Assert.AreEqual (25, mc.GetCount (), "#A4-1");
+ Assert.AreEqual (25, trimmed, "#A4-2");
+ Assert.AreEqual (25, removed.Count, "#A4-3");
+
+ // OK, this is odd... The list is correct in terms of entries removed but the entries
+ // are removed in the _MOST_ frequently used order, within the group selected for removal.
+ for (int i = 24; i >= 0; i--) {
+ int idx = 24 - i;
+ Assert.AreEqual ("key" + i.ToString (), removed [idx], "#A5-" + idx.ToString ());
+ }
}
}
}