golang.org/x/tools/gopls@v0.15.3/internal/test/integration/bench/completion_test.go (about)

     1  // Copyright 2020 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  	"flag"
     9  	"fmt"
    10  	"sync/atomic"
    11  	"testing"
    12  
    13  	"golang.org/x/tools/gopls/internal/protocol"
    14  	. "golang.org/x/tools/gopls/internal/test/integration"
    15  	"golang.org/x/tools/gopls/internal/test/integration/fake"
    16  )
    17  
    18  var completionGOPATH = flag.String("completion_gopath", "", "if set, use this GOPATH for BenchmarkCompletion")
    19  
    20  type completionBenchOptions struct {
    21  	file, locationRegexp string
    22  
    23  	// Hooks to run edits before initial completion
    24  	setup            func(*Env) // run before the benchmark starts
    25  	beforeCompletion func(*Env) // run before each completion
    26  }
    27  
    28  // Deprecated: new tests should be expressed in BenchmarkCompletion.
    29  func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
    30  	repo := getRepo(b, "tools")
    31  	_ = repo.sharedEnv(b) // ensure cache is warm
    32  	env := repo.newEnv(b, fake.EditorConfig{}, "completion", false)
    33  	defer env.Close()
    34  
    35  	// Run edits required for this completion.
    36  	if options.setup != nil {
    37  		options.setup(env)
    38  	}
    39  
    40  	// Run a completion to make sure the system is warm.
    41  	loc := env.RegexpSearch(options.file, options.locationRegexp)
    42  	completions := env.Completion(loc)
    43  
    44  	if testing.Verbose() {
    45  		fmt.Println("Results:")
    46  		for i := 0; i < len(completions.Items); i++ {
    47  			fmt.Printf("\t%d. %v\n", i, completions.Items[i])
    48  		}
    49  	}
    50  
    51  	b.Run("tools", func(b *testing.B) {
    52  		if stopAndRecord := startProfileIfSupported(b, env, qualifiedName("tools", "completion")); stopAndRecord != nil {
    53  			defer stopAndRecord()
    54  		}
    55  
    56  		for i := 0; i < b.N; i++ {
    57  			if options.beforeCompletion != nil {
    58  				options.beforeCompletion(env)
    59  			}
    60  			env.Completion(loc)
    61  		}
    62  	})
    63  }
    64  
    65  // endRangeInBuffer returns the position for last character in the buffer for
    66  // the given file.
    67  func endRangeInBuffer(env *Env, name string) protocol.Range {
    68  	buffer := env.BufferText(name)
    69  	m := protocol.NewMapper("", []byte(buffer))
    70  	rng, err := m.OffsetRange(len(buffer), len(buffer))
    71  	if err != nil {
    72  		env.T.Fatal(err)
    73  	}
    74  	return rng
    75  }
    76  
    77  // Benchmark struct completion in tools codebase.
    78  func BenchmarkStructCompletion(b *testing.B) {
    79  	file := "internal/lsp/cache/session.go"
    80  
    81  	setup := func(env *Env) {
    82  		env.OpenFile(file)
    83  		env.EditBuffer(file, protocol.TextEdit{
    84  			Range:   endRangeInBuffer(env, file),
    85  			NewText: "\nvar testVariable map[string]bool = Session{}.\n",
    86  		})
    87  	}
    88  
    89  	benchmarkCompletion(completionBenchOptions{
    90  		file:           file,
    91  		locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`,
    92  		setup:          setup,
    93  	}, b)
    94  }
    95  
    96  // Benchmark import completion in tools codebase.
    97  func BenchmarkImportCompletion(b *testing.B) {
    98  	const file = "internal/lsp/source/completion/completion.go"
    99  	benchmarkCompletion(completionBenchOptions{
   100  		file:           file,
   101  		locationRegexp: `go\/()`,
   102  		setup:          func(env *Env) { env.OpenFile(file) },
   103  	}, b)
   104  }
   105  
   106  // Benchmark slice completion in tools codebase.
   107  func BenchmarkSliceCompletion(b *testing.B) {
   108  	file := "internal/lsp/cache/session.go"
   109  
   110  	setup := func(env *Env) {
   111  		env.OpenFile(file)
   112  		env.EditBuffer(file, protocol.TextEdit{
   113  			Range:   endRangeInBuffer(env, file),
   114  			NewText: "\nvar testVariable []byte = \n",
   115  		})
   116  	}
   117  
   118  	benchmarkCompletion(completionBenchOptions{
   119  		file:           file,
   120  		locationRegexp: `var testVariable \[\]byte (=)`,
   121  		setup:          setup,
   122  	}, b)
   123  }
   124  
   125  // Benchmark deep completion in function call in tools codebase.
   126  func BenchmarkFuncDeepCompletion(b *testing.B) {
   127  	file := "internal/lsp/source/completion/completion.go"
   128  	fileContent := `
   129  func (c *completer) _() {
   130  	c.inference.kindMatches(c.)
   131  }
   132  `
   133  	setup := func(env *Env) {
   134  		env.OpenFile(file)
   135  		originalBuffer := env.BufferText(file)
   136  		env.EditBuffer(file, protocol.TextEdit{
   137  			Range: endRangeInBuffer(env, file),
   138  			// TODO(rfindley): this is a bug: it should just be fileContent.
   139  			NewText: originalBuffer + fileContent,
   140  		})
   141  	}
   142  
   143  	benchmarkCompletion(completionBenchOptions{
   144  		file:           file,
   145  		locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
   146  		setup:          setup,
   147  	}, b)
   148  }
   149  
   150  type completionTest struct {
   151  	repo           string
   152  	name           string
   153  	file           string // repo-relative file to create
   154  	content        string // file content
   155  	locationRegexp string // regexp for completion
   156  }
   157  
   158  var completionTests = []completionTest{
   159  	{
   160  		"tools",
   161  		"selector",
   162  		"internal/lsp/source/completion/completion2.go",
   163  		`
   164  package completion
   165  
   166  func (c *completer) _() {
   167  	c.inference.kindMatches(c.)
   168  }
   169  `,
   170  		`func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`,
   171  	},
   172  	{
   173  		"tools",
   174  		"unimportedident",
   175  		"internal/lsp/source/completion/completion2.go",
   176  		`
   177  package completion
   178  
   179  func (c *completer) _() {
   180  	lo
   181  }
   182  `,
   183  		`lo()`,
   184  	},
   185  	{
   186  		"tools",
   187  		"unimportedselector",
   188  		"internal/lsp/source/completion/completion2.go",
   189  		`
   190  package completion
   191  
   192  func (c *completer) _() {
   193  	log.
   194  }
   195  `,
   196  		`log\.()`,
   197  	},
   198  	{
   199  		"kubernetes",
   200  		"selector",
   201  		"pkg/kubelet/kubelet2.go",
   202  		`
   203  package kubelet
   204  
   205  func (kl *Kubelet) _() {
   206  	kl.
   207  }
   208  `,
   209  		`kl\.()`,
   210  	},
   211  	{
   212  		"kubernetes",
   213  		"identifier",
   214  		"pkg/kubelet/kubelet2.go",
   215  		`
   216  package kubelet
   217  
   218  func (kl *Kubelet) _() {
   219  	k // here
   220  }
   221  `,
   222  		`k() // here`,
   223  	},
   224  	{
   225  		"oracle",
   226  		"selector",
   227  		"dataintegration/pivot2.go",
   228  		`
   229  package dataintegration
   230  
   231  func (p *Pivot) _() {
   232  	p.
   233  }
   234  `,
   235  		`p\.()`,
   236  	},
   237  }
   238  
   239  // Benchmark completion following an arbitrary edit.
   240  //
   241  // Edits force type-checked packages to be invalidated, so we want to measure
   242  // how long it takes before completion results are available.
   243  func BenchmarkCompletion(b *testing.B) {
   244  	for _, test := range completionTests {
   245  		b.Run(fmt.Sprintf("%s_%s", test.repo, test.name), func(b *testing.B) {
   246  			for _, followingEdit := range []bool{true, false} {
   247  				b.Run(fmt.Sprintf("edit=%v", followingEdit), func(b *testing.B) {
   248  					for _, completeUnimported := range []bool{true, false} {
   249  						b.Run(fmt.Sprintf("unimported=%v", completeUnimported), func(b *testing.B) {
   250  							for _, budget := range []string{"0s", "100ms"} {
   251  								b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
   252  									runCompletion(b, test, followingEdit, completeUnimported, budget)
   253  								})
   254  							}
   255  						})
   256  					}
   257  				})
   258  			}
   259  		})
   260  	}
   261  }
   262  
   263  // For optimizing unimported completion, it can be useful to benchmark with a
   264  // huge GOMODCACHE.
   265  var gomodcache = flag.String("gomodcache", "", "optional GOMODCACHE for unimported completion benchmarks")
   266  
   267  func runCompletion(b *testing.B, test completionTest, followingEdit, completeUnimported bool, budget string) {
   268  	repo := getRepo(b, test.repo)
   269  	gopath := *completionGOPATH
   270  	if gopath == "" {
   271  		// use a warm GOPATH
   272  		sharedEnv := repo.sharedEnv(b)
   273  		gopath = sharedEnv.Sandbox.GOPATH()
   274  	}
   275  	envvars := map[string]string{
   276  		"GOPATH": gopath,
   277  	}
   278  
   279  	if *gomodcache != "" {
   280  		envvars["GOMODCACHE"] = *gomodcache
   281  	}
   282  
   283  	env := repo.newEnv(b, fake.EditorConfig{
   284  		Env: envvars,
   285  		Settings: map[string]interface{}{
   286  			"completeUnimported": completeUnimported,
   287  			"completionBudget":   budget,
   288  		},
   289  	}, "completion", false)
   290  	defer env.Close()
   291  
   292  	env.CreateBuffer(test.file, "// __TEST_PLACEHOLDER_0__\n"+test.content)
   293  	editPlaceholder := func() {
   294  		edits := atomic.AddInt64(&editID, 1)
   295  		env.EditBuffer(test.file, protocol.TextEdit{
   296  			Range: protocol.Range{
   297  				Start: protocol.Position{Line: 0, Character: 0},
   298  				End:   protocol.Position{Line: 1, Character: 0},
   299  			},
   300  			// Increment the placeholder text, to ensure cache misses.
   301  			NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits),
   302  		})
   303  	}
   304  	env.AfterChange()
   305  
   306  	// Run a completion to make sure the system is warm.
   307  	loc := env.RegexpSearch(test.file, test.locationRegexp)
   308  	completions := env.Completion(loc)
   309  
   310  	if testing.Verbose() {
   311  		fmt.Println("Results:")
   312  		for i, item := range completions.Items {
   313  			fmt.Printf("\t%d. %v\n", i, item)
   314  		}
   315  	}
   316  
   317  	b.ResetTimer()
   318  
   319  	if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completion")); stopAndRecord != nil {
   320  		defer stopAndRecord()
   321  	}
   322  
   323  	for i := 0; i < b.N; i++ {
   324  		if followingEdit {
   325  			editPlaceholder()
   326  		}
   327  		loc := env.RegexpSearch(test.file, test.locationRegexp)
   328  		env.Completion(loc)
   329  	}
   330  }