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>
diff --git a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go
index a3780f0..cfe4db6 100644
--- a/gopls/internal/regtest/bench/bench_test.go
+++ b/gopls/internal/regtest/bench/bench_test.go
@@ -133,7 +133,7 @@
 		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 @@
 
 // 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 @@
 
 	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 cdafb08..a8725ce 100644
--- a/gopls/internal/regtest/bench/completion_test.go
+++ b/gopls/internal/regtest/bench/completion_test.go
@@ -18,8 +18,9 @@
 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 @@
 
 	// 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 @@
 		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 @@
 	// 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 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 @@
 	}
 
 	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 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 @@
 	}
 
 	benchmarkCompletion(completionBenchOptions{
-		file:               file,
-		locationRegexp:     `var testVariable \[\]byte (=)`,
-		preCompletionEdits: preCompletionEdits,
+		file:           file,
+		locationRegexp: `var testVariable \[\]byte (=)`,
+		setup:          setup,
 	}, b)
 }
 
@@ -144,7 +153,7 @@
 	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 @@
 	}
 
 	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 e262a39..b223e33 100644
--- a/gopls/internal/regtest/bench/iwl_test.go
+++ b/gopls/internal/regtest/bench/iwl_test.go
@@ -8,6 +8,7 @@
 	"context"
 	"testing"
 
+	"golang.org/x/tools/internal/lsp/fake"
 	. "golang.org/x/tools/internal/lsp/regtest"
 )
 
@@ -19,7 +20,7 @@
 
 	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 8b04c39..579c3a1 100644
--- a/internal/lsp/fake/edit.go
+++ b/internal/lsp/fake/edit.go
@@ -108,6 +108,8 @@
 // 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)