Skip to content

Commit

Permalink
debug: support max Age
Browse files Browse the repository at this point in the history
  • Loading branch information
wellwelwel committed Sep 20, 2024
1 parent 700ac33 commit 1d8d76e
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 47 deletions.
17 changes: 11 additions & 6 deletions benchmark/worker.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
const process = require('node:process');
const { cpuUsage } = require('node:process');
const { performance } = require('node:perf_hooks');
const { LRUCache } = require('lru-cache');
const { createLRU } = require('../lib/index.js');
Expand All @@ -8,26 +7,32 @@ const benchmarkName = process.argv[2];

const measurePerformance = (fn) => {
const startTime = performance.now();
const startCpu = cpuUsage();
const startCpu = process.cpuUsage();

fn();

const endTime = performance.now();
const endCpu = cpuUsage(startCpu);
const endCpu = process.cpuUsage(startCpu);

return {
time: endTime - startTime,
cpu: endCpu.user + endCpu.system,
};
};

const times = 10;
const times = 100;
const max = 100000;
const brute = 1000000;

const benchmarks = {
'lru-cache': () => new LRUCache({ max }),
'lru.min': () => createLRU({ max }),
'lru-cache': () => {
let event = 0;
return new LRUCache({ max, ttl: 1000, dispose: () => event++ });
},
'lru.min': () => {
let event = 0;
return createLRU({ max, maxAge: 1000, onEviction: () => event++ });
},
};

const benchmark = (createCache) => {
Expand Down
23 changes: 18 additions & 5 deletions benchmark/worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,27 @@ const measurePerformance = (fn) => {
};
};

const times = 10;
const times = 100;
const max = 100000;
const brute = 1000000;

const benchmarks = {
'lru-cache': () => new LRUCache({ max }),
'quick-lru': () => new QuickLRU({ maxSize: max }),
'lru.min': () => createLRU({ max }),
'lru-cache': () => {
let event = 0;
return new LRUCache({ max, ttl: 1000, dispose: () => event++ });
},
'quick-lru': () => {
let event = 0;
return new QuickLRU({
maxSize: max,
maxAge: 1000,
onEviction: () => event++,
});
},
'lru.min': () => {
let event = 0;
return createLRU({ max, maxAge: 1000, onEviction: () => event++ });
},
};

