From 553fa7d95e3726543e382515413e3b56c0af992a Mon Sep 17 00:00:00 2001 From: William Dillon Date: Wed, 4 Jun 2025 20:51:49 -0400 Subject: [PATCH] first commit --- SalmonCache.sln | 28 +++++ SalmonCache/SalmonCache.cs | 146 +++++++++++++++++++++++++ SalmonCache/SalmonCache.csproj | 10 ++ SalmonCacheTest/SalmonCacheTest.cs | 84 ++++++++++++++ SalmonCacheTest/SalmonCacheTest.csproj | 27 +++++ 5 files changed, 295 insertions(+) create mode 100644 SalmonCache.sln create mode 100644 SalmonCache/SalmonCache.cs create mode 100644 SalmonCache/SalmonCache.csproj create mode 100644 SalmonCacheTest/SalmonCacheTest.cs create mode 100644 SalmonCacheTest/SalmonCacheTest.csproj diff --git a/SalmonCache.sln b/SalmonCache.sln new file mode 100644 index 0000000..14d1202 --- /dev/null +++ b/SalmonCache.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCache", "SalmonCache\SalmonCache.csproj", "{554D35BB-451F-484B-A959-CF3438F951EA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SalmonCacheTest", "SalmonCacheTest\SalmonCacheTest.csproj", "{FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {554D35BB-451F-484B-A959-CF3438F951EA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {554D35BB-451F-484B-A959-CF3438F951EA}.Release|Any CPU.Build.0 = Release|Any CPU + {FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA3B3EDF-F9A8-47B2-AB2D-9844E92A86EB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/SalmonCache/SalmonCache.cs b/SalmonCache/SalmonCache.cs new file mode 100644 index 0000000..8d8544e --- /dev/null +++ b/SalmonCache/SalmonCache.cs @@ -0,0 +1,146 @@ +namespace SalmonCache; + +public class SalmonCache where K : notnull +{ + private class SalmonCacheValue + { + public K Key { set; get; } + public V Value { set; get; } + public SalmonCacheValue(K key, V value) + { + Key = key; + Value = value; + } + } + private int _capacity; + private bool _logRemovals; + private readonly ReaderWriterLockSlim _lock; + private readonly LinkedList _storage; + private readonly Dictionary> _cache; + public SalmonCache(int capacity, bool logRemovals = false) + { + _capacity = capacity; + _logRemovals = logRemovals; + _lock = new(); + _storage = new(); + _cache = new(_capacity); + } + public int Count + { + get + { + lock (_lock) { return _storage.Count; } + } + } + public int Capacity + { + set + { + Resize(value); + } + get + { + lock (_lock) { return _capacity; } + } + } + private SalmonCacheValue? LockedRemoveLast() + { + var node = _storage.Last; + if (node == null) + return null; + var results = node.Value; + _storage.Remove(node); + _cache.Remove(results.Key); + return results; + } + public int Resize(int capacity) + { + if (capacity <= 0) throw new ArgumentOutOfRangeException("capacity must be >= 0"); + int removedCount = 0; + lock (_lock) + { + _capacity = capacity; + while (_storage.Count > _capacity) + { + var removed = LockedRemoveLast(); + if (removed != null) + { + if (_logRemovals) + Console.WriteLine($"SalmonCache::Resize removed key \"{removed.Key}\""); + removedCount++; + } + } + } + return removedCount; + } + public (V?, bool) Lookup(K key) + { + lock (_lock) + { + LinkedListNode? node; + if (_cache.TryGetValue(key, out node)) + { + _storage.Remove(node); + _storage.AddFirst(node); + return (node.Value.Value, true); + } + else + { + return (default, false); + } + } + } + public bool Insert(K key, V value) + { + lock (_lock) + { + LinkedListNode? node; + if (_cache.TryGetValue(key, out node)) + { + _storage.Remove(node); + _storage.AddFirst(node); + return false; + } + else + { + while (_storage.Count + 1 > _capacity) + { + var removed = LockedRemoveLast(); + if (removed != null && _logRemovals) + { + Console.WriteLine($"SalmonCache::Insert removed key \"{removed.Key}\""); + } + } + node = _storage.AddFirst(new SalmonCacheValue(key, value)); + _cache[key] = node; + return true; + } + } + } + public (V?, bool) Remove(K key) + { + lock (_lock) + { + LinkedListNode? node; + if (_cache.TryGetValue(key, out node)) + { + var stored = node.Value; + _storage.Remove(node); + _cache.Remove(key); + return (stored.Value, true); + } + else + { + return (default, false); + } + } + } + public void Clear() + { + lock (_lock) + { + _storage.Clear(); + _cache.Clear(); + } + } +} diff --git a/SalmonCache/SalmonCache.csproj b/SalmonCache/SalmonCache.csproj new file mode 100644 index 0000000..3d56e2b --- /dev/null +++ b/SalmonCache/SalmonCache.csproj @@ -0,0 +1,10 @@ + + + + netstandard2.0 + 12.0 + enable + enable + + + diff --git a/SalmonCacheTest/SalmonCacheTest.cs b/SalmonCacheTest/SalmonCacheTest.cs new file mode 100644 index 0000000..0465e4c --- /dev/null +++ b/SalmonCacheTest/SalmonCacheTest.cs @@ -0,0 +1,84 @@ +namespace SalmonCacheTest; + +public class SalmonCacheTests +{ + private readonly List _words = new(); + [SetUp] + public void Setup() + { + StreamReader reader = new("/usr/share/dict/words"); + Dictionary Words = new(); + var line = reader.ReadLine(); + while (line != null) + { + var word = line.Trim().ToLower(); + if (!Words.ContainsKey(word)) Words.Add(word, true); + } + reader.Close(); + _words.AddRange(Words.Keys.ToArray()); + _words.Sort(); + } + + [Test] + public void TestBasicInsertAndLookup() + { + var cache = new SalmonCache.SalmonCache(_words.Count); + Assert.That(cache.Capacity, Is.EqualTo(_words.Count)); + foreach (var word in _words) + { + var inserted = cache.Insert(word, word.ToUpper()); + Assert.That(inserted, Is.True); + var (stored, found) = cache.Lookup(word); + Assert.That(found, Is.True); + Assert.That(stored, Is.EqualTo(word.ToUpper())); + } + Assert.That(cache.Count, Is.EqualTo(_words.Count)); + } + + [Test] + public void TestRemove() + { + var cache = new SalmonCache.SalmonCache(_words.Count); + foreach (var word in _words) + { + Assert.That(cache.Insert(word, word.ToUpper()), Is.True); + } + Assert.That(cache.Count, Is.EqualTo(_words.Count)); + foreach (var word in _words) + { + var (stored, found) = cache.Remove(word); + Assert.That(found, Is.True); + Assert.That(stored, Is.EqualTo(word.ToUpper())); + } + } + + [Test] + public void TestResizeAndClear() + { + var cache = new SalmonCache.SalmonCache(_words.Count); + foreach (var word in _words) + { + Assert.That(cache.Insert(word, word.ToUpper()), Is.True); + } + Assert.That(cache.Count, Is.EqualTo(_words.Count)); + var capacity = _words.Count / 2; + Assert.That(cache.Count, Is.EqualTo(capacity)); + // the first (_words.Count - capacity) words will have been removed + for (var i = 0; i < _words.Count - capacity; i++) + { + var word = _words.ElementAt(i); + var (stored, found) = cache.Lookup(word); + Assert.That(found, Is.False); + Assert.That(stored, Is.Empty); + } + for (var i = _words.Count - capacity; i < _words.Count; i++) + { + var word = _words.ElementAt(i); + var (stored, found) = cache.Lookup(word); + Assert.That(found, Is.True); + Assert.That(stored, Is.EqualTo(word.ToUpper())); + } + cache.Clear(); + Assert.That(cache.Count, Is.Empty); + } +} diff --git a/SalmonCacheTest/SalmonCacheTest.csproj b/SalmonCacheTest/SalmonCacheTest.csproj new file mode 100644 index 0000000..b4db8d1 --- /dev/null +++ b/SalmonCacheTest/SalmonCacheTest.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + +