Skip to content

Commit

Permalink
feat(fifo-cache): update docs, finish implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
zhibirc committed Dec 7, 2023
1 parent 5a91b37 commit 7b6c419
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 57 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
142 changes: 87 additions & 55 deletions fifo-cache/index.ts
Original file line number Diff line number Diff line change
@@ -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<any, TNode>;

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 <key:reference> 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 <key:reference> 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();
}
}
Expand Down
2 changes: 1 addition & 1 deletion fifo-cache/readme.md
Original file line number Diff line number Diff line change
@@ -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.

Expand Down
Empty file added libs/filters/bloom/index.ts
Empty file.
Empty file added libs/filters/bloom/readme.md
Empty file.
Empty file added libs/filters/cuckoo/index.ts
Empty file.
Empty file added libs/filters/cuckoo/readme.md
Empty file.
2 changes: 1 addition & 1 deletion rr-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
6 changes: 6 additions & 0 deletions rr-cache/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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)

0 comments on commit 7b6c419

Please sign in to comment.