UNPKG

trakr

Version:

Minimal utility for tracking performance

407 lines (342 loc) 10.5 kB
const IDENTITY = (a: any) => a; interface Performance { now(): number; mark(name: string): void; measure(name: string, startMark: string, endMark: string): void; } interface Tracing { enable(): void; disable(): void; readonly enabled: boolean; } interface TraceEvents { createTracing(options: {categories: string[]}): Tracing; } const NODE = typeof module !== 'undefined' && module.exports; // @ts-ignore const PERF: Performance = (NODE ? require('perf_hooks') : window).performance; export interface Stats { readonly cnt: number; readonly sum: number; readonly avg: number; readonly var: number; readonly std: number; readonly sem: number; readonly moe: number; readonly rme: number; readonly min: number; readonly max: number; readonly p50: number; readonly p90: number; readonly p95: number; readonly p99: number; } // T-Distribution two-tailed critical values for 95% confidence. // http://www.itl.nist.gov/div898/handbook/eda/section3/eda3672.htm. const TABLE = [ 12.706, 4.303, 3.182, 2.776, 2.571, 2.447, 2.365, 2.306, 2.262, 2.228, 2.201, 2.179, 2.16, 2.145, 2.131, 2.12, 2.11, 2.101, 2.093, 2.086, 2.08, 2.074, 2.069, 2.064, 2.06, 2.056, 2.052, 2.048, 2.045, 2.042, ]; const TINF = 1.96; export class Stats { protected constructor() {} // T(N): N lg N + 4N static compute(arr: number[], pop?: boolean): Stats { const sorted = arr.slice(); // N sorted.sort((a, b) => a - b); // N lg N const sum = Stats.sum(sorted); // N const mean = Stats.mean(arr, sum); const variance = Stats.variance(arr, pop, mean); // 2N const stddev = Math.sqrt(variance); const error = Stats.standardErrorOfMean(sorted, pop, stddev); const margin = Stats.marginOfError(sorted, pop, error); // clang-format off return { cnt: sorted.length, sum, avg: mean, var: variance, std: stddev, sem: error, moe: margin, rme: Stats.relativeMarginOfError(sorted, pop, margin, mean), min: sorted[0], max: sorted[sorted.length - 1], p50: Stats.ptile(sorted, 0.50), p90: Stats.ptile(sorted, 0.90), p95: Stats.ptile(sorted, 0.95), p99: Stats.ptile(sorted, 0.99), }; // clang-format on } // T(N): N static max(arr: number[]): number { let m = -Infinity; for (const a of arr) { if (a > m) m = a; } return m; } // T(N): N static min(arr: number[]): number { let m = Infinity; for (const a of arr) { if (a < m) m = a; } return m; } // T(N): N static sum(arr: number[]): number { if (!arr.length) return 0; return arr.reduce((acc, v) => acc + v); } // T(N): N | 1 static mean(arr: number[], sum?: number): number { if (!arr.length) return 0; const s = typeof sum !== 'undefined' ? sum : Stats.sum(arr); return s / arr.length; } // T(N): N lg N static median(arr: number[]): number { return Stats.percentile(arr, 0.5); } // T(N): N lg N + N static percentile(arr: number[], p: number): number { const sorted = arr.slice(); sorted.sort((a, b) => a - b); return Stats.ptile(sorted, p); } // PRE: arr = arr.sort((a, b) => a - b) static ptile(arr: number[], p: number): number { if (!arr.length) return 0; if (p <= 0) return arr[0]; if (p >= 1) return arr[arr.length - 1]; const index = (arr.length - 1) * p; const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; if (upper >= arr.length) return arr[lower]; return arr[lower] * (1 - weight) + arr[upper] * weight; } // T(N): 3N | 2N static variance(arr: number[], pop?: boolean, mean?: number): number { if (!arr.length) return 0; const n = pop ? arr.length : arr.length - 1; const m = typeof mean !== 'undefined' ? mean : Stats.mean(arr); return Stats.sum(arr.map(num => Math.pow(num - m, 2))) / n; } // T(N): 3N | 2N static standardDeviation(arr: number[], pop?: boolean, mean?: number): number { return Math.sqrt(Stats.variance(arr, pop, mean)); } // T(N): 3N | 1 static standardErrorOfMean(arr: number[], pop?: boolean, std?: number) { if (!arr.length) return 0; const dev = typeof std !== 'undefined' ? std : Stats.standardDeviation(arr, pop); return dev / Math.sqrt(arr.length); } // T(N): 3N | 1 static marginOfError(arr: number[], pop?: boolean, sem?: number) { if (!arr.length) return 0; const err = typeof sem !== 'undefined' ? sem : Stats.standardErrorOfMean(arr, pop); return err * (TABLE[(arr.length - 1) - 1] || TINF); } // T(N): 4N | 1 static relativeMarginOfError( arr: number[], pop?: boolean, moe?: number, mean?: number ) { if (!arr.length) return 0; const margin = typeof moe !== 'undefined' ? moe : Stats.marginOfError(arr, pop); const avg = typeof mean !== 'undefined' ? mean : Stats.mean(arr); return (margin / avg) * 100; } } export class Tracer { readonly traceEvents?: TraceEvents; tracing?: Tracing; constructor(traceEvents?: TraceEvents) { this.traceEvents = traceEvents; } get enabled() { return !!this.tracing?.enabled; } enable(categories?: string[]) { if (!this.traceEvents || this.enabled) return; categories = categories || ['node.perf']; this.tracing = this.traceEvents.createTracing({categories}); this.tracing.enable(); } disable() { if (!this.tracing || !this.enabled) return; this.tracing.disable(); this.tracing = undefined; } } export const TRACER = new Tracer((() => { try { return require('trace_events'); } catch { return undefined; } })()); export interface TrackerOptions { buf?: Buffer; } export abstract class Tracker { static create(options?: TrackerOptions) { const buf = options?.buf; return buf ? new BoundedTracker(buf) : new UnboundedTracker(); } readonly counters: Map<string, number>; constructor() { this.counters = new Map(); } count(name: string, val?: number) { val = val || 1; const c = this.counters.get(name); this.counters.set(name, typeof c !== 'undefined' ? (c + val) : val); } abstract add(name: string, val: number): void; abstract stats(pop?: boolean): Map<string, Stats>; protected push(dists: Map<string, number[]>, name: string, val: number) { const d = dists.get(name) || []; if (!d.length) dists.set(name, d); d.push(val); } // T(M, N): M(N lg N + 3N) protected compute(dists: Map<string, number[]>, pop?: boolean) { const stats = new Map(); for (const [name, vals] of dists.entries()) { stats.set(name, Stats.compute(vals, pop)); } return stats; } } class UnboundedTracker extends Tracker { protected readonly distributions: Map<string, number[]>; constructor() { super(); this.distributions = new Map(); } add(name: string, val: number) { return this.push(this.distributions, name, val); } stats(pop?: boolean): Map<string, Stats> { return this.compute(this.distributions, pop); } } class BoundedTracker extends Tracker { protected readonly buf: Buffer; protected readonly next: {tag: number; loc: number; dloc: number}; protected readonly tags: Map<string, number>; protected readonly distributions: Map<string, number[]>; constructor(buf: Buffer) { super(); this.buf = buf; this.next = {tag: 0, loc: 0, dloc: 0}; this.tags = new Map(); this.distributions = new Map(); } add(name: string, val: number) { let tag = this.tags.get(name); if (typeof tag === 'undefined') { this.tags.set(name, (tag = this.next.tag++)); } this.buf.writeUInt8(tag, this.next.loc); this.buf.writeDoubleBE(val, this.next.loc + 1); this.next.loc += 9; } stats(pop?: boolean): Map<string, Stats> { if (this.next.dloc !== this.next.loc) { const names: string[] = []; for (const [name, tag] of this.tags.entries()) { names[tag] = name; } while (this.next.dloc < this.next.loc) { const name = names[this.buf.readUInt8(this.next.dloc)]; const val = this.buf.readDoubleBE(this.next.dloc + 1); this.push(this.distributions, name, val); this.next.dloc += 9; } } return this.compute(this.distributions, pop); } } export interface TimerOptions extends TrackerOptions { trace?: boolean; perf?: Performance; } export abstract class Timer { static create(options?: TimerOptions) { const tracker = Tracker.create(options); const perf = options?.perf || PERF; const trace = (options && typeof options.trace !== 'undefined') ? !!options.trace : TRACER.enabled; return trace ? new TracingTimer(tracker, perf) : new BasicTimer(tracker, perf); } protected readonly tracker: Tracker; protected readonly perf: Performance; protected started?: number; protected stopped?: number; constructor(tracker: Tracker, perf: Performance) { this.tracker = tracker; this.perf = perf; } count(name: string, val?: number) { this.tracker.count(name, val); } get counters(): Map<string, number> { return this.tracker.counters; } get duration(): number|undefined { if (typeof this.started === 'undefined' || typeof this.stopped === 'undefined') { return undefined; } return this.stopped - this.started; } start() { if (!this.started) this.started = this.perf.now(); } stop() { if (!this.stopped) this.stopped = this.perf.now(); } stats(pop?: boolean): Map<string, Stats> { return this.tracker.stats(pop); } abstract time(name: string): (a: any) => any; } class BasicTimer extends Timer { constructor(tracker: Tracker, perf: Performance) { super(tracker, perf); } time(name: string) { if (!this.started || this.stopped) return IDENTITY; const begin = this.perf.now(); return (a: any) => { this.tracker.add(name, this.perf.now() - begin); return a; }; } } class TracingTimer extends Timer { constructor(tracker: Tracker, perf: Performance) { super(tracker, perf); } time(name: string) { if (!this.started || this.stopped) return IDENTITY; const b = `b|${name}`; this.perf.mark(b); const begin = this.perf.now(); return (a: any) => { this.tracker.add(name, this.perf.now() - begin); const e = `e|${name}`; this.perf.mark(e); this.perf.measure(name, b, e); return a; }; } }