2010-04-26 Marek Habersack <mhabersack@novell.com>
authorMarek Habersack <grendel@twistedcode.net>
Mon, 26 Apr 2010 19:39:32 +0000 (19:39 -0000)
committerMarek Habersack <grendel@twistedcode.net>
Mon, 26 Apr 2010 19:39:32 +0000 (19:39 -0000)
* 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

mcs/class/System.Runtime.Caching/System.Runtime.Caching.dll.sources
mcs/class/System.Runtime.Caching/System.Runtime.Caching/ChangeLog
mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCache.cs
mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCacheContainer.cs
mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCacheEntry.cs
mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCacheLRU.cs [new file with mode: 0644]
mcs/class/System.Runtime.Caching/Test/System.Runtime.Caching/ChangeLog
mcs/class/System.Runtime.Caching/Test/System.Runtime.Caching/MemoryCacheTest.cs

index cca8c3cf91634458f821f00100d71378535aa7d8..2278e3e34110b577057c88aab1d062242841944a 100644 (file)
@@ -24,6 +24,7 @@ System.Runtime.Caching/MemoryCache.cs
 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
index c11787466ff173d28749b73080cf2d6e5c28a8a0..3e5faffd2c4c6c85023d2757f3fb3a96f7699046 100644 (file)
@@ -1,3 +1,12 @@
+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.
index a3f3bccbf27bf76a39f4633872f586547029a801..272b006050a469becd8d27e92e58d2fc9e6e3b2f 100644 (file)
@@ -49,7 +49,8 @@ namespace System.Runtime.Caching
                DefaultCacheCapabilities defaultCaps;
                MemoryCachePerformanceCounters perfCounters;
                bool noPerformanceCounters;
-                               
+               bool emulateOneCPU;
+               
                static ulong TotalPhysicalMemory {
                        get {
                                if (totalPhysicalMemory == 0)
@@ -139,6 +140,19 @@ namespace System.Runtime.Caching
 
                        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)
                {
@@ -168,7 +182,7 @@ namespace System.Runtime.Caching
                        
                        return true;
                }
-
+               
                bool ParseTimeSpanConfigValue (string paramName, string name, NameValueCollection config, out TimeSpan parsed)
                {
                        string value = config [name];
@@ -227,6 +241,9 @@ namespace System.Runtime.Caching
                                        TimerPeriod = (long)(parsed * 1000);
                                else
                                        TimerPeriod = DEFAULT_TIMER_PERIOD;
+
+                               if (ParseBoolConfigValue ("config", "__MonoEmulateOneCPU", config, false))
+                                       emulateOneCPU = true;
                        } else
                                TimerPeriod = DEFAULT_TIMER_PERIOD;
 
@@ -349,8 +366,8 @@ namespace System.Runtime.Caching
                {
                        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);
 
index ff424089b1147963d268f22e2344a6fbb3c31fea..43de07dcbb7b5cf2b184d787d69134965c78c921 100644 (file)
@@ -39,12 +39,15 @@ namespace System.Runtime.Caching
 {
        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 {
@@ -64,6 +67,7 @@ namespace System.Runtime.Caching
                        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)
@@ -150,10 +154,11 @@ namespace System.Runtime.Caching
                                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);
                }
@@ -203,7 +208,7 @@ namespace System.Runtime.Caching
 
                                        timedItems.Dequeue ();
                                        count++;
-                                       DoRemoveEntry (entry, entry.Key, CacheEntryRemovedReason.Expired);
+                                       DoRemoveEntry (entry, true, entry.Key, CacheEntryRemovedReason.Expired);
                                        entry = timedItems.Peek ();
                                }
 
@@ -304,21 +309,26 @@ namespace System.Runtime.Caching
                        }
                }
 
-               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)
@@ -327,14 +337,16 @@ namespace System.Runtime.Caching
                                        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);
                }
@@ -355,6 +367,7 @@ namespace System.Runtime.Caching
                                                mce.SetPolicy (policy);
                                                if (mce.IsExpirable)
                                                        UpdateExpirable (mce);
+                                               lru.Update (mce);
                                                return;
                                        }
 
@@ -394,9 +407,8 @@ namespace System.Runtime.Caching
                                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 ();
index 57fbb7b67b1a38b64148396c2db685ca2a423556..9bae662b4ab650d9c86d196ac40afee6eef2d2ba 100644 (file)
@@ -142,6 +142,11 @@ namespace System.Runtime.Caching
                                monitor.NotifyOnChanged (OnMonitorChanged);
                }
 
+               public override int GetHashCode ()
+               {
+                       return Key.GetHashCode ();
+               }
+               
                void OnMonitorChanged (object state)
                {
                        owner.Remove (this);
@@ -149,16 +154,18 @@ namespace System.Runtime.Caching
                
                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;
                        }
                }
 
diff --git a/mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCacheLRU.cs b/mcs/class/System.Runtime.Caching/System.Runtime.Caching/MemoryCacheLRU.cs
new file mode 100644 (file)
index 0000000..2ad420e
--- /dev/null
@@ -0,0 +1,109 @@
+//
+// 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;
+               }
+       }
+}
index 77de4cb8b6151a24bd1cf1888f0ba3dd63ee78da..657cc7b34f37a5d17461c4ab59366a443f55f3bf 100644 (file)
@@ -1,3 +1,7 @@
+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
index fdaa897b0f773fd83aa82a250ba094260eec0954..0039af68b2a259632c5739a53f62ca93dc9349d8 100644 (file)
@@ -1172,20 +1172,69 @@ namespace MonoTests.System.Runtime.Caching
                        }, "#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 ());
+                       }
                }
        }
 }