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
text/typescript
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);
}
}