blob: 3a4575e65c458a818dbab119577bfed51e1702d8 [file] [log] [blame]
Robert Findleybc2e2c22023-02-16 17:49:06 -05001// 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
5package bench
6
7import (
8 "bytes"
9 "context"
10 "errors"
Robert Findley1b71eda2023-03-20 22:59:37 -040011 "flag"
Robert Findleybc2e2c22023-02-16 17:49:06 -050012 "fmt"
13 "log"
14 "os"
15 "path/filepath"
16 "sync"
17 "testing"
Robert Findley6eb432f2023-02-17 18:42:31 -050018 "time"
Robert Findleybc2e2c22023-02-16 17:49:06 -050019
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 Findley6eb432f2023-02-17 18:42:31 -050025//
26// These repos were selected to represent a variety of different types of
27// codebases.
Robert Findleybc2e2c22023-02-16 17:49:06 -050028var repos = map[string]*repo{
Rob Findley4ed7de12023-05-09 18:22:43 -040029 // 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 Findley6eb432f2023-02-17 18:42:31 -050037 // Used by x/benchmarks; large.
38 "istio": {
39 name: "istio",
40 url: "https://github.com/istio/istio",
41 commit: "1.17.0",
Robert Findley1b71eda2023-03-20 22:59:37 -040042 inDir: flag.String("istio_dir", "", "if set, reuse this directory as istio@v1.17.0"),
Robert Findley6eb432f2023-02-17 18:42:31 -050043 },
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 Findleyb9619ee2023-04-20 17:44:41 -040051 short: true,
Robert Findley1b71eda2023-03-20 22:59:37 -040052 inDir: flag.String("kubernetes_dir", "", "if set, reuse this directory as kubernetes@v1.24.0"),
Robert Findley6eb432f2023-02-17 18:42:31 -050053 },
54
55 // A large, industrial application.
56 "kuma": {
57 name: "kuma",
58 url: "https://github.com/kumahq/kuma",
59 commit: "2.1.1",
Robert Findley1b71eda2023-03-20 22:59:37 -040060 inDir: flag.String("kuma_dir", "", "if set, reuse this directory as kuma@v2.1.1"),
Robert Findley6eb432f2023-02-17 18:42:31 -050061 },
62
Rob Findley80c9aad2023-07-11 17:23:25 -040063 // 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 Findley50271872023-08-09 12:01:10 -040068 short: true,
Rob Findley80c9aad2023-07-11 17:23:25 -040069 inDir: flag.String("oracle_dir", "", "if set, reuse this directory as oracle/oci-go-sdk@v65.43.0"),
70 },
71
Robert Findley6eb432f2023-02-17 18:42:31 -050072 // 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 Findley1b71eda2023-03-20 22:59:37 -040079 inDir: flag.String("pkgsite_dir", "", "if set, reuse this directory as pkgsite@81f6f8d4"),
Robert Findley6eb432f2023-02-17 18:42:31 -050080 },
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 Findley1b71eda2023-03-20 22:59:37 -040088 inDir: flag.String("starlark_dir", "", "if set, reuse this directory as starlark@3f75dec8"),
Robert Findley6eb432f2023-02-17 18:42:31 -050089 },
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 Findley1b71eda2023-03-20 22:59:37 -040097 inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/tools@v0.9.0"),
Robert Findley6eb432f2023-02-17 18:42:31 -050098 },
Alan Donovanb71392a2023-06-13 11:47:20 -040099
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 Findley6eb432f2023-02-17 18:42:31 -0500108}
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.
112func 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 fliter2ffc4dc2023-07-25 17:11:18 +0800119 tb.Skipf("large repo %s does not run with -short", repo.name)
Robert Findley6eb432f2023-02-17 18:42:31 -0500120 }
121 return repo
Robert Findleybc2e2c22023-02-16 17:49:06 -0500122}
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.
129type repo struct {
130 // static configuration
Robert Findley1b71eda2023-03-20 22:59:37 -0400131 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 Findleybc2e2c22023-02-16 17:49:06 -0500136
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 Findley1b71eda2023-03-20 22:59:37 -0400147// 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.
152func (r *repo) reusableDir() string {
153 if r.inDir == nil {
154 return ""
155 }
156 return *r.inDir
157}
158
Robert Findleybc2e2c22023-02-16 17:49:06 -0500159// getDir returns directory containing repo source code, creating it if
160// necessary. It is safe for concurrent use.
161func (r *repo) getDir() string {
162 r.dirOnce.Do(func() {
Robert Findley1b71eda2023-03-20 22:59:37 -0400163 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 Findleybc2e2c22023-02-16 17:49:06 -0500175 log.Fatal(err)
Robert Findley1b71eda2023-03-20 22:59:37 -0400176 default:
177 log.Printf("reusing %s as %s@%s", r.dir, r.url, r.commit)
Robert Findleybc2e2c22023-02-16 17:49:06 -0500178 }
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.
192func (r *repo) sharedEnv(tb testing.TB) *Env {
193 r.editorOnce.Do(func() {
194 dir := r.getDir()
195
Robert Findley6eb432f2023-02-17 18:42:31 -0500196 start := time.Now()
197 log.Printf("starting initial workspace load for %s", r.name)
Rob Findleye10bcf62023-07-11 15:44:12 -0400198 ts, err := newGoplsConnector(profileArgs(r.name, false))
Robert Findleybc2e2c22023-02-16 17:49:06 -0500199 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 Findley6eb432f2023-02-17 18:42:31 -0500210 log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start))
Robert Findleybc2e2c22023-02-16 17:49:06 -0500211 })
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 Findleye10bcf62023-07-11 15:44:12 -0400227func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation string, cpuProfile bool) *Env {
Robert Findleybc2e2c22023-02-16 17:49:06 -0500228 dir := r.getDir()
229
Rob Findleye10bcf62023-07-11 15:44:12 -0400230 args := profileArgs(qualifiedName(r.name, forOperation), cpuProfile)
231 ts, err := newGoplsConnector(args)
Robert Findleybc2e2c22023-02-16 17:49:06 -0500232 if err != nil {
233 tb.Fatal(err)
234 }
Robert Findley6eb432f2023-02-17 18:42:31 -0500235 sandbox, editor, awaiter, err := connectEditor(dir, config, ts)
Robert Findleybc2e2c22023-02-16 17:49:06 -0500236 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.
250func (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 Findley1b71eda2023-03-20 22:59:37 -0400262 if r.dir != "" && r.reusableDir() == "" {
Robert Findleybc2e2c22023-02-16 17:49:06 -0500263 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.
274func 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}