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.

gopls/internal/regtest/bench/iwl_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ func BenchmarkInitialWorkspaceLoad(b *testing.B) {
2727
{"pkgsite", "internal/frontend/server.go"},
2828
{"starlark", "starlark/eval.go"},
2929
{"tools", "internal/lsp/cache/snapshot.go"},
30+
{"hashiform", "internal/provider/provider.go"},
3031
}
3132

3233
for _, test := range tests {

gopls/internal/regtest/bench/repo_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ var repos = map[string]*repo{
8787
short: true,
8888
inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/[email protected]"),
8989
},
90+
91+
// A repo of similar size to kubernetes, but with substantially more
92+
// complex types that led to a serious performance regression (issue #60621).
93+
"hashiform": {
94+
name: "hashiform",
95+
url: "https://github.com/hashicorp/terraform-provider-aws",
96+
commit: "ac55de2b1950972d93feaa250d7505d9ed829c7c",
97+
inDir: flag.String("hashiform_dir", "", "if set, reuse this directory as hashiform@ac55de2"),
98+
},
9099
}
91100

92101
// getRepo gets the requested repo, and skips the test if -short is set and

internal/event/tag/tag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ var (
1919
File = keys.NewString("file", "")
2020
Directory = keys.New("directory", "")
2121
URI = keys.New("URI", "")
22-
Package = keys.NewString("package", "") // Package ID
22+
Package = keys.NewString("package", "") // sorted comma-separated list of Package IDs
2323
PackagePath = keys.NewString("package_path", "")
2424
Query = keys.New("query", "")
2525
Snapshot = keys.NewUInt64("snapshot", "")

internal/facts/facts.go

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,24 +158,52 @@ type gobFact struct {
158158
// for the same package. Each call to Decode returns an independent
159159
// fact set.
160160
type Decoder struct {
161-
pkg *types.Package
162-
packages map[string]*types.Package
161+
pkg *types.Package
162+
getPackage GetPackageFunc
163163
}
164164

165165
// NewDecoder returns a fact decoder for the specified package.
166+
//
167+
// It uses a brute-force recursive approach to enumerate all objects
168+
// defined by dependencies of pkg, so that it can learn the set of
169+
// package paths that may be mentioned in the fact encoding. This does
170+
// not scale well; use [NewDecoderFunc] where possible.
166171
func NewDecoder(pkg *types.Package) *Decoder {
167172
// Compute the import map for this package.
168173
// See the package doc comment.
169-
return &Decoder{pkg, importMap(pkg.Imports())}
174+
m := importMap(pkg.Imports())
175+
getPackageFunc := func(path string) *types.Package { return m[path] }
176+
return NewDecoderFunc(pkg, getPackageFunc)
177+
}
178+
179+
// NewDecoderFunc returns a fact decoder for the specified package.
180+
//
181+
// It calls the getPackage function for the package path string of
182+
// each dependency (perhaps indirect) that it encounters in the
183+
// encoding. If the function returns nil, the fact is discarded.
184+
//
185+
// This function is preferred over [NewDecoder] when the client is
186+
// capable of efficient look-up of packages by package path.
187+
func NewDecoderFunc(pkg *types.Package, getPackage GetPackageFunc) *Decoder {
188+
return &Decoder{
189+
pkg: pkg,
190+
getPackage: getPackage,
191+
}
170192
}
171193

172-
// Decode decodes all the facts relevant to the analysis of package pkg.
173-
// The read function reads serialized fact data from an external source
174-
// for one of of pkg's direct imports. The empty file is a valid
175-
// encoding of an empty fact set.
194+
// A GetPackageFunc function returns the package denoted by a package path.
195+
type GetPackageFunc = func(pkgPath string) *types.Package
196+
197+
// Decode decodes all the facts relevant to the analysis of package
198+
// pkg. The read function reads serialized fact data from an external
199+
// source for one of pkg's direct imports, identified by package path.
200+
// The empty file is a valid encoding of an empty fact set.
176201
//
177202
// It is the caller's responsibility to call gob.Register on all
178203
// necessary fact types.
204+
//
205+
// Concurrent calls to Decode are safe, so long as the
206+
// [GetPackageFunc] (if any) is also concurrency-safe.
179207
func (d *Decoder) Decode(read func(*types.Package) ([]byte, error)) (*Set, error) {
180208
// Read facts from imported packages.
181209
// Facts may describe indirectly imported packages, or their objects.
@@ -202,13 +230,11 @@ func (d *Decoder) Decode(read func(*types.Package) ([]byte, error)) (*Set, error
202230
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&gobFacts); err != nil {
203231
return nil, fmt.Errorf("decoding facts for %q: %v", imp.Path(), err)
204232
}
205-
if debug {
206-
logf("decoded %d facts: %v", len(gobFacts), gobFacts)
207-
}
233+
logf("decoded %d facts: %v", len(gobFacts), gobFacts)
208234

209235
// Parse each one into a key and a Fact.
210236
for _, f := range gobFacts {
211-
factPkg := d.packages[f.PkgPath]
237+
factPkg := d.getPackage(f.PkgPath) // possibly an indirect dependency
212238
if factPkg == nil {
213239
// Fact relates to a dependency that was
214240
// unused in this translation unit. Skip.

0 commit comments

Comments
 (0)