blob: baa1d3c0c5579e2e331317663d333672d264e750 [file] [log] [blame] [edit]
// Copyright 2025 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package bench
import (
"context"
"fmt"
"go/token"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
. "golang.org/x/tools/gopls/internal/test/integration"
"golang.org/x/tools/gopls/internal/test/integration/fake"
"golang.org/x/tools/internal/modindex"
)
// experiments show the new code about 15 times faster than the old,
// and the old code sometimes fails to find the completion
func BenchmarkLocalModcache(b *testing.B) {
budgets := []string{"0s", "100ms", "200ms", "500ms", "1s", "5s"}
sources := []string{"gopls", "goimports"}
for _, budget := range budgets {
b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) {
for _, source := range sources {
b.Run(fmt.Sprintf("source=%s", source), func(b *testing.B) {
runModcacheCompletion(b, budget, source)
})
}
})
}
}
func runModcacheCompletion(b *testing.B, budget, source string) {
// First set up the program to be edited
gomod := `
module mod.com
go 1.21
`
pat := `
package main
var _ = %s.%s
`
pkg, name, modcache := findSym(b)
name, _, _ = strings.Cut(name, " ")
mainfile := fmt.Sprintf(pat, pkg, name)
// Second, create the Env and start gopls
dir := getTempDir()
if err := os.Mkdir(dir, 0750); err != nil {
if !os.IsExist(err) {
b.Fatal(err)
}
}
defer os.RemoveAll(dir) // is this right? needed?
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil {
b.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(mainfile), 0644); err != nil {
b.Fatal(err)
}
ts, err := newGoplsConnector(nil)
if err != nil {
b.Fatal(err)
}
// PJW: put better EditorConfig here
envvars := map[string]string{
"GOMODCACHE": modcache,
//"GOPATH": sandbox.GOPATH(), // do we need a GOPATH?
}
fc := fake.EditorConfig{
Env: envvars,
Settings: map[string]any{
"completeUnimported": true,
"completionBudget": budget, // "0s", "100ms"
"importsSource": source, // "gopls" or "goimports"
},
}
sandbox, editor, awaiter, err := connectEditor(dir, fc, ts)
if err != nil {
b.Fatal(err)
}
defer sandbox.Close()
defer editor.Close(context.Background())
if err := awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil {
b.Fatal(err)
}
env := &Env{
TB: b,
Ctx: context.Background(),
Editor: editor,
Sandbox: sandbox,
Awaiter: awaiter,
}
// Check that completion works as expected
env.CreateBuffer("main.go", mainfile)
env.AfterChange()
if false { // warm up? or not?
loc := env.RegexpSearch("main.go", name)
completions := env.Completion(loc)
if len(completions.Items) == 0 {
b.Fatal("no completions")
}
}
// run benchmark
for b.Loop() {
loc := env.RegexpSearch("main.go", name)
env.Completion(loc)
}
}
// find some symbol in the module cache
func findSym(t testing.TB) (pkg, name, gomodcache string) {
initForTest(t)
cmd := exec.Command("go", "env", "GOMODCACHE")
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
modcache := strings.TrimSpace(string(out))
ix, err := modindex.Read(modcache)
if err != nil {
t.Fatal(err)
}
if ix == nil {
t.Fatal("nil index")
}
nth := 100 // or something
for _, e := range ix.Entries {
if token.IsExported(e.PkgName) || strings.HasPrefix(e.PkgName, "_") {
continue // weird stuff in module cache
}
for _, nm := range e.Names {
nth--
if nth == 0 {
return e.PkgName, nm, modcache
}
}
}
t.Fatalf("index doesn't have enough usable names, need another %d", nth)
return "", "", modcache
}
// Set IndexDir, avoiding the special case for tests,
func initForTest(t testing.TB) {
dir, err := os.UserCacheDir()
if err != nil {
t.Fatalf("os.UserCacheDir: %v", err)
}
dir = filepath.Join(dir, "go", "imports")
modindex.IndexDir = dir
}