Skip to content

Commit 1f162c6

Browse files
findleyrgopherbot
authored andcommitted
gopls/internal/cache: async pull diagnostics and joined analysis
Make pull diagnostics async, to optimize handling of many simultaneous requests for the diagnostics of open files. Accordingly, update the pull diagnostic benchmark to request diagnostics concurrently. Naively, these concurrent diagnostics would cause gopls to incur major performance penalty due to duplicate analyses -- as much as 5x total CPU in the pull diagnostic benchmark. To mitigate this cost, join ongoing analysis operations using a shared global transient futureCache. This reduces the benchmark back down close to its previous total CPU, with reduced latency. Eyeballing the net delta of this CL in the benchmark, it looks like +20% total CPU, -30% latency. As a nice side effect, this more than eliminates the need to pre-seed the file cache in the gopls marker tests. The shared futures provide more consolidation of work than the pre-seeding, because of variance in the cache keys of standard library packages, due to different gopls options. For golang/go#53275 Change-Id: Ie92bab4c140e3f86852531be8204b6574b254d8e Reviewed-on: https://go-review.googlesource.com/c/tools/+/622036 LUCI-TryBot-Result: Go LUCI <[email protected]> Auto-Submit: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent dbb8252 commit 1f162c6

File tree

4 files changed

+58
-91
lines changed

4 files changed

+58
-91
lines changed

gopls/internal/cache/analysis.go

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
259259
an = &analysisNode{
260260
allNodes: nodes,
261261
fset: fset,
262-
fsource: struct{ file.Source }{s}, // expose only ReadFile
262+
fsource: s, // expose only ReadFile
263263
viewType: s.View().Type(),
264264
mp: mp,
265265
analyzers: facty, // all nodes run at least the facty analyzers
@@ -305,6 +305,8 @@ func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Pac
305305
from.unfinishedSuccs.Add(+1) // incref
306306
an.preds = append(an.preds, from)
307307
}
308+
// Increment unfinishedPreds even for root nodes (from==nil), so that their
309+
// Action summaries are never cleared.
308310
an.unfinishedPreds.Add(+1)
309311
return an, nil
310312
}
@@ -689,6 +691,19 @@ type actionSummary struct {
689691
Err string // "" => success
690692
}
691693

694+
var (
695+
// inFlightAnalyses records active analysis operations so that later requests
696+
// can be satisfied by joining onto earlier requests that are still active.
697+
//
698+
// Note that persistent=false, so results are cleared once they are delivered
699+
// to awaiting goroutines.
700+
inFlightAnalyses = newFutureCache[file.Hash, *analyzeSummary](false)
701+
702+
// cacheLimit reduces parallelism of filecache updates.
703+
// We allow more than typical GOMAXPROCS as it's a mix of CPU and I/O.
704+
cacheLimit = make(chan unit, 32)
705+
)
706+
692707
// runCached applies a list of analyzers (plus any others
693708
// transitively required by them) to a package. It succeeds as long
694709
// as it could produce a types.Package, even if there were direct or
@@ -725,39 +740,41 @@ func (an *analysisNode) runCached(ctx context.Context) (*analyzeSummary, error)
725740
return nil, bug.Errorf("internal error reading shared cache: %v", err)
726741
} else {
727742
// Cache miss: do the work.
728-
var err error
729-
summary, err = an.run(ctx)
743+
cachedSummary, err := inFlightAnalyses.get(ctx, key, func(ctx context.Context) (*analyzeSummary, error) {
744+
summary, err := an.run(ctx)
745+
if err != nil {
746+
return nil, err
747+
}
748+
if summary == nil { // debugging #66732 (can't happen)
749+
bug.Reportf("analyzeNode.run returned nil *analyzeSummary")
750+
}
751+
go func() {
752+
cacheLimit <- unit{} // acquire token
753+
defer func() { <-cacheLimit }() // release token
754+
755+
data := analyzeSummaryCodec.Encode(summary)
756+
if false {
757+
log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), an.mp.ID)
758+
}
759+
if err := filecache.Set(cacheKind, key, data); err != nil {
760+
event.Error(ctx, "internal error updating analysis shared cache", err)
761+
}
762+
}()
763+
return summary, nil
764+
})
730765
if err != nil {
731766
return nil, err
732767
}
733-
if summary == nil { // debugging #66732 (can't happen)
734-
bug.Reportf("analyzeNode.run returned nil *analyzeSummary")
735-
}
736-
737-
an.unfinishedPreds.Add(+1) // incref
738-
go func() {
739-
defer an.decrefPreds() //decref
740768

741-
cacheLimit <- unit{} // acquire token
742-
defer func() { <-cacheLimit }() // release token
743-
744-
data := analyzeSummaryCodec.Encode(summary)
745-
if false {
746-
log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), an.mp.ID)
747-
}
748-
if err := filecache.Set(cacheKind, key, data); err != nil {
749-
event.Error(ctx, "internal error updating analysis shared cache", err)
750-
}
751-
}()
769+
// Copy the computed summary. In decrefPreds, we may zero out
770+
// summary.actions, but can't mutate a shared result.
771+
copy := *cachedSummary
772+
summary = &copy
752773
}
753774

754775
return summary, nil
755776
}
756777

