Implement LRU Cache with TTL and performance tracking in CacheLookupTool

- Introduced a new LRU cache implementation with time-to-live (TTL) support, enhancing performance for cached results.
- Added detailed cache statistics and performance tracking, including hit/miss counts and eviction metrics.
- Implemented methods for cache entry management, including eviction of least recently used entries and retrieval of top accessed entries.
- Enhanced the clearExpired and getStats methods for better cache management and reporting.
- Added comprehensive logging for cache operations to improve observability and debugging.
This commit is contained in:
Nikhil-Doye
2025-10-27 19:01:10 -04:00
parent 5effc0fe05
commit 3e2d297e56
+200 -14
View File
@@ -1,10 +1,40 @@
import { BaseTool } from "./BaseTool";
import { ToolParameter, ToolResult } from "../../types/tools";
/**
* Cache entry interface with LRU tracking
*/
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
lastAccessed: number;
accessCount: number;
}
/**
* Cache statistics for performance monitoring
*/
interface CacheStats {
size: number;
maxSize: number;
hitRate: number;
hits: number;
misses: number;
evictions: number;
totalRequests: number;
averageAccessCount: number;
oldestEntryAge: number;
newestEntryAge: number;
}
/**
* LRU Cache implementation with TTL support and performance tracking
*/
export class CacheLookupTool extends BaseTool {
name = "cache_lookup";
description =
"Look up cached results for similar requests to improve performance";
"Look up cached results for similar requests to improve performance with LRU eviction";
parameters: ToolParameter[] = [
{
name: "key",
@@ -14,11 +44,14 @@ export class CacheLookupTool extends BaseTool {
},
];
private cache = new Map<
string,
{ data: any; timestamp: number; ttl: number }
>();
private cache = new Map<string, CacheEntry>();
private readonly DEFAULT_TTL = 5 * 60 * 1000; // 5 minutes
private readonly MAX_SIZE = 100; // Maximum cache entries
// Performance tracking
private hits = 0;
private misses = 0;
private evictions = 0;
async execute(params: Record<string, any>): Promise<ToolResult> {
const { key } = params;
@@ -52,6 +85,7 @@ export class CacheLookupTool extends BaseTool {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return null; // Cache miss
}
@@ -59,19 +93,63 @@ export class CacheLookupTool extends BaseTool {
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
this.misses++;
return null; // Cache expired
}
// Cache hit - update LRU tracking
this.hits++;
entry.lastAccessed = now;
entry.accessCount++;
// Move to end of Map (most recently used)
this.cache.delete(key);
this.cache.set(key, entry);
return entry.data;
}
// Helper method to store data in cache
/**
* Store data in cache with LRU eviction
*/
store(key: string, data: any, ttl: number = this.DEFAULT_TTL): void {
this.cache.set(key, {
const now = Date.now();
// Check if we need to evict entries (LRU)
if (this.cache.size >= this.MAX_SIZE && !this.cache.has(key)) {
this.evictLRU();
}
// Update existing entry or add new one
const entry: CacheEntry = {
data,
timestamp: Date.now(),
timestamp: now,
ttl,
});
lastAccessed: now,
accessCount: 0,
};
// Remove old entry if exists (to move to end of Map)
if (this.cache.has(key)) {
this.cache.delete(key);
}
this.cache.set(key, entry);
}
/**
* Evict least recently used entry
*/
private evictLRU(): void {
if (this.cache.size === 0) return;
// First entry in Map is least recently used (due to our re-ordering on access)
const firstKey = this.cache.keys().next().value;
if (firstKey) {
this.cache.delete(firstKey);
this.evictions++;
console.log(`[CacheLookupTool] LRU eviction: ${firstKey}`);
}
}
// Helper method to generate cache key from user input
@@ -79,21 +157,129 @@ export class CacheLookupTool extends BaseTool {
return userInput.toLowerCase().trim().replace(/\s+/g, "_");
}
// Helper method to clear expired entries
clearExpired(): void {
/**
* Clear expired entries
*/
clearExpired(): number {
const now = Date.now();
let cleared = 0;
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
cleared++;
}
}
if (cleared > 0) {
console.log(`[CacheLookupTool] Cleared ${cleared} expired entries`);
}
return cleared;
}
// Helper method to get cache statistics
getStats(): { size: number; hitRate: number } {
/**
* Get comprehensive cache statistics
*/
getStats(): CacheStats {
const totalRequests = this.hits + this.misses;
const hitRate = totalRequests > 0 ? this.hits / totalRequests : 0;
// Calculate average access count
let totalAccessCount = 0;
let oldestTimestamp = Date.now();
let newestTimestamp = 0;
for (const entry of this.cache.values()) {
totalAccessCount += entry.accessCount;
oldestTimestamp = Math.min(oldestTimestamp, entry.timestamp);
newestTimestamp = Math.max(newestTimestamp, entry.timestamp);
}
const averageAccessCount =
this.cache.size > 0 ? totalAccessCount / this.cache.size : 0;
const now = Date.now();
const oldestEntryAge = this.cache.size > 0 ? now - oldestTimestamp : 0;
const newestEntryAge = this.cache.size > 0 ? now - newestTimestamp : 0;
return {
size: this.cache.size,
hitRate: 0.8, // Placeholder - would track actual hit rate
maxSize: this.MAX_SIZE,
hitRate: Number(hitRate.toFixed(4)),
hits: this.hits,
misses: this.misses,
evictions: this.evictions,
totalRequests,
averageAccessCount: Number(averageAccessCount.toFixed(2)),
oldestEntryAge: Math.floor(oldestEntryAge / 1000), // seconds
newestEntryAge: Math.floor(newestEntryAge / 1000), // seconds
};
}
/**
* Get detailed cache report for monitoring
*/
getDetailedReport(): string {
const stats = this.getStats();
const utilizationPercent = ((stats.size / stats.maxSize) * 100).toFixed(1);
return `
Cache Performance Report
========================
Size: ${stats.size}/${stats.maxSize} (${utilizationPercent}% utilized)
Hit Rate: ${(stats.hitRate * 100).toFixed(2)}%
Hits: ${stats.hits}
Misses: ${stats.misses}
Total Requests: ${stats.totalRequests}
Evictions: ${stats.evictions}
Average Access Count: ${stats.averageAccessCount}
Oldest Entry: ${stats.oldestEntryAge}s ago
Newest Entry: ${stats.newestEntryAge}s ago
Performance:
- Cache efficiency: ${
stats.hitRate >= 0.7 ? "Good ✓" : stats.hitRate >= 0.5 ? "Fair" : "Poor ✗"
}
- Memory usage: ${stats.size >= stats.maxSize * 0.9 ? "High ⚠" : "Normal"}
- Eviction pressure: ${
stats.evictions > stats.totalRequests * 0.1 ? "High ⚠" : "Normal"
}
`.trim();
}
/**
* Reset all statistics (useful for testing)
*/
resetStats(): void {
this.hits = 0;
this.misses = 0;
this.evictions = 0;
}
/**
* Clear all cache entries
*/
clear(): void {
this.cache.clear();
console.log("[CacheLookupTool] Cache cleared");
}
/**
* Get cache entries sorted by access frequency (for analysis)
*/
getTopEntries(
limit: number = 10
): Array<{ key: string; accessCount: number; age: number }> {
const entries = Array.from(this.cache.entries())
.map(([key, entry]) => ({
key,
accessCount: entry.accessCount,
age: Math.floor((Date.now() - entry.timestamp) / 1000),
}))
.sort((a, b) => b.accessCount - a.accessCount)
.slice(0, limit);
return entries;
}
}