Skip to content

Commit 9f3c646

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/cache: memoize cache keys
analysisNode.cacheKey shows up as very hot in benchmarks, yet analysis should observe the same invalidation heuristics as other package data. Memoizing the cache keys in the snapshot reduced total CPU of BenchmarkDiagnosePackageFiles by 10-15%. For golang/go#53275 Change-Id: I0f60c3e78c77ac6e58b14308bb0331022babae69 Reviewed-on: https://go-review.googlesource.com/c/tools/+/622037 Auto-Submit: Robert Findley <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 1f162c6 commit 9f3c646

File tree

4 files changed

+87
-58
lines changed

4 files changed

+87
-58
lines changed

gopls/internal/cache/analysis.go

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"go/token"
2020
"go/types"
2121
"log"
22+
"maps"
2223
urlpkg "net/url"
2324
"path/filepath"
2425
"reflect"
@@ -45,6 +46,7 @@ import (
4546
"golang.org/x/tools/gopls/internal/util/bug"
4647
"golang.org/x/tools/gopls/internal/util/frob"
4748
"golang.org/x/tools/gopls/internal/util/moremaps"
49+
"golang.org/x/tools/gopls/internal/util/persistent"
4850
"golang.org/x/tools/internal/analysisinternal"
4951
"golang.org/x/tools/internal/event"
5052
"golang.org/x/tools/internal/facts"
@@ -175,7 +177,7 @@ const AnalysisProgressTitle = "Analyzing Dependencies"
175177
// The analyzers list must be duplicate free; order does not matter.
176178
//
177179
// Notifications of progress may be sent to the optional reporter.
178-
func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Package, analyzers []*settings.Analyzer, reporter *progress.Tracker) ([]*Diagnostic, error) {
180+
func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Package, reporter *progress.Tracker) ([]*Diagnostic, error) {
179181
start := time.Now() // for progress reporting
180182

181183
var tagStr string // sorted comma-separated list of PackageIDs
@@ -192,6 +194,7 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
192194

193195
// Filter and sort enabled root analyzers.
194196
// A disabled analyzer may still be run if required by another.
197+
analyzers := analyzers(s.Options().Staticcheck)
195198
toSrc := make(map[*analysis.Analyzer]*settings.Analyzer)
196199
var enabledAnalyzers []*analysis.Analyzer // enabled subset + transitive requirements
197200
for _, a := range analyzers {
@@ -322,20 +325,14 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
322325
roots = append(roots, root)
323326
}
324327

325-
// Now that we have read all files,
326-
// we no longer need the snapshot.
327-
// (but options are needed for progress reporting)
328-
options := s.Options()
329-
s = nil
330-
331328
// Progress reporting. If supported, gopls reports progress on analysis
332329
// passes that are taking a long time.
333330
maybeReport := func(completed int64) {}
334331

335332
// Enable progress reporting if enabled by the user
336333
// and we have a capable reporter.
337-
if reporter != nil && reporter.SupportsWorkDoneProgress() && options.AnalysisProgressReporting {
338-
var reportAfter = options.ReportAnalysisProgressAfter // tests may set this to 0
334+
if reporter != nil && reporter.SupportsWorkDoneProgress() && s.Options().AnalysisProgressReporting {
335+
var reportAfter = s.Options().ReportAnalysisProgressAfter // tests may set this to 0
339336
const reportEvery = 1 * time.Second
340337

341338
ctx, cancel := context.WithCancel(ctx)
@@ -396,10 +393,34 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
396393
limiter <- unit{}
397394
defer func() { <-limiter }()
398395

399-
summary, err := an.runCached(ctx)
396+
// Check to see if we already have a valid cache key. If not, compute it.
397+
//
398+
// The snapshot field that memoizes keys depends on whether this key is
399+
// for the analysis result including all enabled analyzer, or just facty analyzers.
400+
var keys *persistent.Map[PackageID, file.Hash]
401+
if _, root := pkgs[an.mp.ID]; root {
402+
keys = s.fullAnalysisKeys
403+
} else {
404+
keys = s.factyAnalysisKeys
405+
}
406+
407+
// As keys is referenced by a snapshot field, it's guarded by s.mu.
408+
s.mu.Lock()
409+
key, keyFound := keys.Get(an.mp.ID)
410+
s.mu.Unlock()
411+
412+
if !keyFound {
413+
key = an.cacheKey()
414+
s.mu.Lock()
415+
keys.Set(an.mp.ID, key, nil)
416+
s.mu.Unlock()
417+
}
418+
419+
summary, err := an.runCached(ctx, key)
400420
if err != nil {
401421
return err // cancelled, or failed to produce a package
402422
}
423+
403424
maybeReport(completed.Add(1))
404425
an.summary = summary
405426

@@ -488,6 +509,14 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
488509
return results, nil
489510
}
490511

512+
func analyzers(staticcheck bool) []*settings.Analyzer {
513+
analyzers := slices.Collect(maps.Values(settings.DefaultAnalyzers))
514+
if staticcheck {
515+
analyzers = slices.AppendSeq(analyzers, maps.Values(settings.StaticcheckAnalyzers))
516+
}
517+
return analyzers
518+
}
519+
491520
func (an *analysisNode) decrefPreds() {
492521
if an.unfinishedPreds.Add(-1) == 0 {
493522
an.summary.Actions = nil
@@ -711,21 +740,14 @@ var (
711740
// actions failed. It usually fails only if the package was unknown,
712741
// a file was missing, or the operation was cancelled.
713742
//
714-
// Postcondition: runCached must not continue to use the snapshot
715-
// (in background goroutines) after it has returned; see memoize.RefCounted.
716-
func (an *analysisNode) runCached(ctx context.Context) (*analyzeSummary, error) {
717-
// At this point we have the action results (serialized
718-
// packages and facts) of our immediate dependencies,
719-
// and the metadata and content of this package.
720-
//
721-
// We now compute a hash for all our inputs, and consult a
722-
// global cache of promised results. If nothing material
723-
// has changed, we'll make a hit in the shared cache.
743+
// The provided key is the cache key for this package.
744+
func (an *analysisNode) runCached(ctx context.Context, key file.Hash) (*analyzeSummary, error) {
745+
// At this point we have the action results (serialized packages and facts)
746+
// of our immediate dependencies, and the metadata and content of this
747+
// package.
724748
//
725-
// The hash of our inputs is based on the serialized export
726-
// data and facts so that immaterial changes can be pruned
727-
// without decoding.
728-
key := an.cacheKey()
749+
// We now consult a global cache of promised results. If nothing material has
750+
// changed, we'll make a hit in the shared cache.
729751

730752
// Access the cache.
731753
var summary *analyzeSummary

gopls/internal/cache/session.go

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -240,25 +240,27 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
240240

241241
s.snapshotWG.Add(1)
242242
v.snapshot = &Snapshot{
243-
view: v,
244-
backgroundCtx: backgroundCtx,
245-
cancel: cancel,
246-
store: s.cache.store,
247-
refcount: 1, // Snapshots are born referenced.
248-
done: s.snapshotWG.Done,
249-
packages: new(persistent.Map[PackageID, *packageHandle]),
250-
meta: new(metadata.Graph),
251-
files: newFileMap(),
252-
symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
253-
shouldLoad: new(persistent.Map[PackageID, []PackagePath]),
254-
unloadableFiles: new(persistent.Set[protocol.DocumentURI]),
255-
parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
256-
parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
257-
modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
258-
modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
259-
modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
260-
moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]),
261-
vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]),
243+
view: v,
244+
backgroundCtx: backgroundCtx,
245+
cancel: cancel,
246+
store: s.cache.store,
247+
refcount: 1, // Snapshots are born referenced.
248+
done: s.snapshotWG.Done,
249+
packages: new(persistent.Map[PackageID, *packageHandle]),
250+
fullAnalysisKeys: new(persistent.Map[PackageID, file.Hash]),
251+
factyAnalysisKeys: new(persistent.Map[PackageID, file.Hash]),
252+
meta: new(metadata.Graph),
253+
files: newFileMap(),
254+
symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
255+
shouldLoad: new(persistent.Map[PackageID, []PackagePath]),
256+
unloadableFiles: new(persistent.Set[protocol.DocumentURI]),
257+
parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
258+
parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
259+
modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
260+
modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
261+
modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]),
262+
moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]),
263+
vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]),
262264
}
263265

