Skip to content

Commit

Permalink
Merge pull request #1 from bparli/track-cache-by-size
Browse files Browse the repository at this point in the history
Track cache by size
  • Loading branch information
bparli authored Mar 25, 2020
2 parents 89e396a + 515e95c commit 1689882
Show file tree
Hide file tree
Showing 6 changed files with 223 additions and 86 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ for i := 0; i < 256; i++ {
```

## Acknowledgements
* Paper outling LFU with Dynamic Aging [https://www.hpl.hp.com/techreports/98/HPL-98-173.pdf](https://www.hpl.hp.com/techreports/98/HPL-98-173.pdf)
* Paper outlining LFU with Dynamic Aging [https://www.hpl.hp.com/techreports/98/HPL-98-173.pdf](https://www.hpl.hp.com/techreports/98/HPL-98-173.pdf)
* Squid proxy implementation [https://www.hpl.hp.com/techreports/1999/HPL-1999-69.html](https://www.hpl.hp.com/techreports/1999/HPL-1999-69.html)
* O(1) LFU algorithm paper [http://dhruvbird.com/lfu.pdf](http://dhruvbird.com/lfu.pdf)
* Nice LFU implementation in Go [https://github.com/dgrijalva/lfu-go](https://github.com/dgrijalva/lfu-go)
* Interface patterned after [golang-lru](https://github.com/hashicorp/golang-lru)



16 changes: 12 additions & 4 deletions lfuda.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,25 @@ func (c *Cache) Keys() []interface{} {
}

// Len returns the number of items in the cache.
func (c *Cache) Len() int {
func (c *Cache) Len() (length int) {
c.lock.RLock()
length := c.lfuda.Len()
length = c.lfuda.Len()
c.lock.RUnlock()
return length
}

// Size returns the current size of the cache in bytes.
func (c *Cache) Size() (size int) {
c.lock.RLock()
size = c.lfuda.Size()
c.lock.RUnlock()
return size
}

// Age returns the cache's current age
func (c *Cache) Age() int {
func (c *Cache) Age() (age int) {
c.lock.RLock()
age := c.lfuda.Age()
age = c.lfuda.Age()
c.lock.RUnlock()
return age
}
65 changes: 38 additions & 27 deletions lfuda_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,67 +74,61 @@ func TestLFUDA(t *testing.T) {
evictCounter := 0
onEvicted := func(k interface{}, v interface{}) {
if k != v {
t.Fatalf("Evict values not equal (%v!=%v)", k, v)
t.Errorf("Evict values not equal (%v!=%v)", k, v)
}
evictCounter++
}
l := NewWithEvict(128, onEvicted)
l := NewWithEvict(666, onEvicted)

numSet := 0
for i := 0; i < 256; i++ {
for i := 100; i < 1000; i++ {
if l.Set(i, i) {
numSet++
}
}
if l.Len() != 128 {
t.Fatalf("bad len: %v", l.Len())
if l.Len() != 222 || l.Len() != len(l.Keys()) {
t.Errorf("bad len: %v", l.Len())
}

if numSet != 128 {
t.Fatalf("bad evict count: %v", evictCounter)
if evictCounter != 900-222 {
t.Errorf("bad evict count: %v", evictCounter)
}

for i, k := range l.Keys() {
if v, ok := l.Get(k); !ok || v != k || v != i+128 {
t.Fatalf("bad key: %v, %v, %t, %d", k, v, ok, i)
for _, k := range l.Keys() {
if v, ok := l.Get(k); !ok || v != k {
t.Fatalf("bad key: %v, %v, %t", k, v, ok)
}
}

// bump the hits counter of each item in cache
for i := 128; i < 256; i++ {
_, ok := l.Get(i)
if !ok {
t.Fatalf("should not be evicted")
}
}

// these should all be misses
for i := 0; i < 128; i++ {
// these should all be misses since their hits will be too low
// relative to newer keys set when the cache is more aged
for i := 100; i < 765; i++ {
_, ok := l.Get(i)
if ok {
t.Fatalf("should not be in cache")
}
}

if ok := l.Set(256, 256); !ok {
t.Fatalf("Wasn't able to set key/value in cache (but should have been)")
t.Errorf("Wasn't able to set key/value in cache (but should have been)")
}

// expect 256 to be the top hit in l.Keys()
if l.Keys()[0] != 256 {
t.Fatalf("out of order key (256 should be first)")
if val, _ := l.Get(256); val != 256 {
t.Errorf("Wrong value returned for key")
}

if val, _ := l.Get(256); val != 256 {
t.Fatalf("Wrong value returned for key")
// expect 256 to be the top hit in l.Keys()
if l.Keys()[0] != 256 {
t.Errorf("out of order key (last set of keys should have hits=5 and should be first): %d", l.Keys()[0])
}

l.Purge()
if l.Len() != 0 {
t.Fatalf("bad len: %v", l.Len())
t.Errorf("bad len: %v", l.Len())
}
if _, ok := l.Get(200); ok {
t.Fatalf("should contain nothing")
t.Errorf("should contain nothing")
}
}

Expand Down Expand Up @@ -300,3 +294,20 @@ func TestLFUDAAge(t *testing.T) {
t.Errorf("cache age should have been set to 1 (but it was't)")
}
}

func TestLFUDASize(t *testing.T) {
l := New(11)

for i := 10; i < 30; i++ {
l.Set(i, i)
}

if l.Size() != 10 {
t.Errorf("Cache can only store up to 11 bytes so Size should be divisible by 2")
}

l.Purge()
if l.Size() != 0 {
t.Errorf("Cache size should be reset to 0 (but it wasn't)")
}
}
154 changes: 110 additions & 44 deletions simplelfuda/lfuda.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package simplelfuda

import (
"container/list"
"fmt"
)

/*
Expand All @@ -17,28 +18,37 @@ type EvictCallback func(key interface{}, value interface{})

// LFUDA is a non-threadsafe fixed size LFU with Dynamic Aging Cache
type LFUDA struct {
size int
items map[interface{}]*item
freqs *list.List
onEvict EvictCallback
age int
// size of the entire cache in bytes
size int
currSize int
items map[interface{}]*item
freqs *list.List
onEvict EvictCallback
age int
}

type item struct {
key interface{}
value interface{}
hits int
element *list.Element
key interface{}
value interface{}
size int
hits int
freqNode *list.Element
}

// NewLFUDA constructs an LFUDA of the given size
type listEntry struct {
entries map[*item]byte
freq int
}

// NewLFUDA constructs an LFUDA of the given size in bytes
func NewLFUDA(size int, onEvict EvictCallback) *LFUDA {
return &LFUDA{
size: size,
items: make(map[interface{}]*item),
freqs: list.New(),
onEvict: onEvict,
age: 0,
size: size,
currSize: 0,
items: make(map[interface{}]*item),
freqs: list.New(),
onEvict: onEvict,
age: 0,
}
}

Expand Down Expand Up @@ -69,16 +79,31 @@ func (l *LFUDA) Set(key interface{}, value interface{}) bool {
l.increment(e)
} else {
// check if we need to evict
if len(l.items) >= l.size {
l.evict(1)
evicted = true
// convert to bytes so we can get the size of the value
numBytes := len([]byte(fmt.Sprintf("%v", value.(interface{}))))

// check this value will even fit in the cache. if not just return
if l.size < numBytes {
return false
}

// evict until there is room for the new item
for {
if l.currSize+numBytes > l.size {
l.evict()
evicted = true
} else {
break
}
}

// value doesn't exist. insert
e := new(item)
e.size = numBytes
e.key = key
e.value = value
l.items[key] = e
l.currSize += numBytes
l.increment(e)
}
return evicted
Expand All @@ -89,52 +114,78 @@ func (l *LFUDA) Len() int {
return len(l.items)
}

func (l *LFUDA) evict(count int) int {
var evicted int
for i := 0; i < count; i++ {
if elem := l.freqs.Front(); elem != nil {
entry := elem.Value.(*item)
// Size returns the number of items in the cache.
func (l *LFUDA) Size() int {
return l.currSize
}

func (l *LFUDA) evict() bool {
if place := l.freqs.Front(); place != nil {
for entry := range place.Value.(*listEntry).entries {
// set age to the value of the evicted object
// cache age should be less than or equal to the minimum key value in the cache
l.age = entry.hits
delete(l.items, entry.key)
l.freqs.Remove(elem)
if l.onEvict != nil {
l.onEvict(entry.key, entry.value)
}

// since entries is a map this is a random key in the lowest frequency node
l.Remove(entry.key)
return true
}
evicted++
}
return evicted
return false
}

func (l *LFUDA) increment(e *item) {
oldNode := e.freqNode
cursor := e.freqNode
var nextPlace *list.Element
if e.element == nil {

if cursor == nil {
// new entry
e.hits = l.age + 1
e.element = l.freqs.PushFront(e)
nextPlace = l.freqs.Front()
} else {
if e.hits < l.age {
e.hits = l.age
}
e.hits++
nextPlace = cursor.Next()
}

// move up until hits is < next frequency node's
for {
// move up until hits is < next element's
nextPlace = e.element.Next()
// we've reached the back
if nextPlace == nil {
l.freqs.MoveToBack(e.element)
// we've reached the back or the point where the next frequency
// node is greater than the item's hits count. Either way, create
// a new frequency node
if nextPlace == nil || nextPlace.Value.(*listEntry).freq > e.hits {
// create a new frequency node
li := new(listEntry)
li.freq = e.hits
li.entries = make(map[*item]byte)
if cursor != nil {
nextPlace = l.freqs.InsertAfter(li, cursor)
} else {
nextPlace = l.freqs.PushFront(li)
}
break
} else if e.hits <= nextPlace.Value.(*item).hits {
} else if nextPlace.Value.(*listEntry).freq == e.hits {
// found the right place
break
} else if e.hits > nextPlace.Value.(*item).hits {
l.freqs.MoveAfter(e.element, nextPlace.Value.(*item).element)
} else if e.hits > nextPlace.Value.(*listEntry).freq {
// keep searching
cursor = nextPlace
nextPlace = cursor.Next()
}
}

// set the right frequency node in the master list
e.freqNode = nextPlace
nextPlace.Value.(*listEntry).entries[e] = 1

// clenaup
if oldNode != nil {
// remove from old position
l.remEntry(oldNode, e)
}
}

// Purge will completely clear the LFUDA cache
Expand All @@ -146,6 +197,7 @@ func (l *LFUDA) Purge() {
delete(l.items, k)
}
l.age = 0
l.currSize = 0
l.freqs.Init()
}

Expand All @@ -163,20 +215,34 @@ func (l *LFUDA) Remove(key interface{}) bool {
if l.onEvict != nil {
l.onEvict(item.key, item.value)
}
l.freqs.Remove(item.element)
delete(l.items, key)
l.remEntry(item.freqNode, item)

// subtract current size of the cache by the size of the evicted item
l.currSize -= item.size

return true
}
return false
}

func (l *LFUDA) remEntry(place *list.Element, entry *item) {
entries := place.Value.(*listEntry).entries
delete(entries, entry)
if len(entries) == 0 {
l.freqs.Remove(place)
}
}

// Keys returns a slice of the keys in the cache ordered by frequency
func (l *LFUDA) Keys() []interface{} {
keys := make([]interface{}, len(l.items))
i := 0
for ent := l.freqs.Back(); ent != nil; ent = ent.Prev() {
keys[i] = ent.Value.(*item).key
i++
for node := l.freqs.Back(); node != nil; node = node.Prev() {
for ent := range node.Value.(*listEntry).entries {
keys[i] = ent.key
i++
}
}
return keys
}
Expand Down
Loading

0 comments on commit 1689882

Please sign in to comment.