UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

233 lines (198 loc) • 7.82 kB
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; } }