Skip to content

Commit

Permalink
Merge pull request #68 from streamich/lru-cache
Browse files Browse the repository at this point in the history
LRU cache
  • Loading branch information
streamich authored Aug 11, 2024
2 parents 4763172 + d7c8d78 commit 1eaaf32
Show file tree
Hide file tree
Showing 14 changed files with 504 additions and 4 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ Useful TypeScript utilities.

---

- `LruCache` — a *Least Recently Used Cache* implemented using `Object` and doubly linked list.
The default limit is around 1 billion items (2^30 - 1).

---

- `normalizeEmail` — normalizes email by stripping out `.` and `+` characters and
removing everything after the `+` character and lower-casing the e-mail. Useful for
getting an e-mail into a common form when throttling requests by e-mail.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,19 @@
"tslib": "^2"
},
"devDependencies": {
"@types/benchmark": "^2.1.5",
"@types/jest": "^29.5.12",
"benchmark": "^2.1.4",
"husky": "^8.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.0",
"pretty-quick": "^3.1.1",
"rimraf": "^3.0.2",
"ts-jest": "^29.1.2",
"tslib": "^2.6.2",
"tslint": "^6.1.3",
"tslint-config-common": "^1.6.2",
"typescript": "^5.0.3",
"tslib": "^2.6.2"
"typescript": "^5.0.3"
},
"release": {
"branches": [
Expand Down
107 changes: 107 additions & 0 deletions src/LruCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
export class LruCache<V> {
protected capacity: number;
protected head: LruNode<V> | undefined = undefined;
protected tail: LruNode<V> | undefined = undefined;
protected map: Record<string, LruNode<V>> = Object.create(null);

constructor(protected readonly limit: number = 1000) {
this.capacity = limit | 0;
}

public get size(): number {
return this.limit - this.capacity;
}

public set(key: string, value: V) {
const node = this.map[key];
if (node) {
this.pop(node);
node.v = value;
this.push(node);
} else {
if (!this.capacity) {
const head = this.head;
if (head) {
this.pop(head);
delete this.map[head.k];
this.capacity++;
}
}
this.capacity--;
const node = new LruNode(key, value);
this.map[key] = node;
this.push(node);
}
}

public get(key: string): V | undefined {
const node = this.map[key];
if (!node) return;
if (this.tail !== node) {
this.pop(node);
this.push(node);
}
return node.v;
}

public peek(key: string): V | undefined {
const node = this.map[key];
return node instanceof LruNode ? node.v : undefined;
}

public has(key: string): boolean {
return key in this.map;
}

public clear(): void {
this.head = undefined;
this.tail = undefined;
this.map = Object.create(null);
this.capacity = this.limit;
}

public keys(): string[] {
return Object.keys(this.map);
}

public del(key: string): boolean {
const node = this.map[key];
if (node instanceof LruNode) {
this.pop(node);
delete this.map[key];
++this.capacity;
return true;
}
return false;
}

protected pop(node: LruNode<V>): void {
const l = node.l;
const r = node.r;
if (this.head === node) this.head = r;
else l!.r = r;
if (this.tail === node) this.tail = l;
else r!.l = l;
// node.l = undefined;
// node.r = undefined;
}

protected push(node: LruNode<V>): void {
const tail = this.tail;
if (tail) {
tail.r = node;
node.l = tail;
} else this.head = node;
this.tail = node;
}
}

class LruNode<V> {
public l: LruNode<V> | undefined = undefined;
public r: LruNode<V> | undefined = undefined;

constructor(
public readonly k: string,
public v: V,
) {}
}
5 changes: 4 additions & 1 deletion src/LruMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ export class LruMap<K, V> extends Map<K, V> {

public get(key: K): V | undefined {
const value = super.get(key)!;
if (value === void 0) return super.delete(key) && super.set(key, value), value;
if (value === void 0) {
if (super.delete(key)) super.set(key, value);
return value;
}
super.delete(key);
super.set(key, value);
return value;
Expand Down
79 changes: 79 additions & 0 deletions src/__bench__/lru.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// npx ts-node src/__bench__/lru.bench.ts

/* tslint:disable no-console */

import * as Benchmark from 'benchmark';
import {LruMap} from '../LruMap';
import {LruCache} from '../LruCache';

const lru = require('./vendor/lru');

interface Cache {
name: string;
create: (limit: number) => {set: (key: string, value: any) => void; get: (key: string) => any};
}

const caches: Cache[] = [
{
name: 'LruMap',
create: (limit: number) => new LruMap(limit),
},
{
name: 'LruCache',
create: (limit: number) => new LruCache(limit),
},
{
name: 'lru',
create: (limit: number) => lru(limit),
},
];
const limits = [10, 100, 1000, 10000];
// const iterations = [10, 100, 1000, 10000];
const iterations = [1000000];
// const reads = [true, false];
const reads = [true];

for (const limit of limits) {
for (const iteration of iterations) {
for (const read of reads) {
console.log('');
console.log('limit:', limit, 'iterations:', iteration, 'read:', read);
const suite = new Benchmark.Suite();
for (const {create, name} of caches) {
if (reads) {
suite.add(`${name}`, () => {
let val: any;
const cache = create(limit);
for (let j = 0; j < 3; j++) {
for (let i = 0; i < limit; i++) {
const key = 'foo-' + i;
cache.set(key, i);
}
}
for (let i = 0; i < iteration; i++) {
const key = 'foo-' + (i % limit);
val = cache.get(key);
}
return val;
});
} else {
suite.add(`${name}`, () => {
const cache = create(limit);
for (let i = 0; i < iteration; i++) {
const key = 'foo-' + i;
cache.set(key, i);
}
});
}
}
suite
.on('cycle', (event: any) => {
console.log(String(event.target) + `, ${Math.round(1000000000 / event.target.hz)} ns/op`);
})
.on('complete', () => {
console.log('Fastest is ' + suite.filter('fastest').map('name'));
})
.run();
}
}
}
96 changes: 96 additions & 0 deletions src/__bench__/vendor/lru.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* tslint:disable */

module.exports = function (size) {
return new LruCache(size);
};

function LruCache(size) {
this.capacity = size | 0;
this.map = Object.create(null);
this.list = new DoublyLinkedList();
}

LruCache.prototype.get = function (key) {
var node = this.map[key];
if (node == null) return undefined;
this.used(node);
return node.val;
};

LruCache.prototype.set = function (key, val) {
var node = this.map[key];
if (node != null) {
node.val = val;
} else {
if (!this.capacity) this.prune();
if (!this.capacity) return false;
node = new DoublyLinkedNode(key, val);
this.map[key] = node;
this.capacity--;
}
this.used(node);
return true;
};

LruCache.prototype.used = function (node) {
this.list.moveToFront(node);
};

LruCache.prototype.prune = function () {
var node = this.list.pop();
if (node != null) {
delete this.map[node.key];
this.capacity++;
}
};

function DoublyLinkedList() {
this.firstNode = null;
this.lastNode = null;
}

DoublyLinkedList.prototype.moveToFront = function (node) {
if (this.firstNode == node) return;

this.remove(node);

if (this.firstNode == null) {
this.firstNode = node;
this.lastNode = node;
node.prev = null;
node.next = null;
} else {
node.prev = null;
node.next = this.firstNode;
node.next.prev = node;
this.firstNode = node;
}
};

DoublyLinkedList.prototype.pop = function () {
var lastNode = this.lastNode;
if (lastNode != null) {
this.remove(lastNode);
}
return lastNode;
};

DoublyLinkedList.prototype.remove = function (node) {
if (this.firstNode == node) {
this.firstNode = node.next;
} else if (node.prev != null) {
node.prev.next = node.next;
}
if (this.lastNode == node) {
this.lastNode = node.prev;
} else if (node.next != null) {
node.next.prev = node.prev;
}
};

function DoublyLinkedNode(key, val) {
this.key = key;
this.val = val;
this.prev = null;
this.next = null;
}
Loading

0 comments on commit 1eaaf32

Please sign in to comment.