gopls/internal/lsp: delay workspace diagnostics and analysis

Analysis runs its own type-checking, which we don't want to compete with
normal type-checking of active packages. Delay analysis along with
workspace-wide diagnostics, which seems like a reasonable trade-off.

Also, increase the diagnostics delay to 1s. We produce type-checking
errors for actively edited packages immediately, and the user will
probably not notice an additional delay to other diagnostics.

This change exposed a "bug" in the initial workspace load benchmark,
which opened a file, and therefore had multiple parallel diagnostics
passes running simultaneously. Remove the file opening.

Also: update the marker tests to set a short diagnostics delay, as
otherwise each test incurs this additional delay.

Change-Id: I886ef89f440a9ebae8e4e6d0d076e47c40b5cd04
Reviewed-on: https://go-review.googlesource.com/c/tools/+/476255
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
diff --git a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go
index 1cac729..72f920c 100644
--- a/gopls/internal/lsp/diagnostics.go
+++ b/gopls/internal/lsp/diagnostics.go
@@ -124,7 +124,7 @@
 func (s *Server) diagnoseDetached(snapshot source.Snapshot) {
 	ctx := snapshot.BackgroundContext()
 	ctx = xcontext.Detach(ctx)
-	s.diagnose(ctx, snapshot, false)
+	s.diagnose(ctx, snapshot, analyzeOpenPackages)
 	s.publishDiagnostics(ctx, true, snapshot)
 }
 
@@ -164,14 +164,14 @@
 		// TODO(rfindley): it would be cleaner to simply put the diagnostic
 		// debouncer on the view, and remove the "key" argument to debouncing.
 		if ok := <-s.diagDebouncer.debounce(snapshot.View().Name(), snapshot.SequenceID(), time.After(delay)); ok {
-			s.diagnose(ctx, snapshot, false)
+			s.diagnose(ctx, snapshot, analyzeOpenPackages)
 			s.publishDiagnostics(ctx, true, snapshot)
 		}
 		return
 	}
 
 	// Ignore possible workspace configuration warnings in the normal flow.
-	s.diagnose(ctx, snapshot, false)
+	s.diagnose(ctx, snapshot, analyzeOpenPackages)
 	s.publishDiagnostics(ctx, true, snapshot)
 }
 
@@ -219,12 +219,12 @@
 			}
 		}
 	}
-	s.diagnosePkgs(ctx, snapshot, toDiagnose, false)
+	s.diagnosePkgs(ctx, snapshot, toDiagnose, analyzeNothing)
 }
 
 // diagnose is a helper function for running diagnostics with a given context.
 // Do not call it directly. forceAnalysis is only true for testing purposes.
-func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, forceAnalysis bool) {
+func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze analysisMode) {
 	ctx, done := event.Start(ctx, "Server.diagnose", source.SnapshotLabels(snapshot)...)
 	defer done()
 
@@ -335,7 +335,7 @@
 		}
 		toDiagnose = append(toDiagnose, m)
 	}
-	s.diagnosePkgs(ctx, snapshot, toDiagnose, forceAnalysis)
+	s.diagnosePkgs(ctx, snapshot, toDiagnose, analyze)
 
 	// Orphaned files.
 	// Confirm that every opened file belongs to a package (if any exist in
@@ -352,23 +352,36 @@
 	}
 }
 
