/** * PerformanceMonitor - Track latencies, errors, and cache metrics * * Features: * - Request timing (avg, p95) * - Cache hit/miss tracking * - Error counting * - Simple ring buffer for timings */ export class PerformanceMonitor { constructor(clock = () => Date.now()) { this.clock = clock; this.counters = { cacheHit: 0, cacheMiss: 0, reqCount: 0, reqError: 0, meilisearchRetry: 0, filesystemRetry: 0 }; this.timings = []; // Ring buffer for latencies this.maxTimings = 500; this.startTime = Date.now(); } /** * Mark request start, returns timestamp */ markRequestStart() { return this.clock(); } /** * Mark request end, calculate duration * Returns duration in ms */ markRequestEnd(startTs, ok = true) { const dur = this.clock() - startTs; if (this.timings.length >= this.maxTimings) { this.timings.shift(); } this.timings.push(dur); this.counters.reqCount += 1; if (!ok) this.counters.reqError += 1; return dur; } /** * Mark cache hit or miss */ markCache(hit) { if (hit) { this.counters.cacheHit += 1; } else { this.counters.cacheMiss += 1; } } /** * Mark retry event */ markRetry(source = 'unknown') { if (source === 'meilisearch') { this.counters.meilisearchRetry += 1; } else if (source === 'filesystem') { this.counters.filesystemRetry += 1; } } /** * Get performance snapshot */ snapshot() { const arr = this.timings.slice(); const n = arr.length || 1; const sum = arr.reduce((a, b) => a + b, 0); const avg = sum / n; // Calculate p95 const sorted = arr.slice().sort((a, b) => a - b); const p95Idx = Math.min(n - 1, Math.floor(n * 0.95)); const p95 = sorted[p95Idx] || 0; const uptime = this.clock() - this.startTime; const cacheTotal = this.counters.cacheHit + this.counters.cacheMiss; const cacheHitRate = cacheTotal > 0 ? Math.round((this.counters.cacheHit / cacheTotal) * 1000) / 10 : 0; const errorRate = this.counters.reqCount > 0 ? Math.round((this.counters.reqError / this.counters.reqCount) * 1000) / 10 : 0; return { uptime, requests: { total: this.counters.reqCount, errors: this.counters.reqError, errorRate: `${errorRate}%` }, cache: { hits: this.counters.cacheHit, misses: this.counters.cacheMiss, hitRate: `${cacheHitRate}%` }, retries: { meilisearch: this.counters.meilisearchRetry, filesystem: this.counters.filesystemRetry }, latency: { avgMs: Math.round(avg), p95Ms: Math.round(p95), samples: this.timings.length } }; } /** * Reset all counters */ reset() { this.counters = { cacheHit: 0, cacheMiss: 0, reqCount: 0, reqError: 0, meilisearchRetry: 0, filesystemRetry: 0 }; this.timings = []; this.startTime = this.clock(); } }