cache.go raw

   1  /*
   2   * SPDX-FileCopyrightText: © Hypermode Inc. <hello@hypermode.com>
   3   * SPDX-License-Identifier: Apache-2.0
   4   */
   5  
   6  // Ristretto is a fast, fixed size, in-memory cache with a dual focus on
   7  // throughput and hit ratio performance. You can easily add Ristretto to an
   8  // existing system and keep the most valuable data where you need it.
   9  package ristretto
  10  
  11  import (
  12  	"bytes"
  13  	"errors"
  14  	"fmt"
  15  	"sync"
  16  	"sync/atomic"
  17  	"time"
  18  	"unsafe"
  19  
  20  	"github.com/dgraph-io/ristretto/v2/z"
  21  )
  22  
  23  var (
  24  	// TODO: find the optimal value for this or make it configurable
  25  	setBufSize = 32 * 1024
  26  )
  27  
  28  const itemSize = int64(unsafe.Sizeof(storeItem[any]{}))
  29  
  30  func zeroValue[T any]() T {
  31  	var zero T
  32  	return zero
  33  }
  34  
  35  // Key is the generic type to represent the keys type in key-value pair of the cache.
  36  type Key = z.Key
  37  
  38  // Cache is a thread-safe implementation of a hashmap with a TinyLFU admission
  39  // policy and a Sampled LFU eviction policy. You can use the same Cache instance
  40  // from as many goroutines as you want.
  41  type Cache[K Key, V any] struct {
  42  	// storedItems is the central concurrent hashmap where key-value items are stored.
  43  	storedItems store[V]
  44  	// cachePolicy determines what gets let in to the cache and what gets kicked out.
  45  	cachePolicy *defaultPolicy[V]
  46  	// getBuf is a custom ring buffer implementation that gets pushed to when
  47  	// keys are read.
  48  	getBuf *ringBuffer
  49  	// setBuf is a buffer allowing us to batch/drop Sets during times of high
  50  	// contention.
  51  	setBuf chan *Item[V]
  52  	// onEvict is called for item evictions.
  53  	onEvict func(*Item[V])
  54  	// onReject is called when an item is rejected via admission policy.
  55  	onReject func(*Item[V])
  56  	// onExit is called whenever a value goes out of scope from the cache.
  57  	onExit (func(V))
  58  	// KeyToHash function is used to customize the key hashing algorithm.
  59  	// Each key will be hashed using the provided function. If keyToHash value
  60  	// is not set, the default keyToHash function is used.
  61  	keyToHash func(K) (uint64, uint64)
  62  	// stop is used to stop the processItems goroutine.
  63  	stop chan struct{}
  64  	done chan struct{}
  65  	// indicates whether cache is closed.
  66  	isClosed atomic.Bool
  67  	// cost calculates cost from a value.
  68  	cost func(value V) int64
  69  	// ignoreInternalCost dictates whether to ignore the cost of internally storing
  70  	// the item in the cost calculation.
  71  	ignoreInternalCost bool
  72  	// cleanupTicker is used to periodically check for entries whose TTL has passed.
  73  	cleanupTicker *time.Ticker
  74  	// Metrics contains a running log of important statistics like hits, misses,
  75  	// and dropped items.
  76  	Metrics *Metrics
  77  }
  78  
  79  // Config is passed to NewCache for creating new Cache instances.
  80  type Config[K Key, V any] struct {
  81  	// NumCounters determines the number of counters (keys) to keep that hold
  82  	// access frequency information. It's generally a good idea to have more
  83  	// counters than the max cache capacity, as this will improve eviction
  84  	// accuracy and subsequent hit ratios.
  85  	//
  86  	// For example, if you expect your cache to hold 1,000,000 items when full,
  87  	// NumCounters should be 10,000,000 (10x). Each counter takes up roughly
  88  	// 3 bytes (4 bits for each counter * 4 copies plus about a byte per
  89  	// counter for the bloom filter). Note that the number of counters is
  90  	// internally rounded up to the nearest power of 2, so the space usage
  91  	// may be a little larger than 3 bytes * NumCounters.
  92  	//
  93  	// We've seen good performance in setting this to 10x the number of items
  94  	// you expect to keep in the cache when full.
  95  	NumCounters int64
  96  
  97  	// MaxCost is how eviction decisions are made. For example, if MaxCost is
  98  	// 100 and a new item with a cost of 1 increases total cache cost to 101,
  99  	// 1 item will be evicted.
 100  	//
 101  	// MaxCost can be considered as the cache capacity, in whatever units you
 102  	// choose to use.
 103  	//
 104  	// For example, if you want the cache to have a max capacity of 100MB, you
 105  	// would set MaxCost to 100,000,000 and pass an item's number of bytes as
 106  	// the `cost` parameter for calls to Set. If new items are accepted, the
 107  	// eviction process will take care of making room for the new item and not
 108  	// overflowing the MaxCost value.
 109  	//
 110  	// MaxCost could be anything as long as it matches how you're using the cost
 111  	// values when calling Set.
 112  	MaxCost int64
 113  
 114  	// BufferItems determines the size of Get buffers.
 115  	//
 116  	// Unless you have a rare use case, using `64` as the BufferItems value
 117  	// results in good performance.
 118  	//
 119  	// If for some reason you see Get performance decreasing with lots of
 120  	// contention (you shouldn't), try increasing this value in increments of 64.
 121  	// This is a fine-tuning mechanism and you probably won't have to touch this.
 122  	BufferItems int64
 123  
 124  	// Metrics is true when you want variety of stats about the cache.
 125  	// There is some overhead to keeping statistics, so you should only set this
 126  	// flag to true when testing or throughput performance isn't a major factor.
 127  	Metrics bool
 128  
 129  	// OnEvict is called for every eviction with the evicted item.
 130  	OnEvict func(item *Item[V])
 131  
 132  	// OnReject is called for every rejection done via the policy.
 133  	OnReject func(item *Item[V])
 134  
 135  	// OnExit is called whenever a value is removed from cache. This can be
 136  	// used to do manual memory deallocation. Would also be called on eviction
 137  	// as well as on rejection of the value.
 138  	OnExit func(val V)
 139  
 140  	// ShouldUpdate is called when a value already exists in cache and is being updated.
 141  	// If ShouldUpdate returns true, the cache continues with the update (Set). If the
 142  	// function returns false, no changes are made in the cache. If the value doesn't
 143  	// already exist, the cache continue with setting that value for the given key.
 144  	//
 145  	// In this function, you can check whether the new value is valid. For example, if
 146  	// your value has timestamp assosicated with it, you could check whether the new
 147  	// value has the latest timestamp, preventing you from setting an older value.
 148  	ShouldUpdate func(cur, prev V) bool
 149  
 150  	// KeyToHash function is used to customize the key hashing algorithm.
 151  	// Each key will be hashed using the provided function. If keyToHash value
 152  	// is not set, the default keyToHash function is used.
 153  	//
 154  	// Ristretto has a variety of defaults depending on the underlying interface type
 155  	// https://github.com/hypermodeinc/ristretto/blob/main/z/z.go#L19-L41).
 156  	//
 157  	// Note that if you want 128bit hashes you should use the both the values
 158  	// in the return of the function. If you want to use 64bit hashes, you can
 159  	// just return the first uint64 and return 0 for the second uint64.
 160  	KeyToHash func(key K) (uint64, uint64)
 161  
 162  	// Cost evaluates a value and outputs a corresponding cost. This function is ran
 163  	// after Set is called for a new item or an item is updated with a cost param of 0.
 164  	//
 165  	// Cost is an optional function you can pass to the Config in order to evaluate
 166  	// item cost at runtime, and only whentthe Set call isn't going to be dropped. This
 167  	// is useful if calculating item cost is particularly expensive and you don't want to
 168  	// waste time on items that will be dropped anyways.
 169  	//
 170  	// To signal to Ristretto that you'd like to use this Cost function:
 171  	//   1. Set the Cost field to a non-nil function.
 172  	//   2. When calling Set for new items or item updates, use a `cost` of 0.
 173  	Cost func(value V) int64
 174  
 175  	// IgnoreInternalCost set to true indicates to the cache that the cost of
 176  	// internally storing the value should be ignored. This is useful when the
 177  	// cost passed to set is not using bytes as units. Keep in mind that setting
 178  	// this to true will increase the memory usage.
 179  	IgnoreInternalCost bool
 180  
 181  	// TtlTickerDurationInSec sets the value of time ticker for cleanup keys on TTL expiry.
 182  	TtlTickerDurationInSec int64
 183  }
 184  
 185  type itemFlag byte
 186  
 187  const (
 188  	itemNew itemFlag = iota
 189  	itemDelete
 190  	itemUpdate
 191  )
 192  
 193  // Item is a full representation of what's stored in the cache for each key-value pair.
 194  type Item[V any] struct {
 195  	flag       itemFlag
 196  	Key        uint64
 197  	Conflict   uint64
 198  	Value      V
 199  	Cost       int64
 200  	Expiration time.Time
 201  	wait       chan struct{}
 202  }
 203  
 204  // NewCache returns a new Cache instance and any configuration errors, if any.
 205  func NewCache[K Key, V any](config *Config[K, V]) (*Cache[K, V], error) {
 206  	switch {
 207  	case config.NumCounters == 0:
 208  		return nil, errors.New("NumCounters can't be zero")
 209  	case config.NumCounters < 0:
 210  		return nil, errors.New("NumCounters can't be negative")
 211  	case config.MaxCost == 0:
 212  		return nil, errors.New("MaxCost can't be zero")
 213  	case config.MaxCost < 0:
 214  		return nil, errors.New("MaxCost can't be negative")
 215  	case config.BufferItems == 0:
 216  		return nil, errors.New("BufferItems can't be zero")
 217  	case config.BufferItems < 0:
 218  		return nil, errors.New("BufferItems can't be negative")
 219  	case config.TtlTickerDurationInSec == 0:
 220  		config.TtlTickerDurationInSec = bucketDurationSecs
 221  	}
 222  	policy := newPolicy[V](config.NumCounters, config.MaxCost)
 223  	cache := &Cache[K, V]{
 224  		storedItems:        newStore[V](),
 225  		cachePolicy:        policy,
 226  		getBuf:             newRingBuffer(policy, config.BufferItems),
 227  		setBuf:             make(chan *Item[V], setBufSize),
 228  		keyToHash:          config.KeyToHash,
 229  		stop:               make(chan struct{}),
 230  		done:               make(chan struct{}),
 231  		cost:               config.Cost,
 232  		ignoreInternalCost: config.IgnoreInternalCost,
 233  		cleanupTicker:      time.NewTicker(time.Duration(config.TtlTickerDurationInSec) * time.Second / 2),
 234  	}
 235  	cache.storedItems.SetShouldUpdateFn(config.ShouldUpdate)
 236  	cache.onExit = func(val V) {
 237  		if config.OnExit != nil {
 238  			config.OnExit(val)
 239  		}
 240  	}
 241  	cache.onEvict = func(item *Item[V]) {
 242  		if config.OnEvict != nil {
 243  			config.OnEvict(item)
 244  		}
 245  		cache.onExit(item.Value)
 246  	}
 247  	cache.onReject = func(item *Item[V]) {
 248  		if config.OnReject != nil {
 249  			config.OnReject(item)
 250  		}
 251  		cache.onExit(item.Value)
 252  	}
 253  	if cache.keyToHash == nil {
 254  		cache.keyToHash = z.KeyToHash[K]
 255  	}
 256  
 257  	if config.Metrics {
 258  		cache.collectMetrics()
 259  	}
 260  	// NOTE: benchmarks seem to show that performance decreases the more
 261  	//       goroutines we have running cache.processItems(), so 1 should
 262  	//       usually be sufficient
 263  	go cache.processItems()
 264  	return cache, nil
 265  }
 266  
 267  // Wait blocks until all buffered writes have been applied. This ensures a call to Set()
 268  // will be visible to future calls to Get().
 269  func (c *Cache[K, V]) Wait() {
 270  	if c == nil || c.isClosed.Load() {
 271  		return
 272  	}
 273  	wait := make(chan struct{})
 274  	c.setBuf <- &Item[V]{wait: wait}
 275  	<-wait
 276  }
 277  
 278  // Get returns the value (if any) and a boolean representing whether the
 279  // value was found or not. The value can be nil and the boolean can be true at
 280  // the same time. Get will not return expired items.
 281  func (c *Cache[K, V]) Get(key K) (V, bool) {
 282  	if c == nil || c.isClosed.Load() {
 283  		return zeroValue[V](), false
 284  	}
 285  	keyHash, conflictHash := c.keyToHash(key)
 286  
 287  	c.getBuf.Push(keyHash)
 288  	value, ok := c.storedItems.Get(keyHash, conflictHash)
 289  	if ok {
 290  		c.Metrics.add(hit, keyHash, 1)
 291  	} else {
 292  		c.Metrics.add(miss, keyHash, 1)
 293  	}
 294  	return value, ok
 295  }
 296  
 297  // Set attempts to add the key-value item to the cache. If it returns false,
 298  // then the Set was dropped and the key-value item isn't added to the cache. If
 299  // it returns true, there's still a chance it could be dropped by the policy if
 300  // its determined that the key-value item isn't worth keeping, but otherwise the
 301  // item will be added and other items will be evicted in order to make room.
 302  //
 303  // To dynamically evaluate the items cost using the Config.Coster function, set
 304  // the cost parameter to 0 and Coster will be ran when needed in order to find
 305  // the items true cost.
 306  //
 307  // Set writes the value of type V as is. If type V is a pointer type, It is ok
 308  // to update the memory pointed to by the pointer. Updating the pointer itself
 309  // will not be reflected in the cache. Be careful when using slice types as the
 310  // value type V. Calling `append` may update the underlined array pointer which
 311  // will not be reflected in the cache.
 312  func (c *Cache[K, V]) Set(key K, value V, cost int64) bool {
 313  	return c.SetWithTTL(key, value, cost, 0*time.Second)
 314  }
 315  
 316  // SetWithTTL works like Set but adds a key-value pair to the cache that will expire
 317  // after the specified TTL (time to live) has passed. A zero value means the value never
 318  // expires, which is identical to calling Set. A negative value is a no-op and the value
 319  // is discarded.
 320  //
 321  // See Set for more information.
 322  func (c *Cache[K, V]) SetWithTTL(key K, value V, cost int64, ttl time.Duration) bool {
 323  	if c == nil || c.isClosed.Load() {
 324  		return false
 325  	}
 326  
 327  	var expiration time.Time
 328  	switch {
 329  	case ttl == 0:
 330  		// No expiration.
 331  		break
 332  	case ttl < 0:
 333  		// Treat this a no-op.
 334  		return false
 335  	default:
 336  		expiration = time.Now().Add(ttl)
 337  	}
 338  
 339  	keyHash, conflictHash := c.keyToHash(key)
 340  	i := &Item[V]{
 341  		flag:       itemNew,
 342  		Key:        keyHash,
 343  		Conflict:   conflictHash,
 344  		Value:      value,
 345  		Cost:       cost,
 346  		Expiration: expiration,
 347  	}
 348  	// cost is eventually updated. The expiration must also be immediately updated
 349  	// to prevent items from being prematurely removed from the map.
 350  	if prev, ok := c.storedItems.Update(i); ok {
 351  		c.onExit(prev)
 352  		i.flag = itemUpdate
 353  	}
 354  	// Attempt to send item to cachePolicy.
 355  	select {
 356  	case c.setBuf <- i:
 357  		return true
 358  	default:
 359  		if i.flag == itemUpdate {
 360  			// Return true if this was an update operation since we've already
 361  			// updated the storedItems. For all the other operations (set/delete), we
 362  			// return false which means the item was not inserted.
 363  			return true
 364  		}
 365  		c.Metrics.add(dropSets, keyHash, 1)
 366  		return false
 367  	}
 368  }
 369  
 370  // Del deletes the key-value item from the cache if it exists.
 371  func (c *Cache[K, V]) Del(key K) {
 372  	if c == nil || c.isClosed.Load() {
 373  		return
 374  	}
 375  	keyHash, conflictHash := c.keyToHash(key)
 376  	// Delete immediately.
 377  	_, prev := c.storedItems.Del(keyHash, conflictHash)
 378  	c.onExit(prev)
 379  	// If we've set an item, it would be applied slightly later.
 380  	// So we must push the same item to `setBuf` with the deletion flag.
 381  	// This ensures that if a set is followed by a delete, it will be
 382  	// applied in the correct order.
 383  	c.setBuf <- &Item[V]{
 384  		flag:     itemDelete,
 385  		Key:      keyHash,
 386  		Conflict: conflictHash,
 387  	}
 388  }
 389  
 390  // GetTTL returns the TTL for the specified key and a bool that is true if the
 391  // item was found and is not expired.
 392  func (c *Cache[K, V]) GetTTL(key K) (time.Duration, bool) {
 393  	if c == nil {
 394  		return 0, false
 395  	}
 396  
 397  	keyHash, conflictHash := c.keyToHash(key)
 398  	if _, ok := c.storedItems.Get(keyHash, conflictHash); !ok {
 399  		// not found
 400  		return 0, false
 401  	}
 402  
 403  	expiration := c.storedItems.Expiration(keyHash)
 404  	if expiration.IsZero() {
 405  		// found but no expiration
 406  		return 0, true
 407  	}
 408  
 409  	if time.Now().After(expiration) {
 410  		// found but expired
 411  		return 0, false
 412  	}
 413  
 414  	return time.Until(expiration), true
 415  }
 416  
 417  // Close stops all goroutines and closes all channels.
 418  func (c *Cache[K, V]) Close() {
 419  	if c == nil || c.isClosed.Load() {
 420  		return
 421  	}
 422  	c.Clear()
 423  
 424  	// Block until processItems goroutine is returned.
 425  	c.stop <- struct{}{}
 426  	<-c.done
 427  	close(c.stop)
 428  	close(c.done)
 429  	close(c.setBuf)
 430  	c.cachePolicy.Close()
 431  	c.cleanupTicker.Stop()
 432  	c.isClosed.Store(true)
 433  }
 434  
 435  // Clear empties the hashmap and zeroes all cachePolicy counters. Note that this is
 436  // not an atomic operation (but that shouldn't be a problem as it's assumed that
 437  // Set/Get calls won't be occurring until after this).
 438  func (c *Cache[K, V]) Clear() {
 439  	if c == nil || c.isClosed.Load() {
 440  		return
 441  	}
 442  	// Block until processItems goroutine is returned.
 443  	c.stop <- struct{}{}
 444  	<-c.done
 445  
 446  	// Clear out the setBuf channel.
 447  loop:
 448  	for {
 449  		select {
 450  		case i := <-c.setBuf:
 451  			if i.wait != nil {
 452  				close(i.wait)
 453  				continue
 454  			}
 455  			if i.flag != itemUpdate {
 456  				// In itemUpdate, the value is already set in the storedItems.  So, no need to call
 457  				// onEvict here.
 458  				c.onEvict(i)
 459  			}
 460  		default:
 461  			break loop
 462  		}
 463  	}
 464  
 465  	// Clear value hashmap and cachePolicy data.
 466  	c.cachePolicy.Clear()
 467  	c.storedItems.Clear(c.onEvict)
 468  	// Only reset metrics if they're enabled.
 469  	if c.Metrics != nil {
 470  		c.Metrics.Clear()
 471  	}
 472  	// Restart processItems goroutine.
 473  	go c.processItems()
 474  }
 475  
 476  // MaxCost returns the max cost of the cache.
 477  func (c *Cache[K, V]) MaxCost() int64 {
 478  	if c == nil {
 479  		return 0
 480  	}
 481  	return c.cachePolicy.MaxCost()
 482  }
 483  
 484  // UpdateMaxCost updates the maxCost of an existing cache.
 485  func (c *Cache[K, V]) UpdateMaxCost(maxCost int64) {
 486  	if c == nil {
 487  		return
 488  	}
 489  	c.cachePolicy.UpdateMaxCost(maxCost)
 490  }
 491  
 492  // RemainingCost returns the remaining cost capacity (MaxCost - Used) of an existing cache.
 493  func (c *Cache[K, V]) RemainingCost() int64 {
 494  	if c == nil {
 495  		return 0
 496  	}
 497  	return c.cachePolicy.Cap()
 498  }
 499  
 500  // processItems is ran by goroutines processing the Set buffer.
 501  func (c *Cache[K, V]) processItems() {
 502  	startTs := make(map[uint64]time.Time)
 503  	numToKeep := 100000 // TODO: Make this configurable via options.
 504  
 505  	trackAdmission := func(key uint64) {
 506  		if c.Metrics == nil {
 507  			return
 508  		}
 509  		startTs[key] = time.Now()
 510  		if len(startTs) > numToKeep {
 511  			for k := range startTs {
 512  				if len(startTs) <= numToKeep {
 513  					break
 514  				}
 515  				delete(startTs, k)
 516  			}
 517  		}
 518  	}
 519  	onEvict := func(i *Item[V]) {
 520  		if ts, has := startTs[i.Key]; has {
 521  			c.Metrics.trackEviction(int64(time.Since(ts) / time.Second))
 522  			delete(startTs, i.Key)
 523  		}
 524  		if c.onEvict != nil {
 525  			c.onEvict(i)
 526  		}
 527  	}
 528  
 529  	for {
 530  		select {
 531  		case i := <-c.setBuf:
 532  			if i.wait != nil {
 533  				close(i.wait)
 534  				continue
 535  			}
 536  			// Calculate item cost value if new or update.
 537  			if i.Cost == 0 && c.cost != nil && i.flag != itemDelete {
 538  				i.Cost = c.cost(i.Value)
 539  			}
 540  			if !c.ignoreInternalCost {
 541  				// Add the cost of internally storing the object.
 542  				i.Cost += itemSize
 543  			}
 544  
 545  			switch i.flag {
 546  			case itemNew:
 547  				victims, added := c.cachePolicy.Add(i.Key, i.Cost)
 548  				if added {
 549  					c.storedItems.Set(i)
 550  					c.Metrics.add(keyAdd, i.Key, 1)
 551  					trackAdmission(i.Key)
 552  				} else {
 553  					c.onReject(i)
 554  				}
 555  				for _, victim := range victims {
 556  					victim.Conflict, victim.Value = c.storedItems.Del(victim.Key, 0)
 557  					onEvict(victim)
 558  				}
 559  
 560  			case itemUpdate:
 561  				c.cachePolicy.Update(i.Key, i.Cost)
 562  
 563  			case itemDelete:
 564  				c.cachePolicy.Del(i.Key) // Deals with metrics updates.
 565  				_, val := c.storedItems.Del(i.Key, i.Conflict)
 566  				c.onExit(val)
 567  			}
 568  		case <-c.cleanupTicker.C:
 569  			c.storedItems.Cleanup(c.cachePolicy, onEvict)
 570  		case <-c.stop:
 571  			c.done <- struct{}{}
 572  			return
 573  		}
 574  	}
 575  }
 576  
 577  // collectMetrics just creates a new *Metrics instance and adds the pointers
 578  // to the cache and policy instances.
 579  func (c *Cache[K, V]) collectMetrics() {
 580  	c.Metrics = newMetrics()
 581  	c.cachePolicy.CollectMetrics(c.Metrics)
 582  }
 583  
 584  type metricType int
 585  
 586  const (
 587  	// The following 2 keep track of hits and misses.
 588  	hit = iota
 589  	miss
 590  	// The following 3 keep track of number of keys added, updated and evicted.
 591  	keyAdd
 592  	keyUpdate
 593  	keyEvict
 594  	// The following 2 keep track of cost of keys added and evicted.
 595  	costAdd
 596  	costEvict
 597  	// The following keep track of how many sets were dropped or rejected later.
 598  	dropSets
 599  	rejectSets
 600  	// The following 2 keep track of how many gets were kept and dropped on the
 601  	// floor.
 602  	dropGets
 603  	keepGets
 604  	// This should be the final enum. Other enums should be set before this.
 605  	doNotUse
 606  )
 607  
 608  func stringFor(t metricType) string {
 609  	switch t {
 610  	case hit:
 611  		return "hit"
 612  	case miss:
 613  		return "miss"
 614  	case keyAdd:
 615  		return "keys-added"
 616  	case keyUpdate:
 617  		return "keys-updated"
 618  	case keyEvict:
 619  		return "keys-evicted"
 620  	case costAdd:
 621  		return "cost-added"
 622  	case costEvict:
 623  		return "cost-evicted"
 624  	case dropSets:
 625  		return "sets-dropped"
 626  	case rejectSets:
 627  		return "sets-rejected" // by policy.
 628  	case dropGets:
 629  		return "gets-dropped"
 630  	case keepGets:
 631  		return "gets-kept"
 632  	default:
 633  		return "unidentified"
 634  	}
 635  }
 636  
 637  // Metrics is a snapshot of performance statistics for the lifetime of a cache instance.
 638  type Metrics struct {
 639  	all [doNotUse][]*uint64
 640  
 641  	mu   sync.RWMutex
 642  	life *z.HistogramData // Tracks the life expectancy of a key.
 643  }
 644  
 645  func newMetrics() *Metrics {
 646  	s := &Metrics{
 647  		life: z.NewHistogramData(z.HistogramBounds(1, 16)),
 648  	}
 649  	for i := 0; i < doNotUse; i++ {
 650  		s.all[i] = make([]*uint64, 256)
 651  		slice := s.all[i]
 652  		for j := range slice {
 653  			slice[j] = new(uint64)
 654  		}
 655  	}
 656  	return s
 657  }
 658  
 659  func (p *Metrics) add(t metricType, hash, delta uint64) {
 660  	if p == nil {
 661  		return
 662  	}
 663  	valp := p.all[t]
 664  	// Avoid false sharing by padding at least 64 bytes of space between two
 665  	// atomic counters which would be incremented.
 666  	idx := (hash % 25) * 10
 667  	atomic.AddUint64(valp[idx], delta)
 668  }
 669  
 670  func (p *Metrics) get(t metricType) uint64 {
 671  	if p == nil {
 672  		return 0
 673  	}
 674  	valp := p.all[t]
 675  	var total uint64
 676  	for i := range valp {
 677  		total += atomic.LoadUint64(valp[i])
 678  	}
 679  	return total
 680  }
 681  
 682  // Hits is the number of Get calls where a value was found for the corresponding key.
 683  func (p *Metrics) Hits() uint64 {
 684  	return p.get(hit)
 685  }
 686  
 687  // Misses is the number of Get calls where a value was not found for the corresponding key.
 688  func (p *Metrics) Misses() uint64 {
 689  	return p.get(miss)
 690  }
 691  
 692  // KeysAdded is the total number of Set calls where a new key-value item was added.
 693  func (p *Metrics) KeysAdded() uint64 {
 694  	return p.get(keyAdd)
 695  }
 696  
 697  // KeysUpdated is the total number of Set calls where the value was updated.
 698  func (p *Metrics) KeysUpdated() uint64 {
 699  	return p.get(keyUpdate)
 700  }
 701  
 702  // KeysEvicted is the total number of keys evicted.
 703  func (p *Metrics) KeysEvicted() uint64 {
 704  	return p.get(keyEvict)
 705  }
 706  
 707  // CostAdded is the sum of costs that have been added (successful Set calls).
 708  func (p *Metrics) CostAdded() uint64 {
 709  	return p.get(costAdd)
 710  }
 711  
 712  // CostEvicted is the sum of all costs that have been evicted.
 713  func (p *Metrics) CostEvicted() uint64 {
 714  	return p.get(costEvict)
 715  }
 716  
 717  // SetsDropped is the number of Set calls that don't make it into internal
 718  // buffers (due to contention or some other reason).
 719  func (p *Metrics) SetsDropped() uint64 {
 720  	return p.get(dropSets)
 721  }
 722  
 723  // SetsRejected is the number of Set calls rejected by the policy (TinyLFU).
 724  func (p *Metrics) SetsRejected() uint64 {
 725  	return p.get(rejectSets)
 726  }
 727  
 728  // GetsDropped is the number of Get counter increments that are dropped
 729  // internally.
 730  func (p *Metrics) GetsDropped() uint64 {
 731  	return p.get(dropGets)
 732  }
 733  
 734  // GetsKept is the number of Get counter increments that are kept.
 735  func (p *Metrics) GetsKept() uint64 {
 736  	return p.get(keepGets)
 737  }
 738  
 739  // Ratio is the number of Hits over all accesses (Hits + Misses). This is the
 740  // percentage of successful Get calls.
 741  func (p *Metrics) Ratio() float64 {
 742  	if p == nil {
 743  		return 0.0
 744  	}
 745  	hits, misses := p.get(hit), p.get(miss)
 746  	if hits == 0 && misses == 0 {
 747  		return 0.0
 748  	}
 749  	return float64(hits) / float64(hits+misses)
 750  }
 751  
 752  func (p *Metrics) trackEviction(numSeconds int64) {
 753  	if p == nil {
 754  		return
 755  	}
 756  	p.mu.Lock()
 757  	defer p.mu.Unlock()
 758  	p.life.Update(numSeconds)
 759  }
 760  
 761  func (p *Metrics) LifeExpectancySeconds() *z.HistogramData {
 762  	if p == nil {
 763  		return nil
 764  	}
 765  	p.mu.RLock()
 766  	defer p.mu.RUnlock()
 767  	return p.life.Copy()
 768  }
 769  
 770  // Clear resets all the metrics.
 771  func (p *Metrics) Clear() {
 772  	if p == nil {
 773  		return
 774  	}
 775  	for i := 0; i < doNotUse; i++ {
 776  		for j := range p.all[i] {
 777  			atomic.StoreUint64(p.all[i][j], 0)
 778  		}
 779  	}
 780  	p.mu.Lock()
 781  	p.life = z.NewHistogramData(z.HistogramBounds(1, 16))
 782  	p.mu.Unlock()
 783  }
 784  
 785  // String returns a string representation of the metrics.
 786  func (p *Metrics) String() string {
 787  	if p == nil {
 788  		return ""
 789  	}
 790  	var buf bytes.Buffer
 791  	for i := 0; i < doNotUse; i++ {
 792  		t := metricType(i)
 793  		fmt.Fprintf(&buf, "%s: %d ", stringFor(t), p.get(t))
 794  	}
 795  	fmt.Fprintf(&buf, "gets-total: %d ", p.get(hit)+p.get(miss))
 796  	fmt.Fprintf(&buf, "hit-ratio: %.2f", p.Ratio())
 797  	return buf.String()
 798  }
 799