gopls/internal/regtest/bench: add a test for completion following edits

For golang/go#53992

Change-Id: Ia1f1e27663992707eef9226273b152117ee977ac
Reviewed-on: https://go-review.googlesource.com/c/tools/+/420220
Run-TryBot: Robert Findley <rfindley@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Peter Weinberger <pjw@google.com>
This commit is contained in:
Robert Findley 2022-07-29 17:27:52 -04:00
parent 81c7dc4e4e
commit 06d96ee8fc
4 changed files with 69 additions and 23 deletions

View File

@ -133,7 +133,7 @@ func benchmarkEnv(tb testing.TB) *Env {
dir := benchmarkDir()
var err error
sandbox, editor, awaiter, err = connectEditor(dir)
sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{})
if err != nil {
log.Fatalf("connecting editor: %v", err)
}
@ -154,7 +154,7 @@ func benchmarkEnv(tb testing.TB) *Env {
// connectEditor connects a fake editor session in the given dir, using the
// given editor config.
func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) {
func connectEditor(dir string, config fake.EditorConfig) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) {
s, err := fake.NewSandbox(&fake.SandboxConfig{
Workdir: dir,
GOPROXY: "https://proxy.golang.org",
@ -165,7 +165,7 @@ func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, e
a := regtest.NewAwaiter(s.Workdir)
ts := getServer()
e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(context.Background(), ts, a.Hooks())
e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks())
if err != nil {
return nil, nil, nil, err
}

View File

@ -18,8 +18,9 @@ import (
type completionBenchOptions struct {
file, locationRegexp string
// hook to run edits before initial completion
preCompletionEdits func(*Env)
// Hooks to run edits before initial completion
setup func(*Env) // run before the benchmark starts
beforeCompletion func(*Env) // run before each completion
}
func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
@ -27,7 +28,11 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
// Use a new environment for each test, to avoid any existing state from the
// previous session.
sandbox, editor, awaiter, err := connectEditor(dir)
sandbox, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{
Settings: map[string]interface{}{
"completionBudget": "1m", // arbitrary long completion budget
},
})
if err != nil {
b.Fatal(err)
}
@ -45,11 +50,10 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
Sandbox: sandbox,
Awaiter: awaiter,
}
env.OpenFile(options.file)
// Run edits required for this completion.
if options.preCompletionEdits != nil {
options.preCompletionEdits(env)
if options.setup != nil {
options.setup(env)
}
// Run a completion to make sure the system is warm.
@ -70,6 +74,9 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
// initialization).
b.Run("completion", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if options.beforeCompletion != nil {
options.beforeCompletion(env)
}
env.Completion(options.file, pos)
}
})
@ -92,7 +99,7 @@ func endPosInBuffer(env *Env, name string) fake.Pos {
func BenchmarkStructCompletion(b *testing.B) {
file := "internal/lsp/cache/session.go"
preCompletionEdits := func(env *Env) {
setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@ -102,17 +109,19 @@ func BenchmarkStructCompletion(b *testing.B) {
}
benchmarkCompletion(completionBenchOptions{
file: file,
locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`,
preCompletionEdits: preCompletionEdits,
file: file,
locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`,
setup: setup,
}, b)
}
// Benchmark import completion in tools codebase.
func BenchmarkImportCompletion(b *testing.B) {
const file = "internal/lsp/source/completion/completion.go"
benchmarkCompletion(completionBenchOptions{
file: "internal/lsp/source/completion/completion.go",
file: file,
locationRegexp: `go\/()`,
setup: func(env *Env) { env.OpenFile(file) },
}, b)
}
@ -120,7 +129,7 @@ func BenchmarkImportCompletion(b *testing.B) {
func BenchmarkSliceCompletion(b *testing.B) {
file := "internal/lsp/cache/session.go"
preCompletionEdits := func(env *Env) {
setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@ -130,9 +139,9 @@ func BenchmarkSliceCompletion(b *testing.B) {
}
benchmarkCompletion(completionBenchOptions{
file: file,
locationRegexp: `var testVariable \[\]byte (=)`,
preCompletionEdits: preCompletionEdits,
file: file,
locationRegexp: `var testVariable \[\]byte (=)`,
setup: setup,
}, b)
}
@ -144,7 +153,7 @@ func (c *completer) _() {
c.inference.kindMatches(c.)
}
`
preCompletionEdits := func(env *Env) {
setup := func(env *Env) {
env.OpenFile(file)
originalBuffer := env.Editor.BufferText(file)
env.EditBuffer(file, fake.Edit{
@ -154,8 +163,42 @@ func (c *completer) _() {
}
benchmarkCompletion(completionBenchOptions{
file: file,
locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
preCompletionEdits: preCompletionEdits,
file: file,
locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
setup: setup,
}, b)
}
// Benchmark completion following an arbitrary edit.
//
// Edits force type-checked packages to be invalidated, so we want to measure
// how long it takes before completion results are available.
func BenchmarkCompletionFollowingEdit(b *testing.B) {
file := "internal/lsp/source/completion/completion2.go"
fileContent := `
package completion
func (c *completer) _() {
c.inference.kindMatches(c.)
// __MAGIC_STRING_1
}
`
setup := func(env *Env) {
env.CreateBuffer(file, fileContent)
}
n := 1
beforeCompletion := func(env *Env) {
old := fmt.Sprintf("__MAGIC_STRING_%d", n)
new := fmt.Sprintf("__MAGIC_STRING_%d", n+1)
n++
env.RegexpReplace(file, old, new)
}
benchmarkCompletion(completionBenchOptions{
file: file,
locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
setup: setup,
beforeCompletion: beforeCompletion,
}, b)
}

View File

@ -8,6 +8,7 @@ import (
"context"
"testing"
"golang.org/x/tools/internal/lsp/fake"
. "golang.org/x/tools/internal/lsp/regtest"
)
@ -19,7 +20,7 @@ func BenchmarkIWL(b *testing.B) {
ctx := context.Background()
for i := 0; i < b.N; i++ {
_, editor, awaiter, err := connectEditor(dir)
_, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{})
if err != nil {
b.Fatal(err)
}

View File

@ -108,6 +108,8 @@ func inText(p Pos, content []string) bool {
// editContent implements a simplistic, inefficient algorithm for applying text
// edits to our buffer representation. It returns an error if the edit is
// invalid for the current content.
//
// TODO(rfindley): this function does not handle non-ascii text correctly.
func editContent(content []string, edits []Edit) ([]string, error) {
newEdits := make([]Edit, len(edits))
copy(newEdits, edits)