Skip to content

Commit a3eef25

Browse files
committed
gopls/internal/lsp/cache: record parse keys when they're created
When we parse a file through snapshot.ParseGo, we must record the parse key by its URI for later invalidation. This wasn't done, resulting in a memory leak. Fix the leak by actually recording parse keys in the parseKeysByURI map, which was previously always empty. Write a test that diffs the cache before and after a change, which would have caught this bug (and others). Fixes golang/go#57222 Change-Id: I308812bf1030276dff08c26d359433750f44849a Reviewed-on: https://go-review.googlesource.com/c/tools/+/456642 Reviewed-by: Alan Donovan <adonovan@google.com> Run-TryBot: Robert Findley <rfindley@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> gopls-CI: kokoro <noreply+kokoro@google.com>
1 parent 3da7f1e commit a3eef25

File tree

2 files changed

+106
-2
lines changed

2 files changed

+106
-2
lines changed

gopls/internal/lsp/cache/parse.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc
5959

6060
// cache miss?
6161
if !hit {
62-
handle, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} {
62+
promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} {
6363
parsed, err := parseGoImpl(ctx, arg.(*snapshot).FileSet(), fh, mode)
6464
return parseGoResult{parsed, err}
6565
})
@@ -70,8 +70,30 @@ func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode sourc
7070
entry = prev
7171
release()
7272
} else {
73-
entry = handle
73+
entry = promise
7474
s.parsedGoFiles.Set(key, entry, func(_, _ interface{}) { release() })
75+
76+
// In order to correctly invalidate the key above, we must keep track of
77+
// the parse key just created.
78+
//
79+
// TODO(rfindley): use a two-level map URI->parseKey->promise.
80+
keys, _ := s.parseKeysByURI.Get(fh.URI())
81+
82+
// Only record the new key if it doesn't exist. This is overly cautious:
83+
// we should only be setting the key if it doesn't exist. However, this
84+
// logic will be replaced soon, and erring on the side of caution seemed
85+
// wise.
86+
foundKey := false
87+
for _, existing := range keys {
88+
if existing == key {
89+
foundKey = true
90+
break
91+
}
92+
}
93+
if !foundKey {
94+
keys = append(keys, key)
95+
s.parseKeysByURI.Set(fh.URI(), keys)
96+
}
7597
}
7698
s.mu.Unlock()
7799
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Copyright 2022 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package misc
6+
7+
import (
8+
"context"
9+
"testing"
10+
11+
"github.com/google/go-cmp/cmp"
12+
"golang.org/x/tools/gopls/internal/hooks"
13+
"golang.org/x/tools/gopls/internal/lsp/cache"
14+
"golang.org/x/tools/gopls/internal/lsp/debug"
15+
"golang.org/x/tools/gopls/internal/lsp/fake"
16+
"golang.org/x/tools/gopls/internal/lsp/lsprpc"
17+
. "golang.org/x/tools/gopls/internal/lsp/regtest"
18+
"golang.org/x/tools/internal/jsonrpc2"
19+
"golang.org/x/tools/internal/jsonrpc2/servertest"
20+
)
21+
22+
// Test for golang/go#57222.
23+
func TestCacheLeak(t *testing.T) {
24+
const files = `-- a.go --
25+
package a
26+
27+
func _() {
28+
println("1")
29+
}
30+
`
31+
c := cache.New(nil, nil)
32+
env := setupEnv(t, files, c)
33+
env.Await(InitialWorkspaceLoad)
34+
env.OpenFile("a.go")
35+
36+
// Make a couple edits to stabilize cache state.
37+
//
38+
// For some reason, after only one edit we're left with two parsed files
39+
// (perhaps because something had to ParseHeader). If this test proves flaky,
40+
// we'll need to investigate exactly what is causing various parse modes to
41+
// be present (or rewrite the test to be more tolerant, for example make ~100
42+
// modifications and assert that we're within a few of where we're started).
43+
env.RegexpReplace("a.go", "1", "2")
44+
env.RegexpReplace("a.go", "2", "3")
45+
env.AfterChange()
46+
47+
// Capture cache state, make an arbitrary change, and wait for gopls to do
48+
// its work. Afterward, we should have the exact same number of parsed
49+
before := c.MemStats()
50+
env.RegexpReplace("a.go", "3", "4")
51+
env.AfterChange()
52+
after := c.MemStats()
53+
54+
if diff := cmp.Diff(before, after); diff != "" {
55+
t.Errorf("store objects differ after change (-before +after)\n%s", diff)
56+
}
57+
}
58+
59+
// setupEnv creates a new sandbox environment for editing the txtar encoded
60+
// content of files. It uses a new gopls instance backed by the Cache c.
61+
func setupEnv(t *testing.T, files string, c *cache.Cache) *Env {
62+
ctx := debug.WithInstance(context.Background(), "", "off")
63+
server := lsprpc.NewStreamServer(c, false, hooks.Options)
64+
ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
65+
s, err := fake.NewSandbox(&fake.SandboxConfig{
66+
Files: fake.UnpackTxt(files),
67+
})
68+
if err != nil {
69+
t.Fatal(err)
70+
}
71+
72+
a := NewAwaiter(s.Workdir)
73+
e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(ctx, ts, a.Hooks())
74+
75+
return &Env{
76+
T: t,
77+
Ctx: ctx,
78+
Editor: e,
79+
Sandbox: s,
80+
Awaiter: a,
81+
}
82+
}

0 commit comments

Comments
 (0)