Artifact 76e280ad473731856a2014306aaa722bffd9890650e6799600c229532f6c1d9e:
- File
internal/branch/branch.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: 6604) [annotate] [blame] [check-ins using]
package branch import ( "fmt" "time" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/deck" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/tag" ) type CreateOpts struct { Name string Parent libfossil.FslID User string Time time.Time Color string } type Branch struct { Name string LastMTime float64 IsClosed bool CheckinCount int LatestUUID string } func Create(r *repo.Repo, opts CreateOpts) (libfossil.FslID, string, error) { if r == nil { panic("branch.Create: r must not be nil") } if opts.Name == "" { panic("branch.Create: opts.Name must not be empty") } if opts.Parent <= 0 { panic("branch.Create: opts.Parent must be positive") } if opts.User == "" { panic("branch.Create: opts.User must not be empty") } if opts.Time.IsZero() { opts.Time = time.Now().UTC() } // 1. Get parent's file list. entries, err := manifest.ListFiles(r, opts.Parent) if err != nil { return 0, "", fmt.Errorf("branch.Create list files: %w", err) } var files []manifest.File for _, e := range entries { frid, ok := content.AvailableByUUID(r.DB(), e.UUID) if !ok { return 0, "", fmt.Errorf("branch.Create: file blob %s not found", e.UUID) } // Bare Expand: each file of the single parent checkin is expanded // exactly once to seed the new branch commit. There is no repeated or // overlapping-chain access here for a cache to amortize. data, err := content.Expand(r.DB(), frid) if err != nil { return 0, "", fmt.Errorf("branch.Create: expand %s: %w", e.Name, err) } files = append(files, manifest.File{Name: e.Name, Content: data, Perm: e.Perm}) } // 2. Query existing sym-* tags on parent for cancel cards. rows, err := r.DB().Query( "SELECT tag.tagname FROM tagxref JOIN tag USING(tagid) WHERE tagxref.rid=? AND tagxref.tagtype>0 AND tag.tagname GLOB 'sym-*'", opts.Parent, ) if err != nil { return 0, "", fmt.Errorf("branch.Create query sym tags: %w", err) } defer rows.Close() var oldSyms []string for rows.Next() { var name string if err := rows.Scan(&name); err != nil { return 0, "", err } oldSyms = append(oldSyms, name) } if err := rows.Err(); err != nil { return 0, "", err } // 3. Build T-cards. var tags []deck.TagCard tags = append(tags, deck.TagCard{Type: deck.TagPropagating, Name: "branch", UUID: "*", Value: opts.Name}) tags = append(tags, deck.TagCard{Type: deck.TagPropagating, Name: "sym-" + opts.Name, UUID: "*"}) for _, old := range oldSyms { tags = append(tags, deck.TagCard{Type: deck.TagCancel, Name: old, UUID: "*"}) } if opts.Color != "" { tags = append(tags, deck.TagCard{Type: deck.TagPropagating, Name: "bgcolor", UUID: "*", Value: opts.Color}) } // 4. Create checkin with branch tags. rid, uuid, err := manifest.Checkin(r, manifest.CheckinOpts{ Files: files, Comment: fmt.Sprintf("Create branch %s", opts.Name), User: opts.User, Parent: opts.Parent, Time: opts.Time, Tags: tags, }) if err != nil { return 0, "", fmt.Errorf("branch.Create checkin: %w", err) } // 5. Process inline T-cards to apply tags. // Checkin already inserted event/plink/mlink, so we just need to process tags. if err := processInlineTags(r, rid, tags, opts.Time); err != nil { return 0, "", fmt.Errorf("branch.Create process tags: %w", err) } return rid, uuid, nil } func List(r *repo.Repo) ([]Branch, error) { if r == nil { panic("branch.List: r must not be nil") } rows, err := r.DB().Query(` SELECT tagxref.value AS branch_name, max(event.mtime) AS last_mtime, count(*) AS checkin_count, (SELECT 1 FROM tagxref tx2 JOIN tag t2 ON tx2.tagid=t2.tagid WHERE t2.tagname='closed' AND tx2.rid IN (SELECT rid FROM tagxref tx3 JOIN tag t3 ON tx3.tagid=t3.tagid WHERE t3.tagname='branch' AND tx3.value=tagxref.value AND tx3.tagtype>0) AND tx2.tagtype>0 LIMIT 1) AS isclosed, (SELECT b.uuid FROM blob b JOIN tagxref tx4 ON b.rid=tx4.rid JOIN tag t4 ON tx4.tagid=t4.tagid JOIN event e ON e.objid=tx4.rid WHERE t4.tagname='branch' AND tx4.value=tagxref.value AND tx4.tagtype>0 ORDER BY e.mtime DESC LIMIT 1) AS latest FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid JOIN event ON event.objid=tagxref.rid WHERE tag.tagname='branch' AND tagxref.tagtype>0 GROUP BY tagxref.value `) if err != nil { return nil, fmt.Errorf("branch.List: %w", err) } defer rows.Close() var branches []Branch for rows.Next() { var b Branch var isClosed *int if err := rows.Scan(&b.Name, &b.LastMTime, &b.CheckinCount, &isClosed, &b.LatestUUID); err != nil { return nil, fmt.Errorf("branch.List scan: %w", err) } b.IsClosed = isClosed != nil && *isClosed == 1 branches = append(branches, b) } return branches, rows.Err() } func Close(r *repo.Repo, name string, user string) error { if r == nil { panic("branch.Close: r must not be nil") } if name == "" { panic("branch.Close: name must not be empty") } var tipRID int64 err := r.DB().QueryRow(` SELECT tagxref.rid FROM tagxref JOIN tag ON tagxref.tagid=tag.tagid JOIN event ON event.objid=tagxref.rid WHERE tag.tagname='branch' AND tagxref.value=? AND tagxref.tagtype>0 ORDER BY event.mtime DESC LIMIT 1`, name, ).Scan(&tipRID) if err != nil { return fmt.Errorf("branch.Close: find tip for %q: %w", name, err) } _, err = tag.AddTag(r, tag.TagOpts{ TargetRID: libfossil.FslID(tipRID), TagName: "closed", TagType: tag.TagSingleton, User: user, }) if err != nil { return fmt.Errorf("branch.Close: add closed tag: %w", err) } return nil } func processInlineTags(r *repo.Repo, rid libfossil.FslID, tags []deck.TagCard, mtime time.Time) error { julianMtime := libfossil.TimeToJulian(mtime) for _, tc := range tags { if tc.UUID != "*" { continue // only process inline tags (UUID="*" means this checkin) } var tagType int switch tc.Type { case deck.TagPropagating: tagType = tag.TagPropagating case deck.TagSingleton: tagType = tag.TagSingleton case deck.TagCancel: tagType = tag.TagCancel default: continue } if err := tag.ApplyTag(r, tag.ApplyOpts{ TargetRID: rid, SrcRID: rid, // inline: checkin is its own source TagName: tc.Name, TagType: tagType, Value: tc.Value, MTime: julianMtime, }); err != nil { return fmt.Errorf("apply tag %q: %w", tc.Name, err) } } return nil }