gopls/internal/lsp/cache: use json encoding for diagnostics
Benchmarking demonstrated that encoding and decoding diagnostics when
diagnosting the workspace consumes a disproportionate amount of time,
especially when the diagnostic sets are almost empty.
The encoding/gob package is not intended for thousands of small encoding
and decoding operations at low latency, as described here:
https://github.com/golang/go/issues/29766#issuecomment-454926474
Using JSON improved observed encoding performance significantly.
Also, add a test... and fix the bugs uncovered by the test.
For golang/go#57987
Change-Id: I52af8a680ed20cf07e6c61ea496bcf9fd761e6da
Reviewed-on: https://go-review.googlesource.com/c/tools/+/477977
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
Run-TryBot: Robert Findley <rfindley@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go
index 5d8616b..25e4a01 100644
--- a/gopls/internal/lsp/cache/analysis.go
+++ b/gopls/internal/lsp/cache/analysis.go
@@ -1145,12 +1145,28 @@
return buf.Bytes()
}
+// TODO(rfindley): based on profiling, we should consider using JSON encoding
+// throughout, rather than gob encoding.
+func mustJSONEncode(x interface{}) []byte {
+ data, err := json.Marshal(x)
+ if err != nil {
+ log.Fatalf("internal error marshalling %T: %v", data, err)
+ }
+ return data
+}
+
func mustDecode(data []byte, ptr interface{}) {
if err := gob.NewDecoder(bytes.NewReader(data)).Decode(ptr); err != nil {
log.Fatalf("internal error decoding %T: %v", ptr, err)
}
}
+func mustJSONDecode(data []byte, ptr interface{}) {
+ if err := json.Unmarshal(data, ptr); err != nil {
+ log.Fatalf("internal error unmarshalling %T: %v", ptr, err)
+ }
+}
+
// -- data types for serialization of analysis.Diagnostic and source.Diagnostic --
type gobDiagnostic struct {
diff --git a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go
index f14631b..fcb5c5c 100644
--- a/gopls/internal/lsp/cache/errors.go
+++ b/gopls/internal/lsp/cache/errors.go
@@ -249,13 +249,13 @@
}
gobDiags = append(gobDiags, gobDiag)
}
- return mustEncode(gobDiags)
+ return mustJSONEncode(gobDiags)
}
// decodeDiagnostics decodes the given gob-encoded diagnostics.
func decodeDiagnostics(data []byte) []*source.Diagnostic {
var gobDiags []gobDiagnostic
- mustDecode(data, &gobDiags)
+ mustJSONDecode(data, &gobDiags)
var srcDiags []*source.Diagnostic
for _, gobDiag := range gobDiags {
var srcFixes []source.SuggestedFix
@@ -276,7 +276,7 @@
srcFix.Edits[uri] = append(srcFix.Edits[uri], srcEdit)
}
if gobCmd := gobFix.Command; gobCmd != nil {
- gobFix.Command = &gobCommand{
+ srcFix.Command = &protocol.Command{
Title: gobCmd.Title,
Command: gobCmd.Command,
Arguments: gobCmd.Arguments,
@@ -293,6 +293,8 @@
URI: gobDiag.Location.URI.SpanURI(),
Range: gobDiag.Location.Range,
Severity: gobDiag.Severity,
+ Code: gobDiag.Code,
+ CodeHref: gobDiag.CodeHref,
Source: source.AnalyzerErrorKind(gobDiag.Source),
Message: gobDiag.Message,
Tags: gobDiag.Tags,
diff --git a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/errors_test.go
index 43cc03f..933e9e8 100644
--- a/gopls/internal/lsp/cache/errors_test.go
+++ b/gopls/internal/lsp/cache/errors_test.go
@@ -5,8 +5,14 @@
package cache
import (
+ "encoding/json"
"strings"
"testing"
+
+ "github.com/google/go-cmp/cmp"
+ "golang.org/x/tools/gopls/internal/lsp/protocol"
+ "golang.org/x/tools/gopls/internal/lsp/source"
+ "golang.org/x/tools/gopls/internal/span"
)
func TestParseErrorMessage(t *testing.T) {
@@ -50,3 +56,75 @@
})
}
}
+
+func TestDiagnosticEncoding(t *testing.T) {
+ diags := []*source.Diagnostic{
+ {}, // empty
+ {
+ URI: "file///foo",
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 4, Character: 2},
+ End: protocol.Position{Line: 6, Character: 7},
+ },
+ Severity: protocol.SeverityWarning,
+ Code: "red",
+ CodeHref: "https://go.dev",
+ Source: "test",
+ Message: "something bad happened",
+ Tags: []protocol.DiagnosticTag{81},
+ Related: []protocol.DiagnosticRelatedInformation{
+ {
+ Location: protocol.Location{
+ URI: "file:///other",
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 3, Character: 6},
+ End: protocol.Position{Line: 4, Character: 9},
+ },
+ },
+ Message: "psst, over here",
+ },
+ },
+
+ // Fields below are used internally to generate quick fixes. They aren't
+ // part of the LSP spec and don't leave the server.
+ SuggestedFixes: []source.SuggestedFix{
+ {
+ Title: "fix it!",
+ Edits: map[span.URI][]protocol.TextEdit{
+ "file:///foo": {{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 4, Character: 2},
+ End: protocol.Position{Line: 6, Character: 7},
+ },
+ NewText: "abc",
+ }},
+ "file:///other": {{
+ Range: protocol.Range{
+ Start: protocol.Position{Line: 4, Character: 2},
+ End: protocol.Position{Line: 6, Character: 7},
+ },
+ NewText: "!@#!",
+ }},
+ },
+ Command: &protocol.Command{
+ Title: "run a command",
+ Command: "gopls.fix",
+ Arguments: []json.RawMessage{json.RawMessage(`{"a":1}`)},
+ },
+ ActionKind: protocol.QuickFix,
+ },
+ },
+ },
+ {
+ URI: "file//bar",
+ // other fields tested above
+ },
+ }
+
+ data := encodeDiagnostics(diags)
+ diags2 := decodeDiagnostics(data)
+
+ if diff := cmp.Diff(diags, diags2); diff != "" {
+ t.Errorf("decoded diagnostics do not match (-original +decoded):\n%s", diff)
+ }
+}