@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
233 lines (198 loc) • 7.82 kB
JavaScript
import { assert } from "../assert.js";
import { bitCount } from "../binary/operations/bitCount.js";
import { ceilPowerOfTwo } from "../binary/operations/ceilPowerOfTwo.js";
import { min2 } from "../math/min2.js";
const SEED = [ // A mixture of seeds from FNV-1a, CityHash, and Murmur3
0x97cb3127,
0xbe98f273,
0x2f90404f,
0x84222325
];
const RESET_MASK = 0x77777777;
const ONE_MASK = 0x11111111;
/**
* Maximum value of a signed 32bit integer
* @type {number}
*/
const MAX_INT_32 = 2147483647;
/**
* A probabilistic multiset for estimating the popularity of an element within a time window. The
* maximum frequency of an element is limited to 15 (4-bits) and an aging process periodically
* halves the popularity of all elements.
*
*
* This class maintains a 4-bit CountMinSketch [1] with periodic aging to provide the popularity
* history for the TinyLfu admission policy [2]. The time and space efficiency of the sketch
* allows it to cheaply estimate the frequency of an entry in a stream of cache access events.
*
* The counter matrix is represented as a single dimensional array holding 16 counters per slot. A
* fixed depth of four balances the accuracy and cost, resulting in a width of four times the
* length of the array. To retain an accurate estimation the array's length equals the maximum
* number of entries in the cache, increased to the closest power-of-two to exploit more efficient
* bit masking. This configuration results in a confidence of 93.75% and error bound of e / width.
*
* The frequency of all entries is aged periodically using a sampling window based on the maximum
* number of entries in the cache. This is referred to as the reset operation by TinyLfu and keeps
* the sketch fresh by dividing all counters by two and subtracting based on the number of odd
* counters found. The O(n) cost of aging is amortized, ideal for hardware prefetching, and uses
* inexpensive bit manipulations per array location.
*
* [1] An Improved Data Stream Summary: The Count-Min Sketch and its Applications
* http://dimacs.rutgers.edu/~graham/pubs/papers/cm-full.pdf
* [2] TinyLFU: A Highly Efficient Cache Admission Policy
* https://dl.acm.org/citation.cfm?id=3149371
*
* NOTE: ported from "caffeine"
* @see https://github.com/ben-manes/caffeine/blob/master/caffeine/src/main/java/com/github/benmanes/caffeine/cache/FrequencySketch.java
*
* @author ben.manes@gmail.com (Ben Manes)
* @author Alex Goldring 2021/04/08
* @template T
*/
export class FrequencySketch {
/**
*
* @type {number}
*/
sampleSize = 0;
/**
*
* @type {number}
*/
tableMask = 0;
/**
*
* @type {Uint32Array|null}
*/
table = null;
/**
*
* @type {number}
*/
size = 0;
/**
* Initializes and increases the capacity of this <tt>FrequencySketch</tt> instance, if necessary,
* to ensure that it can accurately estimate the popularity of elements given the maximum size of
* the cache. This operation forgets all previous counts when resizing.
*
* @param {number} maximumSize the maximum size of the cache
*/
ensureCapacity(maximumSize) {
assert.isNonNegativeInteger(maximumSize, 'maximumSize');
if (this.table !== null && this.table.length >= maximumSize) {
return;
}
const table_length = maximumSize <= 0 ? 1 : ceilPowerOfTwo(maximumSize);
this.table = new Uint32Array(table_length);
this.tableMask = Math.max(0, table_length - 1);
this.sampleSize = (maximumSize <= 0) ? 10 : 10 * maximumSize;
if (this.sampleSize <= 0) {
this.sampleSize = Number.MAX_SAFE_INTEGER;
}
this.size = 0;
}
/**
* Returns if the sketch has not yet been initialized, requiring that {@link #ensureCapacity} is
* called before it begins to track frequencies.
*
* @returns {boolean}
*/
isNotInitialized() {
return (this.table == null);
}
/**
* Returns the estimated number of occurrences of an element, up to the maximum (15).
*
* @param {number} input_hash the element to count occurrences of
* @returns {number} the estimated number of occurrences of the element; possibly zero but never negative
*/
frequency(input_hash) {
const hash = this.spread(input_hash);
const start = (hash & 3) << 2;
let frequency = MAX_INT_32;
for (let i = 0; i < 4; i++) {
const index = this.indexOf(hash, i);
const count = (this.table[index] >>> ((start + i) << 2)) & 0xf;
frequency = min2(frequency, count);
}
return frequency;
}
/**
*
* Increments the popularity of the element if it does not exceed the maximum (15). The popularity
* of all elements will be periodically down sampled when the observed events exceeds a threshold.
* This process provides a frequency aging to allow expired long term entries to fade away.
*
* @param {number} input_hash the element to add
*/
increment(input_hash) {
const hash = this.spread(input_hash);
const start = (hash & 3) << 2;
// Loop unrolling improves throughput by 5m ops/s
const index0 = this.indexOf(hash, 0);
const index1 = this.indexOf(hash, 1);
const index2 = this.indexOf(hash, 2);
const index3 = this.indexOf(hash, 3);
const added = this.incrementAt(index0, start)
|| this.incrementAt(index1, start + 1)
|| this.incrementAt(index2, start + 2)
|| this.incrementAt(index3, start + 3);
if (added && (++this.size === this.sampleSize)) {
this.reset();
}
}
/**
* Increments the specified counter by 1 if it is not already at the maximum value (15).
*
* @param {number} i the table index (8 counters)
* @param {number} j the counter to increment
* @returns {boolean} if incremented
*/
incrementAt(i, j) {
const offset = j << 2;
const mask = (0xF << offset);
const table = this.table;
const existing_table_value = table[i];
if ((existing_table_value & mask) !== mask) {
table[i] = existing_table_value + (1 << offset);
return true;
}
return false;
}
/**
* Reduces every counter by half of its original value.
*/
reset() {
let count = 0;
const table = this.table;
const table_size = table.length;
for (let i = 0; i < table_size; i++) {
const existing_table_value = table[i];
count += bitCount(existing_table_value & ONE_MASK);
table[i] = (existing_table_value >>> 1) & RESET_MASK;
}
this.size = (this.size >>> 1) - (count >>> 2);
}
/**
* Returns the table index for the counter at the specified depth.
* @param {number} item the element's hash
* @param {number} i the counter depth
* @returns {number} the table index
*/
indexOf(item, i) {
let hash = (item + SEED[i]) * SEED[i];
hash += (hash >>> 16);
return hash & this.tableMask;
}
/**
* Applies a supplemental hash function to a given hash code, which defends against poor quality
* @param {number} v
* @returns {number}
*/
spread(v) {
let x = v;
x = ((x >>> 16) ^ x) * 0x45d9f3b;
x = ((x >>> 16) ^ x) * 0x45d9f3b;
return (x >>> 16) ^ x;
}
}