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  }