package gitx import ( "context" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "github.com/go-git/go-git/v5/plumbing/format/diff" "sourcecraft.dev/bigbes/sr-ht-compare/core" ) // The benchmarks of the diffing path — what this service spends a request in. // Every page it serves is one of these calls plus a template render: Diff and // DiffStat behind /~owner/repo/compare/base..head, CommitPatch behind // /~owner/repo/commit/rev, mapFilePatches and cutPatch inside all three. // // They are uploaded to bench.srht.bigb.es by the `bench` task of .build.yml, so // their names are part of that pipeline: the task greps for them before it // POSTs, because `go test -bench` that matches nothing still prints "ok" and // exits 0 (docs/ci.md#bench). // // A builds.sr.ht VM measures a shape rather than a number — it is a shared // virtual machine with no CPU pinning — so what is worth reading off the series // is a step, not a millisecond. const ( // benchFiles and benchLines size the fixture repository. They are what makes // this a diff benchmark rather than a go-git-open benchmark: the existing // test fixture's whole history is a dozen lines, which the render finishes // before the tree walk has paid for itself. // // The pair produces a ~30 000-line base tree and a head commit touching a // quarter of it, which lands a two-dot patch in the tens of kilobytes — the // size of an ordinary change under review, and small enough that a run at // the family's -count=10 stays inside a CI VM's patience. benchFiles = 40 benchLines = 120 // benchModified is how many of the files the feature branch edits. benchModified = 12 ) // benchRepo is the fixture the diff benchmarks measure against: // // seed ── main-work (branch: main) // \ // feature-work (branch: feature) // // seed : benchFiles files of benchLines lines each // main-work : edits 4 of them, so main and feature have really diverged // feature-work: edits benchModified, adds 3, deletes 2, renames 1 // // The divergence is the point: with main advanced past the fork, "main..feature" // and "main...feature" are different comparisons, and the three-dot form (the // one a review page uses) has a merge base to find. type benchRepo struct { root string // reposRoot, the directory Open takes main string // SHA of main head string // SHA of feature } // benchLineFile renders one fixture file. The seed and the edit differ in a // handful of lines out of benchLines, so the hunks are hunks and not whole-file // rewrites — a rewrite would measure go-git's line differ on its easiest input. func benchLineFile(name string, edited bool) []byte { var b strings.Builder for i := range benchLines { switch { case edited && i%17 == 0: fmt.Fprintf(&b, "%s line %d edited by the feature branch\n", name, i) default: fmt.Fprintf(&b, "%s line %d with enough text to look like source\n", name, i) } } return []byte(b.String()) } func benchFileName(i int) string { return fmt.Sprintf("src/f%02d.txt", i) } // newBenchRepo builds the bare fixture repository under a temporary directory // owned by b, and returns the handles the benchmarks resolve against. func newBenchRepo(b *testing.B) benchRepo { b.Helper() if _, err := exec.LookPath("git"); err != nil { b.Skipf("git not available: %v", err) } root := b.TempDir() work := b.TempDir() d1 := "2024-03-01T00:00:00Z" d2 := "2024-03-02T00:00:00Z" d3 := "2024-03-03T00:00:00Z" gitTest(b, work, d1, "init", "-b", "main") for i := range benchFiles { writeFile(b, work, benchFileName(i), benchLineFile(benchFileName(i), false)) } gitTest(b, work, d1, "add", "-A") gitTest(b, work, d1, "commit", "-m", "seed: the tree under comparison") gitTest(b, work, d2, "branch", "feature") // main advances past the fork point, so the merge base is not main. for i := range 4 { name := benchFileName(i) writeFile(b, work, name, benchLineFile(name+" on main", true)) } gitTest(b, work, d2, "add", "-A") gitTest(b, work, d2, "commit", "-m", "main-work: edits on the base branch") gitTest(b, work, d3, "checkout", "feature") for i := range benchModified { name := benchFileName(i) writeFile(b, work, name, benchLineFile(name, true)) } for i := range 3 { name := fmt.Sprintf("src/added%02d.txt", i) writeFile(b, work, name, benchLineFile(name, false)) } for i := benchFiles - 2; i < benchFiles; i++ { if err := os.Remove(filepath.Join(work, benchFileName(i))); err != nil { b.Fatal(err) } } gitTest(b, work, d3, "mv", benchFileName(20), "src/renamed.txt") gitTest(b, work, d3, "add", "-A") gitTest(b, work, d3, "commit", "-m", "feature-work: edits, additions, deletions, a rename") if err := os.MkdirAll(filepath.Join(root, "~"+fxOwner), 0o755); err != nil { b.Fatal(err) } gitTest(b, work, d3, "clone", "--bare", work, filepath.Join(root, "~"+fxOwner, "benchrepo")) return benchRepo{ root: root, main: fxRevIn(b, root, "benchrepo", "main"), head: fxRevIn(b, root, "benchrepo", "feature"), } } // openBench opens the fixture through the code under test. func openBench(b *testing.B, fx benchRepo) *Repo { b.Helper() repo, err := Open(fx.root, fxOwner, "benchrepo") if err != nil { b.Fatalf("Open: %v", err) } return repo } // BenchmarkDiff renders the unified diff a compare page shows, in both of the // grammars ParseCompareSpec accepts. Three-dot is the costlier of the two and // the one a review reaches for: it resolves a merge base before it can diff. func BenchmarkDiff(b *testing.B) { fx := newBenchRepo(b) repo := openBench(b, fx) ctx := context.Background() for _, tc := range []struct { name string threeDot bool }{ {"two-dot", false}, {"three-dot", true}, } { b.Run(tc.name, func(b *testing.B) { spec := core.CompareSpec{Base: fx.main, Head: fx.head, ThreeDot: tc.threeDot} b.ReportAllocs() for b.Loop() { p, err := repo.Diff(ctx, spec) if err != nil { b.Fatalf("Diff: %v", err) } if p.Truncated { b.Fatal("the fixture patch hit the byte cap: the benchmark is measuring cutPatch") } } }) } } // BenchmarkDiffStat measures the file list of the same comparison. It is not a // cheaper Diff and is not expected to read as one: the patch is generated in // full either way, and what this leaves out is only the String() of it. func BenchmarkDiffStat(b *testing.B) { fx := newBenchRepo(b) repo := openBench(b, fx) ctx := context.Background() spec := core.CompareSpec{Base: fx.main, Head: fx.head, ThreeDot: true} b.ReportAllocs() for b.Loop() { files, err := repo.DiffStat(ctx, spec) if err != nil { b.Fatalf("DiffStat: %v", err) } if len(files) == 0 { b.Fatal("no file changes: the fixture is not what the benchmark thinks it is") } } } // BenchmarkCommitPatch measures the single-commit view: resolve a revision, // diff it against its first parent, render and map. It is the whole of what // /~owner/repo/commit/ does. func BenchmarkCommitPatch(b *testing.B) { fx := newBenchRepo(b) repo := openBench(b, fx) ctx := context.Background() b.ReportAllocs() for b.Loop() { patch, files, info, err := repo.CommitPatch(ctx, fx.head) if err != nil { b.Fatalf("CommitPatch: %v", err) } if patch.Text == "" || len(files) == 0 || info == nil { b.Fatal("empty commit patch: the fixture is not what the benchmark thinks it is") } } } // BenchmarkMapFilePatches measures the pure projection every diff surface runs // over go-git's result. It uses the synthetic file patches of mapping_test.go // rather than a repository, because it is the tallying of chunks that is being // measured and a real diff would bury it under its own generation. func BenchmarkMapFilePatches(b *testing.B) { line := strings.Repeat("a line of content that is about this long\n", 8) fps := make([]diff.FilePatch, 0, benchFiles) for i := range benchFiles { name := benchFileName(i) fps = append(fps, fakeFilePatch{ from: file(name), to: file(name), chunks: []diff.Chunk{ fakeChunk{content: line, op: diff.Equal}, fakeChunk{content: line, op: diff.Delete}, fakeChunk{content: line, op: diff.Add}, fakeChunk{content: line, op: diff.Equal}, }, }) } b.ReportAllocs() for b.Loop() { if got := mapFilePatches(fps); len(got) != benchFiles { b.Fatalf("mapped %d file changes, want %d", len(got), benchFiles) } } } // BenchmarkCutPatch measures the byte cap on the two inputs that behave // differently: text under the limit, which must be returned untouched, and text // over it, which is scanned backwards for a file boundary. The second is the // one a pathological diff reaches, and it is the reason the cap exists. func BenchmarkCutPatch(b *testing.B) { var sb strings.Builder for i := range benchFiles { name := benchFileName(i) fmt.Fprintf(&sb, "diff --git a/%s b/%s\n--- a/%s\n+++ b/%s\n", name, name, name, name) sb.Write(benchLineFile(name, false)) } text := sb.String() for _, tc := range []struct { name string limit int64 }{ {"under-the-cap", int64(len(text)) + 1}, {"over-the-cap", int64(len(text) / 2)}, } { b.Run(tc.name, func(b *testing.B) { wantTruncated := tc.limit < int64(len(text)) b.ReportAllocs() for b.Loop() { out, truncated := cutPatch(text, tc.limit) if truncated != wantTruncated { b.Fatalf("truncated=%v, want %v", truncated, wantTruncated) } if out == "" { b.Fatal("cutPatch returned nothing") } } }) } }