From 8c953ca4a64e7f11b33f33cab2451b25c6e2702d Mon Sep 17 00:00:00 2001 From: Venkat Date: Mon, 15 Jan 2024 17:02:11 -0800 Subject: [PATCH 01/14] initial commit --- go.mod | 8 +- internal/list.go | 7 +- lru.go | 20 +++ simplelru/lru.go | 114 ++++++++++++++- simplelru/sieve_test.go | 298 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 442 insertions(+), 5 deletions(-) create mode 100644 simplelru/sieve_test.go diff --git a/go.mod b/go.mod index 8aaa473..f09ae2b 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,9 @@ -module github.com/hashicorp/golang-lru/v2 +module github.com/venkatsvpr/golang-lru/v2 go 1.18 + +replace github.com/hashicorp/golang-lru/v2 => ./ + +replace github.com/hashicorp/golang-lru/v2/internal => ./internal + +require github.com/hashicorp/golang-lru/v2 v2.0.0-00010101000000-000000000000 diff --git a/internal/list.go b/internal/list.go index 5cd74a0..0f826c9 100644 --- a/internal/list.go +++ b/internal/list.go @@ -4,7 +4,9 @@ package internal -import "time" +import ( + "time" +) // Entry is an LRU Entry type Entry[K comparable, V any] struct { @@ -29,6 +31,9 @@ type Entry[K comparable, V any] struct { // The expiry bucket item was put in, optional ExpireBucket uint8 + + // Visited to keep track of entries for sieve-eviction + Visited bool } // PrevEntry returns the previous list element or nil. diff --git a/lru.go b/lru.go index 2bb07fd..1172553 100644 --- a/lru.go +++ b/lru.go @@ -28,6 +28,11 @@ func New[K comparable, V any](size int) (*Cache[K, V], error) { return NewWithEvict[K, V](size, nil) } +// NewSieve creates an LRU of the given size. +func NewSieve[K comparable, V any](size int) (*Cache[K, V], error) { + return NewSieveWithEvict[K, V](size, nil) +} + // NewWithEvict constructs a fixed size cache with the given eviction // callback. func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { @@ -43,6 +48,21 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) return } +// NewSieveWithEvict constructs a fixed size cache with the given eviction +// callback. +func NewSieveWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { + // create a cache with default settings + c = &Cache[K, V]{ + onEvictedCB: onEvicted, + } + if onEvicted != nil { + c.initEvictBuffers() + onEvicted = c.onEvicted + } + c.lru, err = simplelru.NewSieve(size, onEvicted) + return +} + func (c *Cache[K, V]) initEvictBuffers() { c.evictedKeys = make([]K, 0, DefaultEvictedBufferSize) c.evictedVals = make([]V, 0, DefaultEvictedBufferSize) diff --git a/simplelru/lru.go b/simplelru/lru.go index 8f45d2e..0d94982 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -18,6 +18,8 @@ type LRU[K comparable, V any] struct { evictList *internal.LruList[K, V] items map[K]*internal.Entry[K, V] onEvict EvictCallback[K, V] + hand *internal.Entry[K, V] + useSieve bool } // NewLRU constructs an LRU of the given size @@ -32,6 +34,25 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, items: make(map[K]*internal.Entry[K, V]), onEvict: onEvict, } + + return c, nil +} + +// NewSieve constructs an LRU of the given size +func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { + if size <= 0 { + return nil, errors.New("must provide a positive size") + } + + c := &LRU[K, V]{ + size: size, + evictList: internal.NewList[K, V](), + items: make(map[K]*internal.Entry[K, V]), + onEvict: onEvict, + hand: nil, + useSieve: true, + } + return c, nil } @@ -50,11 +71,28 @@ func (c *LRU[K, V]) Purge() { func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { // Check for existing item if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) + if c.useSieve { + ent.Visited = true + } else { + c.evictList.MoveToFront(ent) + } + ent.Value = value return false } + if c.useSieve { + if c.evictList.Length() >= c.size { + c.performSieveEviction() + evicted = true + } + + ent := c.evictList.PushFront(key, value) + ent.Visited = false + c.items[key] = ent + return + } + // Add new item ent := c.evictList.PushFront(key, value) c.items[key] = ent @@ -70,7 +108,12 @@ func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { // Get looks up a key's value from the cache. func (c *LRU[K, V]) Get(key K) (value V, ok bool) { if ent, ok := c.items[key]; ok { - c.evictList.MoveToFront(ent) + if c.useSieve { + ent.Visited = true + } else { + c.evictList.MoveToFront(ent) + } + return ent.Value, true } return @@ -93,6 +136,16 @@ func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { return } +// visited returns if the key is visited +func (c *LRU[K, V]) visited(key K) (present bool, visited bool) { + var ent *internal.Entry[K, V] + if ent, present = c.items[key]; present { + visited = ent.Visited + } + + return +} + // Remove removes the provided key from the cache, returning if the // key was contained. func (c *LRU[K, V]) Remove(key K) (present bool) { @@ -105,6 +158,10 @@ func (c *LRU[K, V]) Remove(key K) (present bool) { // RemoveOldest removes the oldest item from the cache. func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { + if c.useSieve { + return c.performSieveEviction() + } + if ent := c.evictList.Back(); ent != nil { c.removeElement(ent) return ent.Key, ent.Value, true @@ -114,6 +171,15 @@ func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { // GetOldest returns the oldest entry func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { + if c.useSieve { + c.getSieveCandidate() + if c.hand != nil { + return c.hand.Key, c.hand.Value, true + } + + return + } + if ent := c.evictList.Back(); ent != nil { return ent.Key, ent.Value, true } @@ -159,14 +225,56 @@ func (c *LRU[K, V]) Resize(size int) (evicted int) { diff = 0 } for i := 0; i < diff; i++ { - c.removeOldest() + if c.useSieve { + c.performSieveEviction() + } else { + c.removeOldest() + } } + c.size = size return diff } +// performSieveEviction - runs a eviction by running Sieve Algorithm and returns the evicted value. +func (c *LRU[K, V]) performSieveEviction() (key K, value V, ok bool) { + c.getSieveCandidate() + if c.hand != nil { + candidate := c.hand + c.hand = c.hand.PrevEntry() + c.removeElement(candidate) + return candidate.Key, candidate.Value, true + } + + return +} + +// getSieveCandidate evicts an entry based on sieve algorithm. +func (c *LRU[K, V]) getSieveCandidate() { + if c.Len() == 0 { + return + } + + if c.hand == nil { + c.hand = c.evictList.Back() + } + + for c.hand != nil && c.hand.Visited { + c.hand.Visited = false + c.hand = c.hand.PrevEntry() + if c.hand == nil { + c.hand = c.evictList.Back() + } + } +} + // removeOldest removes the oldest item from the cache. func (c *LRU[K, V]) removeOldest() { + if c.useSieve { + c.performSieveEviction() + return + } + if ent := c.evictList.Back(); ent != nil { c.removeElement(ent) } diff --git a/simplelru/sieve_test.go b/simplelru/sieve_test.go new file mode 100644 index 0000000..214ee34 --- /dev/null +++ b/simplelru/sieve_test.go @@ -0,0 +1,298 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package simplelru + +import ( + "reflect" + "testing" +) + +func TestSieve(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + l, err := NewSieve(128, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + if l.Cap() != 128 { + t.Fatalf("expect %d, but %d", 128, l.Cap()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + if ok := l.Remove(i); !ok { + t.Fatalf("should be contained") + } + if ok := l.Remove(i); ok { + t.Fatalf("should not be contained") + } + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.Get(192) // expect 192 to be last key in l.Keys() + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +func TestSieve_GetOldest_RemoveOldest(t *testing.T) { + l, err := NewSieve[int, int](128, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + for i := 0; i < 256; i++ { + l.Add(i, i) + } + k, _, ok := l.GetOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 128 { + t.Fatalf("bad: %v", k) + } + + k, _, ok = l.RemoveOldest() + if !ok { + t.Fatalf("missing") + } + if k != 129 { + t.Fatalf("bad: %v", k) + } +} + +// Test that Add returns true/false if an eviction occurred +func TestSieve_Add(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + evictCounter++ + } + + l, err := NewSieve(1, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + if l.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if l.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } +} + +// Test that Contains doesn't update recent-ness +func TestSieve_Contains(t *testing.T) { + l, err := NewSieve[int, int](2, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// Test that Peek doesn't update recent-ness +func TestSieve_Peek(t *testing.T) { + l, err := NewSieve[int, int](2, nil) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} + +// Test that Resize can upsize and downsize +func TestSieve_Resize(t *testing.T) { + onEvictCounter := 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + l, err := NewSieve(2, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } +} + +func TestSieve_EvictionSameKey(t *testing.T) { + var evictedKeys []int + + cache, _ := NewSieve( + 2, + func(key int, _ struct{}) { + evictedKeys = append(evictedKeys, key) + }) + + if evicted := cache.Add(1, struct{}{}); evicted { + t.Error("First 1: got unexpected eviction") + } + cache.wantKeys(t, []int{1}) + + if evicted := cache.Add(2, struct{}{}); evicted { + t.Error("2: got unexpected eviction") + } + cache.wantKeys(t, []int{1, 2}) + + for _, k := range []int{1, 2} { + present, visited := cache.visited(k) + if !present || visited { + t.Errorf("Expecting both the keys to be present without visited being set present:%v visited:%v", present, visited) + } + } + + if evicted := cache.Add(1, struct{}{}); evicted { + t.Error("Second 1: got unexpected eviction") + } + + present, visited := cache.visited(1) + if !present || !visited { + t.Errorf("Expecting the key to be visited and present , Actual present:%v visited:%v", present, visited) + } + + present, visited = cache.visited(2) + if !present || visited { + t.Errorf("Expecting the key to be not visited and present , Actual present:%v visited:%v", present, visited) + } + + if evicted := cache.Add(2, struct{}{}); evicted { + t.Error("Second 1: got unexpected eviction") + } + + for _, k := range []int{2, 1} { + present, visited := cache.visited(k) + if !present || !visited { + t.Errorf("Expecting both the keys to be present and visited, Actual present:%v visited:%v", present, visited) + } + } + + if evicted := cache.Add(3, struct{}{}); !evicted { + t.Error("3: did not get expected eviction") + } + + cache.wantKeys(t, []int{2, 3}) + if evicted := cache.Add(4, struct{}{}); !evicted { + t.Error("4: did not get expected eviction") + } + + cache.wantKeys(t, []int{3, 4}) + if _, ok := cache.Get(3); !ok { + t.Errorf("3 should be present in cache") + } + + present, visited = cache.visited(3) + if !present || !visited { + t.Errorf("Expecting the key to be present and visited, Actual present:%v visited:%v", present, visited) + } + + present, visited = cache.visited(4) + if !present || visited { + t.Errorf("Expecting the key to be present and not visited, Actual present:%v visited:%v", present, visited) + } + + if evicted := cache.Add(1, struct{}{}); !evicted { + t.Error("1: did not get expected eviction") + } + + cache.wantKeys(t, []int{3, 1}) + + want := []int{1, 2, 4} + if !reflect.DeepEqual(evictedKeys, want) { + t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) + } +} + + From bbc5d78fcc4c6b92226ef0637ea40d9a94d434f1 Mon Sep 17 00:00:00 2001 From: Venkat Date: Mon, 15 Jan 2024 17:18:27 -0800 Subject: [PATCH 02/14] benchmark tests --- lru_test.go | 111 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 40 deletions(-) diff --git a/lru_test.go b/lru_test.go index 7ecc7ae..eeaa5e6 100644 --- a/lru_test.go +++ b/lru_test.go @@ -8,63 +8,94 @@ import ( "testing" ) -func BenchmarkLRU_Rand(b *testing.B) { - l, err := New[int64, int64](8192) - if err != nil { - b.Fatalf("err: %v", err) - } +func Benchmark_Rand(b *testing.B) { + var fn = func(b *testing.B, l *Cache[int64, int64]) { + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = getRand(b) % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + } - trace := make([]int64, b.N*2) - for i := 0; i < b.N*2; i++ { - trace[i] = getRand(b) % 32768 + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) } - b.ResetTimer() + b.Run("Benchmark with LRU ", func(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) + + b.Run("Benchmark with Sieve ", func(b *testing.B) { + l, err := NewSieve[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) +} + +func BenchmarkLRU_Freq(b *testing.B) { + var fn = func(b *testing.B, l *Cache[int64, int64]) { + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = getRand(b) % 16384 + } else { + trace[i] = getRand(b) % 32768 + } + } + + b.ResetTimer() - var hit, miss int - for i := 0; i < 2*b.N; i++ { - if i%2 == 0 { + for i := 0; i < b.N; i++ { l.Add(trace[i], trace[i]) - } else { + } + var hit, miss int + for i := 0; i < b.N; i++ { if _, ok := l.Get(trace[i]); ok { hit++ } else { miss++ } } - } - b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) -} - -func BenchmarkLRU_Freq(b *testing.B) { - l, err := New[int64, int64](8192) - if err != nil { - b.Fatalf("err: %v", err) + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) } - trace := make([]int64, b.N*2) - for i := 0; i < b.N*2; i++ { - if i%2 == 0 { - trace[i] = getRand(b) % 16384 - } else { - trace[i] = getRand(b) % 32768 + b.Run("Benchmark with LRU ", func(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) } - } - b.ResetTimer() + fn(b, l) + }) - for i := 0; i < b.N; i++ { - l.Add(trace[i], trace[i]) - } - var hit, miss int - for i := 0; i < b.N; i++ { - if _, ok := l.Get(trace[i]); ok { - hit++ - } else { - miss++ + b.Run("Benchmark with Sieve ", func(b *testing.B) { + l, err := NewSieve[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) } - } - b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) + + fn(b, l) + }) } func TestLRU(t *testing.T) { From 798462d290190cf0008c3bb8606046df184c9eda Mon Sep 17 00:00:00 2001 From: Venkat Date: Mon, 15 Jan 2024 17:34:10 -0800 Subject: [PATCH 03/14] fixing the comments: --- internal/list.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/list.go b/internal/list.go index 0f826c9..8886102 100644 --- a/internal/list.go +++ b/internal/list.go @@ -4,9 +4,7 @@ package internal -import ( - "time" -) +import "time" // Entry is an LRU Entry type Entry[K comparable, V any] struct { @@ -32,7 +30,7 @@ type Entry[K comparable, V any] struct { // The expiry bucket item was put in, optional ExpireBucket uint8 - // Visited to keep track of entries for sieve-eviction + // Visited to keep track if an entry has been recently accessed, used for sieve-eviction Visited bool } From 475d0fe4e5374a21cd9352a2f8ab7d7569c2a320 Mon Sep 17 00:00:00 2001 From: Venkat Date: Mon, 15 Jan 2024 18:52:09 -0800 Subject: [PATCH 04/14] addressing review comments --- lru.go | 42 ++++++++++++++++++++-------------------- simplelru/lru.go | 50 ++++++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/lru.go b/lru.go index 1172553..f9b2b3f 100644 --- a/lru.go +++ b/lru.go @@ -16,7 +16,7 @@ const ( // Cache is a thread-safe fixed size LRU cache. type Cache[K comparable, V any] struct { - lru *simplelru.LRU[K, V] + cache *simplelru.LRU[K, V] evictedKeys []K evictedVals []V onEvictedCB func(k K, v V) @@ -28,7 +28,7 @@ func New[K comparable, V any](size int) (*Cache[K, V], error) { return NewWithEvict[K, V](size, nil) } -// NewSieve creates an LRU of the given size. +// NewSieve creates fixed size cache with SIEVE eviction. https://cachemon.github.io/SIEVE-website/ func NewSieve[K comparable, V any](size int) (*Cache[K, V], error) { return NewSieveWithEvict[K, V](size, nil) } @@ -44,7 +44,7 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) c.initEvictBuffers() onEvicted = c.onEvicted } - c.lru, err = simplelru.NewLRU(size, onEvicted) + c.cache, err = simplelru.NewLRU(size, onEvicted) return } @@ -59,7 +59,7 @@ func NewSieveWithEvict[K comparable, V any](size int, onEvicted func(key K, valu c.initEvictBuffers() onEvicted = c.onEvicted } - c.lru, err = simplelru.NewSieve(size, onEvicted) + c.cache, err = simplelru.NewSieve(size, onEvicted) return } @@ -80,7 +80,7 @@ func (c *Cache[K, V]) Purge() { var ks []K var vs []V c.lock.Lock() - c.lru.Purge() + c.cache.Purge() if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { ks, vs = c.evictedKeys, c.evictedVals c.initEvictBuffers() @@ -99,7 +99,7 @@ func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { var k K var v V c.lock.Lock() - evicted = c.lru.Add(key, value) + evicted = c.cache.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -114,7 +114,7 @@ func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { // Get looks up a key's value from the cache. func (c *Cache[K, V]) Get(key K) (value V, ok bool) { c.lock.Lock() - value, ok = c.lru.Get(key) + value, ok = c.cache.Get(key) c.lock.Unlock() return value, ok } @@ -123,7 +123,7 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) { // recent-ness or deleting it for being stale. func (c *Cache[K, V]) Contains(key K) bool { c.lock.RLock() - containKey := c.lru.Contains(key) + containKey := c.cache.Contains(key) c.lock.RUnlock() return containKey } @@ -132,7 +132,7 @@ func (c *Cache[K, V]) Contains(key K) bool { // the "recently used"-ness of the key. func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { c.lock.RLock() - value, ok = c.lru.Peek(key) + value, ok = c.cache.Peek(key) c.lock.RUnlock() return value, ok } @@ -144,11 +144,11 @@ func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) { var k K var v V c.lock.Lock() - if c.lru.Contains(key) { + if c.cache.Contains(key) { c.lock.Unlock() return true, false } - evicted = c.lru.Add(key, value) + evicted = c.cache.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -167,12 +167,12 @@ func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) { var k K var v V c.lock.Lock() - previous, ok = c.lru.Peek(key) + previous, ok = c.cache.Peek(key) if ok { c.lock.Unlock() return previous, true, false } - evicted = c.lru.Add(key, value) + evicted = c.cache.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -189,7 +189,7 @@ func (c *Cache[K, V]) Remove(key K) (present bool) { var k K var v V c.lock.Lock() - present = c.lru.Remove(key) + present = c.cache.Remove(key) if c.onEvictedCB != nil && present { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -206,7 +206,7 @@ func (c *Cache[K, V]) Resize(size int) (evicted int) { var ks []K var vs []V c.lock.Lock() - evicted = c.lru.Resize(size) + evicted = c.cache.Resize(size) if c.onEvictedCB != nil && evicted > 0 { ks, vs = c.evictedKeys, c.evictedVals c.initEvictBuffers() @@ -225,7 +225,7 @@ func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { var k K var v V c.lock.Lock() - key, value, ok = c.lru.RemoveOldest() + key, value, ok = c.cache.RemoveOldest() if c.onEvictedCB != nil && ok { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -240,7 +240,7 @@ func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { // GetOldest returns the oldest entry func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { c.lock.RLock() - key, value, ok = c.lru.GetOldest() + key, value, ok = c.cache.GetOldest() c.lock.RUnlock() return } @@ -248,7 +248,7 @@ func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { // Keys returns a slice of the keys in the cache, from oldest to newest. func (c *Cache[K, V]) Keys() []K { c.lock.RLock() - keys := c.lru.Keys() + keys := c.cache.Keys() c.lock.RUnlock() return keys } @@ -256,7 +256,7 @@ func (c *Cache[K, V]) Keys() []K { // Values returns a slice of the values in the cache, from oldest to newest. func (c *Cache[K, V]) Values() []V { c.lock.RLock() - values := c.lru.Values() + values := c.cache.Values() c.lock.RUnlock() return values } @@ -264,12 +264,12 @@ func (c *Cache[K, V]) Values() []V { // Len returns the number of items in the cache. func (c *Cache[K, V]) Len() int { c.lock.RLock() - length := c.lru.Len() + length := c.cache.Len() c.lock.RUnlock() return length } // Cap returns the capacity of the cache func (c *Cache[K, V]) Cap() int { - return c.lru.Cap() + return c.cache.Cap() } diff --git a/simplelru/lru.go b/simplelru/lru.go index 0d94982..b9eda4a 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -12,8 +12,8 @@ import ( // EvictCallback is used to get a callback when a cache entry is evicted type EvictCallback[K comparable, V any] func(key K, value V) -// LRU implements a non-thread safe fixed size LRU cache -type LRU[K comparable, V any] struct { +// Cache implements a non-thread safe fixed cache with Cache and SIEVE eviction (https://cachemon.github.io/SIEVE-website/) +type Cache[K comparable, V any] struct { size int evictList *internal.LruList[K, V] items map[K]*internal.Entry[K, V] @@ -23,12 +23,12 @@ type LRU[K comparable, V any] struct { } // NewLRU constructs an LRU of the given size -func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { +func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cache[K, V], error) { if size <= 0 { return nil, errors.New("must provide a positive size") } - c := &LRU[K, V]{ + c := &Cache[K, V]{ size: size, evictList: internal.NewList[K, V](), items: make(map[K]*internal.Entry[K, V]), @@ -38,13 +38,13 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, return c, nil } -// NewSieve constructs an LRU of the given size -func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { +// NewSieve constructs a SIEVE of the given size +func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cache[K, V], error) { if size <= 0 { return nil, errors.New("must provide a positive size") } - c := &LRU[K, V]{ + c := &Cache[K, V]{ size: size, evictList: internal.NewList[K, V](), items: make(map[K]*internal.Entry[K, V]), @@ -57,7 +57,7 @@ func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[ } // Purge is used to completely clear the cache. -func (c *LRU[K, V]) Purge() { +func (c *Cache[K, V]) Purge() { for k, v := range c.items { if c.onEvict != nil { c.onEvict(k, v.Value) @@ -68,7 +68,7 @@ func (c *LRU[K, V]) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { +func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { // Check for existing item if ent, ok := c.items[key]; ok { if c.useSieve { @@ -106,7 +106,7 @@ func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { } // Get looks up a key's value from the cache. -func (c *LRU[K, V]) Get(key K) (value V, ok bool) { +func (c *Cache[K, V]) Get(key K) (value V, ok bool) { if ent, ok := c.items[key]; ok { if c.useSieve { ent.Visited = true @@ -121,14 +121,14 @@ func (c *LRU[K, V]) Get(key K) (value V, ok bool) { // Contains checks if a key is in the cache, without updating the recent-ness // or deleting it for being stale. -func (c *LRU[K, V]) Contains(key K) (ok bool) { +func (c *Cache[K, V]) Contains(key K) (ok bool) { _, ok = c.items[key] return ok } // Peek returns the key value (or undefined if not found) without updating // the "recently used"-ness of the key. -func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { +func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { var ent *internal.Entry[K, V] if ent, ok = c.items[key]; ok { return ent.Value, true @@ -137,7 +137,7 @@ func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { } // visited returns if the key is visited -func (c *LRU[K, V]) visited(key K) (present bool, visited bool) { +func (c *Cache[K, V]) visited(key K) (present bool, visited bool) { var ent *internal.Entry[K, V] if ent, present = c.items[key]; present { visited = ent.Visited @@ -148,7 +148,7 @@ func (c *LRU[K, V]) visited(key K) (present bool, visited bool) { // Remove removes the provided key from the cache, returning if the // key was contained. -func (c *LRU[K, V]) Remove(key K) (present bool) { +func (c *Cache[K, V]) Remove(key K) (present bool) { if ent, ok := c.items[key]; ok { c.removeElement(ent) return true @@ -157,7 +157,7 @@ func (c *LRU[K, V]) Remove(key K) (present bool) { } // RemoveOldest removes the oldest item from the cache. -func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { +func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { if c.useSieve { return c.performSieveEviction() } @@ -170,7 +170,7 @@ func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { } // GetOldest returns the oldest entry -func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { +func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { if c.useSieve { c.getSieveCandidate() if c.hand != nil { @@ -187,7 +187,7 @@ func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { } // Keys returns a slice of the keys in the cache, from oldest to newest. -func (c *LRU[K, V]) Keys() []K { +func (c *Cache[K, V]) Keys() []K { keys := make([]K, c.evictList.Length()) i := 0 for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { @@ -198,7 +198,7 @@ func (c *LRU[K, V]) Keys() []K { } // Values returns a slice of the values in the cache, from oldest to newest. -func (c *LRU[K, V]) Values() []V { +func (c *Cache[K, V]) Values() []V { values := make([]V, len(c.items)) i := 0 for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { @@ -209,17 +209,17 @@ func (c *LRU[K, V]) Values() []V { } // Len returns the number of items in the cache. -func (c *LRU[K, V]) Len() int { +func (c *Cache[K, V]) Len() int { return c.evictList.Length() } // Cap returns the capacity of the cache -func (c *LRU[K, V]) Cap() int { +func (c *Cache[K, V]) Cap() int { return c.size } // Resize changes the cache size. -func (c *LRU[K, V]) Resize(size int) (evicted int) { +func (c *Cache[K, V]) Resize(size int) (evicted int) { diff := c.Len() - size if diff < 0 { diff = 0 @@ -237,7 +237,7 @@ func (c *LRU[K, V]) Resize(size int) (evicted int) { } // performSieveEviction - runs a eviction by running Sieve Algorithm and returns the evicted value. -func (c *LRU[K, V]) performSieveEviction() (key K, value V, ok bool) { +func (c *Cache[K, V]) performSieveEviction() (key K, value V, ok bool) { c.getSieveCandidate() if c.hand != nil { candidate := c.hand @@ -250,7 +250,7 @@ func (c *LRU[K, V]) performSieveEviction() (key K, value V, ok bool) { } // getSieveCandidate evicts an entry based on sieve algorithm. -func (c *LRU[K, V]) getSieveCandidate() { +func (c *Cache[K, V]) getSieveCandidate() { if c.Len() == 0 { return } @@ -269,7 +269,7 @@ func (c *LRU[K, V]) getSieveCandidate() { } // removeOldest removes the oldest item from the cache. -func (c *LRU[K, V]) removeOldest() { +func (c *Cache[K, V]) removeOldest() { if c.useSieve { c.performSieveEviction() return @@ -281,7 +281,7 @@ func (c *LRU[K, V]) removeOldest() { } // removeElement is used to remove a given list element from the cache -func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) { +func (c *Cache[K, V]) removeElement(e *internal.Entry[K, V]) { c.evictList.Remove(e) delete(c.items, e.Key) if c.onEvict != nil { From 3a13508e38ec5450530f91e9112ee6b35e6ad803 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 16 Jan 2024 14:55:53 -0800 Subject: [PATCH 05/14] update doc --- doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index 24107ee..4aa2355 100644 --- a/doc.go +++ b/doc.go @@ -3,8 +3,8 @@ // Package lru provides three different LRU caches of varying sophistication. // -// Cache is a simple LRU cache. It is based on the LRU implementation in -// groupcache: https://github.com/golang/groupcache/tree/master/lru +// Cache is a simple cache with LRU and SIEVE ection. It is based on the LRU +// implementation in groupcache: https://github.com/golang/groupcache/tree/master/lru // // TwoQueueCache tracks frequently used and recently used entries separately. // This avoids a burst of accesses from taking out frequently used entries, at From a3513719101dc7c55734ae3d9c05116c72d1b0e9 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 16 Jan 2024 14:59:35 -0800 Subject: [PATCH 06/14] typo fix --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 4aa2355..8faacd2 100644 --- a/doc.go +++ b/doc.go @@ -3,7 +3,7 @@ // Package lru provides three different LRU caches of varying sophistication. // -// Cache is a simple cache with LRU and SIEVE ection. It is based on the LRU +// Cache is a simple cache with LRU and SIEVE eviction. The LRU is based on the LRU // implementation in groupcache: https://github.com/golang/groupcache/tree/master/lru // // TwoQueueCache tracks frequently used and recently used entries separately. From 68e8f8dfb2b6a6d1a079c48532ac63296fab0f0b Mon Sep 17 00:00:00 2001 From: Venkat Date: Wed, 17 Jan 2024 19:43:32 -0800 Subject: [PATCH 07/14] update the names --- lru.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lru.go b/lru.go index f9b2b3f..885e470 100644 --- a/lru.go +++ b/lru.go @@ -16,7 +16,7 @@ const ( // Cache is a thread-safe fixed size LRU cache. type Cache[K comparable, V any] struct { - cache *simplelru.LRU[K, V] + cache *simplelru.Cache[K, V] evictedKeys []K evictedVals []V onEvictedCB func(k K, v V) From 1a1c399f88d4a8b8f4e28f896641701cf7744233 Mon Sep 17 00:00:00 2001 From: venkata krishnan Date: Sun, 21 Jan 2024 08:54:24 -0800 Subject: [PATCH 08/14] setting up the changes --- lru.go | 89 ++++++++++++++++++++++++++++++------------------ lru_test.go | 4 +-- simplelru/lru.go | 48 +++++++++++++------------- 3 files changed, 81 insertions(+), 60 deletions(-) diff --git a/lru.go b/lru.go index 885e470..acb851f 100644 --- a/lru.go +++ b/lru.go @@ -16,11 +16,12 @@ const ( // Cache is a thread-safe fixed size LRU cache. type Cache[K comparable, V any] struct { - cache *simplelru.Cache[K, V] + lru *simplelru.LRU[K, V] evictedKeys []K evictedVals []V onEvictedCB func(k K, v V) lock sync.RWMutex + sieveOpt bool } // New creates an LRU of the given size. @@ -28,11 +29,6 @@ func New[K comparable, V any](size int) (*Cache[K, V], error) { return NewWithEvict[K, V](size, nil) } -// NewSieve creates fixed size cache with SIEVE eviction. https://cachemon.github.io/SIEVE-website/ -func NewSieve[K comparable, V any](size int) (*Cache[K, V], error) { - return NewSieveWithEvict[K, V](size, nil) -} - // NewWithEvict constructs a fixed size cache with the given eviction // callback. func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { @@ -44,23 +40,48 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) c.initEvictBuffers() onEvicted = c.onEvicted } - c.cache, err = simplelru.NewLRU(size, onEvicted) + c.lru, err = simplelru.NewLRU(size, onEvicted) return } -// NewSieveWithEvict constructs a fixed size cache with the given eviction -// callback. -func NewSieveWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) (c *Cache[K, V], err error) { +// WithCallback returns a SieveOption with eviction callback. +func WithCallback[K comparable, V any](onEvicted func(key K, value V)) SieveOption[K, V] { + return func(c *Cache[K, V]) { + c.onEvictedCB = onEvicted + + if onEvicted != nil { + c.initEvictBuffers() + onEvicted = c.onEvicted + } + } +} + +// WithSieve returns a SieveOption that enables sieve +func WithSieve[K comparable, V any]() SieveOption[K, V] { + return func(c *Cache[K, V]) { + c.sieveOpt = true + } +} + +// SieveOption is used to set options for the Sieve cache. +type SieveOption[K comparable, V any] func(*Cache[K, V]) + +// NewSieveWithOpts helps create a LRU cache with option with options. +func NewSieveWithOpts[K comparable, V any](size int, opts ...SieveOption[K, V]) (c *Cache[K, V], err error) { // create a cache with default settings - c = &Cache[K, V]{ - onEvictedCB: onEvicted, + c = &Cache[K, V]{} + + for _, opt := range opts { + opt(c) } - if onEvicted != nil { - c.initEvictBuffers() - onEvicted = c.onEvicted + + if c.sieveOpt { + c.lru, err = simplelru.NewSieve(size, c.onEvictedCB) + } else { + c.lru, err = simplelru.NewLRU(size, c.onEvictedCB) } - c.cache, err = simplelru.NewSieve(size, onEvicted) - return + + return c, err } func (c *Cache[K, V]) initEvictBuffers() { @@ -80,7 +101,7 @@ func (c *Cache[K, V]) Purge() { var ks []K var vs []V c.lock.Lock() - c.cache.Purge() + c.lru.Purge() if c.onEvictedCB != nil && len(c.evictedKeys) > 0 { ks, vs = c.evictedKeys, c.evictedVals c.initEvictBuffers() @@ -99,7 +120,7 @@ func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { var k K var v V c.lock.Lock() - evicted = c.cache.Add(key, value) + evicted = c.lru.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -114,7 +135,7 @@ func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { // Get looks up a key's value from the cache. func (c *Cache[K, V]) Get(key K) (value V, ok bool) { c.lock.Lock() - value, ok = c.cache.Get(key) + value, ok = c.lru.Get(key) c.lock.Unlock() return value, ok } @@ -123,7 +144,7 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) { // recent-ness or deleting it for being stale. func (c *Cache[K, V]) Contains(key K) bool { c.lock.RLock() - containKey := c.cache.Contains(key) + containKey := c.lru.Contains(key) c.lock.RUnlock() return containKey } @@ -132,7 +153,7 @@ func (c *Cache[K, V]) Contains(key K) bool { // the "recently used"-ness of the key. func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { c.lock.RLock() - value, ok = c.cache.Peek(key) + value, ok = c.lru.Peek(key) c.lock.RUnlock() return value, ok } @@ -144,11 +165,11 @@ func (c *Cache[K, V]) ContainsOrAdd(key K, value V) (ok, evicted bool) { var k K var v V c.lock.Lock() - if c.cache.Contains(key) { + if c.lru.Contains(key) { c.lock.Unlock() return true, false } - evicted = c.cache.Add(key, value) + evicted = c.lru.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -167,12 +188,12 @@ func (c *Cache[K, V]) PeekOrAdd(key K, value V) (previous V, ok, evicted bool) { var k K var v V c.lock.Lock() - previous, ok = c.cache.Peek(key) + previous, ok = c.lru.Peek(key) if ok { c.lock.Unlock() return previous, true, false } - evicted = c.cache.Add(key, value) + evicted = c.lru.Add(key, value) if c.onEvictedCB != nil && evicted { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -189,7 +210,7 @@ func (c *Cache[K, V]) Remove(key K) (present bool) { var k K var v V c.lock.Lock() - present = c.cache.Remove(key) + present = c.lru.Remove(key) if c.onEvictedCB != nil && present { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -206,7 +227,7 @@ func (c *Cache[K, V]) Resize(size int) (evicted int) { var ks []K var vs []V c.lock.Lock() - evicted = c.cache.Resize(size) + evicted = c.lru.Resize(size) if c.onEvictedCB != nil && evicted > 0 { ks, vs = c.evictedKeys, c.evictedVals c.initEvictBuffers() @@ -225,7 +246,7 @@ func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { var k K var v V c.lock.Lock() - key, value, ok = c.cache.RemoveOldest() + key, value, ok = c.lru.RemoveOldest() if c.onEvictedCB != nil && ok { k, v = c.evictedKeys[0], c.evictedVals[0] c.evictedKeys, c.evictedVals = c.evictedKeys[:0], c.evictedVals[:0] @@ -240,7 +261,7 @@ func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { // GetOldest returns the oldest entry func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { c.lock.RLock() - key, value, ok = c.cache.GetOldest() + key, value, ok = c.lru.GetOldest() c.lock.RUnlock() return } @@ -248,7 +269,7 @@ func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { // Keys returns a slice of the keys in the cache, from oldest to newest. func (c *Cache[K, V]) Keys() []K { c.lock.RLock() - keys := c.cache.Keys() + keys := c.lru.Keys() c.lock.RUnlock() return keys } @@ -256,7 +277,7 @@ func (c *Cache[K, V]) Keys() []K { // Values returns a slice of the values in the cache, from oldest to newest. func (c *Cache[K, V]) Values() []V { c.lock.RLock() - values := c.cache.Values() + values := c.lru.Values() c.lock.RUnlock() return values } @@ -264,12 +285,12 @@ func (c *Cache[K, V]) Values() []V { // Len returns the number of items in the cache. func (c *Cache[K, V]) Len() int { c.lock.RLock() - length := c.cache.Len() + length := c.lru.Len() c.lock.RUnlock() return length } // Cap returns the capacity of the cache func (c *Cache[K, V]) Cap() int { - return c.cache.Cap() + return c.lru.Cap() } diff --git a/lru_test.go b/lru_test.go index eeaa5e6..882182b 100644 --- a/lru_test.go +++ b/lru_test.go @@ -43,7 +43,7 @@ func Benchmark_Rand(b *testing.B) { }) b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewSieve[int64, int64](8192) + l, err := NewSieveWithOpts[int64, int64](8192, WithSieve[int64, int64]()) if err != nil { b.Fatalf("err: %v", err) } @@ -89,7 +89,7 @@ func BenchmarkLRU_Freq(b *testing.B) { }) b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewSieve[int64, int64](8192) + l, err := NewSieveWithOpts[int64, int64](8192) if err != nil { b.Fatalf("err: %v", err) } diff --git a/simplelru/lru.go b/simplelru/lru.go index b9eda4a..8e4560f 100644 --- a/simplelru/lru.go +++ b/simplelru/lru.go @@ -12,8 +12,8 @@ import ( // EvictCallback is used to get a callback when a cache entry is evicted type EvictCallback[K comparable, V any] func(key K, value V) -// Cache implements a non-thread safe fixed cache with Cache and SIEVE eviction (https://cachemon.github.io/SIEVE-website/) -type Cache[K comparable, V any] struct { +// LRU implements a non-thread safe fixed cache with LRU and SIEVE eviction (https://cachemon.github.io/SIEVE-website/) +type LRU[K comparable, V any] struct { size int evictList *internal.LruList[K, V] items map[K]*internal.Entry[K, V] @@ -23,12 +23,12 @@ type Cache[K comparable, V any] struct { } // NewLRU constructs an LRU of the given size -func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cache[K, V], error) { +func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { if size <= 0 { return nil, errors.New("must provide a positive size") } - c := &Cache[K, V]{ + c := &LRU[K, V]{ size: size, evictList: internal.NewList[K, V](), items: make(map[K]*internal.Entry[K, V]), @@ -39,12 +39,12 @@ func NewLRU[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cache[ } // NewSieve constructs a SIEVE of the given size -func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cache[K, V], error) { +func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*LRU[K, V], error) { if size <= 0 { return nil, errors.New("must provide a positive size") } - c := &Cache[K, V]{ + c := &LRU[K, V]{ size: size, evictList: internal.NewList[K, V](), items: make(map[K]*internal.Entry[K, V]), @@ -57,7 +57,7 @@ func NewSieve[K comparable, V any](size int, onEvict EvictCallback[K, V]) (*Cach } // Purge is used to completely clear the cache. -func (c *Cache[K, V]) Purge() { +func (c *LRU[K, V]) Purge() { for k, v := range c.items { if c.onEvict != nil { c.onEvict(k, v.Value) @@ -68,7 +68,7 @@ func (c *Cache[K, V]) Purge() { } // Add adds a value to the cache. Returns true if an eviction occurred. -func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { +func (c *LRU[K, V]) Add(key K, value V) (evicted bool) { // Check for existing item if ent, ok := c.items[key]; ok { if c.useSieve { @@ -106,7 +106,7 @@ func (c *Cache[K, V]) Add(key K, value V) (evicted bool) { } // Get looks up a key's value from the cache. -func (c *Cache[K, V]) Get(key K) (value V, ok bool) { +func (c *LRU[K, V]) Get(key K) (value V, ok bool) { if ent, ok := c.items[key]; ok { if c.useSieve { ent.Visited = true @@ -121,14 +121,14 @@ func (c *Cache[K, V]) Get(key K) (value V, ok bool) { // Contains checks if a key is in the cache, without updating the recent-ness // or deleting it for being stale. -func (c *Cache[K, V]) Contains(key K) (ok bool) { +func (c *LRU[K, V]) Contains(key K) (ok bool) { _, ok = c.items[key] return ok } // Peek returns the key value (or undefined if not found) without updating // the "recently used"-ness of the key. -func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { +func (c *LRU[K, V]) Peek(key K) (value V, ok bool) { var ent *internal.Entry[K, V] if ent, ok = c.items[key]; ok { return ent.Value, true @@ -137,7 +137,7 @@ func (c *Cache[K, V]) Peek(key K) (value V, ok bool) { } // visited returns if the key is visited -func (c *Cache[K, V]) visited(key K) (present bool, visited bool) { +func (c *LRU[K, V]) visited(key K) (present bool, visited bool) { var ent *internal.Entry[K, V] if ent, present = c.items[key]; present { visited = ent.Visited @@ -148,7 +148,7 @@ func (c *Cache[K, V]) visited(key K) (present bool, visited bool) { // Remove removes the provided key from the cache, returning if the // key was contained. -func (c *Cache[K, V]) Remove(key K) (present bool) { +func (c *LRU[K, V]) Remove(key K) (present bool) { if ent, ok := c.items[key]; ok { c.removeElement(ent) return true @@ -157,7 +157,7 @@ func (c *Cache[K, V]) Remove(key K) (present bool) { } // RemoveOldest removes the oldest item from the cache. -func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { +func (c *LRU[K, V]) RemoveOldest() (key K, value V, ok bool) { if c.useSieve { return c.performSieveEviction() } @@ -170,7 +170,7 @@ func (c *Cache[K, V]) RemoveOldest() (key K, value V, ok bool) { } // GetOldest returns the oldest entry -func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { +func (c *LRU[K, V]) GetOldest() (key K, value V, ok bool) { if c.useSieve { c.getSieveCandidate() if c.hand != nil { @@ -187,7 +187,7 @@ func (c *Cache[K, V]) GetOldest() (key K, value V, ok bool) { } // Keys returns a slice of the keys in the cache, from oldest to newest. -func (c *Cache[K, V]) Keys() []K { +func (c *LRU[K, V]) Keys() []K { keys := make([]K, c.evictList.Length()) i := 0 for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { @@ -198,7 +198,7 @@ func (c *Cache[K, V]) Keys() []K { } // Values returns a slice of the values in the cache, from oldest to newest. -func (c *Cache[K, V]) Values() []V { +func (c *LRU[K, V]) Values() []V { values := make([]V, len(c.items)) i := 0 for ent := c.evictList.Back(); ent != nil; ent = ent.PrevEntry() { @@ -209,17 +209,17 @@ func (c *Cache[K, V]) Values() []V { } // Len returns the number of items in the cache. -func (c *Cache[K, V]) Len() int { +func (c *LRU[K, V]) Len() int { return c.evictList.Length() } // Cap returns the capacity of the cache -func (c *Cache[K, V]) Cap() int { +func (c *LRU[K, V]) Cap() int { return c.size } // Resize changes the cache size. -func (c *Cache[K, V]) Resize(size int) (evicted int) { +func (c *LRU[K, V]) Resize(size int) (evicted int) { diff := c.Len() - size if diff < 0 { diff = 0 @@ -237,7 +237,7 @@ func (c *Cache[K, V]) Resize(size int) (evicted int) { } // performSieveEviction - runs a eviction by running Sieve Algorithm and returns the evicted value. -func (c *Cache[K, V]) performSieveEviction() (key K, value V, ok bool) { +func (c *LRU[K, V]) performSieveEviction() (key K, value V, ok bool) { c.getSieveCandidate() if c.hand != nil { candidate := c.hand @@ -250,7 +250,7 @@ func (c *Cache[K, V]) performSieveEviction() (key K, value V, ok bool) { } // getSieveCandidate evicts an entry based on sieve algorithm. -func (c *Cache[K, V]) getSieveCandidate() { +func (c *LRU[K, V]) getSieveCandidate() { if c.Len() == 0 { return } @@ -269,7 +269,7 @@ func (c *Cache[K, V]) getSieveCandidate() { } // removeOldest removes the oldest item from the cache. -func (c *Cache[K, V]) removeOldest() { +func (c *LRU[K, V]) removeOldest() { if c.useSieve { c.performSieveEviction() return @@ -281,7 +281,7 @@ func (c *Cache[K, V]) removeOldest() { } // removeElement is used to remove a given list element from the cache -func (c *Cache[K, V]) removeElement(e *internal.Entry[K, V]) { +func (c *LRU[K, V]) removeElement(e *internal.Entry[K, V]) { c.evictList.Remove(e) delete(c.items, e.Key) if c.onEvict != nil { From ee2fa3dbe70c01ae66322dca2a0f36b5221f03e3 Mon Sep 17 00:00:00 2001 From: venkata krishnan Date: Sun, 21 Jan 2024 09:05:44 -0800 Subject: [PATCH 09/14] resetting the comment back --- doc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc.go b/doc.go index 8faacd2..24107ee 100644 --- a/doc.go +++ b/doc.go @@ -3,8 +3,8 @@ // Package lru provides three different LRU caches of varying sophistication. // -// Cache is a simple cache with LRU and SIEVE eviction. The LRU is based on the LRU -// implementation in groupcache: https://github.com/golang/groupcache/tree/master/lru +// Cache is a simple LRU cache. It is based on the LRU implementation in +// groupcache: https://github.com/golang/groupcache/tree/master/lru // // TwoQueueCache tracks frequently used and recently used entries separately. // This avoids a burst of accesses from taking out frequently used entries, at From 55d4c2f939238e72ffb74a32877b5229107e252c Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 23 Jan 2024 19:38:37 -0800 Subject: [PATCH 10/14] update --- lru.go | 16 +- lru_test.go | 5 +- sieve_test.go | 395 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 407 insertions(+), 9 deletions(-) create mode 100644 sieve_test.go diff --git a/lru.go b/lru.go index acb851f..5ca5447 100644 --- a/lru.go +++ b/lru.go @@ -36,10 +36,12 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) c = &Cache[K, V]{ onEvictedCB: onEvicted, } + if onEvicted != nil { c.initEvictBuffers() onEvicted = c.onEvicted } + c.lru, err = simplelru.NewLRU(size, onEvicted) return } @@ -51,7 +53,6 @@ func WithCallback[K comparable, V any](onEvicted func(key K, value V)) SieveOpti if onEvicted != nil { c.initEvictBuffers() - onEvicted = c.onEvicted } } } @@ -66,21 +67,22 @@ func WithSieve[K comparable, V any]() SieveOption[K, V] { // SieveOption is used to set options for the Sieve cache. type SieveOption[K comparable, V any] func(*Cache[K, V]) -// NewSieveWithOpts helps create a LRU cache with option with options. -func NewSieveWithOpts[K comparable, V any](size int, opts ...SieveOption[K, V]) (c *Cache[K, V], err error) { +// NewWithOpts helps create a LRU cache with option with options. +func NewWithOpts[K comparable, V any](size int, opts ...SieveOption[K, V]) (c *Cache[K, V], err error) { // create a cache with default settings - c = &Cache[K, V]{} + c = &Cache[K, V]{ + onEvictedCB: nil, + } for _, opt := range opts { opt(c) } if c.sieveOpt { - c.lru, err = simplelru.NewSieve(size, c.onEvictedCB) + c.lru, err = simplelru.NewSieve(size, c.onEvicted) } else { - c.lru, err = simplelru.NewLRU(size, c.onEvictedCB) + c.lru, err = simplelru.NewLRU(size, c.onEvicted) } - return c, err } diff --git a/lru_test.go b/lru_test.go index 882182b..5b9214b 100644 --- a/lru_test.go +++ b/lru_test.go @@ -43,7 +43,7 @@ func Benchmark_Rand(b *testing.B) { }) b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewSieveWithOpts[int64, int64](8192, WithSieve[int64, int64]()) + l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) if err != nil { b.Fatalf("err: %v", err) } @@ -89,7 +89,7 @@ func BenchmarkLRU_Freq(b *testing.B) { }) b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewSieveWithOpts[int64, int64](8192) + l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) if err != nil { b.Fatalf("err: %v", err) } @@ -106,6 +106,7 @@ func TestLRU(t *testing.T) { } evictCounter++ } + l, err := NewWithEvict(128, onEvicted) if err != nil { t.Fatalf("err: %v", err) diff --git a/sieve_test.go b/sieve_test.go new file mode 100644 index 0000000..b50b3d5 --- /dev/null +++ b/sieve_test.go @@ -0,0 +1,395 @@ +package lru + +import ( + "fmt" + "reflect" + "testing" +) + +func TestSieve(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + if k != v { + t.Fatalf("Evict values not equal (%v!=%v)", k, v) + } + evictCounter++ + } + + l, err := NewWithOpts[int, int](128, WithSieve[int, int](), WithCallback[int, int](onEvicted)) + if err != nil { + t.Fatalf("err: %v", err) + } + + for i := 0; i < 256; i++ { + l.Add(i, i) + } + if l.Len() != 128 { + t.Fatalf("bad len: %v", l.Len()) + } + if l.Cap() != 128 { + t.Fatalf("expect %d, but %d", 128, l.Cap()) + } + + if evictCounter != 128 { + t.Fatalf("bad evict count: %v", evictCounter) + } + + for i, k := range l.Keys() { + if v, ok := l.Get(k); !ok || v != k || v != i+128 { + t.Fatalf("bad key: %v", k) + } + } + for i, v := range l.Values() { + if v != i+128 { + t.Fatalf("bad value: %v", v) + } + } + for i := 0; i < 128; i++ { + if _, ok := l.Get(i); ok { + t.Fatalf("should be evicted") + } + } + for i := 128; i < 256; i++ { + if _, ok := l.Get(i); !ok { + t.Fatalf("should not be evicted") + } + } + for i := 128; i < 192; i++ { + l.Remove(i) + if _, ok := l.Get(i); ok { + t.Fatalf("should be deleted") + } + } + + l.GetOldest() + l.Get(192) // expect 192 to be last key in l.Keys() + l.RemoveOldest() // Remove oldest will set the 192 as visited, which would have not be removed. + + if _, ok := l.Get(192); !ok { + t.Fatalf("should not have been evcited") + } + + l.Purge() + if l.Len() != 0 { + t.Fatalf("bad len: %v", l.Len()) + } + + if _, ok := l.Get(200); ok { + t.Fatalf("should contain nothing") + } +} + +// test that Add returns true/false if an eviction occurred +func TestSieveAdd(t *testing.T) { + evictCounter := 0 + onEvicted := func(k int, v int) { + evictCounter++ + } + + l, err := NewWithOpts[int, int](1, WithSieve[int, int](), WithCallback[int, int](onEvicted)) + if err != nil { + t.Fatalf("err: %v", err) + } + + if l.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if l.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } +} + +// test that Contains doesn't update recent-ness +func TestSieveContains(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if !l.Contains(1) { + t.Errorf("1 should be contained") + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Contains should not have updated recent-ness of 1") + } +} + +// test that ContainsOrAdd doesn't update recent-ness +func TestSieveContainsOrAdd(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + contains, evict := l.ContainsOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } +} + +// test that PeekOrAdd doesn't update recent-ness +func TestSievePeekOrAdd(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + previous, contains, evict := l.PeekOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + if previous != 1 { + t.Errorf("previous is not equal to 1") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } +} + +// test that Peek doesn't update recent-ness +func TestSievePeek(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } +} + +// test that Resize can upsize and downsize +func TestSieveResize(t *testing.T) { + onEvictCounter := 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + + l, err := NewWithOpts[int, int](2, WithSieve[int, int](), WithCallback[int, int](onEvicted)) + if err != nil { + t.Fatalf("err: %v", err) + } + + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } +} + +func (c *Cache[K, V]) checkKeys(t *testing.T, want []K) { + t.Helper() + got := c.Keys() + + existingKeys := make(map[K]struct{}) + for _, k := range got { + existingKeys[k] = struct{}{} + } + + if len(want) != len(existingKeys) { + t.Errorf("Expected Size: %d Actual: %d", len(want), len(existingKeys)) + } + + for _, wnt := range want { + if _, ok := existingKeys[wnt]; !ok { + t.Errorf("Expected %s to be present. ", fmt.Sprint(wnt)) + } + } +} + +func TestSieve_EvictionSameKey(t *testing.T) { + t.Run("Add", func(t *testing.T) { + var evictedKeys []int + + cache, _ := NewWithOpts[int, struct{}](2, WithSieve[int, struct{}](), WithCallback[int, struct{}](func(key int, _ struct{}) { + evictedKeys = append(evictedKeys, key) + })) + + if evicted := cache.Add(1, struct{}{}); evicted { + t.Error("First 1: got unexpected eviction") + } + cache.checkKeys(t, []int{1}) + + if evicted := cache.Add(2, struct{}{}); evicted { + t.Error("2: got unexpected eviction") + } + cache.checkKeys(t, []int{1, 2}) + + if evicted := cache.Add(1, struct{}{}); evicted { + t.Error("Second 1: got unexpected eviction") + } + cache.checkKeys(t, []int{2, 1}) + + if evicted := cache.Add(3, struct{}{}); !evicted { + t.Error("3: did not get expected eviction") + } + cache.checkKeys(t, []int{1, 3}) + + want := []int{2} + if !reflect.DeepEqual(evictedKeys, want) { + t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) + } + }) + + t.Run("ContainsOrAdd", func(t *testing.T) { + var evictedKeys []int + + cache, _ := NewWithOpts[int, struct{}](2, WithSieve[int, struct{}](), WithCallback[int, struct{}](func(key int, _ struct{}) { + evictedKeys = append(evictedKeys, key) + })) + + contained, evicted := cache.ContainsOrAdd(1, struct{}{}) + if contained { + t.Error("First 1: got unexpected contained") + } + if evicted { + t.Error("First 1: got unexpected eviction") + } + cache.checkKeys(t, []int{1}) + + contained, evicted = cache.ContainsOrAdd(2, struct{}{}) + if contained { + t.Error("2: got unexpected contained") + } + if evicted { + t.Error("2: got unexpected eviction") + } + cache.checkKeys(t, []int{1, 2}) + + contained, evicted = cache.ContainsOrAdd(1, struct{}{}) + if !contained { + t.Error("Second 1: did not get expected contained") + } + if evicted { + t.Error("Second 1: got unexpected eviction") + } + cache.checkKeys(t, []int{1, 2}) + + contained, evicted = cache.ContainsOrAdd(3, struct{}{}) + if contained { + t.Error("3: got unexpected contained") + } + if !evicted { + t.Error("3: did not get expected eviction") + } + cache.checkKeys(t, []int{2, 3}) + + want := []int{1} + if !reflect.DeepEqual(evictedKeys, want) { + t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) + } + }) + + t.Run("PeekOrAdd", func(t *testing.T) { + var evictedKeys []int + + cache, _ := NewWithOpts[int, struct{}](2, WithSieve[int, struct{}](), WithCallback[int, struct{}](func(key int, _ struct{}) { + evictedKeys = append(evictedKeys, key) + })) + + _, contained, evicted := cache.PeekOrAdd(1, struct{}{}) + if contained { + t.Error("First 1: got unexpected contained") + } + if evicted { + t.Error("First 1: got unexpected eviction") + } + cache.checkKeys(t, []int{1}) + + _, contained, evicted = cache.PeekOrAdd(2, struct{}{}) + if contained { + t.Error("2: got unexpected contained") + } + if evicted { + t.Error("2: got unexpected eviction") + } + cache.checkKeys(t, []int{1, 2}) + + _, contained, evicted = cache.PeekOrAdd(1, struct{}{}) + if !contained { + t.Error("Second 1: did not get expected contained") + } + if evicted { + t.Error("Second 1: got unexpected eviction") + } + cache.checkKeys(t, []int{1, 2}) + + _, contained, evicted = cache.PeekOrAdd(3, struct{}{}) + if contained { + t.Error("3: got unexpected contained") + } + if !evicted { + t.Error("3: did not get expected eviction") + } + cache.checkKeys(t, []int{2, 3}) + + want := []int{1} + if !reflect.DeepEqual(evictedKeys, want) { + t.Errorf("evictedKeys got: %v want: %v", evictedKeys, want) + } + }) + +} From 93f6f59619693538f40915f51d61f80635539b40 Mon Sep 17 00:00:00 2001 From: Venkat Date: Tue, 23 Jan 2024 19:52:57 -0800 Subject: [PATCH 11/14] rename --- lru.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lru.go b/lru.go index 5ca5447..ea7ce35 100644 --- a/lru.go +++ b/lru.go @@ -47,7 +47,7 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) } // WithCallback returns a SieveOption with eviction callback. -func WithCallback[K comparable, V any](onEvicted func(key K, value V)) SieveOption[K, V] { +func WithCallback[K comparable, V any](onEvicted func(key K, value V)) Option[K, V] { return func(c *Cache[K, V]) { c.onEvictedCB = onEvicted @@ -58,17 +58,17 @@ func WithCallback[K comparable, V any](onEvicted func(key K, value V)) SieveOpti } // WithSieve returns a SieveOption that enables sieve -func WithSieve[K comparable, V any]() SieveOption[K, V] { +func WithSieve[K comparable, V any]() Option[K, V] { return func(c *Cache[K, V]) { c.sieveOpt = true } } -// SieveOption is used to set options for the Sieve cache. -type SieveOption[K comparable, V any] func(*Cache[K, V]) +// Option is used to set options for the cache. +type Option[K comparable, V any] func(*Cache[K, V]) // NewWithOpts helps create a LRU cache with option with options. -func NewWithOpts[K comparable, V any](size int, opts ...SieveOption[K, V]) (c *Cache[K, V], err error) { +func NewWithOpts[K comparable, V any](size int, opts ...Option[K, V]) (c *Cache[K, V], err error) { // create a cache with default settings c = &Cache[K, V]{ onEvictedCB: nil, From c5379196df271496594e591b138b81e507e54eca Mon Sep 17 00:00:00 2001 From: Venkat Date: Wed, 24 Jan 2024 19:57:56 -0800 Subject: [PATCH 12/14] moving tests: --- cache_test.go | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ lru_test.go | 231 ------------------------------------ sieve_test.go | 161 ------------------------- 3 files changed, 323 insertions(+), 392 deletions(-) create mode 100644 cache_test.go diff --git a/cache_test.go b/cache_test.go new file mode 100644 index 0000000..d39f09d --- /dev/null +++ b/cache_test.go @@ -0,0 +1,323 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lru + +import ( + "testing" +) + +func Benchmark_Rand(b *testing.B) { + var fn = func(b *testing.B, l *Cache[int64, int64]) { + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + trace[i] = getRand(b) % 32768 + } + + b.ResetTimer() + + var hit, miss int + for i := 0; i < 2*b.N; i++ { + if i%2 == 0 { + l.Add(trace[i], trace[i]) + } else { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + } + + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) + } + + b.Run("Benchmark with LRU ", func(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) + + b.Run("Benchmark with Sieve ", func(b *testing.B) { + l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) +} + +func BenchmarkLRU_Freq(b *testing.B) { + var fn = func(b *testing.B, l *Cache[int64, int64]) { + trace := make([]int64, b.N*2) + for i := 0; i < b.N*2; i++ { + if i%2 == 0 { + trace[i] = getRand(b) % 16384 + } else { + trace[i] = getRand(b) % 32768 + } + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + l.Add(trace[i], trace[i]) + } + var hit, miss int + for i := 0; i < b.N; i++ { + if _, ok := l.Get(trace[i]); ok { + hit++ + } else { + miss++ + } + } + b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) + } + + b.Run("Benchmark with LRU ", func(b *testing.B) { + l, err := New[int64, int64](8192) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) + + b.Run("Benchmark with Sieve ", func(b *testing.B) { + l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) + if err != nil { + b.Fatalf("err: %v", err) + } + + fn(b, l) + }) +} + +// test that Add returns true/false if an eviction occurred +func TestAdd(t *testing.T) { + var evictCounter = 0 + var add = func(t *testing.T, c *Cache[int, int]) { + if c.Add(1, 1) == true || evictCounter != 0 { + t.Errorf("should not have an eviction") + } + if c.Add(2, 2) == false || evictCounter != 1 { + t.Errorf("should have an eviction") + } + } + + onEvicted := func(k int, v int) { + evictCounter++ + } + + l, err := NewWithEvict(1, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + t.Run("LRU add", func(t1 *testing.T) { + evictCounter = 0 + add(t1, l) + }) + + l, err = NewWithOpts[int, int](1, WithSieve[int, int](), WithCallback[int, int](onEvicted)) + if err != nil { + t.Fatalf("err: %v", err) + } + + t.Run("Sieve add", func(t1 *testing.T) { + evictCounter = 0 + add(t1, l) + }) +} + +// test that ContainsOrAdd doesn't update recent-ness +func TestContainsOrAdd(t *testing.T) { + var containsOrAdd = func(t *testing.T, l *Cache[int, int]) { + l.Add(1, 1) + l.Add(2, 2) + contains, evict := l.ContainsOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } + } + + t.Run(" LRU ContainsOrAdd ", func(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + containsOrAdd(t, l) + }) + + t.Run(" Sieve ContainsOrAdd ", func(t *testing.T) { + l, err := NewWithOpts[int, int](2, WithSieve[int, int]()) + if err != nil { + t.Fatalf("err: %v", err) + } + + containsOrAdd(t, l) + }) +} + +// test that PeekOrAdd doesn't update recent-ness +func TestPeekOrAdd(t *testing.T) { + var peekOrAdd = func(t *testing.T, l *Cache[int, int]) { + l.Add(1, 1) + l.Add(2, 2) + previous, contains, evict := l.PeekOrAdd(1, 1) + if !contains { + t.Errorf("1 should be contained") + } + if evict { + t.Errorf("nothing should be evicted here") + } + if previous != 1 { + t.Errorf("previous is not equal to 1") + } + + l.Add(3, 3) + contains, evict = l.ContainsOrAdd(1, 1) + if contains { + t.Errorf("1 should not have been contained") + } + if !evict { + t.Errorf("an eviction should have occurred") + } + if !l.Contains(1) { + t.Errorf("now 1 should be contained") + } + } + + t.Run("LRU PeekOrAdd", func(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + peekOrAdd(t, l) + }) + + t.Run("Sieve PeekOrAdd", func(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + peekOrAdd(t, l) + }) +} + +// test that Peek doesn't update recent-ness +func TestPeek(t *testing.T) { + var peek = func(t *testing.T, l *Cache[int, int]) { + l.Add(1, 1) + l.Add(2, 2) + if v, ok := l.Peek(1); !ok || v != 1 { + t.Errorf("1 should be set to 1: %v, %v", v, ok) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("should not have updated recent-ness of 1") + } + } + + t.Run("LRU Peek", func(t *testing.T) { + l, err := New[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + peek(t, l) + }) + + t.Run("Sieve Peek", func(t *testing.T) { + l, err := NewWithOpts[int, int](2) + if err != nil { + t.Fatalf("err: %v", err) + } + + peek(t, l) + }) +} + +// test that Resize can upsize and downsize +func TestResize(t *testing.T) { + var onEvictCounter int + var resize = func(t *testing.T, l *Cache[int, int]) { + // Downsize + l.Add(1, 1) + l.Add(2, 2) + evicted := l.Resize(1) + if evicted != 1 { + t.Errorf("1 element should have been evicted: %v", evicted) + } + if onEvictCounter != 1 { + t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) + } + + l.Add(3, 3) + if l.Contains(1) { + t.Errorf("Element 1 should have been evicted") + } + + // Upsize + evicted = l.Resize(2) + if evicted != 0 { + t.Errorf("0 elements should have been evicted: %v", evicted) + } + + l.Add(4, 4) + if !l.Contains(3) || !l.Contains(4) { + t.Errorf("Cache should have contained 2 elements") + } + + } + + t.Run("LRU resize", func(t *testing.T) { + onEvictCounter = 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + l, err := NewWithEvict(2, onEvicted) + if err != nil { + t.Fatalf("err: %v", err) + } + + resize(t, l) + }) + + t.Run("Sieve resize", func(t *testing.T) { + onEvictCounter = 0 + onEvicted := func(k int, v int) { + onEvictCounter++ + } + + l, err := NewWithOpts[int, int](2, WithSieve[int, int](), WithCallback[int, int](onEvicted)) + if err != nil { + t.Fatalf("err: %v", err) + } + + resize(t, l) + }) +} diff --git a/lru_test.go b/lru_test.go index 5b9214b..2455ad3 100644 --- a/lru_test.go +++ b/lru_test.go @@ -8,96 +8,6 @@ import ( "testing" ) -func Benchmark_Rand(b *testing.B) { - var fn = func(b *testing.B, l *Cache[int64, int64]) { - trace := make([]int64, b.N*2) - for i := 0; i < b.N*2; i++ { - trace[i] = getRand(b) % 32768 - } - - b.ResetTimer() - - var hit, miss int - for i := 0; i < 2*b.N; i++ { - if i%2 == 0 { - l.Add(trace[i], trace[i]) - } else { - if _, ok := l.Get(trace[i]); ok { - hit++ - } else { - miss++ - } - } - } - - b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) - } - - b.Run("Benchmark with LRU ", func(b *testing.B) { - l, err := New[int64, int64](8192) - if err != nil { - b.Fatalf("err: %v", err) - } - - fn(b, l) - }) - - b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) - if err != nil { - b.Fatalf("err: %v", err) - } - - fn(b, l) - }) -} - -func BenchmarkLRU_Freq(b *testing.B) { - var fn = func(b *testing.B, l *Cache[int64, int64]) { - trace := make([]int64, b.N*2) - for i := 0; i < b.N*2; i++ { - if i%2 == 0 { - trace[i] = getRand(b) % 16384 - } else { - trace[i] = getRand(b) % 32768 - } - } - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - l.Add(trace[i], trace[i]) - } - var hit, miss int - for i := 0; i < b.N; i++ { - if _, ok := l.Get(trace[i]); ok { - hit++ - } else { - miss++ - } - } - b.Logf("hit: %d miss: %d ratio: %f", hit, miss, float64(hit)/float64(hit+miss)) - } - - b.Run("Benchmark with LRU ", func(b *testing.B) { - l, err := New[int64, int64](8192) - if err != nil { - b.Fatalf("err: %v", err) - } - - fn(b, l) - }) - - b.Run("Benchmark with Sieve ", func(b *testing.B) { - l, err := NewWithOpts[int64, int64](8192, WithSieve[int64, int64]()) - if err != nil { - b.Fatalf("err: %v", err) - } - - fn(b, l) - }) -} - func TestLRU(t *testing.T) { evictCounter := 0 onEvicted := func(k int, v int) { @@ -170,26 +80,6 @@ func TestLRU(t *testing.T) { } } -// test that Add returns true/false if an eviction occurred -func TestLRUAdd(t *testing.T) { - evictCounter := 0 - onEvicted := func(k int, v int) { - evictCounter++ - } - - l, err := NewWithEvict(1, onEvicted) - if err != nil { - t.Fatalf("err: %v", err) - } - - if l.Add(1, 1) == true || evictCounter != 0 { - t.Errorf("should not have an eviction") - } - if l.Add(2, 2) == false || evictCounter != 1 { - t.Errorf("should have an eviction") - } -} - // test that Contains doesn't update recent-ness func TestLRUContains(t *testing.T) { l, err := New[int, int](2) @@ -209,127 +99,6 @@ func TestLRUContains(t *testing.T) { } } -// test that ContainsOrAdd doesn't update recent-ness -func TestLRUContainsOrAdd(t *testing.T) { - l, err := New[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - contains, evict := l.ContainsOrAdd(1, 1) - if !contains { - t.Errorf("1 should be contained") - } - if evict { - t.Errorf("nothing should be evicted here") - } - - l.Add(3, 3) - contains, evict = l.ContainsOrAdd(1, 1) - if contains { - t.Errorf("1 should not have been contained") - } - if !evict { - t.Errorf("an eviction should have occurred") - } - if !l.Contains(1) { - t.Errorf("now 1 should be contained") - } -} - -// test that PeekOrAdd doesn't update recent-ness -func TestLRUPeekOrAdd(t *testing.T) { - l, err := New[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - previous, contains, evict := l.PeekOrAdd(1, 1) - if !contains { - t.Errorf("1 should be contained") - } - if evict { - t.Errorf("nothing should be evicted here") - } - if previous != 1 { - t.Errorf("previous is not equal to 1") - } - - l.Add(3, 3) - contains, evict = l.ContainsOrAdd(1, 1) - if contains { - t.Errorf("1 should not have been contained") - } - if !evict { - t.Errorf("an eviction should have occurred") - } - if !l.Contains(1) { - t.Errorf("now 1 should be contained") - } -} - -// test that Peek doesn't update recent-ness -func TestLRUPeek(t *testing.T) { - l, err := New[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - if v, ok := l.Peek(1); !ok || v != 1 { - t.Errorf("1 should be set to 1: %v, %v", v, ok) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("should not have updated recent-ness of 1") - } -} - -// test that Resize can upsize and downsize -func TestLRUResize(t *testing.T) { - onEvictCounter := 0 - onEvicted := func(k int, v int) { - onEvictCounter++ - } - l, err := NewWithEvict(2, onEvicted) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Downsize - l.Add(1, 1) - l.Add(2, 2) - evicted := l.Resize(1) - if evicted != 1 { - t.Errorf("1 element should have been evicted: %v", evicted) - } - if onEvictCounter != 1 { - t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("Element 1 should have been evicted") - } - - // Upsize - evicted = l.Resize(2) - if evicted != 0 { - t.Errorf("0 elements should have been evicted: %v", evicted) - } - - l.Add(4, 4) - if !l.Contains(3) || !l.Contains(4) { - t.Errorf("Cache should have contained 2 elements") - } -} - func (c *Cache[K, V]) wantKeys(t *testing.T, want []K) { t.Helper() got := c.Keys() diff --git a/sieve_test.go b/sieve_test.go index b50b3d5..5b391f4 100644 --- a/sieve_test.go +++ b/sieve_test.go @@ -79,167 +79,6 @@ func TestSieve(t *testing.T) { } } -// test that Add returns true/false if an eviction occurred -func TestSieveAdd(t *testing.T) { - evictCounter := 0 - onEvicted := func(k int, v int) { - evictCounter++ - } - - l, err := NewWithOpts[int, int](1, WithSieve[int, int](), WithCallback[int, int](onEvicted)) - if err != nil { - t.Fatalf("err: %v", err) - } - - if l.Add(1, 1) == true || evictCounter != 0 { - t.Errorf("should not have an eviction") - } - if l.Add(2, 2) == false || evictCounter != 1 { - t.Errorf("should have an eviction") - } -} - -// test that Contains doesn't update recent-ness -func TestSieveContains(t *testing.T) { - l, err := NewWithOpts[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - if !l.Contains(1) { - t.Errorf("1 should be contained") - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("Contains should not have updated recent-ness of 1") - } -} - -// test that ContainsOrAdd doesn't update recent-ness -func TestSieveContainsOrAdd(t *testing.T) { - l, err := NewWithOpts[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - contains, evict := l.ContainsOrAdd(1, 1) - if !contains { - t.Errorf("1 should be contained") - } - if evict { - t.Errorf("nothing should be evicted here") - } - - l.Add(3, 3) - contains, evict = l.ContainsOrAdd(1, 1) - if contains { - t.Errorf("1 should not have been contained") - } - if !evict { - t.Errorf("an eviction should have occurred") - } - if !l.Contains(1) { - t.Errorf("now 1 should be contained") - } -} - -// test that PeekOrAdd doesn't update recent-ness -func TestSievePeekOrAdd(t *testing.T) { - l, err := NewWithOpts[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - previous, contains, evict := l.PeekOrAdd(1, 1) - if !contains { - t.Errorf("1 should be contained") - } - if evict { - t.Errorf("nothing should be evicted here") - } - if previous != 1 { - t.Errorf("previous is not equal to 1") - } - - l.Add(3, 3) - contains, evict = l.ContainsOrAdd(1, 1) - if contains { - t.Errorf("1 should not have been contained") - } - if !evict { - t.Errorf("an eviction should have occurred") - } - if !l.Contains(1) { - t.Errorf("now 1 should be contained") - } -} - -// test that Peek doesn't update recent-ness -func TestSievePeek(t *testing.T) { - l, err := NewWithOpts[int, int](2) - if err != nil { - t.Fatalf("err: %v", err) - } - - l.Add(1, 1) - l.Add(2, 2) - if v, ok := l.Peek(1); !ok || v != 1 { - t.Errorf("1 should be set to 1: %v, %v", v, ok) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("should not have updated recent-ness of 1") - } -} - -// test that Resize can upsize and downsize -func TestSieveResize(t *testing.T) { - onEvictCounter := 0 - onEvicted := func(k int, v int) { - onEvictCounter++ - } - - l, err := NewWithOpts[int, int](2, WithSieve[int, int](), WithCallback[int, int](onEvicted)) - if err != nil { - t.Fatalf("err: %v", err) - } - - // Downsize - l.Add(1, 1) - l.Add(2, 2) - evicted := l.Resize(1) - if evicted != 1 { - t.Errorf("1 element should have been evicted: %v", evicted) - } - if onEvictCounter != 1 { - t.Errorf("onEvicted should have been called 1 time: %v", onEvictCounter) - } - - l.Add(3, 3) - if l.Contains(1) { - t.Errorf("Element 1 should have been evicted") - } - - // Upsize - evicted = l.Resize(2) - if evicted != 0 { - t.Errorf("0 elements should have been evicted: %v", evicted) - } - - l.Add(4, 4) - if !l.Contains(3) || !l.Contains(4) { - t.Errorf("Cache should have contained 2 elements") - } -} - func (c *Cache[K, V]) checkKeys(t *testing.T, want []K) { t.Helper() got := c.Keys() From 08e3a205ed127d316a7e4292c6f951c740c48863 Mon Sep 17 00:00:00 2001 From: Venkat Date: Wed, 24 Jan 2024 20:01:11 -0800 Subject: [PATCH 13/14] typo --- lru.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lru.go b/lru.go index ea7ce35..a967a93 100644 --- a/lru.go +++ b/lru.go @@ -46,7 +46,7 @@ func NewWithEvict[K comparable, V any](size int, onEvicted func(key K, value V)) return } -// WithCallback returns a SieveOption with eviction callback. +// WithCallback returns a Option with eviction callback. func WithCallback[K comparable, V any](onEvicted func(key K, value V)) Option[K, V] { return func(c *Cache[K, V]) { c.onEvictedCB = onEvicted @@ -57,7 +57,7 @@ func WithCallback[K comparable, V any](onEvicted func(key K, value V)) Option[K, } } -// WithSieve returns a SieveOption that enables sieve +// WithSieve returns a Option that enables sieve func WithSieve[K comparable, V any]() Option[K, V] { return func(c *Cache[K, V]) { c.sieveOpt = true From 546f3e3bf8cbccba380ea74d69e6b62d2a3087ad Mon Sep 17 00:00:00 2001 From: venkata krishnan Date: Sun, 21 Jan 2024 09:12:11 -0800 Subject: [PATCH 14/14] update go mod --- go.mod | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index f09ae2b..3605003 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,3 @@ -module github.com/venkatsvpr/golang-lru/v2 +module github.com/hashicorp/golang-lru/v2 -go 1.18 - -replace github.com/hashicorp/golang-lru/v2 => ./ - -replace github.com/hashicorp/golang-lru/v2/internal => ./internal - -require github.com/hashicorp/golang-lru/v2 v2.0.0-00010101000000-000000000000 +go 1.18 \ No newline at end of file