├── test ├── standard.js └── decay.js ├── package.json ├── .gitignore ├── LICENSE ├── README.md └── index.js /test/standard.js: -------------------------------------------------------------------------------- 1 | var LFU = require('../'); 2 | var assert = require('assert'); 3 | 4 | describe("standard mode", function suite() { 5 | it("should evict first added item", function(done) { 6 | var lfu = LFU(2); 7 | lfu.on('eviction', function(key, obj) { 8 | assert.equal("test", key); 9 | assert.equal("val", obj && obj.my); 10 | done(); 11 | }); 12 | lfu.set('test', {my: "val"}); 13 | lfu.set('test2', {my: "val2"}); 14 | lfu.get('test2'); 15 | lfu.set('test3', {my: "val3"}); 16 | }); 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lfu-cache", 3 | "version": "0.1.0", 4 | "description": "Least Frequently Used cache for Node.js", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/kapouer/node-lfu-cache.git" 12 | }, 13 | "keywords": [ 14 | "LFU" 15 | ], 16 | "author": "Jérémy Lal ", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/kapouer/node-lfu-cache/issues" 20 | }, 21 | "devDependencies": { 22 | "mocha": "^2.1.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jérémy Lal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lfu-cache for Node.js 2 | ===================== 3 | 4 | This module implements an O(1) Least Frequently Used cache, as described 5 | in the paper "An O(1) algorithm for implementing the LFU cache eviction scheme" 6 | by K. Shah, A. Mitra and D. Matani. 7 | 8 | It also features a "decay" parameter (default null, no decay) that penalizes 9 | objects that have not been accessed after `Date.now() - decay`, by halving their 10 | frequency every 'decay' milliseconds. 11 | 12 | Important: decay happens at most every `decay` milliseconds, and it's a costly 13 | operation, so this parameter should not be too small (a minute is good). 14 | 15 | The cache emits an "eviction" event with parameters (key, value), when 16 | a least frequently used element has been evicted to make room for the cache. 17 | 18 | Usage 19 | ----- 20 | 21 | ``` 22 | 23 | var lfu = require('lfu-cache')( 24 | 2, // max size of the cache 25 | 60000 // decay of the entries in milliseconds 26 | ); 27 | 28 | lfu.on('eviction', function(key, obj) { 29 | console.log("test was evicted", key, obj); 30 | }); 31 | lfu.set('test', {my: "val"}); 32 | lfu.set('test2', {my: "val2"}); 33 | lfu.get('test2'); 34 | lfu.set('test3', {my: "val2"}); 35 | 36 | ``` 37 | 38 | -------------------------------------------------------------------------------- /test/decay.js: -------------------------------------------------------------------------------- 1 | var LFU = require('../'); 2 | var assert = require('assert'); 3 | 4 | describe("decay mode", function suite() { 5 | it("should evict least frequently accessed item and decay non-accessed items (2 items)", function(done) { 6 | var decay = 100; 7 | this.timeout(2 * decay); 8 | var lfu = LFU(2, decay); 9 | lfu.on('eviction', function(key, obj) { 10 | assert.equal("perempting", key); 11 | done(); 12 | }); 13 | // access now test1 one time 14 | lfu.set('eventually', 'me'); 15 | setTimeout(function() { 16 | // access before decay test1 one time 17 | lfu.get('eventually'); 18 | lfu.get('eventually'); 19 | }, decay / 2); 20 | lfu.set('perempting', {my: "val2"}); 21 | // access now test2 three times 22 | lfu.get('perempting'); 23 | lfu.get('perempting'); 24 | lfu.get('perempting'); 25 | setTimeout(function() { 26 | // decay should have happened, test2 is going to have Math.floor(frequentation / 2) = 1 27 | // and test1 keeps frequentation at 2 so test2 will be evicted besides it be accessed 28 | // three times and test1 accessed only two times 29 | lfu.set('evicter', 'stuff'); 30 | }, decay + 1); 31 | }); 32 | it("should evict least frequently accessed item and decay non-accessed items (3 items)", function(done) { 33 | var decay = 100; 34 | this.timeout(2 * decay); 35 | var lfu = LFU(3, decay); 36 | lfu.on('eviction', function(key, obj) { 37 | assert.equal("last", key); 38 | done(); 39 | }); 40 | // access now test1 one time 41 | lfu.set('eventually', 'me'); 42 | setTimeout(function() { 43 | // access before decay test1 one time 44 | lfu.get('eventually'); 45 | lfu.get('eventually'); 46 | }, decay / 2); 47 | lfu.set('perempting', {my: "val2"}); 48 | // access now test2 three times 49 | lfu.get('perempting'); 50 | lfu.get('perempting'); 51 | lfu.get('perempting'); 52 | setTimeout(function() { 53 | lfu.set('last', 'one'); 54 | }, decay / 3); 55 | setTimeout(function() { 56 | // decay should have happened, test2 is going to have Math.floor(frequentation / 2) = 1 57 | // and test1 keeps frequentation at 2 so test2 will be evicted besides it be accessed 58 | // three times and test1 accessed only two times 59 | lfu.set('evicter', 'stuff'); 60 | }, decay + 1); 61 | }); 62 | }); 63 | 64 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var events = require('events'); 2 | var util = require('util'); 3 | 4 | module.exports = LFU; 5 | 6 | function LFU(size, halflife) { 7 | if (!(this instanceof LFU)) return new LFU(size, halflife); 8 | this.size = size || 10; 9 | this.halflife = halflife || null; 10 | this.cache = {}; 11 | this.head = freq(); 12 | this.length = 0; 13 | this.lastDecay = Date.now(); 14 | } 15 | util.inherits(LFU, events.EventEmitter); 16 | 17 | LFU.prototype.get = function(key) { 18 | var el = this.cache[key]; 19 | if (!el) return; 20 | var cur = el.parent; 21 | var next = cur.next; 22 | if (!next || next.weight != cur.weight + 1) { 23 | next = node(cur.weight + 1, cur, next); 24 | } 25 | this.removeFromParent(el.parent, key); 26 | next.items.add(key); 27 | el.parent = next; 28 | var now = Date.now(); 29 | el.atime = now; 30 | if (this.halflife && now - this.lastDecay >= this.halflife) this.decay(); 31 | this.atime = now; 32 | return el.data; 33 | }; 34 | 35 | LFU.prototype.decay = function() { 36 | // iterate over all entries and move the ones that have 37 | // this.atime - el.atime > this.halflife 38 | // to lower freq nodes 39 | // the idea is that if there is 10 hits / minute, and a minute gap, 40 | var now = Date.now(); 41 | this.lastDecay = now; 42 | var diff = now - this.halflife; 43 | var halflife = this.halflife; 44 | var el, weight, cur, prev; 45 | for (var key in this.cache) { 46 | el = this.cache[key]; 47 | if (diff > el.atime) { 48 | // decay that one 49 | // 1) find freq 50 | cur = el.parent; 51 | weight = Math.round(cur.weight / 2); 52 | if (weight == 1) continue; 53 | prev = cur.prev; 54 | while (prev) { 55 | if (prev.weight <= weight) break; 56 | cur = prev; 57 | prev = prev.prev; 58 | } 59 | if (!prev || !cur) { 60 | throw new Error("Empty before and after halved weight - please report"); 61 | } 62 | // 2) either prev has the right weight, or we must insert a freq with 63 | // the right weight 64 | if (prev.weight < weight) { 65 | prev = node(weight, prev, cur); 66 | } 67 | this.removeFromParent(el.parent, key); 68 | el.parent = prev; 69 | prev.items.add(key); 70 | } 71 | } 72 | }; 73 | 74 | LFU.prototype.set = function(key, obj) { 75 | var el = this.cache[key]; 76 | if (el) { 77 | el.data = obj; 78 | return; 79 | } 80 | var now = Date.now(); 81 | if (this.halflife && now - this.lastDecay >= this.halflife) { 82 | this.decay(); 83 | } 84 | if (this.length == this.size) { 85 | this.evict(); 86 | } 87 | this.length++; 88 | var cur = this.head.next; 89 | if (!cur || cur.weight != 1) { 90 | cur = node(1, this.head, cur); 91 | } 92 | cur.items.add(key); 93 | this.cache[key] = { 94 | data: obj, 95 | atime: now, 96 | parent: cur 97 | }; 98 | }; 99 | 100 | LFU.prototype.remove = function(key) { 101 | var el = this.cache[key]; 102 | if (!el) return; 103 | delete this.cache[key]; 104 | this.removeFromParent(el.parent, key); 105 | this.length--; 106 | return el.data; 107 | }; 108 | 109 | LFU.prototype.removeFromParent = function(parent, key) { 110 | parent.items.remove(key); 111 | if (parent.items.length == 0) { 112 | parent.prev.next = parent.next; 113 | if (parent.next) parent.next.prev = parent; 114 | } 115 | }; 116 | 117 | LFU.prototype.evict = function() { 118 | var least = this.head.next && this.head.next.items.first(); 119 | if (least) { 120 | this.emit('eviction', least, this.remove(least)); 121 | } else { 122 | throw new Error("Cannot find an element to evict - please report issue"); 123 | } 124 | }; 125 | 126 | function freq() { 127 | return { 128 | weight: 0, 129 | items: new Set() 130 | } 131 | } 132 | function item(obj, parent) { 133 | return { 134 | obj: obj, 135 | parent: parent 136 | }; 137 | } 138 | function node(weight, prev, next) { 139 | var node = freq(); 140 | node.weight = weight; 141 | node.prev = prev; 142 | node.next = next; 143 | prev.next = node; 144 | if (next) next.prev = node; 145 | return node; 146 | } 147 | 148 | function Set() { 149 | this.hash = {}; 150 | this.head = {}; 151 | this.length = 0; 152 | } 153 | 154 | Set.prototype.add = function(str) { 155 | if (this.hash[str]) return; 156 | var item = { 157 | next: this.head.next, 158 | prev: this.head, 159 | val: str 160 | }; 161 | var next = this.head.next; 162 | this.head.next = item; 163 | if (next) next.prev = item; 164 | this.hash[str] = item; 165 | this.length++; 166 | }; 167 | 168 | Set.prototype.remove = function(str) { 169 | var item = this.hash[str]; 170 | if (!item) return; 171 | delete this.hash[str]; 172 | item.prev.next = item.next; 173 | if (item.next) item.next.prev = item.prev; 174 | this.length--; 175 | }; 176 | 177 | Set.prototype.first = function() { 178 | return this.head.next && this.head.next.val; 179 | }; 180 | --------------------------------------------------------------------------------