perf(8.0): bound per-id generation history chains (O(W+L) resident, was O(N))
The MVCC time-travel layer kept nounChains/verbChains as unbounded Map<id, number[]> — one resident chain per id ever touched across retained history (O(N) RAM, defeating billion-scale time travel). Replace with a hot-tail window + bounded cold LRU + a mutex-free bulk resolver, keeping resolveAt exactly correct. The most-recent W generations stay resident as full chains (the common recent-pin read is O(log), zero scan); deeper pins reconstruct one id's chain on demand into an L-bounded LRU. Reconstruction is LOCK-LIGHT — it holds no commit mutex, so a historical read never stalls writers; correctness holds because a live pin's answer is always > minPinnedGeneration, which compaction can never reclaim, and a concurrently-reclaimed gen below that is skipped. materializeAtGeneration routes through a new mutex-free resolveManyAt (one forward pass, O(R + |ids|)) — without it a deep-pin materialize both regresses to O(N*R) and deadlocks on snapshotWith's mutex. The cold cache is invalidated on re-touch to stay coherent. Resident RAM is O(W*d + L), independent of N. 11-case test (oracle-vs-bruteforce, held-Db across eviction + concurrent compaction, no-deadlock, write-path I/O-free); unit 1735 + integration 613 (db-mvcc/db-temporal/db-asof green).
This commit is contained in:
parent
8f4787bb10
commit
a859d6ecf8
4 changed files with 956 additions and 72 deletions
|
|
@ -7475,23 +7475,42 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
const snapshotStorage = new MemoryStorage()
|
||||
await snapshotStorage.init()
|
||||
|
||||
/** Copy one id's at-`generation` state into the snapshot (or ensure absence). */
|
||||
const copyAt = async (kind: 'noun' | 'verb', id: string): Promise<void> => {
|
||||
const resolved = await this.generationStore.resolveAt(kind, id, generation)
|
||||
// Copy one id's at-`generation` state into the snapshot (or ensure absence)
|
||||
// as a PURE LOOKUP against a precomputed `firstAfter` map (id → the first
|
||||
// generation after the pin that touched it). An id absent from the map was
|
||||
// untouched since the pin → its live storage state IS its at-`generation`
|
||||
// state; an id present resolves from that generation's immutable
|
||||
// before-image. Both `resolveManyAt` (builds the map) and
|
||||
// `readGenerationRecord` (reads the before-image) are MUTEX-FREE, so this is
|
||||
// safe to call inside the reconciliation pass below (which holds the commit
|
||||
// mutex via `snapshotWith`) — a per-id `resolveAt` there would re-enter the
|
||||
// mutex and DEADLOCK, and would regress the O(R) bulk copy to O(N·R).
|
||||
const copyAt = async (
|
||||
kind: 'noun' | 'verb',
|
||||
id: string,
|
||||
firstAfter: Map<string, number>
|
||||
): Promise<void> => {
|
||||
let record: { metadata: any; vector: any | null } | null = null
|
||||
if (resolved.source === 'record') {
|
||||
if (resolved.metadata !== null) {
|
||||
record = { metadata: resolved.metadata, vector: resolved.vector }
|
||||
}
|
||||
} else if (resolved.source === 'current') {
|
||||
const candidate = firstAfter.get(id)
|
||||
if (candidate === undefined) {
|
||||
// Untouched since the pin → live storage IS the at-`generation` state.
|
||||
const raw =
|
||||
kind === 'noun' ? await this.storage.readNounRaw(id) : await this.storage.readVerbRaw(id)
|
||||
if (raw.metadata !== null) {
|
||||
record = raw
|
||||
if (raw.metadata !== null) record = raw
|
||||
} else {
|
||||
const before = await this.generationStore.readGenerationRecord(kind, candidate, id)
|
||||
if (before === null) {
|
||||
throw new Error(
|
||||
`Generation record missing: _generations/${candidate}/prev/${id}.json ` +
|
||||
`(store corrupted or records removed outside compactHistory())`
|
||||
)
|
||||
}
|
||||
// A non-null metadata before-image is the at-`generation` live state; a
|
||||
// null-metadata before-image (the create sentinel, or a metadata-less
|
||||
// stored state) is absent at this generation.
|
||||
if (before.metadata !== null) record = { metadata: before.metadata, vector: before.vector }
|
||||
}
|
||||
// `record === null` — absent at this generation (or a metadata-less
|
||||
// stored state, which is not a live entity): writing nulls ensures the
|
||||
// `record === null` — absent at this generation: writing nulls ensures the
|
||||
// id is absent in the snapshot, which also un-copies ids created by
|
||||
// transactions that commit mid-build.
|
||||
if (kind === 'noun') {
|
||||
|
|
@ -7516,25 +7535,42 @@ export class Brainy<T = any> implements BrainyInterface<T> {
|
|||
const id = entityIdFromCanonicalPath(path)
|
||||
if (id) verbIds.add(id)
|
||||
}
|
||||
for (const id of nounIds) await copyAt('noun', id)
|
||||
for (const id of verbIds) await copyAt('verb', id)
|
||||
// ONE ascending pass per kind resolves the whole universe's first-after
|
||||
// generations (O(R) getDelta reads, NOT O(N·R)), then each copy is a lookup.
|
||||
const nounFirstAfter = await this.generationStore.resolveManyAt('noun', nounIds, generation)
|
||||
const verbFirstAfter = await this.generationStore.resolveManyAt('verb', verbIds, generation)
|
||||
for (const id of nounIds) await copyAt('noun', id, nounFirstAfter)
|
||||
for (const id of verbIds) await copyAt('verb', id, verbFirstAfter)
|
||||
|
||||
// Reconciliation pass under the commit mutex: nothing can commit during
|
||||
// this section, so afterwards the snapshot is exactly the at-`generation`
|
||||
// record set even when transactions committed while the bulk copy ran.
|
||||
// Reconciled ids join the enumeration sets so the vector-index build
|
||||
// below sees them too.
|
||||
// below sees them too. `resolveManyAt`/`readGenerationRecord` are mutex-free,
|
||||
// so this section never re-enters the commit mutex (no deadlock).
|
||||
await this.generationStore.snapshotWith(async () => {
|
||||
const committedNow = this.generationStore.committedGeneration()
|
||||
if (committedNow === watermark) return
|
||||
const delta = await this.generationStore.changedBetween(watermark, committedNow)
|
||||
for (const id of delta.nouns) {
|
||||
const reconNouns = new Set<string>(delta.nouns)
|
||||
const reconVerbs = new Set<string>(delta.verbs)
|
||||
const reconNounFirstAfter = await this.generationStore.resolveManyAt(
|
||||
'noun',
|
||||
reconNouns,
|
||||
generation
|
||||
)
|
||||
const reconVerbFirstAfter = await this.generationStore.resolveManyAt(
|
||||
'verb',
|
||||
reconVerbs,
|
||||
generation
|
||||
)
|
||||
for (const id of reconNouns) {
|
||||
nounIds.add(id)
|
||||
await copyAt('noun', id)
|
||||
await copyAt('noun', id, reconNounFirstAfter)
|
||||
}
|
||||
for (const id of delta.verbs) {
|
||||
for (const id of reconVerbs) {
|
||||
verbIds.add(id)
|
||||
await copyAt('verb', id)
|
||||
await copyAt('verb', id, reconVerbFirstAfter)
|
||||
}
|
||||
watermark = committedNow
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue