Skip to content

Commit 787e720

Browse files
committed
gopls/internal/lsp: optimize checks for ignored files
Optimize checking for ignored files to avoid unnecessary checks, and only build prefixes once. Along the way, fix a bug where path segments were not handled correctly in the ignore check. Encapsulate the check to make this easy to test. As a result, the DiagnoseChange/google-cloud-go benchmark improved ~5x from ~1.5s to 300ms. Also remove span.Dir, which tended to lead to unnecessary filepath->span->filepath conversions. Inline it in the one place where it was correct. For golang/go#60089 Change-Id: Id24d05b504b43e6a6d9b77b5b578583e1351de31 Reviewed-on: https://go-review.googlesource.com/c/tools/+/494097 Reviewed-by: Alan Donovan <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Robert Findley <[email protected]> gopls-CI: kokoro <[email protected]>
1 parent 4ed7de1 commit 787e720

File tree

5 files changed

+87
-24
lines changed

5 files changed

+87
-24
lines changed

gopls/internal/lsp/cache/snapshot.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ type snapshot struct {
185185

186186
// pkgIndex is an index of package IDs, for efficient storage of typerefs.
187187
pkgIndex *typerefs.PackageIndex
188+
189+
// Only compute module prefixes once, as they are used with high frequency to
190+
// detect ignored files.
191+
ignoreFilterOnce sync.Once
192+
ignoreFilter *ignoreFilter
188193
}
189194

