ObsiViewer/server/perf/performance-monitor.js

138 lines
3.0 KiB
JavaScript

/**
* 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();
}
}