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  }