Skip to content

Commit

Permalink
use Map for performance
Browse files Browse the repository at this point in the history
  • Loading branch information
aheckmann authored and isaacs committed Nov 27, 2015
1 parent 2920481 commit 0d29f9c
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 70 deletions.
18 changes: 0 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,6 @@ If you put more stuff in it, then items will fall out.
If you try to put an oversized thing in it, then it'll fall out right
away.

## Keys should always be Strings or Numbers

Note: this module will print warnings to `console.error` if you use a
key that is not a String or Number. Because items are stored in an
object, which coerces keys to a string, it won't go well for you if
you try to use a key that is not a unique string, it'll cause surprise
collisions. For example:

```JavaScript
// Bad Example! Dont' do this!
var cache = LRU()
var a = {}
var b = {}
cache.set(a, 'this is a')
cache.set(b, 'this is b')
console.log(cache.get(a)) // prints: 'this is b'
```

## Options

* `max` The maximum size of the cache, checked by applying the length
Expand Down
108 changes: 59 additions & 49 deletions lib/lru-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,11 @@ if (typeof module === 'object' && module.exports) {
this.LRUCache = LRUCache
}

function hOP (obj, key) {
return Object.prototype.hasOwnProperty.call(obj, key)
}

function naiveLength () { return 1 }

var didTypeWarning = false
function typeCheckKey(key) {
if (!didTypeWarning && typeof key !== 'string' && typeof key !== 'number') {
didTypeWarning = true
console.error(new TypeError("LRU: key must be a string or number. Almost certainly a bug! " + typeof key).stack)
}
if (typeof key !== 'string' && typeof key !== 'number')
throw new TypeError("key must be a string or number. " + typeof key)
}

