Skip to content

Commit 4810eda

Browse files
committed
gopls/internal/lsp/cache: memoize active packages after every operation
We were memoizing active packages only after a call to TypeCheck. This means that we may duplicate work if diagnostics, references, or implements requests execute before the first call to TypeCheck for an open package. Fix this by pushing the memoization logic down into forEachPackage. This uncovered a bug in go/types that may have already been affecting users: golang/go#61561. This bug is consistent with user reports on slack of strange errors related to missing methods. Avoid this bug in most cases by ensuring that all instantiated interfaces are completed immediately after type checking. However, this doesn't completely avoid the bug as it may not complete parameterized interface literals elsewhere in the type definition. For example, the following definition is still susceptible to the bug: type T[P any] interface { m() interface{ n(P) } } For golang/go#60926 Fixes golang/go#61005 Change-Id: I324cd13bac7c17b1eb5642973157bdbfb2368650 Reviewed-on: https://go-review.googlesource.com/c/tools/+/512636 Run-TryBot: Robert Findley <rfindley@google.com> Reviewed-by: Alan Donovan <adonovan@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> gopls-CI: kokoro <noreply+kokoro@google.com>
1 parent fa093b2 commit 4810eda

File tree

2 files changed

+47
-52
lines changed

2 files changed

+47
-52
lines changed

