golang.org/x/tools/gopls@v0.15.3/internal/cache/imports.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package cache
     6  
     7  import (
     8  	"context"
     9  	"fmt"
    10  	"sync"
    11  	"time"
    12  
    13  	"golang.org/x/tools/gopls/internal/file"
    14  	"golang.org/x/tools/internal/event"
    15  	"golang.org/x/tools/internal/event/keys"
    16  	"golang.org/x/tools/internal/event/tag"
    17  	"golang.org/x/tools/internal/imports"
    18  )
    19  
    20  // refreshTimer implements delayed asynchronous refreshing of state.
    21  //
    22  // See the [refreshTimer.schedule] documentation for more details.
    23  type refreshTimer struct {
    24  	mu        sync.Mutex
    25  	duration  time.Duration
    26  	timer     *time.Timer
    27  	refreshFn func()
    28  }
    29  
    30  // newRefreshTimer constructs a new refresh timer which schedules refreshes
    31  // using the given function.
    32  func newRefreshTimer(refresh func()) *refreshTimer {
    33  	return &refreshTimer{
    34  		refreshFn: refresh,
    35  	}
    36  }
    37  
    38  // schedule schedules the refresh function to run at some point in the future,
    39  // if no existing refresh is already scheduled.
    40  //
    41  // At a minimum, scheduled refreshes are delayed by 30s, but they may be
    42  // delayed longer to keep their expected execution time under 2% of wall clock
    43  // time.
    44  func (t *refreshTimer) schedule() {
    45  	t.mu.Lock()
    46  	defer t.mu.Unlock()
    47  
    48  	if t.timer == nil {
    49  		// Don't refresh more than twice per minute.
    50  		delay := 30 * time.Second
    51  		// Don't spend more than ~2% of the time refreshing.
    52  		if adaptive := 50 * t.duration; adaptive > delay {
    53  			delay = adaptive
    54  		}
    55  		t.timer = time.AfterFunc(delay, func() {
    56  			start := time.Now()
    57  			t.refreshFn()
    58  			t.mu.Lock()
    59  			t.duration = time.Since(start)
    60  			t.timer = nil
    61  			t.mu.Unlock()
    62  		})
    63  	}
    64  }
    65  
    66  // A sharedModCache tracks goimports state for GOMODCACHE directories
    67  // (each session may have its own GOMODCACHE).
    68  //
    69  // This state is refreshed independently of view-specific imports state.
    70  type sharedModCache struct {
    71  	mu     sync.Mutex
    72  	caches map[string]*imports.DirInfoCache // GOMODCACHE -> cache content; never invalidated
    73  	timers map[string]*refreshTimer         // GOMODCACHE -> timer
    74  }
    75  
    76  func (c *sharedModCache) dirCache(dir string) *imports.DirInfoCache {
    77  	c.mu.Lock()
    78  	defer c.mu.Unlock()
    79  
    80  	cache, ok := c.caches[dir]
    81  	if !ok {
    82  		cache = imports.NewDirInfoCache()
    83  		c.caches[dir] = cache
    84  	}
    85  	return cache
    86  }
    87  
    88  // refreshDir schedules a refresh of the given directory, which must be a
    89  // module cache.
    90  func (c *sharedModCache) refreshDir(ctx context.Context, dir string, logf func(string, ...any)) {
    91  	cache := c.dirCache(dir)
    92  
    93  	c.mu.Lock()
    94  	defer c.mu.Unlock()
    95  	timer, ok := c.timers[dir]
    96  	if !ok {
    97  		timer = newRefreshTimer(func() {
    98  			_, done := event.Start(ctx, "cache.sharedModCache.refreshDir", tag.Directory.Of(dir))
    99  			defer done()
   100  			imports.ScanModuleCache(dir, cache, logf)
   101  		})
   102  		c.timers[dir] = timer
   103  	}
   104  
   105  	timer.schedule()
   106  }
   107  
   108  // importsState tracks view-specific imports state.
   109  type importsState struct {
   110  	ctx          context.Context
   111  	modCache     *sharedModCache
   112  	refreshTimer *refreshTimer
   113  
   114  	mu                sync.Mutex
   115  	processEnv        *imports.ProcessEnv
   116  	cachedModFileHash file.Hash
   117  }
   118  
   119  // newImportsState constructs a new imports state for running goimports
   120  // functions via [runProcessEnvFunc].
   121  //
   122  // The returned state will automatically refresh itself following a call to
   123  // runProcessEnvFunc.
   124  func newImportsState(backgroundCtx context.Context, modCache *sharedModCache, env *imports.ProcessEnv) *importsState {
   125  	s := &importsState{
   126  		ctx:        backgroundCtx,
   127  		modCache:   modCache,
   128  		processEnv: env,
   129  	}
   130  	s.refreshTimer = newRefreshTimer(s.refreshProcessEnv)
   131  	return s
   132  }
   133  
   134  // runProcessEnvFunc runs goimports.
   135  //
   136  // Any call to runProcessEnvFunc will schedule a refresh of the imports state
   137  // at some point in the future, if such a refresh is not already scheduled. See
   138  // [refreshTimer] for more details.
   139  func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error {
   140  	ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc")
   141  	defer done()
   142  
   143  	s.mu.Lock()
   144  	defer s.mu.Unlock()
   145  
   146  	// Find the hash of active mod files, if any. Using the unsaved content
   147  	// is slightly wasteful, since we'll drop caches a little too often, but
   148  	// the mod file shouldn't be changing while people are autocompleting.
   149  	//
   150  	// TODO(rfindley): consider instead hashing on-disk modfiles here.
   151  	var modFileHash file.Hash
   152  	for m := range snapshot.view.workspaceModFiles {
   153  		fh, err := snapshot.ReadFile(ctx, m)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		modFileHash.XORWith(fh.Identity().Hash)
   158  	}
   159  
   160  	// If anything relevant to imports has changed, clear caches and
   161  	// update the processEnv. Clearing caches blocks on any background
   162  	// scans.
   163  	if modFileHash != s.cachedModFileHash {
   164  		s.processEnv.ClearModuleInfo()
   165  		s.cachedModFileHash = modFileHash
   166  	}
   167  
   168  	// Run the user function.
   169  	opts := &imports.Options{
   170  		// Defaults.
   171  		AllErrors:   true,
   172  		Comments:    true,
   173  		Fragment:    true,
   174  		FormatOnly:  false,
   175  		TabIndent:   true,
   176  		TabWidth:    8,
   177  		Env:         s.processEnv,
   178  		LocalPrefix: snapshot.Options().Local,
   179  	}
   180  
   181  	if err := fn(ctx, opts); err != nil {
   182  		return err
   183  	}
   184  
   185  	// Refresh the imports resolver after usage. This may seem counterintuitive,
   186  	// since it means the first ProcessEnvFunc after a long period of inactivity
   187  	// may be stale, but in practice we run ProcessEnvFuncs frequently during
   188  	// active development (e.g. during completion), and so this mechanism will be
   189  	// active while gopls is in use, and inactive when gopls is idle.
   190  	s.refreshTimer.schedule()
   191  
   192  	// TODO(rfindley): the GOMODCACHE value used here isn't directly tied to the
   193  	// ProcessEnv.Env["GOMODCACHE"], though they should theoretically always
   194  	// agree. It would be better if we guaranteed this, possibly by setting all
   195  	// required environment variables in ProcessEnv.Env, to avoid the redundant
   196  	// Go command invocation.
   197  	gomodcache := snapshot.view.folder.Env.GOMODCACHE
   198  	s.modCache.refreshDir(s.ctx, gomodcache, s.processEnv.Logf)
   199  
   200  	return nil
   201  }
   202  
   203  func (s *importsState) refreshProcessEnv() {
   204  	ctx, done := event.Start(s.ctx, "cache.importsState.refreshProcessEnv")
   205  	defer done()
   206  
   207  	start := time.Now()
   208  
   209  	s.mu.Lock()
   210  	resolver, err := s.processEnv.GetResolver()
   211  	s.mu.Unlock()
   212  	if err != nil {
   213  		return
   214  	}
   215  
   216  	event.Log(s.ctx, "background imports cache refresh starting")
   217  
   218  	// Prime the new resolver before updating the processEnv, so that gopls
   219  	// doesn't wait on an unprimed cache.
   220  	if err := imports.PrimeCache(context.Background(), resolver); err == nil {
   221  		event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
   222  	} else {
   223  		event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
   224  	}
   225  
   226  	s.mu.Lock()
   227  	s.processEnv.UpdateResolver(resolver)
   228  	s.mu.Unlock()
   229  }