| // Copyright 2023 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 ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "log" |
| "os" |
| "path/filepath" |
| "sync" |
| "testing" |
| "time" |
| |
| "golang.org/x/tools/gopls/internal/lsp/fake" |
| . "golang.org/x/tools/gopls/internal/lsp/regtest" |
| ) |
| |
| // repos holds shared repositories for use in benchmarks. |
| // |
| // These repos were selected to represent a variety of different types of |
| // codebases. |
| var repos = map[string]*repo{ |
| // Used by x/benchmarks; large. |
| "istio": { |
| name: "istio", |
| url: "https://github.com/istio/istio", |
| commit: "1.17.0", |
| }, |
| |
| // Kubernetes is a large repo with many dependencies, and in the past has |
| // been about as large a repo as gopls could handle. |
| "kubernetes": { |
| name: "kubernetes", |
| url: "https://github.com/kubernetes/kubernetes", |
| commit: "v1.24.0", |
| }, |
| |
| // A large, industrial application. |
| "kuma": { |
| name: "kuma", |
| url: "https://github.com/kumahq/kuma", |
| commit: "2.1.1", |
| }, |
| |
| // x/pkgsite is familiar and represents a common use case (a webserver). It |
| // also has a number of static non-go files and template files. |
| "pkgsite": { |
| name: "pkgsite", |
| url: "https://go.googlesource.com/pkgsite", |
| commit: "81f6f8d4175ad0bf6feaa03543cc433f8b04b19b", |
| short: true, |
| }, |
| |
| // A tiny self-contained project. |
| "starlark": { |
| name: "starlark", |
| url: "https://github.com/google/starlark-go", |
| commit: "3f75dec8e4039385901a30981e3703470d77e027", |
| short: true, |
| }, |
| |
| // The current repository, which is medium-small and has very few dependencies. |
| "tools": { |
| name: "tools", |
| url: "https://go.googlesource.com/tools", |
| commit: "gopls/v0.9.0", |
| short: true, |
| }, |
| } |
| |
| // getRepo gets the requested repo, and skips the test if -short is set and |
| // repo is not configured as a short repo. |
| func getRepo(tb testing.TB, name string) *repo { |
| tb.Helper() |
| repo := repos[name] |
| if repo == nil { |
| tb.Fatalf("repo %s does not exist", name) |
| } |
| if !repo.short && testing.Short() { |
| tb.Skipf("large repo %s does not run whith -short", repo.name) |
| } |
| return repo |
| } |
| |
| // A repo represents a working directory for a repository checked out at a |
| // specific commit. |
| // |
| // Repos are used for sharing state across benchmarks that operate on the same |
| // codebase. |
| type repo struct { |
| // static configuration |
| name string // must be unique, used for subdirectory |
| url string // repo url |
| commit string // full commit hash or tag |
| short bool // whether this repo runs with -short |
| |
| dirOnce sync.Once |
| dir string // directory contaning source code checked out to url@commit |
| |
| // shared editor state |
| editorOnce sync.Once |
| editor *fake.Editor |
| sandbox *fake.Sandbox |
| awaiter *Awaiter |
| } |
| |
| // getDir returns directory containing repo source code, creating it if |
| // necessary. It is safe for concurrent use. |
| func (r *repo) getDir() string { |
| r.dirOnce.Do(func() { |
| r.dir = filepath.Join(getTempDir(), r.name) |
| log.Printf("cloning %s@%s into %s", r.url, r.commit, r.dir) |
| if err := shallowClone(r.dir, r.url, r.commit); err != nil { |
| log.Fatal(err) |
| } |
| }) |
| return r.dir |
| } |
| |
| // sharedEnv returns a shared benchmark environment. It is safe for concurrent |
| // use. |
| // |
| // Every call to sharedEnv uses the same editor and sandbox, as a means to |
| // avoid reinitializing the editor for large repos. Calling repo.Close cleans |
| // up the shared environment. |
| // |
| // Repos in the package-local Repos var are closed at the end of the test main |
| // function. |
| func (r *repo) sharedEnv(tb testing.TB) *Env { |
| r.editorOnce.Do(func() { |
| dir := r.getDir() |
| |
| start := time.Now() |
| log.Printf("starting initial workspace load for %s", r.name) |
| ts, err := newGoplsServer(r.name) |
| if err != nil { |
| log.Fatal(err) |
| } |
| r.sandbox, r.editor, r.awaiter, err = connectEditor(dir, fake.EditorConfig{}, ts) |
| if err != nil { |
| log.Fatalf("connecting editor: %v", err) |
| } |
| |
| if err := r.awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { |
| log.Fatal(err) |
| } |
| log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start)) |
| }) |
| |
| return &Env{ |
| T: tb, |
| Ctx: context.Background(), |
| Editor: r.editor, |
| Sandbox: r.sandbox, |
| Awaiter: r.awaiter, |
| } |
| } |
| |
| // newEnv returns a new Env connected to a new gopls process communicating |
| // over stdin/stdout. It is safe for concurrent use. |
| // |
| // It is the caller's responsibility to call Close on the resulting Env when it |
| // is no longer needed. |
| func (r *repo) newEnv(tb testing.TB, name string, config fake.EditorConfig) *Env { |
| dir := r.getDir() |
| |
| ts, err := newGoplsServer(name) |
| if err != nil { |
| tb.Fatal(err) |
| } |
| sandbox, editor, awaiter, err := connectEditor(dir, config, ts) |
| if err != nil { |
| log.Fatalf("connecting editor: %v", err) |
| } |
| |
| return &Env{ |
| T: tb, |
| Ctx: context.Background(), |
| Editor: editor, |
| Sandbox: sandbox, |
| Awaiter: awaiter, |
| } |
| } |
| |
| // Close cleans up shared state referenced by the repo. |
| func (r *repo) Close() error { |
| var errBuf bytes.Buffer |
| if r.editor != nil { |
| if err := r.editor.Close(context.Background()); err != nil { |
| fmt.Fprintf(&errBuf, "closing editor: %v", err) |
| } |
| } |
| if r.sandbox != nil { |
| if err := r.sandbox.Close(); err != nil { |
| fmt.Fprintf(&errBuf, "closing sandbox: %v", err) |
| } |
| } |
| if r.dir != "" { |
| if err := os.RemoveAll(r.dir); err != nil { |
| fmt.Fprintf(&errBuf, "cleaning dir: %v", err) |
| } |
| } |
| if errBuf.Len() > 0 { |
| return errors.New(errBuf.String()) |
| } |
| return nil |
| } |
| |
| // cleanup cleans up state that is shared across benchmark functions. |
| func cleanup() error { |
| var errBuf bytes.Buffer |
| for _, repo := range repos { |
| if err := repo.Close(); err != nil { |
| fmt.Fprintf(&errBuf, "closing %q: %v", repo.name, err) |
| } |
| } |
| if tempDir != "" { |
| if err := os.RemoveAll(tempDir); err != nil { |
| fmt.Fprintf(&errBuf, "cleaning tempDir: %v", err) |
| } |
| } |
| if errBuf.Len() > 0 { |
| return errors.New(errBuf.String()) |
| } |
| return nil |
| } |