brainy/dist/storage/cacheManager.js
David Snelling f8c45f2d8d Initial commit: Brainy - Multi-Dimensional AI Database
Open source vector database with HNSW indexing, graph relationships,
and metadata facets. Features CLI with professional augmentation registry
integration for discovering extensions and capabilities.
2025-08-18 17:35:06 -07:00

1306 lines
No EOL
56 KiB
JavaScript

/**
* 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