Skip to content

Commit 8b6359d

Browse files
committed
gopls/internal/cache: share goimports state for GOMODCACHE
When using the gopls daemon, or with the multi-View workspaces that will be increasingly common following golang/go#57979, there is a lot of redundant work performed scanning the module cache. This CL eliminates that redundancy, by moving module cache information into the cache.Cache shared by all Sessions and Views. There should be effectively no change in behavior for gopls resulting from this CL. In ModuleResolver.scan, we still require that module cache roots are scanned. However, we no longer invalidate this scan in ModuleResolver.ClearForNewScan: re-scanning the module cache is the responsibility of a new ScanModuleCache function, which is independently scheduled. To enable this separation of refresh logic, a new refreshTimer type is extracted to encapsulate the refresh logic. For golang/go#44863 Change-Id: I333d55fca009be7984a514ed4abdc9a9fcafc08a Reviewed-on: https://go-review.googlesource.com/c/tools/+/559636 Reviewed-by: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 2bb7f1c commit 8b6359d

File tree

7 files changed

+332
-75
lines changed

7 files changed

+332
-75
lines changed

gopls/internal/cache/cache.go

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sync/atomic"
1111

1212
"golang.org/x/tools/gopls/internal/protocol/command"
13+
"golang.org/x/tools/internal/imports"
1314
"golang.org/x/tools/internal/memoize"
1415
)
1516

@@ -30,20 +31,36 @@ func New(store *memoize.Store) *Cache {
3031
id: strconv.FormatInt(index, 10),
3132
store: store,
3233
memoizedFS: newMemoizedFS(),
34+
modCache: &sharedModCache{
35+
caches: make(map[string]*imports.DirInfoCache),
36+
timers: make(map[string]*refreshTimer),
37+
},
3338
}
3439
return c
3540
}
3641

37-
// A Cache holds caching stores that are bundled together for consistency.
38-
//
39-
// TODO(rfindley): once fset and store need not be bundled together, the Cache
40-
// type can be eliminated.
42+
// A Cache holds content that is shared across multiple gopls sessions.
4143
type Cache struct {
4244
id string
4345

46+
// store holds cached calculations.
47+
//
48+
// TODO(rfindley): at this point, these are not important, as we've moved our
49+
// content-addressable cache to the file system (the filecache package). It
50+
// is unlikely that this shared cache provides any shared value. We should
51+
// consider removing it, replacing current uses with a simpler futures cache,
52+
// as we've done for e.g. type-checked packages.
4453
store *memoize.Store
4554

46-
*memoizedFS // implements file.Source
55+
// memoizedFS holds a shared file.Source that caches reads.
56+
//
57+
// Reads are invalidated when *any* session gets a didChangeWatchedFile
58+
// notification. This is fine: it is the responsibility of memoizedFS to hold
59+
// our best knowledge of the current file system state.
60+
*memoizedFS
61+
62+
// modCache holds the
63+
modCache *sharedModCache
4764
}
4865

4966
var cacheIndex, sessionIndex, viewIndex int64

gopls/internal/cache/imports.go

Lines changed: 135 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,129 @@ import (
1313
"golang.org/x/tools/gopls/internal/file"
1414
"golang.org/x/tools/internal/event"
1515
"golang.org/x/tools/internal/event/keys"
16+
"golang.org/x/tools/internal/event/tag"
1617
"golang.org/x/tools/internal/imports"
1718
)
1819

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.
19109
type importsState struct {
20-
ctx context.Context
110+
ctx context.Context
111+
modCache *sharedModCache
112+
refreshTimer *refreshTimer
113+
114+
mu sync.Mutex
115+
processEnv *imports.ProcessEnv
116+
cachedModFileHash file.Hash
117+
}
21118

22-
mu sync.Mutex
23-
processEnv *imports.ProcessEnv
24-
cacheRefreshDuration time.Duration
25-
cacheRefreshTimer *time.Timer
26-
cachedModFileHash file.Hash
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
27132
}
28133

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.
29139
func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error {
30140
ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc")
31141
defer done()
@@ -72,15 +182,20 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot
72182
return err
73183
}
74184

75-
if s.cacheRefreshTimer == nil {
76-
// Don't refresh more than twice per minute.
77-
delay := 30 * time.Second
78-
// Don't spend more than a couple percent of the time refreshing.
79-
if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay {
80-
delay = adaptive
81-
}
82-
s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv)
83-
}
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)
84199

