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}