264266
// Snapshots must observe all open files, as there are some caching

gopls/internal/cache/snapshot.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ type Snapshot struct {
144144
// be in packages, unless there is a missing import
145145
packages *persistent.Map[PackageID, *packageHandle]
146146

147+
// fullAnalysisKeys and factyAnalysisKeys hold memoized cache keys for
148+
// analysis packages. "full" refers to the cache key including all enabled
149+
// analyzers, whereas "facty" is the key including only the subset of enabled
150+
// analyzers that produce facts, such as is required for transitively
151+
// imported packages.
152+
//
153+
// These keys are memoized because they can be quite expensive to compute.
154+
fullAnalysisKeys *persistent.Map[PackageID, file.Hash]
155+
factyAnalysisKeys *persistent.Map[PackageID, file.Hash]
156+
147157
// workspacePackages contains the workspace's packages, which are loaded
148158
// when the view is created. It does not contain intermediate test variants.
149159
workspacePackages immutable.Map[PackageID, PackagePath]
@@ -1611,6 +1621,8 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f
16111621
initialized: s.initialized,
16121622
initialErr: s.initialErr,
16131623
packages: s.packages.Clone(),
1624+
fullAnalysisKeys: s.fullAnalysisKeys.Clone(),
1625+
factyAnalysisKeys: s.factyAnalysisKeys.Clone(),
16141626
files: s.files.clone(changedFiles),
16151627
symbolizeHandles: cloneWithout(s.symbolizeHandles, changedFiles, nil),
16161628
workspacePackages: s.workspacePackages,
@@ -1888,6 +1900,12 @@ func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done f
18881900
// invalidation.
18891901
if ph, ok := result.packages.Get(id); ok {
18901902
needsDiagnosis = true
1903+
1904+
// Always invalidate analysis keys, as we do not implement fine-grained
1905+
// invalidation for analysis.
1906+
result.fullAnalysisKeys.Delete(id)
1907+
result.factyAnalysisKeys.Delete(id)
1908+
18911909
if invalidateMetadata {
18921910
result.packages.Delete(id)
18931911
} else {

gopls/internal/golang/diagnostics.go

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,11 @@ package golang
66

77
import (
88
"context"
9-
"maps"
10-
"slices"
119

1210
"golang.org/x/tools/gopls/internal/cache"
1311
"golang.org/x/tools/gopls/internal/cache/metadata"
1412
"golang.org/x/tools/gopls/internal/progress"
1513
"golang.org/x/tools/gopls/internal/protocol"
16-
"golang.org/x/tools/gopls/internal/settings"
1714
"golang.org/x/tools/gopls/internal/util/moremaps"
1815
)
1916

@@ -35,8 +32,7 @@ func DiagnoseFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.Do
3532
diags := pkgDiags[uri]
3633

3734
// Get analysis diagnostics.
38-
analyzers := analyzers(snapshot.Options().Staticcheck)
39-
pkgAnalysisDiags, err := snapshot.Analyze(ctx, map[PackageID]*metadata.Package{mp.ID: mp}, analyzers, nil)
35+
pkgAnalysisDiags, err := snapshot.Analyze(ctx, map[PackageID]*metadata.Package{mp.ID: mp}, nil)
4036
if err != nil {
4137
return nil, err
4238
}
@@ -60,8 +56,7 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID
6056
return nil, ctx.Err()
6157
}
6258

63-
analyzers := analyzers(snapshot.Options().Staticcheck)
64-
analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers, tracker)
59+
analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, tracker)
6560
if err != nil {
6661
return nil, err
6762
}
@@ -71,14 +66,6 @@ func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID
7166
// byURI is used for grouping diagnostics.
7267
func byURI(d *cache.Diagnostic) protocol.DocumentURI { return d.URI }
7368

74-
func analyzers(staticcheck bool) []*settings.Analyzer {
75-
analyzers := slices.Collect(maps.Values(settings.DefaultAnalyzers))
76-
if staticcheck {
77-
analyzers = slices.AppendSeq(analyzers, maps.Values(settings.StaticcheckAnalyzers))
78-
}
79-
return analyzers
80-
}
81-
8269
// CombineDiagnostics combines and filters list/parse/type diagnostics from
8370
// tdiags with the analysis adiags, returning the resulting combined set.
8471
//

0 commit comments

Comments
 (0)