perf: optimize init() and rebuild performance
- Remove detectAndRepairCorruption from init() hot path — was loading all metadata chunks sequentially on startup. Now available via checkHealth() and repairIndex() methods. - Short-circuit warmCache on empty workspace — skip 4-6 wasted storage reads. - Parallelize MetadataIndex.init() and getGraphIndex() via Promise.all(). - Defer metadata writes during rebuild to batch boundaries (every 5000 entities) instead of flushing per-entity. - Skip pre-reads for new entities in transactions — saves 2 storage round-trips per add() on cloud storage.
This commit is contained in:
parent
cd875294ad
commit
35cb674157
3 changed files with 113 additions and 40 deletions
|
|
@ -218,30 +218,35 @@ export class MetadataIndexManager {
|
|||
// Initialize EntityIdMapper (loads UUID ↔ integer mappings from storage)
|
||||
await this.idMapper.init()
|
||||
|
||||
// Warm the cache with common fields (lazy loading optimization)
|
||||
// This loads the 'noun' sparse index which is needed for type counts
|
||||
await this.warmCache()
|
||||
// Short-circuit: skip warmCache, lazyLoadCounts, syncTypeCountsToFixed for empty workspace
|
||||
// On empty workspace, there are no fields to warm — avoids 4-6 wasted storage reads (404s)
|
||||
const hasFields = this.fieldIndexes.size > 0
|
||||
|
||||
// Load type counts AFTER warmCache (sparse index is now cached)
|
||||
// Previously called in constructor without await and read from wrong source
|
||||
await this.lazyLoadCounts()
|
||||
if (hasFields) {
|
||||
// Warm the cache with common fields (lazy loading optimization)
|
||||
// This loads the 'noun' sparse index which is needed for type counts
|
||||
await this.warmCache()
|
||||
|
||||
// Phase 1b: Sync loaded counts to fixed-size arrays
|
||||
// Now correctly happens AFTER lazyLoadCounts() finishes
|
||||
this.syncTypeCountsToFixed()
|
||||
// Load type counts AFTER warmCache (sparse index is now cached)
|
||||
// Previously called in constructor without await and read from wrong source
|
||||
await this.lazyLoadCounts()
|
||||
|
||||
// Detect index corruption and auto-rebuild if necessary
|
||||
// The update() field asymmetry bug caused indexes to accumulate stale entries
|
||||
// This check runs on startup to detect and repair corrupted indexes automatically
|
||||
await this.detectAndRepairCorruption()
|
||||
// Phase 1b: Sync loaded counts to fixed-size arrays
|
||||
// Now correctly happens AFTER lazyLoadCounts() finishes
|
||||
this.syncTypeCountsToFixed()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect index corruption and automatically repair via rebuild
|
||||
* This catches the update() field asymmetry bug that causes 7 fields to accumulate per update
|
||||
* Corruption threshold: 100 avg metadata entries/entity, excluding __words__ (expected ~30)
|
||||
*
|
||||
* Removed from init() hot path for performance. Call explicitly via:
|
||||
* - brain.checkHealth() — returns health status
|
||||
* - brain.repairIndex() — runs detection + auto-repair
|
||||
*/
|
||||
private async detectAndRepairCorruption(): Promise<void> {
|
||||
async detectAndRepairCorruption(): Promise<void> {
|
||||
const validation = await this.validateConsistency()
|
||||
|
||||
if (!validation.healthy) {
|
||||
|
|
@ -1508,7 +1513,7 @@ export class MetadataIndexManager {
|
|||
* @param entityOrMetadata - Either full entity structure or plain metadata (backward compat)
|
||||
* @param skipFlush - Skip automatic flush (used during batch operations)
|
||||
*/
|
||||
async addToIndex(id: string, entityOrMetadata: any, skipFlush: boolean = false): Promise<void> {
|
||||
async addToIndex(id: string, entityOrMetadata: any, skipFlush: boolean = false, deferWrites: boolean = false): Promise<void> {
|
||||
const fields = this.extractIndexableFields(entityOrMetadata)
|
||||
|
||||
// Sanity check for excessive indexed fields (indicates possible data issue)
|
||||
|
|
@ -1559,7 +1564,11 @@ export class MetadataIndexManager {
|
|||
|
||||
// Flush all dirty chunks and sparse indices accumulated during this add operation
|
||||
// This batches writes that were previously sequential per-field into a single concurrent flush
|
||||
await this.flushDirtyMetadata()
|
||||
// During rebuild (deferWrites=true), defer flushes to periodic batch boundaries (every 5000 entities)
|
||||
// to avoid N × flush I/O amplification. The rebuild() method handles periodic + final flushes.
|
||||
if (!deferWrites) {
|
||||
await this.flushDirtyMetadata()
|
||||
}
|
||||
|
||||
// Adaptive auto-flush based on usage patterns
|
||||
if (!skipFlush) {
|
||||
|
|
@ -3103,10 +3112,16 @@ export class MetadataIndexManager {
|
|||
}
|
||||
|
||||
// Process all nouns
|
||||
let localCount = 0
|
||||
for (const noun of result.items) {
|
||||
const metadata = metadataBatch.get(noun.id)
|
||||
if (metadata) {
|
||||
await this.addToIndex(noun.id, metadata, true)
|
||||
await this.addToIndex(noun.id, metadata, true, true)
|
||||
localCount++
|
||||
// Periodic safety flush every 5000 entities to cap memory during rebuild
|
||||
if (localCount % 5000 === 0) {
|
||||
await this.flushDirtyMetadata()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3192,14 +3207,18 @@ export class MetadataIndexManager {
|
|||
const metadata = metadataBatch.get(noun.id)
|
||||
if (metadata) {
|
||||
// Skip flush during rebuild for performance
|
||||
await this.addToIndex(noun.id, metadata, true)
|
||||
await this.addToIndex(noun.id, metadata, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Yield after processing the entire batch
|
||||
await this.yieldToEventLoop()
|
||||
|
||||
|
||||
totalNounsProcessed += result.items.length
|
||||
// Periodic safety flush every 5000 entities to cap memory during rebuild
|
||||
if (totalNounsProcessed % 5000 === 0) {
|
||||
await this.flushDirtyMetadata()
|
||||
}
|
||||
hasMoreNouns = result.hasMore
|
||||
nounOffset += nounLimit
|
||||
|
||||
|
|
@ -3248,10 +3267,16 @@ export class MetadataIndexManager {
|
|||
}
|
||||
|
||||
// Process all verbs
|
||||
let verbLocalCount = 0
|
||||
for (const verb of result.items) {
|
||||
const metadata = verbMetadataBatch.get(verb.id)
|
||||
if (metadata) {
|
||||
await this.addToIndex(verb.id, metadata, true)
|
||||
await this.addToIndex(verb.id, metadata, true, true)
|
||||
verbLocalCount++
|
||||
// Periodic safety flush every 5000 entities to cap memory during rebuild
|
||||
if (verbLocalCount % 5000 === 0) {
|
||||
await this.flushDirtyMetadata()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3332,14 +3357,18 @@ export class MetadataIndexManager {
|
|||
const metadata = verbMetadataBatch.get(verb.id)
|
||||
if (metadata) {
|
||||
// Skip flush during rebuild for performance
|
||||
await this.addToIndex(verb.id, metadata, true)
|
||||
await this.addToIndex(verb.id, metadata, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Yield after processing the entire batch
|
||||
await this.yieldToEventLoop()
|
||||
|
||||
|
||||
totalVerbsProcessed += result.items.length
|
||||
// Periodic safety flush every 5000 entities to cap memory during rebuild
|
||||
if (totalVerbsProcessed % 5000 === 0) {
|
||||
await this.flushDirtyMetadata()
|
||||
}
|
||||
hasMoreVerbs = result.hasMore
|
||||
verbOffset += verbLimit
|
||||
|
||||
|
|
@ -3356,6 +3385,10 @@ export class MetadataIndexManager {
|
|||
}
|
||||
}
|
||||
|
||||
// Flush remaining dirty chunks/sparse indices accumulated during rebuild
|
||||
// (deferWrites=true prevented per-entity flushes, so dirty data accumulated)
|
||||
await this.flushDirtyMetadata()
|
||||
|
||||
// Flush to storage with final yield
|
||||
prodLog.debug('💾 Flushing metadata index to storage...')
|
||||
await this.flush()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue