21
21
from bzrlib import symbol_versioning
24
class _LRUNode(object):
25
"""This maintains the linked-list which is the lru internals."""
27
__slots__ = ('prev', 'next', 'key', 'value', 'cleanup', 'size')
29
def __init__(self, key, value, cleanup=None):
34
self.cleanup = cleanup
35
# TODO: We could compute this 'on-the-fly' like we used to, and remove
36
# one pointer from this object, we just need to decide if it
37
# actually costs us much of anything in normal usage
49
return '%s(%r n:%r p:%r)' % (self.__class__.__name__, self.key,
52
def run_cleanup(self):
53
if self.cleanup is not None:
54
self.cleanup(self.key, self.value)
56
# Just make sure to break any refcycles, etc
24
60
class LRUCache(object):
25
61
"""A class which manages a cache of entries, removing unused ones."""
33
69
DeprecationWarning)
34
70
after_cleanup_count = after_cleanup_size
37
self._queue = deque() # Track when things are accessed
38
self._refcount = {} # number of entries in self._queue for each key
72
# The "HEAD" of the lru linked list
73
self._most_recently_used = None
74
# The "TAIL" of the lru linked list
75
self._last_recently_used = None
39
76
self._update_max_cache(max_cache, after_cleanup_count)
41
78
def __contains__(self, key):
42
79
return key in self._cache
44
81
def __getitem__(self, key):
45
val = self._cache[key]
46
self._record_access(key)
82
node = self._cache[key]
83
# TODO: We may want to inline _record_access, to decrease the cost of
85
self._record_access(node)
50
89
return len(self._cache)
92
"""Walk the LRU list."""
93
node = self._most_recently_used
95
assert node.prev is None
96
while node is not None:
98
assert node is self._last_recently_used
100
assert node.next.prev is node
101
if node.prev is None:
102
assert node is self._most_recently_used
104
assert node.prev.next is node
52
108
def add(self, key, value, cleanup=None):
53
109
"""Add a new value to the cache.
107
171
"""Add a value to the cache, there will be no cleanup function."""
108
172
self.add(key, value, cleanup=None)
110
def _record_access(self, key):
174
def _record_access(self, node):
111
175
"""Record that key was accessed."""
112
self._queue.append(key)
113
# Can't use setdefault because you can't += 1 the result
114
self._refcount[key] = self._refcount.get(key, 0) + 1
116
# If our access queue is too large, clean it up too
117
if len(self._queue) > self._compact_queue_length:
118
self._compact_queue()
120
def _compact_queue(self):
121
"""Compact the queue, leaving things in sorted last appended order."""
123
for item in self._queue:
124
if self._refcount[item] == 1:
125
new_queue.append(item)
127
self._refcount[item] -= 1
128
self._queue = new_queue
129
# All entries should be of the same size. There should be one entry in
130
# queue for each entry in cache, and all refcounts should == 1
131
if not (len(self._queue) == len(self._cache) ==
132
len(self._refcount) == sum(self._refcount.itervalues())):
133
raise AssertionError()
135
def _remove(self, key):
136
"""Remove an entry, making sure to maintain the invariants."""
137
cleanup = self._cleanup.pop(key, None)
138
val = self._cache.pop(key)
139
if cleanup is not None:
176
# Move 'node' to the front of the queue
177
if self._most_recently_used is None:
178
assert len(self._cache) == 1
179
self._most_recently_used = node
180
self._last_recently_used = node
181
assert node.next == None
182
assert node.prev == None
184
elif node is self._most_recently_used:
185
# Nothing to do, this node is already at the head of the queue
187
elif node is self._last_recently_used:
188
assert self._last_recently_used is not None
189
self._last_recently_used = node.prev
190
assert self._last_recently_used is not None
191
# We've taken care of the tail pointer, remove the node, and insert it
194
if node.prev is not None:
195
node.prev.next = node.next
196
if node.next is not None:
197
node.next.prev = node.prev
199
node.next = self._most_recently_used
200
self._most_recently_used = node
201
node.next.prev = node
204
def _remove_node(self, node):
205
assert node is not None
206
if node is self._last_recently_used:
207
self._last_recently_used = node.prev
208
node2 = self._cache.pop(node.key)
211
# If we have removed all entries, remove the head pointer as well
212
if self._last_recently_used is None:
213
if len(self._cache) != 0:
214
import pdb; pdb.set_trace()
215
assert len(self._cache) == 0
216
assert self._most_recently_used is node
217
self._most_recently_used = None
219
# Shouldn't be necessary
143
223
def _remove_lru(self):
144
224
"""Remove one entry from the lru, and handle consequences.
221
291
:param cleanup: None or a function taking (key, value) to indicate
222
292
'value' sohuld be cleaned up.
224
if key in self._cache:
294
node = self._cache.get(key, None)
226
295
value_len = self._compute_size(value)
227
296
if value_len >= self._after_cleanup_size:
298
# The new value is too large, we won't be replacing the value,
299
# just removing the node completely
300
self._remove_node(node)
303
node = _LRUNode(key, value, cleanup=cleanup)
304
self._cache[key] = node
306
self._value_size -= node.size
307
node.size = value_len
229
308
self._value_size += value_len
230
self._cache[key] = value
231
if cleanup is not None:
232
self._cleanup[key] = cleanup
233
self._record_access(key)
309
self._record_access(node)
235
311
if self._value_size > self._max_size:
236
312
# Time to cleanup
246
322
while self._value_size > self._after_cleanup_size:
247
323
self._remove_lru()
249
def _remove(self, key):
250
"""Remove an entry, making sure to maintain the invariants."""
251
val = LRUCache._remove(self, key)
252
self._value_size -= self._compute_size(val)
325
def _remove_node(self, node):
326
self._value_size -= node.size
327
LRUCache._remove_node(self, node)
254
329
def resize(self, max_size, after_cleanup_size=None):
255
330
"""Change the number of bytes that will be cached."""