github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/cache/cache.go (about) 1 // Copyright 2017 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package cache 6 7 import ( 8 "math/rand" 9 "sync" 10 11 "github.com/golang/groupcache/lru" 12 ) 13 14 // Cache defines an interface for a cache that stores Measurable content. 15 // Eviction only happens when Add() is called, and there's no background 16 // goroutine for eviction. 17 type Cache interface { 18 // Get tries to find and return data assiciated with key. 19 Get(key Measurable) (data Measurable, ok bool) 20 // Add adds or replaces data into the cache, associating it with key. 21 // Entries are evicted when necessary. 22 Add(key Measurable, data Measurable) 23 } 24 25 type randomEvictedCache struct { 26 maxBytes int 27 28 mu sync.RWMutex 29 cachedBytes int 30 data map[Measurable]memoizedMeasurable 31 keys []memoizedMeasurable 32 } 33 34 // NewRandomEvictedCache returns a Cache that uses random eviction strategy. 35 // The cache will have a capacity of maxBytes bytes. A zero-byte capacity cache 36 // is valid. 37 // 38 // Internally we store a memoizing wrapper for the raw Measurable to avoid 39 // unnecessarily frequent size calculations. 40 // 41 // Note that memoizing size means once the entry is in the cache, we never 42 // bother recalculating their size. It's fine if the size changes, but the 43 // cache eviction will continue using the old size. 44 func NewRandomEvictedCache(maxBytes int) Cache { 45 return &randomEvictedCache{ 46 maxBytes: maxBytes, 47 data: make(map[Measurable]memoizedMeasurable), 48 } 49 } 50 51 func (c *randomEvictedCache) entrySize(key Measurable, value Measurable) int { 52 // Key size needs to be counted twice since they take space in both c.data 53 // and c.keys. Note that we are ignoring the map overhead from c.data here. 54 return 2*key.Size() + value.Size() 55 } 56 57 func (c *randomEvictedCache) evictOneLocked() { 58 i := int(rand.Int63()) % len(c.keys) 59 last := len(c.keys) - 1 60 var toRemove memoizedMeasurable 61 toRemove, c.keys[i] = c.keys[i], c.keys[last] 62 c.cachedBytes -= c.entrySize(toRemove, c.data[toRemove.m]) 63 delete(c.data, toRemove.m) 64 c.keys = c.keys[:last] 65 } 66 67 // Get impelments the Cache interface. 68 func (c *randomEvictedCache) Get(key Measurable) (data Measurable, ok bool) { 69 c.mu.RLock() 70 defer c.mu.RUnlock() 71 memoized, ok := c.data[key] 72 if !ok { 73 return nil, false 74 } 75 return memoized.m, ok 76 } 77 78 // Add implements the Cache interface. 79 func (c *randomEvictedCache) Add(key Measurable, data Measurable) { 80 memoizedKey := memoizedMeasurable{m: key} 81 memoizedData := memoizedMeasurable{m: data} 82 increase := c.entrySize(memoizedKey, memoizedData) 83 if increase > c.maxBytes { 84 return 85 } 86 c.mu.Lock() 87 defer c.mu.Unlock() 88 if v, ok := c.data[key]; ok { 89 decrease := c.entrySize(memoizedKey, v) 90 c.cachedBytes -= decrease 91 } 92 c.cachedBytes += increase 93 for c.cachedBytes > c.maxBytes { 94 c.evictOneLocked() 95 } 96 c.data[key] = memoizedData 97 c.keys = append(c.keys, memoizedKey) 98 } 99 100 // lruEvictedCache is a thin layer wrapped around 101 // github.com/golang/groupcache/lru.Cache that 1) makes it goroutine-safe; 2) 102 // caps on bytes; and 2) returns Measurable instead of interface{} 103 type lruEvictedCache struct { 104 maxBytes int 105 106 mu sync.Mutex 107 cachedBytes int 108 data *lru.Cache // not goroutine-safe; protected by mu 109 } 110 111 // NewLRUEvictedCache returns a Cache that uses LRU eviction strategy. 112 // The cache will have a capacity of maxBytes bytes. A zero-byte capacity cache 113 // is valid. 114 // 115 // Internally we store a memoizing wrapper for the raw Measurable to avoid 116 // unnecessarily frequent size calculations. 117 // 118 // Note that this means once the entry is in the cache, we never bother 119 // recalculating their size. It's fine if the size changes, but the cache 120 // eviction will continue using the old size. 121 func NewLRUEvictedCache(maxBytes int) Cache { 122 c := &lruEvictedCache{ 123 maxBytes: maxBytes, 124 } 125 c.data = &lru.Cache{ 126 OnEvicted: func(key lru.Key, value interface{}) { 127 // No locking is needed in this function because we do them in 128 // public methods Get/Add, and RemoveOldest() is only called in the 129 // Add method. 130 if memoized, ok := value.(memoizedMeasurable); ok { 131 if k, ok := key.(Measurable); ok { 132 c.cachedBytes -= k.Size() + memoized.Size() 133 } 134 } 135 }, 136 } 137 return c 138 } 139 140 // Get impelments the Cache interface. 141 func (c *lruEvictedCache) Get(key Measurable) (data Measurable, ok bool) { 142 c.mu.Lock() 143 defer c.mu.Unlock() 144 d, ok := c.data.Get(lru.Key(key)) 145 if !ok { 146 return nil, false 147 } 148 memoized, ok := d.(memoizedMeasurable) 149 if !ok { 150 return nil, false 151 } 152 return memoized.m, ok 153 } 154 155 // Add implements the Cache interface. 156 func (c *lruEvictedCache) Add(key Measurable, data Measurable) { 157 memoized := memoizedMeasurable{m: data} 158 keySize := key.Size() 159 if keySize+memoized.Size() > c.maxBytes { 160 return 161 } 162 c.mu.Lock() 163 defer c.mu.Unlock() 164 if v, ok := c.data.Get(lru.Key(key)); ok { 165 if m, ok := v.(memoizedMeasurable); ok { 166 c.cachedBytes -= keySize + m.Size() 167 } 168 } 169 c.cachedBytes += keySize + memoized.Size() 170 for c.cachedBytes > c.maxBytes { 171 c.data.RemoveOldest() 172 } 173 c.data.Add(lru.Key(key), memoized) 174 }