Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 1 | // Copyright 2023 The Go Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | package bench |
| 6 | |
| 7 | import ( |
| 8 | "bytes" |
| 9 | "context" |
| 10 | "errors" |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 11 | "flag" |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 12 | "fmt" |
| 13 | "log" |
| 14 | "os" |
| 15 | "path/filepath" |
| 16 | "sync" |
| 17 | "testing" |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 18 | "time" |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 19 | |
| 20 | "golang.org/x/tools/gopls/internal/lsp/fake" |
| 21 | . "golang.org/x/tools/gopls/internal/lsp/regtest" |
| 22 | ) |
| 23 | |
| 24 | // repos holds shared repositories for use in benchmarks. |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 25 | // |
| 26 | // These repos were selected to represent a variety of different types of |
| 27 | // codebases. |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 28 | var repos = map[string]*repo{ |
Rob Findley | 4ed7de1 | 2023-05-09 18:22:43 -0400 | [diff] [blame] | 29 | // google-cloud-go has 145 workspace modules (!), and is quite large. |
| 30 | "google-cloud-go": { |
| 31 | name: "google-cloud-go", |
| 32 | url: "https://github.com/googleapis/google-cloud-go.git", |
| 33 | commit: "07da765765218debf83148cc7ed8a36d6e8921d5", |
| 34 | inDir: flag.String("cloud_go_dir", "", "if set, reuse this directory as google-cloud-go@07da7657"), |
| 35 | }, |
| 36 | |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 37 | // Used by x/benchmarks; large. |
| 38 | "istio": { |
| 39 | name: "istio", |
| 40 | url: "https://github.com/istio/istio", |
| 41 | commit: "1.17.0", |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 42 | inDir: flag.String("istio_dir", "", "if set, reuse this directory as istio@v1.17.0"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 43 | }, |
| 44 | |
| 45 | // Kubernetes is a large repo with many dependencies, and in the past has |
| 46 | // been about as large a repo as gopls could handle. |
| 47 | "kubernetes": { |
| 48 | name: "kubernetes", |
| 49 | url: "https://github.com/kubernetes/kubernetes", |
| 50 | commit: "v1.24.0", |
Rob Findley | b9619ee | 2023-04-20 17:44:41 -0400 | [diff] [blame] | 51 | short: true, |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 52 | inDir: flag.String("kubernetes_dir", "", "if set, reuse this directory as kubernetes@v1.24.0"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 53 | }, |
| 54 | |
| 55 | // A large, industrial application. |
| 56 | "kuma": { |
| 57 | name: "kuma", |
| 58 | url: "https://github.com/kumahq/kuma", |
| 59 | commit: "2.1.1", |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 60 | inDir: flag.String("kuma_dir", "", "if set, reuse this directory as kuma@v2.1.1"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 61 | }, |
| 62 | |
Rob Findley | 80c9aad | 2023-07-11 17:23:25 -0400 | [diff] [blame] | 63 | // A repo containing a very large package (./dataintegration). |
| 64 | "oracle": { |
| 65 | name: "oracle", |
| 66 | url: "https://github.com/oracle/oci-go-sdk.git", |
| 67 | commit: "v65.43.0", |
Robert Findley | 5027187 | 2023-08-09 12:01:10 -0400 | [diff] [blame] | 68 | short: true, |
Rob Findley | 80c9aad | 2023-07-11 17:23:25 -0400 | [diff] [blame] | 69 | inDir: flag.String("oracle_dir", "", "if set, reuse this directory as oracle/oci-go-sdk@v65.43.0"), |
| 70 | }, |
| 71 | |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 72 | // x/pkgsite is familiar and represents a common use case (a webserver). It |
| 73 | // also has a number of static non-go files and template files. |
| 74 | "pkgsite": { |
| 75 | name: "pkgsite", |
| 76 | url: "https://go.googlesource.com/pkgsite", |
| 77 | commit: "81f6f8d4175ad0bf6feaa03543cc433f8b04b19b", |
| 78 | short: true, |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 79 | inDir: flag.String("pkgsite_dir", "", "if set, reuse this directory as pkgsite@81f6f8d4"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 80 | }, |
| 81 | |
| 82 | // A tiny self-contained project. |
| 83 | "starlark": { |
| 84 | name: "starlark", |
| 85 | url: "https://github.com/google/starlark-go", |
| 86 | commit: "3f75dec8e4039385901a30981e3703470d77e027", |
| 87 | short: true, |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 88 | inDir: flag.String("starlark_dir", "", "if set, reuse this directory as starlark@3f75dec8"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 89 | }, |
| 90 | |
| 91 | // The current repository, which is medium-small and has very few dependencies. |
| 92 | "tools": { |
| 93 | name: "tools", |
| 94 | url: "https://go.googlesource.com/tools", |
| 95 | commit: "gopls/v0.9.0", |
| 96 | short: true, |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 97 | inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/tools@v0.9.0"), |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 98 | }, |
Alan Donovan | b71392a | 2023-06-13 11:47:20 -0400 | [diff] [blame] | 99 | |
| 100 | // A repo of similar size to kubernetes, but with substantially more |
| 101 | // complex types that led to a serious performance regression (issue #60621). |
| 102 | "hashiform": { |
| 103 | name: "hashiform", |
| 104 | url: "https://github.com/hashicorp/terraform-provider-aws", |
| 105 | commit: "ac55de2b1950972d93feaa250d7505d9ed829c7c", |
| 106 | inDir: flag.String("hashiform_dir", "", "if set, reuse this directory as hashiform@ac55de2"), |
| 107 | }, |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 108 | } |
| 109 | |
| 110 | // getRepo gets the requested repo, and skips the test if -short is set and |
| 111 | // repo is not configured as a short repo. |
| 112 | func getRepo(tb testing.TB, name string) *repo { |
| 113 | tb.Helper() |
| 114 | repo := repos[name] |
| 115 | if repo == nil { |
| 116 | tb.Fatalf("repo %s does not exist", name) |
| 117 | } |
| 118 | if !repo.short && testing.Short() { |
cui fliter | 2ffc4dc | 2023-07-25 17:11:18 +0800 | [diff] [blame] | 119 | tb.Skipf("large repo %s does not run with -short", repo.name) |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 120 | } |
| 121 | return repo |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 122 | } |
| 123 | |
| 124 | // A repo represents a working directory for a repository checked out at a |
| 125 | // specific commit. |
| 126 | // |
| 127 | // Repos are used for sharing state across benchmarks that operate on the same |
| 128 | // codebase. |
| 129 | type repo struct { |
| 130 | // static configuration |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 131 | name string // must be unique, used for subdirectory |
| 132 | url string // repo url |
| 133 | commit string // full commit hash or tag |
| 134 | short bool // whether this repo runs with -short |
| 135 | inDir *string // if set, use this dir as url@commit, and don't delete |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 136 | |
| 137 | dirOnce sync.Once |
| 138 | dir string // directory contaning source code checked out to url@commit |
| 139 | |
| 140 | // shared editor state |
| 141 | editorOnce sync.Once |
| 142 | editor *fake.Editor |
| 143 | sandbox *fake.Sandbox |
| 144 | awaiter *Awaiter |
| 145 | } |
| 146 | |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 147 | // reusableDir return a reusable directory for benchmarking, or "". |
| 148 | // |
| 149 | // If the user specifies a directory, the test will create and populate it |
| 150 | // on the first run an re-use it on subsequent runs. Otherwise it will |
| 151 | // create, populate, and delete a temporary directory. |
| 152 | func (r *repo) reusableDir() string { |
| 153 | if r.inDir == nil { |
| 154 | return "" |
| 155 | } |
| 156 | return *r.inDir |
| 157 | } |
| 158 | |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 159 | // getDir returns directory containing repo source code, creating it if |
| 160 | // necessary. It is safe for concurrent use. |
| 161 | func (r *repo) getDir() string { |
| 162 | r.dirOnce.Do(func() { |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 163 | if r.dir = r.reusableDir(); r.dir == "" { |
| 164 | r.dir = filepath.Join(getTempDir(), r.name) |
| 165 | } |
| 166 | |
| 167 | _, err := os.Stat(r.dir) |
| 168 | switch { |
| 169 | case os.IsNotExist(err): |
| 170 | log.Printf("cloning %s@%s into %s", r.url, r.commit, r.dir) |
| 171 | if err := shallowClone(r.dir, r.url, r.commit); err != nil { |
| 172 | log.Fatal(err) |
| 173 | } |
| 174 | case err != nil: |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 175 | log.Fatal(err) |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 176 | default: |
| 177 | log.Printf("reusing %s as %s@%s", r.dir, r.url, r.commit) |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 178 | } |
| 179 | }) |
| 180 | return r.dir |
| 181 | } |
| 182 | |
| 183 | // sharedEnv returns a shared benchmark environment. It is safe for concurrent |
| 184 | // use. |
| 185 | // |
| 186 | // Every call to sharedEnv uses the same editor and sandbox, as a means to |
| 187 | // avoid reinitializing the editor for large repos. Calling repo.Close cleans |
| 188 | // up the shared environment. |
| 189 | // |
| 190 | // Repos in the package-local Repos var are closed at the end of the test main |
| 191 | // function. |
| 192 | func (r *repo) sharedEnv(tb testing.TB) *Env { |
| 193 | r.editorOnce.Do(func() { |
| 194 | dir := r.getDir() |
| 195 | |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 196 | start := time.Now() |
| 197 | log.Printf("starting initial workspace load for %s", r.name) |
Rob Findley | e10bcf6 | 2023-07-11 15:44:12 -0400 | [diff] [blame] | 198 | ts, err := newGoplsConnector(profileArgs(r.name, false)) |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 199 | if err != nil { |
| 200 | log.Fatal(err) |
| 201 | } |
| 202 | r.sandbox, r.editor, r.awaiter, err = connectEditor(dir, fake.EditorConfig{}, ts) |
| 203 | if err != nil { |
| 204 | log.Fatalf("connecting editor: %v", err) |
| 205 | } |
| 206 | |
| 207 | if err := r.awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { |
| 208 | log.Fatal(err) |
| 209 | } |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 210 | log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start)) |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 211 | }) |
| 212 | |
| 213 | return &Env{ |
| 214 | T: tb, |
| 215 | Ctx: context.Background(), |
| 216 | Editor: r.editor, |
| 217 | Sandbox: r.sandbox, |
| 218 | Awaiter: r.awaiter, |
| 219 | } |
| 220 | } |
| 221 | |
| 222 | // newEnv returns a new Env connected to a new gopls process communicating |
| 223 | // over stdin/stdout. It is safe for concurrent use. |
| 224 | // |
| 225 | // It is the caller's responsibility to call Close on the resulting Env when it |
| 226 | // is no longer needed. |
Rob Findley | e10bcf6 | 2023-07-11 15:44:12 -0400 | [diff] [blame] | 227 | func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation string, cpuProfile bool) *Env { |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 228 | dir := r.getDir() |
| 229 | |
Rob Findley | e10bcf6 | 2023-07-11 15:44:12 -0400 | [diff] [blame] | 230 | args := profileArgs(qualifiedName(r.name, forOperation), cpuProfile) |
| 231 | ts, err := newGoplsConnector(args) |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 232 | if err != nil { |
| 233 | tb.Fatal(err) |
| 234 | } |
Robert Findley | 6eb432f | 2023-02-17 18:42:31 -0500 | [diff] [blame] | 235 | sandbox, editor, awaiter, err := connectEditor(dir, config, ts) |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 236 | if err != nil { |
| 237 | log.Fatalf("connecting editor: %v", err) |
| 238 | } |
| 239 | |
| 240 | return &Env{ |
| 241 | T: tb, |
| 242 | Ctx: context.Background(), |
| 243 | Editor: editor, |
| 244 | Sandbox: sandbox, |
| 245 | Awaiter: awaiter, |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | // Close cleans up shared state referenced by the repo. |
| 250 | func (r *repo) Close() error { |
| 251 | var errBuf bytes.Buffer |
| 252 | if r.editor != nil { |
| 253 | if err := r.editor.Close(context.Background()); err != nil { |
| 254 | fmt.Fprintf(&errBuf, "closing editor: %v", err) |
| 255 | } |
| 256 | } |
| 257 | if r.sandbox != nil { |
| 258 | if err := r.sandbox.Close(); err != nil { |
| 259 | fmt.Fprintf(&errBuf, "closing sandbox: %v", err) |
| 260 | } |
| 261 | } |
Robert Findley | 1b71eda | 2023-03-20 22:59:37 -0400 | [diff] [blame] | 262 | if r.dir != "" && r.reusableDir() == "" { |
Robert Findley | bc2e2c2 | 2023-02-16 17:49:06 -0500 | [diff] [blame] | 263 | if err := os.RemoveAll(r.dir); err != nil { |
| 264 | fmt.Fprintf(&errBuf, "cleaning dir: %v", err) |
| 265 | } |
| 266 | } |
| 267 | if errBuf.Len() > 0 { |
| 268 | return errors.New(errBuf.String()) |
| 269 | } |
| 270 | return nil |
| 271 | } |
| 272 | |
| 273 | // cleanup cleans up state that is shared across benchmark functions. |
| 274 | func cleanup() error { |
| 275 | var errBuf bytes.Buffer |
| 276 | for _, repo := range repos { |
| 277 | if err := repo.Close(); err != nil { |
| 278 | fmt.Fprintf(&errBuf, "closing %q: %v", repo.name, err) |
| 279 | } |
| 280 | } |
| 281 | if tempDir != "" { |
| 282 | if err := os.RemoveAll(tempDir); err != nil { |
| 283 | fmt.Fprintf(&errBuf, "cleaning tempDir: %v", err) |
| 284 | } |
| 285 | } |
| 286 | if errBuf.Len() > 0 { |
| 287 | return errors.New(errBuf.String()) |
| 288 | } |
| 289 | return nil |
| 290 | } |