From 07b98f1b4e64633788ea2b137a8b0ce5035479b8 Mon Sep 17 00:00:00 2001 From: jeet-dhandha Date: Fri, 31 Jul 2026 10:29:48 +0530 Subject: [PATCH] perf: avoid reordering the range cache on every hit --- internal/lrucache.js | 38 ++++++++++++++++------------ test/internal/lrucache.js | 53 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 18 deletions(-) diff --git a/internal/lrucache.js b/internal/lrucache.js index b8bf5262..1636905d 100644 --- a/internal/lrucache.js +++ b/internal/lrucache.js @@ -1,40 +1,46 @@ 'use strict' +// Two generations, with no reordering on read. A hit in the old generation is +// promoted back into the live one. When the live generation fills up it becomes +// the old one and the previous old generation is dropped, so at most 2 * max +// entries are retained. class LRUCache { constructor () { this.max = 1000 this.map = new Map() + this.old = new Map() } get (key) { const value = this.map.get(key) - if (value === undefined) { - return undefined - } else { - // Remove the key from the map and add it to the end - this.map.delete(key) - this.map.set(key, value) + if (value !== undefined) { return value } + const stale = this.old.get(key) + if (stale !== undefined) { + this.set(key, stale) + return stale + } + return undefined } delete (key) { - return this.map.delete(key) + const inMap = this.map.delete(key) + const inOld = this.old.delete(key) + return inMap || inOld } set (key, value) { - const deleted = this.delete(key) - - if (!deleted && value !== undefined) { - // If cache is full, delete the least recently used item - if (this.map.size >= this.max) { - const firstKey = this.map.keys().next().value - this.delete(firstKey) - } + if (value === undefined) { + return this + } - this.map.set(key, value) + if (!this.map.has(key) && this.map.size >= this.max) { + this.old = this.map + this.map = new Map() } + this.map.set(key, value) return this } } diff --git a/test/internal/lrucache.js b/test/internal/lrucache.js index 7eb225d6..918c7ad3 100644 --- a/test/internal/lrucache.js +++ b/test/internal/lrucache.js @@ -14,8 +14,57 @@ test('basic cache operation', t => { t.equal(c.get(i), i) } c.set(1001, 1001) - // lru item should be gone + t.equal(c.get(1001), 1001) + // setting undefined is a no-op and does not clobber an existing value + t.equal(c.set(42, undefined), c) + t.equal(c.get(42), 42) + t.equal(c.get('not-in-the-cache'), undefined) + t.end() +}) + +test('delete removes the key from both generations', t => { + const c = new LRUCache() + const max = 1000 + + for (let i = 0; i < max; i++) { + c.set(i, i) + } + // fills the live generation, demoting everything above into the old one + c.set('rotate', 1) + c.set(7, 'live') + + t.equal(c.delete(7), true) + t.equal(c.get(7), undefined) + t.equal(c.delete(7), false) + t.end() +}) + +test('entries are evicted once the cache is full', t => { + const c = new LRUCache() + + for (let i = 0; i < 10 * c.max; i++) { + c.set(i, i) + } t.equal(c.get(0), undefined) - c.set(42, undefined) + t.ok(c.map.size + c.old.size <= 2 * c.max) + t.end() +}) + +test('promoting from the old generation stays within the bound', t => { + const c = new LRUCache() + const max = c.max + + for (let i = 0; i < max; i++) { + c.set(`a${i}`, i) + } + c.set('rotate', 1) + for (let i = 0; i < max - 2; i++) { + c.set(`b${i}`, i) + } + for (let i = 0; i < max; i++) { + c.get(`a${i}`) + } + + t.ok(c.map.size + c.old.size <= 2 * max) t.end() })