mirror of https://github.com/golang/go.git
278 lines
8.0 KiB
Go
278 lines
8.0 KiB
Go
// 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/cache"
|
|
"golang.org/x/tools/gopls/internal/lsp/fake"
|
|
"golang.org/x/tools/gopls/internal/lsp/lsprpc"
|
|
"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/gopls/internal/lsp/regtest"
|
|
)
|
|
|
|
// This package implements benchmarks that share a common editor session.
|
|
//
|
|
// It is a work-in-progress.
|
|
//
|
|
// Remaining TODO(rfindley):
|
|
// - add detailed documentation for how to write a benchmark, as a package doc
|
|
// - add benchmarks for more features
|
|
// - eliminate flags, and just run benchmarks on with a predefined set of
|
|
// arguments
|
|
|
|
func TestMain(m *testing.M) {
|
|
bug.PanicOnBugs = true
|
|
event.SetExporter(nil) // don't log to stderr
|
|
code := doMain(m)
|
|
os.Exit(code)
|
|
}
|
|
|
|
func doMain(m *testing.M) (code int) {
|
|
defer func() {
|
|
if editor != nil {
|
|
if err := editor.Close(context.Background()); err != nil {
|
|
fmt.Fprintf(os.Stderr, "closing editor: %v", err)
|
|
if code == 0 {
|
|
code = 1
|
|
}
|
|
}
|
|
}
|
|
if tempDir != "" {
|
|
if err := os.RemoveAll(tempDir); err != nil {
|
|
fmt.Fprintf(os.Stderr, "cleaning temp dir: %v", err)
|
|
if code == 0 {
|
|
code = 1
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
return m.Run()
|
|
}
|
|
|
|
var (
|
|
workdir = flag.String("workdir", "", "if set, working directory to use for benchmarks; overrides -repo and -commit")
|
|
repo = flag.String("repo", "https://go.googlesource.com/tools", "if set (and -workdir is unset), run benchmarks in this repo")
|
|
file = flag.String("file", "go/ast/astutil/util.go", "active file, for benchmarks that operate on a file")
|
|
commitish = flag.String("commit", "gopls/v0.9.0", "if set (and -workdir is unset), run benchmarks at this commit")
|
|
|
|
goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit")
|
|
goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path")
|
|
|
|
// If non-empty, tempDir is a temporary working dir that was created by this
|
|
// test suite.
|
|
//
|
|
// The sync.Once variables guard various modifications of the temp directory.
|
|
makeTempDirOnce sync.Once
|
|
checkoutRepoOnce sync.Once
|
|
installGoplsOnce sync.Once
|
|
tempDir string
|
|
|
|
setupEditorOnce sync.Once
|
|
sandbox *fake.Sandbox
|
|
editor *fake.Editor
|
|
awaiter *Awaiter
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// benchmarkDir returns the directory to use for benchmarks.
|
|
//
|
|
// If -workdir is set, just use that directory. Otherwise, check out a shallow
|
|
// copy of -repo at the given -commit, and clean up when the test suite exits.
|
|
func benchmarkDir() string {
|
|
if *workdir != "" {
|
|
return *workdir
|
|
}
|
|
if *repo == "" {
|
|
log.Fatal("-repo must be provided if -workdir is unset")
|
|
}
|
|
if *commitish == "" {
|
|
log.Fatal("-commit must be provided if -workdir is unset")
|
|
}
|
|
|
|
dir := filepath.Join(getTempDir(), "repo")
|
|
checkoutRepoOnce.Do(func() {
|
|
log.Printf("creating working dir: checking out %s@%s to %s\n", *repo, *commitish, dir)
|
|
if err := shallowClone(dir, *repo, *commitish); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
})
|
|
return dir
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// benchmarkEnv returns a shared benchmark environment
|
|
func benchmarkEnv(tb testing.TB) *Env {
|
|
setupEditorOnce.Do(func() {
|
|
dir := benchmarkDir()
|
|
|
|
var err error
|
|
sandbox, editor, awaiter, err = connectEditor(dir, fake.EditorConfig{})
|
|
if err != nil {
|
|
log.Fatalf("connecting editor: %v", err)
|
|
}
|
|
|
|
if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
|
|
panic(err)
|
|
}
|
|
})
|
|
|
|
return &Env{
|
|
T: tb,
|
|
Ctx: context.Background(),
|
|
Editor: editor,
|
|
Sandbox: sandbox,
|
|
Awaiter: awaiter,
|
|
}
|
|
}
|
|
|
|
// connectEditor connects a fake editor session in the given dir, using the
|
|
// given editor config.
|
|
func connectEditor(dir string, config fake.EditorConfig) (*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)
|
|
ts := getServer()
|
|
e, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks())
|
|
if err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
return s, e, a, nil
|
|
}
|
|
|
|
// getServer returns a server connector that either starts a new in-process
|
|
// server, or starts a separate gopls process.
|
|
func getServer() servertest.Connector {
|
|
if *goplsPath != "" && *goplsCommit != "" {
|
|
panic("can't set both -gopls_path and -gopls_commit")
|
|
}
|
|
if *goplsPath != "" {
|
|
return &SidecarServer{*goplsPath}
|
|
}
|
|
if *goplsCommit != "" {
|
|
path := getInstalledGopls()
|
|
return &SidecarServer{path}
|
|
}
|
|
server := lsprpc.NewStreamServer(cache.New(nil, nil, hooks.Options), false)
|
|
return servertest.NewPipeServer(server, jsonrpc2.NewRawStream)
|
|
}
|
|
|
|
// 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(), "tools")
|
|
goplsPath := filepath.Join(toolsDir, "gopls", "gopls")
|
|
|
|
installGoplsOnce.Do(func() {
|
|
log.Printf("installing gopls: checking out x/tools@%s\n", *goplsCommit)
|
|
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(getTempDir(), "tools", "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
|
|
}
|
|
|
|
// Connect creates new io.Pipes and binds them to the underlying StreamServer.
|
|
func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn {
|
|
cmd := exec.CommandContext(ctx, s.goplsPath, "serve")
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
cmd.Stderr = os.Stdout
|
|
if err := cmd.Start(); err != nil {
|
|
log.Fatalf("starting gopls: %v", err)
|
|
}
|
|
|
|
go cmd.Wait() // to free resources; error is ignored
|
|
|
|
clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin))
|
|
clientConn := jsonrpc2.NewConn(clientStream)
|
|
return clientConn
|
|
}
|