Skip to content

Commit bc2e2c2

Browse files
committed
gopls/internal/regtest/bench: support benchmarking multiple repos
Extract benchmark state into a new repo type, so that we may run benchmarks in multiple shared workspaces. Also, add missing cleanup code. Additionally, simplify to always run gopls in a separate process. This means that the normal test profiling flags won't be useful, so add support for threading through profiling flags to the external gopls process. For golang/go#53538 Change-Id: Ib9ab5920dc59f102c62b53b761379dd8ca2d7141 Reviewed-on: https://go-review.googlesource.com/c/tools/+/468940 TryBot-Result: Gopher Robot <[email protected]> gopls-CI: kokoro <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Run-TryBot: Robert Findley <[email protected]>
1 parent 7c35ddf commit bc2e2c2

12 files changed

+304
-214
lines changed

gopls/internal/regtest/bench/bench_test.go

Lines changed: 80 additions & 168 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ import (
1818
"time"
1919

2020
"golang.org/x/tools/gopls/internal/hooks"
21-
"golang.org/x/tools/gopls/internal/lsp/cache"
2221
"golang.org/x/tools/gopls/internal/lsp/cmd"
2322
"golang.org/x/tools/gopls/internal/lsp/fake"
24-
"golang.org/x/tools/gopls/internal/lsp/lsprpc"
2523
"golang.org/x/tools/internal/bug"
2624
"golang.org/x/tools/internal/event"
2725
"golang.org/x/tools/internal/fakenet"
@@ -32,15 +30,21 @@ import (
3230
. "golang.org/x/tools/gopls/internal/lsp/regtest"
3331
)
3432

35-
// This package implements benchmarks that share a common editor session.
36-
//
37-
// It is a work-in-progress.
38-
//
39-
// Remaining TODO(rfindley):
40-
// - add detailed documentation for how to write a benchmark, as a package doc
41-
// - add benchmarks for more features
42-
// - eliminate flags, and just run benchmarks on with a predefined set of
43-
// arguments
33+
var (
34+
goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit")
35+
36+
installGoplsOnce sync.Once // guards installing gopls at -gopls_commit
37+
goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path")
38+
39+
cpuProfile = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc")
40+
memProfile = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc")
41+
trace = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc")
42+
43+
// If non-empty, tempDir is a temporary working dir that was created by this
44+
// test suite.
45+
makeTempDirOnce sync.Once // guards creation of the temp dir
46+
tempDir string
47+
)
4448

4549
// if runAsGopls is "true", run the gopls command instead of the testing.M.
4650
const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS"
@@ -52,56 +56,16 @@ func TestMain(m *testing.M) {
5256
os.Exit(0)
5357
}
5458
event.SetExporter(nil) // don't log to stderr
55-
code := doMain(m)
56-
os.Exit(code)
57-
}
58-
59-
func doMain(m *testing.M) (code int) {
60-
defer func() {
61-
if editor != nil {
62-
if err := editor.Close(context.Background()); err != nil {
63-
fmt.Fprintf(os.Stderr, "closing editor: %v", err)
64-
if code == 0 {
65-
code = 1
66-
}
67-
}
68-
}
69-
if tempDir != "" {
70-
if err := os.RemoveAll(tempDir); err != nil {
71-
fmt.Fprintf(os.Stderr, "cleaning temp dir: %v", err)
72-
if code == 0 {
73-
code = 1
74-
}
75-
}
59+
code := m.Run()
60+
if err := cleanup(); err != nil {
61+
fmt.Fprintf(os.Stderr, "cleaning up after benchmarks: %v\n", err)
62+
if code == 0 {
63+
code = 1
7664
}
77-
}()
78-
return m.Run()
65+
}
66+
os.Exit(code)
7967
}
8068

81-
var (
82-
workdir = flag.String("workdir", "", "if set, working directory to use for benchmarks; overrides -repo and -commit")
83-
repo = flag.String("repo", "https://go.googlesource.com/tools", "if set (and -workdir is unset), run benchmarks in this repo")
84-
file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file")
85-
commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit")
86-
87-
goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit")
88-
goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path")
89-
90-
// If non-empty, tempDir is a temporary working dir that was created by this
91-
// test suite.
92-
//
93-
// The sync.Once variables guard various modifications of the temp directory.
94-
makeTempDirOnce sync.Once
95-
checkoutRepoOnce sync.Once
96-
installGoplsOnce sync.Once
97-
tempDir string
98-
99-
setupEditorOnce sync.Once
100-
sandbox *fake.Sandbox
101-
editor *fake.Editor
102-
awaiter *Awaiter
103-
)
104-
10569
// getTempDir returns the temporary directory to use for benchmark files,
10670
// creating it if necessary.
10771
func getTempDir() string {
@@ -115,31 +79,6 @@ func getTempDir() string {
11579
return tempDir
11680
}
11781

118-
// benchmarkDir returns the directory to use for benchmarks.
119-
//
120-
// If -workdir is set, just use that directory. Otherwise, check out a shallow
121-
// copy of -repo at the given -commit, and clean up when the test suite exits.
122-
func benchmarkDir() string {
123-
if *workdir != "" {
124-
return *workdir
125-
}
126-
if *repo == "" {
127-
log.Fatal("-repo must be provided if -workdir is unset")
128-
}
129-
if *commitish == "" {
130-
log.Fatal("-commit must be provided if -workdir is unset")
131-
}
132-
133-
dir := filepath.Join(getTempDir(), "repo")
134-
checkoutRepoOnce.Do(func() {
135-
log.Printf("creating working dir: checking out %s@%s to %s\n", *repo, *commitish, dir)
136-
if err := shallowClone(dir, *repo, *commitish); err != nil {
137-
log.Fatal(err)
138-
}
139-
})
140-
return dir
141-
}
142-
14382
// shallowClone performs a shallow clone of repo into dir at the given
14483
// 'commitish' ref (any commit reference understood by git).
14584
//
@@ -163,70 +102,6 @@ func shallowClone(dir, repo, commitish string) error {
163102
return nil
164103
}
165104

166-
// sharedEnv returns a shared benchmark environment.
167-
//
168-
// Every call to sharedEnv uses the same editor and sandbox. If -gopls_path and
169-
// -gopls_commit are unset, this environment will run gopls in-process.
170-
func sharedEnv(tb testing.TB) *Env {
171-
setupEditorOnce.Do(func() {
172-
dir := benchmarkDir()
173-
174-
var err error
175-
ts := getServer()
176-
sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{}, ts)
177-
if err != nil {
178-
log.Fatalf("connecting editor: %v", err)
179-
}
180-
181-
if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
182-
panic(err)
183-
}
184-
})
185-
186-
return &Env{
187-
T: tb,
188-
Ctx: context.Background(),
189-
Editor: editor,
190-
Sandbox: sandbox,
191-
Awaiter: awaiter,
192-
}
193-
}
194-
195-
// newEnv returns a new Env connected to separate gopls process communicating
196-
// over stdin/stdout.
197-
//
198-
// Every call to newEnv returns a different Env connected to a distinct gopls
199-
// process.
200-
//
201-
// TODO(rfindley): consolidate gopls server construction: always use a sidecar,
202-
// and make it easy to collect profiles.
203-
func newEnv(dir string, tb testing.TB) *Env {
204-
goplsPath := getGoplsPath()
205-
if goplsPath == "" {
206-
var err error
207-
goplsPath, err = os.Executable()
208-
if err != nil {
209-
tb.Fatal(err)
210-
}
211-
}
212-
ts := &SidecarServer{
213-
goplsPath: goplsPath,
214-
env: []string{fmt.Sprintf("%s=true", runAsGopls)},
215-
}
216-
server, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{}, ts)
217-
if err != nil {
218-
tb.Fatalf("connecting editor: %v", err)
219-
}
220-
221-
return &Env{
222-
T: tb,
223-
Ctx: context.Background(),
224-
Editor: editor,
225-
Sandbox: server,
226-
Awaiter: awaiter,
227-
}
228-
}
229-
230105
// connectEditor connects a fake editor session in the given dir, using the
231106
// given editor config.
232107
func connectEditor(dir string, config fake.EditorConfig, ts servertest.Connector) (*fake.Sandbox, *fake.Editor, *Awaiter, error) {
@@ -246,30 +121,41 @@ func connectEditor(dir string, config fake.EditorConfig, ts servertest.Connector
246121
return s, e, a, nil
247122
}
248123

249-
// getServer returns a server connector that either starts a new in-process
250-
// server, or starts a separate gopls process.
251-
func getServer() servertest.Connector {
124+
// newGoplsServer returns a connector that connects to a new gopls process.
125+
func newGoplsServer(name string) (servertest.Connector, error) {
252126
if *goplsPath != "" && *goplsCommit != "" {
253127
panic("can't set both -gopls_path and -gopls_commit")
254128
}
255-
if path := getGoplsPath(); path != "" {
256-
return &SidecarServer{goplsPath: *goplsPath}
129+
var (
130+
goplsPath = *goplsPath
131+
env []string
132+
)
133+
if *goplsCommit != "" {
134+
goplsPath = getInstalledGopls()
257135
}
258-
server := lsprpc.NewStreamServer(cache.New(nil, nil), false, hooks.Options)
259-
return servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
260-
}
261-
262-
// getGoplsPath returns the path to the external gopls binary to use for
263-
// benchmarks, or the empty string if no external gopls is configured via
264-
// -gopls_path or -gopls_commit.
265-
func getGoplsPath() string {
266-
if *goplsPath != "" {
267-
return *goplsPath
136+
if goplsPath == "" {
137+
var err error
138+
goplsPath, err = os.Executable()
139+
if err != nil {
140+
return nil, err
141+
}
142+
env = []string{fmt.Sprintf("%s=true", runAsGopls)}
268143
}
269-
if *goplsCommit != "" {
270-
return getInstalledGopls()
144+
var args []string
145+
if *cpuProfile != "" {
146+
args = append(args, fmt.Sprintf("-profile.cpu=%s", name+"."+*cpuProfile))
147+
}
148+
if *memProfile != "" {
149+
args = append(args, fmt.Sprintf("-profile.mem=%s", name+"."+*memProfile))
271150
}
272-
return ""
151+
if *trace != "" {
152+
args = append(args, fmt.Sprintf("-profile.trace=%s", name+"."+*trace))
153+
}
154+
return &SidecarServer{
155+
goplsPath: goplsPath,
156+
env: env,
157+
args: args,
158+
}, nil
273159
}
274160

275161
// getInstalledGopls builds gopls at the given -gopls_commit, returning the
@@ -307,11 +193,18 @@ func getInstalledGopls() string {
307193
type SidecarServer struct {
308194
goplsPath string
309195
env []string // additional environment bindings
196+
args []string // command-line arguments
310197
}
311198

312199
// Connect creates new io.Pipes and binds them to the underlying StreamServer.
200+
//
201+
// It implements the servertest.Connector interface.
313202
func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
314-
cmd := exec.CommandContext(ctx, s.goplsPath, "serve")
203+
// Note: don't use CommandContext here, as we want gopls to exit gracefully
204+
// in order to write out profile data.
205+
//
206+
// We close the connection on context cancelation below.
207+
cmd := exec.Command(s.goplsPath, s.args...)
315208

316209
stdin, err := cmd.StdinPipe()
317210
if err != nil {
@@ -321,15 +214,34 @@ func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
321214
if err != nil {
322215
log.Fatal(err)
323216
}
324-
cmd.Stderr = os.Stdout
217+
cmd.Stderr = os.Stderr
325218
cmd.Env = append(os.Environ(), s.env...)
326219
if err := cmd.Start(); err != nil {
327220
log.Fatalf("starting gopls: %v", err)
328221
}
329222

330-
go cmd.Wait() // to free resources; error is ignored
223+
go func() {
224+
// If we don't log.Fatal here, benchmarks may hang indefinitely if gopls
225+
// exits abnormally.
226+
//
227+
// TODO(rfindley): ideally we would shut down the connection gracefully,
228+
// but that doesn't currently work.
229+
if err := cmd.Wait(); err != nil {
230+
log.Fatalf("gopls invocation failed with error: %v", err)
231+
}
232+
}()
331233

332234
clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin))
333235
clientConn := jsonrpc2.NewConn(clientStream)
236+
237+
go func() {
238+
select {
239+
case <-ctx.Done():
240+
clientConn.Close()
241+
clientStream.Close()
242+
case <-clientConn.Done():
243+
}
244+
}()
245+
334246
return clientConn
335247
}

gopls/internal/regtest/bench/completion_test.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,11 @@
55
package bench
66

77
import (
8-
"context"
98
"fmt"
109
"testing"
1110

1211
"golang.org/x/tools/gopls/internal/lsp/protocol"
1312
. "golang.org/x/tools/gopls/internal/lsp/regtest"
14-
15-
"golang.org/x/tools/gopls/internal/lsp/fake"
1613
)
1714

1815
type completionBenchOptions struct {
@@ -24,32 +21,9 @@ type completionBenchOptions struct {
2421
}
2522

2623
func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
27-
dir := benchmarkDir()
28-
29-
// Use a new environment for each test, to avoid any existing state from the
30-
// previous session.
31-
sandbox, editor, awaiter, err := connectEditor(dir, fake.EditorConfig{
32-
Settings: map[string]interface{}{
33-
"completionBudget": "1m", // arbitrary long completion budget
34-
},
35-
}, getServer())
36-
if err != nil {
37-
b.Fatal(err)
38-
}
39-
ctx := context.Background()
40-
defer func() {
41-
if err := editor.Close(ctx); err != nil {
42-
b.Errorf("closing editor: %v", err)
43-
}
44-
}()
45-
46-
env := &Env{
47-
T: b,
48-
Ctx: ctx,
49-
Editor: editor,
50-
Sandbox: sandbox,
51-
Awaiter: awaiter,
52-
}
24+
repo := repos["tools"]
25+
env := repo.newEnv(b)
26+
defer env.Close()
5327

5428
// Run edits required for this completion.
5529
if options.setup != nil {

gopls/internal/regtest/bench/definition_test.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
package bench
66

7-
import "testing"
7+
import (
8+
"testing"
9+
)
810

9-
func BenchmarkGoToDefinition(b *testing.B) {
10-
env := sharedEnv(b)
11+
func BenchmarkDefinition(b *testing.B) {
12+
env := repos["tools"].sharedEnv(b)
1113

1214
env.OpenFile("internal/imports/mod.go")
1315
loc := env.RegexpSearch("internal/imports/mod.go", "ModuleJSON")

0 commit comments

Comments
 (0)