~bigbes/sr-ht-compare

ref: 9720ccc2e149ba5e63bb40822684a97e388c960f sr-ht-compare/gitx/bench_test.go -rw-r--r-- 9.3 KiB
9720ccc2 — bigbes go.mod: take the shared libraries' current heads 2 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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/<rev> 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")
				}
			}
		})
	}
}