github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/internal/lsp/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  	"reflect"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/jhump/golang-x-tools/internal/event"
    16  	"github.com/jhump/golang-x-tools/internal/event/keys"
    17  	"github.com/jhump/golang-x-tools/internal/gocommand"
    18  	"github.com/jhump/golang-x-tools/internal/imports"
    19  	"github.com/jhump/golang-x-tools/internal/lsp/source"
    20  )
    21  
    22  type importsState struct {
    23  	ctx context.Context
    24  
    25  	mu                   sync.Mutex
    26  	processEnv           *imports.ProcessEnv
    27  	cleanupProcessEnv    func()
    28  	cacheRefreshDuration time.Duration
    29  	cacheRefreshTimer    *time.Timer
    30  	cachedModFileHash    string
    31  	cachedBuildFlags     []string
    32  }
    33  
    34  func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error {
    35  	s.mu.Lock()
    36  	defer s.mu.Unlock()
    37  
    38  	// Find the hash of the active mod file, if any. Using the unsaved content
    39  	// is slightly wasteful, since we'll drop caches a little too often, but
    40  	// the mod file shouldn't be changing while people are autocompleting.
    41  	var modFileHash string
    42  	// If we are using 'legacyWorkspace' mode, we can just read the modfile from
    43  	// the snapshot. Otherwise, we need to get the synthetic workspace mod file.
    44  	//
    45  	// TODO(rfindley): we should be able to just always use the synthetic
    46  	// workspace module, or alternatively use the go.work file.
    47  	if snapshot.workspace.moduleSource == legacyWorkspace {
    48  		for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element
    49  			modFH, err := snapshot.GetFile(ctx, m)
    50  			if err != nil {
    51  				return err
    52  			}
    53  			modFileHash = modFH.FileIdentity().Hash
    54  		}
    55  	} else {
    56  		modFile, err := snapshot.workspace.modFile(ctx, snapshot)
    57  		if err != nil {
    58  			return err
    59  		}
    60  		modBytes, err := modFile.Format()
    61  		if err != nil {
    62  			return err
    63  		}
    64  		modFileHash = hashContents(modBytes)
    65  	}
    66  
    67  	// view.goEnv is immutable -- changes make a new view. Options can change.
    68  	// We can't compare build flags directly because we may add -modfile.
    69  	snapshot.view.optionsMu.Lock()
    70  	localPrefix := snapshot.view.options.Local
    71  	currentBuildFlags := snapshot.view.options.BuildFlags
    72  	changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) ||
    73  		snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) ||
    74  		modFileHash != s.cachedModFileHash
    75  	snapshot.view.optionsMu.Unlock()
    76  
    77  	// If anything relevant to imports has changed, clear caches and
    78  	// update the processEnv. Clearing caches blocks on any background
    79  	// scans.
    80  	if changed {
    81  		// As a special case, skip cleanup the first time -- we haven't fully
    82  		// initialized the environment yet and calling GetResolver will do
    83  		// unnecessary work and potentially mess up the go.mod file.
    84  		if s.cleanupProcessEnv != nil {
    85  			if resolver, err := s.processEnv.GetResolver(); err == nil {
    86  				if modResolver, ok := resolver.(*imports.ModuleResolver); ok {
    87  					modResolver.ClearForNewMod()
    88  				}
    89  			}
    90  			s.cleanupProcessEnv()
    91  		}
    92  		s.cachedModFileHash = modFileHash
    93  		s.cachedBuildFlags = currentBuildFlags
    94  		var err error
    95  		s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot)
    96  		if err != nil {
    97  			return err
    98  		}
    99  	}
   100  
   101  	// Run the user function.
   102  	opts := &imports.Options{
   103  		// Defaults.
   104  		AllErrors:   true,
   105  		Comments:    true,
   106  		Fragment:    true,
   107  		FormatOnly:  false,
   108  		TabIndent:   true,
   109  		TabWidth:    8,
   110  		Env:         s.processEnv,
   111  		LocalPrefix: localPrefix,
   112  	}
   113  
   114  	if err := fn(opts); err != nil {
   115  		return err
   116  	}
   117  
   118  	if s.cacheRefreshTimer == nil {
   119  		// Don't refresh more than twice per minute.
   120  		delay := 30 * time.Second
   121  		// Don't spend more than a couple percent of the time refreshing.
   122  		if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay {
   123  			delay = adaptive
   124  		}
   125  		s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv)
   126  	}
   127  
   128  	return nil
   129  }
   130  
   131  // populateProcessEnv sets the dynamically configurable fields for the view's
   132  // process environment. Assumes that the caller is holding the s.view.importsMu.
   133  func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) {
   134  	pe := s.processEnv
   135  
   136  	if snapshot.view.Options().VerboseOutput {
   137  		pe.Logf = func(format string, args ...interface{}) {
   138  			event.Log(ctx, fmt.Sprintf(format, args...))
   139  		}
   140  	} else {
   141  		pe.Logf = nil
   142  	}
   143  
   144  	// Take an extra reference to the snapshot so that its workspace directory
   145  	// (if any) isn't destroyed while we're using it.
   146  	release := snapshot.generation.Acquire()
   147  	_, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{
   148  		WorkingDir: snapshot.view.rootURI.Filename(),
   149  	})
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	pe.WorkingDir = inv.WorkingDir
   154  	pe.BuildFlags = inv.BuildFlags
   155  	pe.WorkingDir = inv.WorkingDir
   156  	pe.ModFile = inv.ModFile
   157  	pe.ModFlag = inv.ModFlag
   158  	pe.Env = map[string]string{}
   159  	for _, kv := range inv.Env {
   160  		split := strings.SplitN(kv, "=", 2)
   161  		if len(split) != 2 {
   162  			continue
   163  		}
   164  		pe.Env[split[0]] = split[1]
   165  	}
   166  
   167  	return func() {
   168  		cleanupInvocation()
   169  		release()
   170  	}, nil
   171  }
   172  
   173  func (s *importsState) refreshProcessEnv() {
   174  	start := time.Now()
   175  
   176  	s.mu.Lock()
   177  	env := s.processEnv
   178  	if resolver, err := s.processEnv.GetResolver(); err == nil {
   179  		resolver.ClearForNewScan()
   180  	}
   181  	s.mu.Unlock()
   182  
   183  	event.Log(s.ctx, "background imports cache refresh starting")
   184  	if err := imports.PrimeCache(context.Background(), env); err == nil {
   185  		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
   186  	} else {
   187  		event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
   188  	}
   189  	s.mu.Lock()
   190  	s.cacheRefreshDuration = time.Since(start)
   191  	s.cacheRefreshTimer = nil
   192  	s.mu.Unlock()
   193  }
   194  
   195  func (s *importsState) destroy() {
   196  	s.mu.Lock()
   197  	if s.cleanupProcessEnv != nil {
   198  		s.cleanupProcessEnv()
   199  	}
   200  	s.mu.Unlock()
   201  }