| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cache |
| |
| import ( |
| "context" |
| "fmt" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/gopls/internal/file" |
| "golang.org/x/tools/internal/event" |
| "golang.org/x/tools/internal/event/keys" |
| "golang.org/x/tools/internal/event/tag" |
| "golang.org/x/tools/internal/imports" |
| ) |
| |
| // refreshTimer implements delayed asynchronous refreshing of state. |
| // |
| // See the [refreshTimer.schedule] documentation for more details. |
| type refreshTimer struct { |
| mu sync.Mutex |
| duration time.Duration |
| timer *time.Timer |
| refreshFn func() |
| } |
| |
| // newRefreshTimer constructs a new refresh timer which schedules refreshes |
| // using the given function. |
| func newRefreshTimer(refresh func()) *refreshTimer { |
| return &refreshTimer{ |
| refreshFn: refresh, |
| } |
| } |
| |
| // schedule schedules the refresh function to run at some point in the future, |
| // if no existing refresh is already scheduled. |
| // |
| // At a minimum, scheduled refreshes are delayed by 30s, but they may be |
| // delayed longer to keep their expected execution time under 2% of wall clock |
| // time. |
| func (t *refreshTimer) schedule() { |
| t.mu.Lock() |
| defer t.mu.Unlock() |
| |
| if t.timer == nil { |
| // Don't refresh more than twice per minute. |
| delay := 30 * time.Second |
| // Don't spend more than ~2% of the time refreshing. |
| if adaptive := 50 * t.duration; adaptive > delay { |
| delay = adaptive |
| } |
| t.timer = time.AfterFunc(delay, func() { |
| start := time.Now() |
| t.refreshFn() |
| t.mu.Lock() |
| t.duration = time.Since(start) |
| t.timer = nil |
| t.mu.Unlock() |
| }) |
| } |
| } |
| |
| // A sharedModCache tracks goimports state for GOMODCACHE directories |
| // (each session may have its own GOMODCACHE). |
| // |
| // This state is refreshed independently of view-specific imports state. |
| type sharedModCache struct { |
| mu sync.Mutex |
| caches map[string]*imports.DirInfoCache // GOMODCACHE -> cache content; never invalidated |
| timers map[string]*refreshTimer // GOMODCACHE -> timer |
| } |
| |
| func (c *sharedModCache) dirCache(dir string) *imports.DirInfoCache { |
| c.mu.Lock() |
| defer c.mu.Unlock() |
| |
| cache, ok := c.caches[dir] |
| if !ok { |
| cache = imports.NewDirInfoCache() |
| c.caches[dir] = cache |
| } |
| return cache |
| } |
| |
| // refreshDir schedules a refresh of the given directory, which must be a |
| // module cache. |
| func (c *sharedModCache) refreshDir(ctx context.Context, dir string, logf func(string, ...any)) { |
| cache := c.dirCache(dir) |
| |
| c.mu.Lock() |
| defer c.mu.Unlock() |
| timer, ok := c.timers[dir] |
| if !ok { |
| timer = newRefreshTimer(func() { |
| _, done := event.Start(ctx, "cache.sharedModCache.refreshDir", tag.Directory.Of(dir)) |
| defer done() |
| imports.ScanModuleCache(dir, cache, logf) |
| }) |
| c.timers[dir] = timer |
| } |
| |
| timer.schedule() |
| } |
| |
| // importsState tracks view-specific imports state. |
| type importsState struct { |
| ctx context.Context |
| modCache *sharedModCache |
| refreshTimer *refreshTimer |
| |
| mu sync.Mutex |
| processEnv *imports.ProcessEnv |
| cachedModFileHash file.Hash |
| } |
| |
| // newImportsState constructs a new imports state for running goimports |
| // functions via [runProcessEnvFunc]. |
| // |
| // The returned state will automatically refresh itself following a call to |
| // runProcessEnvFunc. |
| func newImportsState(backgroundCtx context.Context, modCache *sharedModCache, env *imports.ProcessEnv) *importsState { |
| s := &importsState{ |
| ctx: backgroundCtx, |
| modCache: modCache, |
| processEnv: env, |
| } |
| s.refreshTimer = newRefreshTimer(s.refreshProcessEnv) |
| return s |
| } |
| |
| // runProcessEnvFunc runs goimports. |
| // |
| // Any call to runProcessEnvFunc will schedule a refresh of the imports state |
| // at some point in the future, if such a refresh is not already scheduled. See |
| // [refreshTimer] for more details. |
| func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error { |
| ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc") |
| defer done() |
| |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| // Find the hash of active mod files, if any. Using the unsaved content |
| // is slightly wasteful, since we'll drop caches a little too often, but |
| // the mod file shouldn't be changing while people are autocompleting. |
| // |
| // TODO(rfindley): consider instead hashing on-disk modfiles here. |
| var modFileHash file.Hash |
| for m := range snapshot.view.workspaceModFiles { |
| fh, err := snapshot.ReadFile(ctx, m) |
| if err != nil { |
| return err |
| } |
| modFileHash.XORWith(fh.Identity().Hash) |
| } |
| |
| // If anything relevant to imports has changed, clear caches and |
| // update the processEnv. Clearing caches blocks on any background |
| // scans. |
| if modFileHash != s.cachedModFileHash { |
| s.processEnv.ClearModuleInfo() |
| s.cachedModFileHash = modFileHash |
| } |
| |
| // Run the user function. |
| opts := &imports.Options{ |
| // Defaults. |
| AllErrors: true, |
| Comments: true, |
| Fragment: true, |
| FormatOnly: false, |
| TabIndent: true, |
| TabWidth: 8, |
| Env: s.processEnv, |
| LocalPrefix: snapshot.Options().Local, |
| } |
| |
| if err := fn(ctx, opts); err != nil { |
| return err |
| } |
| |
| // Refresh the imports resolver after usage. This may seem counterintuitive, |
| // since it means the first ProcessEnvFunc after a long period of inactivity |
| // may be stale, but in practice we run ProcessEnvFuncs frequently during |
| // active development (e.g. during completion), and so this mechanism will be |
| // active while gopls is in use, and inactive when gopls is idle. |
| s.refreshTimer.schedule() |
| |
| // TODO(rfindley): the GOMODCACHE value used here isn't directly tied to the |
| // ProcessEnv.Env["GOMODCACHE"], though they should theoretically always |
| // agree. It would be better if we guaranteed this, possibly by setting all |
| // required environment variables in ProcessEnv.Env, to avoid the redundant |
| // Go command invocation. |
| gomodcache := snapshot.view.folder.Env.GOMODCACHE |
| s.modCache.refreshDir(s.ctx, gomodcache, s.processEnv.Logf) |
| |
| return nil |
| } |
| |
| func (s *importsState) refreshProcessEnv() { |
| ctx, done := event.Start(s.ctx, "cache.importsState.refreshProcessEnv") |
| defer done() |
| |
| start := time.Now() |
| |
| s.mu.Lock() |
| resolver, err := s.processEnv.GetResolver() |
| s.mu.Unlock() |
| if err != nil { |
| return |
| } |
| |
| event.Log(s.ctx, "background imports cache refresh starting") |
| |
| // Prime the new resolver before updating the processEnv, so that gopls |
| // doesn't wait on an unprimed cache. |
| if err := imports.PrimeCache(context.Background(), resolver); err == nil { |
| event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) |
| } else { |
| event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) |
| } |
| |
| s.mu.Lock() |
| s.processEnv.UpdateResolver(resolver) |
| s.mu.Unlock() |
| } |