internal/lsp/cache: use GetHandle not Bind for 5 URI-keyed maps

This change replaces the 5 remaining calls to Bind (generational
lifetime) with GetHandle (reference counting). The handles
are now stored in persistent.Maps, which simplifies the
invalidation logic.

All 5 have span.URIs as keys:
  symbolizeHandles
  parse{Mod,Work}Handles
  mod{Tidy,Why}Handles

Also, factor the functions that use these maps to have a common form:
- a fooImpl function that returns an R result and an error;
- a foo wrapper that decorates it with caching.
- a local fooResult type, defined struct{R; error} that is the cache entry.
The functions for getting/setting map entries are all inlined.
The fooHandle types are all replaced by *memoize.Handle, now that
their use is local.

No behavior change is intended.

The other uses of Bind are deleted in these CLs:
https://go-review.googlesource.com/c/tools/+/415975 (astCacheData)
https://go-review.googlesource.com/c/tools/+/415504 (actions)

Change-Id: I77cc4e828936fe171152ca13a12f7a639299e9e5
Reviewed-on: https://go-review.googlesource.com/c/tools/+/415976
Auto-Submit: Alan Donovan <adonovan@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
Run-TryBot: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
This commit is contained in:
Alan Donovan 2022-07-04 13:22:21 -04:00
parent 2aef121b83
commit 1dfab61a48
7 changed files with 421 additions and 473 deletions

View File