-func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, mds []*source.Metadata, alwaysAnalyze bool) {
+// analysisMode parameterizes analysis behavior of a call to diagnosePkgs.
+type analysisMode int
+
+const (
+	analyzeNothing      analysisMode = iota // don't run any analysis
+	analyzeOpenPackages                     // run analysis on packages with open files
+	analyzeEverything                       // run analysis on all packages
+)
+
+func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, mds []*source.Metadata, analysis analysisMode) {
 	ctx, done := event.Start(ctx, "Server.diagnosePkgs", source.SnapshotLabels(snapshot)...)
 	defer done()
 	var needIDs []source.PackageID // exclude e.g. testdata packages
 	needAnalysis := make(map[source.PackageID]*source.Metadata)
+
 	for _, m := range mds {
-		enableDiagnostics := false
-		includeAnalysis := alwaysAnalyze // only run analyses for packages with open files
+		var hasNonIgnored, hasOpenFile bool
 		for _, uri := range m.CompiledGoFiles {
-			enableDiagnostics = enableDiagnostics || !snapshot.IgnoredFile(uri)
-			includeAnalysis = includeAnalysis || snapshot.IsOpen(uri)
+			if !snapshot.IgnoredFile(uri) {
+				hasNonIgnored = true
+			}
+			if snapshot.IsOpen(uri) {
+				hasOpenFile = true
+			}
 		}
-		if enableDiagnostics {
+		if hasNonIgnored {
 			needIDs = append(needIDs, m.ID)
-		}
-		if includeAnalysis && enableDiagnostics {
-			needAnalysis[m.ID] = m
+			if analysis == analyzeEverything || analysis == analyzeOpenPackages && hasOpenFile {
+				needAnalysis[m.ID] = m
+			}
 		}
 	}
 
diff --git a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go
index 4b19d29..dac7407 100644
--- a/gopls/internal/lsp/lsp_test.go
+++ b/gopls/internal/lsp/lsp_test.go
@@ -1265,7 +1265,7 @@
 	defer release()
 
 	// Always run diagnostics with analysis.
-	r.server.diagnose(r.ctx, snapshot, true)
+	r.server.diagnose(r.ctx, snapshot, analyzeEverything)
 	for uri, reports := range r.server.diagnostics {
 		for _, report := range reports.reports {
 			for _, d := range report.diags {
diff --git a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go
index c0ae449..fb7454d 100644
--- a/gopls/internal/lsp/regtest/marker.go
+++ b/gopls/internal/lsp/regtest/marker.go
@@ -293,6 +293,12 @@
 				Settings: test.settings,
 				Env:      test.env,
 			}
+			if _, ok := config.Settings["diagnosticsDelay"]; !ok {
+				if config.Settings == nil {
+					config.Settings = make(map[string]interface{})
+				}
+				config.Settings["diagnosticsDelay"] = "10ms"
+			}
 			run := &markerTestRun{
 				test: test,
 				env:  newEnv(t, cache, test.files, config),
diff --git a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go
old mode 100755
new mode 100644
index e655eb0..74d8d71
--- a/gopls/internal/lsp/source/api_json.go
+++ b/gopls/internal/lsp/source/api_json.go
@@ -509,7 +509,7 @@
 				Name:      "diagnosticsDelay",
 				Type:      "time.Duration",
 				Doc:       "diagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n",
-				Default:   "\"250ms\"",
+				Default:   "\"1s\"",
 				Status:    "advanced",
 				Hierarchy: "ui.diagnostic",
 			},
diff --git a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go
index a4ae51a..e4cfb6d5 100644
--- a/gopls/internal/lsp/source/options.go
+++ b/gopls/internal/lsp/source/options.go
@@ -124,7 +124,7 @@
 				},
 				UIOptions: UIOptions{
 					DiagnosticOptions: DiagnosticOptions{
-						DiagnosticsDelay: 250 * time.Millisecond,
+						DiagnosticsDelay: 1 * time.Second,
 						Annotations: map[Annotation]bool{
 							Bounds: true,
 							Escape: true,
diff --git a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go
index cdfb363..cf3fd6b 100644
--- a/gopls/internal/regtest/bench/iwl_test.go
+++ b/gopls/internal/regtest/bench/iwl_test.go
@@ -53,11 +53,8 @@
 	defer env.Close()
 	b.StartTimer()
 
-	// Open an arbitrary file to ensure that gopls starts working.
-	//
-	// In the future, this may matter if gopls doesn't eagerly construct
-	// the workspace.
-	env.OpenFile(file)
+	// Note: in the future, we may need to open a file in order to cause gopls to
+	// start loading. the workspace.
 
 	env.Await(InitialWorkspaceLoad)
 	// TODO(rfindley): remove this guard once the released gopls version supports