@@ -20,6 +20,7 @@ import (
20
20
"log"
21
21
urlpkg "net/url"
22
22
"reflect"
23
+ "runtime"
23
24
"runtime/debug"
24
25
"sort"
25
26
"strings"
@@ -32,6 +33,7 @@ import (
32
33
"golang.org/x/tools/gopls/internal/bug"
33
34
"golang.org/x/tools/gopls/internal/lsp/filecache"
34
35
"golang.org/x/tools/gopls/internal/lsp/frob"
36
+ "golang.org/x/tools/gopls/internal/lsp/progress"
35
37
"golang.org/x/tools/gopls/internal/lsp/protocol"
36
38
"golang.org/x/tools/gopls/internal/lsp/source"
37
39
"golang.org/x/tools/internal/event"
@@ -154,14 +156,23 @@ import (
154
156
// View() *View // for Options
155
157
// - share cache.{goVersionRx,parseGoImpl}
156
158
159
+ // AnalysisProgressTitle is the title of the progress report for ongoing
160
+ // analysis. It is sought by regression tests for the progress reporting
161
+ // feature.
162
+ const AnalysisProgressTitle = "Analyzing Dependencies"
163
+
157
164
// Analyze applies a set of analyzers to the package denoted by id,
158
165
// and returns their diagnostics for that package.
159
166
//
160
167
// The analyzers list must be duplicate free; order does not matter.
161
168
//
169
+ // Notifications of progress may be sent to the optional reporter.
170
+ //
162
171
// Precondition: all analyzers within the process have distinct names.
163
172
// (The names are relied on by the serialization logic.)
164
- func (snapshot * snapshot ) Analyze (ctx context.Context , pkgs map [PackageID ]unit , analyzers []* source.Analyzer ) ([]* source.Diagnostic , error ) {
173
+ func (snapshot * snapshot ) Analyze (ctx context.Context , pkgs map [PackageID ]unit , analyzers []* source.Analyzer , reporter * progress.Tracker ) ([]* source.Diagnostic , error ) {
174
+ start := time .Now () // for progress reporting
175
+
165
176
var tagStr string // sorted comma-separated list of PackageIDs
166
177
{
167
178
// TODO(adonovan): replace with a generic map[S]any -> string
@@ -296,21 +307,84 @@ func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit,
296
307
297
308
// Now that we have read all files,
298
309
// we no longer need the snapshot.
310
+ // (but options are needed for progress reporting)
311
+ options := snapshot .view .Options ()
299
312
snapshot = nil
300
313
314
+ // Progress reporting. If supported, gopls reports progress on analysis
315
+ // passes that are taking a long time.
316
+ maybeReport := func (completed int64 ) {}
317
+
318
+ // Enable progress reporting if enabled by the user
319
+ // and we have a capable reporter.
320
+ if reporter != nil && reporter .SupportsWorkDoneProgress () && options .AnalysisProgressReporting {
321
+ var reportAfter = options .ReportAnalysisProgressAfter // tests may set this to 0
322
+ const reportEvery = 1 * time .Second
323
+
324
+ ctx , cancel := context .WithCancel (ctx )
325
+ defer cancel ()
326
+
327
+ var (
328
+ reportMu sync.Mutex
329
+ lastReport time.Time
330
+ wd * progress.WorkDone
331
+ )
332
+ defer func () {
333
+ reportMu .Lock ()
334
+ defer reportMu .Unlock ()
335
+
336
+ if wd != nil {
337
+ wd .End (ctx , "Done." ) // ensure that the progress report exits
338
+ }
339
+ }()
340
+ maybeReport = func (completed int64 ) {
341
+ now := time .Now ()
342
+ if now .Sub (start ) < reportAfter {
343
+ return
344
+ }
345
+
346
+ reportMu .Lock ()
347
+ defer reportMu .Unlock ()
348
+
349
+ if wd == nil {
350
+ wd = reporter .Start (ctx , AnalysisProgressTitle , "" , nil , cancel )
351
+ }
352
+
353
+ if now .Sub (lastReport ) > reportEvery {
354
+ lastReport = now
355
+ // Trailing space is intentional: some LSP clients strip newlines.
356
+ msg := fmt .Sprintf (`Constructing index of analysis facts... (%d/%d packages).
357
+ (Set "analysisProgressReporting" to false to disable notifications.)` ,
358
+ completed , len (nodes ))
359
+ pct := 100 * float64 (completed ) / float64 (len (nodes ))
360
+ wd .Report (ctx , msg , pct )
361
+ }
362
+ }
363
+ }
364
+
301
365
// Execute phase: run leaves first, adding
302
366
// new nodes to the queue as they become leaves.
303
367
var g errgroup.Group
304
- // Avoid g.SetLimit here: it makes g.Go stop accepting work,
305
- // which prevents workers from enqeuing, and thus finishing,
306
- // and thus allowing the group to make progress: deadlock.
368
+
369
+ // Analysis is CPU-bound.
370
+ //
371
+ // Note: avoid g.SetLimit here: it makes g.Go stop accepting work, which
372
+ // prevents workers from enqeuing, and thus finishing, and thus allowing the
373
+ // group to make progress: deadlock.
374
+ limiter := make (chan unit , runtime .GOMAXPROCS (0 ))
375
+ var completed int64
376
+
307
377
var enqueue func (* analysisNode )
308
378
enqueue = func (an * analysisNode ) {
309
379
g .Go (func () error {
380
+ limiter <- unit {}
381
+ defer func () { <- limiter }()
382
+
310
383
summary , err := an .runCached (ctx )
311
384
if err != nil {
312
385
return err // cancelled, or failed to produce a package
313
386
}
387
+ maybeReport (atomic .AddInt64 (& completed , 1 ))
314
388
an .summary = summary
315
389
316
390
// Notify each waiting predecessor,
0 commit comments