v0.1.1 de06cbd

headercache_test.go RawHistoryFolder

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}

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