docs: remove Phase 2 completion and executive summary files
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* MetadataCache - TTL + simple LRU cache
|
||||
*
|
||||
* Features:
|
||||
* - TTL-based expiration
|
||||
* - Simple FIFO eviction when maxItems exceeded
|
||||
* - read-through helper for async producers
|
||||
* - Pseudo-LRU via Map re-insertion
|
||||
*/
|
||||
|
||||
export class MetadataCache {
|
||||
constructor({ ttlMs = 5 * 60 * 1000, maxItems = 10_000 } = {}) {
|
||||
this.ttlMs = ttlMs;
|
||||
this.maxItems = maxItems;
|
||||
this.store = new Map(); // key -> { value, exp }
|
||||
this.stats = {
|
||||
hits: 0,
|
||||
misses: 0,
|
||||
evictions: 0,
|
||||
sets: 0
|
||||
};
|
||||
}
|
||||
|
||||
_now() {
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Evict oldest entries if cache exceeds maxItems
|
||||
*/
|
||||
_evictIfNeeded() {
|
||||
if (this.store.size <= this.maxItems) return;
|
||||
|
||||
const toEvict = this.store.size - this.maxItems + Math.floor(this.maxItems * 0.1);
|
||||
let evicted = 0;
|
||||
|
||||
for (const k of this.store.keys()) {
|
||||
this.store.delete(k);
|
||||
evicted++;
|
||||
if (evicted >= toEvict) break;
|
||||
}
|
||||
|
||||
this.stats.evictions += evicted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value from cache
|
||||
* Returns { hit: boolean, value: any }
|
||||
*/
|
||||
get(key) {
|
||||
const entry = this.store.get(key);
|
||||
|
||||
if (!entry) {
|
||||
this.stats.misses++;
|
||||
return { hit: false, value: undefined };
|
||||
}
|
||||
|
||||
// Check expiration
|
||||
if (entry.exp < this._now()) {
|
||||
this.store.delete(key);
|
||||
this.stats.misses++;
|
||||
return { hit: false, value: undefined };
|
||||
}
|
||||
|
||||
// Touch for pseudo-LRU: re-insert at end
|
||||
this.store.delete(key);
|
||||
this.store.set(key, entry);
|
||||
this.stats.hits++;
|
||||
return { hit: true, value: entry.value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value in cache with optional TTL override
|
||||
*/
|
||||
set(key, value, { ttlMs = this.ttlMs } = {}) {
|
||||
const exp = this._now() + ttlMs;
|
||||
this.store.set(key, { value, exp });
|
||||
this.stats.sets++;
|
||||
this._evictIfNeeded();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete specific key
|
||||
*/
|
||||
delete(key) {
|
||||
this.store.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear entire cache
|
||||
*/
|
||||
clear() {
|
||||
this.store.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-through helper: get from cache or call producer
|
||||
* Returns { value, hit: boolean }
|
||||
*/
|
||||
async remember(key, producer, { ttlMs = this.ttlMs } = {}) {
|
||||
const { hit, value } = this.get(key);
|
||||
if (hit) return { value, hit: true };
|
||||
|
||||
const fresh = await producer();
|
||||
this.set(key, fresh, { ttlMs });
|
||||
return { value: fresh, hit: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats() {
|
||||
const total = this.stats.hits + this.stats.misses;
|
||||
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
|
||||
|
||||
return {
|
||||
size: this.store.size,
|
||||
maxItems: this.maxItems,
|
||||
ttlMs: this.ttlMs,
|
||||
hits: this.stats.hits,
|
||||
misses: this.stats.misses,
|
||||
hitRate: Math.round(hitRate * 10) / 10,
|
||||
evictions: this.stats.evictions,
|
||||
sets: this.stats.sets
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset statistics
|
||||
*/
|
||||
resetStats() {
|
||||
this.stats = { hits: 0, misses: 0, evictions: 0, sets: 0 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user