gopls/internal/lsp/cache: evaluate imports lazily in TypeCheck
Now that we do not need a static importMap for importing, and do not
need to eagerly parse or load export data, we can evaluate imported
packages lazily during type-checking, thereby avoiding importing
packages that will not be used.
This has a mild beneficial impact on benchmarks (because iimporting is
already cheap). The other direction -- avoiding invalidating packages
that are unaffected by changes -- should have a more significant impact.
For golang/go#57987
Change-Id: I894656af9ca8dea286b6be55f83c4b6bffaaf110
Reviewed-on: https://go-review.googlesource.com/c/tools/+/473166
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go
index 529940a..a717268 100644
--- a/gopls/internal/lsp/cache/check.go
+++ b/gopls/internal/lsp/cache/check.go
@@ -11,7 +11,6 @@
"go/ast"
"go/token"
"go/types"
- "log"
"regexp"
"runtime"
"sort"
@@ -64,8 +63,8 @@
// In fact, parsedFiles may be counter-productive due to pinning all files in
// memory during large operations.
parsedFiles map[span.URI]*source.ParsedGoFile // parsed files necessary for type-checking
- imports map[PackageID]pkgOrErr // types.Packages to use for importing
packages map[PackageID]*Package
+ errors map[PackageID]error // lazily allocated
}
type pkgOrErr struct {
@@ -85,14 +84,12 @@
func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Package, error) {
// Shared state for efficient type-checking.
b := &typeCheckBatch{
- fset: fileSetWithBase(reservedForParsing),
- parseCache: s.parseCache,
- cpulimit: make(chan struct{}, runtime.GOMAXPROCS(0)),
- needSyntax: make(map[PackageID]bool),
-
- parsedFiles: make(map[span.URI]*source.ParsedGoFile),
+ fset: fileSetWithBase(reservedForParsing),
+ parseCache: s.parseCache,
+ cpulimit: make(chan struct{}, runtime.GOMAXPROCS(0)),
+ needSyntax: make(map[PackageID]bool),
promises: make(map[PackageID]*memoize.Promise),
- imports: make(map[PackageID]pkgOrErr),
+ parsedFiles: make(map[span.URI]*source.ParsedGoFile),
packages: make(map[PackageID]*Package),
}
@@ -155,11 +152,15 @@
debugName := fmt.Sprintf("check(%s)", id)
b.promises[id] = memoize.NewPromise(debugName, func(ctx context.Context, _ interface{}) interface{} {
pkg, err := b.processPackage(ctx, ph)
-
- b.mu.Lock()
- b.imports[m.ID] = pkgOrErr{pkg, err}
- b.mu.Unlock()
- return nil
+ if err != nil {
+ b.mu.Lock()
+ if b.errors == nil {
+ b.errors = make(map[PackageID]error)
+ }
+ b.errors[id] = err
+ b.mu.Unlock()
+ }
+ return pkgOrErr{pkg, err}
})
return nil
}
@@ -169,16 +170,14 @@
// -- await type-checking. --
//
- // Start a single goroutine for each promise.
+ // Start a single goroutine for each syntax package.
+ //
+ // Other promises are reached recursively, and will not be evaluated if they
+ // are not needed.
{
var g errgroup.Group
- // TODO(rfindley): find a good way to limit concurrency of type-checking,
- // which is CPU bound at this point.
- //
- // (calling g.SetLimit here is mostly ineffective, as promises are
- // recursively concurrent.)
- for _, promise := range b.promises {
- promise := promise
+ for id := range b.needSyntax {
+ promise := b.promises[id]
g.Go(func() error {
_, err := promise.Get(ctx, nil)
return err
@@ -195,7 +194,7 @@
if pkgs[i] != nil {
continue
}
- if err := b.imports[id].err; err != nil {
+ if err := b.errors[id]; err != nil {
if firstErr == nil {
firstErr = err
}
@@ -258,24 +257,29 @@
// type-checking for syntax, depending on the requested syntax packages and
// available export data.
func (b *typeCheckBatch) processPackage(ctx context.Context, ph *packageHandle) (*types.Package, error) {
- if err := b.awaitPredecessors(ctx, ph.m); err != nil {
- return nil, err
- }
-
- // Wait to acquire CPU token.
+ // Type-check at most b.cpulimit syntax packages concurrently.
//
- // Note: it is important to acquire this token only after awaiting
- // predecessors, to avoid a starvation lock.
- select {
- case <-ctx.Done():
- return nil, ctx.Err()
- case b.cpulimit <- struct{}{}:
- defer func() {
- <-b.cpulimit // release CPU token
- }()
- }
-
+ // (For a non-syntax package, it is not necessary to await all
+ // predecessors because decoding export data typically only
+ // demands a small subset of all possible dependencies.)
if b.needSyntax[ph.m.ID] {
+ if err := b.awaitPredecessors(ctx, ph.m); err != nil {
+ return nil, err
+ }
+
+ // Wait to acquire CPU token.
+ //
+ // Note: it is important to acquire this token only after awaiting
+ // predecessors, to avoid a starvation lock.
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case b.cpulimit <- struct{}{}:
+ defer func() {
+ <-b.cpulimit // release CPU token
+ }()
+ }
+
// We need a syntax package.
syntaxPkg, err := b.checkPackage(ctx, ph)
if err != nil {
@@ -306,18 +310,43 @@
// importPackage loads the given package from its export data in p.exportData
// (which must already be populated).
func (b *typeCheckBatch) importPackage(ctx context.Context, m *source.Metadata, data []byte) (*types.Package, error) {
- impMap, errMap := b.importMap(m.ID)
- // Any failure to populate an import will cause confusing errors from
- // IImportShallow below.
- for path, err := range errMap {
- return nil, fmt.Errorf("error importing %q for %q: %v", path, m.ID, err)
+ impMap := b.importMap(m.ID)
+
+ var firstErr error
+ thisPackage := types.NewPackage(string(m.PkgPath), string(m.Name))
+ getPackage := func(path, name string) *types.Package {
+ if path == string(m.PkgPath) {
+ return thisPackage
+ }
+
+ promise := impMap[path]
+ v, err := promise.Get(ctx, nil)
+ if err == nil {
+ result := v.(pkgOrErr)
+ err = result.err
+ if err == nil {
+ return result.pkg
+ }
+ }
+ // inv: err != nil
+ if firstErr == nil {
+ firstErr = err
+ }
+
+ // Context cancellation, or a very bad error such as a file permission
+ // error.
+ //
+ // Returning nil here will cause the import to fail (and panic if
+ // gcimporter.debug is set), but that is preferable to the confusing errors
+ // produced when shallow import encounters an empty package.
+ return nil
}
// TODO(rfindley): collect "deep" hashes here using the provided
// callback, for precise pruning.
- imported, err := gcimporter.IImportShallow(b.fset, gcimporter.GetPackageFromMap(impMap), data, string(m.PkgPath), func(*types.Package, string) {})
+ imported, err := gcimporter.IImportShallow(b.fset, getPackage, data, string(m.PkgPath), func(*types.Package, string) {})
if err != nil {
- return nil, bug.Errorf("invalid export data for %q: %v", m.ID, err)
+ return nil, fmt.Errorf("import failed for %q: %v", m.ID, err)
}
return imported, nil
}
@@ -325,16 +354,16 @@
// checkPackageForImport type checks, but skips function bodies and does not
// record syntax information.
func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageHandle) (*types.Package, error) {
- impMap, errMap := b.importMap(ph.inputs.id)
onError := func(e error) {
// Ignore errors for exporting.
}
- cfg := b.typesConfig(ph.inputs, onError, impMap, errMap)
+ cfg := b.typesConfig(ctx, ph.inputs, onError)
+ cfg.IgnoreFuncBodies = true
+
pgfs, err := b.parseFiles(ctx, ph.inputs.compiledGoFiles)
if err != nil {
return nil, err
}
- cfg.IgnoreFuncBodies = true
pkg := types.NewPackage(string(ph.inputs.pkgPath), string(ph.inputs.name))
check := types.NewChecker(cfg, b.fset, pkg, nil)
@@ -404,32 +433,25 @@
// error if awaiting failed due to context cancellation or if there was an
// unrecoverable error loading export data.
func (b *typeCheckBatch) awaitPredecessors(ctx context.Context, m *source.Metadata) error {
+ // await predecessors concurrently, as some of them may be non-syntax
+ // packages, and therefore will not have been started by the type-checking
+ // batch.
+ var g errgroup.Group
for _, depID := range m.DepsByPkgPath {
- depID := depID
if p, ok := b.promises[depID]; ok {
- if _, err := p.Get(ctx, nil); err != nil {
+ g.Go(func() error {
+ _, err := p.Get(ctx, nil)
return err
- }
+ })
}
}
- return nil
+ return g.Wait()
}
// importMap returns an import map for the given package ID, populated with
-// type-checked packages for its dependencies. It is intended for compatibility
-// with gcimporter.IImportShallow, so the first result uses the map signature
-// of that API, where keys are package path strings.
-//
-// importMap must only be used once all promises for dependencies of id have
-// been awaited.
-//
-// For any missing packages, importMap returns an entry in the resulting errMap
-// reporting the error for that package.
-//
-// Invariant: for all recursive dependencies, either impMap[path] or
-// errMap[path] is set.
-func (b *typeCheckBatch) importMap(id PackageID) (impMap map[string]*types.Package, errMap map[PackagePath]error) {
- impMap = make(map[string]*types.Package)
+// promises keyed by package path for its dependencies.
+func (b *typeCheckBatch) importMap(id PackageID) map[string]*memoize.Promise {
+ impMap := make(map[string]*memoize.Promise)
outerID := id
var populateDepsOf func(m *source.Metadata)
populateDepsOf = func(parent *source.Metadata) {
@@ -438,35 +460,17 @@
if _, ok := impMap[string(m.PkgPath)]; ok {
continue
}
- if _, ok := errMap[m.PkgPath]; ok {
- continue
- }
- b.mu.Lock()
- result, ok := b.imports[m.ID]
- b.mu.Unlock()
+ promise, ok := b.promises[m.ID]
if !ok {
panic(fmt.Sprintf("import map for %q missing package data for %q", outerID, m.ID))
}
- // We may fail to produce a package due to e.g. context cancellation
- // (handled elsewhere), or some catastrophic failure such as a package with
- // no files.
- switch {
- case result.err != nil:
- if errMap == nil {
- errMap = make(map[PackagePath]error)
- }
- errMap[m.PkgPath] = result.err
- case result.pkg != nil:
- impMap[string(m.PkgPath)] = result.pkg
- default:
- panic("invalid import for " + id)
- }
+ impMap[string(m.PkgPath)] = promise
populateDepsOf(m)
}
}
m := b.meta.metadata[id]
populateDepsOf(m)
- return impMap, errMap
+ return impMap
}
// packageData holds binary data (e.g. types, xrefs) extracted from a syntax
@@ -862,7 +866,6 @@
var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) {
- impMap, errMap := b.importMap(inputs.id)
pkg := &syntaxPackage{
id: inputs.id,
fset: b.fset, // must match parse call below
@@ -875,7 +878,6 @@
Selections: make(map[*ast.SelectorExpr]*types.Selection),
Scopes: make(map[ast.Node]*types.Scope),
},
- importMap: impMap,
}
typeparams.InitInstanceInfo(pkg.typesInfo)
@@ -916,7 +918,7 @@
onError := func(e error) {
pkg.typeErrors = append(pkg.typeErrors, e.(types.Error))
}
- cfg := b.typesConfig(inputs, onError, impMap, errMap)
+ cfg := b.typesConfig(ctx, inputs, onError)
check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo)
@@ -933,10 +935,26 @@
if ctx.Err() != nil {
return nil, ctx.Err()
}
+
+ // Collect imports by package path for the DependencyTypes API.
+ pkg.importMap = make(map[PackagePath]*types.Package)
+ var collectDeps func(*types.Package)
+ collectDeps = func(p *types.Package) {
+ pkgPath := PackagePath(p.Path())
+ if _, ok := pkg.importMap[pkgPath]; ok {
+ return
+ }
+ pkg.importMap[pkgPath] = p
+ for _, imp := range p.Imports() {
+ collectDeps(imp)
+ }
+ }
+ collectDeps(pkg.types)
+
return pkg, nil
}
-func (b *typeCheckBatch) typesConfig(inputs typeCheckInputs, onError func(e error), impMap map[string]*types.Package, errMap map[PackagePath]error) *types.Config {
+func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config {
cfg := &types.Config{
Sizes: inputs.sizes,
Error: onError,
@@ -960,15 +978,19 @@
if !source.IsValidImport(inputs.pkgPath, depPH.m.PkgPath) {
return nil, fmt.Errorf("invalid use of internal package %q", path)
}
- pkg, ok := impMap[string(depPH.m.PkgPath)]
+ promise, ok := b.promises[id]
if !ok {
- err := errMap[depPH.m.PkgPath]
- if err == nil {
- log.Fatalf("neither pkg nor error is set")
- }
+ panic("missing import")
+ }
+ v, err := promise.Get(ctx, nil)
+ if err != nil {
+ // Context cancellation: note that this could lead to non-deterministic
+ // results in the type-checker, so it is important that we don't use
+ // any type-checking results in the case where ctx is cancelled.
return nil, err
}
- return pkg, nil
+ res := v.(pkgOrErr)
+ return res.pkg, res.err
}),
}
diff --git a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go
index 910aeeb..cb7b2dc 100644
--- a/gopls/internal/lsp/cache/pkg.go
+++ b/gopls/internal/lsp/cache/pkg.go
@@ -51,9 +51,9 @@
typeErrors []types.Error
types *types.Package
typesInfo *types.Info
- importMap map[string]*types.Package // keys are PackagePaths
- hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors
- analyses memoize.Store // maps analyzer.Name to Promise[actionResult]
+ importMap map[PackagePath]*types.Package
+ hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors
+ analyses memoize.Store // maps analyzer.Name to Promise[actionResult]
xrefs []byte
methodsets *methodsets.Index
}
@@ -129,10 +129,7 @@
// dependencies of p, or if no symbols from that package were
// referenced during the type-checking of p.
func (p *Package) DependencyTypes(path source.PackagePath) *types.Package {
- if path == p.m.PkgPath {
- return p.pkg.types
- }
- return p.pkg.importMap[string(path)]
+ return p.pkg.importMap[path]
}
func (p *Package) HasParseErrors() bool {
diff --git a/internal/gcimporter/iexport.go b/internal/gcimporter/iexport.go
index bcb715a..a0dc0b5 100644
--- a/internal/gcimporter/iexport.go
+++ b/internal/gcimporter/iexport.go
@@ -44,12 +44,12 @@
return out.Bytes(), err
}
-// IImportShallow decodes "shallow" types.Package data encoded by IExportShallow
-// in the same executable. This function cannot import data from
+// IImportShallow decodes "shallow" types.Package data encoded by
+// IExportShallow in the same executable. This function cannot import data from
// cmd/compile or gcexportdata.Write.
-func IImportShallow(fset *token.FileSet, importFunc func(path, name string) *types.Package, data []byte, path string, insert InsertType) (*types.Package, error) {
+func IImportShallow(fset *token.FileSet, getPackage GetPackageFunc, data []byte, path string, insert InsertType) (*types.Package, error) {
const bundle = false
- pkgs, err := iimportCommon(fset, importFunc, data, bundle, path, insert)
+ pkgs, err := iimportCommon(fset, getPackage, data, bundle, path, insert)
if err != nil {
return nil, err
}
diff --git a/internal/gcimporter/iimport.go b/internal/gcimporter/iimport.go
index 22698a0..be6dace 100644
--- a/internal/gcimporter/iimport.go
+++ b/internal/gcimporter/iimport.go
@@ -97,12 +97,13 @@
return iimportCommon(fset, GetPackageFromMap(imports), data, true, "", nil)
}
-// A GetPackageFunc is a function that gets the package with
-// the given path from the importer state, creating it
-// (with the specified name) if necessary.
+// A GetPackageFunc is a function that gets the package with the given path
+// from the importer state, creating it (with the specified name) if necessary.
// It is an abstraction of the map historically used to memoize package creation.
//
// Two calls with the same path must return the same package.
+//
+// If the given getPackage func returns nil, the import will fail.
type GetPackageFunc = func(path, name string) *types.Package
// GetPackageFromMap returns a GetPackageFunc that retrieves packages from the
@@ -119,10 +120,6 @@
}
}
-// iimportCommon implements the core of the import algorithm.
-//
-// The given getPackage func must always return a non-nil package
-// (see ImportFromMap for an example).
func iimportCommon(fset *token.FileSet, getPackage GetPackageFunc, data []byte, bundle bool, path string, insert InsertType) (pkgs []*types.Package, err error) {
const currentVersion = iexportVersionCurrent
version := int64(-1)