function LRUCache (options) {
Expand Down Expand Up @@ -63,15 +56,15 @@ Object.defineProperty(LRUCache.prototype, "lengthCalculator",
if (typeof lC !== "function") {
this._lengthCalculator = naiveLength
this._length = this._itemCount
for (var key in this._cache) {
this._cache[key].length = 1
for (var value of this._cache.values()) {
value.length = 1
}
} else {
this._lengthCalculator = lC
this._length = 0
for (var key in this._cache) {
this._cache[key].length = this._lengthCalculator(this._cache[key].value)
this._length += this._cache[key].length
for (var value of this._cache.values()) {
value.length = this._lengthCalculator(value.value)
this._length += value.length
}
}

Expand All @@ -97,9 +90,9 @@ LRUCache.prototype.forEach = function (fn, thisp) {
var i = 0
var itemCount = this._itemCount

for (var k = this._mru - 1; k >= 0 && i < itemCount; k--) if (this._lruList[k]) {
for (var k = this._mru - 1; k >= 0 && i < itemCount; k--) if (this._lruList.has(k)) {
i++
var hit = this._lruList[k]
var hit = this._lruList.get(k)
if (isStale(this, hit)) {
del(this, hit)
if (!this._allowStale) hit = undefined
Expand All @@ -113,8 +106,8 @@ LRUCache.prototype.forEach = function (fn, thisp) {
LRUCache.prototype.keys = function () {
var keys = new Array(this._itemCount)
var i = 0
for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) {
var hit = this._lruList[k]
for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList.has(k)) {
var hit = this._lruList.get(k)
keys[i++] = hit.key
}
return keys
Expand All @@ -123,22 +116,22 @@ LRUCache.prototype.keys = function () {
LRUCache.prototype.values = function () {
var values = new Array(this._itemCount)
var i = 0
for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) {
var hit = this._lruList[k]
for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList.has(k)) {
var hit = this._lruList.get(k)
values[i++] = hit.value
}
return values
}

LRUCache.prototype.reset = function () {
if (this._dispose && this._cache) {
for (var k in this._cache) {
this._dispose(k, this._cache[k].value)
for (var entry of this._cache) {
this._dispose(entry[0], entry[1].value)
}
}

this._cache = Object.create(null) // hash of items by key
this._lruList = Object.create(null) // list of items in order of use recency
this._cache = new Map() // hash of items by key
this._lruList = new Map() // list of items in order of use recency
this._mru = 0 // most recently used
this._lru = 0 // least recently used
this._length = 0 // number of items in the list
Expand All @@ -149,8 +142,8 @@ LRUCache.prototype.dump = function () {
var arr = []
var i = 0

for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList[k]) {
var hit = this._lruList[k]
for (var k = this._mru - 1; k >= 0 && i < this._itemCount; k--) if (this._lruList.has(k)) {
var hit = this._lruList.get(k)
if (!isStale(this, hit)) {
//Do not store staled hits
++i
Expand All @@ -170,26 +163,33 @@ LRUCache.prototype.dumpLru = function () {
}

LRUCache.prototype.set = function (key, value, maxAge) {
// Map allows any type of `key` (objects, number, etc). For backwards
// compatibility, coerce to a string.
key = String(key)

maxAge = maxAge || this._maxAge
typeCheckKey(key)

var now = maxAge ? Date.now() : 0
var len = this._lengthCalculator(value)

if (hOP(this._cache, key)) {
if (this._cache.has(key)) {
if (len > this._max) {
del(this, this._cache[key])
del(this, this._cache.get(key))
return false
}

var item = this._cache.get(key)

// dispose of the old one before overwriting
if (this._dispose)
this._dispose(key, this._cache[key].value)
this._dispose(key, item.value)

this._cache[key].now = now
this._cache[key].maxAge = maxAge
this._cache[key].value = value
this._length += (len - this._cache[key].length)
this._cache[key].length = len
item.now = now
item.maxAge = maxAge
item.value = value
this._length += (len - item.length)
item.length = len
this.get(key)

if (this._length > this._max)
Expand All @@ -207,7 +207,8 @@ LRUCache.prototype.set = function (key, value, maxAge) {
}

this._length += hit.length
this._lruList[hit.lu] = this._cache[key] = hit
this._cache.set(key, hit)
this._lruList.set(hit.lu, hit)
this._itemCount ++

if (this._length > this._max)
Expand All @@ -217,34 +218,39 @@ LRUCache.prototype.set = function (key, value, maxAge) {
}

LRUCache.prototype.has = function (key) {
typeCheckKey(key)
if (!hOP(this._cache, key)) return false
var hit = this._cache[key]
if (typeof key !== 'string' && typeof key !== 'number')
return false

if (!this._cache.has(key)) return false
var hit = this._cache.get(key)
if (isStale(this, hit)) {
return false
}
return true
}

LRUCache.prototype.get = function (key) {
typeCheckKey(key)
if (typeof key !== 'string' && typeof key !== 'number')
return
return get(this, key, true)
}

LRUCache.prototype.peek = function (key) {
typeCheckKey(key)
if (typeof key !== 'string' && typeof key !== 'number')
return
return get(this, key, false)
}

LRUCache.prototype.pop = function () {
var hit = this._lruList[this._lru]
var hit = this._lruList.get(this._lru)
del(this, hit)
return hit || null
}

LRUCache.prototype.del = function (key) {
typeCheckKey(key)
del(this, this._cache[key])
if (typeof key !== 'string' && typeof key !== 'number')
return
del(this, this._cache.get(key))
}

LRUCache.prototype.load = function (arr) {
Expand All @@ -270,7 +276,11 @@ LRUCache.prototype.load = function (arr) {

function get (self, key, doUse) {
typeCheckKey(key)
var hit = self._cache[key]
// Map allows any type of `key` (objects, number, etc). For backwards
// compatibility, coerce to a string.
key = String(key)

var hit = self._cache.get(key)
if (hit) {
if (isStale(self, hit)) {
del(self, hit)
Expand Down Expand Up @@ -298,25 +308,25 @@ function isStale(self, hit) {
function use (self, hit) {
shiftLU(self, hit)
hit.lu = self._mru ++
self._lruList[hit.lu] = hit
self._lruList.set(hit.lu, hit)
}

function trim (self) {
while (self._lru < self._mru && self._length > self._max)
del(self, self._lruList[self._lru])
del(self, self._lruList.get(self._lru))
}

function shiftLU (self, hit) {
delete self._lruList[ hit.lu ]
while (self._lru < self._mru && !self._lruList[self._lru]) self._lru ++
self._lruList.delete(hit.lu)
while (self._lru < self._mru && !self._lruList.has(self._lru)) self._lru ++
}

function del (self, hit) {
if (hit) {
if (self._dispose) self._dispose(hit.key, hit.value)
self._length -= hit.length
self._itemCount --
delete self._cache[ hit.key ]
self._cache.delete(hit.key)
shiftLU(self, hit)
}
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lru-cache",
"description": "A cache object that deletes the least-recently-used items.",
"version": "2.7.3",
"version": "2.7.2",
"author": "Isaac Z. Schlueter <i@izs.me>",
"keywords": [
"mru",
Expand Down
64 changes: 62 additions & 2 deletions test/basic.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ test("drop the old items", function(t) {
}, 155)
})

test("individual item can have it's own maxAge", function(t) {
test("individual item can have its own maxAge", function(t) {
var cache = new LRU({
max: 5,
maxAge: 50
Expand All @@ -242,7 +242,7 @@ test("individual item can have it's own maxAge", function(t) {
}, 25)
})

test("individual item can have it's own maxAge > cache's", function(t) {
test("individual item can have its own maxAge > cache's", function(t) {
var cache = new LRU({
max: 5,
maxAge: 20
Expand Down Expand Up @@ -394,3 +394,63 @@ test("pop the least used item", function (t) {

t.end()
})

test("get and set only accepts strings and numbers as keys", function(t) {
var cache = new LRU()

cache.set("key", "value")
cache.set(123, 456)

t.equal(cache.get("key"), "value")
t.equal(cache.get(123), 456)

t.throws(function() {
cache.set({ someObjectKey: true }, "a")
}, "set should not accept objects as keys")

t.throws(function() {
cache.set([1,2,3], "b")
}, "set should not accept arrays as keys")

t.end()
})

test("peek only accepts strings and numbers as keys", function(t) {
var cache = new LRU()

cache.set("key", "value")
cache.set(123, 456)

t.equal(cache.peek("key"), "value")
t.equal(cache.peek(123), 456)
t.end()
})

test("del only accepts strings and numbers as keys", function(t) {
var cache = new LRU()

cache.set("key", "value")
cache.set(123, 456)

cache.del("key")
cache.del(123)

t.assertNot(cache.has("key"))
t.assertNot(cache.has(123))

cache.set('[object Object]', 123)
t.assertNot(cache.has({}))
t.assert(cache.has(String({})))

t.end()
})


test("has only accepts strings and numbers as keys", function(t) {
var cache = new LRU()

cache.has("key")
cache.has(123)

t.end()
})

0 comments on commit 0d29f9c

Please sign in to comment.