你使用的Go版本是什么( go version
)?
$ go version
go version devel go1.21-518889b35c Thu Feb 16 02:02:00 2023 +0000 linux/amd64
这个问题在最新版本的发布中是否重现?
是的,已通过Go 1.20验证。
你正在使用什么操作系统和处理器架构( go env
)?
go env
输出
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/mvdan/.cache/go-build"
GOENV="/home/mvdan/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/mvdan/go/pkg/mod"
GONOPROXY="github.com/cue-unity"
GONOSUMDB="github.com/cue-unity"
GOOS="linux"
GOPATH="/home/mvdan/go"
GOPRIVATE="github.com/cue-unity"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/home/mvdan/tip"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/home/mvdan/tip/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="devel go1.21-518889b35c Thu Feb 16 02:02:00 2023 +0000"
GCCGO="gccgo"
GOAMD64="v3"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/home/mvdan/src/nowt/go.mod"
GOWORK=""
CGO_CFLAGS="-O2 -g"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-O2 -g"
CGO_FFLAGS="-O2 -g"
CGO_LDFLAGS="-O2 -g"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build3213309622=/tmp/go-build -gno-record-gcc-switches"
你做了什么?
我参与的许多项目都在GitHub Actions上使用go test ./...
。其中大多数还使用GitHub的https://github.com/actions/cache来持久化Go的GOCACHE
和GOMODCACHE
,以便连续的CI运行如果几乎没有代码更改,就可以避免很多工作。
这在很大程度上起作用,但我注意到那些完全没有更改任何代码(例如,只修改README)的提交只会显示一些包为(cached)
,而其他包总是从头开始运行。例如,参见cue-lang/cue#2253。
经过相当多的调查与GODEBUG=gocachehash=1
及其testInputs
哈希值,并阅读了src/cmd/go/internal/test/test.go
后,我认为原因是修改时间。每当测试打开一个文件时,Go的自动测试缓存会跟踪该文件的路径、文件信息和内容,以了解是否应该在未来重新运行测试。文件信息包括修改时间:
func hashWriteStat(h io.Writer, info fs.FileInfo) {
fmt.Fprintf(h, "stat %d %x %v %v\n", info.Size(), uint64(info.Mode()), info.ModTime(), info.IsDir())
}
包括修改时间可能对测试缓存本身有用,而且通常在不断发展的机器上连续开发时不是一个问题。然而,CI有点特殊:它很常见地在临时环境中运行并从获取代码开始。特别是,GitHub Actions作为CI的使用方式往往从https://github.com/actions/checkout开始,它使用git clone
将仓库下载到类似于/home/runner/work/foobar/foobar
的路径。请注意,克隆目录的位置在每次运行之间不会改变;我最初怀疑这是问题所在,但事实并非如此。
问题在于git
没有任何方式存储修改时间,而git clone
在检出仓库时创建新文件,因此克隆的文件最终具有当前时间作为它们的修改时间。
作为一个复现者和演示示例,我已经编写了https://github.com/mvdan/nowt/tree/master/test-cache-open-modtime。它包含一个打开测试数据的Go测试和一个重现这种情况的脚本:
$ ./repro.bash
Clone and test twice in a row, using the same directory and -trimpath. The test caching does not work.
-rw-r--r-- 1 mvdan mvdan 8 Feb 16 22:14 test-cache-open-modtime/testdata/foo.txt
ok mvdan.cc/nowt/test-cache-open-modtime 0.001s
-rw-r--r-- 1 mvdan mvdan 8 Feb 16 22:14 test-cache-open-modtime/testdata/foo.txt
ok mvdan.cc/nowt/test-cache-open-modtime 0.001s
The same again, but this time resetting the testdata file's mtime, since git clone otherwise sets it to the time it ran.
-rw-r--r-- 1 mvdan mvdan 8 Jan 1 2000 test-cache-open-modtime/testdata/foo.txt
ok mvdan.cc/nowt/test-cache-open-modtime 0.001s
-rw-r--r-- 1 mvdan mvdan 8 Jan 1 2000 test-cache-open-modtime/testdata/foo.txt
ok mvdan.cc/nowt/test-cache-open-modtime (cached)
你期望看到什么?
我希望修改时间不影响Go的测试缓存命中与未命中决策。特别是因为Go很大程度上允许其开发者不知道修改时间以便增量构建能够良好工作,与其他构建系统如make
不同。
你看到了什么?
大多数人在像GitHub Actions这样的流行平台上使用CI会导致不必要的Go测试缓存未命中,这是由于修改时间造成的。
我可以为我的项目通过跟随每个actions/checkout
步骤的脚本来修复这个问题,该脚本可以遍历所有克隆的文件并将它们的修改时间重置为静态时间戳。然而,那感觉非常不幸,我想象许多Go用户甚至可能没有意识到他们的CI比一开始就应该慢得多。
我们还可以尝试说服第三方如git clone
或actions/checkout
在文件上留下可重复的修改时间,但我不认为这是可能的。
7条答案
按热度按时间yk9xbfzb1#
如果go不跟踪外部文件的内容,这是不是必要的?
例如,如果你修改了testdata,没有modtime,它将不知道重新运行测试。
oxosxuxt2#
@mvdan 是否有可能先运行一些脚本,将相关文件的修改时间设置为该文件的最后一次提交时间?
wn9m85ua3#
see also #22593
ntjbwcob4#
你是对的,目前打开的文件不会在其内容被测试缓存跟踪。
然而,我认为这种逻辑存在一些问题,可以进行改进。例如,我们已经获取了文件信息,因此如果一个文件很小(比如小于256KiB),我们可以读取并对其内容进行哈希,而不是使用其修改时间作为缓存输入。
这将自动解决我在使用CI时遇到的几乎所有问题;我使用的大多数测试数据文件都是纯文本且非常小,如https://github.com/cue-lang/cue/blob/7d89acd5c44bca0c756f76089436ee9913818b03/cmd/cue/cmd/testdata/script/cmd_dir.txtar。Go的测试缓存不太容易出现意外的丢失,而且我不认为它会明显变慢。
你是否有可能先运行一些脚本,将相关文件的mtime设置为该文件的最后提交时间?
我可以做到这一点,但对每个项目都要这样做相当繁琐。此外,我在多年的时间里甚至没有注意到我的多个项目由于这个bug而在CI上运行缓慢。我认为Go的测试缓存默认情况下在CI系统上表现不佳是不幸的。
总的来说,编写CI脚本也存在问题。这些项目中大多数都在Linux、Mac和Windows上进行测试。即使编写一个适用于所有这些平台的POSIX Shell或Bash脚本也是相当棘手的。
0sgqnhkj5#
我们还可以尝试说服第三方,如
git clone
或actions/checkout
,在文件上保留可复现的修改时间,但我认为这不太可能发生。看起来有一个第三方工具
git-restore-mtime
(用Python编写,我认为它应该是足够便携的),它做了显而易见的事情——将mtime
设置为与每个文件的最后作者时间戳或提交时间戳相匹配。但我同意,最好不要依赖它,因为有各种各样的方法可以改变mtime
。qrjkbowd6#
我们考虑使用
git-restore-mtime
作为我们的CI,但该脚本相当慢,抵消了许多缓存的好处。我们目前正在使用this script的修改版本,该版本改进了目录处理。mum43rcc7#
我们考虑使用
git-restore-mtime
作为我们的 CI,但该脚本相当慢,抵消了许多缓存的好处。我们目前正在使用 this script 的修改版本,以改进目录处理。@oschwald,你是否有可能更新评论中的链接?我们目前正在探索在 CI/CD 中进行测试缓存的选项。