chore(8.0): Phase A + B — purge all @deprecated APIs + cacheManager dead branches
PHASE A — every @deprecated marker resolved (~25 removed) src/coreTypes.ts - GraphVerb: dropped the "@deprecated Will be replaced by HNSWVerbWithMetadata" note. GraphVerb IS the canonical contract — every public API path speaks it. Removed the `source` and `target` legacy alias fields (renamed `from` / `to` callers years ago; no consumers remain). - StorageAdapter: dropped the "@deprecated Use getNouns() with filter" notes from `getNounsByNounType`, `getVerbsBySource`, `getVerbsByTarget`, `getVerbsByType`. They were never deprecated in spirit — they're useful non-paginated convenience wrappers over the paginated `getNouns()` / `getVerbs()` surface. Refreshed JSDoc to explain the role. src/types/graphTypes.ts - Mirrored the GraphVerb cleanup: dropped `source` + `target` legacy aliases. sourceId + targetId are the canonical fields. src/import/ImportCoordinator.ts - Deleted the entire DeprecatedImportOptions interface block (130 LOC). It was a v3 → v4 migration tool using the `?: never` trick to force compile errors on dropped options. Five major versions in, the forced-error gate is no longer pulling its weight. src/triple/TripleIntelligence.ts - Deleted `TripleIntelligenceEngine = any` alias. No consumers; superseded by `TripleIntelligenceSystem`. src/storage/cow/binaryDataCodec.ts - Deleted `wrapBinaryData()`. The COW dispatch layer in `baseStorage.ts` routes by key-prefix convention; the old guess-by-JSON-parse codec was fragile (compressed bytes can accidentally parse as JSON) and unused. src/storage/baseStorage.ts - Refreshed JSDoc on `convertHNSWVerbToGraphVerb()` — the method is alive and used internally; the deprecation note was stale. src/embeddings/wasm/AssetLoader.ts → DELETED - File was @deprecated since model weights moved into the Candle WASM bundle. No consumers. Removed from `embeddings/wasm/index.ts` exports. src/embeddings/wasm/types.ts - Dropped @deprecated tags on `TokenizerConfig` + `TokenizedInput` — still used by `WordPieceTokenizer` (auxiliary tokenization). Deleted `InferenceConfig` (truly dead). Updated `embeddings/wasm/index.ts` exports. src/utils/metadataIndex.ts - Deleted `getIdsForCriteria()` — pure alias for `getIdsForFilter()`, no consumers. src/interfaces/IIndex.ts - Removed RebuildOptions.lazy (deprecated and unused; lazy mode is auto- selected by available-memory detection). src/hnsw/hnswIndex.ts - Removed `getNouns()` (returned a full Map; deprecated in favor of pagination years ago and no consumers in src/ or tests/). PHASE B — cacheManager dead StorageType branches src/storage/cacheManager.ts - Collapsed the `isRemoteStorage` flag and its 15 dead conditional branches spanning calculateOptimalCacheSize() and calculateOptimalBatchSize(). After dropping cloud adapters in step 7, `coldStorageType` is never S3 or REMOTE_API; the branches were dead. Cache sizing and batch sizing now honor the filesystem-only reality with simpler heuristics. - Collapsed `detectWarmStorageType()` + `detectColdStorageType()` from ~40 LOC of environment-+-availability branching to 2-line returns of `StorageType.FILESYSTEM`. Brainy 8.0 ships filesystem + memory only. NOT YET — Phases C-G in follow-up commits C: storageAutoConfig.ts + zeroConfig + extensibleConfig + sharedConfigManager D: TODO/FIXME sweep across src/ E: skipped tests + the parallel-test race condition F: docs deep clean (BATCHING, augmentations, READMEs) G: browser support drop (the last 2 @deprecated) VERIFICATION - npx tsc --noEmit: clean - npm test: 1408 / 1409 (same pre-existing race-condition outstanding)
This commit is contained in:
parent
9f9a41599e
commit
cb16a39a0c
15 changed files with 98 additions and 718 deletions
|
|
@ -623,118 +623,54 @@ export class CacheManager<T extends HNSWNode | Edge | HNSWVerb> {
|
|||
private async tuneHotCacheSize(): Promise<void> {
|
||||
// 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
|
||||
|
||||
// Adjust by total entity count if we have storage statistics.
|
||||
if (this.storageStatistics) {
|
||||
const totalItems = (this.storageStatistics.totalNodes || 0) +
|
||||
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
|
||||
let percentageToCache = isReadOnly ? 0.3 : 0.2
|
||||
|
||||
if (totalItems > 1_000_000) {
|
||||
percentageToCache = Math.min(percentageToCache, 0.15)
|
||||
} else if (totalItems > 100000) { // Over 100K items
|
||||
} else if (totalItems > 100_000) {
|
||||
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
|
||||
|
||||
// Adjust by observed hit ratio.
|
||||
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
|
||||
const hitRatioFactor = isReadOnly ? 1 + baseAdjustment * 1.5 : 1 + baseAdjustment
|
||||
optimalSize = Math.ceil(optimalSize * hitRatioFactor)
|
||||
} else if (hitRatio > 0.9 && !isReadOnly) {
|
||||
optimalSize = Math.ceil(optimalSize * 0.9)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for operation patterns if available
|
||||
|
||||
// Read-heavy workloads warrant a slightly larger cache.
|
||||
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)
|
||||
}
|
||||
|
||||
if (totalOps > 100 && readOps / totalOps > 0.8) {
|
||||
optimalSize = Math.ceil(optimalSize * 1.2)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
const minSize = isReadOnly ? 2000 : 1000
|
||||
optimalSize = Math.max(optimalSize, minSize)
|
||||
|
||||
// Update the hot cache max size
|
||||
|
|
@ -941,98 +877,36 @@ export class CacheManager<T extends HNSWNode | Edge | HNSWVerb> {
|
|||
* @param cacheStats Optional cache statistics for more adaptive tuning
|
||||
*/
|
||||
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
|
||||
|
||||
// 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 totalItems = this.storageStatistics
|
||||
? (this.storageStatistics.totalNodes || 0) + (this.storageStatistics.totalEdges || 0)
|
||||
: 0
|
||||
const isLargeDataset = totalItems > 100_000
|
||||
const isVeryLargeDataset = totalItems > 1_000_000
|
||||
|
||||
// Brainy 8.0 ships Node-like runtimes only (Bun, Deno, Node). All paths
|
||||
// funnel into the same batch-size envelope.
|
||||
batchSize = isReadOnly ? 30 : 20
|
||||
if (isLargeDataset) batchSize = Math.min(100, batchSize * 1.5)
|
||||
if (isVeryLargeDataset) batchSize = Math.min(200, batchSize * 2)
|
||||
|
||||
// Adjust by hit-ratio observations.
|
||||
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 (hotHitRatio > 0.8) batchSize = Math.min(batchSize * 1.5, 150)
|
||||
else if (hotHitRatio < 0.4) 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 (warmHitRatio > 0.7) batchSize = Math.min(batchSize * 1.3, 120)
|
||||
else if (warmHitRatio < 0.3) batchSize = Math.max(5, batchSize * 0.9)
|
||||
}
|
||||
|
||||
// If we have storage statistics with operation counts, adjust based on operation patterns
|
||||
|
|
@ -1043,140 +917,64 @@ export class CacheManager<T extends HNSWNode | Edge | HNSWVerb> {
|
|||
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))
|
||||
batchSize = Math.min(100, Math.ceil(batchSize * 1.5))
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
batchSize = Math.max(10, Math.ceil(batchSize * 0.9))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we're experiencing memory pressure
|
||||
|
||||
// Memory-pressure trim.
|
||||
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
|
||||
|
||||
// Combined hot+warm hit-ratio adjustment.
|
||||
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
|
||||
const increaseFactorForLowHitRatio = isReadOnly ? 1.5 : 1.2
|
||||
const decreaseFactorForHighHitRatio = isReadOnly ? 0.9 : 0.8
|
||||
|
||||
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.max(isReadOnly ? 10 : 5, Math.floor(batchSize * decreaseFactorForHighHitRatio))
|
||||
} else if (hitRatio < 0.5) {
|
||||
const maxBatchSize = 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
|
||||
|
||||
// Min/max envelope. 8.0 is Node-like only.
|
||||
const minBatchSize = isReadOnly ? 10 : 5
|
||||
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)
|
||||
batchSize = Math.min(150, batchSize)
|
||||
|
||||
// Update the batch size with the adaptively tuned value
|
||||
this.batchSize = Math.round(batchSize)
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the appropriate warm storage type based on environment
|
||||
* Resolve the warm-tier storage type. Brainy 8.0 ships filesystem +
|
||||
* memory only.
|
||||
*/
|
||||
private detectWarmStorageType(): StorageType {
|
||||
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 as WorkerGlobalScope).storage!) {
|
||||
return StorageType.OPFS
|
||||
}
|
||||
return StorageType.MEMORY
|
||||
} else {
|
||||
// In Node.js, use filesystem
|
||||
return StorageType.FILESYSTEM
|
||||
}
|
||||
return StorageType.FILESYSTEM
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detect the appropriate cold storage type based on environment
|
||||
* Resolve the cold-tier storage type. Brainy 8.0 ships filesystem +
|
||||
* memory only.
|
||||
*/
|
||||
private detectColdStorageType(): StorageType {
|
||||
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 as WorkerGlobalScope).storage!) {
|
||||
return StorageType.OPFS
|
||||
}
|
||||
return StorageType.MEMORY
|
||||
} else {
|
||||
// In Node.js, use S3 if configured, otherwise filesystem
|
||||
return StorageType.S3
|
||||
}
|
||||
return StorageType.FILESYSTEM
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue