Skip to content

Commit fa55648

Browse files
committed
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 <[email protected]> Reviewed-by: Alan Donovan <[email protected]> gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]>
1 parent a164829 commit fa55648

File tree

4 files changed

+132
-116
lines changed

4 files changed

+132
-116
lines changed

gopls/internal/lsp/cache/check.go

Lines changed: 120 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"go/ast"
1212
"go/token"
1313
"go/types"
14-
"log"
1514
"regexp"
1615
"runtime"
1716
"sort"
@@ -64,8 +63,8 @@ type typeCheckBatch struct {
6463
// In fact, parsedFiles may be counter-productive due to pinning all files in
6564
// memory during large operations.
6665
parsedFiles map[span.URI]*source.ParsedGoFile // parsed files necessary for type-checking
67-
imports map[PackageID]pkgOrErr // types.Packages to use for importing
6866
packages map[PackageID]*Package
67+
errors map[PackageID]error // lazily allocated
6968
}
7069

7170
type pkgOrErr struct {
@@ -85,14 +84,12 @@ type pkgOrErr struct {
8584
func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Package, error) {
8685
// Shared state for efficient type-checking.
8786
b := &typeCheckBatch{
88-
fset: fileSetWithBase(reservedForParsing),
89-
parseCache: s.parseCache,
90-
cpulimit: make(chan struct{}, runtime.GOMAXPROCS(0)),
91-
needSyntax: make(map[PackageID]bool),
92-
93-
parsedFiles: make(map[span.URI]*source.ParsedGoFile),
87+
fset: fileSetWithBase(reservedForParsing),
88+
parseCache: s.parseCache,
89+
cpulimit: make(chan struct{}, runtime.GOMAXPROCS(0)),
90+
needSyntax: make(map[PackageID]bool),
9491
promises: make(map[PackageID]*memoize.Promise),
95-
imports: make(map[PackageID]pkgOrErr),
92+
parsedFiles: make(map[span.URI]*source.ParsedGoFile),
9693
packages: make(map[PackageID]*Package),
9794
}
9895

@@ -155,11 +152,15 @@ func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Pa
155152
debugName := fmt.Sprintf("check(%s)", id)
156153
b.promises[id] = memoize.NewPromise(debugName, func(ctx context.Context, _ interface{}) interface{} {
157154
pkg, err := b.processPackage(ctx, ph)
158-
159-
b.mu.Lock()
160-
b.imports[m.ID] = pkgOrErr{pkg, err}
161-
b.mu.Unlock()
162-
return nil
155+
if err != nil {
156+
b.mu.Lock()
157+
if b.errors == nil {
158+
b.errors = make(map[PackageID]error)
159+
}
160+
b.errors[id] = err
161+
b.mu.Unlock()
162+
}
163+
return pkgOrErr{pkg, err}
163164
})
164165
return nil
165166
}
@@ -169,16 +170,14 @@ func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Pa
169170

170171
// -- await type-checking. --
171172
//
172-
// Start a single goroutine for each promise.
173+
// Start a single goroutine for each syntax package.
174+
//
175+
// Other promises are reached recursively, and will not be evaluated if they
176+
// are not needed.
173177
{
174178
var g errgroup.Group
175-
// TODO(rfindley): find a good way to limit concurrency of type-checking,
176-
// which is CPU bound at this point.
177-
//
178-
// (calling g.SetLimit here is mostly ineffective, as promises are
179-
// recursively concurrent.)
180-
for _, promise := range b.promises {
181-
promise := promise
179+
for id := range b.needSyntax {
180+
promise := b.promises[id]
182181
g.Go(func() error {
183182
_, err := promise.Get(ctx, nil)
184183
return err
@@ -195,7 +194,7 @@ func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Pa
195194
if pkgs[i] != nil {
196195
continue
197196
}
198-
if err := b.imports[id].err; err != nil {
197+
if err := b.errors[id]; err != nil {
199198
if firstErr == nil {
200199
firstErr = err
201200
}
@@ -258,24 +257,29 @@ func (b *typeCheckBatch) parseFiles(ctx context.Context, fhs []source.FileHandle
258257
// type-checking for syntax, depending on the requested syntax packages and
259258
// available export data.
260259
func (b *typeCheckBatch) processPackage(ctx context.Context, ph *packageHandle) (*types.Package, error) {
261-
if err := b.awaitPredecessors(ctx, ph.m); err != nil {
262-
return nil, err
263-
}
264-
265-
// Wait to acquire CPU token.
260+
// Type-check at most b.cpulimit syntax packages concurrently.
266261
//
267-
// Note: it is important to acquire this token only after awaiting
268-
// predecessors, to avoid a starvation lock.
269-
select {
270-
case <-ctx.Done():
271-
return nil, ctx.Err()
272-
case b.cpulimit <- struct{}{}:
273-
defer func() {
274-
<-b.cpulimit // release CPU token
275-
}()
276-
}
277-
262+
// (For a non-syntax package, it is not necessary to await all
263+
// predecessors because decoding export data typically only
264+
// demands a small subset of all possible dependencies.)
278265
if b.needSyntax[ph.m.ID] {
266+
if err := b.awaitPredecessors(ctx, ph.m); err != nil {
267+
return nil, err
268+
}
269+
270+
// Wait to acquire CPU token.
271+
//
272+
// Note: it is important to acquire this token only after awaiting
273+
// predecessors, to avoid a starvation lock.
274+
select {
275+
case <-ctx.Done():
276+
return nil, ctx.Err()
277+
case b.cpulimit <- struct{}{}:
278+
defer func() {
279+
<-b.cpulimit // release CPU token
280+
}()
281+
}
282+
279283
// We need a syntax package.
280284
syntaxPkg, err := b.checkPackage(ctx, ph)
281285
if err != nil {
@@ -306,35 +310,60 @@ func (b *typeCheckBatch) processPackage(ctx context.Context, ph *packageHandle)
306310
// importPackage loads the given package from its export data in p.exportData
307311
// (which must already be populated).
308312
func (b *typeCheckBatch) importPackage(ctx context.Context, m *source.Metadata, data []byte) (*types.Package, error) {
309-
impMap, errMap := b.importMap(m.ID)
310-
// Any failure to populate an import will cause confusing errors from
311-
// IImportShallow below.
312-
for path, err := range errMap {
313-
return nil, fmt.Errorf("error importing %q for %q: %v", path, m.ID, err)
313+
impMap := b.importMap(m.ID)
314+
315+
var firstErr error
316+
thisPackage := types.NewPackage(string(m.PkgPath), string(m.Name))
317+
getPackage := func(path, name string) *types.Package {
318+
if path == string(m.PkgPath) {
319+
return thisPackage
320+
}
321+
322+
promise := impMap[path]
323+
v, err := promise.Get(ctx, nil)
324+
if err == nil {
325+
result := v.(pkgOrErr)
326+
err = result.err
327+
if err == nil {
328+
return result.pkg
329+
}
330+
}
331+
// inv: err != nil
332+
if firstErr == nil {
333+
firstErr = err
334+
}
335+
336+
// Context cancellation, or a very bad error such as a file permission
337+
// error.
338+
//
339+
// Returning nil here will cause the import to fail (and panic if
340+
// gcimporter.debug is set), but that is preferable to the confusing errors
341+
// produced when shallow import encounters an empty package.
342+
return nil
314343
}
315344

316345
// TODO(rfindley): collect "deep" hashes here using the provided
317346
// callback, for precise pruning.
318-
imported, err := gcimporter.IImportShallow(b.fset, gcimporter.GetPackageFromMap(impMap), data, string(m.PkgPath), func(*types.Package, string) {})
347+
imported, err := gcimporter.IImportShallow(b.fset, getPackage, data, string(m.PkgPath), func(*types.Package, string) {})
319348
if err != nil {
320-
return nil, bug.Errorf("invalid export data for %q: %v", m.ID, err)
349+
return nil, fmt.Errorf("import failed for %q: %v", m.ID, err)
321350
}
322351
return imported, nil
323352
}
324353

325354
// checkPackageForImport type checks, but skips function bodies and does not
326355
// record syntax information.
327356
func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageHandle) (*types.Package, error) {
328-
impMap, errMap := b.importMap(ph.inputs.id)
329357
onError := func(e error) {
330358
// Ignore errors for exporting.
331359
}
332-
cfg := b.typesConfig(ph.inputs, onError, impMap, errMap)
360+
cfg := b.typesConfig(ctx, ph.inputs, onError)
361+
cfg.IgnoreFuncBodies = true
362+
333363
pgfs, err := b.parseFiles(ctx, ph.inputs.compiledGoFiles)
334364
if err != nil {
335365
return nil, err
336366
}
337-
cfg.IgnoreFuncBodies = true
338367
pkg := types.NewPackage(string(ph.inputs.pkgPath), string(ph.inputs.name))
339368
check := types.NewChecker(cfg, b.fset, pkg, nil)
340369

@@ -404,32 +433,25 @@ func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (*
404433
// error if awaiting failed due to context cancellation or if there was an
405434
// unrecoverable error loading export data.
406435
func (b *typeCheckBatch) awaitPredecessors(ctx context.Context, m *source.Metadata) error {
436+
// await predecessors concurrently, as some of them may be non-syntax
437+
// packages, and therefore will not have been started by the type-checking
438+
// batch.
439+
var g errgroup.Group
407440
for _, depID := range m.DepsByPkgPath {
408-
depID := depID
409441
if p, ok := b.promises[depID]; ok {
410-
if _, err := p.Get(ctx, nil); err != nil {
442+
g.Go(func() error {
443+
_, err := p.Get(ctx, nil)
411444
return err
412-
}
445+
})
413446
}
414447
}
415-
return nil
448+
return g.Wait()
416449
}
417450

418451
// importMap returns an import map for the given package ID, populated with
419-
// type-checked packages for its dependencies. It is intended for compatibility
420-
// with gcimporter.IImportShallow, so the first result uses the map signature
421-
// of that API, where keys are package path strings.
422-
//
423-
// importMap must only be used once all promises for dependencies of id have
424-
// been awaited.
425-
//
426-
// For any missing packages, importMap returns an entry in the resulting errMap
427-
// reporting the error for that package.
428-
//
429-
// Invariant: for all recursive dependencies, either impMap[path] or
430-
// errMap[path] is set.
431-
func (b *typeCheckBatch) importMap(id PackageID) (impMap map[string]*types.Package, errMap map[PackagePath]error) {
432-
impMap = make(map[string]*types.Package)
452+
// promises keyed by package path for its dependencies.
453+
func (b *typeCheckBatch) importMap(id PackageID) map[string]*memoize.Promise {
454+
impMap := make(map[string]*memoize.Promise)
433455
outerID := id
434456
var populateDepsOf func(m *source.Metadata)
435457
populateDepsOf = func(parent *source.Metadata) {
@@ -438,35 +460,17 @@ func (b *typeCheckBatch) importMap(id PackageID) (impMap map[string]*types.Packa
438460
if _, ok := impMap[string(m.PkgPath)]; ok {
439461
continue
440462
}
441-
if _, ok := errMap[m.PkgPath]; ok {
442-
continue
443-
}
444-
b.mu.Lock()
445-
result, ok := b.imports[m.ID]
446-
b.mu.Unlock()
463+
promise, ok := b.promises[m.ID]
447464
if !ok {
448465
panic(fmt.Sprintf("import map for %q missing package data for %q", outerID, m.ID))
449466
}
450-
// We may fail to produce a package due to e.g. context cancellation
451-
// (handled elsewhere), or some catastrophic failure such as a package with
452-
// no files.
453-
switch {
454-
case result.err != nil:
455-
if errMap == nil {
456-
errMap = make(map[PackagePath]error)
457-
}
458-
errMap[m.PkgPath] = result.err
459-
case result.pkg != nil:
460-
impMap[string(m.PkgPath)] = result.pkg
461-
default:
462-
panic("invalid import for " + id)
463-
}
467+
impMap[string(m.PkgPath)] = promise
464468
populateDepsOf(m)
465469
}
466470
}
467471
m := b.meta.metadata[id]
468472
populateDepsOf(m)
469-
return impMap, errMap
473+
return impMap
470474
}
471475

472476
// packageData holds binary data (e.g. types, xrefs) extracted from a syntax
@@ -862,7 +866,6 @@ func typeCheckImpl(ctx context.Context, b *typeCheckBatch, inputs typeCheckInput
862866
var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
863867

864868
func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) {
865-
impMap, errMap := b.importMap(inputs.id)
866869
pkg := &syntaxPackage{
867870
id: inputs.id,
868871
fset: b.fset, // must match parse call below
@@ -875,7 +878,6 @@ func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs)
875878
Selections: make(map[*ast.SelectorExpr]*types.Selection),
876879
Scopes: make(map[ast.Node]*types.Scope),
877880
},
878-
importMap: impMap,
879881
}
880882
typeparams.InitInstanceInfo(pkg.typesInfo)
881883

@@ -916,7 +918,7 @@ func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs)
916918
onError := func(e error) {
917919
pkg.typeErrors = append(pkg.typeErrors, e.(types.Error))
918920
}
919-
cfg := b.typesConfig(inputs, onError, impMap, errMap)
921+
cfg := b.typesConfig(ctx, inputs, onError)
920922

921923
check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo)
922924

@@ -933,10 +935,26 @@ func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs)
933935
if ctx.Err() != nil {
934936
return nil, ctx.Err()
935937
}
938+
939+
// Collect imports by package path for the DependencyTypes API.
940+
pkg.importMap = make(map[PackagePath]*types.Package)
941+
var collectDeps func(*types.Package)
942+
collectDeps = func(p *types.Package) {
943+
pkgPath := PackagePath(p.Path())
944+
if _, ok := pkg.importMap[pkgPath]; ok {
945+
return
946+
}
947+
pkg.importMap[pkgPath] = p
948+
for _, imp := range p.Imports() {
949+
collectDeps(imp)
950+
}
951+
}
952+
collectDeps(pkg.types)
953+
936954
return pkg, nil
937955
}
938956

939-
func (b *typeCheckBatch) typesConfig(inputs typeCheckInputs, onError func(e error), impMap map[string]*types.Package, errMap map[PackagePath]error) *types.Config {
957+
func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config {
940958
cfg := &types.Config{
941959
Sizes: inputs.sizes,
942960
Error: onError,
@@ -960,15 +978,19 @@ func (b *typeCheckBatch) typesConfig(inputs typeCheckInputs, onError func(e erro
960978
if !source.IsValidImport(inputs.pkgPath, depPH.m.PkgPath) {
961979
return nil, fmt.Errorf("invalid use of internal package %q", path)
962980
}
963-
pkg, ok := impMap[string(depPH.m.PkgPath)]
981+
promise, ok := b.promises[id]
964982
if !ok {
965-
err := errMap[depPH.m.PkgPath]
966-
if err == nil {
967-
log.Fatalf("neither pkg nor error is set")
968-
}
983+
panic("missing import")
984+
}
985+
v, err := promise.Get(ctx, nil)
986+
if err != nil {
987+
// Context cancellation: note that this could lead to non-deterministic
988+
// results in the type-checker, so it is important that we don't use
989+
// any type-checking results in the case where ctx is cancelled.
969990
return nil, err
970991
}
971-
return pkg, nil
992+
res := v.(pkgOrErr)
993+
return res.pkg, res.err
972994
}),
973995
}
974996

gopls/internal/lsp/cache/pkg.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ type syntaxPackage struct {
5151
typeErrors []types.Error
5252
types *types.Package
5353
typesInfo *types.Info
54-
importMap map[string]*types.Package // keys are PackagePaths
55-
hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors
56-
analyses memoize.Store // maps analyzer.Name to Promise[actionResult]
54+
importMap map[PackagePath]*types.Package
55+
hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors
56+
analyses memoize.Store // maps analyzer.Name to Promise[actionResult]
5757
xrefs []byte
5858
methodsets *methodsets.Index
5959
}
@@ -129,10 +129,7 @@ func (p *Package) GetTypesInfo() *types.Info {
129129
// dependencies of p, or if no symbols from that package were
130130
// referenced during the type-checking of p.
131131
func (p *Package) DependencyTypes(path source.PackagePath) *types.Package {
132-
if path == p.m.PkgPath {
133-
return p.pkg.types
134-
}
135-
return p.pkg.importMap[string(path)]
132+
return p.pkg.importMap[path]
136133
}
137134

138135
func (p *Package) HasParseErrors() bool {

0 commit comments

Comments
 (0)