1// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package bench
6
7import (
8	"flag"
9	"fmt"
10	"testing"
11	"time"
12
13	"golang.org/x/tools/gopls/internal/hooks"
14	"golang.org/x/tools/internal/lsp/fake"
15	. "golang.org/x/tools/internal/lsp/regtest"
16
17	"golang.org/x/tools/internal/lsp/protocol"
18)
19
20func TestMain(m *testing.M) {
21	Main(m, hooks.Options)
22}
23
24func benchmarkOptions(dir string) []RunOption {
25	return []RunOption{
26		// Run in an existing directory, since we're trying to simulate known cases
27		// that cause gopls memory problems.
28		InExistingDir(dir),
29		// Skip logs as they buffer up memory unnaturally.
30		SkipLogs(),
31		// The Debug server only makes sense if running in singleton mode.
32		Modes(Singleton),
33		// Set a generous timeout. Individual tests should control their own
34		// graceful termination.
35		Timeout(20 * time.Minute),
36
37		// Use the actual proxy, since we want our builds to succeed.
38		GOPROXY("https://proxy.golang.org"),
39	}
40}
41
42func printBenchmarkResults(result testing.BenchmarkResult) {
43	fmt.Println("Benchmark Statistics:")
44	fmt.Println(result.String())
45	fmt.Println(result.MemString())
46}
47
48var iwlOptions struct {
49	workdir string
50}
51
52func init() {
53	flag.StringVar(&iwlOptions.workdir, "iwl_workdir", "", "if set, run IWL benchmark in this directory")
54}
55
56func TestBenchmarkIWL(t *testing.T) {
57	if iwlOptions.workdir == "" {
58		t.Skip("-iwl_workdir not configured")
59	}
60
61	opts := stressTestOptions(iwlOptions.workdir)
62	// Don't skip hooks, so that we can wait for IWL.
63	opts = append(opts, SkipHooks(false))
64
65	results := testing.Benchmark(func(b *testing.B) {
66		for i := 0; i < b.N; i++ {
67			WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {})
68		}
69	})
70
71	printBenchmarkResults(results)
72}
73
74var symbolOptions struct {
75	workdir, query, matcher, style string
76	printResults                   bool
77}
78
79func init() {
80	flag.StringVar(&symbolOptions.workdir, "symbol_workdir", "", "if set, run symbol benchmark in this directory")
81	flag.StringVar(&symbolOptions.query, "symbol_query", "test", "symbol query to use in benchmark")
82	flag.StringVar(&symbolOptions.matcher, "symbol_matcher", "", "symbol matcher to use in benchmark")
83	flag.StringVar(&symbolOptions.style, "symbol_style", "", "symbol style to use in benchmark")
84	flag.BoolVar(&symbolOptions.printResults, "symbol_print_results", false, "whether to print symbol query results")
85}
86
87func TestBenchmarkSymbols(t *testing.T) {
88	if symbolOptions.workdir == "" {
89		t.Skip("-symbol_workdir not configured")
90	}
91
92	opts := stressTestOptions(symbolOptions.workdir)
93	conf := EditorConfig{}
94	if symbolOptions.matcher != "" {
95		conf.SymbolMatcher = &symbolOptions.matcher
96	}
97	if symbolOptions.style != "" {
98		conf.SymbolStyle = &symbolOptions.style
99	}
100	opts = append(opts, conf)
101
102	WithOptions(opts...).Run(t, "", func(t *testing.T, env *Env) {
103		// We can't Await in this test, since we have disabled hooks. Instead, run
104		// one symbol request to completion to ensure all necessary cache entries
105		// are populated.
106		symbols, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{
107			Query: symbolOptions.query,
108		})
109		if err != nil {
110			t.Fatal(err)
111		}
112
113		if symbolOptions.printResults {
114			fmt.Println("Results:")
115			for i := 0; i < len(symbols); i++ {
116				fmt.Printf("\t%d. %s (%s)\n", i, symbols[i].Name, symbols[i].ContainerName)
117			}
118		}
119
120		results := testing.Benchmark(func(b *testing.B) {
121			for i := 0; i < b.N; i++ {
122				if _, err := env.Editor.Server.Symbol(env.Ctx, &protocol.WorkspaceSymbolParams{
123					Query: symbolOptions.query,
124				}); err != nil {
125					t.Fatal(err)
126				}
127			}
128		})
129		printBenchmarkResults(results)
130	})
131}
132
133var (
134	benchDir  = flag.String("didchange_dir", "", "If set, run benchmarks in this dir. Must also set regtest_bench_file.")
135	benchFile = flag.String("didchange_file", "", "The file to modify")
136)
137
138// TestBenchmarkDidChange benchmarks modifications of a single file by making
139// synthetic modifications in a comment. It controls pacing by waiting for the
140// server to actually start processing the didChange notification before
141// proceeding. Notably it does not wait for diagnostics to complete.
142//
143// Run it by passing -didchange_dir and -didchange_file, where -didchange_dir
144// is the path to a workspace root, and -didchange_file is the
145// workspace-relative path to a file to modify. e.g.:
146//
147//  go test -run=TestBenchmarkDidChange \
148//   -didchange_dir=path/to/kubernetes \
149//   -didchange_file=pkg/util/hash/hash.go
150func TestBenchmarkDidChange(t *testing.T) {
151	if *benchDir == "" {
152		t.Skip("-didchange_dir is not set")
153	}
154	if *benchFile == "" {
155		t.Fatal("-didchange_file must be set if -didchange_dir is set")
156	}
157
158	opts := benchmarkOptions(*benchDir)
159	WithOptions(opts...).Run(t, "", func(_ *testing.T, env *Env) {
160		env.OpenFile(*benchFile)
161		env.Await(env.DoneWithOpen())
162		// Insert the text we'll be modifying at the top of the file.
163		env.EditBuffer(*benchFile, fake.Edit{Text: "// __REGTEST_PLACEHOLDER_0__\n"})
164		result := testing.Benchmark(func(b *testing.B) {
165			b.ResetTimer()
166			for i := 0; i < b.N; i++ {
167				env.EditBuffer(*benchFile, fake.Edit{
168					Start: fake.Pos{Line: 0, Column: 0},
169					End:   fake.Pos{Line: 1, Column: 0},
170					// Increment
171					Text: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", i+1),
172				})
173				env.Await(StartedChange(uint64(i + 1)))
174			}
175			b.StopTimer()
176		})
177		printBenchmarkResults(result)
178	})
179}
180