vgo cache

Problem

vgo 目前还在开发中,相比2个月前,vgo已经从一个原型开始慢慢成熟了,但是仍然还有许多问题需要解决。

这个问题是社区用户反馈的,vgo在fetch依赖的时候并不是并发执行的,所以很耗时间。rsc 于是在vgo 中添加了 par 包,它可以按指定数量并发执行某一项特定工作。同时为了减少网络和硬盘开销,rsc 为 vgo 添加了 cache 功能。

cache

不得不说 cache 的增加大大增加了 vgo model fetch 模块的复杂度,同时引入新的bug也是在所难免的,但是cache所带来的收益也是显而易见的,就像现在 go build 和 go test 中大家使用的cache一样,你感受不到它的存在,但是没有它,你会很难受。

type Repo 是外部操作一个远程代码库的接口,具体方法如下:


type Repo interface {
	// ModulePath returns the module path.
	ModulePath() string

	// Versions lists all known versions with the given prefix.
	// Pseudo-versions are not included.
	// Versions should be returned sorted in semver order
	// (implementations can use SortVersions).
	Versions(prefix string) (tags []string, err error)

	// Stat returns information about the revision rev.
	// A revision can be any identifier known to the underlying service:
	// commit hash, branch, tag, and so on.
	Stat(rev string) (*RevInfo, error)

	// Latest returns the latest revision on the default branch,
	// whatever that means in the underlying source code repository.
	// It is only used when there are no tagged versions.
	Latest() (*RevInfo, error)

	// GoMod returns the go.mod file for the given version.
	GoMod(version string) (data []byte, err error)

	// Zip downloads a zip file for the given version
	// to a new file in a given temporary directory.
	// It returns the name of the new file.
	// The caller should remove the file when finished with it.
	Zip(version, tmpdir string) (tmpfile string, err error)
}

当然 cache 也要实现这些接口,以方便外部无缝接入。


type cachingRepo struct {
	path  string
	cache par.Cache // cache for all operations
	r     Repo
}

cache 结构体为每一个repo准备了一个 par.cache 用于cache相应的数据。当然还要有codeHost返回的 Repo,用于没有命中cache的情况下进行相应的操作。我们看看 par.cache 的实现。


// Cache runs an action once per key and caches the result.
type Cache struct {
	m sync.Map
}

// Do calls the function f if and only if Do is being called for the first time with this key.
// No call to Do with a given key returns until the one call to f returns.
// Do returns the value returned by the one call to f.
func (c *Cache) Do(key interface{}, f func() interface{}) interface{} {
	type entry struct {
		once   sync.Once
		result interface{}
	}

	entryIface, ok := c.m.Load(key)
	if !ok {
		entryIface, _ = c.m.LoadOrStore(key, new(entry))
	}
	e := entryIface.(*entry)

	e.once.Do(func() {
		e.result = f()
	})
	return e.result
}

非常精简的设计,令人发指。为了避免并发带来的问题,直接使用 sync.Map 存储 entry。entry存储了两组信息:1.这个key的操作是否执行过,用 sync.Once来实现 2. 如果执行过了,结果存储在result中。简单举个例子,在一次 vgo build 中你可能会下载很多依赖,如果有个依赖被不同的依赖包引用过多次,那么它只会被下载一次,后面直接复用,另外,vgo会查找这个代码库的tag列表,用于版本管理,这个fetch tag列表的操作也会只进行一次。

在代码中合适的使用sync.Onec,会大大精简你的代码,提高你程序的性能。

References: