mirror of
https://github.com/golang/go.git
synced 2025-05-05 23:53:05 +00:00
internal/lsp: cache package metadata to minimize calls to packages.Load
Instead of calling packages.Load on every character change, we reparse the import declarations of the file and determine if they have changed. We also introduce a metadata cache that caches the import graph. This is used in type-checking and only updated on calls to packages.Load. Change-Id: I7cb384aba77ef3c1565d3b0db58e6c754d5fed15 Reviewed-on: https://go-review.googlesource.com/c/tools/+/165137 Reviewed-by: Ian Cottrell <iancottrell@google.com> Run-TryBot: Ian Cottrell <iancottrell@google.com> TryBot-Result: Gobot Gobot <gobot@golang.org>
This commit is contained in:
parent
ab489119c5
commit
69e0dcfa11
197
internal/lsp/cache/check.go
vendored
197
internal/lsp/cache/check.go
vendored
@ -3,6 +3,7 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
"go/scanner"
|
"go/scanner"
|
||||||
"go/types"
|
"go/types"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
@ -17,36 +18,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (v *View) parse(uri source.URI) error {
|
func (v *View) parse(uri source.URI) error {
|
||||||
imp, pkgs, err := v.collectMetadata(uri)
|
v.mcache.mu.Lock()
|
||||||
|
defer v.mcache.mu.Unlock()
|
||||||
|
|
||||||
|
f := v.files[uri]
|
||||||
|
|
||||||
|
// This should never happen.
|
||||||
|
if f == nil {
|
||||||
|
return fmt.Errorf("no file for %v", uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
imp, err := v.checkMetadata(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, meta := range pkgs {
|
if f.meta == nil {
|
||||||
// If the package comes back with errors from `go list`, don't bother
|
return fmt.Errorf("no metadata found for %v", uri)
|
||||||
// type-checking it.
|
|
||||||
for _, err := range meta.Errors {
|
|
||||||
switch err.Kind {
|
|
||||||
case packages.UnknownError, packages.ListError:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Start prefetching direct imports.
|
// Start prefetching direct imports.
|
||||||
for importPath := range meta.Imports {
|
for importPath := range f.meta.children {
|
||||||
go imp.Import(importPath)
|
go imp.Import(importPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type-check package.
|
// Type-check package.
|
||||||
pkg, err := imp.typeCheck(meta.PkgPath)
|
pkg, err := imp.typeCheck(f.meta.pkgPath)
|
||||||
if pkg == nil || pkg.Types == nil {
|
if pkg == nil || pkg.Types == nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add every file in this package to our cache.
|
// Add every file in this package to our cache.
|
||||||
v.cachePackage(pkg)
|
v.cachePackage(pkg)
|
||||||
}
|
|
||||||
// If we still have not found the package for the file, something is wrong.
|
// If we still have not found the package for the file, something is wrong.
|
||||||
f := v.files[uri]
|
if f.pkg == nil {
|
||||||
if f == nil || f.pkg == nil {
|
|
||||||
return fmt.Errorf("no package found for %v", uri)
|
return fmt.Errorf("no package found for %v", uri)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -68,6 +70,7 @@ func (v *View) cachePackage(pkg *packages.Package) {
|
|||||||
f := v.getFile(fURI)
|
f := v.getFile(fURI)
|
||||||
f.token = tok
|
f.token = tok
|
||||||
f.ast = file
|
f.ast = file
|
||||||
|
f.imports = f.ast.Imports
|
||||||
f.pkg = pkg
|
f.pkg = pkg
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -75,7 +78,6 @@ func (v *View) cachePackage(pkg *packages.Package) {
|
|||||||
type importer struct {
|
type importer struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
entries map[string]*entry
|
entries map[string]*entry
|
||||||
packages map[string]*packages.Package
|
|
||||||
|
|
||||||
*View
|
*View
|
||||||
}
|
}
|
||||||
@ -86,48 +88,107 @@ type entry struct {
|
|||||||
ready chan struct{}
|
ready chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *View) collectMetadata(uri source.URI) (*importer, []*packages.Package, error) {
|
func (v *View) checkMetadata(f *File) (*importer, error) {
|
||||||
filename, err := uri.Filename()
|
filename, err := f.URI.Filename()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// TODO(rstambler): Enforce here that LoadMode is LoadImports?
|
imp := &importer{
|
||||||
pkgs, err := packages.Load(&v.Config, fmt.Sprintf("file=%s", filename))
|
View: v,
|
||||||
|
entries: make(map[string]*entry),
|
||||||
|
}
|
||||||
|
if v.reparseImports(f, filename) {
|
||||||
|
cfg := v.Config
|
||||||
|
cfg.Mode = packages.LoadImports
|
||||||
|
pkgs, err := packages.Load(&cfg, fmt.Sprintf("file=%s", filename))
|
||||||
if len(pkgs) == 0 {
|
if len(pkgs) == 0 {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("no packages found for %s", filename)
|
err = fmt.Errorf("no packages found for %s", filename)
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
}
|
|
||||||
imp := &importer{
|
|
||||||
entries: make(map[string]*entry),
|
|
||||||
packages: make(map[string]*packages.Package),
|
|
||||||
View: v,
|
|
||||||
}
|
}
|
||||||
for _, pkg := range pkgs {
|
for _, pkg := range pkgs {
|
||||||
if err := imp.addImports(pkg.PkgPath, pkg); err != nil {
|
// If the package comes back with errors from `go list`, don't bother
|
||||||
return nil, nil, err
|
// type-checking it.
|
||||||
|
for _, err := range pkg.Errors {
|
||||||
|
switch err.Kind {
|
||||||
|
case packages.UnknownError, packages.ListError:
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return imp, pkgs, nil
|
v.link(pkg.PkgPath, pkg, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imp *importer) addImports(path string, pkg *packages.Package) error {
|
// reparseImports reparses a file's import declarations to determine if they
|
||||||
if _, ok := imp.packages[path]; ok {
|
// have changed.
|
||||||
return nil
|
func (v *View) reparseImports(f *File, filename string) bool {
|
||||||
|
if f.meta == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Get file content in case we don't already have it?
|
||||||
|
f.read()
|
||||||
|
parsed, _ := parser.ParseFile(v.Config.Fset, filename, f.content, parser.ImportsOnly)
|
||||||
|
if parsed == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(f.imports) != len(parsed.Imports) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i, importSpec := range f.imports {
|
||||||
|
if importSpec.Path.Value != f.imports[i].Path.Value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *View) link(pkgPath string, pkg *packages.Package, parent *metadata) *metadata {
|
||||||
|
m, ok := v.mcache.packages[pkgPath]
|
||||||
|
if !ok {
|
||||||
|
m = &metadata{
|
||||||
|
pkgPath: pkgPath,
|
||||||
|
id: pkg.ID,
|
||||||
|
parents: make(map[string]bool),
|
||||||
|
children: make(map[string]bool),
|
||||||
|
}
|
||||||
|
v.mcache.packages[pkgPath] = m
|
||||||
|
}
|
||||||
|
// Reset any field that could have changed across calls to packages.Load.
|
||||||
|
m.name = pkg.Name
|
||||||
|
m.files = pkg.CompiledGoFiles
|
||||||
|
for _, filename := range m.files {
|
||||||
|
if f, ok := v.files[source.ToURI(filename)]; ok {
|
||||||
|
f.meta = m
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Connect the import graph.
|
||||||
|
if parent != nil {
|
||||||
|
m.parents[parent.pkgPath] = true
|
||||||
|
parent.children[pkgPath] = true
|
||||||
}
|
}
|
||||||
imp.packages[path] = pkg
|
|
||||||
for importPath, importPkg := range pkg.Imports {
|
for importPath, importPkg := range pkg.Imports {
|
||||||
if err := imp.addImports(importPath, importPkg); err != nil {
|
if _, ok := m.children[importPath]; !ok {
|
||||||
return err
|
v.link(importPath, importPkg, m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
// Clear out any imports that have been removed.
|
||||||
|
for importPath := range m.children {
|
||||||
|
if _, ok := pkg.Imports[importPath]; !ok {
|
||||||
|
delete(m.children, importPath)
|
||||||
|
if child, ok := v.mcache.packages[importPath]; ok {
|
||||||
|
delete(child.parents, pkgPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (imp *importer) Import(path string) (*types.Package, error) {
|
func (imp *importer) Import(pkgPath string) (*types.Package, error) {
|
||||||
imp.mu.Lock()
|
imp.mu.Lock()
|
||||||
e, ok := imp.entries[path]
|
e, ok := imp.entries[pkgPath]
|
||||||
if ok {
|
if ok {
|
||||||
// cache hit
|
// cache hit
|
||||||
imp.mu.Unlock()
|
imp.mu.Unlock()
|
||||||
@ -136,12 +197,12 @@ func (imp *importer) Import(path string) (*types.Package, error) {
|
|||||||
} else {
|
} else {
|
||||||
// cache miss
|
// cache miss
|
||||||
e = &entry{ready: make(chan struct{})}
|
e = &entry{ready: make(chan struct{})}
|
||||||
imp.entries[path] = e
|
imp.entries[pkgPath] = e
|
||||||
imp.mu.Unlock()
|
imp.mu.Unlock()
|
||||||
|
|
||||||
// This goroutine becomes responsible for populating
|
// This goroutine becomes responsible for populating
|
||||||
// the entry and broadcasting its readiness.
|
// the entry and broadcasting its readiness.
|
||||||
e.pkg, e.err = imp.typeCheck(path)
|
e.pkg, e.err = imp.typeCheck(pkgPath)
|
||||||
close(e.ready)
|
close(e.ready)
|
||||||
}
|
}
|
||||||
if e.err != nil {
|
if e.err != nil {
|
||||||
@ -151,15 +212,36 @@ func (imp *importer) Import(path string) (*types.Package, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) {
|
func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) {
|
||||||
imp.mu.Lock()
|
meta, ok := imp.mcache.packages[pkgPath]
|
||||||
pkg, ok := imp.packages[pkgPath]
|
|
||||||
imp.mu.Unlock()
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("no metadata for %v", pkgPath)
|
return nil, fmt.Errorf("no metadata for %v", pkgPath)
|
||||||
}
|
}
|
||||||
|
// Use the default type information for the unsafe package.
|
||||||
|
var typ *types.Package
|
||||||
|
if meta.pkgPath == "unsafe" {
|
||||||
|
typ = types.Unsafe
|
||||||
|
} else {
|
||||||
|
typ = types.NewPackage(meta.pkgPath, meta.name)
|
||||||
|
}
|
||||||
|
pkg := &packages.Package{
|
||||||
|
ID: meta.id,
|
||||||
|
Name: meta.name,
|
||||||
|
PkgPath: meta.pkgPath,
|
||||||
|
CompiledGoFiles: meta.files,
|
||||||
|
Imports: make(map[string]*packages.Package),
|
||||||
|
Fset: imp.Config.Fset,
|
||||||
|
Types: typ,
|
||||||
|
TypesInfo: &types.Info{
|
||||||
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
||||||
|
Defs: make(map[*ast.Ident]types.Object),
|
||||||
|
Uses: make(map[*ast.Ident]types.Object),
|
||||||
|
Implicits: make(map[ast.Node]types.Object),
|
||||||
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
||||||
|
Scopes: make(map[ast.Node]*types.Scope),
|
||||||
|
},
|
||||||
// TODO(rstambler): Get real TypeSizes from go/packages (golang.org/issues/30139).
|
// TODO(rstambler): Get real TypeSizes from go/packages (golang.org/issues/30139).
|
||||||
pkg.TypesSizes = &types.StdSizes{}
|
TypesSizes: &types.StdSizes{},
|
||||||
pkg.Fset = imp.Config.Fset
|
}
|
||||||
appendError := func(err error) {
|
appendError := func(err error) {
|
||||||
imp.appendPkgError(pkg, err)
|
imp.appendPkgError(pkg, err)
|
||||||
}
|
}
|
||||||
@ -172,15 +254,6 @@ func (imp *importer) typeCheck(pkgPath string) (*packages.Package, error) {
|
|||||||
Error: appendError,
|
Error: appendError,
|
||||||
Importer: imp,
|
Importer: imp,
|
||||||
}
|
}
|
||||||
pkg.Types = types.NewPackage(pkg.PkgPath, pkg.Name)
|
|
||||||
pkg.TypesInfo = &types.Info{
|
|
||||||
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
||||||
Defs: make(map[*ast.Ident]types.Object),
|
|
||||||
Uses: make(map[*ast.Ident]types.Object),
|
|
||||||
Implicits: make(map[ast.Node]types.Object),
|
|
||||||
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
||||||
Scopes: make(map[ast.Node]*types.Scope),
|
|
||||||
}
|
|
||||||
check := types.NewChecker(cfg, imp.Config.Fset, pkg.Types, pkg.TypesInfo)
|
check := types.NewChecker(cfg, imp.Config.Fset, pkg.Types, pkg.TypesInfo)
|
||||||
check.Files(pkg.Syntax)
|
check.Files(pkg.Syntax)
|
||||||
|
|
||||||
@ -229,20 +302,20 @@ var ioLimit = make(chan bool, 20)
|
|||||||
// Because files are scanned in parallel, the token.Pos
|
// Because files are scanned in parallel, the token.Pos
|
||||||
// positions of the resulting ast.Files are not ordered.
|
// positions of the resulting ast.Files are not ordered.
|
||||||
//
|
//
|
||||||
func (imp *importer) parseFiles(filenames []string) ([]*ast.File, []error) {
|
func (v *View) parseFiles(filenames []string) ([]*ast.File, []error) {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
n := len(filenames)
|
n := len(filenames)
|
||||||
parsed := make([]*ast.File, n)
|
parsed := make([]*ast.File, n)
|
||||||
errors := make([]error, n)
|
errors := make([]error, n)
|
||||||
for i, filename := range filenames {
|
for i, filename := range filenames {
|
||||||
if imp.Config.Context.Err() != nil {
|
if v.Config.Context.Err() != nil {
|
||||||
parsed[i] = nil
|
parsed[i] = nil
|
||||||
errors[i] = imp.Config.Context.Err()
|
errors[i] = v.Config.Context.Err()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, check if we have already cached an AST for this file.
|
// First, check if we have already cached an AST for this file.
|
||||||
f := imp.files[source.ToURI(filename)]
|
f := v.files[source.ToURI(filename)]
|
||||||
var fAST *ast.File
|
var fAST *ast.File
|
||||||
if f != nil {
|
if f != nil {
|
||||||
fAST = f.ast
|
fAST = f.ast
|
||||||
@ -258,7 +331,7 @@ func (imp *importer) parseFiles(filenames []string) ([]*ast.File, []error) {
|
|||||||
// We don't have a cached AST for this file.
|
// We don't have a cached AST for this file.
|
||||||
var src []byte
|
var src []byte
|
||||||
// Check for an available overlay.
|
// Check for an available overlay.
|
||||||
for f, contents := range imp.Config.Overlay {
|
for f, contents := range v.Config.Overlay {
|
||||||
if sameFile(f, filename) {
|
if sameFile(f, filename) {
|
||||||
src = contents
|
src = contents
|
||||||
}
|
}
|
||||||
@ -272,7 +345,7 @@ func (imp *importer) parseFiles(filenames []string) ([]*ast.File, []error) {
|
|||||||
parsed[i], errors[i] = nil, err
|
parsed[i], errors[i] = nil, err
|
||||||
} else {
|
} else {
|
||||||
// ParseFile may return both an AST and an error.
|
// ParseFile may return both an AST and an error.
|
||||||
parsed[i], errors[i] = imp.Config.ParseFile(imp.Config.Fset, filename, src)
|
parsed[i], errors[i] = v.Config.ParseFile(v.Config.Fset, filename, src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
7
internal/lsp/cache/file.go
vendored
7
internal/lsp/cache/file.go
vendored
@ -22,6 +22,8 @@ type File struct {
|
|||||||
ast *ast.File
|
ast *ast.File
|
||||||
token *token.File
|
token *token.File
|
||||||
pkg *packages.Package
|
pkg *packages.Package
|
||||||
|
meta *metadata
|
||||||
|
imports []*ast.ImportSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContent returns the contents of the file, reading it from file system if needed.
|
// GetContent returns the contents of the file, reading it from file system if needed.
|
||||||
@ -69,12 +71,13 @@ func (f *File) GetPackage() *packages.Package {
|
|||||||
return f.pkg
|
return f.pkg
|
||||||
}
|
}
|
||||||
|
|
||||||
// read is the internal part of Read that presumes the lock is already held
|
// read is the internal part of GetContent. It assumes that the caller is
|
||||||
|
// holding the mutex of the file's view.
|
||||||
func (f *File) read() {
|
func (f *File) read() {
|
||||||
if f.content != nil {
|
if f.content != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// we don't know the content yet, so read it
|
// We don't know the content yet, so read it.
|
||||||
filename, err := f.URI.Filename()
|
filename, err := f.URI.Filename()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
37
internal/lsp/cache/view.go
vendored
37
internal/lsp/cache/view.go
vendored
@ -14,21 +14,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type View struct {
|
type View struct {
|
||||||
mu sync.Mutex // protects all mutable state of the view
|
// mu protects all mutable state of the view.
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
// Config is the configuration used for the view's interaction with the
|
||||||
|
// go/packages API. It is shared across all views.
|
||||||
Config packages.Config
|
Config packages.Config
|
||||||
|
|
||||||
|
// files caches information for opened files in a view.
|
||||||
files map[source.URI]*File
|
files map[source.URI]*File
|
||||||
|
|
||||||
|
// mcache caches metadata for the packages of the opened files in a view.
|
||||||
|
mcache *metadataCache
|
||||||
|
|
||||||
analysisCache *source.AnalysisCache
|
analysisCache *source.AnalysisCache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewView creates a new View, given a root path and go/packages configuration.
|
type metadataCache struct {
|
||||||
// If config is nil, one is created with the directory set to the rootPath.
|
mu sync.Mutex
|
||||||
|
packages map[string]*metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
type metadata struct {
|
||||||
|
id, pkgPath, name string
|
||||||
|
files []string
|
||||||
|
parents, children map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
func NewView(config *packages.Config) *View {
|
func NewView(config *packages.Config) *View {
|
||||||
return &View{
|
return &View{
|
||||||
Config: *config,
|
Config: *config,
|
||||||
files: make(map[source.URI]*File),
|
files: make(map[source.URI]*File),
|
||||||
|
mcache: &metadataCache{
|
||||||
|
packages: make(map[string]*metadata),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,13 +60,21 @@ func (v *View) GetAnalysisCache() *source.AnalysisCache {
|
|||||||
return v.analysisCache
|
return v.analysisCache
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *View) copyView() *View {
|
||||||
|
return &View{
|
||||||
|
Config: v.Config,
|
||||||
|
files: make(map[source.URI]*File),
|
||||||
|
mcache: v.mcache,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SetContent sets the overlay contents for a file. A nil content value will
|
// SetContent sets the overlay contents for a file. A nil content value will
|
||||||
// remove the file from the active set and revert it to its on-disk contents.
|
// remove the file from the active set and revert it to its on-disk contents.
|
||||||
func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (source.View, error) {
|
func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (source.View, error) {
|
||||||
v.mu.Lock()
|
v.mu.Lock()
|
||||||
defer v.mu.Unlock()
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
newView := NewView(&v.Config)
|
newView := v.copyView()
|
||||||
|
|
||||||
for fURI, f := range v.files {
|
for fURI, f := range v.files {
|
||||||
newView.files[fURI] = &File{
|
newView.files[fURI] = &File{
|
||||||
@ -58,6 +85,8 @@ func (v *View) SetContent(ctx context.Context, uri source.URI, content []byte) (
|
|||||||
ast: f.ast,
|
ast: f.ast,
|
||||||
token: f.token,
|
token: f.token,
|
||||||
pkg: f.pkg,
|
pkg: f.pkg,
|
||||||
|
meta: f.meta,
|
||||||
|
imports: f.imports,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user