Artifact f095c0c6ffcee84da334dbd334d9171d18695ebf699f2f3693ddec9979f1f325:
- File
internal/content/cone_reference_budget.go
— part of check-in
[b28c06506b]
at
2026-10-22 02:57:50
on branch feat/horizon-cones
— fix(content): classify sparse stores from crosslink tables, not the whole store
Sparse classification read, inflated and delta-expanded every held blob, file content included, under a whole-store byte budget. On a clone of the Fossil SCM repository (67,894 blobs, 8.4 GB expanded) LoadAbsences failed at once with "byte budget exceeded", so no path-cone or content-horizon store of a real repository could classify its phantoms, and pull, verify and compaction failed with it.
Reference facts now come from the tables wherever they provably stand in for raw cards. A blob the crosslinker recorded as no artifact and mlink names as a file version is never read. A completely linked check-in (event row, no orphan row, a primary parent edge) whose primary parent and baseline trees are themselves reproduced contributes its mlink rows as F cards and its plink, cherrypick and tagxref rows as references; by induction over the primary chain those rows give the same (name, file) pairs as its raw cards. Everything else is still read raw: unlinked and orphaned check-ins, roots, check-ins whose parent never arrived, other artifacts, and every check-in once the store has seen a sub-manifest, since trees are not crosslinked. Budgets are charged per round for the selected blobs and their delta ancestors only, and a base's bytes are released once its last delta is applied.
A content horizon now reads only its held set's manifests and the baselines and nodes they reach, on demand, with one visited set and a byte budget scaled to the held set.
On the Fossil SCM clone, cone {src} classifies in about 2 s at a 65 MiB peak heap, reading 8,655 of 67,894 blobs; the inside/outside sets equal the all-raw result, and Outside/Required agree on every artifact. (user: [email protected], size: 12586) [annotate] [blame] [check-ins using] [more...]
package content import ( "bytes" "compress/zlib" "context" "errors" "fmt" "io" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/deck" "github.com/danmestas/go-libfossil/internal/delta" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // ReferenceBudget bounds a whole-store reference load. Byte budgets count payload buffers, not Go allocator overhead or parser objects. // The artifact/card caps separately bound that overhead. The caller must hold // a stable database snapshot through preflight and materialization. type ReferenceBudget struct { Artifacts int StoredBytes int64 InflatedBytes int64 ExpandedBytes int64 Cards int } type ReferenceWork struct { PrefixReads int PrefixBytes int64 PayloadReads int InflateBytes int64 ExpandBytes int64 Visits int // TreeReads and TreeBytes count a content horizon's manifest and // sub-manifest reads, charged as each one is expanded. TreeReads int TreeBytes int64 } type referenceBlob struct { rid, source libfossil.FslID uuid string size int64 stored int64 inflated int64 payload []byte data []byte children []int pending int // Children not yet expanded; data is released at zero. unreadable bool member bool // Parsed, not only expanded as a delta ancestor. verified bool } // referenceReader reads raw artifacts in rounds. Every budget is charged // across all rounds, and only for the blobs a round selects: its members // and their delta ancestors. type referenceReader struct { ctx context.Context q db.Querier budget ReferenceBudget retainUnreadable bool held []referenceBlob // Every held blob's metadata, by rid. index map[libfossil.FslID]int facts *ConeReferenceFacts work ReferenceWork stored, expanded int64 inflated int64 cards int } // loadConeReferenceFactsBounded names every artifact, then reads raw bytes in // at most three rounds: first everything the crosslink tables cannot answer, // then the linked check-ins whose trees the first round showed mlink cannot // reproduce, and, only if one of those proves unreadable, every remaining // linked check-in. Within a round it proves stored bytes before prefix reads, // which SQLite may materialize internally, and declared inflation before // full driver payload reads. Inflation uses a fixed, precharged buffer and // verifies EOF. Delta output size is checked before Apply can allocate. func loadConeReferenceFactsBounded( ctx context.Context, q db.Querier, budget ReferenceBudget, retainUnreadable bool, ) (*ConeReferenceFacts, ReferenceWork, error) { r, err := referencePreflight(ctx, q, budget) if err != nil { return nil, ReferenceWork{}, err } r.retainUnreadable = retainUnreadable tables, err := loadReferenceTables(ctx, q) if err != nil { return nil, r.work, err } isLinked := func(rid libfossil.FslID) bool { _, ok := tables.linked[rid]; return ok } if _, err := r.read(func(rid libfossil.FslID) bool { return !tables.files[rid] && !isLinked(rid) }); err != nil { return nil, r.work, err } answer := tables.answerable(r.facts) failed, err := r.read(func(rid libfossil.FslID) bool { return isLinked(rid) && !answer[rid] }) if err != nil { return nil, r.work, err } if failed { // answerable took every linked check-in to be readable. if _, err := r.read(func(rid libfossil.FslID) bool { return answer[rid] }); err != nil { return nil, r.work, err } answer = nil } if err := r.facts.addLinked(ctx, q, answer); err != nil { return nil, r.work, err } return r.facts, r.work, nil } const referenceMetadataSQL = `SELECT b.rid,substr(b.uuid,1,65),b.size, CASE WHEN typeof(b.content)='blob' THEN length(b.content) ELSE -1 END, coalesce(d.srcid,0) FROM blob b LEFT JOIN delta d ON d.rid=b.rid ORDER BY b.rid` // referencePreflight names every artifact. It reads no content, and charges // nothing but the artifact budget: a round charges what it selects. func referencePreflight( ctx context.Context, q db.Querier, budget ReferenceBudget, ) (*referenceReader, error) { if ctx == nil || q == nil { panic("content.referencePreflight: nil context or querier") } if budget.Artifacts <= 0 || budget.Artifacts > coneReferenceNodes || budget.Cards <= 0 || budget.StoredBytes <= 0 || budget.InflatedBytes <= 0 || budget.ExpandedBytes <= 0 { return nil, fmt.Errorf("sparse preparation: invalid budget") } if err := ctx.Err(); err != nil { return nil, err } var count int64 if err := q.QueryRow("SELECT count(*) FROM (SELECT rid FROM blob LIMIT ?)", budget.Artifacts+1).Scan(&count); err != nil { return nil, err } if count > int64(budget.Artifacts) { return nil, fmt.Errorf("sparse preparation: artifact budget exceeded") } r := &referenceReader{ctx: ctx, q: q, budget: budget, held: make([]referenceBlob, 0, int(count)), index: make(map[libfossil.FslID]int, int(count)), facts: &ConeReferenceFacts{names: make(map[libfossil.FslID]string, int(count)), held: map[string]libfossil.FslID{}, nodes: map[libfossil.FslID]*deck.Deck{}, byUUID: map[string]*deck.Deck{}}} rows, err := q.Query(referenceMetadataSQL) if err != nil { return nil, err } for rows.Next() { if err := ctx.Err(); err != nil { return nil, errors.Join(err, rows.Close()) } var b referenceBlob if err := rows.Scan(&b.rid, &b.uuid, &b.size, &b.stored, &b.source); err != nil { return nil, errors.Join(err, rows.Close()) } if len(r.facts.names) >= budget.Artifacts || len(b.uuid) > 64 { return nil, errors.Join(fmt.Errorf("sparse preparation: identity budget exceeded"), rows.Close()) } r.facts.names[b.rid] = b.uuid if b.size >= 0 { r.index[b.rid] = len(r.held) r.held = append(r.held, b) } } return r, errors.Join(rows.Err(), rows.Close()) } // read reads the held blobs member selects, and parses them, reporting // whether any member could not be read. Delta ancestors are expanded too, // but they are not parsed: they are what the selection costs. func (r *referenceReader) read(member func(libfossil.FslID) bool) (bool, error) { blobs, err := r.selectRound(member) if err != nil || len(blobs) == 0 { return false, err } if err := referencePrefixes(r.ctx, r.q, blobs, r.budget, &r.work, &r.inflated); err != nil { return false, err } if err := r.payloads(blobs); err != nil { return false, err } if err := r.materialize(blobs); err != nil { return false, err } failed := false for i := range blobs { failed = failed || blobs[i].member && !blobs[i].verified } return failed, nil } // selectRound copies out the members and their held delta ancestors, in rid // order, and charges their stored and expanded bytes before any read. func (r *referenceReader) selectRound(member func(libfossil.FslID) bool) ([]referenceBlob, error) { chosen := map[int]bool{} for i := range r.held { if !member(r.held[i].rid) { continue } for at, ok := i, true; ok && !chosen[at]; at, ok = r.index[r.held[at].source] { chosen[at] = true // Each step chooses a new blob, so this ends. } } blobs := make([]referenceBlob, 0, len(chosen)) for i := range r.held { if !chosen[i] { continue } b := r.held[i] b.member = member(b.rid) b.unreadable = b.stored < 0 if b.unreadable { b.stored = 0 } if b.stored > r.budget.StoredBytes-r.stored || b.size > r.budget.ExpandedBytes-r.expanded { return nil, fmt.Errorf("sparse preparation: byte budget exceeded") } r.stored += b.stored r.expanded += b.size blobs = append(blobs, b) } return blobs, nil } func (r *referenceReader) payloads(blobs []referenceBlob) error { for i := range blobs { b := &blobs[i] if err := r.ctx.Err(); err != nil { return err } if b.unreadable { if r.retainUnreadable { continue } return fmt.Errorf("sparse preparation: invalid compressed prefix") } var packed []byte // Non-BLOB storage classes are unknown, never an allocation proof. // The same-snapshot predicate guards accidental unbound use too. err := r.q.QueryRow(`SELECT content FROM blob WHERE rid=? AND typeof(content)='blob' AND length(content)=?`, b.rid, b.stored).Scan(&packed) if err != nil { return err } r.work.PayloadReads++ r.work.InflateBytes += b.inflated b.payload, err = referenceInflate(packed, int(b.inflated)) if err != nil { if !r.retainUnreadable { return err } b.unreadable = true } } return nil } func referenceInflate(packed []byte, size int) ([]byte, error) { if len(packed) < 5 || size < 0 { return nil, fmt.Errorf("sparse preparation: invalid compressed bytes") } zr, err := zlib.NewReader(bytes.NewReader(packed[4:])) if err != nil { return nil, err } data := make([]byte, size) if _, err := io.ReadFull(zr, data); err != nil { return nil, errors.Join(err, zr.Close()) } var extra [1]byte n, err := zr.Read(extra[:]) if n != 0 || err != io.EOF { return nil, errors.Join(fmt.Errorf("sparse preparation: compressed size mismatch"), zr.Close()) } if err := zr.Close(); err != nil { return nil, err } return data, nil } // materialize expands the round's blobs base first. A base's bytes are // released once its last delta is applied, so a chain costs its width, not // its length. func (r *referenceReader) materialize(blobs []referenceBlob) error { index := make(map[libfossil.FslID]int, len(blobs)) queue := make([]int, 0, len(blobs)) for i := range blobs { index[blobs[i].rid] = i } for i := range blobs { if blobs[i].source == 0 { queue = append(queue, i) } else if parent, ok := index[blobs[i].source]; ok { blobs[parent].children = append(blobs[parent].children, i) } } for at := 0; at < len(queue); at++ { if err := r.ctx.Err(); err != nil { return err } if r.work.Visits >= r.budget.Artifacts { return fmt.Errorf("sparse preparation: visit budget exceeded") } r.work.Visits++ b := &blobs[queue[at]] data, err := r.expand(b, blobs, index) if err != nil { return err } b.payload = nil if b.unreadable || int64(len(data)) != b.size || bindToUUID(b.rid, b.uuid, data) != nil { data = nil } if data == nil { r.facts.unreadable = append(r.facts.unreadable, b.uuid) } else if err := referenceRecord(r.facts, b, data, &r.cards, r.budget.Cards); err != nil { return err } b.verified = data != nil b.pending = len(b.children) if b.pending > 0 { b.data = data } queue = append(queue, b.children...) } // Missing-source/cyclic components cannot yield verified outside proofs. for i := range blobs { if !blobs[i].verified { r.facts.unreadable = append(r.facts.unreadable, blobs[i].uuid) } } return nil } // expand applies b's delta to its base, then releases the base when b was // the last delta waiting on it. func (r *referenceReader) expand(b *referenceBlob, blobs []referenceBlob, index map[libfossil.FslID]int) ([]byte, error) { if b.source == 0 { return b.payload, nil } parent := &blobs[index[b.source]] base := parent.data parent.pending-- if parent.pending == 0 { parent.data = nil } if b.unreadable { return nil, nil } size, err := delta.OutputSize(b.payload) if err != nil || size != uint64(b.size) || size > uint64(r.budget.ExpandedBytes-r.work.ExpandBytes) { return nil, fmt.Errorf("sparse preparation: delta output budget or size mismatch") } if base == nil { return nil, nil } r.work.ExpandBytes += int64(size) // Before Apply allocates. data, err := delta.Apply(base, b.payload) if err != nil { return nil, nil // Corrupt content cannot prove outside membership. } return data, nil } func referenceRecord(facts *ConeReferenceFacts, b *referenceBlob, data []byte, aliases *int, max int) error { facts.held[b.uuid] = b.rid if !b.member || !deck.HasValidZ(data) { return nil // Ordinary file bytes are never parsed; byte budgets bound them. } // Bound parser card allocations before Parse, including non-F cards. cards := bytes.Count(data, []byte{'\n'}) if cards > max-*aliases { return fmt.Errorf("sparse preparation: card budget exceeded") } *aliases += cards d, err := deck.Parse(data) if err != nil { return nil // Ordinary file bytes do not provide reference facts. } d = referenceDeck(d) facts.records = append(facts.records, coneReferenceRecord{uuid: b.uuid, deck: d}) if d.Type == deck.Submanifest { facts.byUUID[b.uuid] = d facts.nodes[b.rid] = d } if d.Type == deck.Checkin { facts.roots = append(facts.roots, d) } return nil }