1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bufio"
7 "context"
8 "io"
9 "os/exec"
10 "path"
11 "testing"
12)
13
14// gitLogCount runs git log like the gitcli adapter does and reads every
15// line of the output.
16func gitLogCount(b *testing.B, x *realWalk, args ...string) int {
17 cmd := exec.Command("git", append([]string{"-C", x.dir}, args...)...)
18 cmd.Env = gitEnv()
19 out, err := cmd.StdoutPipe()
20 if err != nil {
21 b.Fatal(err)
22 }
23 if err := cmd.Start(); err != nil {
24 b.Fatal(err)
25 }
26 n := 0
27 br := bufio.NewReader(out)
28 for {
29 _, err := br.ReadSlice('\n')
30 if err == io.EOF {
31 break
32 }
33 if err != nil && err != bufio.ErrBufferFull {
34 b.Fatal(err)
35 }
36 n++
37 }
38 if err := cmd.Wait(); err != nil {
39 b.Fatal(err)
40 }
41 return n
42}
43
44// gitcliFormat is the format the gitcli adapter of ermite asks git log for.
45const gitcliFormat = "%H%x00%P%x00%an%x00%ae%x00%at%x00%cn%x00%ce%x00%ct%x00%B"
46
47// benchWalk compares Walk with git log in two shapes: ids only (git answers
48// from the commit-graph without reading commits) and complete commits, which
49// is what the gitcli adapter asks for (git has to read every commit).
50func benchWalk(b *testing.B, x *realWalk, opts WalkOptions, gitArgs ...string) {
51 run := func(b *testing.B, o WalkOptions) {
52 b.ReportAllocs()
53 var n int
54 for i := 0; i < b.N; i++ {
55 n = 0
56 for c, err := range x.r.Walk(context.Background(), x.head, o) {
57 if err != nil {
58 b.Fatal(err)
59 }
60 if !o.IDOnly {
61 n += len(c.Message) // the commit is really parsed
62 }
63 n++
64 }
65 }
66 _ = n
67 }
68 for _, shape := range []struct {
69 name string
70 idOnly bool
71 format string
72 }{{"ids", true, "--format=%H"}, {"full", false, "-z --format=" + gitcliFormat}} {
73 b.Run(shape.name+"/ermite", func(b *testing.B) {
74 o := opts
75 o.IDOnly = shape.idOnly
76 run(b, o)
77 })
78 b.Run(shape.name+"/git", func(b *testing.B) {
79 b.ReportAllocs()
80 args := []string{"log"}
81 if shape.idOnly {
82 args = append(args, shape.format)
83 } else {
84 args = append(args, "-z", "--format="+gitcliFormat)
85 }
86 args = append(args, gitArgs...)
87 for i := 0; i < b.N; i++ {
88 gitLogCount(b, x, args...)
89 }
90 })
91 }
92}
93
94func BenchmarkWalk(b *testing.B) {
95 x := getRealWalk(b)
96 head := x.head.String()
97 b.Run("full-history", func(b *testing.B) { benchWalk(b, x, WalkOptions{}, head) })
98 b.Run("skip5000-limit100", func(b *testing.B) {
99 benchWalk(b, x, WalkOptions{Skip: 5000, Limit: 100}, "--skip=5000", "-n", "100", head)
100 })
101 b.Run("file", func(b *testing.B) {
102 benchWalk(b, x, WalkOptions{Path: "pkg/gui/gui.go"}, head, "--", "pkg/gui/gui.go")
103 })
104 b.Run("dir", func(b *testing.B) {
105 benchWalk(b, x, WalkOptions{Path: "pkg/commands"}, head, "--", "pkg/commands")
106 })
107 b.Run("rare-file", func(b *testing.B) {
108 benchWalk(b, x, WalkOptions{Path: "go.mod"}, head, "--", "go.mod")
109 })
110 b.Run("page-100-of-file", func(b *testing.B) {
111 benchWalk(b, x, WalkOptions{Path: "pkg/gui/gui.go", Limit: 100}, "-n", "100", head, "--", "pkg/gui/gui.go")
112 })
113}
114
115func BenchmarkLastCommits(b *testing.B) {
116 x := getRealWalk(b)
117 head := x.head.String()
118 for _, dir := range []string{"", "pkg/gui"} {
119 spec := head
120 if dir != "" {
121 spec += ":" + dir
122 }
123 names := lines(x.git(b, "ls-tree", "--name-only", spec))
124 name := dir
125 if name == "" {
126 name = "root"
127 }
128 b.Run(name+"/ermite", func(b *testing.B) {
129 b.ReportAllocs()
130 for i := 0; i < b.N; i++ {
131 if _, err := x.r.LastCommits(context.Background(), x.head, dir, names); err != nil {
132 b.Fatal(err)
133 }
134 }
135 b.ReportMetric(float64(len(names)), "entries")
136 })
137 b.Run(name+"/git-n-calls", func(b *testing.B) {
138 b.ReportAllocs()
139 for i := 0; i < b.N; i++ {
140 for _, n := range names {
141 gitLogCount(b, x, "log", "-n", "1", "--format=%H", head, "--", path.Join(dir, n))
142 }
143 }
144 })
145 }
146}