shields/lib/lru-cache.js
Thaddee Tyl 5dd58142cb Fix LRU cache index stalling
The old LRU implementation stored a list's indices to reference items in that
list, but deletions from the list made indices point to the wrong slot.

Functionally, it meant that deleted slots were not guaranteed to be the oldest
slot.

Using a linked list fixes that.
2017-03-26 23:00:31 +02:00

125 lines
3.2 KiB
JavaScript

// In-memory KV, remove the oldest data when the capacity is reached.
var typeEnum = {
unit: 0,
heap: 1,
};
function CacheSlot(key, value) {
this.key = key;
this.value = value;
this.older = null; // Newest slot that is older than this slot.
this.newer = null; // Oldest slot that is newer than this slot.
}
function Cache(capacity, type) {
if (!(this instanceof Cache)) { return new Cache(capacity, type); }
type = type || 'unit';
this.capacity = capacity;
this.type = typeEnum[type];
this.cache = new Map(); // Maps cache keys to CacheSlots.
this.newest = null; // Newest slot in the cache.
this.oldest = null;
}
Cache.prototype = {
set: function addToCache(cacheKey, cached) {
var slot = this.cache.get(cacheKey);
if (slot === undefined) {
slot = new CacheSlot(cacheKey, cached);
this.cache.set(cacheKey, slot);
}
this.makeNewest(slot);
var numItemsToRemove = this.limitReached();
if (numItemsToRemove > 0) {
for (var i = 0; i < numItemsToRemove; i++) {
this.removeOldest();
}
}
},
get: function getFromCache(cacheKey) {
var slot = this.cache.get(cacheKey);
if (slot !== undefined) {
this.makeNewest(slot);
return slot.value;
}
},
has: function hasInCache(cacheKey) {
return this.cache.has(cacheKey);
},
makeNewest: function makeNewestSlot(slot) {
var previousNewest = this.newest;
if (previousNewest === slot) { return; }
var older = slot.older;
var newer = slot.newer;
if (older !== null) {
older.newer = newer;
} else if (newer !== null) {
this.oldest = newer;
}
if (newer !== null) {
newer.older = older;
}
this.newest = slot;
if (previousNewest !== null) {
slot.older = previousNewest;
slot.newer = null;
previousNewest.newer = slot;
} else {
// If previousNewest is null, the cache used to be empty.
this.oldest = slot;
}
},
removeOldest: function removeOldest() {
var cacheKey = this.oldest.key;
if (this.oldest !== null) {
this.oldest = this.oldest.newer;
if (this.oldest !== null) {
this.oldest.older = null;
}
}
this.cache.delete(cacheKey);
},
// Returns the number of elements to remove if we're past the limit.
limitReached: function heuristic() {
if (this.type === typeEnum.unit) {
// Remove the excess.
return Math.max(0, (this.cache.size - this.capacity));
} else if (this.type === typeEnum.heap) {
if (getHeapSize() >= this.capacity) {
console.log('LRU HEURISTIC heap:', getHeapSize());
// Remove half of them.
return (this.cache.size >> 1);
} else { return 0; }
} else {
console.error("Unknown heuristic '" + this.type + "' for LRU cache.");
return 1;
}
},
};
// In bytes.
var heapSize;
var heapSizeTimeout;
function getHeapSize() {
if (heapSizeTimeout == null) {
// Compute the heap size every 60 seconds.
heapSizeTimeout = setInterval(computeHeapSize, 60 * 1000);
return computeHeapSize();
} else {
return heapSize;
}
}
function computeHeapSize() {
return heapSize = process.memoryUsage().heapTotal;
}
module.exports = Cache;