@@ -13,19 +13,129 @@ import (
13
13
"golang.org/x/tools/gopls/internal/file"
14
14
"golang.org/x/tools/internal/event"
15
15
"golang.org/x/tools/internal/event/keys"
16
+ "golang.org/x/tools/internal/event/tag"
16
17
"golang.org/x/tools/internal/imports"
17
18
)
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.
19
109
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
+ }
21
118
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
27
132
}
28
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.
29
139
func (s * importsState ) runProcessEnvFunc (ctx context.Context , snapshot * Snapshot , fn func (context.Context , * imports.Options ) error ) error {
30
140
ctx , done := event .Start (ctx , "cache.importsState.runProcessEnvFunc" )
31
141
defer done ()
@@ -72,15 +182,20 @@ func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot
72
182
return err
73
183
}
74
184
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 )
84
199
85
200
return nil
86
201
}
@@ -96,16 +211,17 @@ func (s *importsState) refreshProcessEnv() {
96
211
if resolver , err := s .processEnv .GetResolver (); err == nil {
97
212
resolver .ClearForNewScan ()
98
213
}
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.
99
216
s .mu .Unlock ()
100
217
101
218
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.
102
222
if err := imports .PrimeCache (context .Background (), env ); err == nil {
103
223
event .Log (ctx , fmt .Sprintf ("background refresh finished after %v" , time .Since (start )))
104
224
} else {
105
225
event .Log (ctx , fmt .Sprintf ("background refresh finished after %v" , time .Since (start )), keys .Err .Of (err ))
106
226
}
107
- s .mu .Lock ()
108
- s .cacheRefreshDuration = time .Since (start )
109
- s .cacheRefreshTimer = nil
110
- s .mu .Unlock ()
111
227
}
0 commit comments