internal/lsp/regtest: add benchmarks for IWL and completion
Add additional benchmarks following the pattern of symbol benchmarks.
One for initial workspace load, and another for completion.
Change-Id: Iba826b188cb81dffabb1b08287dc7b76250dc54c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/250802
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/fake/edit.go b/internal/lsp/fake/edit.go
index fb28841..e5be4f6 100644
--- a/internal/lsp/fake/edit.go
+++ b/internal/lsp/fake/edit.go
@@ -25,7 +25,7 @@
End Pos
}
-func (p Pos) toProtocolPosition() protocol.Position {
+func (p Pos) ToProtocolPosition() protocol.Position {
return protocol.Position{
Line: float64(p.Line),
Character: float64(p.Column),
@@ -73,8 +73,8 @@
func (e Edit) toProtocolChangeEvent() protocol.TextDocumentContentChangeEvent {
return protocol.TextDocumentContentChangeEvent{
Range: &protocol.Range{
- Start: e.Start.toProtocolPosition(),
- End: e.End.toProtocolPosition(),
+ Start: e.Start.ToProtocolPosition(),
+ End: e.End.ToProtocolPosition(),
},
Text: e.Text,
}
diff --git a/internal/lsp/fake/editor.go b/internal/lsp/fake/editor.go
index 1a89f9a..0f104e4 100644
--- a/internal/lsp/fake/editor.go
+++ b/internal/lsp/fake/editor.go
@@ -579,7 +579,7 @@
}
params := &protocol.DefinitionParams{}
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- params.Position = pos.toProtocolPosition()
+ params.Position = pos.ToProtocolPosition()
resp, err := e.Server.Definition(ctx, params)
if err != nil {
@@ -802,7 +802,7 @@
params := &protocol.ReferenceParams{
TextDocumentPositionParams: protocol.TextDocumentPositionParams{
TextDocument: e.textDocumentIdentifier(path),
- Position: pos.toProtocolPosition(),
+ Position: pos.ToProtocolPosition(),
},
Context: protocol.ReferenceContext{
IncludeDeclaration: true,
@@ -846,7 +846,7 @@
}
params := &protocol.HoverParams{}
params.TextDocument.URI = e.sandbox.Workdir.URI(path)
- params.Position = pos.toProtocolPosition()
+ params.Position = pos.ToProtocolPosition()
resp, err := e.Server.Hover(ctx, params)
if err != nil {
diff --git a/internal/lsp/regtest/bench_test.go b/internal/lsp/regtest/bench_test.go
index d91c424..763e547 100644
--- a/internal/lsp/regtest/bench_test.go
+++ b/internal/lsp/regtest/bench_test.go
@@ -9,10 +9,38 @@
"fmt"
"testing"
+ "golang.org/x/tools/internal/lsp"
"golang.org/x/tools/internal/lsp/fake"
"golang.org/x/tools/internal/lsp/protocol"
)
+var iwlBench = struct {
+ workdir string
+}{}
+
+func init() {
+ flag.StringVar(&iwlBench.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory")
+}
+
+func TestBenchmarkIWL(t *testing.T) {
+ if iwlBench.workdir == "" {
+ t.Skip("-iwl_workdir not configured")
+ }
+ opts := stressTestOptions(iwlBench.workdir)
+ // Don't skip hooks, so that we can wait for IWL.
+ opts = append(opts, SkipHooks(false))
+ b := testing.Benchmark(func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+ env.Await(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+ )
+ })
+ }
+ })
+ printBench(b)
+}
+
var symbolBench = struct {
workdir, query, matcher, style string
printResults bool
@@ -23,7 +51,7 @@
flag.StringVar(&symbolBench.query, "symbol_query", "test", "symbol query to use in benchmark")
flag.StringVar(&symbolBench.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark")
flag.StringVar(&symbolBench.style, "symbol_style", "", "symbol style to use in benchmark")
- flag.BoolVar(&symbolBench.printResults, "symbol_print_results", false, "symbol style to use in benchmark")
+ flag.BoolVar(&symbolBench.printResults, "symbol_print_results", false, "whether to print symbol query results")
}
func TestBenchmarkSymbols(t *testing.T) {
@@ -64,8 +92,68 @@
}
}
})
- fmt.Println("Benchmark stats:")
- fmt.Println(b.String())
- fmt.Println(b.MemString())
+ printBench(b)
+ })
+}
+
+func printBench(b testing.BenchmarkResult) {
+ fmt.Println("Benchmark stats:")
+ fmt.Println(b.String())
+ fmt.Println(b.MemString())
+}
+
+func dummyCompletionBenchmarkFunction() { const s = "placeholder"; fmt.Printf("%s", s) }
+
+var completionBench = struct {
+ workdir, fileName, locationRegexp string
+ printResults bool
+}{}
+
+func init() {
+ flag.StringVar(&completionBench.workdir, "completion_workdir", "", "if set run completion benchmark in this directory (other benchmark flags expect an x/tools dir)")
+ flag.StringVar(&completionBench.fileName, "completion_file", "internal/lsp/regtest/bench_test.go", "relative path to the file to complete")
+ flag.StringVar(&completionBench.locationRegexp, "completion_regexp", `dummyCompletionBenchmarkFunction.*fmt\.Printf\("%s", s(\))`, "regexp location to complete at")
+ flag.BoolVar(&completionBench.printResults, "completion_print_results", false, "whether to print completion results")
+}
+
+func TestBenchmarkCompletion(t *testing.T) {
+ if completionBench.workdir == "" {
+ t.Skip("-completion_workdir not configured")
+ }
+ opts := stressTestOptions(completionBench.workdir)
+ // Completion gives bad results if IWL is not yet complete, so we must await
+ // it first (and therefore need hooks).
+ opts = append(opts, SkipHooks(false))
+ withOptions(opts...).run(t, "", func(t *testing.T, env *Env) {
+ env.Await(
+ CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1),
+ )
+ env.OpenFile(completionBench.fileName)
+ params := &protocol.CompletionParams{}
+ params.Context.TriggerCharacter = "s"
+ params.Context.TriggerKind = protocol.TriggerCharacter
+ params.TextDocument.URI = env.Sandbox.Workdir.URI(completionBench.fileName)
+ params.Position = env.RegexpSearch(completionBench.fileName, completionBench.locationRegexp).ToProtocolPosition()
+
+ // Run one completion to make sure everything is warm.
+ list, err := env.Editor.Server.Completion(env.Ctx, params)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if completionBench.printResults {
+ fmt.Println("Results:")
+ for i := 0; i < len(list.Items); i++ {
+ fmt.Printf("\t%d. %v\n", i, list.Items[i])
+ }
+ }
+ b := testing.Benchmark(func(b *testing.B) {
+ for i := 0; i < b.N; i++ {
+ _, err := env.Editor.Server.Completion(env.Ctx, params)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ })
+ printBench(b)
})
}
diff --git a/internal/lsp/regtest/runner.go b/internal/lsp/regtest/runner.go
index 1b042f5..15bac64 100644
--- a/internal/lsp/regtest/runner.go
+++ b/internal/lsp/regtest/runner.go
@@ -176,12 +176,12 @@
})
}
-// NoHooks disables the test runner's client hooks that are used for
-// instrumenting expectations (tracking diagnostics, logs, work done, etc.). It
-// is intended for performance-sensitive stress tests.
-func NoHooks() RunOption {
+// SkipHooks allows for disabling the test runner's client hooks that are used
+// for instrumenting expectations (tracking diagnostics, logs, work done,
+// etc.). It is intended for performance-sensitive stress tests or benchmarks.
+func SkipHooks(skip bool) RunOption {
return optionSetter(func(opts *runConfig) {
- opts.skipHooks = true
+ opts.skipHooks = skip
})
}
diff --git a/internal/lsp/regtest/stress_test.go b/internal/lsp/regtest/stress_test.go
index 55e81ae..b3f3a3b 100644
--- a/internal/lsp/regtest/stress_test.go
+++ b/internal/lsp/regtest/stress_test.go
@@ -29,9 +29,10 @@
// Enable live debugging.
WithDebugAddress(":8087"),
- // Skip logs and hooks, as they buffer up memory unnaturally.
+ // Skip logs as they buffer up memory unnaturally.
SkipLogs(),
- NoHooks(),
+ // Similarly to logs: disable hooks so that they don't affect performance.
+ SkipHooks(true),
// The Debug server only makes sense if running in singleton mode.
WithModes(Singleton),
// Set a generous timeout. Individual tests should control their own