757-
// cacheLimit reduces parallelism of cache updates.
758-
// We allow more than typical GOMAXPROCS as it's a mix of CPU and I/O.
759-
var cacheLimit = make(chan unit, 32)
760-
761778
// analysisCacheKey returns a cache key that is a cryptographic digest
762779
// of the all the values that might affect type checking and analysis:
763780
// the analyzer names, package metadata, names and contents of

gopls/internal/server/diagnostics.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"golang.org/x/tools/gopls/internal/work"
3030
"golang.org/x/tools/internal/event"
3131
"golang.org/x/tools/internal/event/keys"
32+
"golang.org/x/tools/internal/jsonrpc2"
3233
)
3334

3435
// Diagnostic implements the textDocument/diagnostic LSP request, reporting
@@ -50,6 +51,9 @@ func (s *server) Diagnostic(ctx context.Context, params *protocol.DocumentDiagno
5051
return nil, err
5152
}
5253
defer release()
54+
55+
jsonrpc2.Async(ctx) // allow asynchronous collection of diagnostics
56+
5357
uri := fh.URI()
5458
kind := snapshot.FileKind(fh)
5559
var diagnostics []*cache.Diagnostic

gopls/internal/test/integration/bench/diagnostic_test.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package bench
66

77
import (
8+
"sync"
89
"testing"
910

1011
"golang.org/x/tools/gopls/internal/protocol"
@@ -61,17 +62,19 @@ func BenchmarkDiagnosePackageFiles(b *testing.B) {
6162

6263
for i := 0; i < b.N; i++ {
6364
edit()
64-
var diags []protocol.Diagnostic
65+
var wg sync.WaitGroup
6566
for _, file := range files {
66-
fileDiags := env.Diagnostics(file)
67-
for _, d := range fileDiags {
68-
if d.Severity == protocol.SeverityError {
69-
diags = append(diags, d)
67+
wg.Add(1)
68+
go func() {
69+
defer wg.Done()
70+
fileDiags := env.Diagnostics(file)
71+
for _, d := range fileDiags {
72+
if d.Severity == protocol.SeverityError {
73+
b.Errorf("unexpected error diagnostic: %s", d.Message)
74+
}
7075
}
71-
}
72-
}
73-
if len(diags) != 0 {
74-
b.Fatalf("got %d error diagnostics, want 0\ndiagnostics:\n%v", len(diags), diags)
76+
}()
7577
}
78+
wg.Wait()
7679
}
7780
}

gopls/internal/test/marker/marker_test.go

Lines changed: 0 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import (
2727
"sort"
2828
"strings"
2929
"testing"
30-
"time"
3130

3231
"github.com/google/go-cmp/cmp"
3332
"github.com/google/go-cmp/cmp/cmpopts"
@@ -108,10 +107,6 @@ func Test(t *testing.T) {
108107
// Opt: use a shared cache.
109108
cache := cache.New(nil)
110109

111-
// Opt: seed the cache and file cache by type-checking and analyzing common
112-
// standard library packages.
113-
seedCache(t, cache)
114-
115110
for _, test := range tests {
116111
test := test
117112
t.Run(test.name, func(t *testing.T) {
@@ -290,58 +285,6 @@ func Test(t *testing.T) {
290285
}
291286
}
292287

293-
// seedCache populates the file cache by type checking and analyzing standard
294-
// library packages that are reachable from tests.
295-
//
296-
// Most tests are themselves small codebases, and yet may reference large
297-
// amounts of standard library code. Since tests are heavily parallelized, they
298-
// naively end up type checking and analyzing many of the same standard library
299-
// packages. By seeding the cache, we ensure cache hits for these standard
300-
// library packages, significantly reducing the amount of work done by each
301-
// test.
302-
//
303-
// The following command was used to determine the set of packages to import
304-
// below:
305-
//
306-
// rm -rf ~/.cache/gopls && \
307-
// go test -count=1 ./internal/test/marker -cpuprofile=prof -v
308-
//
309-
// Look through the individual test timings to see which tests are slow, then
310-
// look through the imports of slow tests to see which standard library
311-
// packages are imported. Choose high level packages such as go/types that
312-
// import others such as fmt or go/ast. After doing so, re-run the command and
313-
// verify that the total samples in the collected profile decreased.
314-
func seedCache(t *testing.T, cache *cache.Cache) {
315-
start := time.Now()
316-
317-
// The the doc string for details on how this seed was produced.
318-
seed := `package p
319-
import (
320-
_ "net/http"
321-
_ "sort"
322-
_ "go/types"
323-
_ "testing"
324-
)
325-
`
326-
327-
// Create a test environment for the seed file.
328-
env := newEnv(t, cache, map[string][]byte{"p.go": []byte(seed)}, nil, nil, fake.EditorConfig{})
329-
// See other TODO: this cleanup logic is too messy.
330-
defer env.Editor.Shutdown(context.Background()) // ignore error
331-
defer env.Sandbox.Close() // ignore error
332-
env.Awaiter.Await(context.Background(), integration.InitialWorkspaceLoad)
333-
334-
// Opening the file is necessary to trigger analysis.
335-
env.OpenFile("p.go")
336-
337-
// As a checksum, verify that the file has no errors after analysis.
338-
// This isn't strictly necessary, but helps avoid incorrect seeding due to
339-
// typos.
340-
env.AfterChange(integration.NoDiagnostics())
341-
342-
t.Logf("warming the cache took %s", time.Since(start))
343-
}
344-
345288
// A marker holds state for the execution of a single @marker
346289
// annotation in the source.
347290
type marker struct {

0 commit comments

Comments
 (0)