Skip to content

Commit 463a76b

Browse files
committed
internal/lsp: only reload invalid metadata when necessary
This change adds a shouldLoad field to knownMetadata so that we can be more selective about reloading these. If a package has invalid metadata, but its metadata hasn't changed, we shouldn't attempt to reload it until the metadata changes. Fixes golang/go#40312 Change-Id: Icf5a13fd179421b8f70a5eab6a74b30aaf841f49 Reviewed-on: https://go-review.googlesource.com/c/tools/+/298489 Trust: Rebecca Stambler <rstambler@golang.org> Run-TryBot: Rebecca Stambler <rstambler@golang.org> gopls-CI: kokoro <noreply+kokoro@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Robert Findley <rfindley@google.com>
1 parent 116feae commit 463a76b

File tree

5 files changed

+199
-25
lines changed

5 files changed

+199
-25
lines changed

gopls/internal/regtest/diagnostics/diagnostics_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2045,3 +2045,92 @@ func Hello() {}
20452045
)
20462046
})
20472047
}
2048+
2049+
func TestReloadInvalidMetadata(t *testing.T) {
2050+
// We only use invalid metadata for Go versions > 1.12.
2051+
testenv.NeedsGo1Point(t, 13)
2052+
2053+
const mod = `
2054+
-- go.mod --
2055+
module mod.com
2056+
2057+
go 1.12
2058+
-- main.go --
2059+
package main
2060+
2061+
func _() {}
2062+
`
2063+
WithOptions(
2064+
EditorConfig{
2065+
ExperimentalUseInvalidMetadata: true,
2066+
},
2067+
// ExperimentalWorkspaceModule has a different failure mode for this
2068+
// case.
2069+
Modes(Singleton),
2070+
).Run(t, mod, func(t *testing.T, env *Env) {
2071+
env.Await(
2072+
OnceMet(
2073+
InitialWorkspaceLoad,
2074+
CompletedWork("Load", 1, false),
2075+
),
2076+
)
2077+
2078+
// Break the go.mod file on disk, expecting a reload.
2079+
env.WriteWorkspaceFile("go.mod", `modul mod.com
2080+
2081+
go 1.12
2082+
`)
2083+
env.Await(
2084+
OnceMet(
2085+
env.DoneWithChangeWatchedFiles(),
2086+
env.DiagnosticAtRegexp("go.mod", "modul"),
2087+
CompletedWork("Load", 1, false),
2088+
),
2089+
)
2090+
2091+
env.OpenFile("main.go")
2092+
env.Await(env.DoneWithOpen())
2093+
// The first edit after the go.mod file invalidation should cause a reload.
2094+
// Any subsequent simple edits should not.
2095+
content := `package main
2096+
2097+
func main() {
2098+
_ = 1
2099+
}
2100+
`
2101+
env.EditBuffer("main.go", fake.NewEdit(0, 0, 3, 0, content))
2102+
env.Await(
2103+
OnceMet(
2104+
env.DoneWithChange(),
2105+
CompletedWork("Load", 2, false),
2106+
NoLogMatching(protocol.Error, "error loading file"),
2107+
),
2108+
)
2109+
env.RegexpReplace("main.go", "_ = 1", "_ = 2")
2110+
env.Await(
2111+
OnceMet(
2112+
env.DoneWithChange(),
2113+
CompletedWork("Load", 2, false),
2114+
NoLogMatching(protocol.Error, "error loading file"),
2115+
),
2116+
)
2117+
// Add an import to the main.go file and confirm that it does get
2118+
// reloaded, but the reload fails, so we see a diagnostic on the new
2119+
// "fmt" import.
2120+
env.EditBuffer("main.go", fake.NewEdit(0, 0, 5, 0, `package main
2121+
2122+
import "fmt"
2123+
2124+
func main() {
2125+
fmt.Println("")
2126+
}
2127+
`))
2128+
env.Await(
2129+
OnceMet(
2130+
env.DoneWithChange(),
2131+
env.DiagnosticAtRegexp("main.go", `"fmt"`),
2132+
CompletedWork("Load", 3, false),
2133+
),
2134+
)
2135+
})
2136+
}

