Skip to content

Commit 6b4d1de

Browse files
committed
gopls/internal/lsp: avoid duplicate type checking following invalidation
Following a keystroke, it is common to compute both diagnostics and completion results. For small packages, this sometimes results in redundant work, but not enough to significantly affect benchmarks. However, for very large packages where type checking takes >100ms, these two operations always run in parallel recomputing the same shared state. This is made clear in the oracle completion benchmark. Fix this by guarding type checking with a mutex, and slightly delaying initial diagnostics to yield to other operations (though because diagnostics will also recompute shared, it doesn't matter too much which operation acquires the mutex first). For golang/go#61207 Change-Id: I761aef9c66ebdd54fab8c61605c42d82a8f412cc Reviewed-on: https://go-review.googlesource.com/c/tools/+/511435 gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Hyang-Ah Hana Kim <[email protected]>
1 parent 47c5305 commit 6b4d1de

File tree

3 files changed

+33
-0
lines changed

3 files changed

+33
-0
lines changed

gopls/internal/lsp/cache/check.go

+3
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ type (
323323
//
324324
// Both pre and post may be called concurrently.
325325
func (s *snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preTypeCheck, post postTypeCheck) error {
326+
s.typeCheckMu.Lock()
327+
defer s.typeCheckMu.Unlock()
328+
326329
ctx, done := event.Start(ctx, "cache.forEachPackage", tag.PackageCount.Of(len(ids)))
327330
defer done()
328331

gopls/internal/lsp/cache/snapshot.go

+12
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,18 @@ type snapshot struct {
184184
// detect ignored files.
185185
ignoreFilterOnce sync.Once
186186
ignoreFilter *ignoreFilter
187+
188+
// typeCheckMu guards type checking.
189+
//
190+
// Only one type checking pass should be running at a given time, for two reasons:
191+
// 1. type checking batches are optimized to use all available processors.
192+
// Generally speaking, running two type checking batches serially is about
193+
// as fast as running them in parallel.
194+
// 2. type checking produces cached artifacts that may be re-used by the
195+
// next type-checking batch: the shared import graph and the set of
196+
// active packages. Running type checking batches in parallel after an
197+
// invalidation can cause redundant calculation of this shared state.
198+
typeCheckMu sync.Mutex
187199
}
188200

189201
var globalSnapshotID uint64

gopls/internal/lsp/diagnostics.go

+18
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,27 @@ func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.U
188188
// file modifications.
189189
//
190190
// The second phase runs after the delay, and does everything.
191+
//
192+
// We wait a brief delay before the first phase, to allow higher priority
193+
// work such as autocompletion to acquire the type checking mutex (though
194+
// typically both diagnosing changed files and performing autocompletion
195+
// will be doing the same work: recomputing active packages).
196+
const minDelay = 20 * time.Millisecond
197+
select {
198+
case <-time.After(minDelay):
199+
case <-ctx.Done():
200+
return
201+
}
202+
191203
s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk)
192204
s.publishDiagnostics(ctx, false, snapshot)
193205

206+
if delay < minDelay {
207+
delay = 0
208+
} else {
209+
delay -= minDelay
210+
}
211+
194212
select {
195213
case <-time.After(delay):
196214
case <-ctx.Done():

0 commit comments

Comments
 (0)