blob: 9b509052b8f25f06e7357de0e420ad0ccfa9c4ff [file] [log] [blame]
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package bench
import (
"context"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"sync"
"testing"
"time"
"golang.org/x/tools/gopls/internal/hooks"
"golang.org/x/tools/gopls/internal/lsp/cmd"
"golang.org/x/tools/gopls/internal/lsp/fake"
"golang.org/x/tools/internal/bug"
"golang.org/x/tools/internal/event"
"golang.org/x/tools/internal/fakenet"
"golang.org/x/tools/internal/jsonrpc2"
"golang.org/x/tools/internal/jsonrpc2/servertest"
"golang.org/x/tools/internal/tool"
. "golang.org/x/tools/gopls/internal/lsp/regtest"
)
var (
goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit")
installGoplsOnce sync.Once // guards installing gopls at -gopls_commit
goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path")
cpuProfile = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc")
memProfile = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc")
allocProfile = flag.String("gopls_allocprofile", "", "if set, the alloc profile file suffix; see \"Profiling\" in the package doc")
trace = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc")
// If non-empty, tempDir is a temporary working dir that was created by this
// test suite.
makeTempDirOnce sync.Once // guards creation of the temp dir
tempDir string
)
// if runAsGopls is "true", run the gopls command instead of the testing.M.
const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS"
func TestMain(m *testing.M) {
bug.PanicOnBugs = true
if os.Getenv(runAsGopls) == "true" {
tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:])
os.Exit(0)
}
event.SetExporter(nil) // don't log to stderr
code := m.Run()
if err := cleanup(); err != nil {
fmt.Fprintf(os.Stderr, "cleaning up after benchmarks: %v\n", err)
if code == 0 {
code = 1
}
}
os.Exit(code)
}
// getTempDir returns the temporary directory to use for benchmark files,
// creating it if necessary.
func getTempDir() string {
makeTempDirOnce.Do(func() {
var err error
tempDir, err = ioutil.TempDir("", "gopls-bench")
if err != nil {
log.Fatal(err)
}
})
return tempDir
}
// shallowClone performs a shallow clone of repo into dir at the given
// 'commitish' ref (any commit reference understood by git).
//
// The directory dir must not already exist.
func shallowClone(dir, repo, commitish string) error {
if err := os.Mkdir(dir, 0750); err != nil {
return fmt.Errorf("creating dir for %s: %v", repo, err)
}
// Set a timeout for git fetch. If this proves flaky, it can be removed.
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
// Use a shallow fetch to download just the relevant commit.
shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish)
initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit)
initCmd.Dir = dir
if output, err := initCmd.CombinedOutput(); err != nil {
return fmt.Errorf("checking out %s: %v\n%s", repo, err, output)
}
return nil
}
// connectEditor connects a fake editor session in the given dir, using the
// given editor config.
func connectEditor(dir string, config fake.EditorConfig, ts servertest.Connector) (*fake.Sandbox, *fake.Editor, *Awaiter, error) {
s, err := fake.NewSandbox(&fake.SandboxConfig{
Workdir: dir,
GOPROXY: "https://proxy.golang.org",
})
if err != nil {
return nil, nil, nil, err
}
a := NewAwaiter(s.Workdir)
const skipApplyEdits = false
editor, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks(), skipApplyEdits)
if err != nil {
return nil, nil, nil, err
}
return s, editor, a, nil
}
// newGoplsServer returns a connector that connects to a new gopls process.
func newGoplsServer(name string) (servertest.Connector, error) {
if *goplsPath != "" && *goplsCommit != "" {
panic("can't set both -gopls_path and -gopls_commit")
}
var (
goplsPath = *goplsPath
env []string
)
if *goplsCommit != "" {
goplsPath = getInstalledGopls()
}
if goplsPath == "" {
var err error
goplsPath, err = os.Executable()
if err != nil {
return nil, err
}
env = []string{fmt.Sprintf("%s=true", runAsGopls)}
}
var args []string
if *cpuProfile != "" {
args = append(args, fmt.Sprintf("-profile.cpu=%s", name+"."+*cpuProfile))
}
if *memProfile != "" {
args = append(args, fmt.Sprintf("-profile.mem=%s", name+"."+*memProfile))
}
if *allocProfile != "" {
args = append(args, fmt.Sprintf("-profile.alloc=%s", name+"."+*allocProfile))
}
if *trace != "" {
args = append(args, fmt.Sprintf("-profile.trace=%s", name+"."+*trace))
}
return &SidecarServer{
goplsPath: goplsPath,
env: env,
args: args,
}, nil
}
// getInstalledGopls builds gopls at the given -gopls_commit, returning the
// path to the gopls binary.
func getInstalledGopls() string {
if *goplsCommit == "" {
panic("must provide -gopls_commit")
}
toolsDir := filepath.Join(getTempDir(), "gopls_build")
goplsPath := filepath.Join(toolsDir, "gopls", "gopls")
installGoplsOnce.Do(func() {
log.Printf("installing gopls: checking out x/tools@%s into %s\n", *goplsCommit, toolsDir)
if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil {
log.Fatal(err)
}
log.Println("installing gopls: building...")
bld := exec.Command("go", "build", ".")
bld.Dir = filepath.Join(toolsDir, "gopls")
if output, err := bld.CombinedOutput(); err != nil {
log.Fatalf("building gopls: %v\n%s", err, output)
}
// Confirm that the resulting path now exists.
if _, err := os.Stat(goplsPath); err != nil {
log.Fatalf("os.Stat(%s): %v", goplsPath, err)
}
})
return goplsPath
}
// A SidecarServer starts (and connects to) a separate gopls process at the
// given path.
type SidecarServer struct {
goplsPath string
env []string // additional environment bindings
args []string // command-line arguments
}
// Connect creates new io.Pipes and binds them to the underlying StreamServer.
//
// It implements the servertest.Connector interface.
func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
// Note: don't use CommandContext here, as we want gopls to exit gracefully
// in order to write out profile data.
//
// We close the connection on context cancelation below.
cmd := exec.Command(s.goplsPath, s.args...)
stdin, err := cmd.StdinPipe()
if err != nil {
log.Fatal(err)
}
stdout, err := cmd.StdoutPipe()
if err != nil {
log.Fatal(err)
}
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), s.env...)
if err := cmd.Start(); err != nil {
log.Fatalf("starting gopls: %v", err)
}
go func() {
// If we don't log.Fatal here, benchmarks may hang indefinitely if gopls
// exits abnormally.
//
// TODO(rfindley): ideally we would shut down the connection gracefully,
// but that doesn't currently work.
if err := cmd.Wait(); err != nil {
log.Fatalf("gopls invocation failed with error: %v", err)
}
}()
clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin))
clientConn := jsonrpc2.NewConn(clientStream)
go func() {
select {
case <-ctx.Done():
clientConn.Close()
clientStream.Close()
case <-clientConn.Done():
}
}()
return clientConn
}