├── .gitignore ├── .travis.yml ├── ARC.js ├── ARC.test.js ├── Item.js ├── Item.test.js ├── LICENSE ├── List.js ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "iojs" 5 | -------------------------------------------------------------------------------- /ARC.js: -------------------------------------------------------------------------------- 1 | var List = require('./List') 2 | var Item = require('./Item') 3 | 4 | function ARC (c, mainStorage) { 5 | if (!(this instanceof ARC)) return new ARC(c, mainStorage) 6 | this.c = c 7 | this.p = 0 8 | this.mainStorage = mainStorage 9 | this.t1 = new List() 10 | this.b1 = new List() 11 | this.t2 = new List() 12 | this.b2 = new List() 13 | this.keyToItem = {} 14 | } 15 | 16 | ARC.prototype.request = function (key) { 17 | 18 | var item = this.keyToItem[key] 19 | 20 | if (!item) { 21 | item = new Item(key, null) 22 | this.keyToItem[key] = item 23 | } 24 | 25 | var delta 26 | 27 | switch (item.list) { 28 | case this.t1: 29 | case this.t2: 30 | // Case I 31 | item.prependTo(this.t2) 32 | break 33 | case this.b1: 34 | // Case II 35 | delta = this.b1.count >= this.b2.count ? 1 : this.b2.count / this.b1.count 36 | this.p = Math.min( 37 | this.p + delta, 38 | this.c 39 | ) 40 | item.value = this.mainStorage.get(key) 41 | this._replace(item) 42 | item.prependTo(this.t2) 43 | break 44 | case this.b2: 45 | // Case III 46 | delta = this.b2.count >= this.b1.count ? 1 : this.b1.count / this.b2.count 47 | this.p = Math.max( 48 | this.p - delta, 49 | 0 50 | ) 51 | item.value = this.mainStorage.get(key) 52 | this._replace(item) 53 | item.prependTo(this.t2) 54 | break 55 | default: 56 | // Case IV: Cache miss 57 | if (this.c === this.t1.count + this.b1.count) { 58 | // Case A 59 | if (this.t1.count < this.c) { 60 | if (this.b1.last) { 61 | delete this.keyToItem[this.b1.last.key] 62 | this.b1.last.remove() 63 | } 64 | this._replace(item) 65 | } else { 66 | if (this.t1.last) { 67 | delete this.keyToItem[this.t1.last.key] 68 | this.t1.last.remove() 69 | } 70 | } 71 | } else { 72 | // Case B 73 | if (this.t1.count + this.t2.count + this.b1.count + this.b2.count >= this.c) { 74 | if (this.t1.count + this.t2.count + this.b1.count + this.b2.count === 2 * this.c) { 75 | if (this.b2.last) { 76 | delete this.keyToItem[this.b2.last.key] 77 | this.b2.last.remove() 78 | } 79 | } 80 | this._replace(item) 81 | } 82 | } 83 | item.value = this.mainStorage.get(key) 84 | item.prependTo(this.t1) 85 | } 86 | 87 | return item.value 88 | } 89 | 90 | ARC.prototype._replace = function (item) { 91 | if ( 92 | this.t1.count > 0 && 93 | ( 94 | (this.t1.count > this.p) || 95 | (item.list === this.b2 && this.t1.count === this.p) 96 | ) 97 | ) { 98 | this.t1.last.value = null 99 | this.t1.last.prependTo(this.b1) 100 | } else { 101 | this.t2.last.value = null 102 | this.t2.last.prependTo(this.b2) 103 | } 104 | } 105 | 106 | module.exports = ARC 107 | -------------------------------------------------------------------------------- /ARC.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var ARC = require('./ARC') 3 | 4 | var storage = {} 5 | 6 | for (var i = 0; i < 100; i++) { 7 | storage['key: ' + i] = 'value: ' + i 8 | } 9 | 10 | var mainStorage = (function (storage) { 11 | return { 12 | get: function (key) { 13 | return storage[key] 14 | } 15 | } 16 | })(storage) 17 | 18 | test('ARC#request', function (t) { 19 | var arc = new ARC(3, mainStorage) 20 | 21 | t.equal(arc.request('key: ' + 1), 'value: ' + 1) 22 | 23 | var keys = Object.keys(storage) 24 | 25 | // Probalisitic test 26 | for (var i = 0; i < 100; i++) { 27 | var key = keys[~~(Math.random() * keys.length)] 28 | t.equal(arc.request(key), storage[key]) 29 | } 30 | 31 | t.end() 32 | }) 33 | -------------------------------------------------------------------------------- /Item.js: -------------------------------------------------------------------------------- 1 | function Item (key, value) { 2 | this.next = null 3 | this.prev = null 4 | this.list = null 5 | 6 | this.key = key 7 | this.value = value 8 | } 9 | 10 | Item.prototype.remove = function () { 11 | if (!this.list) return 12 | this.list.count-- 13 | 14 | if (this.prev) { 15 | this.prev.next = this.next 16 | } 17 | 18 | if (this.next) { 19 | this.next.prev = this.prev 20 | } 21 | 22 | if (this.list.first === this) { 23 | this.list.first = this.next 24 | } 25 | 26 | if (this.list.last === this) { 27 | this.list.last = this.prev 28 | } 29 | 30 | this.prev = null 31 | this.list = null 32 | } 33 | 34 | Item.prototype.appendTo = function (list) { 35 | this.remove() 36 | 37 | if (list.last) { 38 | this.prev = list.last 39 | this.prev.next = this 40 | } 41 | 42 | this.list = list 43 | this.list.count++ 44 | 45 | list.last = this 46 | list.first = list.first || this 47 | } 48 | 49 | Item.prototype.prependTo = function (list) { 50 | this.remove() 51 | 52 | if (list.first) { 53 | this.next = list.first 54 | this.next.prev = this 55 | } 56 | 57 | this.list = list 58 | this.list.count++ 59 | 60 | list.first = this 61 | list.last = list.last || this 62 | } 63 | 64 | module.exports = Item 65 | -------------------------------------------------------------------------------- /Item.test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var List = require('./List') 3 | var Item = require('./Item') 4 | 5 | test('Item#appendTo', function (t) { 6 | var list = new List() 7 | 8 | var a = new Item('a-key', { a: 'value' }) 9 | var b = new Item('b-key', { b: 'value' }) 10 | var c = new Item('c-key', { c: 'value' }) 11 | 12 | a.appendTo(list) 13 | 14 | t.equal(list.first, a) 15 | t.equal(list.last, a) 16 | 17 | t.equal(a.prev, null) 18 | t.equal(a.next, null) 19 | 20 | b.appendTo(list) 21 | 22 | t.equal(list.last, b) 23 | t.equal(list.first, a) 24 | 25 | t.equal(a.next, b) 26 | t.equal(a.prev, null) 27 | 28 | t.equal(b.prev, a) 29 | t.equal(b.next, null) 30 | 31 | c.appendTo(list) 32 | 33 | t.equal(list.last, c) 34 | t.equal(list.first, a) 35 | 36 | t.end() 37 | }) 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Alexander Gugel 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /List.js: -------------------------------------------------------------------------------- 1 | function List () { 2 | this.first = null 3 | this.last = null 4 | this.count = 0 5 | } 6 | 7 | module.exports = List 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://img.shields.io/badge/stability-experimental-orange.svg?style=flat) 2 | [![Build Status](https://travis-ci.org/alexanderGugel/arc-js.svg?branch=master)](https://travis-ci.org/alexanderGugel/arc-js) 3 | [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://github.com/feross/standard) 4 | 5 | arc-js 6 | ====== 7 | 8 | An [Adaptive Replacement Cache (ARC)](http://web.archive.org/web/20150405221102/https://www.usenix.org/legacy/event/fast03/tech/full_papers/megiddo/megiddo.pdf) written in JavaScript. 9 | 10 | Looking for the Go version? [alexanderGugel/arc](https://github.com/alexanderGugel/arc) 11 | 12 | Overview 13 | -------- 14 | 15 | This project implements "ARC", a self-tuning, low overhead replacement cache. The goal of this project is to follow the algorithm outlined in the linked research paper as closely as possible. ARC uses a learning rule to adaptively and continually revise its assumptions about the workload in order to adjust the internal LRU and LFU cache sizes. 16 | 17 | This implementation is based on Nimrod Megiddo and Dharmendra S. Modha's ["ARC: A SELF-TUNING, LOW OVERHEAD REPLACEMENT CACHE"](http://web.archive.org/web/20150405221102/https://www.usenix.org/legacy/event/fast03/tech/full_papers/megiddo/megiddo.pdf), while definitely useable, this is still an experiment and shouldn't be considered production-ready. 18 | 19 | ``` 20 | <------- cache size c ------> 21 | +-----------------+---------- 22 | | LFU | LRU | 23 | +-----------------+---------- 24 | ^ 25 | | 26 | p (dynamically adjusted by learning rule) 27 | 28 | B1 [...] 29 | B2 [...] 30 | ``` 31 | 32 | The cache is implemented using two internal caching systems L1 and L2. The cache size c defines the maximum number of entries stored (excluding ghost entries). Ghost entries are being stored in two "ghost registries" B1 and B1. Ghost entries no longer have a value associated with them. 33 | 34 | Ghost entries are being used in order to keep track of expelled pages. They no longer have a value associated with them, but can be promoted into the internal LRU cache. 35 | 36 | Frequently requested pages are being promoted into the LFU. 37 | 38 | Install 39 | ------- 40 | 41 | With [npm](https://www.npmjs.org/) do: 42 | 43 | ``` 44 | npm i arc-js 45 | ``` 46 | 47 | Usage 48 | ----- 49 | 50 | ```js 51 | var mainStorage = { 52 | get: function (key) { 53 | // User specific logic (FS/ DB interaction) 54 | return value 55 | } 56 | } 57 | 58 | // c = 10: Cache size 59 | // mainStorage = slow storage medium 60 | var arc = new ARC(10, mainStorage) 61 | 62 | // Retrieves my_key from cache (uses the mainStorage if item is not cached) 63 | var value = arc.request('my_key') 64 | ``` 65 | 66 | TODO 67 | ---- 68 | 69 | - [ ] More tests (remove randomized tests) 70 | - [ ] Careful code review 71 | - [ ] Review API 72 | 73 | License 74 | ------- 75 | 76 | See `LICENSE` file. 77 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./ARC') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arc-js", 3 | "version": "0.0.2", 4 | "description": "An Adaptive Replacement Cache (ARC) written in JavaScript.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "standard && tape *.test.js", 8 | "dev": "hihat index.js" 9 | }, 10 | "author": "alexander.gugel@gmail.com", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "hihat": "^1.2.1", 14 | "standard": "^4.5.4", 15 | "tape": "^4.0.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/alexanderGugel/arc-js.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/alexanderGugel/arc-js/issues" 23 | }, 24 | "homepage": "https://github.com/alexanderGugel/arc-js#readme" 25 | } 26 | --------------------------------------------------------------------------------