go/loader: reduce contention for findPackage mutex
...by making the cache non-blocking and duplicate-suppressing.
(The bottleneck was introduced while adding vendoring support.)
This reduces the 95th percentile map shard time for type-checking 120K
packages to 8min from 20min.
Also: move the I/O-limiting counting semaphore and the NoGoError check
from FindPackage to findPackage so that all implementations benefit.
Change-Id: I43527122262cf80475dd3212d78c340e1c71e36c
Reviewed-on: https://go-review.googlesource.com/18580
Reviewed-by: Robert Griesemer <gri@golang.org>
diff --git a/go/loader/loader.go b/go/loader/loader.go
index 19e2bbd..b0f8336 100644
--- a/go/loader/loader.go
+++ b/go/loader/loader.go
@@ -100,10 +100,10 @@
// FindPackage is called during Load to create the build.Package
// for a given import path from a given directory.
- // If FindPackage is nil, a default implementation
- // based on ctxt.Import is used. A client may use this hook to
- // adapt to a proprietary build system that does not follow the
- // "go build" layout conventions, for example.
+ // If FindPackage is nil, (*build.Context).Import is used.
+ // A client may use this hook to adapt to a proprietary build
+ // system that does not follow the "go build" layout
+ // conventions, for example.
//
// It must be safe to call concurrently from multiple goroutines.
FindPackage func(ctxt *build.Context, fromDir, importPath string, mode build.ImportMode) (*build.Package, error)
@@ -382,7 +382,7 @@
// findpkg is a memoization of FindPackage.
findpkgMu sync.Mutex // guards findpkg
- findpkg map[findpkgKey]findpkgValue
+ findpkg map[findpkgKey]*findpkgValue
importedMu sync.Mutex // guards imported
imported map[string]*importInfo // all imported packages (incl. failures) by import path
@@ -403,8 +403,9 @@
}
type findpkgValue struct {
- bp *build.Package
- err error
+ ready chan struct{} // closed to broadcast readiness
+ bp *build.Package
+ err error
}
// importInfo tracks the success or failure of a single import.
@@ -470,15 +471,7 @@
// Install default FindPackage hook using go/build logic.
if conf.FindPackage == nil {
- conf.FindPackage = func(ctxt *build.Context, path, fromDir string, mode build.ImportMode) (*build.Package, error) {
- ioLimit <- true
- bp, err := ctxt.Import(path, fromDir, mode)
- <-ioLimit
- if _, ok := err.(*build.NoGoError); ok {
- return bp, nil // empty directory is not an error
- }
- return bp, err
- }
+ conf.FindPackage = (*build.Context).Import
}
prog := &Program{
@@ -491,7 +484,7 @@
imp := importer{
conf: conf,
prog: prog,
- findpkg: make(map[findpkgKey]findpkgValue),
+ findpkg: make(map[findpkgKey]*findpkgValue),
imported: make(map[string]*importInfo),
start: time.Now(),
graph: make(map[string]map[string]bool),
@@ -803,16 +796,32 @@
// findPackage locates the package denoted by the importPath in the
// specified directory.
func (imp *importer) findPackage(importPath, fromDir string, mode build.ImportMode) (*build.Package, error) {
- // TODO(adonovan): opt: non-blocking duplicate-suppressing cache.
- // i.e. don't hold the lock around FindPackage.
+ // We use a non-blocking duplicate-suppressing cache (gopl.io §9.7)
+ // to avoid holding the lock around FindPackage.
key := findpkgKey{importPath, fromDir, mode}
imp.findpkgMu.Lock()
- defer imp.findpkgMu.Unlock()
v, ok := imp.findpkg[key]
- if !ok {
- bp, err := imp.conf.FindPackage(imp.conf.build(), importPath, fromDir, mode)
- v = findpkgValue{bp, err}
+ if ok {
+ // cache hit
+ imp.findpkgMu.Unlock()
+
+ <-v.ready // wait for entry to become ready
+ } else {
+ // Cache miss: this goroutine becomes responsible for
+ // populating the map entry and broadcasting its readiness.
+ v = &findpkgValue{ready: make(chan struct{})}
imp.findpkg[key] = v
+ imp.findpkgMu.Unlock()
+
+ ioLimit <- true
+ v.bp, v.err = imp.conf.FindPackage(imp.conf.build(), importPath, fromDir, mode)
+ <-ioLimit
+
+ if _, ok := v.err.(*build.NoGoError); ok {
+ v.err = nil // empty directory is not an error
+ }
+
+ close(v.ready) // broadcast ready condition
}
return v.bp, v.err
}