ObsiViewer/server/performance-config.mjs

180 lines
4.1 KiB
JavaScript

/**
* Performance configuration for Phase 1 optimization
*/
export const PERFORMANCE_CONFIG = {
// Metadata cache TTL (5 minutes)
METADATA_CACHE_TTL: 5 * 60 * 1000,
// Preload nearby notes (number of notes before/after)
PRELOAD_NEARBY: 5,
// Pagination size for large vaults
PAGINATION_SIZE: 100,
// Maximum notes to load in metadata endpoint
MAX_METADATA_ITEMS: 10000,
// Enable performance logging
ENABLE_PERF_LOGGING: process.env.PERF_LOGGING === 'true',
};
/**
* Advanced in-memory cache for metadata with metrics and intelligent invalidation
*/
export class MetadataCache {
constructor(ttl = PERFORMANCE_CONFIG.METADATA_CACHE_TTL, maxSize = 10000) {
this.cache = new Map();
this.ttl = ttl;
this.maxSize = maxSize;
this.hits = 0;
this.misses = 0;
this.isLoading = false;
this.loadPromise = null;
}
/**
* Get cached metadata for a vault
*/
async getMetadata(vaultDir, loader) {
const now = Date.now();
const cacheKey = this.getCacheKey(vaultDir);
const cached = this.cache.get(cacheKey);
// Cache valide ?
if (cached && (now - cached.timestamp) < this.ttl) {
this.hits++;
return cached.data;
}
// Cache miss - recharger
this.misses++;
return await this.loadFreshMetadata(vaultDir, cacheKey, loader);
}
/**
* Charger les métadonnées fraiches
*/
async loadFreshMetadata(vaultDir, cacheKey, loader) {
// Éviter les chargements concurrents
if (this.isLoading && this.loadPromise) {
return this.loadPromise;
}
this.isLoading = true;
try {
this.loadPromise = loader();
const metadata = await this.loadPromise;
// Stocker en cache
this.cache.set(cacheKey, {
data: metadata,
timestamp: Date.now()
});
// Nettoyer le cache si trop gros
this.cleanupIfNeeded();
return metadata;
} finally {
this.isLoading = false;
this.loadPromise = null;
}
}
/**
* Invalider le cache pour une voûte
*/
invalidate(vaultDir = null) {
if (vaultDir) {
const cacheKey = this.getCacheKey(vaultDir);
this.cache.delete(cacheKey);
} else {
this.cache.clear();
}
}
/**
* Générer une clé de cache unique
*/
getCacheKey(vaultDir) {
return `metadata_${vaultDir.replace(/[/\\]/g, '_')}`;
}
/**
* Nettoyer le cache si nécessaire (LRU simple)
*/
cleanupIfNeeded() {
if (this.cache.size > this.maxSize) {
// Supprimer les entrées les plus anciennes (LRU simple)
const entries = Array.from(this.cache.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = entries.slice(0, Math.floor(this.maxSize * 0.1));
toRemove.forEach(([key]) => this.cache.delete(key));
console.log(`[Cache] Cleaned up ${toRemove.length} old entries`);
}
}
/**
* Obtenir les statistiques du cache
*/
getStats() {
const total = this.hits + this.misses;
return {
size: this.cache.size,
hitRate: total > 0 ? (this.hits / total) * 100 : 0,
hits: this.hits,
misses: this.misses,
maxSize: this.maxSize,
ttl: this.ttl
};
}
/**
* Réinitialiser les métriques
*/
resetStats() {
this.hits = 0;
this.misses = 0;
}
}
/**
* Performance logger
*/
export class PerformanceLogger {
static log(operation, duration, metadata = {}) {
if (!PERFORMANCE_CONFIG.ENABLE_PERF_LOGGING) {
return;
}
const level = duration > 1000 ? 'warn' : 'info';
const message = `[PERF] ${operation}: ${duration.toFixed(0)}ms`;
if (level === 'warn') {
console.warn(message, metadata);
} else {
console.log(message, metadata);
}
}
static mark(label) {
if (typeof performance !== 'undefined' && performance.mark) {
performance.mark(label);
}
}
static measure(label, startMark, endMark) {
if (typeof performance !== 'undefined' && performance.measure) {
try {
performance.measure(label, startMark, endMark);
} catch (e) {
// Ignore if marks don't exist
}
}
}
}