internal/lsp/cache: move to a model of caching in a snapshot
This change moves from caching package information the file object to
caching in a map that gets invalidated when content changes.
This simplifies cache invalidation and reduces the number of fields
guarded by the (*goFile).mu lock.
Change-Id: I33fef6e0b18badb97e49052d9d6e3c15047c4b63
Reviewed-on: https://go-review.googlesource.com/c/tools/+/196984
Run-TryBot: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Ian Cottrell <iancottrell@google.com>
diff --git a/internal/lsp/cache/gofile.go b/internal/lsp/cache/gofile.go
index e368fa3..a08cb19 100644
--- a/internal/lsp/cache/gofile.go
+++ b/internal/lsp/cache/gofile.go
@@ -11,6 +11,7 @@
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/telemetry"
+ "golang.org/x/tools/internal/span"
errors "golang.org/x/xerrors"
)
@@ -27,9 +28,6 @@
missingImports map[packagePath]struct{}
imports []*ast.ImportSpec
-
- cphs map[packageKey]*checkPackageHandle
- meta map[packageID]*metadata
}
type packageKey struct {
@@ -37,117 +35,70 @@
mode source.ParseMode
}
-func (f *goFile) CheckPackageHandles(ctx context.Context) ([]source.CheckPackageHandle, error) {
+func (f *goFile) CheckPackageHandles(ctx context.Context) (cphs []source.CheckPackageHandle, err error) {
ctx = telemetry.File.With(ctx, f.URI())
fh := f.Handle(ctx)
- if f.isDirty(ctx, fh) {
- if err := f.view.loadParseTypecheck(ctx, f, fh); err != nil {
+ cphs = f.isDirty(ctx, fh)
+ if len(cphs) == 0 {
+ cphs, err = f.view.loadParseTypecheck(ctx, f, fh)
+ if err != nil {
return nil, err
}
}
-
- f.mu.Lock()
- defer f.mu.Unlock()
-
- var results []source.CheckPackageHandle
- seenIDs := make(map[string]bool)
- for _, cph := range f.cphs {
- if seenIDs[cph.ID()] {
- continue
- }
- if cph.mode() < source.ParseFull {
- continue
- }
- results = append(results, cph)
- seenIDs[cph.ID()] = true
- }
- if len(results) == 0 {
+ if len(cphs) == 0 {
return nil, errors.Errorf("no CheckPackageHandles for %s", f.URI())
}
- return results, nil
+ return cphs, nil
}
-func (f *goFile) GetActiveReverseDeps(ctx context.Context) (files []source.GoFile) {
- seen := make(map[packageID]struct{}) // visited packages
- results := make(map[*goFile]struct{})
-
- f.view.mu.Lock()
- defer f.view.mu.Unlock()
-
- f.view.mcache.mu.Lock()
- defer f.view.mcache.mu.Unlock()
-
- for _, m := range f.metadata() {
- f.view.reverseDeps(ctx, seen, results, m.id)
- for f := range results {
- if f == nil {
- continue
- }
- // Don't return any of the active files in this package.
- f.mu.Lock()
- _, ok := f.meta[m.id]
- f.mu.Unlock()
- if ok {
- continue
- }
-
- files = append(files, f)
+func (v *view) GetActiveReverseDeps(ctx context.Context, uri span.URI) (results []source.CheckPackageHandle) {
+ var (
+ rdeps = v.reverseDependencies(ctx, uri)
+ files = v.openFiles(ctx, rdeps)
+ seen = make(map[span.URI]struct{})
+ )
+ for _, f := range files {
+ if _, ok := seen[f.URI()]; ok {
+ continue
}
- }
- return files
-}
-
-func (v *view) reverseDeps(ctx context.Context, seen map[packageID]struct{}, results map[*goFile]struct{}, id packageID) {
- if _, ok := seen[id]; ok {
- return
- }
- seen[id] = struct{}{}
- m, ok := v.mcache.packages[id]
- if !ok {
- return
- }
- for _, uri := range m.files {
- // Call unlocked version of getFile since we hold the lock on the view.
- if f, err := v.getFile(ctx, uri, source.Go); err == nil && v.session.IsOpen(uri) {
- results[f.(*goFile)] = struct{}{}
+ gof, ok := f.(source.GoFile)
+ if !ok {
+ continue
}
+ cphs, err := gof.CheckPackageHandles(ctx)
+ if err != nil {
+ continue
+ }
+ cph := source.WidestCheckPackageHandle(cphs)
+ for _, ph := range cph.Files() {
+ seen[ph.File().Identity().URI] = struct{}{}
+ }
+ results = append(results, cph)
}
- for parentID := range m.parents {
- v.reverseDeps(ctx, seen, results, parentID)
- }
-}
-
-// metadata assumes that the caller holds the f.mu lock.
-func (f *goFile) metadata() []*metadata {
- result := make([]*metadata, 0, len(f.meta))
- for _, m := range f.meta {
- result = append(result, m)
- }
- return result
+ return results
}
// isDirty is true if the file needs to be type-checked.
// It assumes that the file's view's mutex is held by the caller.
-func (f *goFile) isDirty(ctx context.Context, fh source.FileHandle) bool {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- if len(f.meta) == 0 || len(f.cphs) == 0 {
- return true
+func (f *goFile) isDirty(ctx context.Context, fh source.FileHandle) []source.CheckPackageHandle {
+ meta, cphs := f.view.getSnapshot(f.URI())
+ if len(meta) == 0 {
+ return nil
}
- if len(f.missingImports) > 0 {
- return true
- }
- for key, cph := range f.cphs {
+ var results []source.CheckPackageHandle
+ for key, cph := range cphs {
+ // If we're explicitly checking if a file needs to be type-checked,
+ // we need it to be fully parsed.
if key.mode != source.ParseFull {
continue
}
+ // Check if there is a fully-parsed package to which this file belongs.
for _, file := range cph.Files() {
if file.File().Identity() == fh.Identity() {
- return false
+ results = append(results, cph)
}
}
}
- return true
+ return results
}
diff --git a/internal/lsp/cache/load.go b/internal/lsp/cache/load.go
index fc2f399..fad0ec9 100644
--- a/internal/lsp/cache/load.go
+++ b/internal/lsp/cache/load.go
@@ -19,62 +19,58 @@
errors "golang.org/x/xerrors"
)
-func (view *view) loadParseTypecheck(ctx context.Context, f *goFile, fh source.FileHandle) error {
+func (v *view) loadParseTypecheck(ctx context.Context, f *goFile, fh source.FileHandle) ([]source.CheckPackageHandle, error) {
ctx, done := trace.StartSpan(ctx, "cache.view.loadParseTypeCheck", telemetry.URI.Of(f.URI()))
defer done()
- meta, err := view.load(ctx, f, fh)
+ meta, err := v.load(ctx, f, fh)
if err != nil {
- return err
+ return nil, err
}
+ var (
+ cphs []*checkPackageHandle
+ results []source.CheckPackageHandle
+ )
for _, m := range meta {
imp := &importer{
- view: view,
- config: view.Config(ctx),
+ view: v,
+ config: v.Config(ctx),
seen: make(map[packageID]struct{}),
topLevelPackageID: m.id,
}
cph, err := imp.checkPackageHandle(ctx, m)
if err != nil {
- log.Error(ctx, "loadParseTypeCheck: failed to get CheckPackageHandle", err, telemetry.Package.Of(m.id))
- continue
+ return nil, err
}
- // Cache the package type information for the top-level package.
for _, ph := range cph.files {
- file, _, _, err := ph.Parse(ctx)
- if err != nil {
- return err
- }
- f, err := imp.view.GetFile(ctx, ph.File().Identity().URI)
- if err != nil {
- return errors.Errorf("no such file %s: %v", ph.File().Identity().URI, err)
- }
- gof, ok := f.(*goFile)
- if !ok {
- return errors.Errorf("%s is not a Go file", ph.File().Identity().URI)
- }
- if err := cachePerFile(ctx, gof, ph.Mode(), file.Imports, cph); err != nil {
- return err
+ if err := v.cachePerFile(ctx, ph); err != nil {
+ return nil, err
}
}
+ cphs = append(cphs, cph)
+ results = append(results, cph)
}
- return nil
+ // Cache the package type information for the top-level package.
+ v.updatePackages(cphs)
+ return results, nil
}
-func cachePerFile(ctx context.Context, f *goFile, mode source.ParseMode, imports []*ast.ImportSpec, cph *checkPackageHandle) error {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- f.imports = imports
-
- if f.cphs == nil {
- f.cphs = make(map[packageKey]*checkPackageHandle)
+func (v *view) cachePerFile(ctx context.Context, ph source.ParseGoHandle) error {
+ file, _, _, err := ph.Parse(ctx)
+ if err != nil {
+ return err
}
- f.cphs[packageKey{
- id: cph.m.id,
- mode: mode,
- }] = cph
-
+ f, err := v.GetFile(ctx, ph.File().Identity().URI)
+ if err != nil {
+ return err
+ }
+ gof, ok := f.(*goFile)
+ if !ok {
+ return errors.Errorf("%s is not a Go file", ph.File().Identity().URI)
+ }
+ gof.mu.Lock()
+ gof.imports = file.Imports
+ gof.mu.Unlock()
return nil
}
@@ -82,12 +78,6 @@
ctx, done := trace.StartSpan(ctx, "cache.view.load", telemetry.URI.Of(f.URI()))
defer done()
- view.mu.Lock()
- defer view.mu.Unlock()
-
- view.mcache.mu.Lock()
- defer view.mcache.mu.Unlock()
-
// Get the metadata for the file.
meta, err := view.checkMetadata(ctx, f, fh)
if err != nil {
@@ -101,23 +91,24 @@
// checkMetadata determines if we should run go/packages.Load for this file.
// If yes, update the metadata for the file and its package.
-func (v *view) checkMetadata(ctx context.Context, f *goFile, fh source.FileHandle) (metadata []*metadata, err error) {
- // Check if we need to re-run go/packages before loading the package.
- var runGopackages bool
- func() {
- f.mu.Lock()
- defer f.mu.Unlock()
+func (v *view) checkMetadata(ctx context.Context, f *goFile, fh source.FileHandle) ([]*metadata, error) {
+ var shouldRunGopackages bool
- runGopackages, err = v.shouldRunGopackages(ctx, f, fh)
- metadata = f.metadata()
- }()
+ m := v.getMetadata(fh.Identity().URI)
+ if len(m) == 0 {
+ shouldRunGopackages = true
+ }
+ // Get file content in case we don't already have it.
+ parsed, _, _, err := v.session.cache.ParseGoHandle(fh, source.ParseHeader).Parse(ctx)
if err != nil {
return nil, err
}
+ // Check if we need to re-run go/packages before loading the package.
+ shouldRunGopackages = shouldRunGopackages || v.shouldRunGopackages(ctx, f, parsed, m)
// The package metadata is correct as-is, so just return it.
- if !runGopackages {
- return metadata, nil
+ if !shouldRunGopackages {
+ return m, nil
}
// Don't bother running go/packages if the context has been canceled.
@@ -129,6 +120,8 @@
defer done()
pkgs, err := packages.Load(v.Config(ctx), fmt.Sprintf("file=%s", f.filename()))
+ log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
+
if len(pkgs) == 0 {
if err == nil {
err = errors.Errorf("go/packages.Load: no packages found for %s", f.filename())
@@ -136,200 +129,32 @@
// Return this error as a diagnostic to the user.
return nil, err
}
- // Track missing imports as we look at the package's errors.
- missingImports := make(map[packagePath]struct{})
-
- // Clear metadata since we are re-running go/packages.
- // Reset the file's metadata and type information if we are re-running `go list`.
- f.mu.Lock()
- for k := range f.meta {
- delete(f.meta, k)
- }
- for k := range f.cphs {
- delete(f.cphs, k)
- }
- f.mu.Unlock()
-
- log.Print(ctx, "go/packages.Load", tag.Of("packages", len(pkgs)))
- for _, pkg := range pkgs {
- log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
- // Build the import graph for this package.
- if err := v.link(ctx, &importGraph{
- pkgPath: packagePath(pkg.PkgPath),
- pkg: pkg,
- parent: nil,
- missingImports: make(map[packagePath]struct{}),
- }); err != nil {
- return nil, err
- }
- }
- m, err := validateMetadata(ctx, missingImports, f)
- if err != nil {
- return nil, err
- }
- return m, nil
-}
-
-func validateMetadata(ctx context.Context, missingImports map[packagePath]struct{}, f *goFile) ([]*metadata, error) {
- f.mu.Lock()
- defer f.mu.Unlock()
-
- // If `go list` failed to get data for the file in question (this should never happen).
- if len(f.meta) == 0 {
- return nil, errors.Errorf("loadParseTypecheck: no metadata found for %v", f.filename())
- }
-
- // If we have already seen these missing imports before, and we have type information,
- // there is no need to continue.
- if sameSet(missingImports, f.missingImports) && len(f.cphs) != 0 {
- return nil, nil
- }
-
- // Otherwise, update the missing imports map.
- f.missingImports = missingImports
-
- return f.metadata(), nil
-}
-
-func sameSet(x, y map[packagePath]struct{}) bool {
- if len(x) != len(y) {
- return false
- }
- for k := range x {
- if _, ok := y[k]; !ok {
- return false
- }
- }
- return true
+ return v.updateMetadata(ctx, f.URI(), pkgs)
}
// shouldRunGopackages reparses a file's package and import declarations to
// determine if they have changed.
// It assumes that the caller holds the lock on the f.mu lock.
-func (v *view) shouldRunGopackages(ctx context.Context, f *goFile, fh source.FileHandle) (result bool, err error) {
- if len(f.meta) == 0 || len(f.missingImports) > 0 {
- return true, nil
- }
- // Get file content in case we don't already have it.
- parsed, _, _, err := v.session.cache.ParseGoHandle(fh, source.ParseHeader).Parse(ctx)
- if err != nil {
- return false, err
- }
+func (v *view) shouldRunGopackages(ctx context.Context, f *goFile, file *ast.File, metadata []*metadata) bool {
// Check if the package's name has changed, by checking if this is a filename
// we already know about, and if so, check if its package name has changed.
- for _, m := range f.meta {
+ for _, m := range metadata {
for _, uri := range m.files {
if span.CompareURI(uri, f.URI()) == 0 {
- if m.name != parsed.Name.Name {
- return true, nil
+ if m.name != file.Name.Name {
+ return true
}
}
}
}
// If the package's imports have changed, re-run `go list`.
- if len(f.imports) != len(parsed.Imports) {
- return true, nil
+ if len(f.imports) != len(file.Imports) {
+ return true
}
for i, importSpec := range f.imports {
- if importSpec.Path.Value != parsed.Imports[i].Path.Value {
- return true, nil
+ if importSpec.Path.Value != file.Imports[i].Path.Value {
+ return true
}
}
- return false, nil
-}
-
-type importGraph struct {
- pkgPath packagePath
- pkg *packages.Package
- parent *metadata
- missingImports map[packagePath]struct{}
-}
-
-func (v *view) link(ctx context.Context, g *importGraph) error {
- // Recreate the metadata rather than reusing it to avoid locking.
- m := &metadata{
- id: packageID(g.pkg.ID),
- pkgPath: g.pkgPath,
- name: g.pkg.Name,
- typesSizes: g.pkg.TypesSizes,
- errors: g.pkg.Errors,
- }
- for _, filename := range g.pkg.CompiledGoFiles {
- m.files = append(m.files, span.FileURI(filename))
-
- // Call the unlocked version of getFile since we are holding the view's mutex.
- f, err := v.getFile(ctx, span.FileURI(filename), source.Go)
- if err != nil {
- log.Error(ctx, "no file", err, telemetry.File.Of(filename))
- continue
- }
- gof, ok := f.(*goFile)
- if !ok {
- log.Error(ctx, "not a Go file", nil, telemetry.File.Of(filename))
- continue
- }
- // Cache the metadata for this file.
- gof.mu.Lock()
- if gof.meta == nil {
- gof.meta = make(map[packageID]*metadata)
- }
- gof.meta[m.id] = m
- gof.mu.Unlock()
- }
-
- // Preserve the import graph.
- if original, ok := v.mcache.packages[m.id]; ok {
- m.children = original.children
- m.parents = original.parents
- }
- if m.children == nil {
- m.children = make(map[packageID]*metadata)
- }
- if m.parents == nil {
- m.parents = make(map[packageID]bool)
- }
-
- // Add the metadata to the cache.
- v.mcache.packages[m.id] = m
-
- // Connect the import graph.
- if g.parent != nil {
- m.parents[g.parent.id] = true
- g.parent.children[m.id] = m
- }
- for importPath, importPkg := range g.pkg.Imports {
- importPkgPath := packagePath(importPath)
- if importPkgPath == g.pkgPath {
- return fmt.Errorf("cycle detected in %s", importPath)
- }
- // Don't remember any imports with significant errors.
- if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
- g.missingImports[importPkgPath] = struct{}{}
- continue
- }
- if _, ok := m.children[packageID(importPkg.ID)]; !ok {
- if err := v.link(ctx, &importGraph{
- pkgPath: importPkgPath,
- pkg: importPkg,
- parent: m,
- missingImports: g.missingImports,
- }); err != nil {
- log.Error(ctx, "error in dependency", err)
- }
- }
- }
- // Clear out any imports that have been removed since the package was last loaded.
- for importID := range m.children {
- child, ok := v.mcache.packages[importID]
- if !ok {
- continue
- }
- importPath := string(child.pkgPath)
- if _, ok := g.pkg.Imports[importPath]; ok {
- continue
- }
- delete(m.children, importID)
- delete(child.parents, m.id)
- }
- return nil
+ return false
}
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 34d89d5..6cd4a61 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -98,8 +98,10 @@
folder: folder,
filesByURI: make(map[span.URI]viewFile),
filesByBase: make(map[string][]viewFile),
- mcache: &metadataCache{
- packages: make(map[packageID]*metadata),
+ snapshot: &snapshot{
+ packages: make(map[span.URI]map[packageKey]*checkPackageHandle),
+ ids: make(map[span.URI][]packageID),
+ metadata: make(map[packageID]*metadata),
},
ignoredURIs: make(map[span.URI]struct{}),
builtin: &builtinPkg{},
@@ -144,6 +146,19 @@
return v
}
+func (s *session) viewsOf(uri span.URI) []*view {
+ s.viewMu.Lock()
+ defer s.viewMu.Unlock()
+
+ var views []*view
+ for _, view := range s.views {
+ if strings.HasPrefix(string(uri), string(view.Folder())) {
+ views = append(views, view)
+ }
+ }
+ return views
+}
+
func (s *session) Views() []source.View {
s.viewMu.Lock()
defer s.viewMu.Unlock()
@@ -219,21 +234,7 @@
// A file may be in multiple views.
for _, view := range s.views {
if strings.HasPrefix(string(uri), string(view.Folder())) {
- f, err := view.GetFile(ctx, uri)
- if err != nil {
- log.Error(ctx, "error getting file", nil, telemetry.File)
- return
- }
- gof, ok := f.(*goFile)
- if !ok {
- log.Error(ctx, "not a Go file", nil, telemetry.File)
- return
- }
- // Force a reload of the package metadata by clearing the cached data.
- gof.mu.Lock()
- gof.meta = make(map[packageID]*metadata)
- gof.cphs = make(map[packageKey]*checkPackageHandle)
- gof.mu.Unlock()
+ view.invalidateMetadata(uri)
}
}
}
@@ -341,15 +342,16 @@
return overlays
}
-func (s *session) DidChangeOutOfBand(ctx context.Context, f source.GoFile, changeType protocol.FileChangeType) {
+func (s *session) DidChangeOutOfBand(ctx context.Context, uri span.URI, changeType protocol.FileChangeType) {
if changeType == protocol.Deleted {
// After a deletion we must invalidate the package's metadata to
- // force a go/packages invocation to refresh the package's file
- // list.
- f.(*goFile).invalidateMeta(ctx)
+ // force a go/packages invocation to refresh the package's file list.
+ views := s.viewsOf(uri)
+ for _, v := range views {
+ v.invalidateMetadata(uri)
+ }
}
-
- s.filesWatchMap.Notify(f.URI())
+ s.filesWatchMap.Notify(uri)
}
func (o *overlay) FileSystem() source.FileSystem {
diff --git a/internal/lsp/cache/snapshot.go b/internal/lsp/cache/snapshot.go
new file mode 100644
index 0000000..38312ec
--- /dev/null
+++ b/internal/lsp/cache/snapshot.go
@@ -0,0 +1,309 @@
+package cache
+
+import (
+ "context"
+ "go/types"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/span"
+ "golang.org/x/tools/internal/telemetry/log"
+ "golang.org/x/tools/internal/telemetry/tag"
+ errors "golang.org/x/xerrors"
+)
+
+type snapshot struct {
+ id uint64
+
+ packages map[span.URI]map[packageKey]*checkPackageHandle
+ ids map[span.URI][]packageID
+ metadata map[packageID]*metadata
+}
+
+type metadata struct {
+ id packageID
+ pkgPath packagePath
+ name string
+ files []span.URI
+ typesSizes types.Sizes
+ parents map[packageID]bool
+ children map[packageID]*metadata
+ errors []packages.Error
+}
+
+func (v *view) getSnapshot(uri span.URI) ([]*metadata, map[packageKey]*checkPackageHandle) {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ var m []*metadata
+ for _, id := range v.snapshot.ids[uri] {
+ m = append(m, v.snapshot.metadata[id])
+ }
+ return m, v.snapshot.packages[uri]
+}
+
+func (v *view) getMetadata(uri span.URI) []*metadata {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ var m []*metadata
+ for _, id := range v.snapshot.ids[uri] {
+ m = append(m, v.snapshot.metadata[id])
+ }
+ return m
+}
+
+func (v *view) getPackages(uri span.URI) map[packageKey]*checkPackageHandle {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ return v.snapshot.packages[uri]
+}
+
+func (v *view) updateMetadata(ctx context.Context, uri span.URI, pkgs []*packages.Package) ([]*metadata, error) {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ // Clear metadata since we are re-running go/packages.
+ without := make(map[span.URI]struct{})
+ for _, id := range v.snapshot.ids[uri] {
+ v.remove(id, without, map[packageID]struct{}{})
+ }
+ v.snapshot = v.snapshot.cloneMetadata(without)
+
+ var results []*metadata
+ for _, pkg := range pkgs {
+ log.Print(ctx, "go/packages.Load", tag.Of("package", pkg.PkgPath), tag.Of("files", pkg.CompiledGoFiles))
+
+ // Build the import graph for this package.
+ if err := v.updateImportGraph(ctx, &importGraph{
+ pkgPath: packagePath(pkg.PkgPath),
+ pkg: pkg,
+ parent: nil,
+ }); err != nil {
+ return nil, err
+ }
+ results = append(results, v.snapshot.metadata[packageID(pkg.ID)])
+ }
+ return results, nil
+}
+
+type importGraph struct {
+ pkgPath packagePath
+ pkg *packages.Package
+ parent *metadata
+}
+
+func (v *view) updateImportGraph(ctx context.Context, g *importGraph) error {
+ // Recreate the metadata rather than reusing it to avoid locking.
+ m := &metadata{
+ id: packageID(g.pkg.ID),
+ pkgPath: g.pkgPath,
+ name: g.pkg.Name,
+ typesSizes: g.pkg.TypesSizes,
+ errors: g.pkg.Errors,
+ }
+ for _, filename := range g.pkg.CompiledGoFiles {
+ uri := span.FileURI(filename)
+ v.snapshot.ids[uri] = append(v.snapshot.ids[uri], m.id)
+ m.files = append(m.files, uri)
+ }
+ // Preserve the import graph.
+ if original, ok := v.snapshot.metadata[m.id]; ok {
+ m.children = original.children
+ m.parents = original.parents
+ }
+ if m.children == nil {
+ m.children = make(map[packageID]*metadata)
+ }
+ if m.parents == nil {
+ m.parents = make(map[packageID]bool)
+ }
+
+ // Add the metadata to the cache.
+ v.snapshot.metadata[m.id] = m
+
+ // Connect the import graph.
+ if g.parent != nil {
+ m.parents[g.parent.id] = true
+ g.parent.children[m.id] = m
+ }
+ for importPath, importPkg := range g.pkg.Imports {
+ importPkgPath := packagePath(importPath)
+ if importPkgPath == g.pkgPath {
+ return errors.Errorf("cycle detected in %s", importPath)
+ }
+ // Don't remember any imports with significant errors.
+ if importPkgPath != "unsafe" && len(importPkg.CompiledGoFiles) == 0 {
+ continue
+ }
+ if _, ok := m.children[packageID(importPkg.ID)]; !ok {
+ if err := v.updateImportGraph(ctx, &importGraph{
+ pkgPath: importPkgPath,
+ pkg: importPkg,
+ parent: m,
+ }); err != nil {
+ log.Error(ctx, "error in dependency", err)
+ }
+ }
+ }
+ // Clear out any imports that have been removed since the package was last loaded.
+ for importID := range m.children {
+ child, ok := v.snapshot.metadata[importID]
+ if !ok {
+ continue
+ }
+ importPath := string(child.pkgPath)
+ if _, ok := g.pkg.Imports[importPath]; ok {
+ continue
+ }
+ delete(m.children, importID)
+ delete(child.parents, m.id)
+ }
+ return nil
+}
+
+func (v *view) updatePackages(cphs []*checkPackageHandle) {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ for _, cph := range cphs {
+ for _, ph := range cph.files {
+ uri := ph.File().Identity().URI
+ if _, ok := v.snapshot.packages[uri]; !ok {
+ v.snapshot.packages[uri] = make(map[packageKey]*checkPackageHandle)
+ }
+ v.snapshot.packages[uri][packageKey{
+ id: cph.m.id,
+ mode: ph.Mode(),
+ }] = cph
+ }
+ }
+}
+
+// invalidateContent invalidates the content of a Go file,
+// including any position and type information that depends on it.
+func (v *view) invalidateContent(ctx context.Context, f *goFile) {
+ f.handleMu.Lock()
+ defer f.handleMu.Unlock()
+
+ without := make(map[span.URI]struct{})
+
+ // Remove the package and all of its reverse dependencies from the cache.
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ for _, id := range v.snapshot.ids[f.URI()] {
+ f.view.remove(id, without, map[packageID]struct{}{})
+ }
+ v.snapshot = v.snapshot.clonePackages(without)
+ f.handle = nil
+}
+
+// invalidateMeta invalidates package metadata for all files in f's
+// package. This forces f's package's metadata to be reloaded next
+// time the package is checked.
+func (v *view) invalidateMetadata(uri span.URI) {
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ without := make(map[span.URI]struct{})
+
+ for _, id := range v.snapshot.ids[uri] {
+ v.remove(id, without, map[packageID]struct{}{})
+ }
+ v.snapshot = v.snapshot.cloneMetadata(without)
+}
+
+// remove invalidates a package and its reverse dependencies in the view's
+// package cache. It is assumed that the caller has locked both the mutexes
+// of both the mcache and the pcache.
+func (v *view) remove(id packageID, toDelete map[span.URI]struct{}, seen map[packageID]struct{}) {
+ if _, ok := seen[id]; ok {
+ return
+ }
+ m, ok := v.snapshot.metadata[id]
+ if !ok {
+ return
+ }
+ seen[id] = struct{}{}
+ for parentID := range m.parents {
+ v.remove(parentID, toDelete, seen)
+ }
+ for _, uri := range m.files {
+ toDelete[uri] = struct{}{}
+ }
+}
+
+func (s *snapshot) clonePackages(without map[span.URI]struct{}) *snapshot {
+ result := &snapshot{
+ id: s.id + 1,
+ packages: make(map[span.URI]map[packageKey]*checkPackageHandle),
+ ids: s.ids,
+ metadata: s.metadata,
+ }
+ for k, v := range s.packages {
+ if _, ok := without[k]; ok {
+ continue
+ }
+ result.packages[k] = v
+ }
+ return result
+}
+
+func (s *snapshot) cloneMetadata(without map[span.URI]struct{}) *snapshot {
+ result := &snapshot{
+ id: s.id + 1,
+ packages: s.packages,
+ ids: make(map[span.URI][]packageID),
+ metadata: make(map[packageID]*metadata),
+ }
+ withoutIDs := make(map[packageID]struct{})
+ for k, ids := range s.ids {
+ if _, ok := without[k]; ok {
+ for _, id := range ids {
+ withoutIDs[id] = struct{}{}
+ }
+ continue
+ }
+ result.ids[k] = ids
+ }
+ for k, v := range s.metadata {
+ if _, ok := withoutIDs[k]; ok {
+ continue
+ }
+ result.metadata[k] = v
+ }
+ return result
+}
+
+func (v *view) reverseDependencies(ctx context.Context, uri span.URI) map[span.URI]struct{} {
+ seen := make(map[packageID]struct{})
+ uris := make(map[span.URI]struct{})
+
+ v.snapshotMu.Lock()
+ defer v.snapshotMu.Unlock()
+
+ for _, id := range v.snapshot.ids[uri] {
+ v.rdeps(id, seen, uris, id)
+ }
+ return uris
+}
+
+func (v *view) rdeps(topID packageID, seen map[packageID]struct{}, results map[span.URI]struct{}, id packageID) {
+ if _, ok := seen[id]; ok {
+ return
+ }
+ seen[id] = struct{}{}
+ m, ok := v.snapshot.metadata[id]
+ if !ok {
+ return
+ }
+ if id != topID {
+ for _, uri := range m.files {
+ results[uri] = struct{}{}
+ }
+ }
+ for parentID := range m.parents {
+ v.rdeps(topID, seen, results, parentID)
+ }
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 1b6bccb..34cebd4 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -10,7 +10,6 @@
"fmt"
"go/ast"
"go/token"
- "go/types"
"os"
"os/exec"
"strings"
@@ -21,7 +20,6 @@
"golang.org/x/tools/internal/imports"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
- "golang.org/x/tools/internal/lsp/telemetry"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/log"
errors "golang.org/x/xerrors"
@@ -72,8 +70,8 @@
filesByURI map[span.URI]viewFile
filesByBase map[string][]viewFile
- // mcache caches metadata for the packages of the opened files in a view.
- mcache *metadataCache
+ snapshotMu sync.Mutex
+ snapshot *snapshot
// builtin is used to resolve builtin types.
builtin *builtinPkg
@@ -85,23 +83,6 @@
analyzers []*analysis.Analyzer
}
-type metadataCache struct {
- mu sync.Mutex // guards both maps
- packages map[packageID]*metadata
-}
-
-type metadata struct {
- id packageID
- pkgPath packagePath
- name string
- files []span.URI
- key string
- typesSizes types.Sizes
- parents map[packageID]bool
- children map[packageID]*metadata
- errors []packages.Error
-}
-
func (v *view) Session() source.Session {
return v.session
}
@@ -324,102 +305,6 @@
return false, nil
}
-// invalidateContent invalidates the content of a Go file,
-// including any position and type information that depends on it.
-func (f *goFile) invalidateContent(ctx context.Context) {
- // Mutex acquisition order here is important. It must match the order
- // in loadParseTypecheck to avoid deadlocks.
- f.view.mcache.mu.Lock()
- defer f.view.mcache.mu.Unlock()
-
- toDelete := make(map[packageID]bool)
- f.mu.Lock()
- for key, cph := range f.cphs {
- if cph != nil {
- toDelete[key.id] = true
- }
- }
- f.mu.Unlock()
-
- f.handleMu.Lock()
- defer f.handleMu.Unlock()
-
- // Remove the package and all of its reverse dependencies from the cache.
- for id := range toDelete {
- f.view.remove(ctx, id, map[packageID]struct{}{})
- }
-
- f.handle = nil
-}
-
-// invalidateMeta invalidates package metadata for all files in f's
-// package. This forces f's package's metadata to be reloaded next
-// time the package is checked.
-func (f *goFile) invalidateMeta(ctx context.Context) {
- cphs, err := f.CheckPackageHandles(ctx)
- if err != nil {
- log.Error(ctx, "invalidateMeta: GetPackages", err, telemetry.File.Of(f.URI()))
- return
- }
-
- for _, pkg := range cphs {
- for _, pgh := range pkg.Files() {
- uri := pgh.File().Identity().URI
- if gof, _ := f.view.FindFile(ctx, uri).(*goFile); gof != nil {
- gof.mu.Lock()
- gof.meta = nil
- gof.mu.Unlock()
- }
- }
- f.view.mcache.mu.Lock()
- delete(f.view.mcache.packages, packageID(pkg.ID()))
- f.view.mcache.mu.Unlock()
- }
-}
-
-// remove invalidates a package and its reverse dependencies in the view's
-// package cache. It is assumed that the caller has locked both the mutexes
-// of both the mcache and the pcache.
-func (v *view) remove(ctx context.Context, id packageID, seen map[packageID]struct{}) {
- if _, ok := seen[id]; ok {
- return
- }
- m, ok := v.mcache.packages[id]
- if !ok {
- return
- }
- seen[id] = struct{}{}
- for parentID := range m.parents {
- v.remove(ctx, parentID, seen)
- }
- // All of the files in the package may also be holding a pointer to the
- // invalidated package.
- for _, uri := range m.files {
- f, err := v.findFile(uri)
- if err != nil {
- log.Error(ctx, "cannot find file", err, telemetry.File.Of(f.URI()))
- continue
- }
- gof, ok := f.(*goFile)
- if !ok {
- log.Error(ctx, "non-Go file", nil, telemetry.File.Of(f.URI()))
- continue
- }
- gof.mu.Lock()
- for _, mode := range []source.ParseMode{
- source.ParseExported,
- source.ParseFull,
- } {
- delete(gof.cphs, packageKey{
- id: id,
- mode: mode,
- })
- }
- gof.mu.Unlock()
- }
- return
-}
-
// FindFile returns the file if the given URI is already a part of the view.
func (v *view) FindFile(ctx context.Context, uri span.URI) source.File {
v.mu.Lock()
@@ -481,7 +366,7 @@
if !ok {
return
}
- gof.invalidateContent(ctx)
+ v.invalidateContent(ctx, gof)
})
}
v.mapFile(uri, f)
@@ -540,6 +425,19 @@
}
}
+func (v *view) openFiles(ctx context.Context, uris map[span.URI]struct{}) (results []source.File) {
+ v.mu.Lock()
+ defer v.mu.Unlock()
+
+ for uri := range uris {
+ // Call unlocked version of getFile since we hold the lock on the view.
+ if f, err := v.getFile(ctx, uri, source.Go); err == nil && v.session.IsOpen(uri) {
+ results = append(results, f)
+ }
+ }
+ return results
+}
+
type debugView struct{ *view }
func (v debugView) ID() string { return v.id }
diff --git a/internal/lsp/source/diagnostics.go b/internal/lsp/source/diagnostics.go
index c48a2cc..1278131 100644
--- a/internal/lsp/source/diagnostics.go
+++ b/internal/lsp/source/diagnostics.go
@@ -84,13 +84,8 @@
}
}
// Updates to the diagnostics for this package may need to be propagated.
- revDeps := f.GetActiveReverseDeps(ctx)
- for _, f := range revDeps {
- cphs, err := f.CheckPackageHandles(ctx)
- if err != nil {
- return nil, "", err
- }
- cph := WidestCheckPackageHandle(cphs)
+ revDeps := view.GetActiveReverseDeps(ctx, f.URI())
+ for _, cph := range revDeps {
pkg, err := cph.Check(ctx)
if err != nil {
return nil, warningMsg, err
diff --git a/internal/lsp/source/view.go b/internal/lsp/source/view.go
index aa8a872..2019186 100644
--- a/internal/lsp/source/view.go
+++ b/internal/lsp/source/view.go
@@ -192,7 +192,7 @@
// DidChangeOutOfBand is called when a file under the root folder
// changes. The file is not necessarily open in the editor.
- DidChangeOutOfBand(ctx context.Context, f GoFile, change protocol.FileChangeType)
+ DidChangeOutOfBand(ctx context.Context, uri span.URI, change protocol.FileChangeType)
// Options returns a copy of the SessionOptions for this session.
Options() Options
@@ -254,6 +254,10 @@
// Analyzers returns the set of Analyzers active for this view.
Analyzers() []*analysis.Analyzer
+
+ // GetActiveReverseDeps returns the active files belonging to the reverse
+ // dependencies of this file's package.
+ GetActiveReverseDeps(ctx context.Context, uri span.URI) []CheckPackageHandle
}
// File represents a source file of any type.
@@ -270,10 +274,6 @@
// GetCheckPackageHandles returns the CheckPackageHandles for the packages
// that this file belongs to.
CheckPackageHandles(ctx context.Context) ([]CheckPackageHandle, error)
-
- // GetActiveReverseDeps returns the active files belonging to the reverse
- // dependencies of this file's package.
- GetActiveReverseDeps(ctx context.Context) []GoFile
}
type ModFile interface {
diff --git a/internal/lsp/watched_files.go b/internal/lsp/watched_files.go
index 5a70029..281b7ab 100644
--- a/internal/lsp/watched_files.go
+++ b/internal/lsp/watched_files.go
@@ -44,7 +44,7 @@
case protocol.Changed:
log.Print(ctx, "watched file changed", telemetry.File)
- s.session.DidChangeOutOfBand(ctx, gof, change.Type)
+ s.session.DidChangeOutOfBand(ctx, uri, change.Type)
// Refresh diagnostics to reflect updated file contents.
go s.diagnostics(view, uri)
@@ -77,7 +77,7 @@
break
}
}
- s.session.DidChangeOutOfBand(ctx, gof, change.Type)
+ s.session.DidChangeOutOfBand(ctx, uri, change.Type)
// If this was the only file in the package, clear its diagnostics.
if otherFile == nil {