perf(vfs): repairContainment's reconcile is one paged edge walk, not one graph call per file
Pass 2 issued one awaited related({ to }) per VFS entity — O(entities)
serialized graph calls, measured in whole minutes on large brains. Now a
single paged walk over every Contains edge (type-only, 1,000 per page)
feeds an in-memory group-by-target, and only actual defects mutate. The
verdicts are unchanged: a stale parent's edge is removed, a missing edge
is restored, duplicates cannot survive, and user knowledge edges are
never touched.
Pinned in tests/integration/vfs-containment-batched.test.ts: exact
removed/restored counts on a seeded defect tree, tree correctness after
the repair, user edges untouched, and the cost shape — related() call
count independent of the entity count.
This commit is contained in:
parent
5e3b343a0e
commit
3e60aded36
2 changed files with 141 additions and 1 deletions
|
|
@ -2295,6 +2295,31 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
cursor = page.nextCursor
|
||||
}
|
||||
|
||||
// Pass 2: ONE paged walk over every Contains edge, grouped by target in
|
||||
// memory. The earlier shape issued one awaited related({ to }) per VFS
|
||||
// entity — O(entities) serialized graph calls, measured in whole minutes
|
||||
// on large brains. This shape is O(edges / page) calls regardless of how
|
||||
// many entities exist; mutations alone stay per-defect.
|
||||
const incomingByTarget = new Map<string, Relation<any>[]>()
|
||||
{
|
||||
const pageSize = 1000
|
||||
let pageOffset = 0
|
||||
for (;;) {
|
||||
const page = await this.brain.related({
|
||||
type: VerbType.Contains,
|
||||
limit: pageSize,
|
||||
offset: pageOffset
|
||||
})
|
||||
for (const edge of page) {
|
||||
const bucket = incomingByTarget.get(edge.to)
|
||||
if (bucket) bucket.push(edge)
|
||||
else incomingByTarget.set(edge.to, [edge])
|
||||
}
|
||||
if (page.length < pageSize) break
|
||||
pageOffset += pageSize
|
||||
}
|
||||
}
|
||||
|
||||
let removed = 0
|
||||
let restored = 0
|
||||
for (const { id, path } of vfsEntities) {
|
||||
|
|
@ -2307,7 +2332,7 @@ export class VirtualFileSystem implements IVirtualFileSystem {
|
|||
continue
|
||||
}
|
||||
|
||||
const incoming = await this.brain.related({ to: id, type: VerbType.Contains })
|
||||
const incoming = incomingByTarget.get(id) ?? []
|
||||
let expectedSeen = false
|
||||
for (const edge of incoming) {
|
||||
const isVfsEdge = edge.subtype === 'vfs-contains' || (edge.metadata as any)?.isVFS === true
|
||||
|
|
|
|||
115
tests/integration/vfs-containment-batched.test.ts
Normal file
115
tests/integration/vfs-containment-batched.test.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/**
|
||||
* @module tests/integration/vfs-containment-batched
|
||||
* @description repairContainment costs O(edges/page) graph calls, not O(entities) (10.4.9 train).
|
||||
*
|
||||
* Pass 2 used to issue one awaited `related({ to })` per VFS entity — minutes
|
||||
* of serialized graph calls on large brains. Now one paged walk over every
|
||||
* Contains edge feeds an in-memory group-by-target, and only actual defects
|
||||
* mutate. These pins hold the verdicts (duplicate removed, stale parent
|
||||
* removed, missing edge restored, user knowledge edges untouched) AND the
|
||||
* cost shape (related() call count independent of the entity count).
|
||||
*/
|
||||
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
||||
import { Brainy } from '../../src/brainy'
|
||||
import { NounType, VerbType } from '../../src/types/graphTypes'
|
||||
|
||||
const FILES = 60
|
||||
|
||||
describe('repairContainment: batched pass 2', () => {
|
||||
let brain: Brainy<any>
|
||||
let result: { removed: number; restored: number }
|
||||
let relatedCalls = 0
|
||||
|
||||
beforeAll(async () => {
|
||||
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
|
||||
await brain.init()
|
||||
const vfs = (brain as any).vfs ?? (brain as any)._vfs
|
||||
expect(vfs).toBeTruthy()
|
||||
await vfs.init()
|
||||
|
||||
// A directory and FILES entries under it, wired as real VFS rows.
|
||||
const mkNode = async (id: string, path: string, vfsType: string): Promise<void> => {
|
||||
await brain.add({
|
||||
id,
|
||||
data: `vfs node ${path}`,
|
||||
type: NounType.File,
|
||||
visibility: 'system',
|
||||
metadata: { vfsType, path }
|
||||
})
|
||||
}
|
||||
await mkNode('dir', '/docs', 'directory')
|
||||
const rootId = vfs.rootEntityId ?? (await vfs.initializeRoot?.())
|
||||
if (rootId) {
|
||||
await brain.relate({
|
||||
from: rootId,
|
||||
to: 'dir',
|
||||
type: VerbType.Contains,
|
||||
subtype: 'vfs-contains',
|
||||
metadata: { isVFS: true }
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < FILES; i++) {
|
||||
await mkNode(`f-${i}`, `/docs/f-${i}.md`, 'file')
|
||||
if (i === 0) continue // f-0: MISSING edge — must be restored
|
||||
await brain.relate({
|
||||
from: 'dir',
|
||||
to: `f-${i}`,
|
||||
type: VerbType.Contains,
|
||||
subtype: 'vfs-contains',
|
||||
metadata: { isVFS: true }
|
||||
})
|
||||
}
|
||||
// NOTE: relate() is idempotent for an identical from/to/type, so a true
|
||||
// duplicate (a concurrent-writer artifact) cannot be seeded through the
|
||||
// public API — the duplicate branch is covered by the tree-correctness
|
||||
// pin below, which proves at most one vfs edge survives per file.
|
||||
// f-2: STALE parent edge (from a sibling file) — must be removed.
|
||||
await brain.relate({
|
||||
from: 'f-3',
|
||||
to: 'f-2',
|
||||
type: VerbType.Contains,
|
||||
subtype: 'vfs-contains',
|
||||
metadata: { isVFS: true }
|
||||
})
|
||||
// A USER knowledge Contains edge (not vfs-flagged) — must be untouched.
|
||||
await brain.relate({ from: 'f-4', to: 'f-5', type: VerbType.Contains })
|
||||
|
||||
const spy = vi.spyOn(brain, 'related')
|
||||
result = await vfs.repairContainment()
|
||||
relatedCalls = spy.mock.calls.length
|
||||
spy.mockRestore()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
brain = null as any
|
||||
})
|
||||
|
||||
it('restores the missing edge and removes the stale parent — exactly', () => {
|
||||
expect(result.restored).toBe(1) // f-0's missing edge
|
||||
expect(result.removed).toBe(1) // f-2's stale parent (f-3 → f-2)
|
||||
})
|
||||
|
||||
it('the repaired tree is correct: every file has exactly one vfs edge from its dir', async () => {
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const incoming = await brain.related({ to: `f-${i}`, type: VerbType.Contains })
|
||||
const vfsEdges = incoming.filter(
|
||||
(e) => e.subtype === 'vfs-contains' || (e.metadata as any)?.isVFS === true
|
||||
)
|
||||
expect(vfsEdges, `f-${i}`).toHaveLength(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('never touches user knowledge edges', async () => {
|
||||
const incoming = await brain.related({ to: 'f-5', type: VerbType.Contains })
|
||||
const user = incoming.filter(
|
||||
(e) => e.subtype !== 'vfs-contains' && (e.metadata as any)?.isVFS !== true
|
||||
)
|
||||
expect(user).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('cost shape: related() calls do not scale with the entity count', () => {
|
||||
// One paged type-only walk (~E/1000 pages) — with 60+ entities the old
|
||||
// shape issued 60+ calls; the new one a handful. Bound generously.
|
||||
expect(relatedCalls).toBeLessThanOrEqual(5)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue