1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "fmt"
7 "math"
8 "sync"
9 "testing"
10)
11
12func TestHeaderCacheSizing(t *testing.T) {
13 for _, c := range []struct {
14 objects uint32
15 slots int
16 }{
17 {0, minHeaderSlots}, {1, minHeaderSlots}, {64, 64}, {65, 128}, {1000, 1024},
18 {maxHeaderSlots, maxHeaderSlots}, {maxHeaderSlots + 1, maxHeaderSlots}, {1 << 31, maxHeaderSlots},
19 } {
20 hc := newHeaderCache(c.objects)
21 if len(hc.slots) != c.slots {
22 t.Errorf("%d objects: %d slots, want %d", c.objects, len(hc.slots), c.slots)
23 }
24 }
25 if max := maxHeaderSlots * headerSlotBytes; max > 2<<20 {
26 t.Errorf("a pack may pin %d bytes of header cache", max)
27 }
28}
29
30func TestHeaderCacheGetPut(t *testing.T) {
31 hc := newHeaderCache(1000)
32 if _, _, ok := hc.get(12); ok {
33 t.Fatal("hit in an empty cache")
34 }
35 hc.put(12, TypeTree, 4242)
36 if typ, size, ok := hc.get(12); !ok || typ != TypeTree || size != 4242 {
37 t.Fatalf("get = %v %v %v", typ, size, ok)
38 }
39 // Offset 0 and size 0 are values like others.
40 hc.put(0, TypeBlob, 0)
41 if typ, size, ok := hc.get(0); !ok || typ != TypeBlob || size != 0 {
42 t.Fatalf("get(0) = %v %v %v", typ, size, ok)
43 }
44 // An offset that would wrap to the empty marker (off+1 == 0) never hits
45 // an empty slot and is never stored.
46 hc.put(math.MaxUint64, TypeBlob, 7)
47 if _, _, ok := hc.get(math.MaxUint64); ok {
48 t.Fatal("MaxUint64 cached")
49 }
50 if _, _, ok := newHeaderCache(1000).get(math.MaxUint64); ok {
51 t.Fatal("MaxUint64 hits an empty slot")
52 }
53 // A size that does not fit a slot is not cached, not truncated.
54 hc.put(99, TypeBlob, maxCachedSize)
55 if _, _, ok := hc.get(99); ok {
56 t.Fatal("oversized entry cached")
57 }
58 // Distinct offsets that share a slot never answer for each other.
59 for off := uint64(1); off < 20000; off++ {
60 hc.put(off, TypeBlob, int64(off)*3)
61 }
62 hits := 0
63 for off := uint64(1); off < 20000; off++ {
64 if typ, size, ok := hc.get(off); ok {
65 hits++
66 if typ != TypeBlob || size != int64(off)*3 {
67 t.Fatalf("offset %d answered %v %d", off, typ, size)
68 }
69 }
70 }
71 if hits == 0 || hits > len(hc.slots) {
72 t.Fatalf("%d hits for %d slots", hits, len(hc.slots))
73 }
74}
75
76func TestHeaderCacheConcurrent(t *testing.T) {
77 hc := newHeaderCache(64)
78 var wg sync.WaitGroup
79 for g := range 8 {
80 wg.Add(1)
81 go func() {
82 defer wg.Done()
83 for i := range 20000 {
84 off := uint64((i*7 + g) % 500)
85 if typ, size, ok := hc.get(off); ok {
86 if typ != TypeBlob || size != int64(off)*5 {
87 t.Errorf("torn read at %d: %v %d", off, typ, size)
88 return
89 }
90 } else {
91 hc.put(off, TypeBlob, int64(off)*5)
92 }
93 }
94 }()
95 }
96 wg.Wait()
97}
98
99func TestHeaderCacheNoAllocOnHit(t *testing.T) {
100 hc := newHeaderCache(64)
101 hc.put(77, TypeBlob, 5)
102 if got := testing.AllocsPerRun(100, func() { hc.get(77) }); got != 0 {
103 t.Fatalf("a hit allocates %v", got)
104 }
105}
106
107// ObjectHeader answers the same with and without the cache for every object of
108// a deltified pack, a second time too (from the cache), and a hit allocates
109// nothing and does not inflate.
110func TestObjectHeaderCached(t *testing.T) {
111 f := newFx(t)
112 for i := 0; i < 40; i++ {
113 f.write("f.txt", append(textLines(300, 0), fmt.Sprintf("rev %d\n", i)...), 0o644)
114 f.write(fmt.Sprintf("many/file-%03d.txt", i), []byte{byte(i)}, 0o644)
115 f.commit(fmt.Sprintf("commit %d", i))
116 }
117 f.git("repack", "-adf", "--depth=50", "--window=250")
118 r := f.open()
119 p := r.snap.Load().packs[0]
120 deltas := 0
121 for i := range p.idx.n {
122 off, _ := p.idx.offset(i)
123 var id ObjectID
124 copy(id[:], p.idx.id(i))
125 wt, ws, _, err := p.readHeader(off) // uncached
126 if err != nil {
127 t.Fatal(err)
128 }
129 for range 2 {
130 gt, gs, err := r.ObjectHeader(id)
131 if err != nil || gt != wt || gs != ws {
132 t.Fatalf("%s: ObjectHeader = %v %d %v, want %v %d", id, gt, gs, err, wt, ws)
133 }
134 }
135 if h, _ := parseEntryHeader(p.data, off); h.isDelta() {
136 deltas++
137 if _, _, ok := p.hdrs.Load().get(off); !ok && len(p.hdrs.Load().slots) >= int(p.idx.n) {
138 t.Errorf("%s: deltified header not cached", id)
139 }
140 if raceEnabled {
141 continue
142 }
143 if got := testing.AllocsPerRun(20, func() { _, _, _ = r.ObjectHeader(id) }); got != 0 {
144 t.Errorf("%s: cached ObjectHeader allocates %v", id, got)
145 }
146 }
147 }
148 if deltas == 0 {
149 t.Fatal("no deltified object in the fixture")
150 }
151}
152
153// An offset past the end of the pack is an error, never an answer from an
154// empty cache slot (math.MaxUint64 + 1 wraps to the empty marker).
155func TestPackHeaderOutOfRange(t *testing.T) {
156 f := newFx(t)
157 for i := 0; i < 5; i++ {
158 f.write("f.txt", append(textLines(300, 0), fmt.Sprintf("rev %d\n", i)...), 0o644)
159 f.commit(fmt.Sprintf("commit %d", i))
160 }
161 f.git("repack", "-adf", "--depth=50", "--window=250")
162 r := f.open()
163 p := r.snap.Load().packs[0]
164 for i := range p.idx.n { // allocate the cache
165 off, _ := p.idx.offset(i)
166 if _, _, err := p.header(off); err != nil {
167 t.Fatal(err)
168 }
169 }
170 if p.hdrs.Load() == nil {
171 t.Skip("no deltified object, no cache")
172 }
173 for _, off := range []uint64{uint64(len(p.data)), math.MaxUint64 - 1, math.MaxUint64} {
174 if typ, size, err := p.header(off); err == nil {
175 t.Errorf("header(%d) = %v %d, want an error", off, typ, size)
176 }
177 }
178}