Skip to content

Commit 87f47bb

Browse files
committed
gopls/internal/regtest/bench: refactor and improve benchmarks
Significantly refactor the gopls benchmarks to turn them into proper benchmarks, eliminate the need for passing flags, and allow running them on external gopls processes so that they may be used to test older gopls versions. Doing this required decoupling the benchmarks themselves from the regtest.Runner. Instead, they just create their own regtest.Env to use for scripting operations. In order to facilitate this, I tried to redefine Env as a convenience wrapper around other primitives. By using a separate environment setup for benchmarks, I was able to eliminate a lot of regtest.Options that existed only to prevent the regtest runner from adding instrumentation that would affect benchmarking. This also helped clean up Runner.Run somewhat, though it is still too complicated. Also eliminate the unused AnyDiagnosticAtCurrentVersion, and make a few other TODOs about future cleanup. For golang/go#53992 For golang/go#53538 Change-Id: Idbf923178d4256900c3c05bc8999c0c9839a3c07 Reviewed-on: https://go-review.googlesource.com/c/tools/+/419988 gopls-CI: kokoro <[email protected]> Reviewed-by: Peter Weinberger <[email protected]> Run-TryBot: Robert Findley <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 8b9a1fb commit 87f47bb

23 files changed

+578
-536
lines changed

gopls/internal/regtest/bench/bench_test.go

Lines changed: 188 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,38 +5,209 @@
55
package bench
66

77
import (
8+
"context"
9+
"flag"
810
"fmt"
11+
"io/ioutil"
12+
"log"
13+
"os"
14+
"os/exec"
15+
"sync"
916
"testing"
17+
"time"
1018

1119
"golang.org/x/tools/gopls/internal/hooks"
20+
"golang.org/x/tools/internal/event"
21+
"golang.org/x/tools/internal/fakenet"
22+
"golang.org/x/tools/internal/jsonrpc2"
23+
"golang.org/x/tools/internal/jsonrpc2/servertest"
1224
"golang.org/x/tools/internal/lsp/bug"
25+
"golang.org/x/tools/internal/lsp/cache"
26+
"golang.org/x/tools/internal/lsp/fake"
27+
"golang.org/x/tools/internal/lsp/lsprpc"
28+
"golang.org/x/tools/internal/lsp/regtest"
1329

1430
. "golang.org/x/tools/internal/lsp/regtest"
1531
)
1632

33+
// This package implements benchmarks that share a common editor session.
34+
//
35+
// It is a work-in-progress.
36+
//
37+
// Remaining TODO(rfindley):
38+
// - add detailed documentation for how to write a benchmark, as a package doc
39+
// - add benchmarks for more features
40+
// - eliminate flags, and just run benchmarks on with a predefined set of
41+
// arguments
42+
1743
func TestMain(m *testing.M) {
1844
bug.PanicOnBugs = true
19-
Main(m, hooks.Options)
45+
event.SetExporter(nil) // don't log to stderr
46+
code := doMain(m)
47+
os.Exit(code)
48+
}
49+
50+
func doMain(m *testing.M) (code int) {
51+
defer func() {
52+
if editor != nil {
53+
if err := editor.Close(context.Background()); err != nil {
54+
fmt.Fprintf(os.Stderr, "closing editor: %v", err)
55+
if code == 0 {
56+
code = 1
57+
}
58+
}
59+
}
60+
if tempDir != "" {
61+
if err := os.RemoveAll(tempDir); err != nil {
62+
fmt.Fprintf(os.Stderr, "cleaning temp dir: %v", err)
63+
if code == 0 {
64+
code = 1
65+
}
66+
}
67+
}
68+
}()
69+
return m.Run()
2070
}
2171

