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