/** * Multi-level Cache Manager * * Implements a three-level caching strategy: * - Level 1: Hot cache (most accessed nodes) - RAM (automatically detecting and adjusting in each environment) * - Level 2: Warm cache (recent nodes) - OPFS, Filesystem or S3 depending on environment * - Level 3: Cold storage (all nodes) - OPFS, Filesystem or S3 depending on environment */ // Environment detection for storage selection var Environment; (function (Environment) { Environment[Environment["BROWSER"] = 0] = "BROWSER"; Environment[Environment["NODE"] = 1] = "NODE"; Environment[Environment["WORKER"] = 2] = "WORKER"; })(Environment || (Environment = {})); // Storage type for warm and cold caches var StorageType; (function (StorageType) { StorageType[StorageType["MEMORY"] = 0] = "MEMORY"; StorageType[StorageType["OPFS"] = 1] = "OPFS"; StorageType[StorageType["FILESYSTEM"] = 2] = "FILESYSTEM"; StorageType[StorageType["S3"] = 3] = "S3"; StorageType[StorageType["REMOTE_API"] = 4] = "REMOTE_API"; })(StorageType || (StorageType = {})); /** * Multi-level cache manager for efficient data access */ export class CacheManager { /** * Initialize the cache manager * @param options Configuration options */ constructor(options = {}) { // Hot cache (RAM) this.hotCache = new Map(); // Cache statistics this.stats = { hits: 0, misses: 0, evictions: 0, size: 0, maxSize: 0, hotCacheSize: 0, warmCacheSize: 0, hotCacheHits: 0, hotCacheMisses: 0, warmCacheHits: 0, warmCacheMisses: 0 }; this.lastAutoTuneTime = 0; this.autoTuneInterval = 5 * 60 * 1000; // 5 minutes this.storageStatistics = null; // Store options for later reference this.options = options; // Detect environment this.environment = this.detectEnvironment(); // Set storage types based on environment this.warmStorageType = this.detectWarmStorageType(); this.coldStorageType = this.detectColdStorageType(); // Initialize storage adapters this.warmStorage = options.warmStorage || this.initializeWarmStorage(); this.coldStorage = options.coldStorage || this.initializeColdStorage(); // Set auto-tuning flag this.autoTune = options.autoTune !== undefined ? options.autoTune : true; // Get environment-specific configuration if available const envConfig = options.environmentConfig?.[Environment[this.environment].toLowerCase()]; // Set default values or use environment-specific values or global values this.hotCacheMaxSize = envConfig?.hotCacheMaxSize || options.hotCacheMaxSize || this.detectOptimalCacheSize(); this.hotCacheEvictionThreshold = envConfig?.hotCacheEvictionThreshold || options.hotCacheEvictionThreshold || 0.8; this.warmCacheTTL = envConfig?.warmCacheTTL || options.warmCacheTTL || 24 * 60 * 60 * 1000; // 24 hours this.batchSize = envConfig?.batchSize || options.batchSize || 10; // If auto-tuning is enabled, perform initial tuning if (this.autoTune) { this.tuneParameters(); } // Log configuration if (process.env.DEBUG) { console.log('Cache Manager initialized with configuration:', { environment: Environment[this.environment], hotCacheMaxSize: this.hotCacheMaxSize, hotCacheEvictionThreshold: this.hotCacheEvictionThreshold, warmCacheTTL: this.warmCacheTTL, batchSize: this.batchSize, autoTune: this.autoTune, warmStorageType: StorageType[this.warmStorageType], coldStorageType: StorageType[this.coldStorageType] }); } } /** * Detect the current environment */ detectEnvironment() { if (typeof window !== 'undefined' && typeof document !== 'undefined') { return Environment.BROWSER; } else if (typeof self !== 'undefined' && typeof window === 'undefined') { // In a worker environment, self is defined but window is not return Environment.WORKER; } else { return Environment.NODE; } } /** * Detect the optimal cache size based on available memory and operating mode * * Enhanced to better handle large datasets in S3 or other storage: * - Increases cache size for read-only mode * - Adjusts based on total dataset size when available * - Provides more aggressive caching for large datasets * - Optimizes memory usage based on environment */ detectOptimalCacheSize() { try { // Default to a conservative value const defaultSize = 1000; // Get the total dataset size if available const totalItems = this.storageStatistics ? (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0; // Determine if we're dealing with a large dataset (>100K items) const isLargeDataset = totalItems > 100000; // Check if we're in read-only mode (from parent BrainyData instance) const isReadOnly = this.options?.readOnly || false; // In Node.js, use available system memory with enhanced allocation if (this.environment === Environment.NODE) { try { // For ES module compatibility, we'll use a fixed default value // since we can't use dynamic imports in a synchronous function // Use conservative defaults that don't require OS module // These values are reasonable for most systems const estimatedTotalMemory = 8 * 1024 * 1024 * 1024; // Assume 8GB total const estimatedFreeMemory = 4 * 1024 * 1024 * 1024; // Assume 4GB free // Estimate average entry size (in bytes) // This is a conservative estimate for complex objects with vectors const ESTIMATED_BYTES_PER_ENTRY = 1024; // 1KB per entry // Base memory percentage - 10% by default let memoryPercentage = 0.1; // Adjust based on operating mode and dataset size if (isReadOnly) { // In read-only mode, we can use more memory for caching memoryPercentage = 0.25; // 25% of free memory // For large datasets in read-only mode, be even more aggressive if (isLargeDataset) { memoryPercentage = 0.4; // 40% of free memory } } else if (isLargeDataset) { // For large datasets in normal mode, increase slightly memoryPercentage = 0.15; // 15% of free memory } // Calculate optimal size based on adjusted percentage const optimalSize = Math.max(Math.floor(estimatedFreeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY), 1000); // If we know the total dataset size, cap at a reasonable percentage if (totalItems > 0) { // In read-only mode, we can cache a larger percentage const maxPercentage = isReadOnly ? 0.5 : 0.3; const maxItems = Math.ceil(totalItems * maxPercentage); // Return the smaller of the two to avoid excessive memory usage return Math.min(optimalSize, maxItems); } return optimalSize; } catch (error) { console.warn('Failed to detect optimal cache size:', error); return defaultSize; } } // In browser, use navigator.deviceMemory with enhanced allocation if (this.environment === Environment.BROWSER && navigator.deviceMemory) { // Base entries per GB let entriesPerGB = 500; // Adjust based on operating mode and dataset size if (isReadOnly) { entriesPerGB = 800; // More aggressive caching in read-only mode if (isLargeDataset) { entriesPerGB = 1000; // Even more aggressive for large datasets } } else if (isLargeDataset) { entriesPerGB = 600; // Slightly more aggressive for large datasets } // Calculate based on device memory const browserCacheSize = Math.max(navigator.deviceMemory * entriesPerGB, 1000); // If we know the total dataset size, cap at a reasonable percentage if (totalItems > 0) { // In read-only mode, we can cache a larger percentage const maxPercentage = isReadOnly ? 0.4 : 0.25; const maxItems = Math.ceil(totalItems * maxPercentage); // Return the smaller of the two to avoid excessive memory usage return Math.min(browserCacheSize, maxItems); } return browserCacheSize; } // For worker environments or when memory detection fails if (this.environment === Environment.WORKER) { // Workers typically have limited memory, be conservative return isReadOnly ? 2000 : 1000; } return defaultSize; } catch (error) { console.warn('Error detecting optimal cache size:', error); return 1000; // Conservative default } } /** * Async version of detectOptimalCacheSize that uses dynamic imports * to access system information in Node.js environments * * This method provides more accurate memory detection by using * the OS module's dynamic import in Node.js environments */ async detectOptimalCacheSizeAsync() { try { // Default to a conservative value const defaultSize = 1000; // Get the total dataset size if available const totalItems = this.storageStatistics ? (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0; // Determine if we're dealing with a large dataset (>100K items) const isLargeDataset = totalItems > 100000; // Check if we're in read-only mode (from parent BrainyData instance) const isReadOnly = this.options?.readOnly || false; // Get memory information based on environment const memoryInfo = await this.detectAvailableMemory(); // If memory detection failed, use the synchronous method if (!memoryInfo) { return this.detectOptimalCacheSize(); } // Estimate average entry size (in bytes) // This is a conservative estimate for complex objects with vectors const ESTIMATED_BYTES_PER_ENTRY = 1024; // 1KB per entry // Base memory percentage - 10% by default let memoryPercentage = 0.1; // Adjust based on operating mode and dataset size if (isReadOnly) { // In read-only mode, we can use more memory for caching memoryPercentage = 0.25; // 25% of free memory // For large datasets in read-only mode, be even more aggressive if (isLargeDataset) { memoryPercentage = 0.4; // 40% of free memory } } else if (isLargeDataset) { // For large datasets in normal mode, increase slightly memoryPercentage = 0.15; // 15% of free memory } // Calculate optimal size based on adjusted percentage const optimalSize = Math.max(Math.floor(memoryInfo.freeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY), 1000); // If we know the total dataset size, cap at a reasonable percentage if (totalItems > 0) { // In read-only mode, we can cache a larger percentage const maxPercentage = isReadOnly ? 0.5 : 0.3; const maxItems = Math.ceil(totalItems * maxPercentage); // Return the smaller of the two to avoid excessive memory usage return Math.min(optimalSize, maxItems); } return optimalSize; } catch (error) { console.warn('Error detecting optimal cache size asynchronously:', error); return 1000; // Conservative default } } /** * Detects available memory across different environments * * This method uses different techniques to detect memory in: * - Node.js: Uses the OS module with dynamic import * - Browser: Uses performance.memory or navigator.deviceMemory * - Worker: Uses performance.memory if available * * @returns An object with totalMemory and freeMemory in bytes, or null if detection fails */ async detectAvailableMemory() { try { // Node.js environment if (this.environment === Environment.NODE) { try { // Use dynamic import for OS module const os = await import('os'); // Get actual system memory information const totalMemory = os.totalmem(); const freeMemory = os.freemem(); return { totalMemory, freeMemory }; } catch (error) { console.warn('Failed to detect memory in Node.js environment:', error); } } // Browser environment if (this.environment === Environment.BROWSER) { // Try using performance.memory (Chrome only) if (performance && performance.memory) { const memoryInfo = performance.memory; // jsHeapSizeLimit is the maximum size of the heap // totalJSHeapSize is the currently allocated heap size // usedJSHeapSize is the amount of heap currently being used const totalMemory = memoryInfo.jsHeapSizeLimit || 0; const usedMemory = memoryInfo.usedJSHeapSize || 0; const freeMemory = Math.max(totalMemory - usedMemory, 0); return { totalMemory, freeMemory }; } // Try using navigator.deviceMemory as fallback if (navigator.deviceMemory) { // deviceMemory is in GB, convert to bytes const totalMemory = navigator.deviceMemory * 1024 * 1024 * 1024; // Assume 50% is free const freeMemory = totalMemory * 0.5; return { totalMemory, freeMemory }; } } // Worker environment if (this.environment === Environment.WORKER) { // Try using performance.memory if available (Chrome workers) if (performance && performance.memory) { const memoryInfo = performance.memory; const totalMemory = memoryInfo.jsHeapSizeLimit || 0; const usedMemory = memoryInfo.usedJSHeapSize || 0; const freeMemory = Math.max(totalMemory - usedMemory, 0); return { totalMemory, freeMemory }; } // For workers, use a conservative estimate // Assume 2GB total memory with 1GB free return { totalMemory: 2 * 1024 * 1024 * 1024, freeMemory: 1 * 1024 * 1024 * 1024 }; } // If all detection methods fail, use conservative defaults return { totalMemory: 8 * 1024 * 1024 * 1024, // Assume 8GB total freeMemory: 4 * 1024 * 1024 * 1024 // Assume 4GB free }; } catch (error) { console.warn('Memory detection failed:', error); return null; } } /** * Tune cache parameters based on statistics and environment * This method is called periodically if auto-tuning is enabled * * The auto-tuning process: * 1. Retrieves storage statistics if available * 2. Tunes each parameter based on statistics and environment * 3. Logs the tuned parameters if debug is enabled * * Auto-tuning helps optimize cache performance by adapting to: * - The current environment (Node.js, browser, worker) * - Available system resources (memory, CPU) * - Usage patterns (read-heavy vs. write-heavy workloads) * - Cache efficiency (hit/miss ratios) */ async tuneParameters() { // Skip if auto-tuning is disabled if (!this.autoTune) return; // Check if it's time to tune parameters const now = Date.now(); if (now - this.lastAutoTuneTime < this.autoTuneInterval) return; // Update last tune time this.lastAutoTuneTime = now; try { // Get storage statistics if available if (this.coldStorage && typeof this.coldStorage.getStatistics === 'function') { this.storageStatistics = await this.coldStorage.getStatistics(); } // Get cache statistics for adaptive tuning const cacheStats = this.getStats(); // Use the async version of tuneHotCacheSize which uses detectOptimalCacheSizeAsync await this.tuneHotCacheSize(); // Tune eviction threshold based on hit/miss ratio this.tuneEvictionThreshold(cacheStats); // Tune warm cache TTL based on access patterns this.tuneWarmCacheTTL(cacheStats); // Tune batch size based on access patterns and storage type this.tuneBatchSize(cacheStats); // Log tuned parameters if debug is enabled if (process.env.DEBUG) { console.log('Cache parameters auto-tuned:', { hotCacheMaxSize: this.hotCacheMaxSize, hotCacheEvictionThreshold: this.hotCacheEvictionThreshold, warmCacheTTL: this.warmCacheTTL, batchSize: this.batchSize, cacheStats: { hotCacheSize: cacheStats.hotCacheSize, warmCacheSize: cacheStats.warmCacheSize, hotCacheHits: cacheStats.hotCacheHits, hotCacheMisses: cacheStats.hotCacheMisses, warmCacheHits: cacheStats.warmCacheHits, warmCacheMisses: cacheStats.warmCacheMisses } }); } } catch (error) { console.warn('Error during cache parameter auto-tuning:', error); } } /** * Tune hot cache size based on statistics, environment, and operating mode * * The hot cache size is tuned based on: * 1. Available memory in the current environment * 2. Total number of nodes and edges in the system * 3. Cache hit/miss ratio * 4. Operating mode (read-only vs. read-write) * 5. Storage type (S3, filesystem, memory) * * Enhanced algorithm: * - Start with a size based on available memory and operating mode * - For large datasets in S3 or other remote storage, use more aggressive caching * - Adjust based on access patterns (read-heavy vs. write-heavy) * - For read-only mode, prioritize cache size over eviction speed * - Dynamically adjust based on hit/miss ratio and query patterns */ async tuneHotCacheSize() { // Use the async version to get more accurate memory information let optimalSize = await this.detectOptimalCacheSizeAsync(); // Check if we're in read-only mode const isReadOnly = this.options?.readOnly || false; // Check if we're using S3 or other remote storage const isRemoteStorage = this.coldStorageType === StorageType.S3 || this.coldStorageType === StorageType.REMOTE_API; // If we have storage statistics, adjust based on total nodes/edges if (this.storageStatistics) { const totalItems = (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0); // If total items is significant, adjust cache size if (totalItems > 0) { // Base percentage to cache - adjusted based on mode and storage let percentageToCache = 0.2; // Cache 20% of items by default // For read-only mode, increase cache percentage if (isReadOnly) { percentageToCache = 0.3; // 30% for read-only mode // For remote storage in read-only mode, be even more aggressive if (isRemoteStorage) { percentageToCache = 0.4; // 40% for remote storage in read-only mode } } // For remote storage in normal mode, increase slightly else if (isRemoteStorage) { percentageToCache = 0.25; // 25% for remote storage } // For large datasets, cap the percentage to avoid excessive memory usage if (totalItems > 1000000) { // Over 1 million items percentageToCache = Math.min(percentageToCache, 0.15); } else if (totalItems > 100000) { // Over 100K items percentageToCache = Math.min(percentageToCache, 0.25); } const statisticsBasedSize = Math.ceil(totalItems * percentageToCache); // Use the smaller of the two to avoid memory issues optimalSize = Math.min(optimalSize, statisticsBasedSize); } } // Adjust based on hit/miss ratio if we have enough data const totalAccesses = this.stats.hits + this.stats.misses; if (totalAccesses > 100) { const hitRatio = this.stats.hits / totalAccesses; // Base adjustment factor let hitRatioFactor = 1.0; // If hit ratio is low, we might need a larger cache if (hitRatio < 0.5) { // Calculate adjustment factor based on hit ratio const baseAdjustment = 0.5 - hitRatio; // For read-only mode or remote storage, be more aggressive if (isReadOnly || isRemoteStorage) { hitRatioFactor = 1 + (baseAdjustment * 1.5); // Up to 75% increase } else { hitRatioFactor = 1 + baseAdjustment; // Up to 50% increase } optimalSize = Math.ceil(optimalSize * hitRatioFactor); } // If hit ratio is very high, we might be able to reduce cache size slightly else if (hitRatio > 0.9 && !isReadOnly && !isRemoteStorage) { // Only reduce cache size in normal mode with local storage // and only if hit ratio is very high hitRatioFactor = 0.9; // 10% reduction optimalSize = Math.ceil(optimalSize * hitRatioFactor); } } // Check for operation patterns if available if (this.storageStatistics?.operations) { const ops = this.storageStatistics.operations; const totalOps = ops.total || 1; // Calculate read/write ratio const readOps = (ops.search || 0) + (ops.get || 0); const writeOps = (ops.add || 0) + (ops.update || 0) + (ops.delete || 0); if (totalOps > 100) { const readRatio = readOps / totalOps; // For read-heavy workloads, increase cache size if (readRatio > 0.8) { // More aggressive for remote storage const readAdjustment = isRemoteStorage ? 1.3 : 1.2; optimalSize = Math.ceil(optimalSize * readAdjustment); } } } // Ensure we have a reasonable minimum size based on environment and mode let minSize = 1000; // Default minimum // For read-only mode, use a higher minimum if (isReadOnly) { minSize = 2000; } // For remote storage, use an even higher minimum if (isRemoteStorage) { minSize = isReadOnly ? 3000 : 2000; } optimalSize = Math.max(optimalSize, minSize); // Update the hot cache max size this.hotCacheMaxSize = optimalSize; this.stats.maxSize = optimalSize; } /** * Tune eviction threshold based on statistics * * The eviction threshold determines when items start being evicted from the hot cache. * It is tuned based on: * 1. Cache hit/miss ratio * 2. Operation patterns (read-heavy vs. write-heavy workloads) * 3. Memory pressure and available resources * * Algorithm: * - Start with a default threshold of 0.8 (80% of max size) * - For high hit ratios, increase the threshold to keep more items in cache * - For low hit ratios, decrease the threshold to evict items more aggressively * - For read-heavy workloads, use a higher threshold * - For write-heavy workloads, use a lower threshold * - Under memory pressure, use a lower threshold to conserve resources * * @param cacheStats Optional cache statistics for more adaptive tuning */ tuneEvictionThreshold(cacheStats) { // Default threshold let threshold = 0.8; // Use provided cache stats or internal stats const stats = cacheStats || this.getStats(); // Adjust based on hit/miss ratio if we have enough data const totalHotAccesses = stats.hotCacheHits + stats.hotCacheMisses; if (totalHotAccesses > 100) { const hotHitRatio = stats.hotCacheHits / totalHotAccesses; // If hit ratio is high, we can use a higher threshold // If hit ratio is low, we should use a lower threshold to evict more aggressively if (hotHitRatio > 0.8) { // High hit ratio, increase threshold (up to 0.9) threshold = Math.min(0.9, 0.8 + (hotHitRatio - 0.8) * 0.5); } else if (hotHitRatio < 0.5) { // Low hit ratio, decrease threshold (down to 0.6) threshold = Math.max(0.6, 0.8 - (0.5 - hotHitRatio) * 0.5); } } // If we have storage statistics with operation counts, adjust based on operation patterns if (this.storageStatistics && this.storageStatistics.operations) { const ops = this.storageStatistics.operations; const totalOps = ops.total || 1; // Calculate read/write ratio const readOps = ops.search || 0; const writeOps = (ops.add || 0) + (ops.update || 0) + (ops.delete || 0); if (totalOps > 100) { const readRatio = readOps / totalOps; const writeRatio = writeOps / totalOps; // For read-heavy workloads, use higher threshold // For write-heavy workloads, use lower threshold if (readRatio > 0.8) { // Read-heavy, increase threshold slightly threshold = Math.min(0.9, threshold + 0.05); } else if (writeRatio > 0.5) { // Write-heavy, decrease threshold threshold = Math.max(0.6, threshold - 0.1); } } } // Check memory pressure - if hot cache is growing too fast relative to hits, // reduce the threshold to conserve memory if (stats.hotCacheSize > 0 && totalHotAccesses > 0) { const sizeToAccessRatio = stats.hotCacheSize / totalHotAccesses; // If the ratio is high, it means we're caching a lot but not getting many hits if (sizeToAccessRatio > 10) { // Reduce threshold more aggressively under high memory pressure threshold = Math.max(0.5, threshold - 0.1); } } // If we're in read-only mode, we can be more aggressive with caching const isReadOnly = this.options?.readOnly || false; if (isReadOnly) { threshold = Math.min(0.95, threshold + 0.05); } // Update the eviction threshold this.hotCacheEvictionThreshold = threshold; } /** * Tune warm cache TTL based on statistics * * The warm cache TTL determines how long items remain in the warm cache. * It is tuned based on: * 1. Update frequency from operation statistics * 2. Warm cache hit/miss ratio * 3. Access patterns and frequency * 4. Available storage resources * * Algorithm: * - Start with a default TTL of 24 hours * - For frequently updated data, use a shorter TTL * - For rarely updated data, use a longer TTL * - For frequently accessed data, use a longer TTL * - For rarely accessed data, use a shorter TTL * - Under storage pressure, use a shorter TTL * * @param cacheStats Optional cache statistics for more adaptive tuning */ tuneWarmCacheTTL(cacheStats) { // Default TTL (24 hours) let ttl = 24 * 60 * 60 * 1000; // Use provided cache stats or internal stats const stats = cacheStats || this.getStats(); // Adjust based on warm cache hit/miss ratio if we have enough data const totalWarmAccesses = stats.warmCacheHits + stats.warmCacheMisses; if (totalWarmAccesses > 50) { const warmHitRatio = stats.warmCacheHits / totalWarmAccesses; // If warm cache hit ratio is high, items in warm cache are useful // so we should keep them longer if (warmHitRatio > 0.7) { // High hit ratio, increase TTL (up to 36 hours) ttl = Math.min(36 * 60 * 60 * 1000, ttl * (1 + (warmHitRatio - 0.7))); } else if (warmHitRatio < 0.3) { // Low hit ratio, decrease TTL (down to 12 hours) ttl = Math.max(12 * 60 * 60 * 1000, ttl * (0.8 - (0.3 - warmHitRatio))); } } // If we have storage statistics with operation counts, adjust based on update frequency if (this.storageStatistics && this.storageStatistics.operations) { const ops = this.storageStatistics.operations; const totalOps = ops.total || 1; const updateOps = (ops.update || 0); if (totalOps > 100) { const updateRatio = updateOps / totalOps; // For frequently updated data, use shorter TTL // For rarely updated data, use longer TTL if (updateRatio > 0.3) { // Frequently updated, decrease TTL (down to 6 hours) ttl = Math.max(6 * 60 * 60 * 1000, ttl * (1 - updateRatio * 0.5)); } else if (updateRatio < 0.1) { // Rarely updated, increase TTL (up to 48 hours) ttl = Math.min(48 * 60 * 60 * 1000, ttl * (1.2 - updateRatio)); } } } // Check warm cache size relative to hot cache size // If warm cache is much larger than hot cache, reduce TTL to prevent excessive storage use if (stats.warmCacheSize > 0 && stats.hotCacheSize > 0) { const warmToHotRatio = stats.warmCacheSize / stats.hotCacheSize; if (warmToHotRatio > 5) { // Warm cache is much larger than hot cache, reduce TTL ttl = Math.max(6 * 60 * 60 * 1000, ttl * (0.9 - Math.min(0.3, (warmToHotRatio - 5) / 20))); } } // If we're in read-only mode, we can use a longer TTL const isReadOnly = this.options?.readOnly || false; if (isReadOnly) { ttl = Math.min(72 * 60 * 60 * 1000, ttl * 1.5); } // Update the warm cache TTL this.warmCacheTTL = ttl; } /** * Tune batch size based on environment, statistics, and operating mode * * The batch size determines how many items are processed in a single batch * for operations like prefetching. It is tuned based on: * 1. Current environment (Node.js, browser, worker) * 2. Available memory * 3. Operation patterns * 4. Cache hit/miss ratio * 5. Operating mode (read-only vs. read-write) * 6. Storage type (S3, filesystem, memory) * 7. Dataset size * 8. Cache efficiency and access patterns * * Enhanced algorithm: * - Start with a default based on the environment * - For large datasets in S3 or other remote storage, use larger batches * - For read-only mode, use larger batches to improve throughput * - Dynamically adjust based on network latency and throughput * - Balance between memory usage and performance * - Adapt to cache hit/miss patterns * * @param cacheStats Optional cache statistics for more adaptive tuning */ tuneBatchSize(cacheStats) { // Default batch size let batchSize = 10; // Use provided cache stats or internal stats const stats = cacheStats || this.getStats(); // Check if we're in read-only mode const isReadOnly = this.options?.readOnly || false; // Check if we're using S3 or other remote storage const isRemoteStorage = this.coldStorageType === StorageType.S3 || this.coldStorageType === StorageType.REMOTE_API; // Get the total dataset size if available const totalItems = this.storageStatistics ? (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0) : 0; // Determine if we're dealing with a large dataset const isLargeDataset = totalItems > 100000; const isVeryLargeDataset = totalItems > 1000000; // Base batch size adjustment based on environment if (this.environment === Environment.NODE) { // Node.js can handle larger batches batchSize = isReadOnly ? 30 : 20; // For remote storage, increase batch size if (isRemoteStorage) { batchSize = isReadOnly ? 50 : 30; } // For large datasets, adjust batch size if (isLargeDataset) { batchSize = Math.min(100, batchSize * 1.5); } // For very large datasets, adjust even more if (isVeryLargeDataset) { batchSize = Math.min(200, batchSize * 2); } } else if (this.environment === Environment.BROWSER) { // Browsers might need smaller batches batchSize = isReadOnly ? 15 : 10; // If we have memory information, adjust accordingly if (navigator.deviceMemory) { // Scale batch size with available memory const memoryFactor = isReadOnly ? 3 : 2; batchSize = Math.max(5, Math.min(30, Math.floor(navigator.deviceMemory * memoryFactor))); // For large datasets, adjust based on memory if (isLargeDataset && navigator.deviceMemory > 4) { batchSize = Math.min(50, batchSize * 1.5); } } } else if (this.environment === Environment.WORKER) { // Workers can handle moderate batch sizes batchSize = isReadOnly ? 20 : 15; } // Adjust based on cache hit/miss ratios const totalHotAccesses = stats.hotCacheHits + stats.hotCacheMisses; const totalWarmAccesses = stats.warmCacheHits + stats.warmCacheMisses; if (totalHotAccesses > 100) { const hotHitRatio = stats.hotCacheHits / totalHotAccesses; // If hot cache hit ratio is high, we're effectively using the cache // so we can use larger batches for better throughput if (hotHitRatio > 0.8) { // High hit ratio, increase batch size batchSize = Math.min(batchSize * 1.5, isRemoteStorage ? 250 : 150); } else if (hotHitRatio < 0.4) { // Low hit ratio, we might be fetching too much at once // Reduce batch size to be more selective batchSize = Math.max(5, batchSize * 0.8); } } if (totalWarmAccesses > 50) { const warmHitRatio = stats.warmCacheHits / totalWarmAccesses; // If warm cache hit ratio is high, prefetching is effective // so we can use larger batches if (warmHitRatio > 0.7) { // High warm hit ratio, increase batch size batchSize = Math.min(batchSize * 1.3, isRemoteStorage ? 200 : 120); } else if (warmHitRatio < 0.3) { // Low warm hit ratio, reduce batch size batchSize = Math.max(5, batchSize * 0.9); } } // If we have storage statistics with operation counts, adjust based on operation patterns if (this.storageStatistics && this.storageStatistics.operations) { const ops = this.storageStatistics.operations; const totalOps = ops.total || 1; const searchOps = (ops.search || 0); const getOps = (ops.get || 0); if (totalOps > 100) { // Calculate search and get ratios const searchRatio = searchOps / totalOps; const getRatio = getOps / totalOps; // For search-heavy workloads, use larger batch size if (searchRatio > 0.6) { // Search-heavy, increase batch size const searchFactor = isRemoteStorage ? 1.8 : 1.5; batchSize = Math.min(isRemoteStorage ? 200 : 100, Math.ceil(batchSize * searchFactor)); } // For get-heavy workloads, adjust batch size if (getRatio > 0.6) { // Get-heavy, adjust batch size based on storage type if (isRemoteStorage) { // For remote storage, larger batches reduce network overhead batchSize = Math.min(150, Math.ceil(batchSize * 1.5)); } else { // For local storage, smaller batches might be more efficient batchSize = Math.max(10, Math.ceil(batchSize * 0.9)); } } } } // Check if we're experiencing memory pressure if (stats.hotCacheSize > 0 && this.hotCacheMaxSize > 0) { const cacheUtilization = stats.hotCacheSize / this.hotCacheMaxSize; // If cache utilization is high, reduce batch size to avoid memory pressure if (cacheUtilization > 0.85) { batchSize = Math.max(5, Math.floor(batchSize * 0.8)); } } // Adjust based on overall hit/miss ratio if we have enough data const totalAccesses = stats.hotCacheHits + stats.hotCacheMisses + stats.warmCacheHits + stats.warmCacheMisses; if (totalAccesses > 100) { const hitRatio = (stats.hotCacheHits + stats.warmCacheHits) / totalAccesses; // Base adjustment factors let increaseFactorForLowHitRatio = isRemoteStorage ? 1.5 : 1.2; let decreaseFactorForHighHitRatio = 0.8; // In read-only mode, be more aggressive with batch size adjustments if (isReadOnly) { increaseFactorForLowHitRatio = isRemoteStorage ? 2.0 : 1.5; decreaseFactorForHighHitRatio = 0.9; // Less reduction in read-only mode } // If hit ratio is high, we can use smaller batches if (hitRatio > 0.8 && !isVeryLargeDataset) { // High hit ratio, decrease batch size slightly // But don't decrease too much for large datasets or remote storage if (!(isLargeDataset && isRemoteStorage)) { batchSize = Math.max(isReadOnly ? 10 : 5, Math.floor(batchSize * decreaseFactorForHighHitRatio)); } } // If hit ratio is low, we need larger batches else if (hitRatio < 0.5) { // Low hit ratio, increase batch size const maxBatchSize = isRemoteStorage ? (isVeryLargeDataset ? 300 : 200) : (isVeryLargeDataset ? 150 : 100); batchSize = Math.min(maxBatchSize, Math.ceil(batchSize * increaseFactorForLowHitRatio)); } } // Set minimum batch sizes based on storage type and mode let minBatchSize = 5; if (isRemoteStorage) { minBatchSize = isReadOnly ? 20 : 10; } else if (isReadOnly) { minBatchSize = 10; } // Ensure batch size is within reasonable limits batchSize = Math.max(minBatchSize, batchSize); // Cap maximum batch size based on environment and storage const maxBatchSize = isRemoteStorage ? (this.environment === Environment.NODE ? 300 : 150) : (this.environment === Environment.NODE ? 150 : 75); batchSize = Math.min(maxBatchSize, batchSize); // Update the batch size with the adaptively tuned value this.batchSize = Math.round(batchSize); } /** * Detect the appropriate warm storage type based on environment */ detectWarmStorageType() { if (this.environment === Environment.BROWSER) { // Use OPFS if available, otherwise use memory if ('storage' in navigator && 'getDirectory' in navigator.storage) { return StorageType.OPFS; } return StorageType.MEMORY; } else if (this.environment === Environment.WORKER) { // Use OPFS if available, otherwise use memory if ('storage' in self && 'getDirectory' in self.storage) { return StorageType.OPFS; } return StorageType.MEMORY; } else { // In Node.js, use filesystem return StorageType.FILESYSTEM; } } /** * Detect the appropriate cold storage type based on environment */ detectColdStorageType() { if (this.environment === Environment.BROWSER) { // Use OPFS if available, otherwise use memory if ('storage' in navigator && 'getDirectory' in navigator.storage) { return StorageType.OPFS; } return StorageType.MEMORY; } else if (this.environment === Environment.WORKER) { // Use OPFS if available, otherwise use memory if ('storage' in self && 'getDirectory' in self.storage) { return StorageType.OPFS; } return StorageType.MEMORY; } else { // In Node.js, use S3 if configured, otherwise filesystem return StorageType.S3; } } /** * Initialize warm storage adapter */ initializeWarmStorage() { // Implementation depends on the detected storage type // For now, return null as this will be provided by the storage adapter return null; } /** * Initialize cold storage adapter */ initializeColdStorage() { // Implementation depends on the detected storage type // For now, return null as this will be provided by the storage adapter return null; } /** * Get an item from cache, trying each level in order * @param id The item ID * @returns The cached item or null if not found */ async get(id) { // Check if it's time to tune parameters await this.checkAndTuneParameters(); // Try hot cache first (fastest) const hotCacheEntry = this.hotCache.get(id); if (hotCacheEntry) { // Update access metadata hotCacheEntry.lastAccessed = Date.now(); hotCacheEntry.accessCount++; // Update stats this.stats.hits++; return hotCacheEntry.data; } // Try warm cache next try { const warmCacheItem = await this.getFromWarmCache(id); if (warmCacheItem) { // Promote to hot cache this.addToHotCache(id, warmCacheItem); // Update stats this.stats.hits++; return warmCacheItem; } } catch (error) { console.warn(`Error accessing warm cache for ${id}:`, error); } // Finally, try cold storage try { const coldStorageItem = await this.getFromColdStorage(id); if (coldStorageItem) { // Promote to hot and warm caches this.addToHotCache(id, coldStorageItem); await this.addToWarmCache(id, coldStorageItem); // Update stats this.stats.misses++; return coldStorageItem; } } catch (error) { console.warn(`Error accessing cold storage for ${id}:`, error); } // Item not found in any cache level this.stats.misses++; return null; } /** * Get an item from warm cache * @param id The item ID * @returns The cached item or null if not found */ async getFromWarmCache(id) { if (!this.warmStorage) return null; try { return await this.warmStorage.get(id); } catch (error) { console.warn(`Error getting item ${id} from warm cache:`, error); return null; } } /** * Get an item from cold storage * @param id The item ID * @returns The item or null if not found */ async getFromColdStorage(id) { if (!this.coldStorage) return null; try { return await this.coldStorage.get(id); } catch (error) { console.warn(`Error getting item ${id} from cold storage:`, error); return null; } } /** * Add an item to hot cache * @param id The item ID * @param item The item to cache */ addToHotCache(id, item) { // Check if we need to evict items if (this.hotCache.size >= this.hotCacheMaxSize * this.hotCacheEvictionThreshold) { this.evictFromHotCache(); } // Add to hot cache this.hotCache.set(id, { data: item, lastAccessed: Date.now(), accessCount: 1, expiresAt: null // Hot cache items don't expire }); // Update stats this.stats.size = this.hotCache.size; } /** * Add an item to warm cache * @param id The item ID * @param item The item to cache */ async addToWarmCache(id, item) { if (!this.warmStorage) return; try { // Add to warm cache with TTL await this.warmStorage.set(id, item, { ttl: this.warmCacheTTL }); } catch (error) { console.warn(`Error adding item ${id} to warm cache:`, error); } } /** * Evict items from hot cache based on LRU policy */ evictFromHotCache() { // Find the least recently used items const entries = Array.from(this.hotCache.entries()); // Sort by last accessed time (oldest first) entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed); // Remove the oldest 20% of items const itemsToRemove = Math.ceil(this.hotCache.size * 0.2); for (let i = 0; i < itemsToRemove && i < entries.length; i++) { this.hotCache.delete(entries[i][0]); this.stats.evictions++; } // Update stats this.stats.size = this.hotCache.size; if (process.env.DEBUG) { console.log(`Evicted ${itemsToRemove} items from hot cache, new size: ${this.hotCache.size}`); } } /** * Set an item in all cache levels * @param id The item ID * @param item The item to cache */ async set(id, item) { // Add to hot cache this.addToHotCache(id, item); // Add to warm cache await this.addToWarmCache(id, item); // Add to cold storage if (this.coldStorage) { try { await this.coldStorage.set(id, item); } catch (error) { console.warn(`Error adding item ${id} to cold storage:`, error); } } } /** * Delete an item from all cache levels * @param id The item ID to delete */ async delete(id) { // Remove from hot cache this.hotCache.delete(id); // Remove from warm cache if (this.warmStorage) { try { await this.warmStorage.delete(id); } catch (error) { console.warn(`Error deleting item ${id} from warm cache:`, error); } } // Remove from cold storage if (this.coldStorage) { try { await this.coldStorage.delete(id); } catch (error) { console.warn(`Error deleting item ${id} from cold storage:`, error); } } // Update stats this.stats.size = this.hotCache.size; } /** * Clear all cache levels */ async clear() { // Clear hot cache this.hotCache.clear(); // Clear warm cache if (this.warmStorage) { try { await this.warmStorage.clear(); } catch (error) { console.warn('Error clearing warm cache:', error); } } // Clear cold storage if (this.coldStorage) { try { await this.coldStorage.clear(); } catch (error) { console.warn('Error clearing cold storage:', error); } } // Reset stats this.stats = { hits: 0, misses: 0, evictions: 0, size: 0, maxSize: this.hotCacheMaxSize, hotCacheSize: 0, warmCacheSize: 0, hotCacheHits: 0, hotCacheMisses: 0, warmCacheHits: 0, warmCacheMisses: 0 }; } /** * Get cache statistics * @returns Cache statistics */ getStats() { return { ...this.stats }; } /** * Prefetch items based on ID patterns or relationships * @param ids Array of IDs to prefetch */ async prefetch(ids) { // Check if it's time to tune parameters await this.checkAndTuneParameters(); // Prefetch in batches to avoid overwhelming the system const batches = []; // Split into batches using the configurable batch size for (let i = 0; i < ids.length; i += this.batchSize) { const batch = ids.slice(i, i + this.batchSize); batches.push(batch); } // Process each batch for (const batch of batches) { await Promise.all(batch.map(async (id) => { // Skip if already in hot cache if (this.hotCache.has(id)) return; try { // Try to get from any cache level await this.get(id); } catch (error) { // Ignore errors during prefetching if (process.env.DEBUG) { console.warn(`Error prefetching ${id}:`, error); } } })); } } /** * Check if it's time to tune parameters and do so if needed * This is called before operations that might benefit from tuned parameters * * This method serves as a checkpoint for auto-tuning, ensuring that: * 1. Parameters are tuned periodically based on the auto-tune interval * 2. Tuning happens before critical operations that would benefit from optimized parameters * 3. Tuning doesn't happen too frequently, which could impact performance * * By calling this method before get(), getMany(), and prefetch() operations, * we ensure that the cache parameters are optimized for the current workload * without adding unnecessary overhead to every operation. */ async checkAndTuneParameters() { // Skip if auto-tuning is disabled if (!this.autoTune) return; // Check if it's time to tune parameters const now = Date.now(); if (now - this.lastAutoTuneTime >= this.autoTuneInterval) { await this.tuneParameters(); } } /** * Get multiple items at once, optimizing for batch retrieval * @param ids Array of IDs to get * @returns Map of ID to item */ async getMany(ids) { // Check if it's time to tune parameters await this.checkAndTuneParameters(); const result = new Map(); // First check hot cache for all IDs const missingIds = []; for (const id of ids) { const hotCacheEntry = this.hotCache.get(id); if (hotCacheEntry) { // Update access metadata hotCacheEntry.lastAccessed = Date.now(); hotCacheEntry.accessCount++; // Add to result result.set(id, hotCacheEntry.data); // Update stats this.stats.hits++; } else { missingIds.push(id); } } if (missingIds.length === 0) { return result; } // Try to get missing items from warm cache if (this.warmStorage) { try { const warmCacheItems = await this.warmStorage.getMany(missingIds); for (const [id, item] of warmCacheItems.entries()) { if (item) { // Promote to hot cache this.addToHotCache(id, item); // Add to result result.set(id, item); // Update stats this.stats.hits++; // Remove from missing IDs const index = missingIds.indexOf(id); if (index !== -1) { missingIds.splice(index, 1); } } } } catch (error) { console.warn('Error accessing warm cache for batch:', error); } } if (missingIds.length === 0) { return result; } // Try to get remaining missing items from cold storage if (this.coldStorage) { try { const coldStorageItems = await this.coldStorage.getMany(missingIds); for (const [id, item] of coldStorageItems.entries()) { if (item) { // Promote to hot and warm caches this.addToHotCache(id, item); await this.addToWarmCache(id, item); // Add to result result.set(id, item); // Update stats this.stats.misses++; } } } catch (error) { console.warn('Error accessing cold storage for batch:', error); } } return result; } /** * Set the storage adapters for warm and cold caches * @param warmStorage Warm cache storage adapter * @param coldStorage Cold storage adapter */ setStorageAdapters(warmStorage, coldStorage) { this.warmStorage = warmStorage; this.coldStorage = coldStorage; } } //# sourceMappingURL=cacheManager.js.map