Skip to content

Commit

Permalink
[Chore] Use statistics collectors in the cache
Browse files Browse the repository at this point in the history
  • Loading branch information
maypok86 committed Aug 28, 2024
1 parent 186ff41 commit 44e3877
Show file tree
Hide file tree
Showing 15 changed files with 138 additions and 773 deletions.
15 changes: 10 additions & 5 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
type Builder[K comparable, V any] struct {
capacity *int
initialCapacity *int
statsEnabled bool
statsCollector StatsCollector
ttl *time.Duration
withVariableTTL bool
weigher func(key K, value V) uint32
Expand All @@ -38,14 +38,16 @@ func NewBuilder[K comparable, V any](capacity int) *Builder[K, V] {
weigher: func(key K, value V) uint32 {
return 1
},
statsCollector: noopStatsCollector{},
}
}

// CollectStats determines whether statistics should be calculated when the cache is running.
// CollectStats enables the accumulation of statistics during the operation of the cache.
//
// By default, statistics calculating is disabled.
func (b *Builder[K, V]) CollectStats() *Builder[K, V] {
b.statsEnabled = true
// NOTE: collecting statistics requires bookkeeping to be performed with each operation,
// and thus imposes a performance penalty on cache operations.
func (b *Builder[K, V]) CollectStats(statsCollector StatsCollector) *Builder[K, V] {
b.statsCollector = statsCollector
return b
}

Expand Down Expand Up @@ -100,6 +102,9 @@ func (b *Builder[K, V]) validate() error {
if b.weigher == nil {
return errors.New("otter: weigher should not be nil")
}
if b.statsCollector == nil {
return errors.New("otter: stats collector should not be nil")
}
if b.ttl != nil && *b.ttl <= 0 {
return errors.New("otter: ttl should be positive")
}
Expand Down
10 changes: 9 additions & 1 deletion builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package otter
import (
"testing"
"time"

"github.com/maypok86/otter/v2/stats"
)

func TestBuilder_NewFailed(t *testing.T) {
Expand Down Expand Up @@ -53,11 +55,17 @@ func TestBuilder_NewFailed(t *testing.T) {
if err == nil {
t.Fatalf("should fail with an error")
}

// nil stats collector
_, err = NewBuilder[int, int](capacity).CollectStats(nil).Build()
if err == nil {
t.Fatalf("should fail with an error")
}
}

func TestBuilder_BuildSuccess(t *testing.T) {
_, err := NewBuilder[int, int](10).
CollectStats().
CollectStats(stats.NewCounter()).
InitialCapacity(10).
Weigher(func(key int, value int) uint32 {
return 2
Expand Down
39 changes: 8 additions & 31 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"github.com/maypok86/otter/v2/internal/lossy"
"github.com/maypok86/otter/v2/internal/queue"
"github.com/maypok86/otter/v2/internal/s3fifo"
"github.com/maypok86/otter/v2/internal/stats"
"github.com/maypok86/otter/v2/internal/unixtime"
"github.com/maypok86/otter/v2/internal/xmath"
"github.com/maypok86/otter/v2/internal/xruntime"
Expand Down Expand Up @@ -83,18 +82,6 @@ func getExpiration(ttl time.Duration) uint32 {
return unixtime.Now() + getTTL(ttl)
}

// config is a set of cache settings.
type config[K comparable, V any] struct {
capacity int
initialCapacity *int
statsEnabled bool
ttl *time.Duration
withVariableTTL bool
weigher func(key K, value V) uint32
withWeight bool
deletionListener func(key K, value V, cause DeletionCause)
}

type expiryPolicy[K comparable, V any] interface {
Add(n node.Node[K, V])
Delete(n node.Node[K, V])
Expand All @@ -109,7 +96,7 @@ type Cache[K comparable, V any] struct {
hashmap *hashtable.Map[K, V]
policy *s3fifo.Policy[K, V]
expiryPolicy expiryPolicy[K, V]
stats *stats.Stats
stats statsCollector
stripedBuffer []*lossy.Buffer[K, V]
writeBuffer *queue.Growable[task[K, V]]
evictionMutex sync.Mutex
Expand Down Expand Up @@ -146,6 +133,7 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] {
cache := &Cache[K, V]{
nodeManager: nodeManager,
hashmap: hashmap,
stats: newStatsCollector(b.statsCollector),
stripedBuffer: stripedBuffer,
writeBuffer: queue.NewGrowable[task[K, V]](minWriteBufferSize, maxWriteBufferSize),
doneClear: make(chan struct{}),
Expand All @@ -167,9 +155,6 @@ func newCache[K comparable, V any](b *Builder[K, V]) *Cache[K, V] {
cache.expiryPolicy = expiry.NewDisabled[K, V]()
}

if b.statsEnabled {
cache.stats = stats.New()
}
if b.ttl != nil {
cache.ttl = getTTL(*b.ttl)
}
Expand Down Expand Up @@ -210,7 +195,7 @@ func (c *Cache[K, V]) Get(key K) (V, bool) {
func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) {
n, ok := c.hashmap.Get(key)
if !ok || !n.IsAlive() {
c.stats.IncMisses()
c.stats.CollectMisses(1)
return nil, false
}

Expand All @@ -221,12 +206,12 @@ func (c *Cache[K, V]) GetNode(key K) (node.Node[K, V], bool) {
n.Die()
c.writeBuffer.Push(newExpiredTask(n))
}
c.stats.IncMisses()
c.stats.CollectMisses(1)
return nil, false
}

c.afterGet(n)
c.stats.IncHits()
c.stats.CollectHits(1)

return n, true
}
Expand Down Expand Up @@ -300,7 +285,7 @@ func (c *Cache[K, V]) SetIfAbsentWithTTL(key K, value V, ttl time.Duration) bool
func (c *Cache[K, V]) set(key K, value V, expiration uint32, onlyIfAbsent bool) bool {
weight := c.weigher(key, value)
if int(weight) > c.policy.MaxAvailableWeight() {
c.stats.IncRejectedSets()
c.stats.CollectRejectedSets(1)
return false
}

Expand All @@ -312,7 +297,6 @@ func (c *Cache[K, V]) set(key K, value V, expiration uint32, onlyIfAbsent bool)
c.writeBuffer.Push(newAddTask(n))
return true
}
c.stats.IncRejectedSets()
return false
}

Expand Down Expand Up @@ -374,6 +358,7 @@ func (c *Cache[K, V]) deleteExpiredNode(n node.Node[K, V]) {
if deleted != nil {
n.Die()
c.notifyDeletion(n.Key(), n.Value(), Expired)
c.stats.CollectEviction(n.Weight())
}
}

Expand All @@ -398,8 +383,7 @@ func (c *Cache[K, V]) evictNode(n node.Node[K, V]) {
if deleted != nil {
n.Die()
c.notifyDeletion(n.Key(), n.Value(), Size)
c.stats.IncEvictedCount()
c.stats.AddEvictedWeight(n.Weight())
c.stats.CollectEviction(n.Weight())
}
}

Expand Down Expand Up @@ -486,8 +470,6 @@ func (c *Cache[K, V]) clear(t task[K, V]) {

c.writeBuffer.Push(t)
<-c.doneClear

c.stats.Clear()
}

// Close clears the hash table, all policies, buffers, etc and stop all goroutines.
Expand All @@ -512,11 +494,6 @@ func (c *Cache[K, V]) Capacity() int {
return c.capacity
}

// Stats returns a current snapshot of this cache's cumulative statistics.
func (c *Cache[K, V]) Stats() Stats {
return newStats(c.stats)
}

// Extension returns access to inspect and perform low-level operations on this cache based on its runtime
// characteristics. These operations are optional and dependent on how the cache was constructed
// and what abilities the implementation exposes.
Expand Down
27 changes: 16 additions & 11 deletions cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

"github.com/maypok86/otter/v2/internal/generated/node"
"github.com/maypok86/otter/v2/internal/xruntime"
"github.com/maypok86/otter/v2/stats"
)

func getRandomSize(t *testing.T) int {
Expand Down Expand Up @@ -162,9 +163,10 @@ func TestCache_Set(t *testing.T) {
size := getRandomSize(t)
var mutex sync.Mutex
m := make(map[DeletionCause]int)
statsCounter := stats.NewCounter()
c, err := NewBuilder[int, int](size).
WithTTL(time.Minute).
CollectStats().
CollectStats(statsCounter).
DeletionListener(func(key int, value int, cause DeletionCause) {
mutex.Lock()
m[cause]++
Expand Down Expand Up @@ -211,7 +213,7 @@ func TestCache_Set(t *testing.T) {
if err != nil {
t.Fatalf("not found key: %v", err)
}
ratio := c.Stats().Ratio()
ratio := statsCounter.Snapshot().HitRatio()
if ratio != 1.0 {
t.Fatalf("cache hit ratio should be 1.0, but got %v", ratio)
}
Expand All @@ -225,7 +227,8 @@ func TestCache_Set(t *testing.T) {

func TestCache_SetIfAbsent(t *testing.T) {
size := getRandomSize(t)
c, err := NewBuilder[int, int](size).WithTTL(time.Minute).CollectStats().Build()
statsCounter := stats.NewCounter()
c, err := NewBuilder[int, int](size).WithTTL(time.Hour).CollectStats(statsCounter).Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}
Expand All @@ -250,7 +253,7 @@ func TestCache_SetIfAbsent(t *testing.T) {

c.Clear()

cc, err := NewBuilder[int, int](size).WithVariableTTL().CollectStats().Build()
cc, err := NewBuilder[int, int](size).WithVariableTTL().CollectStats(statsCounter).Build()
if err != nil {
t.Fatalf("can not create cache: %v", err)
}
Expand All @@ -273,8 +276,8 @@ func TestCache_SetIfAbsent(t *testing.T) {
}
}

if hits := cc.Stats().Hits(); hits != int64(size) {
t.Fatalf("hit ratio should be 100%%. Hits: %d", hits)
if hitRatio := statsCounter.Snapshot().HitRatio(); hitRatio != 1.0 {
t.Fatalf("hit rate should be 100%%. Hite rate: %.2f", hitRatio*100)
}

cc.Close()
Expand Down Expand Up @@ -322,9 +325,10 @@ func TestCache_SetWithTTL(t *testing.T) {
mutex.Unlock()

m = make(map[DeletionCause]int)
statsCounter := stats.NewCounter()
cc, err := NewBuilder[int, int](size).
WithVariableTTL().
CollectStats().
CollectStats(statsCounter).
DeletionListener(func(key int, value int, cause DeletionCause) {
mutex.Lock()
m[cause]++
Expand Down Expand Up @@ -352,7 +356,7 @@ func TestCache_SetWithTTL(t *testing.T) {
if cacheSize := cc.Size(); cacheSize != 0 {
t.Fatalf("c.Size() = %d, want = %d", cacheSize, 0)
}
if misses := cc.Stats().Misses(); misses != int64(size) {
if misses := statsCounter.Snapshot().Misses(); misses != uint64(size) {
t.Fatalf("c.Stats().Misses() = %d, want = %d", misses, size)
}
mutex.Lock()
Expand Down Expand Up @@ -507,8 +511,9 @@ func TestCache_Advanced(t *testing.T) {
func TestCache_Ratio(t *testing.T) {
var mutex sync.Mutex
m := make(map[DeletionCause]int)
statsCounter := stats.NewCounter()
c, err := NewBuilder[uint64, uint64](100).
CollectStats().
CollectStats(statsCounter).
DeletionListener(func(key uint64, value uint64, cause DeletionCause) {
mutex.Lock()
m[cause]++
Expand All @@ -532,7 +537,7 @@ func TestCache_Ratio(t *testing.T) {
}

t.Logf("actual size: %d, capacity: %d", c.Size(), c.Capacity())
t.Logf("actual: %.2f, optimal: %.2f", c.Stats().Ratio(), o.Ratio())
t.Logf("actual: %.2f, optimal: %.2f", statsCounter.Snapshot().HitRatio(), o.Ratio())

mutex.Lock()
defer mutex.Unlock()
Expand Down Expand Up @@ -611,7 +616,7 @@ func (h *optimalHeap) Pop() any {

func Test_GetExpired(t *testing.T) {
c, err := NewBuilder[string, string](1000000).
CollectStats().
CollectStats(stats.NewCounter()).
DeletionListener(func(key string, value string, cause DeletionCause) {
fmt.Println(cause)
if cause != Expired {
Expand Down
Loading

0 comments on commit 44e3877

Please sign in to comment.