From 7b6c419d3f13472226f0a5fb361f0ffce517d9c2 Mon Sep 17 00:00:00 2001 From: Yaroslav Surilov Date: Thu, 7 Dec 2023 23:56:02 +0200 Subject: [PATCH] feat(fifo-cache): update docs, finish implementation --- README.md | 5 ++ fifo-cache/index.ts | 142 +++++++++++++++++++++------------- fifo-cache/readme.md | 2 +- libs/filters/bloom/index.ts | 0 libs/filters/bloom/readme.md | 0 libs/filters/cuckoo/index.ts | 0 libs/filters/cuckoo/readme.md | 0 rr-cache/index.ts | 2 +- rr-cache/readme.md | 6 ++ 9 files changed, 100 insertions(+), 57 deletions(-) create mode 100644 libs/filters/bloom/index.ts create mode 100644 libs/filters/bloom/readme.md create mode 100644 libs/filters/cuckoo/index.ts create mode 100644 libs/filters/cuckoo/readme.md diff --git a/README.md b/README.md index 0175389..e033869 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ --- +## Table of Contents + +[First-In-First-Out (FIFO) Cache](./fifo-cache/) +[Random Replacement (RR) Cache](./rr-cache/) + ## Motivation Yet another cache implementations. But why? Yeah, there are a few reasons for that. diff --git a/fifo-cache/index.ts b/fifo-cache/index.ts index 0ae62b5..83b123f 100644 --- a/fifo-cache/index.ts +++ b/fifo-cache/index.ts @@ -1,102 +1,134 @@ -interface IFifoCache { - get size(): number; - set capacity(value: number); - read: (key: any) => any; - store: (key: any, value: any) => void; - has: (key: any) => boolean; - clear: () => void; -}; - -type TConfigOptions = { - /** - * Capacity means how many items can be stored at the same time in cache. - * For FIFO cache, by definition, capacity is a required restriction, - * without it becomes almost meaningless, so this option is mandatory. - */ - capacity: number; -}; +import { ICache } from '../libs/types.js'; type TNode = { data: { key: any, value: any }; - next?: TNode; + next: TNode | null; + prev: TNode | null; }; -const Node = (data: {key: any, value: any}, next?: TNode) => ({data, next}); +const Node = ( + data: {key: any, value: any}, + next = null, + prev = null +) => ({data, next, prev}); -class FifoCache implements IFifoCache { +class FifoCache implements ICache { + #hits: number; + #misses: number; #capacity: number; - #store; - #map: Map; - - constructor ( options: TConfigOptions ) { - const { capacity } = options; - - if (!Number.isInteger(capacity) || capacity <= 0) throw new Error('invalid "capacity": positive integer expected'); + #locked: boolean; + #head: any; + #tail: any; + #map; + + constructor ( capacity: number ) { + if (!Number.isInteger(capacity) || capacity <= 0) { + throw new Error('invalid "capacity": positive integer expected'); + } + this.#hits = 0; + this.#misses = 0; this.#capacity = capacity; - // Store is a Singly linked list to support fast add (to tail) and delete (from head) records. - this.#store = {head: null, tail: null}; - // Struct for fast access to cache records, use as lookup table. + this.#locked = false; + + // store is a Singly Linked List to support fast add (to tail) and delete (from head) records + this.#head = null; + this.#tail = null; + // struct for fast access to cache records, use as lookup table this.#map = new Map(); } - get size() { - return this.#map.size; + get stats() { + return { + size: this.#map.size, + capacity: this.#capacity, + locked: this.#locked, + hitRatio: this.#hits / (this.#hits + this.#misses) + }; } - set capacity (value: number) { - if (!Number.isInteger(value) || value <= 0) throw new Error('invalid "capacity": positive integer expected'); - - if (value < this.#capacity) { - // @todo: implement - } - - this.#capacity = value; + set lock (state: boolean) { + this.#locked = Boolean(state); } + /** + * Read value stored in cache by assosiated key. + * @param {*} key - cache record's key + * @return {*|void} record's value retrieved by key or undefined if record is absent + */ read (key: any) { if (this.#map.has(key)) { - return this.#map.get(key)?.data.value; + this.#hits += 1; + return this.#map.get(key).data.value; } - return null; + this.#misses += 1; } - store (key: any, value: any) { + add (key: any, value: any) { // check if cache capacity limit is reached if (this.#map.size === this.#capacity) { // evict head since we are out of capacity - const node = this.#store.head; - this.#store.head = node.next; - this.#map.delete(node.data.key); - node.next = null; + const head = this.#head; + this.#head = head.next; + head.next = null; + this.#head.prev = null; + this.#map.delete(head.data.key); } - const node = Node({key, value}); + const node: TNode = Node({key, value}); if (this.#map.size === 0) { - this.#store.head = node; + this.#head = node; } else if (this.#map.size === 1) { - this.#store.tail = node; - this.#store.head.next = this.#store.tail; + this.#tail = node; + this.#head.next = this.#tail; + this.#tail.prev = this.#head; } else { - this.#store.tail.next = node; - this.#store.tail = node; + this.#tail.next = node; + node.prev = this.#tail; + this.#tail = node; } this.#map.set(key, node); } + /** + * Check if record by given key exists in cache. + * @param {*} key - cache record's key + * @return {boolean} return true if record is in the cache + */ has (key: any) { return this.#map.has(key); } + /** + * Remove an item from the cache. + * @param {*} key - cache record's key + * @return {void} + */ + remove (key: any) { + if (this.#map.has(key)) { + const node = this.#map.get(key); + + node.prev.next = node.next; + node.next.prev = node.prev; + node.next = null; + node.prev = null; + this.#map.delete(key); + } + } + + /** + * Remove all items from the cache and clear internal structures. + * @return {void} + */ clear () { - this.#store.head = null; - this.#store.tail = null; + this.#hits = this.#misses = 0; + this.#head = this.#tail = null; this.#map.clear(); } } diff --git a/fifo-cache/readme.md b/fifo-cache/readme.md index e1cd0be..ef23c42 100644 --- a/fifo-cache/readme.md +++ b/fifo-cache/readme.md @@ -1,4 +1,4 @@ -# First-In-First-Out (FIFO) cache +# First-In-First-Out (FIFO) Cache In __FIFO__ caching, the first item added to the cache is the first one to be removed when the cache reaches its capacity. It follows a queue-like behaviour. diff --git a/libs/filters/bloom/index.ts b/libs/filters/bloom/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/filters/bloom/readme.md b/libs/filters/bloom/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/libs/filters/cuckoo/index.ts b/libs/filters/cuckoo/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/libs/filters/cuckoo/readme.md b/libs/filters/cuckoo/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/rr-cache/index.ts b/rr-cache/index.ts index 88ca981..930c4a4 100644 --- a/rr-cache/index.ts +++ b/rr-cache/index.ts @@ -39,7 +39,7 @@ class RRCache implements ICache { /** * Read value stored in cache by assosiated key. * @param {*} key - cache record's key - * @return {*|void} record's value retrieved by key or null if record is absent + * @return {*|void} record's value retrieved by key or undefined if record is absent */ read (key: any) { if (this.#store.has(key)) { diff --git a/rr-cache/readme.md b/rr-cache/readme.md index 18093ed..bdc8f78 100644 --- a/rr-cache/readme.md +++ b/rr-cache/readme.md @@ -2,6 +2,10 @@ ## Definition +## Replacement policy + +This cache algorithm selects "victim" for eviction randomly when cache capacity is reached. Unlike algorithms such as LRU/LFU, access information isn't a criteria for choosing candidate for eviction. However, access history as well as cache hits/misses can be tracked for other optimization or monitoring purposes. + ## Implementation details | Operation | Time complexity | Space complexity | Description | @@ -22,4 +26,6 @@ This caching algorithm may be used when no specific access patterns are known or ARM processors use this type of cache due to its simplicity. For example, the Cortex-R4, Cortex-R5, and Cortex-R7 processors only support the pseudo-random replacement policy. +## More information + [ARM Cortex-R Series Programmer's Guide](https://developer.arm.com/documentation/den0042/a/Caches/Cache-policies/Replacement-policy) \ No newline at end of file