A consumer's clean-room verification of the 8.0.10 fix found relate() still
hanging their scripts. Root-causing the CLASS instead of the repro found two
mechanisms:
1. The 'beforeExit' auto-flush hook looped forever on any script that never
reaches close(): Node re-emits beforeExit after every event-loop drain, and
the async flush schedules new work — flush, drain, flush, forever. The
listener now self-deregisters BEFORE its one flush, so the next drain exits.
(Empirically: process.on('beforeExit', async () => await anything) alone
never exits — this was the deepest root of the whole hang class.)
2. Every background-maintenance interval is now unref'd at creation — graph
auto-flush, LSM compaction, metadata write-buffer flush, VFS cache
maintenance, PathResolver cache maintenance, statistics debounce (the
writer-lock heartbeat, flush watcher, and cache monitors already were).
Durability is owned by close() and the beforeExit flush, both deterministic;
a best-effort interval must never keep the host process alive.
Proof: the reported shape (add x2 + relate, filesystem) exits cleanly BOTH
with close() (~0.5 s) and with NO teardown at all — and in the no-teardown
case the beforeExit flush still lands the data (verified by reopen: both
nouns + the edge present). New per-op-class sweep test asserts no ref'd
timer survives close() for add / relate / graph find / metadata update /
vfs — turning this bug class off permanently instead of per-repro.
632 lines
No EOL
20 KiB
TypeScript
632 lines
No EOL
20 KiB
TypeScript
/**
|
||
* Path Resolution System with High-Performance Caching
|
||
*
|
||
* Path resolution for the VFS
|
||
* Handles millions of paths efficiently with multi-layer caching
|
||
*/
|
||
|
||
import { Brainy } from '../brainy.js'
|
||
import { VerbType, NounType } from '../types/graphTypes.js'
|
||
import { VFSEntity, VFSError, VFSErrorCode } from './types.js'
|
||
import { getGlobalCache } from '../utils/unifiedCache.js'
|
||
import { prodLog } from '../utils/logger.js'
|
||
|
||
/**
|
||
* Path cache entry
|
||
*/
|
||
interface PathCacheEntry {
|
||
entityId: string
|
||
timestamp: number
|
||
hits: number // Track hot paths
|
||
}
|
||
|
||
/**
|
||
* Per-process sequence used to scope the PROCESS-GLOBAL path cache to a single
|
||
* PathResolver instance. The unified path cache (`getGlobalCache()`) is shared
|
||
* across every Brainy in the process; multiple instances over DIFFERENT storage
|
||
* are a supported pattern, and the VFS root id is a fixed sentinel, so a path key
|
||
* with no instance scope let instance A's `/x → id_A` satisfy instance B's
|
||
* `stat('/x')` against unrelated storage → stale id → "Entity not found". A
|
||
* monotonic per-process token isolates each instance (the in-memory global cache
|
||
* is itself process-scoped, so this is sufficient and survives restarts trivially
|
||
* — the cache is empty then anyway). Cross-instance sharing was only an
|
||
* optimization and is the very collision this removes; each instance keeps its
|
||
* own local pathCache.
|
||
*/
|
||
let pathResolverScopeSeq = 0
|
||
|
||
/**
|
||
* High-performance path resolver with intelligent caching
|
||
*/
|
||
export class PathResolver {
|
||
private brain: Brainy
|
||
private rootEntityId: string
|
||
/** Stable per-instance prefix scoping this resolver's global-cache entries. */
|
||
private readonly cacheScope: string
|
||
|
||
// Multi-layer cache system
|
||
private pathCache: Map<string, PathCacheEntry>
|
||
private parentCache: Map<string, Set<string>> // parent ID -> child names
|
||
private hotPaths: Set<string> // Frequently accessed paths
|
||
|
||
// Cache configuration
|
||
private readonly maxCacheSize: number
|
||
private readonly cacheTTL: number
|
||
private readonly hotPathThreshold: number
|
||
|
||
// Statistics
|
||
private cacheHits = 0
|
||
private cacheMisses = 0
|
||
private metadataIndexHits = 0
|
||
private metadataIndexMisses = 0
|
||
private graphTraversalFallbacks = 0
|
||
|
||
// Maintenance timer
|
||
private maintenanceTimer: NodeJS.Timeout | null = null
|
||
|
||
constructor(brain: Brainy, rootEntityId: string, config?: {
|
||
maxCacheSize?: number
|
||
cacheTTL?: number
|
||
hotPathThreshold?: number
|
||
}) {
|
||
this.brain = brain
|
||
this.rootEntityId = rootEntityId
|
||
// Scope global-cache keys to this instance so two Brainys in one process
|
||
// (over different storage) can't read each other's path→id mappings.
|
||
this.cacheScope = `${rootEntityId}#${++pathResolverScopeSeq}`
|
||
|
||
// Initialize caches
|
||
this.pathCache = new Map()
|
||
this.parentCache = new Map()
|
||
this.hotPaths = new Set()
|
||
|
||
// Configure cache
|
||
this.maxCacheSize = config?.maxCacheSize || 100_000
|
||
this.cacheTTL = config?.cacheTTL || 5 * 60 * 1000 // 5 minutes
|
||
this.hotPathThreshold = config?.hotPathThreshold || 10
|
||
|
||
// Start cache maintenance
|
||
this.startCacheMaintenance()
|
||
}
|
||
|
||
/**
|
||
* Resolve a path to an entity ID
|
||
* Uses 3-tier caching + MetadataIndexManager for optimal performance
|
||
* Works for ALL storage adapters (FileSystem, GCS, S3, Azure, R2, OPFS)
|
||
*/
|
||
async resolve(path: string, options?: {
|
||
followSymlinks?: boolean
|
||
cache?: boolean
|
||
}): Promise<string> {
|
||
// Normalize path
|
||
const normalizedPath = this.normalizePath(path)
|
||
|
||
// Handle root
|
||
if (normalizedPath === '/') {
|
||
return this.rootEntityId
|
||
}
|
||
|
||
const cacheKey = this.globalPathKey(normalizedPath)
|
||
|
||
// L1: UnifiedCache (global LRU cache, <1ms, works for ALL adapters)
|
||
if (options?.cache !== false) {
|
||
const cached = getGlobalCache().getSync(cacheKey)
|
||
if (cached) {
|
||
this.cacheHits++
|
||
return cached
|
||
}
|
||
}
|
||
|
||
// L2: Local hot paths cache (warm, <1ms)
|
||
if (options?.cache !== false && this.hotPaths.has(normalizedPath)) {
|
||
const cached = this.pathCache.get(normalizedPath)
|
||
if (cached && this.isCacheValid(cached)) {
|
||
this.cacheHits++
|
||
cached.hits++
|
||
|
||
// Also cache in UnifiedCache for cross-instance sharing
|
||
getGlobalCache().set(cacheKey, cached.entityId, 'other', 64, 20)
|
||
|
||
return cached.entityId
|
||
}
|
||
}
|
||
|
||
// L2b: Regular local cache
|
||
if (options?.cache !== false && this.pathCache.has(normalizedPath)) {
|
||
const cached = this.pathCache.get(normalizedPath)!
|
||
if (this.isCacheValid(cached)) {
|
||
this.cacheHits++
|
||
cached.hits++
|
||
|
||
// Promote to hot path if accessed frequently
|
||
if (cached.hits >= this.hotPathThreshold) {
|
||
this.hotPaths.add(normalizedPath)
|
||
}
|
||
|
||
// Also cache in UnifiedCache
|
||
getGlobalCache().set(cacheKey, cached.entityId, 'other', 64, 20)
|
||
|
||
return cached.entityId
|
||
} else {
|
||
// Remove stale entry
|
||
this.pathCache.delete(normalizedPath)
|
||
}
|
||
}
|
||
|
||
this.cacheMisses++
|
||
|
||
// L3: MetadataIndexManager query (cold, 5-20ms on GCS, works for ALL adapters)
|
||
// Falls back to graph traversal automatically if MetadataIndex unavailable
|
||
const entityId = await this.resolveWithMetadataIndex(normalizedPath)
|
||
|
||
// Cache the result in ALL layers for future hits
|
||
if (options?.cache !== false) {
|
||
getGlobalCache().set(cacheKey, entityId, 'other', 64, 20)
|
||
this.cachePathEntry(normalizedPath, entityId)
|
||
}
|
||
|
||
return entityId
|
||
}
|
||
|
||
/**
|
||
* Build this instance's process-global cache key for a normalized path.
|
||
* Scoped by {@link cacheScope} so instances over different storage never collide.
|
||
*/
|
||
private globalPathKey(normalizedPath: string): string {
|
||
return `vfs:path:${this.cacheScope}:${normalizedPath}`
|
||
}
|
||
|
||
/**
|
||
* This instance's global-cache key prefix (for scoped prefix deletes).
|
||
*/
|
||
private globalPathPrefix(): string {
|
||
return `vfs:path:${this.cacheScope}:`
|
||
}
|
||
|
||
/**
|
||
* Full path resolution by traversing the graph
|
||
*/
|
||
private async fullResolve(path: string, options?: {
|
||
followSymlinks?: boolean
|
||
}): Promise<string> {
|
||
const parts = this.splitPath(path)
|
||
let currentId = this.rootEntityId
|
||
let currentPath = '/'
|
||
|
||
for (const part of parts) {
|
||
if (!part) continue // Skip empty parts
|
||
|
||
// Find child with matching name
|
||
const childId = await this.resolveChild(currentId, part)
|
||
|
||
if (!childId) {
|
||
throw new VFSError(
|
||
VFSErrorCode.ENOENT,
|
||
`No such file or directory: ${path}`,
|
||
path,
|
||
'resolve'
|
||
)
|
||
}
|
||
|
||
currentPath = this.joinPath(currentPath, part)
|
||
currentId = childId
|
||
|
||
// Cache intermediate paths
|
||
this.cachePathEntry(currentPath, currentId)
|
||
|
||
// Handle symlinks if needed
|
||
if (options?.followSymlinks) {
|
||
const entity = await this.getEntity(currentId)
|
||
if (entity.metadata.vfsType === 'symlink') {
|
||
// Resolve symlink target
|
||
const target = entity.metadata.attributes?.target
|
||
if (target) {
|
||
currentId = await this.resolve(target, options)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return currentId
|
||
}
|
||
|
||
/**
|
||
* Resolve path using MetadataIndexManager (O(log n) direct query)
|
||
* Works for ALL storage adapters (FileSystem, GCS, S3, Azure, R2, OPFS)
|
||
* Falls back to graph traversal if MetadataIndex unavailable
|
||
*/
|
||
private async resolveWithMetadataIndex(path: string): Promise<string> {
|
||
// Access MetadataIndexManager from brain instance (not storage — metadataIndex
|
||
// lives on Brainy). Bracket access reaches the private field.
|
||
const metadataIndex = this.brain['metadataIndex']
|
||
|
||
if (!metadataIndex) {
|
||
// MetadataIndex not available, use graph traversal
|
||
prodLog.debug(`MetadataIndex not available for ${path}, using graph traversal`)
|
||
this.graphTraversalFallbacks++
|
||
return await this.fullResolve(path)
|
||
}
|
||
|
||
try {
|
||
// Direct O(log n) query to roaring bitmap index
|
||
// This queries the 'path' field in VFS entity metadata
|
||
const ids = await metadataIndex.getIds('path', path)
|
||
|
||
if (ids.length === 0) {
|
||
this.metadataIndexMisses++
|
||
throw new VFSError(
|
||
VFSErrorCode.ENOENT,
|
||
`No such file or directory: ${path}`,
|
||
path,
|
||
'resolveWithMetadataIndex'
|
||
)
|
||
}
|
||
|
||
this.metadataIndexHits++
|
||
return ids[0] // VFS paths are unique, return first match
|
||
} catch (error) {
|
||
// MetadataIndex query failed (index not built, path not indexed, etc.)
|
||
// Fallback to reliable graph traversal
|
||
if (error instanceof VFSError) {
|
||
throw error // Re-throw ENOENT errors
|
||
}
|
||
|
||
prodLog.debug(`MetadataIndex query failed for ${path}, falling back to graph traversal:`, error)
|
||
this.metadataIndexMisses++
|
||
this.graphTraversalFallbacks++
|
||
return await this.fullResolve(path)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Resolve a child entity by name within a parent directory
|
||
* Uses proper graph relationships instead of metadata queries
|
||
*/
|
||
private async resolveChild(parentId: string, name: string): Promise<string | null> {
|
||
// Check parent cache first
|
||
const cachedChildren = this.parentCache.get(parentId)
|
||
if (cachedChildren && cachedChildren.has(name)) {
|
||
// Use cached knowledge to quickly find the child
|
||
// Still need to verify it exists
|
||
}
|
||
|
||
// Use proper graph traversal to find children
|
||
// VFS relationships are now part of the knowledge graph
|
||
const relations = await this.brain.related({
|
||
from: parentId,
|
||
type: VerbType.Contains
|
||
})
|
||
|
||
// PERFORMANCE FIX - Batch fetch all children (eliminates N+1 pattern)
|
||
// Before: N sequential get() calls (10 children = 10 × 300ms = 3000ms on GCS)
|
||
// After: 1 batch call (10 children = 1 × 300ms = 300ms on GCS)
|
||
// 10x improvement for cloud storage (GCS, S3, Azure)
|
||
// Same pattern as getChildren() (line 240) - now consistently applied
|
||
const childIds = relations.map(r => r.to)
|
||
const childrenMap = await this.brain.batchGet(childIds)
|
||
|
||
// Find the child with matching name
|
||
for (const relation of relations) {
|
||
const childEntity = childrenMap.get(relation.to)
|
||
if (childEntity && childEntity.metadata?.name === name) {
|
||
// Update parent cache
|
||
if (!this.parentCache.has(parentId)) {
|
||
this.parentCache.set(parentId, new Set())
|
||
}
|
||
this.parentCache.get(parentId)!.add(name)
|
||
|
||
return childEntity.id
|
||
}
|
||
}
|
||
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* Get all children of a directory
|
||
* Uses proper graph relationships to traverse the tree
|
||
*/
|
||
async getChildren(dirId: string): Promise<VFSEntity[]> {
|
||
// Use O(1) graph relationships (VFS creates these in mkdir/writeFile)
|
||
// VFS relationships are now part of the knowledge graph (no special filtering needed)
|
||
const relations = await this.brain.related({
|
||
from: dirId,
|
||
type: VerbType.Contains
|
||
})
|
||
|
||
const validChildren: VFSEntity[] = []
|
||
const childNames = new Set<string>()
|
||
|
||
// Batch fetch all child entities (eliminates N+1 query pattern)
|
||
// This is WIRED UP AND USED - no longer a stub!
|
||
const childIds = relations.map(r => r.to)
|
||
const childrenMap = await this.brain.batchGet(childIds)
|
||
|
||
// Deduplicate by entity ID to handle duplicate relationship records
|
||
// This can occur when multiple Brainy instances create relationships concurrently
|
||
// for the same storage path (each instance has its own in-memory GraphAdjacencyIndex).
|
||
// The Set lookup is O(1), adding negligible overhead.
|
||
const seenEntityIds = new Set<string>()
|
||
|
||
// Process batched results
|
||
for (const relation of relations) {
|
||
// Skip if we've already processed this entity
|
||
if (seenEntityIds.has(relation.to)) {
|
||
continue
|
||
}
|
||
seenEntityIds.add(relation.to)
|
||
|
||
const entity = childrenMap.get(relation.to)
|
||
if (entity && entity.metadata?.vfsType && entity.metadata?.name) {
|
||
validChildren.push(entity as VFSEntity)
|
||
childNames.add(entity.metadata.name)
|
||
}
|
||
}
|
||
|
||
// Update cache
|
||
this.parentCache.set(dirId, childNames)
|
||
return validChildren
|
||
}
|
||
|
||
/**
|
||
* Create a new path entry (for mkdir/writeFile)
|
||
*/
|
||
async createPath(path: string, entityId: string): Promise<void> {
|
||
const normalizedPath = this.normalizePath(path)
|
||
|
||
// Cache the new path
|
||
this.cachePathEntry(normalizedPath, entityId)
|
||
|
||
// Update parent cache
|
||
const parentPath = this.getParentPath(normalizedPath)
|
||
const name = this.getBasename(normalizedPath)
|
||
|
||
if (parentPath) {
|
||
const parentId = await this.resolve(parentPath)
|
||
if (!this.parentCache.has(parentId)) {
|
||
this.parentCache.set(parentId, new Set())
|
||
}
|
||
this.parentCache.get(parentId)!.add(name)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Invalidate ALL caches
|
||
* Call this when switching branches (checkout), clearing data (clear), or forking
|
||
* This ensures no stale data from previous branch/state remains in cache
|
||
*/
|
||
invalidateAllCaches(): void {
|
||
// Clear all local caches
|
||
this.pathCache.clear()
|
||
this.parentCache.clear()
|
||
this.hotPaths.clear()
|
||
|
||
// Clear THIS instance's VFS entries from UnifiedCache (scoped so a branch
|
||
// switch / clear / fork on one brain can't wipe another brain's cache).
|
||
getGlobalCache().deleteByPrefix(this.globalPathPrefix())
|
||
|
||
// Reset statistics (optional but helpful for debugging)
|
||
this.cacheHits = 0
|
||
this.cacheMisses = 0
|
||
this.metadataIndexHits = 0
|
||
this.metadataIndexMisses = 0
|
||
this.graphTraversalFallbacks = 0
|
||
|
||
prodLog.info('[PathResolver] All caches invalidated')
|
||
}
|
||
|
||
/**
|
||
* Invalidate cache entries for a path and its children
|
||
* FIX: Also invalidates UnifiedCache to prevent stale entity IDs
|
||
* This fixes the "Source entity not found" bug after delete+recreate operations
|
||
*/
|
||
invalidatePath(path: string, recursive = false): void {
|
||
const normalizedPath = this.normalizePath(path)
|
||
|
||
// FIX: Clear parent cache BEFORE deleting from pathCache
|
||
// (we need the entityId from the cache entry)
|
||
const cached = this.pathCache.get(normalizedPath)
|
||
if (cached) {
|
||
this.parentCache.delete(cached.entityId)
|
||
}
|
||
|
||
// Remove from local caches
|
||
this.pathCache.delete(normalizedPath)
|
||
this.hotPaths.delete(normalizedPath)
|
||
|
||
// CRITICAL FIX: Also invalidate UnifiedCache (global LRU cache)
|
||
// This was missing before, causing stale entity IDs to be returned after delete
|
||
const cacheKey = this.globalPathKey(normalizedPath)
|
||
getGlobalCache().delete(cacheKey)
|
||
|
||
if (recursive) {
|
||
// Remove all paths that start with this path
|
||
const prefix = normalizedPath.endsWith('/') ? normalizedPath : normalizedPath + '/'
|
||
|
||
for (const [cachedPath, entry] of this.pathCache) {
|
||
if (cachedPath.startsWith(prefix)) {
|
||
this.pathCache.delete(cachedPath)
|
||
this.hotPaths.delete(cachedPath)
|
||
// Also clear parent cache for this entry
|
||
this.parentCache.delete(entry.entityId)
|
||
}
|
||
}
|
||
|
||
// CRITICAL FIX: Also invalidate UnifiedCache entries with this prefix
|
||
const globalCachePrefix = this.globalPathKey(prefix)
|
||
getGlobalCache().deleteByPrefix(globalCachePrefix)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Cache a path entry
|
||
*/
|
||
private cachePathEntry(path: string, entityId: string): void {
|
||
// Evict old entries if cache is full
|
||
if (this.pathCache.size >= this.maxCacheSize) {
|
||
this.evictOldEntries()
|
||
}
|
||
|
||
const existing = this.pathCache.get(path)
|
||
this.pathCache.set(path, {
|
||
entityId,
|
||
timestamp: Date.now(),
|
||
hits: existing?.hits || 0
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Check if a cache entry is still valid
|
||
*/
|
||
private isCacheValid(entry: PathCacheEntry): boolean {
|
||
return (Date.now() - entry.timestamp) < this.cacheTTL
|
||
}
|
||
|
||
/**
|
||
* Evict old cache entries (LRU with TTL)
|
||
*/
|
||
private evictOldEntries(): void {
|
||
const now = Date.now()
|
||
const entries = Array.from(this.pathCache.entries())
|
||
|
||
// Sort by least recently used (combination of timestamp and hits)
|
||
entries.sort((a, b) => {
|
||
const scoreA = a[1].timestamp + (a[1].hits * 60000) // Boost for hits
|
||
const scoreB = b[1].timestamp + (b[1].hits * 60000)
|
||
return scoreA - scoreB
|
||
})
|
||
|
||
// Remove 10% of cache
|
||
const toRemove = Math.floor(this.maxCacheSize * 0.1)
|
||
for (let i = 0; i < toRemove && i < entries.length; i++) {
|
||
const [path] = entries[i]
|
||
this.pathCache.delete(path)
|
||
this.hotPaths.delete(path)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Start periodic cache maintenance
|
||
*/
|
||
private startCacheMaintenance(): void {
|
||
this.maintenanceTimer = setInterval(() => {
|
||
// Clean up expired entries
|
||
const now = Date.now()
|
||
for (const [path, entry] of this.pathCache) {
|
||
if (!this.isCacheValid(entry)) {
|
||
this.pathCache.delete(path)
|
||
this.hotPaths.delete(path)
|
||
}
|
||
}
|
||
|
||
// Log cache statistics (in production, send to monitoring)
|
||
const hitRate = this.cacheHits / (this.cacheHits + this.cacheMisses)
|
||
if ((this.cacheHits + this.cacheMisses) % 1000 === 0) {
|
||
console.log(`[PathResolver] Cache stats: ${Math.round(hitRate * 100)}% hit rate, ${this.pathCache.size} entries, ${this.hotPaths.size} hot paths`)
|
||
}
|
||
}, 60000) // Every minute
|
||
// Cache maintenance must never keep the host process alive.
|
||
if (this.maintenanceTimer && typeof this.maintenanceTimer.unref === 'function') {
|
||
this.maintenanceTimer.unref()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get entity by ID
|
||
*/
|
||
private async getEntity(entityId: string): Promise<VFSEntity> {
|
||
const entity = await this.brain.get(entityId)
|
||
|
||
if (!entity) {
|
||
throw new VFSError(
|
||
VFSErrorCode.ENOENT,
|
||
`Entity not found: ${entityId}`,
|
||
undefined,
|
||
'getEntity'
|
||
)
|
||
}
|
||
|
||
return entity as VFSEntity
|
||
}
|
||
|
||
// ============= Path Utilities =============
|
||
|
||
private normalizePath(path: string): string {
|
||
// Remove multiple slashes, trailing slashes (except for root)
|
||
let normalized = path.replace(/\/+/g, '/')
|
||
if (normalized.length > 1 && normalized.endsWith('/')) {
|
||
normalized = normalized.slice(0, -1)
|
||
}
|
||
return normalized || '/'
|
||
}
|
||
|
||
private splitPath(path: string): string[] {
|
||
return this.normalizePath(path).split('/').filter(Boolean)
|
||
}
|
||
|
||
private joinPath(parent: string, child: string): string {
|
||
if (parent === '/') return `/${child}`
|
||
return `${parent}/${child}`
|
||
}
|
||
|
||
private getParentPath(path: string): string | null {
|
||
const normalized = this.normalizePath(path)
|
||
if (normalized === '/') return null
|
||
|
||
const lastSlash = normalized.lastIndexOf('/')
|
||
if (lastSlash === 0) return '/'
|
||
return normalized.substring(0, lastSlash)
|
||
}
|
||
|
||
private getBasename(path: string): string {
|
||
const normalized = this.normalizePath(path)
|
||
if (normalized === '/') return ''
|
||
|
||
const lastSlash = normalized.lastIndexOf('/')
|
||
return normalized.substring(lastSlash + 1)
|
||
}
|
||
|
||
/**
|
||
* Cleanup resources
|
||
*/
|
||
cleanup(): void {
|
||
if (this.maintenanceTimer) {
|
||
clearInterval(this.maintenanceTimer)
|
||
this.maintenanceTimer = null
|
||
}
|
||
this.pathCache.clear()
|
||
this.parentCache.clear()
|
||
this.hotPaths.clear()
|
||
}
|
||
|
||
/**
|
||
* Get cache statistics
|
||
* Added MetadataIndexManager metrics
|
||
*/
|
||
getStats(): {
|
||
cacheSize: number
|
||
hotPaths: number
|
||
hitRate: number
|
||
hits: number
|
||
misses: number
|
||
metadataIndexHits: number
|
||
metadataIndexMisses: number
|
||
metadataIndexHitRate: number
|
||
graphTraversalFallbacks: number
|
||
} {
|
||
const totalMetadataIndexQueries = this.metadataIndexHits + this.metadataIndexMisses
|
||
return {
|
||
cacheSize: this.pathCache.size,
|
||
hotPaths: this.hotPaths.size,
|
||
hitRate: this.cacheHits / (this.cacheHits + this.cacheMisses) || 0,
|
||
hits: this.cacheHits,
|
||
misses: this.cacheMisses,
|
||
metadataIndexHits: this.metadataIndexHits,
|
||
metadataIndexMisses: this.metadataIndexMisses,
|
||
metadataIndexHitRate: totalMetadataIndexQueries > 0
|
||
? this.metadataIndexHits / totalMetadataIndexQueries
|
||
: 0,
|
||
graphTraversalFallbacks: this.graphTraversalFallbacks
|
||
}
|
||
}
|
||
} |