190195
var globalSnapshotID uint64
@@ -1195,7 +1200,7 @@ func (s *snapshot) GoModForFile(uri span.URI) span.URI {
11951200
func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI {
11961201
var match span.URI
11971202
for modURI := range modFiles {
1198-
if !source.InDir(span.Dir(modURI).Filename(), uri.Filename()) {
1203+
if !source.InDir(filepath.Dir(modURI.Filename()), uri.Filename()) {
11991204
continue
12001205
}
12011206
if len(modURI) > len(match) {

gopls/internal/lsp/cache/view.go

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -588,22 +588,57 @@ func (v *View) shutdown() {
588588
v.snapshotWG.Wait()
589589
}
590590

591+
// While go list ./... skips directories starting with '.', '_', or 'testdata',
592+
// gopls may still load them via file queries. Explicitly filter them out.
591593
func (s *snapshot) IgnoredFile(uri span.URI) bool {
592-
filename := uri.Filename()
593-
var prefixes []string
594-
if len(s.workspaceModFiles) == 0 {
595-
for _, entry := range filepath.SplitList(s.view.gopath) {
596-
prefixes = append(prefixes, filepath.Join(entry, "src"))
594+
// Fast path: if uri doesn't contain '.', '_', or 'testdata', it is not
595+
// possible that it is ignored.
596+
{
597+
uriStr := string(uri)
598+
if !strings.Contains(uriStr, ".") && !strings.Contains(uriStr, "_") && !strings.Contains(uriStr, "testdata") {
599+
return false
597600
}
598-
} else {
599-
prefixes = append(prefixes, s.view.gomodcache)
600-
for m := range s.workspaceModFiles {
601-
prefixes = append(prefixes, span.Dir(m).Filename())
601+
}
602+
603+
s.ignoreFilterOnce.Do(func() {
604+
var dirs []string
605+
if len(s.workspaceModFiles) == 0 {
606+
for _, entry := range filepath.SplitList(s.view.gopath) {
607+
dirs = append(dirs, filepath.Join(entry, "src"))
608+
}
609+
} else {
610+
dirs = append(dirs, s.view.gomodcache)
611+
for m := range s.workspaceModFiles {
612+
dirs = append(dirs, filepath.Dir(m.Filename()))
613+
}
602614
}
615+
s.ignoreFilter = newIgnoreFilter(dirs)
616+
})
617+
618+
return s.ignoreFilter.ignored(uri.Filename())
619+
}
620+
621+
// An ignoreFilter implements go list's exclusion rules via its 'ignored' method.
622+
type ignoreFilter struct {
623+
prefixes []string // root dirs, ending in filepath.Separator
624+
}
625+
626+
// newIgnoreFilter returns a new ignoreFilter implementing exclusion rules
627+
// relative to the provided directories.
628+
func newIgnoreFilter(dirs []string) *ignoreFilter {
629+
f := new(ignoreFilter)
630+
for _, d := range dirs {
631+
f.prefixes = append(f.prefixes, filepath.Clean(d)+string(filepath.Separator))
603632
}
604-
for _, prefix := range prefixes {
605-
if strings.HasPrefix(filename, prefix) {
606-
return checkIgnored(filename[len(prefix):])
633+
return f
634+
}
635+
636+
func (f *ignoreFilter) ignored(filename string) bool {
637+
for _, prefix := range f.prefixes {
638+
if suffix := strings.TrimPrefix(filename, prefix); suffix != filename {
639+
if checkIgnored(suffix) {
640+
return true
641+
}
607642
}
608643
}
609644
return false
@@ -615,6 +650,8 @@ func (s *snapshot) IgnoredFile(uri span.URI) bool {
615650
// Directory and file names that begin with "." or "_" are ignored
616651
// by the go tool, as are directories named "testdata".
617652
func checkIgnored(suffix string) bool {
653+
// Note: this could be further optimized by writing a HasSegment helper, a
654+
// segment-boundary respecting variant of strings.Contains.
618655
for _, component := range strings.Split(suffix, string(filepath.Separator)) {
619656
if len(component) == 0 {
620657
continue
@@ -911,7 +948,7 @@ func (v *View) workingDir() span.URI {
911948
// TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting
912949
// entirely.
913950
if v.Options().ExpandWorkspaceToModule && v.gomod != "" {
914-
return span.Dir(v.gomod)
951+
return span.URIFromPath(filepath.Dir(v.gomod.Filename()))
915952
}
916953
return v.folder
917954
}

gopls/internal/lsp/cache/view_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,32 @@ func toJSON(x interface{}) string {
276276
b, _ := json.MarshalIndent(x, "", " ")
277277
return string(b)
278278
}
279+
280+
func TestIgnoreFilter(t *testing.T) {
281+
tests := []struct {
282+
dirs []string
283+
path string
284+
want bool
285+
}{
286+
{[]string{"a"}, "a/testdata/foo", true},
287+
{[]string{"a"}, "a/_ignore/foo", true},
288+
{[]string{"a"}, "a/.ignore/foo", true},
289+
{[]string{"a"}, "b/testdata/foo", false},
290+
{[]string{"a"}, "testdata/foo", false},
291+
{[]string{"a", "b"}, "b/testdata/foo", true},
292+
{[]string{"a"}, "atestdata/foo", false},
293+
}
294+
295+
for _, test := range tests {
296+
// convert to filepaths, for convenience
297+
for i, dir := range test.dirs {
298+
test.dirs[i] = filepath.FromSlash(dir)
299+
}
300+
test.path = filepath.FromSlash(test.path)
301+
302+
f := newIgnoreFilter(test.dirs)
303+
if got := f.ignored(test.path); got != test.want {
304+
t.Errorf("newIgnoreFilter(%q).ignore(%q) = %t, want %t", test.dirs, test.path, got, test.want)
305+
}
306+
}
307+
}

gopls/internal/lsp/diagnostics.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -365,10 +365,10 @@ func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze
365365
var hasNonIgnored, hasOpenFile bool
366366
for _, uri := range m.CompiledGoFiles {
367367
seen[uri] = struct{}{}
368-
if !snapshot.IgnoredFile(uri) {
368+
if !hasNonIgnored && !snapshot.IgnoredFile(uri) {
369369
hasNonIgnored = true
370370
}
371-
if snapshot.IsOpen(uri) {
371+
if !hasOpenFile && snapshot.IsOpen(uri) {
372372
hasOpenFile = true
373373
}
374374
}

gopls/internal/span/uri.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,3 @@ func isWindowsDriveURIPath(uri string) bool {
175175
}
176176
return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
177177
}
178-
179-
// Dir returns the URI for the directory containing uri. Dir panics if uri is
180-
// not a file uri.
181-
//
182-
// TODO(rfindley): add a unit test for various edge cases.
183-
func Dir(uri URI) URI {
184-
return URIFromPath(filepath.Dir(uri.Filename()))
185-
}

0 commit comments

Comments
 (0)