Skip to content

Commit c6c9830

Browse files
committed
go/types/objectpath: memoize scope lookup in objectpath.Encoder
Profiling revealed that scope lookup itself was costly, so memoize the objects themselves, not just their names. Also update BenchmarkCompletionFollowingEdit to allow it to be run on multiple repos, and add a test case for kubernetes. For golang/go#53992 Change-Id: I17b1f39d8c356e8628610a544306686573a813a7 Reviewed-on: https://go-review.googlesource.com/c/tools/+/502976 gopls-CI: kokoro <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]> TryBot-Bypass: Robert Findley <[email protected]>
1 parent 0245e1d commit c6c9830

File tree

2 files changed

+90
-37
lines changed

2 files changed

+90
-37
lines changed

go/types/objectpath/objectpath.go

+21-18
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,8 @@ func For(obj types.Object) (Path, error) {
121121
// An Encoder amortizes the cost of encoding the paths of multiple objects.
122122
// The zero value of an Encoder is ready to use.
123123
type Encoder struct {
124-
scopeNamesMemo map[*types.Scope][]string // memoization of Scope.Names()
125-
namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods()
124+
scopeMemo map[*types.Scope][]types.Object // memoization of scopeObjects
125+
namedMethodsMemo map[*types.Named][]*types.Func // memoization of namedMethods()
126126
}
127127

128128
// For returns the path to an object relative to its package,
@@ -255,15 +255,14 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
255255
// the best paths because non-types may
256256
// refer to types, but not the reverse.
257257
empty := make([]byte, 0, 48) // initial space
258-
names := enc.scopeNames(scope)
259-
for _, name := range names {
260-
o := scope.Lookup(name)
258+
objs := enc.scopeObjects(scope)
259+
for _, o := range objs {
261260
tname, ok := o.(*types.TypeName)
262261
if !ok {
263262
continue // handle non-types in second pass
264263
}
265264

266-
path := append(empty, name...)
265+
path := append(empty, o.Name()...)
267266
path = append(path, opType)
268267

269268
T := o.Type()
@@ -289,9 +288,8 @@ func (enc *Encoder) For(obj types.Object) (Path, error) {
289288

290289
// Then inspect everything else:
291290
// non-types, and declared methods of defined types.
292-
for _, name := range names {
293-
o := scope.Lookup(name)
294-
path := append(empty, name...)
291+
for _, o := range objs {
292+
path := append(empty, o.Name()...)
295293
if _, ok := o.(*types.TypeName); !ok {
296294
if o.Exported() {
297295
// exported non-type (const, var, func)
@@ -746,17 +744,22 @@ func (enc *Encoder) namedMethods(named *types.Named) []*types.Func {
746744
return methods
747745
}
748746

749-
// scopeNames is a memoization of scope.Names. Callers must not modify the result.
750-
func (enc *Encoder) scopeNames(scope *types.Scope) []string {
751-
m := enc.scopeNamesMemo
747+
// scopeObjects is a memoization of scope objects.
748+
// Callers must not modify the result.
749+
func (enc *Encoder) scopeObjects(scope *types.Scope) []types.Object {
750+
m := enc.scopeMemo
752751
if m == nil {
753-
m = make(map[*types.Scope][]string)
754-
enc.scopeNamesMemo = m
752+
m = make(map[*types.Scope][]types.Object)
753+
enc.scopeMemo = m
755754
}
756-
names, ok := m[scope]
755+
objs, ok := m[scope]
757756
if !ok {
758-
names = scope.Names() // allocates and sorts
759-
m[scope] = names
757+
names := scope.Names() // allocates and sorts
758+
objs = make([]types.Object, len(names))
759+
for i, name := range names {
760+
objs[i] = scope.Lookup(name)
761+
}
762+
m[scope] = objs
760763
}
761-
return names
764+
return objs
762765
}

gopls/internal/regtest/bench/completion_test.go

+69-19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package bench
66

77
import (
88
"fmt"
9+
"sync/atomic"
910
"testing"
1011

1112
"golang.org/x/tools/gopls/internal/lsp/fake"
@@ -145,31 +146,80 @@ func (c *completer) _() {
145146
// Edits force type-checked packages to be invalidated, so we want to measure
146147
// how long it takes before completion results are available.
147148
func BenchmarkCompletionFollowingEdit(b *testing.B) {
148-
file := "internal/lsp/source/completion/completion2.go"
149-
fileContent := `
149+
tests := []struct {
150+
repo string
151+
file string // repo-relative file to create
152+
content string // file content
153+
locationRegexp string // regexp for completion
154+
}{
155+
{
156+
"tools",
157+
"internal/lsp/source/completion/completion2.go",
158+
`
150159
package completion
151160
152161
func (c *completer) _() {
153162
c.inference.kindMatches(c.)
154-
// __MAGIC_STRING_1
155163
}
156-
`
157-
setup := func(env *Env) {
158-
env.CreateBuffer(file, fileContent)
164+
`,
165+
`func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
166+
},
167+
{
168+
"kubernetes",
169+
"pkg/kubelet/kubelet2.go",
170+
`
171+
package kubelet
172+
173+
func (kl *Kubelet) _() {
174+
kl.
175+
}
176+
`,
177+
`kl\.()`,
178+
},
159179
}
160180

161-
n := 1
162-
beforeCompletion := func(env *Env) {
163-
old := fmt.Sprintf("__MAGIC_STRING_%d", n)
164-
new := fmt.Sprintf("__MAGIC_STRING_%d", n+1)
165-
n++
166-
env.RegexpReplace(file, old, new)
167-
}
181+
for _, test := range tests {
182+
b.Run(test.repo, func(b *testing.B) {
183+
repo := getRepo(b, test.repo)
184+
_ = repo.sharedEnv(b) // ensure cache is warm
185+
env := repo.newEnv(b, "completion."+test.repo, fake.EditorConfig{
186+
Settings: map[string]interface{}{
187+
"completeUnimported": false,
188+
},
189+
})
190+
defer env.Close()
191+
192+
env.CreateBuffer(test.file, "// __REGTEST_PLACEHOLDER_0__\n"+test.content)
193+
editPlaceholder := func() {
194+
edits := atomic.AddInt64(&editID, 1)
195+
env.EditBuffer(test.file, protocol.TextEdit{
196+
Range: protocol.Range{
197+
Start: protocol.Position{Line: 0, Character: 0},
198+
End: protocol.Position{Line: 1, Character: 0},
199+
},
200+
// Increment the placeholder text, to ensure cache misses.
201+
NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits),
202+
})
203+
}
204+
env.AfterChange()
168205

169-
benchmarkCompletion(completionBenchOptions{
170-
file: file,
171-
locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
172-
setup: setup,
173-
beforeCompletion: beforeCompletion,
174-
}, b)
206+
// Run a completion to make sure the system is warm.
207+
loc := env.RegexpSearch(test.file, test.locationRegexp)
208+
completions := env.Completion(loc)
209+
210+
if testing.Verbose() {
211+
fmt.Println("Results:")
212+
for i := 0; i < len(completions.Items); i++ {
213+
fmt.Printf("\t%d. %v\n", i, completions.Items[i])
214+
}
215+
}
216+
217+
b.ResetTimer()
218+
for i := 0; i < b.N; i++ {
219+
editPlaceholder()
220+
loc := env.RegexpSearch(test.file, test.locationRegexp)
221+
env.Completion(loc)
222+
}
223+
})
224+
}
175225
}

0 commit comments

Comments
 (0)