gopls/internal/lsp/cache/check.go

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ type unit = struct{}
5050
// It shares state such as parsed files and imports, to optimize type-checking
5151
// for packages with overlapping dependency graphs.
5252
type typeCheckBatch struct {
53+
activePackageCache interface {
54+
getActivePackage(id PackageID) *Package
55+
setActivePackage(id PackageID, pkg *Package)
56+
}
5357
syntaxIndex map[PackageID]int // requested ID -> index in ids
5458
pre preTypeCheck
5559
post postTypeCheck
@@ -89,39 +93,10 @@ type pkgOrErr struct {
8993
// the type-checking operation.
9094
func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Package, error) {
9195
pkgs := make([]source.Package, len(ids))
92-
93-
var (
94-
needIDs []PackageID // ids to type-check
95-
indexes []int // original index of requested ids
96-
)
97-
98-
// Check for existing active packages.
99-
//
100-
// Since gopls can't depend on package identity, any instance of the
101-
// requested package must be ok to return.
102-
//
103-
// This is an optimization to avoid redundant type-checking: following
104-
// changes to an open package many LSP clients send several successive
105-
// requests for package information for the modified package (semantic
106-
// tokens, code lens, inlay hints, etc.)
107-
for i, id := range ids {
108-
if pkg := s.getActivePackage(id); pkg != nil {
109-
pkgs[i] = pkg
110-
} else {
111-
needIDs = append(needIDs, id)
112-
indexes = append(indexes, i)
113-
}
114-
}
115-
11696
post := func(i int, pkg *Package) {
117-
if alt := s.memoizeActivePackage(pkg.ph.m.ID, pkg); alt != nil && alt != pkg {
118-
// pkg is an open package, but we've lost a race and an existing package
119-
// has already been memoized.
120-
pkg = alt
121-
}
122-
pkgs[indexes[i]] = pkg
97+
pkgs[i] = pkg
12398
}
124-
return pkgs, s.forEachPackage(ctx, needIDs, nil, post)
99+
return pkgs, s.forEachPackage(ctx, ids, nil, post)
125100
}
126101

127102
// getImportGraph returns a shared import graph use for this snapshot, or nil.
@@ -355,15 +330,16 @@ func (s *snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preT
355330
// If a non-nil importGraph is provided, imports in this graph will be reused.
356331
func (s *snapshot) forEachPackageInternal(ctx context.Context, importGraph *importGraph, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) (*typeCheckBatch, error) {
357332
b := &typeCheckBatch{
358-
parseCache: s.view.parseCache,
359-
pre: pre,
360-
post: post,
361-
handles: handles,
362-
fset: fileSetWithBase(reservedForParsing),
363-
syntaxIndex: make(map[PackageID]int),
364-
cpulimit: make(chan unit, runtime.GOMAXPROCS(0)),
365-
syntaxPackages: make(map[PackageID]*futurePackage),
366-
importPackages: make(map[PackageID]*futurePackage),
333+
activePackageCache: s,
334+
pre: pre,
335+
post: post,
336+
handles: handles,
337+
parseCache: s.view.parseCache,
338+
fset: fileSetWithBase(reservedForParsing),
339+
syntaxIndex: make(map[PackageID]int),
340+
cpulimit: make(chan unit, runtime.GOMAXPROCS(0)),
341+
syntaxPackages: make(map[PackageID]*futurePackage),
342+
importPackages: make(map[PackageID]*futurePackage),
367343
}
368344

369345
if importGraph != nil {
@@ -500,6 +476,20 @@ func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id Pack
500476
return nil, nil // skip: export data only
501477
}
502478

479+
// Check for existing active packages.
480+
//
481+
// Since gopls can't depend on package identity, any instance of the
482+
// requested package must be ok to return.
483+
//
484+
// This is an optimization to avoid redundant type-checking: following
485+
// changes to an open package many LSP clients send several successive
486+
// requests for package information for the modified package (semantic
487+
// tokens, code lens, inlay hints, etc.)
488+
if pkg := b.activePackageCache.getActivePackage(id); ok {
489+
b.post(i, pkg)
490+
return nil, nil // skip: not checked in this batch
491+
}
492+
503493
if err := b.awaitPredecessors(ctx, ph.m); err != nil {
504494
// One failed precessesor should not fail the entire type checking
505495
// operation. Errors related to imports will be reported as type checking
@@ -527,7 +517,9 @@ func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id Pack
527517
if err != nil {
528518
return nil, err
529519
}
520+
b.activePackageCache.setActivePackage(id, syntaxPkg)
530521
b.post(i, syntaxPkg)
522+
531523
return syntaxPkg.pkg.types, nil
532524
}
533525

@@ -1465,6 +1457,14 @@ func typeCheckImpl(ctx context.Context, b *typeCheckBatch, inputs typeCheckInput
14651457
}
14661458
}
14671459

1460+
// Work around golang/go#61561: interface instances aren't concurrency-safe
1461+
// as they are not completed by the type checker.
1462+
for _, inst := range typeparams.GetInstances(pkg.typesInfo) {
1463+
if iface, _ := inst.Type.Underlying().(*types.Interface); iface != nil {
1464+
iface.Complete()
1465+
}
1466+
}
1467+
14681468
return pkg, nil
14691469
}
14701470

gopls/internal/lsp/cache/snapshot.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -864,26 +864,21 @@ func (s *snapshot) getActivePackage(id PackageID) *Package {
864864
return nil
865865
}
866866

867-
// memoizeActivePackage checks if pkg is active, and if so either records it in
867+
// setActivePackage checks if pkg is active, and if so either records it in
868868
// the active packages map or returns the existing memoized active package for id.
869-
//
870-
// The resulting package is non-nil if and only if the specified package is open.
871-
func (s *snapshot) memoizeActivePackage(id PackageID, pkg *Package) (active *Package) {
869+
func (s *snapshot) setActivePackage(id PackageID, pkg *Package) {
872870
s.mu.Lock()
873871
defer s.mu.Unlock()
874872

875-
if value, ok := s.activePackages.Get(id); ok {
876-
return value.(*Package) // possibly nil, if we have already checked this id.
873+
if _, ok := s.activePackages.Get(id); ok {
874+
return // already memoized
877875
}
878876

879-
defer func() {
880-
s.activePackages.Set(id, active, nil) // store the result either way: remember that pkg is not open
881-
}()
882-
883877
if containsOpenFileLocked(s, pkg.Metadata()) {
884-
return pkg
878+
s.activePackages.Set(id, pkg, nil)
879+
} else {
880+
s.activePackages.Set(id, (*Package)(nil), nil) // remember that pkg is not open
885881
}
886-
return nil
887882
}
888883

889884
func (s *snapshot) resetActivePackagesLocked() {

0 commit comments

Comments
 (0)