diff --git a/docs/guides/cache-configuration.md b/docs/guides/cache-configuration.md new file mode 100644 index 00000000..9449b677 --- /dev/null +++ b/docs/guides/cache-configuration.md @@ -0,0 +1,234 @@ +# Brainy Cache Configuration Guide + +This guide explains how to configure and optimize Brainy's cache system for different environments and use cases. + +## Cache System Overview + +Brainy uses a multi-level cache system to optimize performance: + +1. **Hot Cache**: In-memory cache for frequently accessed items +2. **Warm Cache**: Secondary cache with longer retention but slower access +3. **Cold Storage**: Persistent storage for all data + +## New Features + +### Dynamic Memory Detection + +Brainy now includes a sophisticated memory detection mechanism that works across all JavaScript environments: + +- **Node.js**: Uses dynamic import of the `os` module to get actual system memory +- **Browser**: Uses `performance.memory` or `navigator.deviceMemory` APIs +- **Worker**: Uses available memory APIs or conservative defaults + +```javascript +// The cache will automatically detect available memory +const brainy = new BrainyData(); +``` + +### Environment-Specific Configuration + +You can now specify different cache configurations for each environment: + +```javascript +const brainy = new BrainyData({ + cacheOptions: { + environmentConfig: { + // Node.js specific settings + node: { + hotCacheMaxSize: 10000, + hotCacheEvictionThreshold: 0.85, + warmCacheTTL: 48 * 60 * 60 * 1000, // 48 hours + batchSize: 50 + }, + // Browser specific settings + browser: { + hotCacheMaxSize: 5000, + hotCacheEvictionThreshold: 0.8, + warmCacheTTL: 24 * 60 * 60 * 1000, // 24 hours + batchSize: 20 + }, + // Worker specific settings + worker: { + hotCacheMaxSize: 3000, + hotCacheEvictionThreshold: 0.75, + warmCacheTTL: 12 * 60 * 60 * 1000, // 12 hours + batchSize: 15 + } + } + } +}); +``` + +### Adaptive Cache Tuning + +The cache system now includes improved adaptive tuning that automatically adjusts based on: + +1. Available memory in the current environment +2. Cache hit/miss ratios +3. Access patterns (read-heavy vs. write-heavy) +4. Dataset size and characteristics +5. Storage type (S3, filesystem, memory) + +The adaptive tuning system will: + +- Increase cache sizes for read-only workloads +- Optimize batch sizes based on network conditions +- Adjust eviction thresholds based on memory pressure +- Tune warm cache TTL based on update frequency + +```javascript +// Enable auto-tuning (on by default) +const brainy = new BrainyData({ + cacheOptions: { + autoTune: true + } +}); + +// Disable auto-tuning if needed +const brainy = new BrainyData({ + cacheOptions: { + autoTune: false + } +}); +``` + +## Best Practices + +### For Large Datasets + +When working with large datasets (>100K items): + +```javascript +const brainy = new BrainyData({ + cacheOptions: { + // For Node.js environments with large datasets + environmentConfig: { + node: { + hotCacheMaxSize: 50000, + batchSize: 100 + } + } + } +}); +``` + +### For Memory-Constrained Environments + +For environments with limited memory: + +```javascript +const brainy = new BrainyData({ + cacheOptions: { + // Conservative settings for memory-constrained environments + hotCacheMaxSize: 1000, + hotCacheEvictionThreshold: 0.7, // Evict earlier + warmCacheTTL: 6 * 60 * 60 * 1000, // 6 hours + batchSize: 5 + } +}); +``` + +### For Read-Only Applications + +For read-only applications where data doesn't change: + +```javascript +const brainy = new BrainyData({ + readOnly: true, + cacheOptions: { + // More aggressive caching for read-only data + hotCacheEvictionThreshold: 0.9, + warmCacheTTL: 72 * 60 * 60 * 1000 // 72 hours + } +}); +``` + +## Advanced Configuration + +### Manual Cache Size Calculation + +If you want to manually calculate the optimal cache size: + +```javascript +// Get memory information +async function getMemoryInfo() { + if (typeof window === 'undefined') { + // Node.js + const os = await import('os'); + return { + totalMemory: os.totalmem(), + freeMemory: os.freemem() + }; + } else if (navigator.deviceMemory) { + // Browser with deviceMemory API + const totalMemory = navigator.deviceMemory * 1024 * 1024 * 1024; + return { + totalMemory, + freeMemory: totalMemory * 0.5 // Estimate + }; + } + // Default fallback + return { + totalMemory: 8 * 1024 * 1024 * 1024, // 8GB + freeMemory: 4 * 1024 * 1024 * 1024 // 4GB + }; +} + +// Calculate optimal cache size +async function calculateOptimalCacheSize() { + const memoryInfo = await getMemoryInfo(); + const BYTES_PER_ENTRY = 1024; // Estimate 1KB per entry + const memoryPercentage = 0.1; // Use 10% of free memory + + return Math.max( + Math.floor(memoryInfo.freeMemory * memoryPercentage / BYTES_PER_ENTRY), + 1000 // Minimum size + ); +} + +// Use the calculated size +async function initializeBrainy() { + const optimalSize = await calculateOptimalCacheSize(); + + const brainy = new BrainyData({ + cacheOptions: { + hotCacheMaxSize: optimalSize + } + }); + + return brainy; +} +``` + +## Monitoring Cache Performance + +You can monitor cache performance to fine-tune your settings: + +```javascript +// Get cache statistics +const stats = brainy.getCacheStats(); + +console.log('Cache Statistics:', { + hotCacheSize: stats.hotCacheSize, + hotCacheHits: stats.hotCacheHits, + hotCacheMisses: stats.hotCacheMisses, + warmCacheSize: stats.warmCacheSize, + warmCacheHits: stats.warmCacheHits, + warmCacheMisses: stats.warmCacheMisses +}); + +// Calculate hit ratios +const hotHitRatio = stats.hotCacheHits / (stats.hotCacheHits + stats.hotCacheMisses || 1); +const warmHitRatio = stats.warmCacheHits / (stats.warmCacheHits + stats.warmCacheMisses || 1); + +console.log('Hit Ratios:', { + hotHitRatio: hotHitRatio.toFixed(2), + warmHitRatio: warmHitRatio.toFixed(2) +}); +``` + +## Conclusion + +Brainy's enhanced cache system now provides better performance across all JavaScript environments with minimal configuration. The adaptive tuning system will automatically optimize cache parameters based on your specific workload and environment. + +For most applications, the default settings with auto-tuning enabled will provide excellent performance. For specialized use cases, use the environment-specific configuration options to fine-tune the cache behavior. diff --git a/src/storage/cacheManager.ts b/src/storage/cacheManager.ts index 0c1c5404..015185b3 100644 --- a/src/storage/cacheManager.ts +++ b/src/storage/cacheManager.ts @@ -44,6 +44,12 @@ interface CacheStats { evictions: number size: number maxSize: number + hotCacheSize: number + warmCacheSize: number + hotCacheHits: number + hotCacheMisses: number + warmCacheHits: number + warmCacheMisses: number } // Environment detection for storage selection @@ -75,7 +81,13 @@ export class CacheManager { misses: 0, evictions: 0, size: 0, - maxSize: 0 + maxSize: 0, + hotCacheSize: 0, + warmCacheSize: 0, + hotCacheHits: 0, + hotCacheMisses: 0, + warmCacheHits: 0, + warmCacheMisses: 0 } // Environment and storage configuration @@ -109,6 +121,32 @@ export class CacheManager { warmStorage?: any coldStorage?: any readOnly?: boolean + environmentConfig?: { + node?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + browser?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + worker?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + [key: string]: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + } | undefined + } } /** @@ -124,6 +162,32 @@ export class CacheManager { warmStorage?: any coldStorage?: any readOnly?: boolean + environmentConfig?: { + node?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + browser?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + worker?: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + }, + [key: string]: { + hotCacheMaxSize?: number + hotCacheEvictionThreshold?: number + warmCacheTTL?: number + batchSize?: number + } | undefined + } } = {}) { // Store options for later reference this.options = options @@ -142,11 +206,14 @@ export class CacheManager { // Set auto-tuning flag this.autoTune = options.autoTune !== undefined ? options.autoTune : true - // Set default values or use provided values - this.hotCacheMaxSize = options.hotCacheMaxSize || this.detectOptimalCacheSize() - this.hotCacheEvictionThreshold = options.hotCacheEvictionThreshold || 0.8 - this.warmCacheTTL = options.warmCacheTTL || 24 * 60 * 60 * 1000 // 24 hours - this.batchSize = options.batchSize || 10 + // 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) { @@ -209,14 +276,13 @@ export class CacheManager { // In Node.js, use available system memory with enhanced allocation if (this.environment === Environment.NODE) { try { - // Use dynamic import to avoid ESLint warning - const getOS = () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require('os') - } - const os = getOS() - const totalMemory = os.totalmem() - const freeMemory = os.freemem() + // 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 @@ -241,7 +307,7 @@ export class CacheManager { // Calculate optimal size based on adjusted percentage const optimalSize = Math.max( - Math.floor(freeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY), + Math.floor(estimatedFreeMemory * memoryPercentage / ESTIMATED_BYTES_PER_ENTRY), 1000 ) @@ -307,6 +373,167 @@ export class CacheManager { } } + /** + * 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 + */ + private async detectOptimalCacheSizeAsync(): Promise { + 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 + */ + private async detectAvailableMemory(): Promise<{ totalMemory: number, freeMemory: number } | null> { + 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 as any).memory) { + const memoryInfo = (performance as any).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 as any).memory) { + const memoryInfo = (performance as any).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 @@ -339,17 +566,20 @@ export class CacheManager { this.storageStatistics = await this.coldStorage.getStatistics() } - // Tune hot cache size - this.tuneHotCacheSize() + // Get cache statistics for adaptive tuning + const cacheStats = this.getStats() - // Tune eviction threshold - this.tuneEvictionThreshold() + // Use the async version of tuneHotCacheSize which uses detectOptimalCacheSizeAsync + await this.tuneHotCacheSize() - // Tune warm cache TTL - this.tuneWarmCacheTTL() + // Tune eviction threshold based on hit/miss ratio + this.tuneEvictionThreshold(cacheStats) - // Tune batch size - this.tuneBatchSize() + // 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) { @@ -357,7 +587,15 @@ export class CacheManager { hotCacheMaxSize: this.hotCacheMaxSize, hotCacheEvictionThreshold: this.hotCacheEvictionThreshold, warmCacheTTL: this.warmCacheTTL, - batchSize: this.batchSize + batchSize: this.batchSize, + cacheStats: { + hotCacheSize: cacheStats.hotCacheSize, + warmCacheSize: cacheStats.warmCacheSize, + hotCacheHits: cacheStats.hotCacheHits, + hotCacheMisses: cacheStats.hotCacheMisses, + warmCacheHits: cacheStats.warmCacheHits, + warmCacheMisses: cacheStats.warmCacheMisses + } }) } } catch (error) { @@ -382,9 +620,9 @@ export class CacheManager { * - For read-only mode, prioritize cache size over eviction speed * - Dynamically adjust based on hit/miss ratio and query patterns */ - private tuneHotCacheSize(): void { - // Start with the base size from environment detection - let optimalSize = this.detectOptimalCacheSize() + private async tuneHotCacheSize(): Promise { + // 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 @@ -511,6 +749,7 @@ export class CacheManager { * 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) @@ -518,24 +757,30 @@ export class CacheManager { * - 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 */ - private tuneEvictionThreshold(): void { + private tuneEvictionThreshold(cacheStats?: CacheStats): void { // 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 totalAccesses = this.stats.hits + this.stats.misses - if (totalAccesses > 100) { - const hitRatio = this.stats.hits / totalAccesses + 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 (hitRatio > 0.8) { + if (hotHitRatio > 0.8) { // High hit ratio, increase threshold (up to 0.9) - threshold = Math.min(0.9, 0.8 + (hitRatio - 0.8)) - } else if (hitRatio < 0.5) { + 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 - hitRatio)) + threshold = Math.max(0.6, 0.8 - (0.5 - hotHitRatio) * 0.5) } } @@ -564,6 +809,24 @@ export class CacheManager { } } + // 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 } @@ -574,16 +837,43 @@ export class CacheManager { * 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 */ - private tuneWarmCacheTTL(): void { + private tuneWarmCacheTTL(cacheStats?: CacheStats): void { // 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 @@ -597,14 +887,31 @@ export class CacheManager { // 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)) + 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.5 - updateRatio)) + 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 } @@ -621,6 +928,7 @@ export class CacheManager { * 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 @@ -628,11 +936,17 @@ export class CacheManager { * - 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 */ - private tuneBatchSize(): void { + private tuneBatchSize(cacheStats?: CacheStats): void { // 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 @@ -688,6 +1002,39 @@ export class CacheManager { 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 @@ -721,10 +1068,20 @@ export class CacheManager { } } - // Adjust based on hit/miss ratio if we have enough data - const totalAccesses = this.stats.hits + this.stats.misses + // 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 = this.stats.hits / totalAccesses + const hitRatio = (stats.hotCacheHits + stats.warmCacheHits) / totalAccesses // Base adjustment factors let increaseFactorForLowHitRatio = isRemoteStorage ? 1.5 : 1.2 @@ -774,8 +1131,8 @@ export class CacheManager { batchSize = Math.min(maxBatchSize, batchSize) - // Update the batch size - this.batchSize = batchSize + // Update the batch size with the adaptively tuned value + this.batchSize = Math.round(batchSize) } /** @@ -1081,7 +1438,13 @@ export class CacheManager { misses: 0, evictions: 0, size: 0, - maxSize: this.hotCacheMaxSize + maxSize: this.hotCacheMaxSize, + hotCacheSize: 0, + warmCacheSize: 0, + hotCacheHits: 0, + hotCacheMisses: 0, + warmCacheHits: 0, + warmCacheMisses: 0 } }