From 06d96ee8fcfeb1083837cc5c3459f85e0c225fa2 Mon Sep 17 00:00:00 2001 From: Robert Findley Date: Fri, 29 Jul 2022 17:27:52 -0400 Subject: [PATCH] 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 gopls-CI: kokoro TryBot-Result: Gopher Robot Reviewed-by: Peter Weinberger --- gopls/internal/regtest/bench/bench_test.go | 6 +- .../internal/regtest/bench/completion_test.go | 81 ++++++++++++++----- gopls/internal/regtest/bench/iwl_test.go | 3 +- internal/lsp/fake/edit.go | 2 + 4 files changed, 69 insertions(+), 23 deletions(-) diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go index a3780f02d8..cfe4db6648 100644 --- a/gopls/internal/regtest/bench/bench_test.go +++ b/gopls/internal/regtest/bench/bench_test.go @@ -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 } diff --git a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go index cdafb08092..a8725cee3d 100644 --- a/gopls/internal/regtest/bench/completion_test.go +++ b/gopls/internal/regtest/bench/completion_test.go @@ -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) } diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go index e262a398f1..b223e336db 100644 --- a/gopls/internal/regtest/bench/iwl_test.go +++ b/gopls/internal/regtest/bench/iwl_test.go @@ -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) } diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go index 8b04c390fc..579c3a18de 100644 --- a/internal/lsp/fake/edit.go +++ b/internal/lsp/fake/edit.go @@ -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)