gopls/internal/regtest/workspace/workspace_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,6 @@ func Hello() int {
365365
)
366366
env.ApplyQuickFixes("moda/a/go.mod", d.Diagnostics)
367367
env.Await(env.DoneWithChangeWatchedFiles())
368-
369368
got, _ := env.GoToDefinition("moda/a/a.go", env.RegexpSearch("moda/a/a.go", "Hello"))
370369
if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) {
371370
t.Errorf("expected %s, got %v", want, got)

internal/lsp/cache/load.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,21 @@ type metadata struct {
5454

5555
// load calls packages.Load for the given scopes, updating package metadata,
5656
// import graph, and mapped files with the result.
57-
func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) error {
58-
if s.view.Options().VerboseWorkDoneProgress {
59-
work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading scopes %s", scopes), nil, nil)
60-
defer func() {
61-
go func() {
62-
work.End("Done.")
63-
}()
64-
}()
65-
}
66-
57+
func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interface{}) (err error) {
6758
var query []string
6859
var containsDir bool // for logging
6960
for _, scope := range scopes {
70-
if scope == "" {
61+
if !s.shouldLoad(scope) {
7162
continue
7263
}
64+
// Unless the context was canceled, set "shouldLoad" to false for all
65+
// of the metadata we attempted to load.
66+
defer func() {
67+
if errors.Is(err, context.Canceled) {
68+
return
69+
}
70+
s.clearShouldLoad(scope)
71+
}()
7372
switch scope := scope.(type) {
7473
case packagePath:
7574
if source.IsCommandLineArguments(string(scope)) {
@@ -110,6 +109,15 @@ func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...interf
110109
}
111110
sort.Strings(query) // for determinism
112111

112+
if s.view.Options().VerboseWorkDoneProgress {
113+
work := s.view.session.progress.Start(ctx, "Load", fmt.Sprintf("Loading query=%s", query), nil, nil)
114+
defer func() {
115+
go func() {
116+
work.End("Done.")
117+
}()
118+
}()
119+
}
120+
113121
ctx, done := event.Start(ctx, "cache.view.load", tag.Query.Of(query))
114122
defer done()
115123

@@ -452,6 +460,8 @@ func (s *snapshot) setMetadata(ctx context.Context, pkgPath packagePath, pkg *pa
452460

453461
// If we've already set the metadata for this snapshot, reuse it.
454462
if original, ok := s.metadata[m.id]; ok && original.valid {
463+
// Since we've just reloaded, clear out shouldLoad.
464+
original.shouldLoad = false
455465
m = original.metadata
456466
} else {
457467
s.metadata[m.id] = &knownMetadata{

internal/lsp/cache/snapshot.go

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ type knownMetadata struct {
138138
// valid is true if the given metadata is valid.
139139
// Invalid metadata can still be used if a metadata reload fails.
140140
valid bool
141+
142+
// shouldLoad is true if the given metadata should be reloaded.
143+
shouldLoad bool
141144
}
142145

143146
func (s *snapshot) ID() uint64 {
@@ -1028,6 +1031,71 @@ func (s *snapshot) getMetadata(id packageID) *knownMetadata {
10281031
return s.metadata[id]
10291032
}
10301033

1034+
func (s *snapshot) shouldLoad(scope interface{}) bool {
1035+
s.mu.Lock()
1036+
defer s.mu.Unlock()
1037+
1038+
switch scope := scope.(type) {
1039+
case packagePath:
1040+
var meta *knownMetadata
1041+
for _, m := range s.metadata {
1042+
if m.pkgPath != scope {
1043+
continue
1044+
}
1045+
meta = m
1046+
}
1047+
if meta == nil || meta.shouldLoad {
1048+
return true
1049+
}
1050+
return false
1051+
case fileURI:
1052+
uri := span.URI(scope)
1053+
ids := s.ids[uri]
1054+
if len(ids) == 0 {
1055+
return true
1056+
}
1057+
for _, id := range ids {
1058+
m, ok := s.metadata[id]
1059+
if !ok || m.shouldLoad {
1060+
return true
1061+
}
1062+
}
1063+
return false
1064+
default:
1065+
return true
1066+
}
1067+
}
1068+
1069+
func (s *snapshot) clearShouldLoad(scope interface{}) {
1070+
s.mu.Lock()
1071+
defer s.mu.Unlock()
1072+
1073+
switch scope := scope.(type) {
1074+
case packagePath:
1075+
var meta *knownMetadata
1076+
for _, m := range s.metadata {
1077+
if m.pkgPath == scope {
1078+
meta = m
1079+
}
1080+
}
1081+
if meta == nil {
1082+
return
1083+
}
1084+
meta.shouldLoad = false
1085+
case fileURI:
1086+
uri := span.URI(scope)
1087+
ids := s.ids[uri]
1088+
if len(ids) == 0 {
1089+
return
1090+
}
1091+
for _, id := range ids {
1092+
if m, ok := s.metadata[id]; ok {
1093+
m.shouldLoad = false
1094+
}
1095+
}
1096+
}
1097+
}
1098+
10311099
// noValidMetadataForURILocked reports whether there is any valid metadata for
10321100
// the given URI.
10331101
func (s *snapshot) noValidMetadataForURILocked(uri span.URI) bool {
@@ -1573,9 +1641,12 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
15731641

15741642
// Check if the file's package name or imports have changed,
15751643
// and if so, invalidate this file's packages' metadata.
1576-
shouldInvalidateMetadata, pkgNameChanged, importDeleted := s.shouldInvalidateMetadata(ctx, result, originalFH, change.fileHandle)
1577-
anyImportDeleted = anyImportDeleted || importDeleted
1644+
var shouldInvalidateMetadata, pkgNameChanged, importDeleted bool
1645+
if !isGoMod(uri) {
1646+
shouldInvalidateMetadata, pkgNameChanged, importDeleted = s.shouldInvalidateMetadata(ctx, result, originalFH, change.fileHandle)
1647+
}
15781648
invalidateMetadata := forceReloadMetadata || workspaceReload || shouldInvalidateMetadata
1649+
anyImportDeleted = anyImportDeleted || importDeleted
15791650

15801651
// Mark all of the package IDs containing the given file.
15811652
// TODO: if the file has moved into a new package, we should invalidate that too.
@@ -1748,8 +1819,9 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
17481819
invalidateMetadata := idsToInvalidate[k]
17491820
// Mark invalidated metadata rather than deleting it outright.
17501821
result.metadata[k] = &knownMetadata{
1751-
metadata: v.metadata,
1752-
valid: v.valid && !invalidateMetadata,
1822+
metadata: v.metadata,
1823+
valid: v.valid && !invalidateMetadata,
1824+
shouldLoad: v.shouldLoad || invalidateMetadata,
17531825
}
17541826
}
17551827
// Copy the URI to package ID mappings, skipping only those URIs whose

internal/lsp/regtest/expectation.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ type Expectation interface {
2929
var (
3030
// InitialWorkspaceLoad is an expectation that the workspace initial load has
3131
// completed. It is verified via workdone reporting.
32-
InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1)
32+
InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1, false)
3333
)
3434

3535
// A Verdict is the result of checking an expectation against the current
@@ -196,7 +196,7 @@ func ShowMessageRequest(title string) SimpleExpectation {
196196
// to be completely processed.
197197
func (e *Env) DoneWithOpen() Expectation {
198198
opens := e.Editor.Stats().DidOpen
199-
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens)
199+
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true)
200200
}
201201

202202
// StartedChange expects there to have been i work items started for
@@ -209,28 +209,28 @@ func StartedChange(i uint64) Expectation {
209209
// editor to be completely processed.
210210
func (e *Env) DoneWithChange() Expectation {
211211
changes := e.Editor.Stats().DidChange
212-
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes)
212+
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes, true)
213213
}
214214

215215
// DoneWithSave expects all didSave notifications currently sent by the editor
216216
// to be completely processed.
217217
func (e *Env) DoneWithSave() Expectation {
218218
saves := e.Editor.Stats().DidSave
219-
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves)
219+
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true)
220220
}
221221

222222
// DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications
223223
// currently sent by the editor to be completely processed.
224224
func (e *Env) DoneWithChangeWatchedFiles() Expectation {
225225
changes := e.Editor.Stats().DidChangeWatchedFiles
226-
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes)
226+
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes, true)
227227
}
228228

229229
// DoneWithClose expects all didClose notifications currently sent by the
230230
// editor to be completely processed.
231231
func (e *Env) DoneWithClose() Expectation {
232232
changes := e.Editor.Stats().DidClose
233-
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes)
233+
return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes, true)
234234
}
235235

236236
// StartedWork expect a work item to have been started >= atLeast times.
@@ -253,16 +253,20 @@ func StartedWork(title string, atLeast uint64) SimpleExpectation {
253253
//
254254
// Since the Progress API doesn't include any hidden metadata, we must use the
255255
// progress notification title to identify the work we expect to be completed.
256-
func CompletedWork(title string, atLeast uint64) SimpleExpectation {
256+
func CompletedWork(title string, count uint64, atLeast bool) SimpleExpectation {
257257
check := func(s State) Verdict {
258-
if s.completedWork[title] >= atLeast {
258+
if s.completedWork[title] == count || atLeast && s.completedWork[title] > count {
259259
return Met
260260
}
261261
return Unmet
262262
}
263+
desc := fmt.Sprintf("completed work %q %v times", title, count)
264+
if atLeast {
265+
desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count)
266+
}
263267
return SimpleExpectation{
264268
check: check,
265-
description: fmt.Sprintf("completed work %q at least %d time(s)", title, atLeast),
269+
description: desc,
266270
}
267271
}
268272

0 commit comments

Comments
 (0)