Skip to content

Commit b71392a

Browse files
committed
gopls/internal/lsp/cache: reduce importing in analysis
This CL is a substantial reorganization of the analysis driver to ensure that export data is imported at most once per batch of packages that are analyzed, instead of once per import edge. This greatly reduces the amount of allocation and computation done during analysis. In cache/analysis.go, Snapshot.Analyze (which now takes a set of PackageIDs, instead of being called singly in a loop) constructs an ephemeral DAG that mirrors the package graph, and then works in parallel postorder over this graph doing analysis. It uses a single FileSet for the whole batch of packages it creates. The subgraph rooted at each node is effectively a types.Importer for that node, as it represents the mapping from PackagePath to *types.Package. We no longer bother with promises or invalidation. We rely on the fact that the graph is relatively cheap to construct, cache hits are cheap to process, and the whole process only occurs after an idle delay of about a second. Also: - In internal/facts, optimize the fact decoder by using a callback. Previously, it was spending a lot of time traversing the API of all imports of a package to build a PackagePath-to-types.Package mapping. For many packages in terraform-provider-aws this visits over 1M objects (!!). But of course this is trivially computed from the new representation. - In internal/gcimporter, IImportShallow now uses a single callback to get all the types.Package symbols from the client, potentially in parallel (and that's what gopls does). The previous separation of "create" and "populate" has gone away. The analysis driver additionally exploits the getPackages callback to efficiently read the package manifest of an export data file, then abort with an error before proceeding to actually decode the rest of the file. With this change, we can process the internal/provider package of the terraform-provider-aws repo in 20s cold, 4s hot. (Before, it would run out of memory.) $ go test -bench=InitialWorkspaceLoad/hashiform ./gopls/internal/regtest/bench BenchmarkInitialWorkspaceLoad/hashiform-8 1 4014521793 ns/op 349570384 alloc_bytes 439230464 in_use_bytes 668992216 total_alloc_bytes PASS Fixes golang/go#60621 Change-Id: Iadeb02f57eb19dcccb639857053b897a60e0a90e Reviewed-on: https://go-review.googlesource.com/c/tools/+/503195 Reviewed-by: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Alan Donovan <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent d1a388b commit b71392a

File tree

19 files changed

+652
-535
lines changed

19 files changed

+652
-535
lines changed

gopls/internal/lsp/cache/analysis.go

Lines changed: 435 additions & 356 deletions
Large diffs are not rendered by default.

gopls/internal/lsp/cache/check.go

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -531,29 +531,19 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, m *source.Metadata,
531531

532532
impMap := b.importMap(m.ID)
533533

534-
var firstErr error // TODO(rfindley): unused: revisit or remove.
535534
thisPackage := types.NewPackage(string(m.PkgPath), string(m.Name))
536-
getPackage := func(path, name string) *types.Package {
537-
if path == string(m.PkgPath) {
538-
return thisPackage
539-
}
540-
541-
id := impMap[path]
542-
imp, err := b.getImportPackage(ctx, id)
543-
if err == nil {
544-
return imp
545-
}
546-
// inv: err != nil
547-
if firstErr == nil {
548-
firstErr = err
535+
getPackages := func(items []gcimporter.GetPackagesItem) error {
536+
for i, item := range items {
537+
if item.Path == string(m.PkgPath) {
538+
items[i].Pkg = thisPackage
539+
} else {
540+
pkg, err := b.getImportPackage(ctx, impMap[item.Path])
541+
if err != nil {
542+
return err
543+
}
544+
items[i].Pkg = pkg
545+
}
549546
}
550-
551-
// Context cancellation, or a very bad error such as a file permission
552-
// error.
553-
//
554-
// Returning nil here will cause the import to fail (and panic if
555-
// gcimporter.debug is set), but that is preferable to the confusing errors
556-
// produced when shallow import encounters an empty package.
557547
return nil
558548
}
559549

@@ -563,9 +553,9 @@ func (b *typeCheckBatch) importPackage(ctx context.Context, m *source.Metadata,
563553
return nil, ctx.Err()
564554
}
565555

566-
// TODO(rfindley): collect "deep" hashes here using the provided
556+
// TODO(rfindley): collect "deep" hashes here using the getPackages
567557
// callback, for precise pruning.
568-
imported, err := gcimporter.IImportShallow(b.fset, getPackage, data, string(m.PkgPath), func(*types.Package, string) {})
558+
imported, err := gcimporter.IImportShallow(b.fset, getPackages, data, string(m.PkgPath))
569559
if err != nil {
570560
return nil, fmt.Errorf("import failed for %q: %v", m.ID, err)
571561
}

gopls/internal/lsp/cache/maps.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
package cache
66

77
import (
8-
"strings"
9-
108
"golang.org/x/tools/gopls/internal/lsp/source"
119
"golang.org/x/tools/gopls/internal/span"
1210
"golang.org/x/tools/internal/persistent"
@@ -136,13 +134,3 @@ func (s knownDirsSet) Insert(key span.URI) {
136134
func (s knownDirsSet) Remove(key span.URI) {
137135
s.impl.Delete(key)
138136
}
139-
140-
// analysisKeyLessInterface is the less-than relation for analysisKey
141-
// values wrapped in an interface.
142-
func analysisKeyLessInterface(a, b interface{}) bool {
143-
x, y := a.(analysisKey), b.(analysisKey)
144-
if cmp := strings.Compare(x.analyzerNames, y.analyzerNames); cmp != 0 {
145-
return cmp < 0
146-
}
147-
return x.pkgid < y.pkgid
148-
}

gopls/internal/lsp/cache/session.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
171171
parseCache: new(parseCache),
172172
activePackages: persistent.NewMap(packageIDLessInterface),
173173
symbolizeHandles: persistent.NewMap(uriLessInterface),
174-
analyses: persistent.NewMap(analysisKeyLessInterface),
175174
workspacePackages: make(map[PackageID]PackagePath),
176175
unloadableFiles: make(map[span.URI]struct{}),
177176
parseModHandles: persistent.NewMap(uriLessInterface),

gopls/internal/lsp/cache/snapshot.go

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,6 @@ type snapshot struct {
122122
// IDs not contained in the map are not known to be open or not open.
123123
activePackages *persistent.Map // from packageID to *Package
124124

125-
// analyses maps an analysisKey (which identifies a package
126-
// and a set of analyzers) to the handle for the future result
127-
// of loading the package and analyzing it.
128-
analyses *persistent.Map // from analysisKey to analysisPromise
129-
130125
// workspacePackages contains the workspace's packages, which are loaded
131126
// when the view is created. It contains no intermediate test variants.
132127
workspacePackages map[PackageID]PackagePath
@@ -268,7 +263,6 @@ func (s *snapshot) destroy(destroyedBy string) {
268263

269264
s.packages.Destroy()
270265
s.activePackages.Destroy()
271-
s.analyses.Destroy()
272266
s.files.Destroy()
273267
s.knownSubdirs.Destroy()
274268
s.symbolizeHandles.Destroy()
@@ -2014,7 +2008,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
20142008
initializedErr: s.initializedErr,
20152009
packages: s.packages.Clone(),
20162010
activePackages: s.activePackages.Clone(),
2017-
analyses: s.analyses.Clone(),
20182011
files: s.files.Clone(),
20192012
parseCache: s.parseCache,
20202013
symbolizeHandles: s.symbolizeHandles.Clone(),
@@ -2242,18 +2235,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
22422235
result.activePackages.Delete(id)
22432236
}
22442237

2245-
// Delete invalidated analysis actions.
2246-
var actionsToDelete []analysisKey
2247-
result.analyses.Range(func(k, _ interface{}) {
2248-
key := k.(analysisKey)
2249-
if _, ok := idsToInvalidate[key.pkgid]; ok {
2250-
actionsToDelete = append(actionsToDelete, key)
2251-
}
2252-
})
2253-
for _, key := range actionsToDelete {
2254-
result.analyses.Delete(key)
2255-
}
2256-
22572238
// If a file has been deleted, we must delete metadata for all packages
22582239
// containing that file.
22592240
//

gopls/internal/lsp/code_action.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionPara
203203
if err != nil {
204204
return nil, err
205205
}
206-
analysisDiags, err := source.Analyze(ctx, snapshot, pkg.Metadata().ID, true)
206+
analysisDiags, err := source.Analyze(ctx, snapshot, map[source.PackageID]unit{pkg.Metadata().ID: {}}, true)
207207
if err != nil {
208208
return nil, err
209209
}
@@ -575,3 +575,5 @@ func goTest(ctx context.Context, snapshot source.Snapshot, uri span.URI, rng pro
575575
Command: &cmd,
576576
}}, nil
577577
}
578+
579+
type unit = struct{}

gopls/internal/lsp/diagnostics.go

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"os"
1313
"path/filepath"
14+
"sort"
1415
"strings"
1516
"sync"
1617
"time"
@@ -362,7 +363,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze
362363
var (
363364
seen = map[span.URI]struct{}{}
364365
toDiagnose = make(map[source.PackageID]*source.Metadata)
365-
toAnalyze = make(map[source.PackageID]*source.Metadata)
366+
toAnalyze = make(map[source.PackageID]unit)
366367
)
367368
for _, m := range workspace {
368369
var hasNonIgnored, hasOpenFile bool
@@ -378,7 +379,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze
378379
if hasNonIgnored {
379380
toDiagnose[m.ID] = m
380381
if analyze == analyzeEverything || analyze == analyzeOpenPackages && hasOpenFile {
381-
toAnalyze[m.ID] = m
382+
toAnalyze[m.ID] = unit{}
382383
}
383384
}
384385
}
@@ -416,7 +417,7 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze
416417
// of concurrent dispatch: as of writing we concurrently run TidyDiagnostics
417418
// and diagnosePkgs, and diagnosePkgs concurrently runs PackageDiagnostics and
418419
// analysis.
419-
func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toDiagnose, toAnalyze map[source.PackageID]*source.Metadata) {
420+
func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toDiagnose map[source.PackageID]*source.Metadata, toAnalyze map[source.PackageID]unit) {
420421
ctx, done := event.Start(ctx, "Server.diagnosePkgs", source.SnapshotLabels(snapshot)...)
421422
defer done()
422423

@@ -425,7 +426,6 @@ func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toD
425426
var (
426427
wg sync.WaitGroup
427428
pkgDiags map[span.URI][]*source.Diagnostic
428-
analysisMu sync.Mutex
429429
analysisDiags = make(map[span.URI][]*source.Diagnostic)
430430
)
431431

@@ -446,28 +446,30 @@ func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toD
446446

447447
// Get diagnostics from analysis framework.
448448
// This includes type-error analyzers, which suggest fixes to compiler errors.
449-
//
450-
// TODO(adonovan): in many cases we will be analyze multiple open variants of
451-
// an open package, which have significantly overlapping import graphs.
452-
// It may make sense to change the Analyze API to accept a slice of IDs, or
453-
// merge analysis with type-checking.
454-
for _, m := range toAnalyze {
455-
m := m
456-
wg.Add(1)
457-
go func() {
458-
defer wg.Done()
459-
diags, err := source.Analyze(ctx, snapshot, m.ID, false)
460-
if err != nil {
461-
event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...)
462-
return
463-
}
464-
analysisMu.Lock()
465-
for uri, diags := range diags {
466-
analysisDiags[uri] = append(analysisDiags[uri], diags...)
449+
wg.Add(1)
450+
go func() {
451+
defer wg.Done()
452+
diags, err := source.Analyze(ctx, snapshot, toAnalyze, false)
453+
if err != nil {
454+
var tagStr string // sorted comma-separated list of package IDs
455+
{
456+
// TODO(adonovan): replace with a generic map[S]any -> string
457+
// function in the tag package, and use maps.Keys + slices.Sort.
458+
keys := make([]string, 0, len(toDiagnose))
459+
for id := range toDiagnose {
460+
keys = append(keys, string(id))
461+
}
462+
sort.Strings(keys)
463+
tagStr = strings.Join(keys, ",")
467464
}
468-
analysisMu.Unlock()
469-
}()
470-
}
465+
event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(tagStr))...)
466+
return
467+
}
468+
for uri, diags := range diags {
469+
analysisDiags[uri] = append(analysisDiags[uri], diags...)
470+
}
471+
}()
472+
471473
wg.Wait()
472474

473475
// TODO(rfindley): remove the guards against snapshot.IsBuiltin, after the

gopls/internal/lsp/source/diagnostics.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type SuggestedFix struct {
2121
}
2222

2323
// Analyze reports go/analysis-framework diagnostics in the specified package.
24-
func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeConvenience bool) (map[span.URI][]*Diagnostic, error) {
24+
func Analyze(ctx context.Context, snapshot Snapshot, pkgIDs map[PackageID]unit, includeConvenience bool) (map[span.URI][]*Diagnostic, error) {
2525
// Exit early if the context has been canceled. This also protects us
2626
// from a race on Options, see golang/go#36699.
2727
if ctx.Err() != nil {
@@ -45,7 +45,7 @@ func Analyze(ctx context.Context, snapshot Snapshot, pkgid PackageID, includeCon
4545
}
4646
}
4747

48-
analysisDiagnostics, err := snapshot.Analyze(ctx, pkgid, analyzers)
48+
analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers)
4949
if err != nil {
5050
return nil, err
5151
}
@@ -81,7 +81,7 @@ func FileDiagnostics(ctx context.Context, snapshot Snapshot, uri span.URI) (File
8181
if err != nil {
8282
return nil, nil, err
8383
}
84-
adiags, err := Analyze(ctx, snapshot, pkg.Metadata().ID, false)
84+
adiags, err := Analyze(ctx, snapshot, map[PackageID]unit{pkg.Metadata().ID: {}}, false)
8585
if err != nil {
8686
return nil, nil, err
8787
}

gopls/internal/lsp/source/rename.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ package source
2121
// - special cases: embedded fields, interfaces, test variants,
2222
// function-local things with uppercase names;
2323
// packages with type errors (currently 'satisfy' rejects them),
24-
// pakage with missing imports;
24+
// package with missing imports;
2525
//
2626
// - measure performance in k8s.
2727
//

gopls/internal/lsp/source/view.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ type Snapshot interface {
9292
// Position information is added to FileSet().
9393
ParseGo(ctx context.Context, fh FileHandle, mode parser.Mode) (*ParsedGoFile, error)
9494

95-
// Analyze runs the specified analyzers on the given package at this snapshot.
96-
Analyze(ctx context.Context, id PackageID, analyzers []*Analyzer) ([]*Diagnostic, error)
95+
// Analyze runs the specified analyzers on the given packages at this snapshot.
96+
Analyze(ctx context.Context, pkgIDs map[PackageID]unit, analyzers []*Analyzer) ([]*Diagnostic, error)
9797

9898
// RunGoCommandPiped runs the given `go` command, writing its output
9999
// to stdout and stderr. Verb, Args, and WorkingDir must be specified.

0 commit comments

Comments
 (0)