mirror of https://github.com/golang/go.git
internal/lsp/cache: simplify modtidy
Previously, the modtidy operation used a persistent map
of handles in the central store that cached the result
of a parsing the go.mod file after running 'go mod tidy'.
The key was complex, including the session, view, imports
of all dependencies, and the names of all unsaved overlays.
The fine-grained key prevented spurious cache hits for
invalid inputs by (we suspect) preventing nearly all cache hits.
The existing snapshot invalidation mechanism should be
sufficient to solve this problem, as the map entry is evicted
whenever the metadata or overlays change. So, this change
avoids keeping handles in the central store, so they are
never shared across views.
Also, modtidy exploited the fact that a packageHandle
used to include a copy of all the Go source files
of each package, to avoid having to read the files
itself. As a result it would entail lots of unnecessary
work building package handles and reading dependencies
when it has no business even thinking about type checking.
This change:
- extracts the logic to read Metadata.{GoFiles,CompiledGo}Files
so that it can be shared by modtidy and buildPackageHandle.
- packageHandle.imports has moved into mod_tidy.
One call (to compute the hash key) has gone away,
as have various other hashing operations.
- removes the packagesMap typed persistent.Map wrapper.
- analysis: check cache before calling buildPackageHandle.
- decouple Handle from Store so that unstored handles may
be used.
- adds various TODO comments for further simplification.
Change-Id: Ibdc086ca76d6483b094ef48aac5b1dd0cdd04973
Reviewed-on: https://go-review.googlesource.com/c/tools/+/417116
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
This commit is contained in:
parent
b230791f2d
commit
dcb576d3b6
|
|
@ -23,6 +23,11 @@ import (
|
|||
)
|
||||
|
||||
func (s *snapshot) Analyze(ctx context.Context, id string, analyzers []*source.Analyzer) ([]*source.Diagnostic, error) {
|
||||
// TODO(adonovan): merge these two loops. There's no need to
|
||||
// construct all the root action handles before beginning
|
||||
// analysis. Operations should be concurrent (though that first
|
||||
// requires buildPackageHandle not to be inefficient when
|
||||
// called in parallel.)
|
||||
var roots []*actionHandle
|
||||
for _, a := range analyzers {
|
||||
if !a.IsEnabled(s.view) {
|
||||
|
|
@ -95,15 +100,18 @@ func (s *snapshot) actionHandle(ctx context.Context, id PackageID, a *analysis.A
|
|||
// use of concurrency would lead to an exponential amount of duplicated
|
||||
// work. We should instead use an atomically updated future cache
|
||||
// and a parallel graph traversal.
|
||||
ph, err := s.buildPackageHandle(ctx, id, source.ParseFull)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if act := s.getActionHandle(id, ph.mode, a); act != nil {
|
||||
|
||||
// TODO(adonovan): in the code below, follow the structure of
|
||||
// the other handle-map accessors.
|
||||
|
||||
const mode = source.ParseFull
|
||||
if act := s.getActionHandle(id, mode, a); act != nil {
|
||||
return act, nil
|
||||
}
|
||||
if len(ph.key) == 0 {
|
||||
return nil, fmt.Errorf("actionHandle: no key for package %s", id)
|
||||
|
||||
ph, err := s.buildPackageHandle(ctx, id, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pkg, err := ph.check(ctx, s)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ import (
|
|||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -34,23 +32,32 @@ import (
|
|||
"golang.org/x/tools/internal/typesinternal"
|
||||
)
|
||||
|
||||
// A packageKey identifies a packageHandle in the snapshot.packages map.
|
||||
type packageKey struct {
|
||||
mode source.ParseMode
|
||||
id PackageID
|
||||
}
|
||||
|
||||
type packageHandleKey source.Hash
|
||||
|
||||
// A packageHandle is a handle to the future result of type-checking a package.
|
||||
// The resulting package is obtained from the check() method.
|
||||
type packageHandle struct {
|
||||
handle *memoize.Handle
|
||||
|
||||
// goFiles and compiledGoFiles are the lists of files in the package.
|
||||
// The latter is the list of files seen by the type checker (in which
|
||||
// those that import "C" have been replaced by generated code).
|
||||
goFiles, compiledGoFiles []source.FileHandle
|
||||
|
||||
// mode is the mode the files were parsed in.
|
||||
// mode is the mode the files will be parsed in.
|
||||
mode source.ParseMode
|
||||
|
||||
// m is the metadata associated with the package.
|
||||
m *KnownMetadata
|
||||
|
||||
// key is the hashed key for the package.
|
||||
//
|
||||
// It includes the all bits of the transitive closure of
|
||||
// dependencies's sources. This is more than type checking
|
||||
// really depends on: export data of direct deps should be
|
||||
// enough. (The key for analysis actions could similarly
|
||||
// hash only Facts of direct dependencies.)
|
||||
key packageHandleKey
|
||||
}
|
||||
|
||||
|
|
@ -61,26 +68,6 @@ func (ph *packageHandle) packageKey() packageKey {
|
|||
}
|
||||
}
|
||||
|
||||
func (ph *packageHandle) imports(ctx context.Context, s source.Snapshot) (result []string) {
|
||||
for _, goFile := range ph.goFiles {
|
||||
f, err := s.ParseGo(ctx, goFile, source.ParseHeader)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, impSpec := range f.File.Imports {
|
||||
imp, _ := strconv.Unquote(impSpec.Path.Value)
|
||||
if _, ok := seen[imp]; !ok {
|
||||
seen[imp] = struct{}{}
|
||||
result = append(result, imp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
// packageData contains the data produced by type-checking a package.
|
||||
type packageData struct {
|
||||
pkg *pkg
|
||||
|
|
@ -145,21 +132,8 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
|
|||
}
|
||||
|
||||
// Read both lists of files of this package, in parallel.
|
||||
var group errgroup.Group
|
||||
getFileHandles := func(files []span.URI) []source.FileHandle {
|
||||
fhs := make([]source.FileHandle, len(files))
|
||||
for i, uri := range files {
|
||||
i, uri := i, uri
|
||||
group.Go(func() (err error) {
|
||||
fhs[i], err = s.GetFile(ctx, uri) // ~25us
|
||||
return
|
||||
})
|
||||
}
|
||||
return fhs
|
||||
}
|
||||
goFiles := getFileHandles(m.GoFiles)
|
||||
compiledGoFiles := getFileHandles(m.CompiledGoFiles)
|
||||
if err := group.Wait(); err != nil {
|
||||
goFiles, compiledGoFiles, err := readGoFiles(ctx, s, m.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
|
@ -197,12 +171,10 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
|
|||
})
|
||||
|
||||
ph := &packageHandle{
|
||||
handle: handle,
|
||||
goFiles: goFiles,
|
||||
compiledGoFiles: compiledGoFiles,
|
||||
mode: mode,
|
||||
m: m,
|
||||
key: key,
|
||||
handle: handle,
|
||||
mode: mode,
|
||||
m: m,
|
||||
key: key,
|
||||
}
|
||||
|
||||
// Cache the handle in the snapshot. If a package handle has already
|
||||
|
|
@ -212,6 +184,26 @@ func (s *snapshot) buildPackageHandle(ctx context.Context, id PackageID, mode so
|
|||
return s.addPackageHandle(ph, release)
|
||||
}
|
||||
|
||||
// readGoFiles reads the content of Metadata.GoFiles and
|
||||
// Metadata.CompiledGoFiles, in parallel.
|
||||
func readGoFiles(ctx context.Context, s *snapshot, m *Metadata) (goFiles, compiledGoFiles []source.FileHandle, err error) {
|
||||
var group errgroup.Group
|
||||
getFileHandles := func(files []span.URI) []source.FileHandle {
|
||||
fhs := make([]source.FileHandle, len(files))
|
||||
for i, uri := range files {
|
||||
i, uri := i, uri
|
||||
group.Go(func() (err error) {
|
||||
fhs[i], err = s.GetFile(ctx, uri) // ~25us
|
||||
return
|
||||
})
|
||||
}
|
||||
return fhs
|
||||
}
|
||||
return getFileHandles(m.GoFiles),
|
||||
getFileHandles(m.CompiledGoFiles),
|
||||
group.Wait()
|
||||
}
|
||||
|
||||
func (s *snapshot) workspaceParseMode(id PackageID) source.ParseMode {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
|
|||
|
|
@ -149,16 +149,8 @@ func (m parseKeysByURIMap) Delete(key span.URI) {
|
|||
m.impl.Delete(key)
|
||||
}
|
||||
|
||||
type packagesMap struct {
|
||||
impl *persistent.Map
|
||||
}
|
||||
|
||||
func newPackagesMap() packagesMap {
|
||||
return packagesMap{
|
||||
impl: persistent.NewMap(func(a, b interface{}) bool {
|
||||
return packageKeyLess(a.(packageKey), b.(packageKey))
|
||||
}),
|
||||
}
|
||||
func packageKeyLessInterface(x, y interface{}) bool {
|
||||
return packageKeyLess(x.(packageKey), y.(packageKey))
|
||||
}
|
||||
|
||||
func packageKeyLess(x, y packageKey) bool {
|
||||
|
|
@ -168,40 +160,6 @@ func packageKeyLess(x, y packageKey) bool {
|
|||
return x.id < y.id
|
||||
}
|
||||
|
||||
func (m packagesMap) Clone() packagesMap {
|
||||
return packagesMap{
|
||||
impl: m.impl.Clone(),
|
||||
}
|
||||
}
|
||||
|
||||
func (m packagesMap) Destroy() {
|
||||
m.impl.Destroy()
|
||||
}
|
||||
|
||||
func (m packagesMap) Get(key packageKey) (*packageHandle, bool) {
|
||||
value, ok := m.impl.Get(key)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return value.(*packageHandle), true
|
||||
}
|
||||
|
||||
func (m packagesMap) Range(do func(key packageKey, value *packageHandle)) {
|
||||
m.impl.Range(func(key, value interface{}) {
|
||||
do(key.(packageKey), value.(*packageHandle))
|
||||
})
|
||||
}
|
||||
|
||||
func (m packagesMap) Set(key packageKey, value *packageHandle, release func()) {
|
||||
m.impl.Set(key, value, func(key, value interface{}) {
|
||||
release()
|
||||
})
|
||||
}
|
||||
|
||||
func (m packagesMap) Delete(key packageKey) {
|
||||
m.impl.Delete(key)
|
||||
}
|
||||
|
||||
type knownDirsSet struct {
|
||||
impl *persistent.Map
|
||||
}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,7 @@ func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string
|
|||
if !hit {
|
||||
// TODO(adonovan): use a simpler cache of promises that
|
||||
// is shared across snapshots. See comment at modTidyKey.
|
||||
// We can then delete hashEnv too.
|
||||
type modWhyKey struct {
|
||||
// TODO(rfindley): is sessionID used to identify overlays because modWhy
|
||||
// looks at overlay state? In that case, I am not sure that this key
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -28,7 +27,8 @@ import (
|
|||
"golang.org/x/tools/internal/span"
|
||||
)
|
||||
|
||||
// modTidyImpl runs "go mod tidy" on a go.mod file, using a cache.
|
||||
// ModTidy returns the go.mod file that would be obtained by running
|
||||
// "go mod tidy". Concurrent requests are combined into a single command.
|
||||
func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
|
||||
uri := pm.URI
|
||||
if pm.File == nil {
|
||||
|
|
@ -46,63 +46,38 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc
|
|||
|
||||
// Cache miss?
|
||||
if !hit {
|
||||
// If the file handle is an overlay, it may not be written to disk.
|
||||
// The go.mod file has to be on disk for `go mod tidy` to work.
|
||||
// TODO(rfindley): is this still true with Go 1.16 overlay support?
|
||||
fh, err := s.GetFile(ctx, pm.URI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// If the file handle is an overlay, it may not be written to disk.
|
||||
// The go.mod file has to be on disk for `go mod tidy` to work.
|
||||
// TODO(rfindley): is this still true with Go 1.16 overlay support?
|
||||
if _, ok := fh.(*overlay); ok {
|
||||
if info, _ := os.Stat(fh.URI().Filename()); info == nil {
|
||||
if info, _ := os.Stat(uri.Filename()); info == nil {
|
||||
return nil, source.ErrNoModOnDisk
|
||||
}
|
||||
}
|
||||
|
||||
if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
|
||||
return &source.TidiedModule{
|
||||
Diagnostics: criticalErr.DiagList,
|
||||
}, nil
|
||||
}
|
||||
workspacePkgs, err := s.workspacePackageHandles(ctx)
|
||||
if err != nil {
|
||||
|
||||
if err := s.awaitLoaded(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
overlayHash := hashUnsavedOverlays(s.files)
|
||||
s.mu.Unlock()
|
||||
handle := memoize.NewHandle("modTidy", func(ctx context.Context, arg interface{}) interface{} {
|
||||
|
||||
// There's little reason at to use the shared cache for mod
|
||||
// tidy (and mod why) as their key includes the view and session.
|
||||
// Its only real value is to de-dup requests in flight, for
|
||||
// which a singleflight in the View would suffice.
|
||||
// TODO(adonovan): use a simpler cache of promises that
|
||||
// is shared across snapshots.
|
||||
type modTidyKey struct {
|
||||
// TODO(rfindley): this key is also suspicious (see modWhyKey).
|
||||
sessionID string
|
||||
env source.Hash
|
||||
gomod source.FileIdentity
|
||||
imports source.Hash
|
||||
unsavedOverlays source.Hash
|
||||
view string
|
||||
}
|
||||
key := modTidyKey{
|
||||
sessionID: s.view.session.id,
|
||||
view: s.view.folder.Filename(),
|
||||
imports: s.hashImports(ctx, workspacePkgs),
|
||||
unsavedOverlays: overlayHash,
|
||||
gomod: fh.FileIdentity(),
|
||||
env: hashEnv(s),
|
||||
}
|
||||
handle, release := s.store.Handle(key, func(ctx context.Context, arg interface{}) interface{} {
|
||||
tidied, err := modTidyImpl(ctx, arg.(*snapshot), fh, pm, workspacePkgs)
|
||||
tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm)
|
||||
return modTidyResult{tidied, err}
|
||||
})
|
||||
|
||||
entry = handle
|
||||
s.mu.Lock()
|
||||
s.modTidyHandles.Set(uri, entry, func(_, _ interface{}) { release() })
|
||||
s.modTidyHandles.Set(uri, entry, nil)
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
|
|
@ -116,15 +91,16 @@ func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*sourc
|
|||
}
|
||||
|
||||
// modTidyImpl runs "go mod tidy" on a go.mod file.
|
||||
func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle, pm *source.ParsedModule, workspacePkgs []*packageHandle) (*source.TidiedModule, error) {
|
||||
ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(fh.URI()))
|
||||
func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) {
|
||||
ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename))
|
||||
defer done()
|
||||
|
||||
inv := &gocommand.Invocation{
|
||||
Verb: "mod",
|
||||
Args: []string{"tidy"},
|
||||
WorkingDir: filepath.Dir(fh.URI().Filename()),
|
||||
WorkingDir: filepath.Dir(filename),
|
||||
}
|
||||
// TODO(adonovan): ensure that unsaved overlays are passed through to 'go'.
|
||||
tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -151,7 +127,7 @@ func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle,
|
|||
|
||||
// Compare the original and tidied go.mod files to compute errors and
|
||||
// suggested fixes.
|
||||
diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs)
|
||||
diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -162,25 +138,10 @@ func modTidyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle,
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) source.Hash {
|
||||
seen := map[string]struct{}{}
|
||||
var imports []string
|
||||
for _, ph := range wsPackages {
|
||||
for _, imp := range ph.imports(ctx, s) {
|
||||
if _, ok := seen[imp]; !ok {
|
||||
imports = append(imports, imp)
|
||||
seen[imp] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(imports)
|
||||
return source.Hashf("%s", imports)
|
||||
}
|
||||
|
||||
// modTidyDiagnostics computes the differences between the original and tidied
|
||||
// go.mod files to produce diagnostic and suggested fixes. Some diagnostics
|
||||
// may appear on the Go files that import packages from missing modules.
|
||||
func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *source.ParsedModule, ideal *modfile.File, workspacePkgs []*packageHandle) (diagnostics []*source.Diagnostic, err error) {
|
||||
func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) {
|
||||
// First, determine which modules are unused and which are missing from the
|
||||
// original go.mod file.
|
||||
var (
|
||||
|
|
@ -229,15 +190,25 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc
|
|||
}
|
||||
// Add diagnostics for missing modules anywhere they are imported in the
|
||||
// workspace.
|
||||
for _, ph := range workspacePkgs {
|
||||
// TODO(adonovan): opt: opportunities for parallelism abound.
|
||||
for _, id := range snapshot.workspacePackageIDs() {
|
||||
m := snapshot.getMetadata(id)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("no metadata for %s", id)
|
||||
}
|
||||
|
||||
// Read both lists of files of this package, in parallel.
|
||||
goFiles, compiledGoFiles, err := readGoFiles(ctx, snapshot, m.Metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
missingImports := map[string]*modfile.Require{}
|
||||
|
||||
// If -mod=readonly is not set we may have successfully imported
|
||||
// packages from missing modules. Otherwise they'll be in
|
||||
// MissingDependencies. Combine both.
|
||||
importedPkgs := ph.imports(ctx, snapshot)
|
||||
|
||||
for _, imp := range importedPkgs {
|
||||
for imp := range parseImports(ctx, snapshot, goFiles) {
|
||||
if req, ok := missing[imp]; ok {
|
||||
missingImports[imp] = req
|
||||
break
|
||||
|
|
@ -266,7 +237,7 @@ func modTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, pm *sourc
|
|||
if len(missingImports) == 0 {
|
||||
continue
|
||||
}
|
||||
for _, goFile := range ph.compiledGoFiles {
|
||||
for _, goFile := range compiledGoFiles {
|
||||
pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader)
|
||||
if err != nil {
|
||||
continue
|
||||
|
|
@ -507,3 +478,27 @@ func spanFromPositions(m *protocol.ColumnMapper, s, e modfile.Position) (span.Sp
|
|||
}
|
||||
return span.New(m.URI, start, end), nil
|
||||
}
|
||||
|
||||
// parseImports parses the headers of the specified files and returns
|
||||
// the set of strings that appear in import declarations within
|
||||
// GoFiles. Errors are ignored.
|
||||
//
|
||||
// (We can't simply use ph.m.Metadata.Deps because it contains
|
||||
// PackageIDs--not import paths--and is based on CompiledGoFiles,
|
||||
// after cgo processing.)
|
||||
func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) map[string]bool {
|
||||
s.mu.Lock() // peekOrParse requires a locked snapshot (!)
|
||||
defer s.mu.Unlock()
|
||||
seen := make(map[string]bool)
|
||||
for _, file := range files {
|
||||
f, err := peekOrParse(ctx, s, file, source.ParseHeader)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, spec := range f.File.Imports {
|
||||
path, _ := strconv.Unquote(spec.Path.Value)
|
||||
seen[path] = true
|
||||
}
|
||||
}
|
||||
return seen
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
|
|||
cancel: cancel,
|
||||
initializeOnce: &sync.Once{},
|
||||
store: &s.cache.store,
|
||||
packages: newPackagesMap(),
|
||||
packages: persistent.NewMap(packageKeyLessInterface),
|
||||
meta: &metadataGraph{},
|
||||
files: newFilesMap(),
|
||||
isActivePackageCache: newIsActivePackageCacheMap(),
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ type snapshot struct {
|
|||
files filesMap
|
||||
|
||||
// parsedGoFiles maps a parseKey to the handle of the future result of parsing it.
|
||||
parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle
|
||||
parsedGoFiles *persistent.Map // from parseKey to *memoize.Handle[parseGoResult]
|
||||
|
||||
// parseKeysByURI records the set of keys of parsedGoFiles that
|
||||
// need to be invalidated for each URI.
|
||||
|
|
@ -95,7 +95,7 @@ type snapshot struct {
|
|||
|
||||
// symbolizeHandles maps each file URI to a handle for the future
|
||||
// result of computing the symbols declared in that file.
|
||||
symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle
|
||||
symbolizeHandles *persistent.Map // from span.URI to *memoize.Handle[symbolizeResult]
|
||||
|
||||
// packages maps a packageKey to a *packageHandle.
|
||||
// It may be invalidated when a file's content changes.
|
||||
|
|
@ -104,7 +104,7 @@ type snapshot struct {
|
|||
// - packages.Get(id).m.Metadata == meta.metadata[id].Metadata for all ids
|
||||
// - if a package is in packages, then all of its dependencies should also
|
||||
// be in packages, unless there is a missing import
|
||||
packages packagesMap
|
||||
packages *persistent.Map // from packageKey to *memoize.Handle[*packageHandle]
|
||||
|
||||
// isActivePackageCache maps package ID to the cached value if it is active or not.
|
||||
// It may be invalidated when metadata changes or a new file is opened or closed.
|
||||
|
|
@ -123,17 +123,17 @@ type snapshot struct {
|
|||
|
||||
// parseModHandles keeps track of any parseModHandles for the snapshot.
|
||||
// The handles need not refer to only the view's go.mod file.
|
||||
parseModHandles *persistent.Map // from span.URI to *memoize.Handle
|
||||
parseModHandles *persistent.Map // from span.URI to *memoize.Handle[parseModResult]
|
||||
|
||||
// parseWorkHandles keeps track of any parseWorkHandles for the snapshot.
|
||||
// The handles need not refer to only the view's go.work file.
|
||||
parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle
|
||||
parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle[parseWorkResult]
|
||||
|
||||
// Preserve go.mod-related handles to avoid garbage-collecting the results
|
||||
// of various calls to the go command. The handles need not refer to only
|
||||
// the view's go.mod file.
|
||||
modTidyHandles *persistent.Map // from span.URI to *memoize.Handle
|
||||
modWhyHandles *persistent.Map // from span.URI to *memoize.Handle
|
||||
modTidyHandles *persistent.Map // from span.URI to *memoize.Handle[modTidyResult]
|
||||
modWhyHandles *persistent.Map // from span.URI to *memoize.Handle[modWhyResult]
|
||||
|
||||
workspace *workspace // (not guarded by mu)
|
||||
|
||||
|
|
@ -175,11 +175,6 @@ func (s *snapshot) awaitHandle(ctx context.Context, h *memoize.Handle) (interfac
|
|||
return h.Get(ctx, s)
|
||||
}
|
||||
|
||||
type packageKey struct {
|
||||
mode source.ParseMode
|
||||
id PackageID
|
||||
}
|
||||
|
||||
type actionKey struct {
|
||||
pkg packageKey
|
||||
analyzer *analysis.Analyzer
|
||||
|
|
@ -603,17 +598,6 @@ func (s *snapshot) buildOverlay() map[string][]byte {
|
|||
return overlays
|
||||
}
|
||||
|
||||
func hashUnsavedOverlays(files filesMap) source.Hash {
|
||||
var unsaved []string
|
||||
files.Range(func(uri span.URI, fh source.VersionedFileHandle) {
|
||||
if overlay, ok := fh.(*overlay); ok && !overlay.saved {
|
||||
unsaved = append(unsaved, uri.Filename())
|
||||
}
|
||||
})
|
||||
sort.Strings(unsaved)
|
||||
return source.Hashf("%s", unsaved)
|
||||
}
|
||||
|
||||
func (s *snapshot) PackagesForFile(ctx context.Context, uri span.URI, mode source.TypecheckMode, includeTestVariants bool) ([]source.Package, error) {
|
||||
ctx = event.Label(ctx, tag.URI.Of(uri))
|
||||
|
||||
|
|
@ -791,6 +775,8 @@ func (s *snapshot) getImportedBy(id PackageID) []PackageID {
|
|||
// for the given package key, if it exists.
|
||||
//
|
||||
// An error is returned if the metadata used to build ph is no longer relevant.
|
||||
//
|
||||
// TODO(adonovan): inline sole use in buildPackageHandle.
|
||||
func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*packageHandle, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -801,14 +787,15 @@ func (s *snapshot) addPackageHandle(ph *packageHandle, release func()) (*package
|
|||
|
||||
// If the package handle has already been cached,
|
||||
// return the cached handle instead of overriding it.
|
||||
if result, ok := s.packages.Get(ph.packageKey()); ok {
|
||||
if v, ok := s.packages.Get(ph.packageKey()); ok {
|
||||
result := v.(*packageHandle)
|
||||
release()
|
||||
if result.m.Metadata != ph.m.Metadata {
|
||||
return nil, bug.Errorf("existing package handle does not match for %s", ph.m.ID)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
s.packages.Set(ph.packageKey(), ph, release)
|
||||
s.packages.Set(ph.packageKey(), ph, func(_, _ interface{}) { release() })
|
||||
return ph, nil
|
||||
}
|
||||
|
||||
|
|
@ -1179,8 +1166,8 @@ func (s *snapshot) CachedImportPaths(ctx context.Context) (map[string]source.Pac
|
|||
defer s.mu.Unlock()
|
||||
|
||||
results := map[string]source.Package{}
|
||||
s.packages.Range(func(key packageKey, ph *packageHandle) {
|
||||
cachedPkg, err := ph.cached()
|
||||
s.packages.Range(func(_, v interface{}) {
|
||||
cachedPkg, err := v.(*packageHandle).cached()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -1215,6 +1202,7 @@ func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI {
|
|||
return match
|
||||
}
|
||||
|
||||
// TODO(adonovan): inline sole use in buildPackageHandle.
|
||||
func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandle {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
|
@ -1223,8 +1211,11 @@ func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandl
|
|||
id: id,
|
||||
mode: mode,
|
||||
}
|
||||
ph, _ := s.packages.Get(key)
|
||||
return ph
|
||||
v, ok := s.packages.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return v.(*packageHandle)
|
||||
}
|
||||
|
||||
func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle {
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ type View struct {
|
|||
// snapshots have been destroyed via the destroy method, and snapshotWG may
|
||||
// be waited upon to let these destroy operations complete.
|
||||
snapshotMu sync.Mutex
|
||||
snapshot *snapshot // latest snapshot
|
||||
snapshot *snapshot // latest snapshot; nil after shutdown has been called
|
||||
releaseSnapshot func() // called when snapshot is no longer needed
|
||||
snapshotWG sync.WaitGroup // refcount for pending destroy operations
|
||||
|
||||
|
|
|
|||
|
|
@ -22,10 +22,12 @@ import (
|
|||
"golang.org/x/tools/internal/xcontext"
|
||||
)
|
||||
|
||||
// TODO(adonovan): rename Handle to Promise, and present it before Store.
|
||||
|
||||
// Store binds keys to functions, returning handles that can be used to access
|
||||
// the functions results.
|
||||
// the function's result.
|
||||
type Store struct {
|
||||
handlesMu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu
|
||||
handlesMu sync.Mutex
|
||||
handles map[interface{}]*Handle
|
||||
}
|
||||
|
||||
|
|
@ -71,8 +73,9 @@ const (
|
|||
|
||||
// A Handle represents the future result of a call to a function.
|
||||
type Handle struct {
|
||||
key interface{}
|
||||
mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu
|
||||
debug string // for observability
|
||||
|
||||
mu sync.Mutex // lock ordering: Store.handlesMu before Handle.mu
|
||||
|
||||
// A Handle starts out IDLE, waiting for something to demand
|
||||
// its evaluation. It then transitions into RUNNING state.
|
||||
|
|
@ -119,9 +122,9 @@ func (store *Store) Handle(key interface{}, function Function) (*Handle, func())
|
|||
if !ok {
|
||||
// new handle
|
||||
h = &Handle{
|
||||
key: key,
|
||||
function: function,
|
||||
refcount: 1,
|
||||
debug: reflect.TypeOf(key).String(),
|
||||
}
|
||||
|
||||
if store.handles == nil {
|
||||
|
|
@ -137,7 +140,7 @@ func (store *Store) Handle(key interface{}, function Function) (*Handle, func())
|
|||
release := func() {
|
||||
if atomic.AddInt32(&h.refcount, -1) == 0 {
|
||||
store.handlesMu.Lock()
|
||||
delete(store.handles, h.key)
|
||||
delete(store.handles, key)
|
||||
store.handlesMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
@ -170,6 +173,18 @@ func (s *Store) DebugOnlyIterate(f func(k, v interface{})) {
|
|||
}
|
||||
}
|
||||
|
||||
// NewHandle returns a handle for the future result of calling the
|
||||
// specified function.
|
||||
//
|
||||
// The debug string is used to classify handles in logs and metrics.
|
||||
// It should be drawn from a small set.
|
||||
func NewHandle(debug string, function Function) *Handle {
|
||||
return &Handle{
|
||||
debug: debug,
|
||||
function: function,
|
||||
}
|
||||
}
|
||||
|
||||
// Cached returns the value associated with a handle.
|
||||
//
|
||||
// It will never cause the value to be generated.
|
||||
|
|
@ -220,7 +235,7 @@ func (h *Handle) run(ctx context.Context, arg interface{}) (interface{}, error)
|
|||
}
|
||||
|
||||
go func() {
|
||||
trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %T", h.key), func() {
|
||||
trace.WithRegion(childCtx, fmt.Sprintf("Handle.run %s", h.debug), func() {
|
||||
defer release()
|
||||
// Just in case the function does something expensive without checking
|
||||
// the context, double-check we're still alive.
|
||||
|
|
|
|||
Loading…
Reference in New Issue