1
|
|
|
// Package lfucache implements LFU (Least Frequently Used) cache |
2
|
|
|
package lfucache |
3
|
|
|
|
4
|
|
|
import ( |
5
|
|
|
"github.com/arazmj/gerdu/dlinklist" |
6
|
|
|
"github.com/arazmj/gerdu/metrics" |
7
|
|
|
"github.com/inhies/go-bytesize" |
8
|
|
|
"sync" |
9
|
|
|
) |
10
|
|
|
|
11
|
|
|
// LFUCache data structure |
12
|
|
|
type LFUCache struct { |
13
|
|
|
sync.Mutex |
14
|
|
|
size bytesize.ByteSize |
15
|
|
|
capacity bytesize.ByteSize |
16
|
|
|
node map[string]*dlinklist.Node |
17
|
|
|
freq map[int]*dlinklist.DLinkedList |
18
|
|
|
minFreq int |
19
|
|
|
} |
20
|
|
|
|
21
|
|
|
// NewCache LFUCache constructor |
22
|
|
|
func NewCache(capacity bytesize.ByteSize) *LFUCache { |
23
|
|
|
return &LFUCache{ |
24
|
|
|
size: 0, |
25
|
|
|
capacity: capacity, |
26
|
|
|
node: map[string]*dlinklist.Node{}, |
27
|
|
|
freq: map[int]*dlinklist.DLinkedList{}, |
28
|
|
|
minFreq: 0, |
29
|
|
|
} |
30
|
|
|
} |
31
|
|
|
|
32
|
|
|
// This is a helper function that used in the following two cases: |
33
|
|
|
// |
34
|
|
|
// 1. when Get(key)` is called; and |
35
|
|
|
// 2. when Put(key, value)` is called and the key exists. |
36
|
|
|
// |
37
|
|
|
// The common point of these two cases is that: |
38
|
|
|
// |
39
|
|
|
// 1. no new node comes in, and |
40
|
|
|
// 2. the node is visited one more times -> node.freq changed -> |
41
|
|
|
// thus the place of this node will change |
42
|
|
|
// |
43
|
|
|
// The logic of this function is: |
44
|
|
|
// |
45
|
|
|
// 1. pop the node from the old DLinkedList (with freq `f`) |
46
|
|
|
// 2. append the node to new DLinkedList (with freq `f+1`) |
47
|
|
|
// 3. if old DLinkedList has size 0 and minFreq is `f`, |
48
|
|
|
// update minFreq to `f+1` |
49
|
|
|
// |
50
|
|
|
// All of the above operations took O(1) time. |
51
|
|
|
func (c *LFUCache) update(node *dlinklist.Node) { |
52
|
|
|
freq := node.Freq |
53
|
|
|
|
54
|
|
|
c.freq[freq].RemoveNode(node) |
55
|
|
|
if v, _ := c.freq[freq]; c.minFreq == freq && v.Size() == 0 { |
56
|
|
|
delete(c.freq, freq) |
57
|
|
|
c.minFreq++ |
58
|
|
|
} |
59
|
|
|
|
60
|
|
|
node.Freq++ |
61
|
|
|
freq = node.Freq |
62
|
|
|
if _, ok := c.freq[freq]; !ok { |
63
|
|
|
c.freq[freq] = dlinklist.NewLinkedList() |
64
|
|
|
} |
65
|
|
|
c.freq[freq].AddNode(node) |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
// Get through checking node[key], we can get the node in O(1) time. |
69
|
|
|
// Just performs update, then we can return the value of node. |
70
|
|
|
func (c *LFUCache) Get(key string) (value string, ok bool) { |
71
|
|
|
defer c.Unlock() |
72
|
|
|
c.Lock() |
73
|
|
|
|
74
|
|
|
if _, ok := c.node[key]; !ok { |
75
|
|
|
metrics.Miss.Inc() |
76
|
|
|
return "", false |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
metrics.Hits.Inc() |
80
|
|
|
node := c.node[key] |
81
|
|
|
c.update(node) |
82
|
|
|
return node.Value, true |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
// Put If `key` already exists in self._node, we do the same operations as `get`, except |
86
|
|
|
// updating the node.val to new value. Otherwise |
87
|
|
|
// 1. if the cache reaches its capacity, pop the least frequently used item. (*) |
88
|
|
|
// 2. add new node to self._node |
89
|
|
|
// 3. add new node to the DLinkedList with frequency 1 |
90
|
|
|
// 4. reset minFreq to 1 |
91
|
|
|
// |
92
|
|
|
// (*) How to pop the least frequently used item? Two facts: |
93
|
|
|
// 1. we maintain the minFreq, the minimum possible frequency in cache. |
94
|
|
|
// 2. All cache with the same frequency are stored as a DLinkedList, with |
95
|
|
|
// recently used order (Always append at head) |
96
|
|
|
// 3. The tail of the DLinkedList with minFreq is the least |
97
|
|
|
//recently used one, pop it. |
98
|
|
|
func (c *LFUCache) Put(key, value string) (created bool) { |
99
|
|
|
defer c.Unlock() |
100
|
|
|
c.Lock() |
101
|
|
|
if c.capacity == 0 { |
102
|
|
|
return |
103
|
|
|
} |
104
|
|
|
if _, ok := c.node[key]; ok { |
105
|
|
|
metrics.Hits.Inc() |
106
|
|
|
node := c.node[key] |
107
|
|
|
c.update(node) |
108
|
|
|
node.Value = value |
109
|
|
|
created = false |
110
|
|
|
} else { |
111
|
|
|
c.size += bytesize.ByteSize(len(value)) |
112
|
|
|
for c.size > c.capacity { |
113
|
|
|
minList, ok := c.freq[c.minFreq] |
114
|
|
|
metrics.Deletes.Inc() |
115
|
|
|
if !ok || minList.Size() == 0 { |
116
|
|
|
delete(c.freq, c.minFreq) |
117
|
|
|
c.minFreq++ |
118
|
|
|
} else { |
119
|
|
|
node := minList.PopTail() |
120
|
|
|
freq := node.Freq |
121
|
|
|
if v, _ := c.freq[c.minFreq]; c.minFreq == freq && v.Size() == 0 { |
122
|
|
|
delete(c.freq, freq) |
123
|
|
|
c.minFreq++ |
124
|
|
|
} |
125
|
|
|
c.size -= bytesize.ByteSize(len(node.Value)) |
126
|
|
|
delete(c.node, node.Key) |
127
|
|
|
} |
128
|
|
|
} |
129
|
|
|
metrics.Adds.Inc() |
130
|
|
|
node := &dlinklist.Node{ |
131
|
|
|
Key: key, |
132
|
|
|
Value: value, |
133
|
|
|
Freq: 1, |
134
|
|
|
} |
135
|
|
|
c.node[key] = node |
136
|
|
|
if _, ok := c.freq[1]; !ok { |
137
|
|
|
c.freq[1] = dlinklist.NewLinkedList() |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
c.freq[1].AddNode(node) |
141
|
|
|
c.minFreq = 1 |
142
|
|
|
created = true |
143
|
|
|
} |
144
|
|
|
return created |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
func (c *LFUCache) Delete(key string) (ok bool) { |
|
|
|
|
148
|
|
|
node, ok := c.node[key] |
149
|
|
|
if !ok { |
150
|
|
|
return false |
151
|
|
|
} |
152
|
|
|
freq := node.Freq |
153
|
|
|
if v, _ := c.freq[c.minFreq]; c.minFreq == freq && v.Size() == 0 { |
154
|
|
|
metrics.Deletes.Inc() |
155
|
|
|
delete(c.freq, freq) |
156
|
|
|
c.minFreq++ |
157
|
|
|
} |
158
|
|
|
delete(c.node, key) |
159
|
|
|
return true |
160
|
|
|
} |
161
|
|
|
|