Skip to content

Commit 5ebb0d1

Browse files
Napolitainandreyneringtrulede
authored
test: add benchmarks for glob cache performance (#2881)
Co-authored-by: Andrey Nering <andreynering@users.noreply.github.com> Co-authored-by: Timothy Rule <34501912+trulede@users.noreply.github.com>
1 parent 1f34895 commit 5ebb0d1

2 files changed

Lines changed: 220 additions & 0 deletions

File tree

Taskfile.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ tasks:
152152
cmds:
153153
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
154154

155+
bench:checksum:
156+
desc: Runs checksum benchmarks
157+
aliases: [bench]
158+
cmds:
159+
- go test -bench=. -benchmem -tags=fsbench -run=^$ ./...
160+
155161
goreleaser:test:
156162
desc: Tests release process without publishing
157163
cmds:

checksum_benchmark_test.go

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
//go:build fsbench
2+
// +build fsbench
3+
4+
package task_test
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"io"
10+
"io/fs"
11+
"os"
12+
"path/filepath"
13+
"testing"
14+
"time"
15+
16+
"github.com/stretchr/testify/require"
17+
18+
"github.com/go-task/task/v3"
19+
)
20+
21+
const (
22+
manySmallFileCount = 20_000
23+
smallFileSize = 5
24+
fewLargeFileCount = 4
25+
largeFileSize = 128 * 1024 * 1024
26+
)
27+
28+
func BenchmarkManySmallFiles(b *testing.B) {
29+
dir := b.TempDir()
30+
createBenchmarkFixture(b, dir, manySmallFileCount, smallFileSize)
31+
32+
benchmarkModes(b, dir, manySmallFileCount, smallFileSize)
33+
}
34+
35+
func BenchmarkFewLargeFiles(b *testing.B) {
36+
dir := b.TempDir()
37+
createBenchmarkFixture(b, dir, fewLargeFileCount, largeFileSize)
38+
39+
benchmarkModes(b, dir, fewLargeFileCount, largeFileSize)
40+
}
41+
42+
func benchmarkModes(b *testing.B, dir string, fileCount int, fileSize int64) {
43+
b.Helper()
44+
45+
for _, mode := range []struct {
46+
name string
47+
task string
48+
expectCache bool
49+
nativeMTime bool
50+
}{
51+
{name: "checksum", task: "checksum-yaml", expectCache: true},
52+
{name: "timestamp", task: "timestamp-yaml", expectCache: true},
53+
{name: "native-mtime", nativeMTime: true},
54+
{name: "none", task: "uncached-yaml"},
55+
} {
56+
b.Run(mode.name, func(b *testing.B) {
57+
if mode.nativeMTime {
58+
benchmarkNativeMTime(b, dir, fileCount, fileSize)
59+
return
60+
}
61+
benchmarkTask(b, dir, mode.task, mode.expectCache, fileCount, fileSize)
62+
})
63+
}
64+
}
65+
66+
func benchmarkTask(
67+
b *testing.B,
68+
dir string,
69+
taskName string,
70+
expectCache bool,
71+
fileCount int,
72+
fileSize int64,
73+
) {
74+
b.Helper()
75+
76+
tempDir := task.TempDir{
77+
Remote: filepath.Join(dir, ".task"),
78+
Fingerprint: filepath.Join(dir, ".task"),
79+
}
80+
81+
if expectCache {
82+
e := task.NewExecutor(
83+
task.WithDir(dir),
84+
task.WithStdout(io.Discard),
85+
task.WithStderr(io.Discard),
86+
task.WithTempDir(tempDir),
87+
)
88+
require.NoError(b, e.Setup())
89+
require.NoError(b, e.Run(b.Context(), &task.Call{Task: taskName}))
90+
}
91+
92+
b.ReportAllocs()
93+
sourceBytes := int64(fileCount) * fileSize
94+
if expectCache {
95+
b.SetBytes(sourceBytes)
96+
}
97+
b.ResetTimer()
98+
for range b.N {
99+
var buff bytes.Buffer
100+
e := task.NewExecutor(
101+
task.WithDir(dir),
102+
task.WithStdout(&buff),
103+
task.WithStderr(&buff),
104+
task.WithTempDir(tempDir),
105+
)
106+
require.NoError(b, e.Setup())
107+
require.NoError(b, e.Run(b.Context(), &task.Call{Task: taskName}))
108+
if expectCache {
109+
require.Contains(b, buff.String(), fmt.Sprintf(`Task "%s" is up to date`, taskName))
110+
}
111+
}
112+
if expectCache {
113+
b.ReportMetric(float64(fileCount), "source_files/op")
114+
b.ReportMetric(float64(sourceBytes)/(1024*1024), "source_MiB/op")
115+
}
116+
}
117+
118+
func benchmarkNativeMTime(b *testing.B, dir string, fileCount int, fileSize int64) {
119+
b.Helper()
120+
121+
output := filepath.Join(dir, "out", "native-mtime.txt")
122+
require.NoError(b, os.WriteFile(output, []byte("ok"), 0o644))
123+
outputTime := time.Now().Add(time.Second)
124+
require.NoError(b, os.Chtimes(output, outputTime, outputTime))
125+
126+
sourceRoot := filepath.Join(dir, "path", "to", "folder")
127+
sourceBytes := int64(fileCount) * fileSize
128+
129+
b.ReportAllocs()
130+
b.SetBytes(sourceBytes)
131+
b.ResetTimer()
132+
for range b.N {
133+
outputInfo, err := os.Stat(output)
134+
require.NoError(b, err)
135+
136+
upToDate, err := nativeMTimeUpToDate(sourceRoot, outputInfo.ModTime())
137+
require.NoError(b, err)
138+
require.True(b, upToDate)
139+
}
140+
b.ReportMetric(float64(fileCount), "source_files/op")
141+
b.ReportMetric(float64(sourceBytes)/(1024*1024), "source_MiB/op")
142+
}
143+
144+
func nativeMTimeUpToDate(sourceRoot string, outputTime time.Time) (bool, error) {
145+
upToDate := true
146+
err := filepath.WalkDir(sourceRoot, func(path string, d fs.DirEntry, err error) error {
147+
if err != nil {
148+
return err
149+
}
150+
if d.IsDir() || filepath.Ext(path) != ".yaml" {
151+
return nil
152+
}
153+
info, err := d.Info()
154+
if err != nil {
155+
return err
156+
}
157+
if info.ModTime().After(outputTime) {
158+
upToDate = false
159+
return fs.SkipAll
160+
}
161+
return nil
162+
})
163+
return upToDate, err
164+
}
165+
166+
func createBenchmarkFixture(tb testing.TB, dir string, fileCount int, fileSize int64) {
167+
tb.Helper()
168+
169+
taskfile := `version: '3'
170+
171+
tasks:
172+
checksum-yaml:
173+
sources:
174+
- path/to/folder/**/*.yaml
175+
generates:
176+
- out/checksum.txt
177+
cmds:
178+
- printf ok > out/checksum.txt
179+
180+
timestamp-yaml:
181+
method: timestamp
182+
sources:
183+
- path/to/folder/**/*.yaml
184+
generates:
185+
- out/timestamp.txt
186+
cmds:
187+
- printf ok > out/timestamp.txt
188+
189+
uncached-yaml:
190+
method: none
191+
cmds:
192+
- printf ok > out/uncached.txt
193+
`
194+
require.NoError(tb, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(taskfile), 0o644))
195+
require.NoError(tb, os.MkdirAll(filepath.Join(dir, "out"), 0o755))
196+
197+
for i := 1; i <= fileCount; i++ {
198+
subdir := filepath.Join(dir, "path", "to", "folder", fmt.Sprintf("%04d", i/100))
199+
require.NoError(tb, os.MkdirAll(subdir, 0o755))
200+
name := filepath.Join(subdir, fmt.Sprintf("file-%05d.yaml", i))
201+
createSparseFile(tb, name, fileSize)
202+
}
203+
}
204+
205+
func createSparseFile(tb testing.TB, name string, size int64) {
206+
tb.Helper()
207+
208+
file, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
209+
require.NoError(tb, err)
210+
defer func() {
211+
require.NoError(tb, file.Close())
212+
}()
213+
require.NoError(tb, file.Truncate(size))
214+
}

0 commit comments

Comments
 (0)