Skip to content

Commit d444fa3

Browse files
committed
gopls/internal/lsp/cache: simplify canonical URI cache
The View's URI canonicalization cache entry, fileBase, used to hold a list of URIs even though only the first is ever needed. This change simplifies it to a pair of (URI, filename). This should reduce allocation slightly. Also: - Eliminate the unnecessary pointer type to further reduce allocation. - Use a dedicated mutex for the two maps. - Update the names to better reflect the purpose. - Document various failed approaches to optimization and two problems in the existing logic, one of correctness, the other of performance. Change-Id: Ic74ae4bae8864de292fe97d26c5f9aacf2367961 Reviewed-on: https://go-review.googlesource.com/c/tools/+/454435 Run-TryBot: Alan Donovan <adonovan@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Robert Findley <rfindley@google.com> gopls-CI: kokoro <noreply+kokoro@google.com>
1 parent 25fdb81 commit d444fa3

File tree

3 files changed

+77
-103
lines changed

3 files changed

+77
-103
lines changed

gopls/internal/lsp/cache/session.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
235235
folder: folder,
236236
moduleUpgrades: map[span.URI]map[string]string{},
237237
vulns: map[span.URI]*govulncheck.Result{},
238-
filesByURI: map[span.URI]*fileBase{},
239-
filesByBase: map[string][]*fileBase{},
238+
filesByURI: make(map[span.URI]span.URI),
239+
filesByBase: make(map[string][]canonicalURI),
240240
rootURI: root,
241241
explicitGowork: goworkURI,
242242
workspaceInformation: *wsInfo,
@@ -510,8 +510,8 @@ func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModif
510510

