319 lines
9.5 KiB
JavaScript
319 lines
9.5 KiB
JavaScript
|
|
/**
|
||
|
|
* Universal Display Augmentation - Intelligent Caching System
|
||
|
|
*
|
||
|
|
* High-performance LRU cache with smart eviction and batch optimization
|
||
|
|
* Designed for minimal memory footprint and maximum hit ratio
|
||
|
|
*/
|
||
|
|
/**
|
||
|
|
* LRU (Least Recently Used) Cache for computed display fields
|
||
|
|
* Optimized for the display augmentation use case
|
||
|
|
*/
|
||
|
|
export class DisplayCache {
|
||
|
|
constructor(maxSize = 1000) {
|
||
|
|
this.cache = new Map();
|
||
|
|
this.stats = {
|
||
|
|
hits: 0,
|
||
|
|
misses: 0,
|
||
|
|
evictions: 0,
|
||
|
|
totalComputations: 0,
|
||
|
|
totalComputationTime: 0
|
||
|
|
};
|
||
|
|
this.maxSize = maxSize;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get cached display fields with LRU update
|
||
|
|
* @param key Cache key
|
||
|
|
* @returns Cached fields or null if not found
|
||
|
|
*/
|
||
|
|
get(key) {
|
||
|
|
const entry = this.cache.get(key);
|
||
|
|
if (!entry) {
|
||
|
|
this.stats.misses++;
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
// Update LRU - move to end
|
||
|
|
this.cache.delete(key);
|
||
|
|
entry.lastAccessed = Date.now();
|
||
|
|
entry.accessCount++;
|
||
|
|
this.cache.set(key, entry);
|
||
|
|
this.stats.hits++;
|
||
|
|
return entry.fields;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Store computed display fields in cache
|
||
|
|
* @param key Cache key
|
||
|
|
* @param fields Computed display fields
|
||
|
|
* @param computationTime Time taken to compute (for stats)
|
||
|
|
*/
|
||
|
|
set(key, fields, computationTime) {
|
||
|
|
// Remove if already exists (for LRU update)
|
||
|
|
if (this.cache.has(key)) {
|
||
|
|
this.cache.delete(key);
|
||
|
|
}
|
||
|
|
// Create cache entry
|
||
|
|
const entry = {
|
||
|
|
fields,
|
||
|
|
lastAccessed: Date.now(),
|
||
|
|
accessCount: 1
|
||
|
|
};
|
||
|
|
// Add to end (most recently used)
|
||
|
|
this.cache.set(key, entry);
|
||
|
|
// Update stats
|
||
|
|
this.stats.totalComputations++;
|
||
|
|
if (computationTime) {
|
||
|
|
this.stats.totalComputationTime += computationTime;
|
||
|
|
}
|
||
|
|
// Evict oldest if over capacity
|
||
|
|
if (this.cache.size > this.maxSize) {
|
||
|
|
this.evictOldest();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Check if a key exists in cache without affecting LRU order
|
||
|
|
* @param key Cache key
|
||
|
|
* @returns True if key exists
|
||
|
|
*/
|
||
|
|
has(key) {
|
||
|
|
return this.cache.has(key);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Generate cache key from data
|
||
|
|
* @param id Entity ID (preferred)
|
||
|
|
* @param data Fallback data for key generation
|
||
|
|
* @param entityType Type of entity (noun/verb)
|
||
|
|
* @returns Cache key string
|
||
|
|
*/
|
||
|
|
generateKey(id, data, entityType = 'noun') {
|
||
|
|
// Use ID if available (most reliable)
|
||
|
|
if (id) {
|
||
|
|
return `${entityType}:${id}`;
|
||
|
|
}
|
||
|
|
// Generate hash from data
|
||
|
|
if (data) {
|
||
|
|
const dataString = JSON.stringify(data, Object.keys(data).sort());
|
||
|
|
const hash = this.simpleHash(dataString);
|
||
|
|
return `${entityType}:hash:${hash}`;
|
||
|
|
}
|
||
|
|
// Fallback to timestamp (not ideal but prevents crashes)
|
||
|
|
return `${entityType}:temp:${Date.now()}:${Math.random()}`;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear all cached entries
|
||
|
|
*/
|
||
|
|
clear() {
|
||
|
|
this.cache.clear();
|
||
|
|
this.stats = {
|
||
|
|
hits: 0,
|
||
|
|
misses: 0,
|
||
|
|
evictions: 0,
|
||
|
|
totalComputations: 0,
|
||
|
|
totalComputationTime: 0
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get cache statistics
|
||
|
|
* @returns Cache performance statistics
|
||
|
|
*/
|
||
|
|
getStats() {
|
||
|
|
const hitRatio = this.stats.hits + this.stats.misses > 0
|
||
|
|
? this.stats.hits / (this.stats.hits + this.stats.misses)
|
||
|
|
: 0;
|
||
|
|
const avgComputationTime = this.stats.totalComputations > 0
|
||
|
|
? this.stats.totalComputationTime / this.stats.totalComputations
|
||
|
|
: 0;
|
||
|
|
// Analyze cached types for common types statistics
|
||
|
|
const typeCount = new Map();
|
||
|
|
let fastestComputation = Infinity;
|
||
|
|
let slowestComputation = 0;
|
||
|
|
for (const entry of this.cache.values()) {
|
||
|
|
const type = entry.fields.type;
|
||
|
|
typeCount.set(type, (typeCount.get(type) || 0) + 1);
|
||
|
|
}
|
||
|
|
const commonTypes = Array.from(typeCount.entries())
|
||
|
|
.sort(([, a], [, b]) => b - a)
|
||
|
|
.slice(0, 10)
|
||
|
|
.map(([type, count]) => ({
|
||
|
|
type,
|
||
|
|
count,
|
||
|
|
percentage: Math.round((count / this.cache.size) * 100)
|
||
|
|
}));
|
||
|
|
return {
|
||
|
|
totalComputations: this.stats.totalComputations,
|
||
|
|
cacheHitRatio: Math.round(hitRatio * 100) / 100,
|
||
|
|
averageComputationTime: Math.round(avgComputationTime * 100) / 100,
|
||
|
|
commonTypes,
|
||
|
|
performance: {
|
||
|
|
fastestComputation,
|
||
|
|
slowestComputation,
|
||
|
|
totalComputationTime: this.stats.totalComputationTime
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get current cache size
|
||
|
|
* @returns Number of cached entries
|
||
|
|
*/
|
||
|
|
size() {
|
||
|
|
return this.cache.size;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get cache capacity
|
||
|
|
* @returns Maximum cache size
|
||
|
|
*/
|
||
|
|
capacity() {
|
||
|
|
return this.maxSize;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Evict least recently used entry
|
||
|
|
*/
|
||
|
|
evictOldest() {
|
||
|
|
// First entry is oldest (LRU)
|
||
|
|
const firstKey = this.cache.keys().next().value;
|
||
|
|
if (firstKey) {
|
||
|
|
this.cache.delete(firstKey);
|
||
|
|
this.stats.evictions++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Simple hash function for cache keys
|
||
|
|
* @param str String to hash
|
||
|
|
* @returns Simple hash number
|
||
|
|
*/
|
||
|
|
simpleHash(str) {
|
||
|
|
let hash = 0;
|
||
|
|
for (let i = 0; i < str.length; i++) {
|
||
|
|
const char = str.charCodeAt(i);
|
||
|
|
hash = ((hash << 5) - hash) + char;
|
||
|
|
hash = hash & hash; // Convert to 32-bit integer
|
||
|
|
}
|
||
|
|
return Math.abs(hash);
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Optimize cache by removing stale entries
|
||
|
|
* Called periodically to maintain cache health
|
||
|
|
*/
|
||
|
|
optimizeCache() {
|
||
|
|
const now = Date.now();
|
||
|
|
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
||
|
|
const minAccessCount = 2; // Minimum access count to keep
|
||
|
|
const toDelete = [];
|
||
|
|
for (const [key, entry] of this.cache.entries()) {
|
||
|
|
// Remove very old entries with low access count
|
||
|
|
if (now - entry.lastAccessed > maxAge && entry.accessCount < minAccessCount) {
|
||
|
|
toDelete.push(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// Remove stale entries
|
||
|
|
for (const key of toDelete) {
|
||
|
|
this.cache.delete(key);
|
||
|
|
this.stats.evictions++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Precompute display fields for a batch of entities
|
||
|
|
* @param entities Array of entities with their compute functions
|
||
|
|
* @returns Promise resolving when batch is complete
|
||
|
|
*/
|
||
|
|
async batchPrecompute(entities) {
|
||
|
|
const promises = entities.map(async ({ key, computeFn }) => {
|
||
|
|
if (!this.has(key)) {
|
||
|
|
const startTime = Date.now();
|
||
|
|
try {
|
||
|
|
const fields = await computeFn();
|
||
|
|
const computationTime = Date.now() - startTime;
|
||
|
|
this.set(key, fields, computationTime);
|
||
|
|
}
|
||
|
|
catch (error) {
|
||
|
|
console.warn(`Batch precompute failed for key ${key}:`, error);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
await Promise.all(promises);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Request deduplicator for batch processing
|
||
|
|
* Prevents duplicate computations for the same data
|
||
|
|
*/
|
||
|
|
export class RequestDeduplicator {
|
||
|
|
constructor(batchSize = 50) {
|
||
|
|
this.pendingRequests = new Map();
|
||
|
|
this.batchSize = batchSize;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Deduplicate computation request
|
||
|
|
* @param key Unique key for the computation
|
||
|
|
* @param computeFn Function to compute the result
|
||
|
|
* @returns Promise that resolves to the computed fields
|
||
|
|
*/
|
||
|
|
async deduplicate(key, computeFn) {
|
||
|
|
// Return existing promise if already pending
|
||
|
|
if (this.pendingRequests.has(key)) {
|
||
|
|
return this.pendingRequests.get(key);
|
||
|
|
}
|
||
|
|
// Create new computation promise
|
||
|
|
const promise = computeFn().finally(() => {
|
||
|
|
// Remove from pending when complete
|
||
|
|
this.pendingRequests.delete(key);
|
||
|
|
});
|
||
|
|
this.pendingRequests.set(key, promise);
|
||
|
|
return promise;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Get number of pending requests
|
||
|
|
* @returns Number of pending computations
|
||
|
|
*/
|
||
|
|
getPendingCount() {
|
||
|
|
return this.pendingRequests.size;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear all pending requests
|
||
|
|
*/
|
||
|
|
clear() {
|
||
|
|
this.pendingRequests.clear();
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Shutdown the deduplicator
|
||
|
|
*/
|
||
|
|
shutdown() {
|
||
|
|
this.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Global cache instance management
|
||
|
|
* Provides singleton access to display cache
|
||
|
|
*/
|
||
|
|
let globalDisplayCache = null;
|
||
|
|
/**
|
||
|
|
* Get global display cache instance
|
||
|
|
* @param maxSize Optional cache size (only used on first call)
|
||
|
|
* @returns Shared display cache instance
|
||
|
|
*/
|
||
|
|
export function getGlobalDisplayCache(maxSize) {
|
||
|
|
if (!globalDisplayCache) {
|
||
|
|
globalDisplayCache = new DisplayCache(maxSize);
|
||
|
|
// Set up periodic optimization
|
||
|
|
setInterval(() => {
|
||
|
|
globalDisplayCache?.optimizeCache();
|
||
|
|
}, 60 * 60 * 1000); // Every hour
|
||
|
|
}
|
||
|
|
return globalDisplayCache;
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Clear global cache (for testing or memory management)
|
||
|
|
*/
|
||
|
|
export function clearGlobalDisplayCache() {
|
||
|
|
if (globalDisplayCache) {
|
||
|
|
globalDisplayCache.clear();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
/**
|
||
|
|
* Shutdown global cache and cleanup
|
||
|
|
*/
|
||
|
|
export function shutdownGlobalDisplayCache() {
|
||
|
|
if (globalDisplayCache) {
|
||
|
|
globalDisplayCache.clear();
|
||
|
|
globalDisplayCache = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=cache.js.map
|