@ -16,11 +16,14 @@ type filesMap struct {
impl *persistent.Map
}
// uriLessInterface is the < relation for "any" values containing span.URIs.
func uriLessInterface(a, b interface{}) bool {
return a.(span.URI) < b.(span.URI)
}
func newFilesMap() filesMap {
return filesMap{
impl: persistent.NewMap(func(a, b interface{}) bool {
return a.(span.URI) < b.(span.URI)
}),
impl: persistent.NewMap(uriLessInterface),
}
}
@ -152,9 +155,7 @@ type parseKeysByURIMap struct {
func newParseKeysByURIMap() parseKeysByURIMap {
return parseKeysByURIMap{
impl: persistent.NewMap(func(a, b interface{}) bool {
return a.(span.URI) < b.(span.URI)
}),
impl: persistent.NewMap(uriLessInterface),
}
}

View File

@ -24,152 +24,156 @@ import (
"golang.org/x/tools/internal/span"
)
type parseModHandle struct {
handle *memoize.Handle
}
// ParseMod parses a go.mod file, using a cache. It may return partial results and an error.
func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) {
uri := fh.URI()
type parseModData struct {
parsed *source.ParsedModule
s.mu.Lock()
entry, hit := s.parseModHandles.Get(uri)
s.mu.Unlock()
// err is any error encountered while parsing the file.
err error
}
type parseModResult struct {
parsed *source.ParsedModule
err error
}
func (mh *parseModHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedModule, error) {
v, err := mh.handle.Get(ctx, snapshot.generation, snapshot)
// cache miss?
if !hit {
handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} {
parsed, err := parseModImpl(ctx, fh)
return parseModResult{parsed, err}
})
entry = handle
s.mu.Lock()
s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() })
s.mu.Unlock()
}
// Await result.
v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s)
if err != nil {
return nil, err
}
data := v.(*parseModData)
return data.parsed, data.err
res := v.(parseModResult)
return res.parsed, res.err
}
func (s *snapshot) ParseMod(ctx context.Context, modFH source.FileHandle) (*source.ParsedModule, error) {
if handle := s.getParseModHandle(modFH.URI()); handle != nil {
return handle.parse(ctx, s)
}
h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} {
_, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI()))
defer done()
// parseModImpl parses the go.mod file whose name and contents are in fh.
// It may return partial results and an error.
func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) {
_, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI()))
defer done()
contents, err := modFH.Read()
if err != nil {
return &parseModData{err: err}
}
m := protocol.NewColumnMapper(modFH.URI(), contents)
file, parseErr := modfile.Parse(modFH.URI().Filename(), contents, nil)
// Attempt to convert the error to a standardized parse error.
var parseErrors []*source.Diagnostic
if parseErr != nil {
mfErrList, ok := parseErr.(modfile.ErrorList)
if !ok {
return &parseModData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
}
for _, mfErr := range mfErrList {
rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
if err != nil {
return &parseModData{err: err}
}
parseErrors = append(parseErrors, &source.Diagnostic{
URI: modFH.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
})
}
}
return &parseModData{
parsed: &source.ParsedModule{
URI: modFH.URI(),
Mapper: m,
File: file,
ParseErrors: parseErrors,
},
err: parseErr,
}
})
pmh := &parseModHandle{handle: h}
s.mu.Lock()
s.parseModHandles[modFH.URI()] = pmh
s.mu.Unlock()
return pmh.parse(ctx, s)
}
type parseWorkHandle struct {
handle *memoize.Handle
}
type parseWorkData struct {
parsed *source.ParsedWorkFile
// err is any error encountered while parsing the file.
err error
}
func (mh *parseWorkHandle) parse(ctx context.Context, snapshot *snapshot) (*source.ParsedWorkFile, error) {
v, err := mh.handle.Get(ctx, snapshot.generation, snapshot)
contents, err := fh.Read()
if err != nil {
return nil, err
}
data := v.(*parseWorkData)
return data.parsed, data.err
m := protocol.NewColumnMapper(fh.URI(), contents)
file, parseErr := modfile.Parse(fh.URI().Filename(), contents, nil)
// Attempt to convert the error to a standardized parse error.
var parseErrors []*source.Diagnostic
if parseErr != nil {
mfErrList, ok := parseErr.(modfile.ErrorList)
if !ok {
return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
}
for _, mfErr := range mfErrList {
rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
if err != nil {
return nil, err
}
parseErrors = append(parseErrors, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
})
}
}
return &source.ParsedModule{
URI: fh.URI(),
Mapper: m,
File: file,
ParseErrors: parseErrors,
}, parseErr
}
func (s *snapshot) ParseWork(ctx context.Context, modFH source.FileHandle) (*source.ParsedWorkFile, error) {
if handle := s.getParseWorkHandle(modFH.URI()); handle != nil {
return handle.parse(ctx, s)
}
h := s.generation.Bind(modFH.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} {
_, done := event.Start(ctx, "cache.ParseModHandle", tag.URI.Of(modFH.URI()))
defer done()
// ParseWork parses a go.work file, using a cache. It may return partial results and an error.
// TODO(adonovan): move to new work.go file.
func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) {
uri := fh.URI()
contents, err := modFH.Read()
if err != nil {
return &parseWorkData{err: err}
}
m := protocol.NewColumnMapper(modFH.URI(), contents)
file, parseErr := modfile.ParseWork(modFH.URI().Filename(), contents, nil)
// Attempt to convert the error to a standardized parse error.
var parseErrors []*source.Diagnostic
if parseErr != nil {
mfErrList, ok := parseErr.(modfile.ErrorList)
if !ok {
return &parseWorkData{err: fmt.Errorf("unexpected parse error type %v", parseErr)}
}
for _, mfErr := range mfErrList {
rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
if err != nil {
return &parseWorkData{err: err}
}
parseErrors = append(parseErrors, &source.Diagnostic{
URI: modFH.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
})
}
}
return &parseWorkData{
parsed: &source.ParsedWorkFile{
URI: modFH.URI(),
Mapper: m,
File: file,
ParseErrors: parseErrors,
},
err: parseErr,
}
})
pwh := &parseWorkHandle{handle: h}
s.mu.Lock()
s.parseWorkHandles[modFH.URI()] = pwh
entry, hit := s.parseWorkHandles.Get(uri)
s.mu.Unlock()
return pwh.parse(ctx, s)
type parseWorkResult struct {
parsed *source.ParsedWorkFile
err error
}
// cache miss?
if !hit {
handle, release := s.generation.GetHandle(fh.FileIdentity(), func(ctx context.Context, _ memoize.Arg) interface{} {
parsed, err := parseWorkImpl(ctx, fh)
return parseWorkResult{parsed, err}
})
entry = handle
s.mu.Lock()
s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() })
s.mu.Unlock()
}
// Await result.
v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s)
if err != nil {
return nil, err
}
res := v.(parseWorkResult)
return res.parsed, res.err
}
// parseWorkImpl parses a go.work file. It may return partial results and an error.
func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) {
_, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI()))
defer done()
contents, err := fh.Read()
if err != nil {
return nil, err
}
m := protocol.NewColumnMapper(fh.URI(), contents)
file, parseErr := modfile.ParseWork(fh.URI().Filename(), contents, nil)
// Attempt to convert the error to a standardized parse error.
var parseErrors []*source.Diagnostic
if parseErr != nil {
mfErrList, ok := parseErr.(modfile.ErrorList)
if !ok {
return nil, fmt.Errorf("unexpected parse error type %v", parseErr)
}
for _, mfErr := range mfErrList {
rng, err := rangeFromPositions(m, mfErr.Pos, mfErr.Pos)
if err != nil {
return nil, err
}
parseErrors = append(parseErrors, &source.Diagnostic{
URI: fh.URI(),
Range: rng,
Severity: protocol.SeverityError,
Source: source.ParseError,
Message: mfErr.Err.Error(),
})
}
}
return &source.ParsedWorkFile{
URI: fh.URI(),
Mapper: m,
File: file,
ParseErrors: parseErrors,
}, parseErr
}
// goSum reads the go.sum file for the go.mod file at modURI, if it exists. If
@ -198,104 +202,100 @@ func sumFilename(modURI span.URI) string {
return strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum"
}
// modKey is uniquely identifies cached data for `go mod why` or dependencies
// to upgrade.
type modKey struct {
sessionID string
env source.Hash
view string
mod source.FileIdentity
verb modAction
}
// ModWhy returns the "go mod why" result for each module named in a
// require statement in the go.mod file.
// TODO(adonovan): move to new mod_why.go file.
func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
uri := fh.URI()
type modAction int
if s.View().FileKind(fh) != source.Mod {
return nil, fmt.Errorf("%s is not a go.mod file", uri)
}
const (
why modAction = iota
upgrade
)
s.mu.Lock()
entry, hit := s.modWhyHandles.Get(uri)
s.mu.Unlock()
type modWhyHandle struct {
handle *memoize.Handle
}
type modWhyResult struct {
why map[string]string
err error
}
type modWhyData struct {
// why keeps track of the `go mod why` results for each require statement
// in the go.mod file.
why map[string]string
// cache miss?
if !hit {
// TODO(adonovan): use a simpler cache of promises that
// is shared across snapshots. See comment at modTidyKey.
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
// is actually correct. The key should probably just be URI, and
// invalidated in clone when any import changes.
sessionID string
env source.Hash
view string
mod source.FileIdentity
}
key := modWhyKey{
sessionID: s.view.session.id,
env: hashEnv(s),
mod: fh.FileIdentity(),
view: s.view.rootURI.Filename(),
}
handle, release := s.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} {
why, err := modWhyImpl(ctx, arg.(*snapshot), fh)
return modWhyResult{why, err}
})
err error
}
entry = handle
s.mu.Lock()
s.modWhyHandles.Set(uri, entry, func(_, _ interface{}) { release() })
s.mu.Unlock()
}
func (mwh *modWhyHandle) why(ctx context.Context, snapshot *snapshot) (map[string]string, error) {
v, err := mwh.handle.Get(ctx, snapshot.generation, snapshot)
// Await result.
v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s)
if err != nil {
return nil, err
}
data := v.(*modWhyData)
return data.why, data.err
res := v.(modWhyResult)
return res.why, res.err
}
func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) {
if s.View().FileKind(fh) != source.Mod {
return nil, fmt.Errorf("%s is not a go.mod file", fh.URI())
// modWhyImpl returns the result of "go mod why -m" on the specified go.mod file.
func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) (map[string]string, error) {
ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI()))
defer done()
pm, err := snapshot.ParseMod(ctx, fh)
if err != nil {
return nil, err
}
if handle := s.getModWhyHandle(fh.URI()); handle != nil {
return handle.why(ctx, s)
// No requires to explain.
if len(pm.File.Require) == 0 {
return nil, nil // empty result
}
key := modKey{
sessionID: s.view.session.id,
env: hashEnv(s),
mod: fh.FileIdentity(),
view: s.view.rootURI.Filename(),
verb: why,
// Run `go mod why` on all the dependencies.
inv := &gocommand.Invocation{
Verb: "mod",
Args: []string{"why", "-m"},
WorkingDir: filepath.Dir(fh.URI().Filename()),
}
h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
ctx, done := event.Start(ctx, "cache.ModWhyHandle", tag.URI.Of(fh.URI()))
defer done()
snapshot := arg.(*snapshot)
pm, err := snapshot.ParseMod(ctx, fh)
if err != nil {
return &modWhyData{err: err}
}
// No requires to explain.
if len(pm.File.Require) == 0 {
return &modWhyData{}
}
// Run `go mod why` on all the dependencies.
inv := &gocommand.Invocation{
Verb: "mod",
Args: []string{"why", "-m"},
WorkingDir: filepath.Dir(fh.URI().Filename()),
}
for _, req := range pm.File.Require {
inv.Args = append(inv.Args, req.Mod.Path)
}
stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv)
if err != nil {
return &modWhyData{err: err}
}
whyList := strings.Split(stdout.String(), "\n\n")
if len(whyList) != len(pm.File.Require) {
return &modWhyData{
err: fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)),
}
}
why := make(map[string]string, len(pm.File.Require))
for i, req := range pm.File.Require {
why[req.Mod.Path] = whyList[i]
}
return &modWhyData{why: why}
})
mwh := &modWhyHandle{handle: h}
s.mu.Lock()
s.modWhyHandles[fh.URI()] = mwh
s.mu.Unlock()
return mwh.why(ctx, s)
for _, req := range pm.File.Require {
inv.Args = append(inv.Args, req.Mod.Path)
}
stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv)
if err != nil {
return nil, err
}
whyList := strings.Split(stdout.String(), "\n\n")
if len(whyList) != len(pm.File.Require) {
return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require))
}
why := make(map[string]string, len(pm.File.Require))
for i, req := range pm.File.Require {
why[req.Mod.Path] = whyList[i]
}
return why, nil
}
// extractGoCommandError tries to parse errors that come from the go command

