UNPKG

transitory

Version:

In-memory cache with high hit rates via LFU eviction. Supports time-based expiration, automatic loading and metrics.

142 lines (118 loc) 3.42 kB
import { KeyType } from '../KeyType'; import { hashcode } from './hashcode'; /** * Helper function to calculate the closest power of two to N. * * @param n - * input * @returns * closest power of two to `n` */ function toPowerOfN(n: number) { return Math.pow(2, Math.ceil(Math.log(n) / Math.LN2)); } /** * Calculates a component of the hash. * * @param a0 - * @returns * hash */ function hash2(a0: number) { let a = (a0 ^ 61) ^ (a0 >>> 16); a = a + (a << 3); a = a ^ (a >>> 4); a = safeishMultiply(a, 0x27d4eb2d); a = a ^ (a >>> 15); return a; } /** * Tiny helper to perform a multiply that's slightly safer to use for hashing. * * @param a - * @param b - * @returns a * b */ function safeishMultiply(a: number, b: number) { return ((a & 0xffff) * b) + ((((a >>> 16) * b) & 0xffff) << 16); } /** * Count-min sketch suitable for use with W-TinyLFU. Similiar to a regular * count-min sketch but with a few important differences to achieve better * estimations: * * 1) Enforces that the width of the sketch is a power of 2. * 2) Uses a reset that decays all values by half when width * 10 additions * have been made. */ export class CountMinSketch { private readonly width: number; private readonly depth: number; public readonly maxSize: number; public readonly halfMaxSize: number; public readonly slightlyLessThanHalfMaxSize: number; private additions: number; public readonly resetAfter: number; private table: Uint8Array; public constructor(width: number, depth: number, decay: boolean) { this.width = toPowerOfN(width); this.depth = depth; // Get the maximum size of values, assuming unsigned ints this.maxSize = Math.pow(2, Uint8Array.BYTES_PER_ELEMENT * 8) - 1; this.halfMaxSize = this.maxSize / 2; this.slightlyLessThanHalfMaxSize = this.halfMaxSize - Math.max(this.halfMaxSize / 4, 1); // Track additions and when to reset this.additions = 0; this.resetAfter = decay ? width * 10 : -1; // Create the table to store data in this.table = new Uint8Array(this.width * depth); } private findIndex(h1: number, h2: number, d: number) { const h = h1 + safeishMultiply(h2, d); return (d * this.width) + (h & (this.width - 1)); } public update(hashCode: number) { const table = this.table; const maxSize = this.maxSize; const estimate = this.estimate(hashCode); const h2 = hash2(hashCode); let added = false; for(let i = 0, n = this.depth; i < n; i++) { const idx = this.findIndex(hashCode, h2, i); const v = table[idx]; if(v + 1 < maxSize && v <= estimate) { table[idx] = v + 1; added = true; } } if(added && ++this.additions === this.resetAfter) { this.performReset(); } } public estimate(hashCode: number) { const table = this.table; const h2 = hash2(hashCode); let result = this.maxSize; for(let i = 0, n = this.depth; i < n; i++) { const value = table[this.findIndex(hashCode, h2, i)]; if(value < result) { result = value; } } return result; } private performReset() { const table = this.table; this.additions /= 2; for(let i = 0, n = table.length; i < n; i++) { this.additions -= table[i] & 1; table[i] = Math.floor(table[i] >>> 1); } } public static hash(key: KeyType) { return hashcode(key); } public static uint8(width: number, depth: number, decay = true) { return new CountMinSketch(width, depth, decay); } }