golang.org/x/tools/gopls@v0.15.3/internal/test/integration/bench/repo_test.go (about) 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" 11 "flag" 12 "fmt" 13 "log" 14 "os" 15 "path/filepath" 16 "sync" 17 "testing" 18 "time" 19 20 . "golang.org/x/tools/gopls/internal/test/integration" 21 "golang.org/x/tools/gopls/internal/test/integration/fake" 22 ) 23 24 // repos holds shared repositories for use in benchmarks. 25 // 26 // These repos were selected to represent a variety of different types of 27 // codebases. 28 var repos = map[string]*repo{ 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 37 // Used by x/benchmarks; large. 38 "istio": { 39 name: "istio", 40 url: "https://github.com/istio/istio", 41 commit: "1.17.0", 42 inDir: flag.String("istio_dir", "", "if set, reuse this directory as istio@v1.17.0"), 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", 51 short: true, 52 inDir: flag.String("kubernetes_dir", "", "if set, reuse this directory as kubernetes@v1.24.0"), 53 }, 54 55 // A large, industrial application. 56 "kuma": { 57 name: "kuma", 58 url: "https://github.com/kumahq/kuma", 59 commit: "2.1.1", 60 inDir: flag.String("kuma_dir", "", "if set, reuse this directory as kuma@v2.1.1"), 61 }, 62 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", 68 short: true, 69 inDir: flag.String("oracle_dir", "", "if set, reuse this directory as oracle/oci-go-sdk@v65.43.0"), 70 }, 71 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, 79 inDir: flag.String("pkgsite_dir", "", "if set, reuse this directory as pkgsite@81f6f8d4"), 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, 88 inDir: flag.String("starlark_dir", "", "if set, reuse this directory as starlark@3f75dec8"), 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, 97 inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/tools@v0.9.0"), 98 }, 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 }, 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() { 119 tb.Skipf("large repo %s does not run with -short", repo.name) 120 } 121 return repo 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 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 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 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 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() { 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: 175 log.Fatal(err) 176 default: 177 log.Printf("reusing %s as %s@%s", r.dir, r.url, r.commit) 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 196 start := time.Now() 197 log.Printf("starting initial workspace load for %s", r.name) 198 ts, err := newGoplsConnector(profileArgs(r.name, false)) 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 } 210 log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start)) 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. 227 func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation string, cpuProfile bool) *Env { 228 dir := r.getDir() 229 230 args := profileArgs(qualifiedName(r.name, forOperation), cpuProfile) 231 ts, err := newGoplsConnector(args) 232 if err != nil { 233 tb.Fatal(err) 234 } 235 sandbox, editor, awaiter, err := connectEditor(dir, config, ts) 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 } 262 if r.dir != "" && r.reusableDir() == "" { 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 }