View File

@ -28,125 +28,139 @@ import (
"golang.org/x/tools/internal/span"
)
type modTidyKey struct {
sessionID string
env source.Hash
gomod source.FileIdentity
imports source.Hash
unsavedOverlays source.Hash
view string
}
type modTidyHandle struct {
handle *memoize.Handle
}
type modTidyData struct {
tidied *source.TidiedModule
err error
}
func (mth *modTidyHandle) tidy(ctx context.Context, snapshot *snapshot) (*source.TidiedModule, error) {
v, err := mth.handle.Get(ctx, snapshot.generation, snapshot)
if err != nil {
return nil, err
}
data := v.(*modTidyData)
return data.tidied, data.err
}
// modTidyImpl runs "go mod tidy" on a go.mod file, using a cache.
//
// REVIEWERS: what does it mean to cache an operation that has side effects?
// Or are we de-duplicating operations in flight on the same file?
func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) {
uri := pm.URI
if pm.File == nil {
return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", pm.URI)
return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri)
}
if handle := s.getModTidyHandle(pm.URI); handle != nil {
return handle.tidy(ctx, s)
s.mu.Lock()
entry, hit := s.modTidyHandles.Get(uri)
s.mu.Unlock()
type modTidyResult struct {
tidied *source.TidiedModule
err error
}
fh, err := s.GetFile(ctx, pm.URI)
// Cache miss?
if !hit {
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 {
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 {
return nil, err
}
s.mu.Lock()
overlayHash := hashUnsavedOverlays(s.files)
s.mu.Unlock()
// There's little reason at to use the shared cache for mod
// tidy (and mod why) as their key includes the view and session.
// 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.generation.GetHandle(key, func(ctx context.Context, arg memoize.Arg) interface{} {
tidied, err := modTidyImpl(ctx, arg.(*snapshot), fh, pm, workspacePkgs)
return modTidyResult{tidied, err}
})
entry = handle
s.mu.Lock()
s.modTidyHandles.Set(uri, entry, func(_, _ interface{}) { release() })
s.mu.Unlock()
}
// Await result.
v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s)
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.
if _, ok := fh.(*overlay); ok {
if info, _ := os.Stat(fh.URI().Filename()); info == nil {
return nil, source.ErrNoModOnDisk
}
res := v.(modTidyResult)
return res.tidied, res.err
}
// 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()))
defer done()
inv := &gocommand.Invocation{
Verb: "mod",
Args: []string{"tidy"},
WorkingDir: filepath.Dir(fh.URI().Filename()),
}
if criticalErr := s.GetCriticalError(ctx); criticalErr != nil {
return &source.TidiedModule{
Diagnostics: criticalErr.DiagList,
}, nil
tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
if err != nil {
return nil, err
}
workspacePkgs, err := s.workspacePackageHandles(ctx)
// Keep the temporary go.mod file around long enough to parse it.
defer cleanup()
if _, err := snapshot.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
return nil, err
}
// Go directly to disk to get the temporary mod file,
// since it is always on disk.
tempContents, err := ioutil.ReadFile(tmpURI.Filename())
if err != nil {
return nil, err
}
ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
if err != nil {
// We do not need to worry about the temporary file's parse errors
// since it has been "tidied".
return nil, err
}
// Compare the original and tidied go.mod files to compute errors and
// suggested fixes.
diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs)
if err != nil {
return nil, err
}
s.mu.Lock()
overlayHash := hashUnsavedOverlays(s.files)
s.mu.Unlock()
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),
}
h := s.generation.Bind(key, func(ctx context.Context, arg memoize.Arg) interface{} {
ctx, done := event.Start(ctx, "cache.ModTidyHandle", tag.URI.Of(fh.URI()))
defer done()
snapshot := arg.(*snapshot)
inv := &gocommand.Invocation{
Verb: "mod",
Args: []string{"tidy"},
WorkingDir: filepath.Dir(fh.URI().Filename()),
}
tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv)
if err != nil {
return &modTidyData{err: err}
}
// Keep the temporary go.mod file around long enough to parse it.
defer cleanup()
if _, err := s.view.session.gocmdRunner.Run(ctx, *inv); err != nil {
return &modTidyData{err: err}
}
// Go directly to disk to get the temporary mod file, since it is
// always on disk.
tempContents, err := ioutil.ReadFile(tmpURI.Filename())
if err != nil {
return &modTidyData{err: err}
}
ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil)
if err != nil {
// We do not need to worry about the temporary file's parse errors
// since it has been "tidied".
return &modTidyData{err: err}
}
// Compare the original and tidied go.mod files to compute errors and
// suggested fixes.
diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal, workspacePkgs)
if err != nil {
return &modTidyData{err: err}
}
return &modTidyData{
tidied: &source.TidiedModule{
Diagnostics: diagnostics,
TidiedContent: tempContents,
},
}
})
mth := &modTidyHandle{handle: h}
s.mu.Lock()
s.modTidyHandles[fh.URI()] = mth
s.mu.Unlock()
return mth.tidy(ctx, s)
return &source.TidiedModule{
Diagnostics: diagnostics,
TidiedContent: tempContents,
}, nil
}
func (s *snapshot) hashImports(ctx context.Context, wsPackages []*packageHandle) source.Hash {

View File

@ -238,14 +238,14 @@ func (s *Session) createView(ctx context.Context, name string, folder span.URI,
isActivePackageCache: newIsActivePackageCacheMap(),
goFiles: newGoFilesMap(),
parseKeysByURI: newParseKeysByURIMap(),
symbols: make(map[span.URI]*symbolHandle),
symbolizeHandles: persistent.NewMap(uriLessInterface),
actions: persistent.NewMap(actionKeyLessInterface),
workspacePackages: make(map[PackageID]PackagePath),
unloadableFiles: make(map[span.URI]struct{}),
parseModHandles: make(map[span.URI]*parseModHandle),
parseWorkHandles: make(map[span.URI]*parseWorkHandle),
modTidyHandles: make(map[span.URI]*modTidyHandle),
modWhyHandles: make(map[span.URI]*modWhyHandle),
parseModHandles: persistent.NewMap(uriLessInterface),
parseWorkHandles: persistent.NewMap(uriLessInterface),
modTidyHandles: persistent.NewMap(uriLessInterface),
modWhyHandles: persistent.NewMap(uriLessInterface),
knownSubdirs: newKnownDirsSet(),
workspace: workspace,
}

View File

@ -85,8 +85,9 @@ type snapshot struct {
goFiles goFilesMap
parseKeysByURI parseKeysByURIMap
// TODO(rfindley): consider merging this with files to reduce burden on clone.
symbols map[span.URI]*symbolHandle
// 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
// packages maps a packageKey to a *packageHandle.
// It may be invalidated when a file's content changes.
@ -109,17 +110,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 map[span.URI]*parseModHandle
parseModHandles *persistent.Map // from span.URI to *memoize.Handle
// parseWorkHandles keeps track of any parseWorkHandles for the snapshot.
// The handles need not refer to only the view's go.work file.
parseWorkHandles map[span.URI]*parseWorkHandle
parseWorkHandles *persistent.Map // from span.URI to *memoize.Handle
// 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 map[span.URI]*modTidyHandle
modWhyHandles map[span.URI]*modWhyHandle
modTidyHandles *persistent.Map // from span.URI to *memoize.Handle
modWhyHandles *persistent.Map // from span.URI to *memoize.Handle
workspace *workspace // (not guarded by mu)
@ -156,6 +157,11 @@ func (s *snapshot) Destroy(destroyedBy string) {
s.goFiles.Destroy()
s.parseKeysByURI.Destroy()
s.knownSubdirs.Destroy()
s.symbolizeHandles.Destroy()
s.parseModHandles.Destroy()
s.parseWorkHandles.Destroy()
s.modTidyHandles.Destroy()
s.modWhyHandles.Destroy()
if s.workspaceDir != "" {
if err := os.RemoveAll(s.workspaceDir); err != nil {
@ -700,30 +706,6 @@ func (s *snapshot) addGoFile(key parseKey, pgh *parseGoHandle, release func()) *
return pgh
}
func (s *snapshot) getParseModHandle(uri span.URI) *parseModHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.parseModHandles[uri]
}
func (s *snapshot) getParseWorkHandle(uri span.URI) *parseWorkHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.parseWorkHandles[uri]
}
func (s *snapshot) getModWhyHandle(uri span.URI) *modWhyHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.modWhyHandles[uri]
}
func (s *snapshot) getModTidyHandle(uri span.URI) *modTidyHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.modTidyHandles[uri]
}
func (s *snapshot) getImportedBy(id PackageID) []PackageID {
s.mu.Lock()
defer s.mu.Unlock()
@ -1039,12 +1021,12 @@ func (s *snapshot) Symbols(ctx context.Context) map[span.URI][]source.Symbol {
iolimit <- struct{}{} // acquire token
group.Go(func() error {
defer func() { <-iolimit }() // release token
v, err := s.buildSymbolHandle(ctx, f).handle.Get(ctx, s.generation, s)
symbols, err := s.symbolize(ctx, f)
if err != nil {
return err
}
resultMu.Lock()
result[uri] = v.(*symbolData).symbols
result[uri] = symbols
resultMu.Unlock()
return nil
})
@ -1159,26 +1141,6 @@ func (s *snapshot) getPackage(id PackageID, mode source.ParseMode) *packageHandl
return ph
}
func (s *snapshot) getSymbolHandle(uri span.URI) *symbolHandle {
s.mu.Lock()
defer s.mu.Unlock()
return s.symbols[uri]
}
func (s *snapshot) addSymbolHandle(uri span.URI, sh *symbolHandle) *symbolHandle {
s.mu.Lock()
defer s.mu.Unlock()
// If the package handle has already been cached,
// return the cached handle instead of overriding it.
if sh, ok := s.symbols[uri]; ok {
return sh
}
s.symbols[uri] = sh
return sh
}
func (s *snapshot) getActionHandle(id PackageID, m source.ParseMode, a *analysis.Analyzer) *actionHandle {
key := actionKey{
pkg: packageKey{
@ -1732,42 +1694,23 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
files: s.files.Clone(),
goFiles: s.goFiles.Clone(),
parseKeysByURI: s.parseKeysByURI.Clone(),
symbols: make(map[span.URI]*symbolHandle, len(s.symbols)),
symbolizeHandles: s.symbolizeHandles.Clone(),
workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)),
unloadableFiles: make(map[span.URI]struct{}, len(s.unloadableFiles)),
parseModHandles: make(map[span.URI]*parseModHandle, len(s.parseModHandles)),
parseWorkHandles: make(map[span.URI]*parseWorkHandle, len(s.parseWorkHandles)),
modTidyHandles: make(map[span.URI]*modTidyHandle, len(s.modTidyHandles)),
modWhyHandles: make(map[span.URI]*modWhyHandle, len(s.modWhyHandles)),
parseModHandles: s.parseModHandles.Clone(),
parseWorkHandles: s.parseWorkHandles.Clone(),
modTidyHandles: s.modTidyHandles.Clone(),
modWhyHandles: s.modWhyHandles.Clone(),
knownSubdirs: s.knownSubdirs.Clone(),
workspace: newWorkspace,
}
// Copy all of the FileHandles.
for k, v := range s.symbols {
if change, ok := changes[k]; ok {
if change.exists {
result.symbols[k] = result.buildSymbolHandle(ctx, change.fileHandle)
}
continue
}
newGen.Inherit(v.handle)
result.symbols[k] = v
}
// Copy the set of unloadable files.
for k, v := range s.unloadableFiles {
result.unloadableFiles[k] = v
}
// Copy all of the modHandles.
for k, v := range s.parseModHandles {
result.parseModHandles[k] = v
}
// Copy all of the parseWorkHandles.
for k, v := range s.parseWorkHandles {
result.parseWorkHandles[k] = v
}
// TODO(adonovan): merge loops over "changes".
for uri := range changes {
keys, ok := result.parseKeysByURI.Get(uri)
if ok {
@ -1776,21 +1719,13 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
}
result.parseKeysByURI.Delete(uri)
}
}
// Copy all of the go.mod-related handles. They may be invalidated later,
// so we inherit them at the end of the function.
for k, v := range s.modTidyHandles {
if _, ok := changes[k]; ok {
continue
}
result.modTidyHandles[k] = v
}
for k, v := range s.modWhyHandles {
if _, ok := changes[k]; ok {
continue
}
result.modWhyHandles[k] = v
// Invalidate go.mod-related handles.
result.modTidyHandles.Delete(uri)
result.modWhyHandles.Delete(uri)
// Invalidate handles for cached symbols.
result.symbolizeHandles.Delete(uri)
}
// Add all of the known subdirectories, but don't update them for the
@ -1857,17 +1792,16 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
// Invalidate the previous modTidyHandle if any of the files have been
// saved or if any of the metadata has been invalidated.
if invalidateMetadata || fileWasSaved(originalFH, change.fileHandle) {
// TODO(rstambler): Only delete mod handles for which the
// withoutURI is relevant.
for k := range s.modTidyHandles {
delete(result.modTidyHandles, k)
}
for k := range s.modWhyHandles {
delete(result.modWhyHandles, k)
}
// TODO(maybe): Only delete mod handles for
// which the withoutURI is relevant.
// Requires reverse-engineering the go command. (!)
result.modTidyHandles.Clear()
result.modWhyHandles.Clear()
}
delete(result.parseModHandles, uri)
delete(result.parseWorkHandles, uri)
result.parseModHandles.Delete(uri)
result.parseWorkHandles.Delete(uri)
// Handle the invalidated file; it may have new contents or not exist.
if !change.exists {
result.files.Delete(uri)
@ -2011,19 +1945,6 @@ func (s *snapshot) clone(ctx, bgCtx context.Context, changes map[span.URI]*fileC
result.workspacePackages = s.workspacePackages
}
// Inherit all of the go.mod-related handles.
for _, v := range result.modTidyHandles {
newGen.Inherit(v.handle)
}
for _, v := range result.modWhyHandles {
newGen.Inherit(v.handle)
}
for _, v := range result.parseModHandles {
newGen.Inherit(v.handle)
}
for _, v := range result.parseWorkHandles {
newGen.Inherit(v.handle)
}
// Don't bother copying the importedBy graph,
// as it changes each time we update metadata.

View File

@ -18,43 +18,48 @@ import (
"golang.org/x/tools/internal/memoize"
)
// A symbolHandle contains a handle to the result of symbolizing a file.
type symbolHandle struct {
handle *memoize.Handle
}
// symbolize returns the result of symbolizing the file identified by fh, using a cache.
func (s *snapshot) symbolize(ctx context.Context, fh source.FileHandle) ([]source.Symbol, error) {
uri := fh.URI()
// symbolData contains the data produced by extracting symbols from a file.
type symbolData struct {
symbols []source.Symbol
err error
}
s.mu.Lock()
entry, hit := s.symbolizeHandles.Get(uri)
s.mu.Unlock()
// buildSymbolHandle returns a handle to the future result of
// symbolizing the file identified by fh,
// if necessary creating it and saving it in the snapshot.
func (s *snapshot) buildSymbolHandle(ctx context.Context, fh source.FileHandle) *symbolHandle {
if h := s.getSymbolHandle(fh.URI()); h != nil {
return h
}
type symbolHandleKey source.Hash
key := symbolHandleKey(fh.FileIdentity().Hash)
handle := s.generation.Bind(key, func(_ context.Context, arg memoize.Arg) interface{} {
snapshot := arg.(*snapshot)
symbols, err := symbolize(snapshot, fh)
return &symbolData{symbols, err}
})
sh := &symbolHandle{
handle: handle,
type symbolizeResult struct {
symbols []source.Symbol
err error
}
return s.addSymbolHandle(fh.URI(), sh)
// Cache miss?
if !hit {
type symbolHandleKey source.Hash
key := symbolHandleKey(fh.FileIdentity().Hash)
handle, release := s.generation.GetHandle(key, func(_ context.Context, arg memoize.Arg) interface{} {
symbols, err := symbolizeImpl(arg.(*snapshot), fh)
return symbolizeResult{symbols, err}
})
entry = handle
s.mu.Lock()
s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() })
s.mu.Unlock()
}
// Await result.
v, err := entry.(*memoize.Handle).Get(ctx, s.generation, s)
if err != nil {
return nil, err
}
res := v.(symbolizeResult)
return res.symbols, res.err
}
// symbolize reads and parses a file and extracts symbols from it.
// symbolizeImpl reads and parses a file and extracts symbols from it.
// It may use a parsed file already present in the cache but
// otherwise does not populate the cache.
func symbolize(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
func symbolizeImpl(snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) {
src, err := fh.Read()
if err != nil {
return nil, err

View File

@ -120,10 +120,17 @@ func (pm *Map) Clone() *Map {
}
}
// Destroy the persistent map.
// Destroy destroys the map.
//
// After Destroy, the Map should not be used again.
func (pm *Map) Destroy() {
// The implementation of these two functions is the same,
// but their intent is different.
pm.Clear()
}
// Clear removes all entries from the map.
func (pm *Map) Clear() {
pm.root.decref()
pm.root = nil
}