511511
// Apply the changes to all affected views.
512512
for _, view := range changedViews {
513-
// Make sure that the file is added to the view.
514-
_ = view.getFile(c.URI)
513+
// Make sure that the file is added to the view's knownFiles set.
514+
view.canonicalURI(c.URI, true) // ignore result
515515
if _, ok := views[view]; !ok {
516516
views[view] = make(map[span.URI]*fileChange)
517517
}

gopls/internal/lsp/cache/snapshot.go

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,12 +1287,12 @@ func (s *snapshot) isWorkspacePackage(id PackageID) bool {
12871287
}
12881288

12891289
func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle {
1290-
f := s.view.getFile(uri)
1290+
uri, _ = s.view.canonicalURI(uri, true)
12911291

12921292
s.mu.Lock()
12931293
defer s.mu.Unlock()
12941294

1295-
result, _ := s.files.Get(f.URI())
1295+
result, _ := s.files.Get(uri)
12961296
return result
12971297
}
12981298

@@ -1302,32 +1302,29 @@ func (s *snapshot) FindFile(uri span.URI) source.VersionedFileHandle {
13021302
// GetVersionedFile succeeds even if the file does not exist. A non-nil error return
13031303
// indicates some type of internal error, for example if ctx is cancelled.
13041304
func (s *snapshot) GetVersionedFile(ctx context.Context, uri span.URI) (source.VersionedFileHandle, error) {
1305-
f := s.view.getFile(uri)
1305+
uri, _ = s.view.canonicalURI(uri, true)
13061306

13071307
s.mu.Lock()
13081308
defer s.mu.Unlock()
1309-
return s.getFileLocked(ctx, f)
1310-
}
1311-
1312-
// GetFile implements the fileSource interface by wrapping GetVersionedFile.
1313-
func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
1314-
return s.GetVersionedFile(ctx, uri)
1315-
}
13161309

1317-
func (s *snapshot) getFileLocked(ctx context.Context, f *fileBase) (source.VersionedFileHandle, error) {
1318-
if fh, ok := s.files.Get(f.URI()); ok {
1310+
if fh, ok := s.files.Get(uri); ok {
13191311
return fh, nil
13201312
}
13211313

1322-
fh, err := s.view.cache.getFile(ctx, f.URI()) // read the file
1314+
fh, err := s.view.cache.getFile(ctx, uri) // read the file
13231315
if err != nil {
13241316
return nil, err
13251317
}
13261318
closed := &closedFile{fh}
1327-
s.files.Set(f.URI(), closed)
1319+
s.files.Set(uri, closed)
13281320
return closed, nil
13291321
}
13301322

1323+
// GetFile implements the fileSource interface by wrapping GetVersionedFile.
1324+
func (s *snapshot) GetFile(ctx context.Context, uri span.URI) (source.FileHandle, error) {
1325+
return s.GetVersionedFile(ctx, uri)
1326+
}
1327+
13311328
func (s *snapshot) IsOpen(uri span.URI) bool {
13321329
s.mu.Lock()
13331330
defer s.mu.Unlock()

gopls/internal/lsp/cache/view.go

Lines changed: 62 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,12 @@ type View struct {
6565

6666
vulns map[span.URI]*govulncheck.Result
6767

68-
// keep track of files by uri and by basename, a single file may be mapped
69-
// to multiple uris, and the same basename may map to multiple files
70-
filesByURI map[span.URI]*fileBase
71-
filesByBase map[string][]*fileBase
68+
// filesByURI maps URIs to the canonical URI for the file it denotes.
69+
// We also keep a set of candidates for a given basename
70+
// to reduce the set of pairs that need to be tested for sameness.
71+
filesByMu sync.Mutex
72+
filesByURI map[span.URI]span.URI // key is noncanonical URI (alias)
73+
filesByBase map[string][]canonicalURI // key is basename
7274

7375
// initCancelFirstAttempt can be used to terminate the view's first
7476
// attempt at initialization.
@@ -205,28 +207,6 @@ const (
205207
tempModfile
206208
)
207209

208-
// fileBase holds the common functionality for all files.
209-
// It is intended to be embedded in the file implementations
210-
type fileBase struct {
211-
uris []span.URI
212-
fname string
213-
214-
view *View
215-
}
216-
217-
func (f *fileBase) URI() span.URI {
218-
return f.uris[0]
219-
}
220-
221-
func (f *fileBase) filename() string {
222-
return f.fname
223-
}
224-
225-
func (f *fileBase) addURI(uri span.URI) int {
226-
f.uris = append(f.uris, uri)
227-
return len(f.uris)
228-
}
229-
230210
func (v *View) ID() string { return v.id }
231211

232212
// tempModFile creates a temporary go.mod file based on the contents
@@ -493,18 +473,6 @@ func (v *View) filterFunc() func(span.URI) bool {
493473
}
494474
}
495475

496-
func (v *View) mapFile(uri span.URI, f *fileBase) {
497-
v.filesByURI[uri] = f
498-
if f.addURI(uri) == 1 {
499-
basename := basename(f.filename())
500-
v.filesByBase[basename] = append(v.filesByBase[basename], f)
501-
}
502-
}
503-
504-
func basename(filename string) string {
505-
return strings.ToLower(filepath.Base(filename))
506-
}
507-
508476
func (v *View) relevantChange(c source.FileModification) bool {
509477
// If the file is known to the view, the change is relevant.
510478
if v.knownFile(c.URI) {
@@ -530,64 +498,73 @@ func (v *View) relevantChange(c source.FileModification) bool {
530498
return v.contains(c.URI)
531499
}
532500

501+
// knownFile reports whether the specified valid URI (or an alias) is known to the view.
533502
func (v *View) knownFile(uri span.URI) bool {
534-
v.mu.Lock()
535-
defer v.mu.Unlock()
536-
537-
f, err := v.findFile(uri)
538-
return f != nil && err == nil
503+
_, known := v.canonicalURI(uri, false)
504+
return known
539505
}
540506

541-
// getFile returns a file for the given URI.
542-
func (v *View) getFile(uri span.URI) *fileBase {
543-
v.mu.Lock()
544-
defer v.mu.Unlock()
545-
546-
f, _ := v.findFile(uri)
547-
if f != nil {
548-
return f
549-
}
550-
f = &fileBase{
551-
view: v,
552-
fname: uri.Filename(),
553-
}
554-
v.mapFile(uri, f)
555-
return f
507+
// TODO(adonovan): opt: eliminate 'filename' optimization. I doubt the
508+
// cost of allocation is significant relative to the
509+
// stat/open/fstat/close operations that follow on Windows.
510+
type canonicalURI struct {
511+
uri span.URI
512+
filename string // = uri.Filename(), an optimization (on Windows)
556513
}
557514

558-
// findFile checks the cache for any file matching the given uri.
515+
// canonicalURI returns the canonical URI that denotes the same file
516+
// as uri, which may differ due to case insensitivity, unclean paths,
517+
// soft or hard links, and so on. If no previous alias was found, or
518+
// the file is missing, insert determines whether to make uri the
519+
// canonical representative of the file or to return false.
559520
//
560-
// An error is only returned for an irreparable failure, for example, if the
561-
// filename in question does not exist.
562-
func (v *View) findFile(uri span.URI) (*fileBase, error) {
563-
if f := v.filesByURI[uri]; f != nil {
564-
// a perfect match
565-
return f, nil
566-
}
567-
// no exact match stored, time to do some real work
568-
// check for any files with the same basename
569-
fname := uri.Filename()
570-
basename := basename(fname)
521+
// The cache grows indefinitely without invalidation: file system
522+
// operations may cause two URIs that used to denote the same file to
523+
// no longer to do so. Also, the basename cache grows without bound.
524+
// TODO(adonovan): fix both bugs.
525+
func (v *View) canonicalURI(uri span.URI, insert bool) (span.URI, bool) {
526+
v.filesByMu.Lock()
527+
defer v.filesByMu.Unlock()
528+
529+
// Have we seen this exact URI before?
530+
if canonical, ok := v.filesByURI[uri]; ok {
531+
return canonical, true
532+
}
533+
534+
// Inspect all candidates with the same lowercase basename.
535+
// This heuristic is easily defeated by symbolic links to files.
536+
// Files with some basenames (e.g. doc.go) are very numerous.
537+
//
538+
// The set of candidates grows without bound, and incurs a
539+
// linear sequence of SameFile queries to the file system.
540+
//
541+
// It is tempting to fetch the device/inode pair that
542+
// uniquely identifies a file once, and then compare those
543+
// pairs, but that would cause us to cache stale file system
544+
// state (in addition to the filesByURI staleness).
545+
filename := uri.Filename()
546+
basename := strings.ToLower(filepath.Base(filename))
571547
if candidates := v.filesByBase[basename]; candidates != nil {
572-
pathStat, err := os.Stat(fname)
573-
if os.IsNotExist(err) {
574-
return nil, err
575-
}
576-
if err != nil {
577-
return nil, nil // the file may exist, return without an error
578-
}
579-
for _, c := range candidates {
580-
if cStat, err := os.Stat(c.filename()); err == nil {
581-
if os.SameFile(pathStat, cStat) {
582-
// same file, map it
583-
v.mapFile(uri, c)
584-
return c, nil
548+
if pathStat, _ := os.Stat(filename); pathStat != nil {
549+
for _, c := range candidates {
550+
if cStat, _ := os.Stat(c.filename); cStat != nil {
551+
// On Windows, SameFile is more expensive as it must
552+
// open the file and use the equivalent of fstat(2).
553+
if os.SameFile(pathStat, cStat) {
554+
v.filesByURI[uri] = c.uri
555+
return c.uri, true
556+
}
585557
}
586558
}
587559
}
588560
}
589-
// no file with a matching name was found, it wasn't in our cache
590-
return nil, nil
561+
562+
// No candidates, stat failed, or no candidate matched.
563+
if insert {
564+
v.filesByURI[uri] = uri
565+
v.filesByBase[basename] = append(v.filesByBase[basename], canonicalURI{uri, filename})
566+
}
567+
return uri, insert
591568
}
592569

593570
// shutdown releases resources associated with the view, and waits for ongoing

0 commit comments

Comments
 (0)