Skip to content

Commit 36c4f98

Browse files
committed
gopls/internal/lsp/cache: simplify tracking of snapshot directories
Great care was taken to track known directories in the snapshot without blocking in snapshot.Clone, introducing significant complexity. This complexity can be avoided by instead keeping track of observed directories as files are set in the snapshot. These directories need only be reset when files are deleted from the snapshot, which is a relatively rare event. Also rename filesMap->fileMap, and move to filemap.go, with a new unit test. This reduces some path dependence on seen files, as the set of directories is well defined and depends only on the files in the snapshot. Previously, when a file was removed, gopls called Stat to check if the directory still existed, which leads to path dependence: an add+remove was not the same as nothing at all. Updates golang/go#57558 Change-Id: I5fd89ce870fa7d8afd19471d150396b1e4ea8875 Reviewed-on: https://go-review.googlesource.com/c/tools/+/525616 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent fe324ac commit 36c4f98

File tree

8 files changed

+324
-301
lines changed

8 files changed

+324
-301
lines changed

gopls/internal/lsp/cache/filemap.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Copyright 2022 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 cache
6+
7+
import (
8+
"path/filepath"
9+
10+
"golang.org/x/tools/gopls/internal/lsp/source"
11+
"golang.org/x/tools/gopls/internal/span"
12+
"golang.org/x/tools/internal/persistent"
13+
)
14+
15+
// A fileMap maps files in the snapshot, with some additional bookkeeping:
16+
// It keeps track of overlays as well as directories containing any observed
17+
// file.
18+
type fileMap struct {
19+
files *persistent.Map[span.URI, source.FileHandle]
20+
overlays *persistent.Map[span.URI, *Overlay] // the subset of files that are overlays
21+
dirs *persistent.Set[string] // all dirs containing files; if nil, dirs have not been initialized
22+
}
23+
24+
func newFileMap() *fileMap {
25+
return &fileMap{
26+
files: new(persistent.Map[span.URI, source.FileHandle]),
27+
overlays: new(persistent.Map[span.URI, *Overlay]),
28+
dirs: new(persistent.Set[string]),
29+
}
30+
}
31+
32+
func (m *fileMap) Clone() *fileMap {
33+
m2 := &fileMap{
34+
files: m.files.Clone(),
35+
overlays: m.overlays.Clone(),
36+
}
37+
if m.dirs != nil {
38+
m2.dirs = m.dirs.Clone()
39+
}
40+
return m2
41+
}
42+
43+
func (m *fileMap) Destroy() {
44+
m.files.Destroy()
45+
m.overlays.Destroy()
46+
if m.dirs != nil {
47+
m.dirs.Destroy()
48+
}
49+
}
50+
51+
// Get returns the file handle mapped by the given key, or (nil, false) if the
52+
// key is not present.
53+
func (m *fileMap) Get(key span.URI) (source.FileHandle, bool) {
54+
return m.files.Get(key)
55+
}
56+
57+
// Range calls f for each (uri, fh) in the map.
58+
func (m *fileMap) Range(f func(uri span.URI, fh source.FileHandle)) {
59+
m.files.Range(f)
60+
}
61+
62+
// Set stores the given file handle for key, updating overlays and directories
63+
// accordingly.
64+
func (m *fileMap) Set(key span.URI, fh source.FileHandle) {
65+
m.files.Set(key, fh, nil)
66+
67+
// update overlays
68+
if o, ok := fh.(*Overlay); ok {
69+
m.overlays.Set(key, o, nil)
70+
} else {
71+
// Setting a non-overlay must delete the corresponding overlay, to preserve
72+
// the accuracy of the overlay set.
73+
m.overlays.Delete(key)
74+
}
75+
76+
// update dirs
77+
if m.dirs == nil {
78+
m.initDirs()
79+
} else {
80+
m.addDirs(key)
81+
}
82+
}
83+
84+
func (m *fileMap) initDirs() {
85+
m.dirs = new(persistent.Set[string])
86+
m.files.Range(func(u span.URI, _ source.FileHandle) {
87+
m.addDirs(u)
88+
})
89+
}
90+
91+
// addDirs adds all directories containing u to the dirs set.
92+
func (m *fileMap) addDirs(u span.URI) {
93+
dir := filepath.Dir(u.Filename())
94+
for dir != "" && !m.dirs.Contains(dir) {
95+
m.dirs.Add(dir)
96+
dir = filepath.Dir(dir)
97+
}
98+
}
99+
100+
// Delete removes a file from the map, and updates overlays and dirs
101+
// accordingly.
102+
func (m *fileMap) Delete(key span.URI) {
103+
m.files.Delete(key)
104+
m.overlays.Delete(key)
105+
106+
// Deleting a file may cause the set of dirs to shrink; therefore we must
107+
// re-evaluate the dir set.
108+
//
109+
// Do this lazily, to avoid work if there are multiple deletions in a row.
110+
if m.dirs != nil {
111+
m.dirs.Destroy()
112+
m.dirs = nil
113+
}
114+
}
115+
116+
// Overlays returns a new unordered array of overlay files.
117+
func (m *fileMap) Overlays() []*Overlay {
118+
var overlays []*Overlay
119+
m.overlays.Range(func(_ span.URI, o *Overlay) {
120+
overlays = append(overlays, o)
121+
})
122+
return overlays
123+
}
124+
125+
// Dirs reports returns the set of dirs observed by the fileMap.
126+
//
127+
// This operation mutates the fileMap.
128+
// The result must not be mutated by the caller.
129+
func (m *fileMap) Dirs() *persistent.Set[string] {
130+
if m.dirs == nil {
131+
m.initDirs()
132+
}
133+
return m.dirs
134+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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 cache
6+
7+
import (
8+
"path/filepath"
9+
"sort"
10+
"strings"
11+
"testing"
12+
13+
"github.com/google/go-cmp/cmp"
14+
"golang.org/x/tools/gopls/internal/lsp/source"
15+
"golang.org/x/tools/gopls/internal/span"
16+
)
17+
18+
func TestFileMap(t *testing.T) {
19+
const (
20+
set = iota
21+
del
22+
)
23+
type op struct {
24+
op int // set or remove
25+
path string
26+
overlay bool
27+
}
28+
tests := []struct {
29+
label string
30+
ops []op
31+
wantFiles []string
32+
wantOverlays []string
33+
wantDirs []string
34+
}{
35+
{"empty", nil, nil, nil, nil},
36+
{"singleton", []op{
37+
{set, "/a/b", false},
38+
}, []string{"/a/b"}, nil, []string{"/", "/a"}},
39+
{"overlay", []op{
40+
{set, "/a/b", true},
41+
}, []string{"/a/b"}, []string{"/a/b"}, []string{"/", "/a"}},
42+
{"replace overlay", []op{
43+
{set, "/a/b", true},
44+
{set, "/a/b", false},
45+
}, []string{"/a/b"}, nil, []string{"/", "/a"}},
46+
{"multi dir", []op{
47+
{set, "/a/b", false},
48+
{set, "/c/d", false},
49+
}, []string{"/a/b", "/c/d"}, nil, []string{"/", "/a", "/c"}},
50+
{"empty dir", []op{
51+
{set, "/a/b", false},
52+
{set, "/c/d", false},
53+
{del, "/a/b", false},
54+
}, []string{"/c/d"}, nil, []string{"/", "/c"}},
55+
}
56+
57+
// Normalize paths for windows compatibility.
58+
normalize := func(path string) string {
59+
return strings.TrimPrefix(filepath.ToSlash(path), "C:") // the span packages adds 'C:'
60+
}
61+
62+
for _, test := range tests {
63+
t.Run(test.label, func(t *testing.T) {
64+
m := newFileMap()
65+
for _, op := range test.ops {
66+
uri := span.URIFromPath(filepath.FromSlash(op.path))
67+
switch op.op {
68+
case set:
69+
var fh source.FileHandle
70+
if op.overlay {
71+
fh = &Overlay{uri: uri}
72+
} else {
73+
fh = &DiskFile{uri: uri}
74+
}
75+
m.Set(uri, fh)
76+
case del:
77+
m.Delete(uri)
78+
}
79+
}
80+
81+
var gotFiles []string
82+
m.Range(func(uri span.URI, _ source.FileHandle) {
83+
gotFiles = append(gotFiles, normalize(uri.Filename()))
84+
})
85+
sort.Strings(gotFiles)
86+
if diff := cmp.Diff(test.wantFiles, gotFiles); diff != "" {
87+
t.Errorf("Files mismatch (-want +got):\n%s", diff)
88+
}
89+
90+
var gotOverlays []string
91+
for _, o := range m.Overlays() {
92+
gotOverlays = append(gotOverlays, normalize(o.URI().Filename()))
93+
}
94+
if diff := cmp.Diff(test.wantOverlays, gotOverlays); diff != "" {
95+
t.Errorf("Overlays mismatch (-want +got):\n%s", diff)
96+
}
97+
98+
var gotDirs []string
99+
m.Dirs().Range(func(dir string) {
100+
gotDirs = append(gotDirs, normalize(dir))
101+
})
102+
sort.Strings(gotDirs)
103+
if diff := cmp.Diff(test.wantDirs, gotDirs); diff != "" {
104+
t.Errorf("Dirs mismatch (-want +got):\n%s", diff)
105+
}
106+
})
107+
}
108+
}

gopls/internal/lsp/cache/maps.go

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)