22-
func benchmarkOptions(dir string) []RunOption {
23-
return []RunOption{
24-
// Run in an existing directory, since we're trying to simulate known cases
25-
// that cause gopls memory problems.
26-
InExistingDir(dir),
27-
// Skip logs as they buffer up memory unnaturally.
28-
SkipLogs(),
29-
// The Debug server only makes sense if running in singleton mode.
30-
Modes(Default),
31-
// Remove the default timeout. Individual tests should control their
32-
// own graceful termination.
33-
NoDefaultTimeout(),
72+
var (
73+
workdir = flag.String("workdir", "", "if set, working directory to use for benchmarks; overrides -repo and -commit")
74+
repo = flag.String("repo", "https://go.googlesource.com/tools", "if set (and -workdir is unset), run benchmarks in this repo")
75+
file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file")
76+
commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit")
77+
78+
goplsPath = flag.String("gopls", "", "if set, use this gopls for testing")
79+
80+
// If non-empty, tempDir is a temporary working dir that was created by this
81+
// test suite.
82+
setupDirOnce sync.Once
83+
tempDir string
84+
85+
setupEditorOnce sync.Once
86+
sandbox *fake.Sandbox
87+
editor *fake.Editor
88+
awaiter *regtest.Awaiter
89+
)
90+
91+
// benchmarkDir returns the directory to use for benchmarks.
92+
//
93+
// If -workdir is set, just use that directory. Otherwise, check out a shallow
94+
// copy of -repo at the given -commit, and clean up when the test suite exits.
95+
func benchmarkDir() string {
96+
if *workdir != "" {
97+
return *workdir
98+
}
99+
setupDirOnce.Do(func() {
100+
if *repo == "" {
101+
log.Fatal("-repo must be provided")
102+
}
103+
104+
if *commitish == "" {
105+
log.Fatal("-commit must be provided")
106+
}
107+
108+
var err error
109+
tempDir, err = ioutil.TempDir("", "gopls-bench")
110+
if err != nil {
111+
log.Fatal(err)
112+
}
113+
fmt.Printf("checking out %s@%s to %s\n", *repo, *commitish, tempDir)
114+
115+
// Set a timeout for git fetch. If this proves flaky, it can be removed.
116+
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
117+
defer cancel()
118+
119+
// Use a shallow fetch to download just the releveant commit.
120+
shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", *repo, *commitish)
121+
initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit)
122+
initCmd.Dir = tempDir
123+
if err := initCmd.Run(); err != nil {
124+
log.Fatalf("checking out %s: %v", *repo, err)
125+
}
126+
})
127+
return tempDir
128+
}
129+
130+
// benchmarkEnv returns a shared benchmark environment
131+
func benchmarkEnv(tb testing.TB) *Env {
132+
setupEditorOnce.Do(func() {
133+
dir := benchmarkDir()
134+
135+
var err error
136+
sandbox, editor, awaiter, err = connectEditor(dir)
137+
if err != nil {
138+
log.Fatalf("connecting editor: %v", err)
139+
}
140+
141+
if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
142+
panic(err)
143+
}
144+
})
34145

35-
// Use the actual proxy, since we want our builds to succeed.
36-
GOPROXY("https://proxy.golang.org"),
146+
return &Env{
147+
T: tb,
148+
Ctx: context.Background(),
149+
Editor: editor,
150+
Sandbox: sandbox,
151+
Awaiter: awaiter,
37152
}
38153
}
39154

40-
func printBenchmarkResults(result testing.BenchmarkResult) {
41-
fmt.Printf("BenchmarkStatistics\t%s\t%s\n", result.String(), result.MemString())
155+
// connectEditor connects a fake editor session in the given dir, using the
156+
// given editor config.
157+
func connectEditor(dir string) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) {
158+
s, err := fake.NewSandbox(&fake.SandboxConfig{
159+
Workdir: dir,
160+
GOPROXY: "https://proxy.golang.org",
161+
})
162+
if err != nil {
163+
return nil, nil, nil, err
164+
}
165+
166+
a := regtest.NewAwaiter(s.Workdir)
167+
ts := getServer()
168+
e, err := fake.NewEditor(s, fake.EditorConfig{}).Connect(context.Background(), ts, a.Hooks())
169+
if err != nil {
170+
return nil, nil, nil, err
171+
}
172+
return s, e, a, nil
173+
}
174+
175+
// getServer returns a server connector that either starts a new in-process
176+
// server, or starts a separate gopls process.
177+
func getServer() servertest.Connector {
178+
if *goplsPath != "" {
179+
return &SidecarServer{*goplsPath}
180+
}
181+
server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false)
182+
return servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
183+
}
184+
185+
// A SidecarServer starts (and connects to) a separate gopls process at the
186+
// given path.
187+
type SidecarServer struct {
188+
goplsPath string
189+
}
190+
191+
// Connect creates new io.Pipes and binds them to the underlying StreamServer.
192+
func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
193+
cmd := exec.CommandContext(ctx, *goplsPath, "serve")
194+
195+
stdin, err := cmd.StdinPipe()
196+
if err != nil {
197+
log.Fatal(err)
198+
}
199+
stdout, err := cmd.StdoutPipe()
200+
if err != nil {
201+
log.Fatal(err)
202+
}
203+
cmd.Stderr = os.Stdout
204+
if err := cmd.Start(); err != nil {
205+
log.Fatalf("starting gopls: %v", err)
206+
}
207+
208+
go cmd.Wait() // to free resources; error is ignored
209+
210+
clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin))
211+
clientConn := jsonrpc2.NewConn(clientStream)
212+
return clientConn
42213
}

0 commit comments

Comments
 (0)