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}