@@ -18,10 +18,8 @@ import (
18
18
"time"
19
19
20
20
"golang.org/x/tools/gopls/internal/hooks"
21
- "golang.org/x/tools/gopls/internal/lsp/cache"
22
21
"golang.org/x/tools/gopls/internal/lsp/cmd"
23
22
"golang.org/x/tools/gopls/internal/lsp/fake"
24
- "golang.org/x/tools/gopls/internal/lsp/lsprpc"
25
23
"golang.org/x/tools/internal/bug"
26
24
"golang.org/x/tools/internal/event"
27
25
"golang.org/x/tools/internal/fakenet"
@@ -32,15 +30,21 @@ import (
32
30
. "golang.org/x/tools/gopls/internal/lsp/regtest"
33
31
)
34
32
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
+ )
44
48
45
49
// if runAsGopls is "true", run the gopls command instead of the testing.M.
46
50
const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS"
@@ -52,56 +56,16 @@ func TestMain(m *testing.M) {
52
56
os .Exit (0 )
53
57
}
54
58
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
76
64
}
77
- }()
78
- return m . Run ( )
65
+ }
66
+ os . Exit ( code )
79
67
}
80
68
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
-
105
69
// getTempDir returns the temporary directory to use for benchmark files,
106
70
// creating it if necessary.
107
71
func getTempDir () string {
@@ -115,31 +79,6 @@ func getTempDir() string {
115
79
return tempDir
116
80
}
117
81
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
-
143
82
// shallowClone performs a shallow clone of repo into dir at the given
144
83
// 'commitish' ref (any commit reference understood by git).
145
84
//
@@ -163,70 +102,6 @@ func shallowClone(dir, repo, commitish string) error {
163
102
return nil
164
103
}
165
104
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
-
230
105
// connectEditor connects a fake editor session in the given dir, using the
231
106
// given editor config.
232
107
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
246
121
return s , e , a , nil
247
122
}
248
123
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 ) {
252
126
if * goplsPath != "" && * goplsCommit != "" {
253
127
panic ("can't set both -gopls_path and -gopls_commit" )
254
128
}
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 ()
257
135
}
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 )}
268
143
}
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 ))
271
150
}
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
273
159
}
274
160
275
161
// getInstalledGopls builds gopls at the given -gopls_commit, returning the
@@ -307,11 +193,18 @@ func getInstalledGopls() string {
307
193
type SidecarServer struct {
308
194
goplsPath string
309
195
env []string // additional environment bindings
196
+ args []string // command-line arguments
310
197
}
311
198
312
199
// Connect creates new io.Pipes and binds them to the underlying StreamServer.
200
+ //
201
+ // It implements the servertest.Connector interface.
313
202
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 ... )
315
208
316
209
stdin , err := cmd .StdinPipe ()
317
210
if err != nil {
@@ -321,15 +214,34 @@ func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
321
214
if err != nil {
322
215
log .Fatal (err )
323
216
}
324
- cmd .Stderr = os .Stdout
217
+ cmd .Stderr = os .Stderr
325
218
cmd .Env = append (os .Environ (), s .env ... )
326
219
if err := cmd .Start (); err != nil {
327
220
log .Fatalf ("starting gopls: %v" , err )
328
221
}
329
222
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
+ }()
331
233
332
234
clientStream := jsonrpc2 .NewHeaderStream (fakenet .NewConn ("stdio" , stdout , stdin ))
333
235
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
+
334
246
return clientConn
335
247
}
0 commit comments