v0.1.1 de06cbd

bench_cold_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"context"
7	"sync/atomic"
8	"testing"
9)
10
11// The benchmarks of bench_test.go read one object over and over, which a cache
12// answers from its first hit. These are the cold ones. Each has a serial and
13// a parallel variant (b.RunParallel, GOMAXPROCS goroutines, each starting at
14// another object) since a server reads from many requests at once:
15//
16//   - "nocache": WithDeltaCacheSize(0), so a deltified object is rebuilt from
17//     its base each time, with the cost of inflating the base;
18//   - "rotating": a different object each time, from a set of up to 8192.
19
20// coldSet returns up to n objects of the repository of the fixture, spread
21// over its packs, with their kinds: the first return holds the deltified
22// ones, the second the others.
23func coldSet(tb testing.TB, r *Repo, n int) (deltified, plain []ObjectID) {
24	tb.Helper()
25	s := r.snap.Load()
26	for _, p := range s.packs {
27		step := max(1, int(p.idx.n)/(2*n))
28		for i := uint32(0); i < p.idx.n && (len(deltified) < n || len(plain) < n); i += uint32(step) {
29			off, err := p.idx.offset(i)
30			if err != nil {
31				tb.Fatal(err)
32			}
33			h, err := parseEntryHeader(p.data, off)
34			if err != nil {
35				tb.Fatal(err)
36			}
37			var id ObjectID
38			copy(id[:], p.idx.id(i))
39			switch {
40			case h.isDelta() && len(deltified) < n:
41				deltified = append(deltified, id)
42			case !h.isDelta() && len(plain) < n:
43				plain = append(plain, id)
44			}
45		}
46	}
47	if len(deltified) == 0 {
48		tb.Fatal("no deltified object in the packs")
49	}
50	return deltified, plain
51}
52
53// reopen opens the repository of the fixture again, without the work of
54// the timer.
55func reopen(b *testing.B, fx *benchFixture, opts ...Option) *Repo {
56	b.Helper()
57	dir := fx.r.Dir()
58	r, err := Open(dir, opts...)
59	if err != nil {
60		b.Fatal(err)
61	}
62	return r
63}
64
65// runSerialAndParallel runs the step returned by setup b.N times in one
66// goroutine ("serial"), then b.N times spread over RunParallel's goroutines
67// ("parallel", one setup per goroutine). mk opens what the steps use.
68func runSerialAndParallel(b *testing.B, mk func(b *testing.B) (setup func() func(i int) error, closeFn func())) {
69	b.Run("serial", func(b *testing.B) {
70		setup, closeFn := mk(b)
71		defer closeFn()
72		step := setup()
73		b.ReportAllocs()
74		b.ResetTimer()
75		for i := 0; i < b.N; i++ {
76			if err := step(i); err != nil {
77				b.Fatal(err)
78			}
79		}
80	})
81	b.Run("parallel", func(b *testing.B) {
82		setup, closeFn := mk(b)
83		defer closeFn()
84		var worker atomic.Int64
85		b.ReportAllocs()
86		b.ResetTimer()
87		b.RunParallel(func(pb *testing.PB) {
88			step := setup()
89			i := int(worker.Add(1)) * 7919
90			for pb.Next() {
91				if err := step(i); err != nil {
92					b.Error(err)
93					return
94				}
95				i++
96			}
97		})
98	})
99}
100
101// BenchmarkReadObjectCold: nocache/<kind> reads one deltified object with no
102// delta cache; rotating reads a different object each time (deltified and
103// plain, default cache); rotating-nocache the same without the cache.
104func BenchmarkReadObjectCold(b *testing.B) {
105	fx := getBench(b)
106	delta, plain := coldSet(b, fx.r, 4096)
107	mixed := append(append([]ObjectID(nil), delta...), plain...)
108	for _, c := range []struct {
109		name  string
110		ids   []ObjectID
111		cache bool
112	}{
113		{"nocache/deltified-blob", []ObjectID{fx.deltaBlob}, false},
114		{"nocache/deltified-tree", []ObjectID{fx.deltaTree}, false},
115		{"rotating-nocache", mixed, false},
116		{"rotating", mixed, true},
117	} {
118		b.Run(c.name, func(b *testing.B) {
119			runSerialAndParallel(b, func(b *testing.B) (func() func(int) error, func()) {
120				var opts []Option
121				if !c.cache {
122					opts = append(opts, WithDeltaCacheSize(0))
123				}
124				r := reopen(b, fx, opts...)
125				return func() func(int) error {
126					var buf []byte
127					return func(i int) error {
128						_, data, err := r.ReadObject(c.ids[i%len(c.ids)], buf)
129						buf = data
130						return err
131					}
132				}, func() { _ = r.Close() }
133			})
134		})
135	}
136}
137
138// BenchmarkObjectHeaderCold: first-lap reads the header of a different
139// deltified object each time and reopens the Repo (so nothing is cached) after
140// every lap over the set: the cold cost alone. warm-rotating reads them again
141// and again, as GetTree does for the files of a directory.
142func BenchmarkObjectHeaderCold(b *testing.B) {
143	fx := getBench(b)
144	delta, _ := coldSet(b, fx.r, 4096)
145	b.Run("deltified-first-lap", func(b *testing.B) {
146		b.ReportAllocs()
147		r := reopen(b, fx)
148		defer func() { _ = r.Close() }()
149		for i := 0; i < b.N; i++ {
150			if i > 0 && i%len(delta) == 0 {
151				b.StopTimer()
152				_ = r.Close()
153				r = reopen(b, fx)
154				b.StartTimer()
155			}
156			if _, _, err := r.ObjectHeader(delta[i%len(delta)]); err != nil {
157				b.Fatal(err)
158			}
159		}
160	})
161	b.Run("deltified-warm-rotating", func(b *testing.B) {
162		r := reopen(b, fx)
163		defer func() { _ = r.Close() }()
164		for _, id := range delta { // warm the cache
165			if _, _, err := r.ObjectHeader(id); err != nil {
166				b.Fatal(err)
167			}
168		}
169		runSerialAndParallel(b, func(b *testing.B) (func() func(int) error, func()) {
170			return func() func(int) error {
171				return func(i int) error {
172					_, _, err := r.ObjectHeader(delta[i%len(delta)])
173					return err
174				}
175			}, func() {}
176		})
177	})
178}
179
180// BenchmarkWalkCold walks 100 commits from a different start each time, with
181// no delta cache: the commits come from the pack, deltified or not, and the
182// graph if the repository has one.
183func BenchmarkWalkCold(b *testing.B) {
184	fx := getBench(b)
185	starts := fx.allCommits
186	if len(starts) > 4096 {
187		starts = starts[:4096]
188	}
189	for _, shape := range []struct {
190		name   string
191		idOnly bool
192	}{{"ids", true}, {"full", false}} {
193		b.Run(shape.name, func(b *testing.B) {
194			runSerialAndParallel(b, func(b *testing.B) (func() func(int) error, func()) {
195				r := reopen(b, fx, WithDeltaCacheSize(0))
196				return func() func(int) error {
197					return func(i int) error {
198						for _, err := range r.Walk(context.Background(), starts[i%len(starts)], WalkOptions{Limit: 100, IDOnly: shape.idOnly}) {
199							if err != nil {
200								return err
201							}
202						}
203						return nil
204					}
205				}, func() { _ = r.Close() }
206			})
207		})
208	}
209}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close