// 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" "reflect" "strings" "sync" "time" "golang.org/x/tools/internal/event" "golang.org/x/tools/internal/event/keys" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/imports" "golang.org/x/tools/internal/lsp/source" ) type importsState struct { ctx context.Context mu sync.Mutex processEnv *imports.ProcessEnv cleanupProcessEnv func() cacheRefreshDuration time.Duration cacheRefreshTimer *time.Timer cachedModFileHash string cachedBuildFlags []string } func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(*imports.Options) error) error { s.mu.Lock() defer s.mu.Unlock() // Find the hash of the active mod file, 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. var modFileHash string if snapshot.workspaceMode()&usesWorkspaceModule == 0 { for m := range snapshot.workspace.getActiveModFiles() { // range to access the only element modFH, err := snapshot.GetFile(ctx, m) if err != nil { return err } modFileHash = modFH.FileIdentity().Hash } } else { modFile, err := snapshot.workspace.modFile(ctx, snapshot) if err != nil { return err } modBytes, err := modFile.Format() if err != nil { return err } modFileHash = hashContents(modBytes) } // view.goEnv is immutable -- changes make a new view. Options can change. // We can't compare build flags directly because we may add -modfile. snapshot.view.optionsMu.Lock() localPrefix := snapshot.view.options.Local currentBuildFlags := snapshot.view.options.BuildFlags changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || snapshot.view.options.VerboseOutput != (s.processEnv.Logf != nil) || modFileHash != s.cachedModFileHash snapshot.view.optionsMu.Unlock() // If anything relevant to imports has changed, clear caches and // update the processEnv. Clearing caches blocks on any background // scans. if changed { // As a special case, skip cleanup the first time -- we haven't fully // initialized the environment yet and calling GetResolver will do // unnecessary work and potentially mess up the go.mod file. if s.cleanupProcessEnv != nil { if resolver, err := s.processEnv.GetResolver(); err == nil { if modResolver, ok := resolver.(*imports.ModuleResolver); ok { modResolver.ClearForNewMod() } } s.cleanupProcessEnv() } s.cachedModFileHash = modFileHash s.cachedBuildFlags = currentBuildFlags var err error s.cleanupProcessEnv, err = s.populateProcessEnv(ctx, snapshot) if err != nil { return err } } // Run the user function. opts := &imports.Options{ // Defaults. AllErrors: true, Comments: true, Fragment: true, FormatOnly: false, TabIndent: true, TabWidth: 8, Env: s.processEnv, LocalPrefix: localPrefix, } if err := fn(opts); err != nil { return err } if s.cacheRefreshTimer == nil { // Don't refresh more than twice per minute. delay := 30 * time.Second // Don't spend more than a couple percent of the time refreshing. if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay { delay = adaptive } s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv) } return nil } // populateProcessEnv sets the dynamically configurable fields for the view's // process environment. Assumes that the caller is holding the s.view.importsMu. func (s *importsState) populateProcessEnv(ctx context.Context, snapshot *snapshot) (cleanup func(), err error) { pe := s.processEnv if snapshot.view.Options().VerboseOutput { pe.Logf = func(format string, args ...interface{}) { event.Log(ctx, fmt.Sprintf(format, args...)) } } else { pe.Logf = nil } // Take an extra reference to the snapshot so that its workspace directory // (if any) isn't destroyed while we're using it. release := snapshot.generation.Acquire(ctx) _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ WorkingDir: snapshot.view.rootURI.Filename(), }) if err != nil { return nil, err } pe.WorkingDir = inv.WorkingDir pe.BuildFlags = inv.BuildFlags pe.WorkingDir = inv.WorkingDir pe.ModFile = inv.ModFile pe.ModFlag = inv.ModFlag pe.Env = map[string]string{} for _, kv := range inv.Env { split := strings.SplitN(kv, "=", 2) if len(split) != 2 { continue } pe.Env[split[0]] = split[1] } return func() { cleanupInvocation() release() }, nil } func (s *importsState) refreshProcessEnv() { start := time.Now() s.mu.Lock() env := s.processEnv if resolver, err := s.processEnv.GetResolver(); err == nil { resolver.ClearForNewScan() } s.mu.Unlock() event.Log(s.ctx, "background imports cache refresh starting") if err := imports.PrimeCache(context.Background(), env); err == nil { event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) } else { event.Log(s.ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) } s.mu.Lock() s.cacheRefreshDuration = time.Since(start) s.cacheRefreshTimer = nil s.mu.Unlock() } func (s *importsState) destroy() { s.mu.Lock() if s.cleanupProcessEnv != nil { s.cleanupProcessEnv() } s.mu.Unlock() }