v0.1.0 5228465

alloc_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"bytes"
7	"fmt"
8	"testing"
9)
10
11func TestAllocTreeIterationPerEntry(t *testing.T) {
12	var tree []byte
13	const n = 1000
14	for i := 0; i < n; i++ {
15		tree = append(tree, fmt.Sprintf("100644 file-%04d.txt\x00", i)...)
16		tree = append(tree, bytes.Repeat([]byte{byte(i)}, 20)...)
17	}
18	count := 0
19	cb := func(e Entry) bool { count++; return true }
20	allocs := testing.AllocsPerRun(50, func() {
21		if err := ParseTree(tree, cb); err != nil {
22			t.Fatal(err)
23		}
24	})
25	if allocs != 0 {
26		t.Fatalf("ParseTree allocates %v per tree of %d entries, want 0", allocs, n)
27	}
28}
29
30func TestAllocReadTreeAndCommit(t *testing.T) {
31	if raceEnabled {
32		t.Skip("sync.Pool drops items under the race detector")
33	}
34	f := newFx(t)
35	for i := 0; i < 200; i++ {
36		f.write(fmt.Sprintf("dir/file-%03d.txt", i), []byte{byte(i)}, 0o644)
37	}
38	c := mustID(t, f.commit("one"))
39	tree := mustID(t, f.git("rev-parse", "HEAD:dir"))
40	for _, packed := range []bool{false, true} {
41		if packed {
42			f.git("repack", "-adf")
43		}
44		r := f.open()
45		entries := 0
46		cb := func(e Entry) bool { entries++; return true }
47		_ = r.ReadTree(tree, cb) // warm up the pools
48		got := testing.AllocsPerRun(50, func() {
49			if err := r.ReadTree(tree, cb); err != nil {
50				t.Fatal(err)
51			}
52		})
53		// A constant cost per tree (file handle, path), not per entry.
54		if got > 8 {
55			t.Errorf("packed=%v: ReadTree allocates %v for 200 entries", packed, got)
56		}
57		if got/float64(entries/50) > 0.1 {
58			t.Errorf("packed=%v: %v allocs per tree is not 0 per entry", packed, got)
59		}
60		var commit Commit
61		_ = r.ReadCommit(c, &commit)
62		got = testing.AllocsPerRun(50, func() {
63			if err := r.ReadCommit(c, &commit); err != nil {
64				t.Fatal(err)
65			}
66		})
67		limit := 8.0 // loose: path string, *os.File, inflater state
68		if packed {
69			limit = 1 // the snapshot reference only
70		}
71		t.Logf("packed=%v: ReadCommit allocs/op = %v", packed, got)
72		if got > limit {
73			t.Errorf("packed=%v: ReadCommit allocates %v per call after warm-up, want <= %v", packed, got, limit)
74		}
75	}
76	var commit Commit
77	raw := []byte("tree " + fmt.Sprintf("%040d", 1) + "\nparent " + fmt.Sprintf("%040d", 2) + "\nauthor A <a@x> 1 +0000\ncommitter B <b@x> 2 +0000\n\nmsg\n")
78	_ = ParseCommit(raw, &commit)
79	if got := testing.AllocsPerRun(100, func() { _ = ParseCommit(raw, &commit) }); got != 0 {
80		t.Errorf("ParseCommit allocates %v on a reused Commit", got)
81	}
82}
83
84// Packed reads allocate nothing once the pools are warm: the zlib reader
85// (flate decompressor and adler32 state) is reused, and so are the buffers.
86// The delta cache is on (the default): a deltified object is then a cached
87// base plus one small delta. Without it the base is inflated again at each
88// read, and compress/flate allocates the link tables of every dynamic block
89// with a code longer than 9 bits (see TestAllocColdDynamicBlock).
90func TestAllocPackedReads(t *testing.T) {
91	if raceEnabled {
92		t.Skip("sync.Pool drops items under the race detector")
93	}
94	f := newFx(t)
95	for i := 0; i < 30; i++ {
96		f.write("f.txt", append(textLines(400, 0), fmt.Sprintf("rev %d\n", i)...), 0o644)
97		f.write(fmt.Sprintf("many/file-%03d.txt", i), []byte{byte(i)}, 0o644)
98		f.commit(fmt.Sprintf("commit %d", i))
99	}
100	f.git("repack", "-adf", "--depth=50", "--window=250")
101	r := f.open()
102	s := r.snap.Load()
103	var commit, deltaBlob, deltaTree ObjectID
104	for _, p := range s.packs {
105		for i := range p.idx.n {
106			off, _ := p.idx.offset(i)
107			h, err := parseEntryHeader(p.data, off)
108			if err != nil {
109				t.Fatal(err)
110			}
111			var id ObjectID
112			copy(id[:], p.idx.id(i))
113			if !h.isDelta() {
114				if h.typ == uint8(TypeCommit) {
115					commit = id
116				}
117				continue
118			}
119			typ, err := p.chainEnd(off, h)
120			if err != nil {
121				t.Fatal(err)
122			}
123			switch typ {
124			case TypeBlob:
125				deltaBlob = id
126			case TypeTree:
127				deltaTree = id
128			}
129		}
130	}
131	if commit.IsZero() || deltaBlob.IsZero() || deltaTree.IsZero() {
132		t.Fatalf("fixture lacks an object: commit %v, deltified blob %v, deltified tree %v", !commit.IsZero(), !deltaBlob.IsZero(), !deltaTree.IsZero())
133	}
134	for _, c := range []struct {
135		name string
136		id   ObjectID
137	}{{"packed commit", commit}, {"deltified blob", deltaBlob}, {"deltified tree", deltaTree}} {
138		var buf []byte
139		read := func() {
140			_, data, err := r.ReadObject(c.id, buf)
141			if err != nil {
142				t.Fatal(err)
143			}
144			buf = data
145		}
146		read() // warm up
147		if got := testing.AllocsPerRun(100, read); got != 0 {
148			t.Errorf("%s: ReadObject allocates %v per call after warm-up, want 0", c.name, got)
149		}
150	}
151	var cm Commit
152	_ = r.ReadCommit(commit, &cm)
153	if got := testing.AllocsPerRun(100, func() { _ = r.ReadCommit(commit, &cm) }); got != 0 {
154		t.Errorf("packed commit: ReadCommit allocates %v per call after warm-up, want 0", got)
155	}
156}
157
158// compress/flate keeps the 9-bit decoding table of each Huffman tree in the
159// decompressor, so Reset reuses it. For codes longer than 9 bits it builds
160// link tables with make() in huffmanDecoder.init at every dynamic block, and
161// Reset clears them (*h = huffmanDecoder{}), so they are never reused. A
162// zlibReader therefore allocates per dynamic block with long codes and
163// nothing otherwise. This pins the behaviour: the numbers are Go's, not
164// ours, and the test fails if a Go release changes them (good news then).
165func TestAllocColdDynamicBlock(t *testing.T) {
166	if raceEnabled {
167		t.Skip("sync.Pool drops items under the race detector")
168	}
169	var z zlibReader
170	for _, c := range []struct {
171		name string
172		data []byte
173		zero bool
174	}{
175		{"short codes (all literals the same)", bytes.Repeat([]byte("ab"), 4000), true},
176		{"fixed Huffman block", []byte("tiny"), true},
177		{"prose-like text", textLines(400, 0), false},
178	} {
179		src := zlibBytes(c.data)
180		dst := make([]byte, len(c.data))
181		var br bytes.Reader
182		probe := make([]byte, 1)
183		read := func() {
184			br.Reset(src)
185			if err := z.Reset(&br); err != nil {
186				t.Fatal(err)
187			}
188			if err := inflateExact(&z, dst, probe); err != nil {
189				t.Fatal(err)
190			}
191		}
192		read()
193		got := testing.AllocsPerRun(50, read)
194		t.Logf("%s: %v allocs per stream", c.name, got)
195		if c.zero && got != 0 {
196			t.Errorf("%s: %v allocs, want 0", c.name, got)
197		}
198	}
199}

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