Artifact 302ce118e07659ffda853d0ff17cfb824aa6745aa48b621ac7954299e9c7f4f3:
- File
internal/verify/check_blobs.go
— part of check-in
[0fd2a507b8]
at
2026-07-24 15:27:03
on branch trunk
— perf: route history-walking expansions through content.Cache (#151) (#153)
The buffer-reuse fix in #145/#150 cut the cost of expanding one delta chain once. It did nothing for callers that expand many versions of the same artifact in a single operation: each bare content.Expand still replayed the whole O(depth) chain, so a walk over N overlapping revisions cost O(N^2) blob reads even though content.Cache already existed for exactly this pattern.
Audited every non-test content.Expand caller. Two access patterns genuinely repeat over overlapping delta chains and are now routed through a per-operation content.Cache:
- annotate walks every revision of one file along the parent chain. content_deltify stores each older file blob as a delta against its successor, so the per-revision expansions are all walks of the same few chains. One cache threaded through the walk (file blob + manifest listing via new manifest.ListFilesCached/GetManifestCached, both nil-passthrough so existing callers are unchanged) turns it linear. Measured on a deltified 150-revision file: 1.60s -> 0.65s (2.5x), 764k -> 308k allocs; the win grows with history depth.
- verify.Verify and verify.Rebuild sweep every blob more than once (Rebuild: checkBlobs + rebuildManifests + rebuildTags x2). A single cache shared across the pass walks each chain once instead of once per sweep and amortizes overlap within each sweep. Measured on a 150-revision two-file repo: Rebuild 3.34s -> 0.068s (48x), 4.59M -> 98k allocs.
Callers that only ever touch the tip or make a single pass over distinct blobs (branch create, merge-config load, R-card / delta manifest build, MergeParentFiles, crosslinkSingle, shun undelta, pairwise Diff) are left on bare Expand, each with a comment stating why a cache cannot amortize their access.
No behavior change: expansion output is byte-identical, verified across the modernc and ncruces drivers with REQUIRE_FOSSIL_BIN=1 cross-checks and the DST suite. (user: [email protected], size: 2703) [annotate] [blame] [check-ins using]
package verify import ( "fmt" "github.com/danmestas/go-libfossil/internal/content" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/repo" ) // checkBlobs verifies blob integrity: content hashing and delta application. // Walks every non-phantom blob, expands delta chains, hashes content, and // compares against stored UUID. // // cache is the shared expansion cache for the whole Verify/Rebuild pass. This // sweep touches every blob, and content_deltify stores older revisions as // deltas against newer ones, so two blobs in one file's history share the // same chain: expanding the deepest member materializes and caches every // interior, turning what would be O(revisions^2) blob reads into O(revisions). // The same cache carries forward to the later sweeps, which would otherwise // re-expand every blob from scratch. A nil cache expands uncached. func checkBlobs(r *repo.Repo, report *Report, cache *content.Cache) error { if r == nil { panic("checkBlobs: nil *repo.Repo") } if report == nil { panic("checkBlobs: nil *Report") } rows, err := r.DB().Query("SELECT rid, uuid FROM blob WHERE size >= 0") if err != nil { return fmt.Errorf("checkBlobs: query: %w", err) } defer rows.Close() type blobEntry struct { rid int64 uuid string } var entries []blobEntry for rows.Next() { var e blobEntry if err := rows.Scan(&e.rid, &e.uuid); err != nil { return fmt.Errorf("checkBlobs: scan: %w", err) } entries = append(entries, e) } if err := rows.Err(); err != nil { return fmt.Errorf("checkBlobs: rows: %w", err) } for _, e := range entries { report.BlobsChecked++ rid := libfossil.FslID(e.rid) data, err := cache.Expand(r.DB(), rid) if err != nil { report.BlobsFailed++ report.addIssue(Issue{ Kind: IssueBlobCorrupt, RID: rid, UUID: e.uuid, Table: "blob", Message: fmt.Sprintf("rid %d: expand failed: %v", e.rid, err), }) continue } var computed string if len(e.uuid) == 64 { computed = hash.SHA3(data) } else { computed = hash.SHA1(data) } if computed != e.uuid { report.BlobsFailed++ report.addIssue(Issue{ Kind: IssueHashMismatch, RID: rid, UUID: e.uuid, Table: "blob", Message: fmt.Sprintf("rid %d: hash mismatch (stored=%s computed=%s)", e.rid, e.uuid, computed), }) continue } report.BlobsOK++ } // Postcondition: every checked blob is either OK or failed. if report.BlobsChecked != report.BlobsOK+report.BlobsFailed { panic("checkBlobs: postcondition: BlobsChecked != BlobsOK + BlobsFailed") } return nil }