const benchmark = (createCache) => {
Expand Down Expand Up @@ -57,7 +70,7 @@ const benchmark = (createCache) => {
results.time += result.time;
results.cpu += result.cpu;

cache.clear();
// cache.clear(); // quick-lru doesn't evict on `clear`
}

return {
Expand Down
144 changes: 112 additions & 32 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { performance } from 'node:perf_hooks';

export type CacheOptions<Key = unknown, Value = unknown> = {
/** Maximum number of items the cache can hold. */
max: number;
maxAge?: number;
/** Function called when an item is evicted from the cache. */
onEviction?: (key: Key, value: Value) => unknown;
};

export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
let { max, onEviction } = options;
let { max, onEviction, maxAge = undefined } = options;

if (!(Number.isInteger(max) && max > 0))
throw new TypeError('`max` must be a positive integer');

if (typeof maxAge === 'number' && maxAge <= 0)
throw new TypeError('`maxAge` must be a positive number');

const Age = typeof performance?.now === 'object' ? performance : Date;

let size = 0;
let head = 0;
let tail = 0;
Expand All @@ -19,6 +27,8 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
const keyMap: Map<Key, number> = new Map();
const keyList: (Key | undefined)[] = new Array(max).fill(undefined);
const valList: (Value | undefined)[] = new Array(max).fill(undefined);
const expList: number[] = new Array(max).fill(0);
const ageList: number[] = new Array(max).fill(0);
const next: number[] = new Array(max).fill(0);
const prev: number[] = new Array(max).fill(0);

Expand Down Expand Up @@ -48,6 +58,8 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {

keyList[evictHead] = undefined;
valList[evictHead] = undefined;
expList[evictHead] = 0;
ageList[evictHead] = 0;
head = next[evictHead];

if (head !== 0) prev[head] = 0;
Expand All @@ -61,9 +73,52 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
return evictHead;
};

const _delete = (key: Key): boolean => {
const index = keyMap.get(key);

if (index === undefined) return false;

onEviction?.(key, valList[index]!);
keyMap.delete(key);
free.push(index);

keyList[index] = undefined;
valList[index] = undefined;
expList[index] = 0;
ageList[index] = 0;

const prevIndex = prev[index];
const nextIndex = next[index];

if (prevIndex !== 0) next[prevIndex] = nextIndex;
if (nextIndex !== 0) prev[nextIndex] = prevIndex;

if (index === head) head = nextIndex;
if (index === tail) tail = prevIndex;

size--;

return true;
};

const _checkAge = (key: Key): boolean => {
const index = keyMap.get(key);

if (index !== undefined) {
const expiresAt = expList[index];

if (expiresAt !== 0 && Age.now() > expiresAt) {
_delete(key);
return false;
}
}

return true;
};

return {
/** Adds a key-value pair to the cache. Updates the value if the key already exists. */
set(key: Key, value: Value): undefined {
set(key: Key, value: Value, options?: { maxAge?: number }): undefined {
if (key === undefined) return;

let index = keyMap.get(key);
Expand All @@ -77,6 +132,19 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {

valList[index] = value;

const keyMaxAge = options?.maxAge !== undefined ? options.maxAge : maxAge;

if (keyMaxAge !== undefined) {
if (typeof keyMaxAge !== 'number' || keyMaxAge <= 0)
throw new TypeError('`maxAge` must be a positive number');

expList[index] = Age.now() + keyMaxAge;
ageList[index] = keyMaxAge;
} else {
expList[index] = 0;
ageList[index] = 0;
}

if (size === 1) head = tail = index;
else setTail(index, 'set');
},
Expand All @@ -86,20 +154,40 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
const index = keyMap.get(key);

if (index === undefined) return;
if (!_checkAge(key)) return;

if (index !== tail) setTail(index, 'get');

const itemMaxAge = ageList[index];
if (itemMaxAge !== 0) {
expList[index] = Age.now() + itemMaxAge;
}

return valList[index];
},

/** Retrieves the value for a given key without changing its position. */
peek: (key: Key): Value | undefined => {
const index = keyMap.get(key);

if (!_checkAge(key)) return;
return index !== undefined ? valList[index] : undefined;
},

/** Checks if a key exists in the cache. */
has: (key: Key): boolean => keyMap.has(key),
has: (key: Key): boolean => {
const index = keyMap.get(key);

if (index === undefined) return false;
if (!_checkAge(key)) return false;

const itemMaxAge = ageList[index];
if (itemMaxAge !== 0) {
expList[index] = Age.now() + itemMaxAge;
}

return true;
},

/** Iterates over all keys in the cache, from most recent to least recent. */
*keys(): IterableIterator<Key> {
Expand Down Expand Up @@ -146,31 +234,7 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
},

/** Deletes a key-value pair from the cache. */
delete(key: Key): boolean {
const index = keyMap.get(key);

if (index === undefined) return false;

onEviction?.(key, valList[index]!);
keyMap.delete(key);
free.push(index);

keyList[index] = undefined;
valList[index] = undefined;

const prevIndex = prev[index];
const nextIndex = next[index];

if (prevIndex !== 0) next[prevIndex] = nextIndex;
if (nextIndex !== 0) prev[nextIndex] = prevIndex;

if (index === head) head = nextIndex;
if (index === tail) tail = prevIndex;

size--;

return true;
},
delete: _delete,

/** Evicts the oldest item or the specified number of the oldest items from the cache. */
evict: (number: number): undefined => {
Expand Down Expand Up @@ -198,6 +262,8 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {
keyMap.clear();
keyList.fill(undefined);
valList.fill(undefined);
expList.fill(0);
ageList.fill(0);

free = [];
size = 0;
Expand All @@ -216,17 +282,25 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {

const preserve = Math.min(size, newMax);
const remove = size - preserve;
const newKeyList: (Key | undefined)[] = new Array(newMax);
const newValList: (Value | undefined)[] = new Array(newMax);
const newNext: number[] = new Array(newMax);
const newPrev: number[] = new Array(newMax);
const newKeyList: (Key | undefined)[] = new Array(newMax).fill(
undefined
);
const newValList: (Value | undefined)[] = new Array(newMax).fill(
undefined
);
const newExpList: number[] = new Array(newMax).fill(0);
const newAgeList: number[] = new Array(newMax).fill(0);
const newNext: number[] = new Array(newMax).fill(0);
const newPrev: number[] = new Array(newMax).fill(0);

for (let i = 1; i <= remove; i++)
onEviction?.(keyList[i]!, valList[i]!);

for (let i = preserve - 1; i >= 0; i--) {
newKeyList[i] = keyList[current];
newValList[i] = valList[current];
newExpList[i] = expList[current];
newAgeList[i] = ageList[current];
newNext[i] = i + 1;
newPrev[i] = i - 1;
keyMap.set(newKeyList[i]!, i);
Expand All @@ -239,12 +313,16 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {

keyList.length = newMax;
valList.length = newMax;
expList.length = newMax;
ageList.length = newMax;
next.length = newMax;
prev.length = newMax;

for (let i = 0; i < preserve; i++) {
keyList[i] = newKeyList[i];
valList[i] = newValList[i];
expList[i] = newExpList[i];
ageList[i] = newAgeList[i];
next[i] = newNext[i];
prev[i] = newPrev[i];
}
Expand All @@ -257,6 +335,8 @@ export const createLRU = <Key, Value>(options: CacheOptions<Key, Value>) => {

keyList.push(...new Array(fill).fill(undefined));
valList.push(...new Array(fill).fill(undefined));
expList.push(...new Array(fill).fill(0));
ageList.push(...new Array(fill).fill(0));
next.push(...new Array(fill).fill(0));
prev.push(...new Array(fill).fill(0));
}
Expand Down
12 changes: 12 additions & 0 deletions t.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createLRU } from './src/index.js';

const LRU = createLRU({
max: 100,
maxAge: 1000,
});

LRU.set('A', 'Value');

setTimeout(() => {
console.log(LRU.get('A'));
}, 2000);
7 changes: 3 additions & 4 deletions tools/browserfy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ import { transformAsync } from '@babel/core';
import { minify } from 'terser';

(async () => {
const contents = (await readFile('src/index.ts', 'utf8')).replace(
/export const /gim,
'window.'
);
const contents = (await readFile('src/index.ts', 'utf8'))
.replace(/export const/gim, 'window.')
.replace(/import { performance } from 'node:perf_hooks';/gim, '');

const result = await esbuild.build({
stdin: {
Expand Down

0 comments on commit 1d8d76e

Please sign in to comment.