Skip to content

Commit 6eb432f

Browse files
committed
gopls/internal/regtest/bench: add benchmarks in a wider variety of repos
Extend existing benchmarks to run in more repos, choosing an initial set with different features that may affect performance. Some of these take too long if run in the same batch, so guard with -short. Several benchmarks need a location within the codebase. For these, I have chosen somewhat arbitrarily, but tried to select locations within the core of the codebase. We can always adjust in the future. Additionally: - fix the fake file polling to scale to larger codebases, by avoiding reading files if it isn't necessary - fix a polling bug related to symlinks - fix a couple places the benchmarks weren't cleaning up after themselves correctly - fix a bug where the gopls install used the wrong directory For golang/go#53538 Change-Id: I559031cb068086cd5ec19e36bb12da396194933c Reviewed-on: https://go-review.googlesource.com/c/tools/+/469355 gopls-CI: kokoro <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent c91d0b8 commit 6eb432f

15 files changed

+407
-243
lines changed

gopls/internal/lsp/fake/editor.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ type EditorConfig struct {
106106
Settings map[string]interface{}
107107
}
108108

109-
// NewEditor Creates a new Editor.
109+
// NewEditor creates a new Editor.
110110
func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor {
111111
return &Editor{
112112
buffers: make(map[string]buffer),
@@ -959,7 +959,7 @@ func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCom
959959
// Some commands use the go command, which writes directly to disk.
960960
// For convenience, check for those changes.
961961
if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil {
962-
return nil, err
962+
return nil, fmt.Errorf("checking for file changes: %v", err)
963963
}
964964
return result, nil
965965
}

gopls/internal/lsp/fake/sandbox.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
118118
// this is used for running in an existing directory.
119119
// TODO(findleyr): refactor this to be less of a workaround.
120120
if filepath.IsAbs(config.Workdir) {
121-
sb.Workdir = NewWorkdir(config.Workdir)
121+
sb.Workdir, err = NewWorkdir(config.Workdir, nil)
122+
if err != nil {
123+
return nil, err
124+
}
122125
return sb, nil
123126
}
124127
var workdir string
@@ -136,8 +139,8 @@ func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) {
136139
if err := os.MkdirAll(workdir, 0755); err != nil {
137140
return nil, err
138141
}
139-
sb.Workdir = NewWorkdir(workdir)
140-
if err := sb.Workdir.writeInitialFiles(config.Files); err != nil {
142+
sb.Workdir, err = NewWorkdir(workdir, config.Files)
143+
if err != nil {
141144
return nil, err
142145
}
143146
return sb, nil

gopls/internal/lsp/fake/workdir.go

Lines changed: 87 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"context"
1010
"crypto/sha256"
1111
"fmt"
12+
"io/fs"
1213
"io/ioutil"
1314
"os"
1415
"path/filepath"
@@ -103,49 +104,27 @@ type Workdir struct {
103104
files map[string]fileID
104105
}
105106

106-
// fileID is a file identity for the purposes of detecting on-disk
107-
// modifications.
108-
type fileID struct {
109-
hash string
110-
mtime time.Time
111-
}
112-
113107
// NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a
114108
// Workir for operating on these files using
115-
func NewWorkdir(dir string) *Workdir {
116-
return &Workdir{RelativeTo: RelativeTo(dir)}
117-
}
118-
119-
func hashFile(data []byte) string {
120-
return fmt.Sprintf("%x", sha256.Sum256(data))
121-
}
122-
123-
func (w *Workdir) writeInitialFiles(files map[string][]byte) error {
124-
w.files = map[string]fileID{}
109+
func NewWorkdir(dir string, files map[string][]byte) (*Workdir, error) {
110+
w := &Workdir{RelativeTo: RelativeTo(dir)}
125111
for name, data := range files {
126112
if err := writeFileData(name, data, w.RelativeTo); err != nil {
127-
return fmt.Errorf("writing to workdir: %w", err)
113+
return nil, fmt.Errorf("writing to workdir: %w", err)
128114
}
129-
fp := w.AbsPath(name)
115+
}
116+
_, err := w.pollFiles() // poll files to populate the files map.
117+
return w, err
118+
}
130119

131-
// We need the mtime of the file just written for the purposes of tracking
132-
// file identity. Calling Stat here could theoretically return an mtime
133-
// that is inconsistent with the file contents represented by the hash, but
134-
// since we "own" this file we assume that the mtime is correct.
135-
//
136-
// Furthermore, see the documentation for Workdir.files for why mismatches
137-
// between identifiers are considered to be benign.
138-
fi, err := os.Stat(fp)
139-
if err != nil {
140-
return fmt.Errorf("reading file info: %v", err)
141-
}
120+
// fileID identifies a file version on disk.
121+
type fileID struct {
122+
mtime time.Time
123+
hash string // empty if mtime is old enough to be reliabe; otherwise a file digest
124+
}
142125

143-
w.files[name] = fileID{
144-
hash: hashFile(data),
145-
mtime: fi.ModTime(),
146-
}
147-
}
148-
return nil
126+
func hashFile(data []byte) string {
127+
return fmt.Sprintf("%x", sha256.Sum256(data))
149128
}
150129

151130
// RootURI returns the root URI for this working directory of this scratch
@@ -335,49 +314,21 @@ func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error
335314
// ListFiles returns a new sorted list of the relative paths of files in dir,
336315
// recursively.
337316
func (w *Workdir) ListFiles(dir string) ([]string, error) {
338-
m, err := w.listFiles(dir)
339-
if err != nil {
340-
return nil, err
341-
}
342-
343-
var paths []string
344-
for p := range m {
345-
paths = append(paths, p)
346-
}
347-
sort.Strings(paths)
348-
return paths, nil
349-
}
350-
351-
// listFiles lists files in the given directory, returning a map of relative
352-
// path to contents and modification time.
353-
func (w *Workdir) listFiles(dir string) (map[string]fileID, error) {
354-
files := make(map[string]fileID)
355317
absDir := w.AbsPath(dir)
318+
var paths []string
356319
if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error {
357320
if err != nil {
358321
return err
359322
}
360-
if info.IsDir() {
361-
return nil
362-
}
363-
path := w.RelPath(fp)
364-
365-
data, err := ioutil.ReadFile(fp)
366-
if err != nil {
367-
return err
368-
}
369-
// The content returned by ioutil.ReadFile could be inconsistent with
370-
// info.ModTime(), due to a subsequent modification. See the documentation
371-
// for w.files for why we consider this to be benign.
372-
files[path] = fileID{
373-
hash: hashFile(data),
374-
mtime: info.ModTime(),
323+
if info.Mode()&(fs.ModeDir|fs.ModeSymlink) == 0 {
324+
paths = append(paths, w.RelPath(fp))
375325
}
376326
return nil
377327
}); err != nil {
378328
return nil, err
379329
}
380-
return files, nil
330+
sort.Strings(paths)
331+
return paths, nil
381332
}
382333

383334
// CheckForFileChanges walks the working directory and checks for any files
@@ -406,36 +357,82 @@ func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) {
406357
w.fileMu.Lock()
407358
defer w.fileMu.Unlock()
408359

409-
files, err := w.listFiles(".")
410-
if err != nil {
411-
return nil, err
412-
}
360+
newFiles := make(map[string]fileID)
413361
var evts []protocol.FileEvent
414-
// Check which files have been added or modified.
415-
for path, id := range files {
416-
oldID, ok := w.files[path]
417-
delete(w.files, path)
418-
var typ protocol.FileChangeType
419-
switch {
420-
case !ok:
421-
typ = protocol.Created
422-
case oldID != id:
423-
typ = protocol.Changed
424-
default:
425-
continue
362+
if err := filepath.Walk(string(w.RelativeTo), func(fp string, info os.FileInfo, err error) error {
363+
if err != nil {
364+
return err
426365
}
427-
evts = append(evts, protocol.FileEvent{
428-
URI: w.URI(path),
429-
Type: typ,
430-
})
366+
// Skip directories and symbolic links (which may be links to directories).
367+
//
368+
// The latter matters for repos like Kubernetes, which use symlinks.
369+
if info.Mode()&(fs.ModeDir|fs.ModeSymlink) != 0 {
370+
return nil
371+
}
372+
373+
// Opt: avoid reading the file if mtime is sufficently old to be reliable.
374+
//
375+
// If mtime is recent, it may not sufficiently identify the file contents:
376+
// a subsequent write could result in the same mtime. For these cases, we
377+
// must read the file contents.
378+
id := fileID{mtime: info.ModTime()}
379+
if time.Since(info.ModTime()) < 2*time.Second {
380+
data, err := ioutil.ReadFile(fp)
381+
if err != nil {
382+
return err
383+
}
384+
id.hash = hashFile(data)
385+
}
386+
path := w.RelPath(fp)
387+
newFiles[path] = id
388+
389+
if w.files != nil {
390+
oldID, ok := w.files[path]
391+
delete(w.files, path)
392+
switch {
393+
case !ok:
394+
evts = append(evts, protocol.FileEvent{
395+
URI: w.URI(path),
396+
Type: protocol.Created,
397+
})
398+
case oldID != id:
399+
changed := true
400+
401+
// Check whether oldID and id do not match because oldID was polled at
402+
// a recent enough to time such as to require hashing.
403+
//
404+
// In this case, read the content to check whether the file actually
405+
// changed.
406+
if oldID.mtime.Equal(id.mtime) && oldID.hash != "" && id.hash == "" {
407+
data, err := ioutil.ReadFile(fp)
408+
if err != nil {
409+
return err
410+
}
411+
if hashFile(data) == oldID.hash {
412+
changed = false
413+
}
414+
}
415+
if changed {
416+
evts = append(evts, protocol.FileEvent{
417+
URI: w.URI(path),
418+
Type: protocol.Changed,
419+
})
420+
}
421+
}
422+
}
423+
424+
return nil
425+
}); err != nil {
426+
return nil, err
431427
}
428+
432429
// Any remaining files must have been deleted.
433430
for path := range w.files {
434431
evts = append(evts, protocol.FileEvent{
435432
URI: w.URI(path),
436433
Type: protocol.Deleted,
437434
})
438435
}
439-
w.files = files
436+
w.files = newFiles
440437
return evts, nil
441438
}

gopls/internal/lsp/fake/workdir_test.go

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"context"
99
"io/ioutil"
1010
"os"
11-
"sort"
1211
"sync"
1312
"testing"
1413

@@ -37,8 +36,8 @@ func newWorkdir(t *testing.T, txt string) (*Workdir, *eventBuffer, func()) {
3736
if err != nil {
3837
t.Fatal(err)
3938
}
40-
wd := NewWorkdir(tmpdir)
41-
if err := wd.writeInitialFiles(UnpackTxt(txt)); err != nil {
39+
wd, err := NewWorkdir(tmpdir, UnpackTxt(txt))
40+
if err != nil {
4241
t.Fatal(err)
4342
}
4443
cleanup := func() {
@@ -162,35 +161,6 @@ func TestWorkdir_FileWatching(t *testing.T) {
162161
checkEvent(changeMap{"bar.go": protocol.Deleted})
163162
}
164163

165-
func TestWorkdir_ListFiles(t *testing.T) {
166-
wd, _, cleanup := newWorkdir(t, sharedData)
167-
defer cleanup()
168-
169-
checkFiles := func(dir string, want []string) {
170-
files, err := wd.listFiles(dir)
171-
if err != nil {
172-
t.Fatal(err)
173-
}
174-
sort.Strings(want)
175-
var got []string
176-
for p := range files {
177-
got = append(got, p)
178-
}
179-
sort.Strings(got)
180-
if len(got) != len(want) {
181-
t.Fatalf("ListFiles(): len = %d, want %d; got=%v; want=%v", len(got), len(want), got, want)
182-
}
183-
for i, f := range got {
184-
if f != want[i] {
185-
t.Errorf("ListFiles()[%d] = %s, want %s", i, f, want[i])
186-
}
187-
}
188-
}
189-
190-
checkFiles(".", []string{"go.mod", "nested/README.md"})
191-
checkFiles("nested", []string{"nested/README.md"})
192-
}
193-
194164
func TestWorkdir_CheckForFileChanges(t *testing.T) {
195165
t.Skip("broken on darwin-amd64-10_12")
196166
wd, events, cleanup := newWorkdir(t, sharedData)

gopls/internal/regtest/bench/bench_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,18 +164,18 @@ func getInstalledGopls() string {
164164
if *goplsCommit == "" {
165165
panic("must provide -gopls_commit")
166166
}
167-
toolsDir := filepath.Join(getTempDir(), "tools")
167+
toolsDir := filepath.Join(getTempDir(), "gopls_build")
168168
goplsPath := filepath.Join(toolsDir, "gopls", "gopls")
169169

170170
installGoplsOnce.Do(func() {
171-
log.Printf("installing gopls: checking out x/tools@%s\n", *goplsCommit)
171+
log.Printf("installing gopls: checking out x/tools@%s into %s\n", *goplsCommit, toolsDir)
172172
if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil {
173173
log.Fatal(err)
174174
}
175175

176176
log.Println("installing gopls: building...")
177177
bld := exec.Command("go", "build", ".")
178-
bld.Dir = filepath.Join(getTempDir(), "tools", "gopls")
178+
bld.Dir = filepath.Join(toolsDir, "gopls")
179179
if output, err := bld.CombinedOutput(); err != nil {
180180
log.Fatalf("building gopls: %v\n%s", err, output)
181181
}

gopls/internal/regtest/bench/completion_test.go

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import (
88
"fmt"
99
"testing"
1010

11+
"golang.org/x/tools/gopls/internal/lsp/fake"
1112
"golang.org/x/tools/gopls/internal/lsp/protocol"
1213
. "golang.org/x/tools/gopls/internal/lsp/regtest"
1314
)
1415

16+
// TODO(rfindley): update these completion tests to run on multiple repos.
17+
1518
type completionBenchOptions struct {
1619
file, locationRegexp string
1720

@@ -21,8 +24,8 @@ type completionBenchOptions struct {
2124
}
2225

2326
func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
24-
repo := repos["tools"]
25-
env := repo.newEnv(b)
27+
repo := getRepo(b, "tools")
28+
env := repo.newEnv(b, "completion.tools", fake.EditorConfig{})
2629
defer env.Close()
2730

2831
// Run edits required for this completion.
@@ -41,12 +44,7 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
4144
}
4245
}
4346

44-
b.ResetTimer()
45-
46-
// Use a subtest to ensure that benchmarkCompletion does not itself get
47-
// executed multiple times (as it is doing expensive environment
48-
// initialization).
49-
b.Run("completion", func(b *testing.B) {
47+
b.Run("tools", func(b *testing.B) {
5048
for i := 0; i < b.N; i++ {
5149
if options.beforeCompletion != nil {
5250
options.beforeCompletion(env)
@@ -56,7 +54,7 @@ func benchmarkCompletion(options completionBenchOptions, b *testing.B) {
5654
})
5755
}
5856

59-
// endPosInBuffer returns the position for last character in the buffer for
57+
// endRangeInBuffer returns the position for last character in the buffer for
6058
// the given file.
6159
func endRangeInBuffer(env *Env, name string) protocol.Range {
6260
buffer := env.BufferText(name)

0 commit comments

Comments
 (0)