85200
return nil
86201
}
@@ -96,16 +211,17 @@ func (s *importsState) refreshProcessEnv() {
96211
if resolver, err := s.processEnv.GetResolver(); err == nil {
97212
resolver.ClearForNewScan()
98213
}
214+
// TODO(rfindley): it's not clear why we're unlocking here. Shouldn't we
215+
// guard the use of env below? In any case, we can prime a separate resolver.
99216
s.mu.Unlock()
100217

101218
event.Log(s.ctx, "background imports cache refresh starting")
219+
220+
// TODO(rfindley, golang/go#59216): do this priming with a separate resolver,
221+
// and then replace, so that we never have to wait on an unprimed cache.
102222
if err := imports.PrimeCache(context.Background(), env); err == nil {
103223
event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)))
104224
} else {
105225
event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err))
106226
}
107-
s.mu.Lock()
108-
s.cacheRefreshDuration = time.Since(start)
109-
s.cacheRefreshTimer = nil
110-
s.mu.Unlock()
111227
}

gopls/internal/cache/session.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
209209
SkipPathInScan: skipPath,
210210
Env: env,
211211
WorkingDir: def.root.Path(),
212+
ModCache: s.cache.modCache.dirCache(def.folder.Env.GOMODCACHE),
212213
}
213214
if def.folder.Options.VerboseOutput {
214215
pe.Logf = func(format string, args ...interface{}) {
@@ -227,10 +228,7 @@ func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *
227228
ignoreFilter: ignoreFilter,
228229
fs: s.overlayFS,
229230
viewDefinition: def,
230-
importsState: &importsState{
231-
ctx: backgroundCtx,
232-
processEnv: pe,
233-
},
231+
importsState: newImportsState(backgroundCtx, s.cache.modCache, pe),
234232
}
235233

236234
s.snapshotWG.Add(1)

internal/imports/fix.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,10 @@ type ProcessEnv struct {
884884
// If Logf is non-nil, debug logging is enabled through this function.
885885
Logf func(format string, args ...interface{})
886886

887+
// If set, ModCache holds a shared cache of directory info to use across
888+
// multiple ProcessEnvs.
889+
ModCache *DirInfoCache
890+
887891
initialized bool // see TODO above
888892

889893
// resolver and resolverErr are lazily evaluated (see GetResolver).
@@ -984,7 +988,7 @@ func (e *ProcessEnv) GetResolver() (Resolver, error) {
984988
if len(e.Env["GOMOD"]) == 0 && len(e.Env["GOWORK"]) == 0 {
985989
e.resolver = newGopathResolver(e)
986990
} else {
987-
e.resolver, e.resolverErr = newModuleResolver(e)
991+
e.resolver, e.resolverErr = newModuleResolver(e, e.ModCache)
988992
}
989993
}
990994

@@ -1252,17 +1256,14 @@ func ImportPathToAssumedName(importPath string) string {
12521256
type gopathResolver struct {
12531257
env *ProcessEnv
12541258
walked bool
1255-
cache *dirInfoCache
1259+
cache *DirInfoCache
12561260
scanSema chan struct{} // scanSema prevents concurrent scans.
12571261
}
12581262

12591263
func newGopathResolver(env *ProcessEnv) *gopathResolver {
12601264
r := &gopathResolver{
1261-
env: env,
1262-
cache: &dirInfoCache{
1263-
dirs: map[string]*directoryPackageInfo{},
1264-
listeners: map[*int]cacheListener{},
1265-
},
1265+
env: env,
1266+
cache: NewDirInfoCache(),
12661267
scanSema: make(chan struct{}, 1),
12671268
}
12681269
r.scanSema <- struct{}{}
@@ -1271,10 +1272,7 @@ func newGopathResolver(env *ProcessEnv) *gopathResolver {
12711272

12721273
func (r *gopathResolver) ClearForNewScan() {
12731274
<-r.scanSema
1274-
r.cache = &dirInfoCache{
1275-
dirs: map[string]*directoryPackageInfo{},
1276-
listeners: map[*int]cacheListener{},
1277-
}
1275+
r.cache = NewDirInfoCache()
12781276
r.walked = false
12791277
r.scanSema <- struct{}{}
12801278
}

0 commit comments

Comments
 (0)