gopls/integration: add the replay command to replay LSP logs
Documentation is in replay/README.md
Change-Id: Ic5a2b59269d640747a9fea7bb99fb74b2d069111
Reviewed-on: https://go-review.googlesource.com/c/tools/+/209577
Run-TryBot: Peter Weinberger <pjw@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/gopls/integration/parse/parse.go b/gopls/integration/parse/parse.go
new file mode 100644
index 0000000..72e6b48
--- /dev/null
+++ b/gopls/integration/parse/parse.go
@@ -0,0 +1,231 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package parse provides functions to parse LSP logs.
+// Fully processed logs are returned by ToRLog().
+package parse
+
+import (
+ "bufio"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "regexp"
+ "strings"
+)
+
+// Direction is the type of message,
+type Direction int
+
+const (
+ // Clrequest from client to server has method and id
+ Clrequest Direction = iota
+ // Clresponse from server to client
+ Clresponse
+ // Svrequest from server to client, has method and id
+ Svrequest
+ // Svresponse from client to server
+ Svresponse
+ // Toserver notification has method, but no id
+ Toserver
+ // Toclient notification
+ Toclient
+ // Reporterr is an error message
+ Reporterr // errors have method and id
+)
+
+// Logmsg is the type of a parsed log entry
+type Logmsg struct {
+ Dir Direction
+ Method string
+ ID string // for requests/responses. Client and server request ids overlap
+ Elapsed string // for responses
+ Hdr string // header. do we need to keep all these strings?
+ Rest string // the unparsed result, with newlines or not
+ Body interface{} // the parsed result
+}
+
+// ReadLogs from a file. Most users should use TRlog().
+func ReadLogs(fname string) ([]*Logmsg, error) {
+ byid := make(map[string]int)
+ msgs := []*Logmsg{}
+ fd, err := os.Open(fname)
+ if err != nil {
+ return nil, err
+ }
+ logrdr := bufio.NewScanner(fd)
+ logrdr.Buffer(nil, 1<<25) // a large buffer, for safety
+ logrdr.Split(logRec)
+ for i := 0; logrdr.Scan(); i++ {
+ flds := strings.SplitN(logrdr.Text(), "\n", 2)
+ if len(flds) == 1 {
+ flds = append(flds, "") // for Errors
+ }
+ msg := parselog(flds[0], flds[1])
+ if msg == nil {
+ log.Fatalf("failed to parse %q", logrdr.Text())
+ continue
+ }
+ switch msg.Dir {
+ case Clrequest, Svrequest:
+ v, err := msg.unmarshal(Requests(msg.Method))
+ if err != nil {
+ log.Fatalf("%v for %s, %T", err, msg.Method, Requests(msg.Method))
+ }
+ msg.Body = v
+ case Clresponse, Svresponse:
+ v, err := msg.doresponse()
+ if err != nil {
+ log.Fatalf("%v %s", err, msg.Method)
+ }
+ msg.Body = v
+ case Toserver, Toclient:
+ v, err := msg.unmarshal(Notifs(msg.Method))
+ if err != nil && Notifs(msg.Method) != nil {
+ log.Fatalf("%s/%T: %v", msg.Method, Notifs(msg.Method), err)
+ }
+ msg.Body = v
+ case Reporterr:
+ msg.Body = msg.ID // cause?
+ }
+ byid[msg.ID]++
+ msgs = append(msgs, msg)
+ }
+ if err = logrdr.Err(); err != nil {
+ return msgs, err
+ }
+ return msgs, nil
+}
+
+// parse a single log message, given first line, and the rest
+func parselog(first, rest string) *Logmsg {
+ if strings.HasPrefix(rest, "Params: ") {
+ rest = rest[8:]
+ } else if strings.HasPrefix(rest, "Result: ") {
+ rest = rest[8:]
+ }
+ ans := &Logmsg{Hdr: first, Rest: rest}
+ fixid := func(s string) string {
+ // emacs does (n)., gopls does (n)'.
+ s = strings.Trim(s, "()'.{)")
+ return s
+ }
+ flds := strings.Fields(first)
+ chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
+ // gopls and emacs differ in how they report elapsed time
+ switch {
+ case chk("Sending request", 9):
+ ans.Dir = Clrequest
+ ans.Method = flds[6][1:]
+ ans.ID = fixid(flds[8][:len(flds[8])-2])
+ case chk("Received response", 11):
+ ans.Dir = Clresponse
+ ans.Method = flds[6][1:]
+ ans.ID = fixid(flds[8])
+ ans.Elapsed = flds[10]
+ case chk("Received request", 9):
+ ans.Dir = Svrequest
+ ans.Method = flds[6][1:]
+ ans.ID = fixid(flds[8])
+ case chk("Sending response", 11), // gopls
+ chk("Sending response", 13): // emacs
+ ans.Dir = Svresponse
+ ans.Method = flds[6][1:]
+ ans.ID = fixid(flds[8][:len(flds[8])-1])
+ ans.Elapsed = flds[10]
+ case chk("Sending notification", 7):
+ ans.Dir = Toserver
+ ans.Method = strings.Trim(flds[6], ".'")
+ if len(flds) == 9 {
+ log.Printf("len=%d method=%s %q", len(flds), ans.Method, first)
+ }
+ case chk("Received notification", 7):
+ ans.Dir = Toclient
+ ans.Method = flds[6][1 : len(flds[6])-2]
+ case strings.HasPrefix(first, "[Error - "):
+ //log.Printf("%s %s %v", first, rest, flds)
+ ans.Dir = Reporterr
+ both := flds[5]
+ idx := strings.Index(both, "#") // relies on ID.Number
+ ans.Method = both[:idx]
+ ans.ID = fixid(both[idx+1:])
+ ans.Rest = strings.Join(flds[6:], " ")
+ default:
+ log.Fatalf("surprise, first=%q with %d flds", first, len(flds))
+ return nil
+ }
+ return ans
+}
+
+// unmarshal into a proposed type
+func (l *Logmsg) unmarshal(p interface{}) (interface{}, error) {
+ r := []byte(l.Rest)
+ if err := json.Unmarshal(r, p); err != nil {
+ // need general alternatives, but for now
+ // if p is *[]foo and rest is {}, return an empty p (or *p?)
+ // or, cheat:
+ if l.Rest == "{}" {
+ return nil, nil
+ }
+ return nil, err
+ }
+ return p, nil
+}
+
+func (l *Logmsg) doresponse() (interface{}, error) {
+ for _, x := range Responses(l.Method) {
+ v, err := l.unmarshal(x)
+ if err == nil {
+ return v, nil
+ }
+ if x == nil {
+ return new(interface{}), nil
+ }
+ }
+ // failure!
+ rr := Responses(l.Method)
+ for _, x := range rr {
+ log.Printf("tried %T", x)
+ }
+ log.Fatalf("(%d) doresponse failed for %s %q", len(rr), l.Method, l.Rest)
+ return nil, nil
+}
+
+// be a little forgiving in separating log records
+var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
+
+// return offset of start of next record, contents of record, error
+func logRec(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
+ got := recSep.FindIndex(b)
+ if got == nil {
+ if !atEOF {
+ return 0, nil, nil // need more
+ }
+ return 0, nil, io.EOF
+ }
+ return got[1], b[:got[0]], nil
+}
+
+// String returns a user-useful versin of a Direction
+func (d Direction) String() string {
+ switch d {
+ case Clrequest:
+ return "clrequest"
+ case Clresponse:
+ return "clresponse"
+ case Svrequest:
+ return "svrequest"
+ case Svresponse:
+ return "svresponse"
+ case Toserver:
+ return "toserver"
+ case Toclient:
+ return "toclient"
+ case Reporterr:
+ return "reporterr"
+ }
+ return fmt.Sprintf("dirname: %d unknown", d)
+}
diff --git a/gopls/integration/parse/protocol.go b/gopls/integration/parse/protocol.go
new file mode 100644
index 0000000..d812a54
--- /dev/null
+++ b/gopls/integration/parse/protocol.go
@@ -0,0 +1,310 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package parse
+
+import (
+ "log"
+
+ p "golang.org/x/tools/internal/lsp/protocol"
+)
+
+// Requests and notifications are fixed types
+// Responses may be one of several types
+
+// Requests returns a pointer to a type suitable for Unmarshal
+func Requests(m string) interface{} {
+ // these are in the documentation's order
+ switch m {
+ case "initialize":
+ return new(p.InitializeParams)
+ case "shutdown":
+ return new(struct{})
+ case "window/showMessgeRequest":
+ return new(p.ShowMessageRequestParams)
+ case "client/registerCapability":
+ return new(p.RegistrationParams)
+ case "client/unregisterCapability":
+ return new(p.UnregistrationParams)
+ case "workspace/workspaceFolders":
+ return nil
+ case "workspace/configuration":
+ return new(p.ConfigurationParams)
+ case "workspace/symbol":
+ return new(p.WorkspaceSymbolParams)
+ case "workspace/executeCommand":
+ return new(p.ExecuteCommandParams)
+ case "workspace/applyEdit":
+ return new(p.ApplyWorkspaceEditParams)
+ case "textDocument/willSaveWaitUntil":
+ return new(p.WillSaveTextDocumentParams)
+ case "textDocument/completion":
+ return new(p.CompletionParams)
+ case "completionItem/resolve":
+ return new(p.CompletionItem)
+ case "textDocument/hover":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/signatureHelp":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/declaration":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/definition":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/typeDefinition":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/implementation":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/references":
+ return new(p.ReferenceParams)
+ case "textDocument/documentHighlight":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/documentSymbol":
+ return new(p.DocumentSymbolParams)
+ case "textDocument/codeAction":
+ return new(p.CodeActionParams)
+ case "textDocument/codeLens":
+ return new(p.CodeLensParams)
+ case "codeLens/resolve":
+ return new(p.CodeLens)
+ case "textDocument/documentLink":
+ return new(p.DocumentLinkParams)
+ case "documentLink/resolve":
+ return new(p.DocumentLink)
+ case "textDocument/documentColor":
+ return new(p.DocumentColorParams)
+ case "textDocument/colorPressentation":
+ return new(p.ColorPresentationParams)
+ case "textDocument/formatting":
+ return new(p.DocumentFormattingParams)
+ case "textDocument/rangeFormatting":
+ return new(p.DocumentRangeFormattingParams)
+ case "textDocument/typeFormatting":
+ return new(p.DocumentOnTypeFormattingParams)
+ case "textDocument/rename":
+ return new(p.RenameParams)
+ case "textDocument/prepareRename":
+ return new(p.TextDocumentPositionParams)
+ case "textDocument/foldingRange":
+ return new(p.FoldingRangeParams)
+ }
+ log.Fatalf("request(%s) undefined", m)
+ return ""
+}
+
+// Notifs returns a pointer to a type suitable for Unmarshal
+func Notifs(m string) interface{} {
+ switch m {
+ case "$/cancelRequest":
+ return new(p.CancelParams)
+ case "$/setTraceNotification":
+ return new(struct{ Value string })
+ case "client/registerCapability": // why is this a notification? (serer->client rpc)
+ return new(p.RegistrationParams)
+ case "initialized":
+ return new(p.InitializedParams)
+ case "exit":
+ return nil
+ case "window/showMessage":
+ return new(p.ShowMessageParams)
+ case "window/logMessage":
+ return new(p.LogMessageParams)
+ case "telemetry/event":
+ return new(interface{}) // any
+ case "workspace/didChangeWorkspaceFolders":
+ return new(p.DidChangeWorkspaceFoldersParams)
+ case "workspace/didChangeConfiguration":
+ return new(p.DidChangeConfigurationParams)
+ case "workspace/didChangeWatchedFiles":
+ return new(p.DidChangeWatchedFilesParams)
+ case "textDocument/didOpen":
+ return new(p.DidOpenTextDocumentParams)
+ case "textDocument/didChange":
+ return new(p.DidChangeTextDocumentParams)
+ case "textDocument/willSave":
+ return new(p.WillSaveTextDocumentParams)
+ case "textDocument/didSave":
+ return new(p.DidSaveTextDocumentParams)
+ case "textDocument/didClose":
+ return new(p.DidCloseTextDocumentParams)
+ case "textDocument/willClose":
+ return new(p.DidCloseTextDocumentParams)
+ case "textDocument/publishDiagnostics":
+ return new(p.PublishDiagnosticsParams)
+ }
+ log.Fatalf("notif(%s) undefined", m)
+ return ""
+}
+
+// Responses returns a slice of types, one of which should be
+// suitable for Unmarshal
+func Responses(m string) []interface{} {
+ switch m {
+ case "initialize":
+ return []interface{}{new(p.InitializeResult)}
+ case "shutdown":
+ return []interface{}{nil}
+ case "window/showMessageRequest":
+ return []interface{}{new(p.MessageActionItem), nil}
+ case "client/registerCapability":
+ return []interface{}{nil}
+ case "client/unregisterCapability":
+ return []interface{}{nil}
+ case "workspace/workspaceFolder":
+ return []interface{}{new([]p.WorkspaceFolder), nil}
+ case "workspace/configuration":
+ return []interface{}{new([]interface{}), new(interface{})}
+ case "workspace/symbol":
+ return []interface{}{new([]p.SymbolInformation), nil}
+ case "workspace/executeCommand":
+ return []interface{}{new(interface{}), nil}
+ case "workspace/applyEdit":
+ return []interface{}{new(p.ApplyWorkspaceEditResponse)}
+ case "textDocument/willSaveWaitUntil":
+ return []interface{}{new([]p.TextEdit), nil}
+ case "textDocument/completion":
+ return []interface{}{new(p.CompletionList), new([]p.CompletionItem), nil}
+ case "completionItem/resolve":
+ return []interface{}{new(p.CompletionItem)}
+ case "textDocument/hover":
+ return []interface{}{new(p.Hover), nil}
+ case "textDocument/signatureHelp":
+ return []interface{}{new(p.SignatureHelp), nil}
+ case "textDocument/declaration":
+ return []interface{}{new(p.Location), new([]p.Location), new([]p.LocationLink), nil}
+ case "textDocument/definition":
+ return []interface{}{new([]p.Location), new([]p.Location), new([]p.LocationLink), nil}
+ case "textDocument/typeDefinition":
+ return []interface{}{new([]p.Location), new([]p.LocationLink), new(p.Location), nil}
+ case "textDocument/implementation":
+ return []interface{}{new(p.Location), new([]p.Location), new([]p.LocationLink), nil}
+ case "textDocument/references":
+ return []interface{}{new([]p.Location), nil}
+ case "textDocument/documentHighlight":
+ return []interface{}{new([]p.DocumentHighlight), nil}
+ case "textDocument/documentSymbol":
+ return []interface{}{new([]p.DocumentSymbol), new([]p.SymbolInformation), nil}
+ case "textDocument/codeAction":
+ return []interface{}{new([]p.CodeAction), new(p.Command), nil}
+ case "textDocument/codeLens":
+ return []interface{}{new([]p.CodeLens), nil}
+ case "codelens/resolve":
+ return []interface{}{new(p.CodeLens)}
+ case "textDocument/documentLink":
+ return []interface{}{new([]p.DocumentLink), nil}
+ case "documentLink/resolve":
+ return []interface{}{new(p.DocumentLink)}
+ case "textDocument/documentColor":
+ return []interface{}{new([]p.ColorInformation)}
+ case "textDocument/colorPresentation":
+ return []interface{}{new([]p.ColorPresentation)}
+ case "textDocument/formatting":
+ return []interface{}{new([]p.TextEdit), nil}
+ case "textDocument/rangeFormatting":
+ return []interface{}{new([]p.TextEdit), nil}
+ case "textDocument/onTypeFormatting":
+ return []interface{}{new([]p.TextEdit), nil}
+ case "textDocument/rename":
+ return []interface{}{new(p.WorkspaceEdit), nil}
+ case "textDocument/prepareRename":
+ return []interface{}{new(p.Range), nil}
+ case "textDocument/foldingRange":
+ return []interface{}{new([]p.FoldingRange), nil}
+ }
+ log.Fatalf("responses(%q) undefined", m)
+ return nil
+}
+
+// Msgtype given method names. Note that mSrv|mCl is possible
+type Msgtype int
+
+const (
+ // Mnot for notifications
+ Mnot Msgtype = 1
+ // Mreq for requests
+ Mreq Msgtype = 2
+ // Msrv for messages from the server
+ Msrv Msgtype = 4
+ // Mcl for messages from the client
+ Mcl Msgtype = 8
+)
+
+// IsNotify says if the message is a notification
+func IsNotify(msg string) bool {
+ m, ok := fromMethod[msg]
+ if !ok {
+ log.Fatalf("%q", msg)
+ }
+ return m&Mnot != 0
+}
+
+// FromServer says if the message is from the server
+func FromServer(msg string) bool {
+ m, ok := fromMethod[msg]
+ if !ok {
+ log.Fatalf("%q", msg)
+ }
+ return m&Msrv != 0
+}
+
+// FromClient says if the message is from the client
+func FromClient(msg string) bool {
+ m, ok := fromMethod[msg]
+ if !ok {
+ log.Fatalf("%q", msg)
+ }
+ return m&Mcl != 0
+}
+
+// rpc name to message type
+var fromMethod = map[string]Msgtype{
+ "$/cancelRequest": Mnot | Msrv | Mcl,
+ "initialize": Mreq | Msrv,
+ "initialized": Mnot | Mcl,
+ "shutdown": Mreq | Mcl,
+ "exit": Mnot | Mcl,
+ "window/showMessage": Mreq | Msrv,
+ "window/logMessage": Mnot | Msrv,
+ "telemetry'event": Mnot | Msrv,
+ "client/registerCapability": Mreq | Msrv,
+ "client/unregisterCapability": Mreq | Msrv,
+ "workspace/workspaceFolders": Mreq | Msrv,
+ "workspace/workspaceDidChangeWorkspaceFolders": Mnot | Mcl,
+ "workspace/didChangeConfiguration": Mnot | Mcl,
+ "workspace/configuration": Mreq | Msrv,
+ "workspace/didChangeWatchedFiles": Mnot | Mcl,
+ "workspace/symbol": Mreq | Mcl,
+ "workspace/executeCommand": Mreq | Mcl,
+ "workspace/applyEdit": Mreq | Msrv,
+ "textDocument/didOpen": Mnot | Mcl,
+ "textDocument/didChange": Mnot | Mcl,
+ "textDocument/willSave": Mnot | Mcl,
+ "textDocument/willSaveWaitUntil": Mreq | Mcl,
+ "textDocument/didSave": Mnot | Mcl,
+ "textDocument/didClose": Mnot | Mcl,
+ "textDocument/publishDiagnostics": Mnot | Msrv,
+ "textDocument/completion": Mreq | Mcl,
+ "completionItem/resolve": Mreq | Mcl,
+ "textDocument/hover": Mreq | Mcl,
+ "textDocument/signatureHelp": Mreq | Mcl,
+ "textDocument/declaration": Mreq | Mcl,
+ "textDocument/definition": Mreq | Mcl,
+ "textDocument/typeDefinition": Mreq | Mcl,
+ "textDocument/implementation": Mreq | Mcl,
+ "textDocument/references": Mreq | Mcl,
+ "textDocument/documentHighlight": Mreq | Mcl,
+ "textDocument/documentSymbol": Mreq | Mcl,
+ "textDocument/codeAction": Mreq | Mcl,
+ "textDocument/codeLens": Mreq | Mcl,
+ "codeLens/resolve": Mreq | Mcl,
+ "textDocument/documentLink": Mreq | Mcl,
+ "documentLink/resolve": Mreq | Mcl,
+ "textDocument/documentColor": Mreq | Mcl,
+ "textDocument/colorPresentation": Mreq | Mcl,
+ "textDocument/formatting": Mreq | Mcl,
+ "textDocument/rangeFormatting": Mreq | Mcl,
+ "textDocument/onTypeFormatting": Mreq | Mcl,
+ "textDocument/rename": Mreq | Mcl,
+ "textDocument/prepareRename": Mreq | Mcl,
+ "textDocument/foldingRange": Mreq | Mcl,
+}
diff --git a/gopls/integration/parse/rlog.go b/gopls/integration/parse/rlog.go
new file mode 100644
index 0000000..00cd16c
--- /dev/null
+++ b/gopls/integration/parse/rlog.go
@@ -0,0 +1,125 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package parse
+
+import (
+ "fmt"
+ "log"
+ "strings"
+)
+
+// Rlog contains the processed logs
+type Rlog struct {
+ Logs []*Logmsg // In the order in the log file
+ ServerCall map[string]*Logmsg // ID->Request, client->server
+ ServerReply map[string]*Logmsg // ID->Response, server->client (includes Errors)
+ ClientCall map[string]*Logmsg
+ ClientReply map[string]*Logmsg
+ ClientNotifs []*Logmsg
+ ServerNotifs []*Logmsg
+ Histogram *LogHist
+}
+
+func newRlog(x []*Logmsg) *Rlog {
+ return &Rlog{Logs: x,
+ ServerCall: make(map[string]*Logmsg),
+ ServerReply: make(map[string]*Logmsg),
+ ClientCall: make(map[string]*Logmsg),
+ ClientReply: make(map[string]*Logmsg),
+ ClientNotifs: []*Logmsg{},
+ ServerNotifs: []*Logmsg{},
+ Histogram: &LogHist{},
+ }
+}
+
+// Counts returns a one-line summary of an Rlog
+func (r *Rlog) Counts() string {
+ return fmt.Sprintf("logs:%d srvC:%d srvR:%d clC:%d clR:%d clN:%d srvN:%d",
+ len(r.Logs),
+ len(r.ServerCall), len(r.ServerReply), len(r.ClientCall), len(r.ClientReply),
+ len(r.ClientNotifs), len(r.ServerNotifs))
+}
+
+// ToRlog reads a log file and returns a *Rlog
+func ToRlog(fname string) (*Rlog, error) {
+ x, err := ReadLogs(fname)
+ if err != nil {
+ return nil, err
+ }
+ ans := newRlog(x)
+ for _, l := range x {
+ switch l.Dir {
+ case Clrequest:
+ ans.ServerCall[l.ID] = l
+ case Clresponse:
+ ans.ServerReply[l.ID] = l
+ if l.Dir != Reporterr {
+ n := 0
+ fmt.Sscanf(l.Elapsed, "%d", &n)
+ ans.Histogram.add(n)
+ }
+ case Svrequest:
+ ans.ClientCall[l.ID] = l
+ case Svresponse:
+ ans.ClientReply[l.ID] = l
+ case Toclient:
+ ans.ClientNotifs = append(ans.ClientNotifs, l)
+ case Toserver:
+ ans.ServerNotifs = append(ans.ServerNotifs, l)
+ case Reporterr:
+ ans.ServerReply[l.ID] = l
+ default:
+ log.Fatalf("eh? %s/%s (%s)", l.Dir, l.Method, l.ID)
+ }
+ }
+ return ans, nil
+}
+
+// LogHist gets ints, and puts them into buckets:
+// <=10, <=30, 100, 300, 1000, ...
+// It produces a historgram of elapsed times in milliseconds
+type LogHist struct {
+ cnts []int
+}
+
+func (l *LogHist) add(n int) {
+ if n < 0 {
+ n = 0
+ }
+ bucket := 0
+ for ; n > 0; n /= 10 {
+ if n < 10 {
+ break
+ }
+ if n < 30 {
+ bucket++
+ break
+ }
+ bucket += 2
+ }
+ if len(l.cnts) <= bucket {
+ for j := len(l.cnts); j < bucket+10; j++ {
+ l.cnts = append(l.cnts, 0)
+ }
+ }
+ l.cnts[bucket]++
+}
+
+// String returns a string describing a histogram
+func (l *LogHist) String() string {
+ top := len(l.cnts) - 1
+ for ; top > 0 && l.cnts[top] == 0; top-- {
+ }
+ labs := []string{"10", "30"}
+ out := strings.Builder{}
+ out.WriteByte('[')
+ for i := 0; i <= top; i++ {
+ label := labs[i%2]
+ labs[i%2] += "0"
+ fmt.Fprintf(&out, "%s:%d ", label, l.cnts[i])
+ }
+ out.WriteByte(']')
+ return out.String()
+}
diff --git a/gopls/integration/replay/README.md b/gopls/integration/replay/README.md
new file mode 100644
index 0000000..3b29538
--- /dev/null
+++ b/gopls/integration/replay/README.md
@@ -0,0 +1,79 @@
+# Replaying Logs
+
+The LSP log replayer takes a log from a gopls session, starts up an instance of goppls,
+and tries to replay the session. It produces a log from the replayed session and reports
+some comparative statistics of the two logs.
+
+```replay -log <logfile>```
+
+The `logfile` should be the log produced by gopls. It will have a name like
+`/tmp/gopls-89775` or, on a Mac, `$TMPDIR/gopls-29388`.
+
+If `replay` cannot find a copy of gopls to execute, use `-cmd <path to gopls>`.
+It looks in the same places where `go install` would put its output,
+namely `$GOBIN/gopls`, `$GOPATH/bin/gopls`, `$HOME/go/bin/gopls`.
+
+The log for the replayed session is saved in `/tmp/seen`.
+
+There is aloso a boolean argument `-cmp` which compares the log file
+with `/tmp/seen` without invoking gopls and rerunning the session.
+
+The output is fairly cryptic, and generated by logging. Ideas for better output would be welcome.
+Here's an example, with intermingled comments:
+
+```
+main.go:50: old 1856, hist:[10:177 30:1 100:0 300:3 1000:4 ]
+```
+This says that the original log had 1856 records in it. The histogram is
+counting how long RPCs took, in milliseconds. In this case 177 took no more
+than 10ms, and 4 took between 300ms and 1000ms.
+```
+main.go:53: calling mimic
+main.go:293: mimic 1856
+```
+This is a reminder that it's replaying in a new session, with a log file
+containing 1856 records
+```
+main.go:61: new 1846, hist:[10:181 30:1 100:1 300:1 1000:1 ]
+```
+The new session produced 1846 log records (that's 10 fewer),
+and a vaguely similar histogram.
+```
+main.go:96: old: clrequest:578 clresponse:185 svrequest:2 svresponse:2 toserver:244 toclient:460 reporterr:385
+main.go:96: new: clrequest:571 clresponse:185 svrequest:2 svresponse:2 toserver:241 toclient:460 reporterr:385
+```
+The first line is for the original log, the second for the new log. The new log has 7 fewer RPC requests
+from the client *clrequest* (578 vs 571), the same number of client responses *clresponse*, 3 fewer
+notifications *toserver* from the client, the same number from the server *toclient* to the client, and
+the same number of errors *reporterr*. (That's mysterious, but a look at the ends of the log files shows
+that the original session ended with several RPCs that don't show up, for whatever reason, in the new session.)
+
+Finally, there are counts of the various nofications seen, in the new log and the old log, and
+which direction they went. (The 3 fewer notifications in the summary above can be seen here to be from cancels
+and a didChange.)
+```
+main.go:107: counts of notifications
+main.go:110: '$/cancelRequest'. new toserver 1
+main.go:110: '$/cancelRequest'. old toserver 3
+main.go:110: 'initialized'. new toserver 1
+main.go:110: 'initialized'. old toserver 1
+main.go:110: 'textDocument/didChange'. new toserver 231
+main.go:110: 'textDocument/didChange'. old toserver 232
+main.go:110: 'textDocument/didOpen'. new toserver 1
+main.go:110: 'textDocument/didOpen'. old toserver 1
+main.go:110: 'textDocument/didSave'. new toserver 7
+main.go:110: 'textDocument/didSave'. old toserver 7
+main.go:110: 'textDocument/publishDiagnostics'. new toclient 182
+main.go:110: 'textDocument/publishDiagnostics'. old toclient 182
+main.go:110: 'window/logMessage'. new toclient 278
+main.go:110: 'window/logMessage'. old toclient 278
+```
+### Caveats
+Replay cannot restore the exact environment gopls saw for the original session.
+For instance, the first didOpen message in the new session will see the file
+as it was left by the original session.
+
+Gopls invokes various tools, and the environment they see could have changed too.
+
+Replay will use the gopls it finds (or is given). It has no way of using
+the same version that created the original session.
\ No newline at end of file
diff --git a/gopls/integration/replay/main.go b/gopls/integration/replay/main.go
new file mode 100644
index 0000000..92993cf
--- /dev/null
+++ b/gopls/integration/replay/main.go
@@ -0,0 +1,575 @@
+// Copyright 2019 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Replay logs. See README.md
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "os/exec"
+ "regexp"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+
+ "golang.org/x/tools/gopls/integration/parse"
+ "golang.org/x/tools/internal/jsonrpc2"
+ p "golang.org/x/tools/internal/lsp/protocol"
+)
+
+var (
+ ctx = context.Background()
+ command = flag.String("cmd", "", "location of server to send to, looks for gopls")
+ logf = flag.String("log", "", "log file to replay")
+ cmp = flag.Bool("cmp", false, "only compare log and /tmp/seen")
+ logrdr *bufio.Scanner
+ msgs []*parse.Logmsg
+ // requests and responses/errors, by id
+ clreq = make(map[string]*logmsg)
+ clresp = make(map[string]*logmsg)
+ svreq = make(map[string]*logmsg)
+ svresp = make(map[string]*logmsg)
+)
+
+func main() {
+ log.SetFlags(log.Lshortfile)
+ flag.Parse()
+ if *logf == "" {
+ log.Fatal("need -log")
+ }
+
+ orig, err := parse.ToRlog(*logf)
+ if err != nil {
+ log.Fatalf("logfile %q %v", *logf, err)
+ }
+ msgs = orig.Logs
+ log.Printf("old %d, hist:%s", len(msgs), orig.Histogram)
+
+ if !*cmp {
+ log.Print("calling mimic")
+ mimic()
+ }
+ seen, err := parse.ToRlog("/tmp/seen")
+ if err != nil {
+ log.Fatal(err)
+ }
+ vvv := seen.Logs
+ log.Printf("new %d, hist:%s", len(vvv), seen.Histogram)
+
+ ok := make(map[string]int)
+ f := func(x []*parse.Logmsg, label string, diags map[string][]p.Diagnostic) {
+ cnts := make(map[parse.Direction]int)
+ for _, l := range x {
+ if l.Method == "window/logMessage" {
+ // don't care
+ //continue
+ }
+ if l.Method == "textDocument/publishDiagnostics" {
+ v, ok := l.Body.(*p.PublishDiagnosticsParams)
+ if !ok {
+ log.Fatalf("got %T expected PublishDiagnosticsParams", l.Body)
+ }
+ diags[v.URI] = v.Diagnostics
+ }
+ cnts[l.Dir]++
+ // notifications only
+ if l.Dir != parse.Toserver && l.Dir != parse.Toclient {
+ continue
+ }
+ s := fmt.Sprintf("%s %s %s", strings.Replace(l.Hdr, "\r", "", -1), label, l.Dir)
+ if i := strings.Index(s, "notification"); i != -1 {
+ s = s[i+12:]
+ }
+ if len(s) > 120 {
+ s = s[:120]
+ }
+ ok[s]++
+ }
+ msg := ""
+ for i := parse.Clrequest; i <= parse.Reporterr; i++ {
+ msg += fmt.Sprintf("%s:%d ", i, cnts[i])
+ }
+ log.Printf("%s: %s", label, msg)
+ }
+ mdiags := make(map[string][]p.Diagnostic)
+ f(msgs, "old", mdiags)
+ vdiags := make(map[string][]p.Diagnostic)
+ f(vvv, "new", vdiags)
+ buf := []string{}
+ for k := range ok {
+ buf = append(buf, fmt.Sprintf("%s %d", k, ok[k]))
+ }
+ if len(buf) > 0 {
+ log.Printf("counts of notifications")
+ sort.Strings(buf)
+ for _, k := range buf {
+ log.Print(k)
+ }
+ }
+ buf = buf[0:0]
+ for k, v := range mdiags {
+ va := vdiags[k]
+ if len(v) != len(va) {
+ buf = append(buf, fmt.Sprintf("new has %d, old has %d for %s",
+ len(va), len(v), k))
+ }
+ }
+ for ka := range vdiags {
+ if _, ok := mdiags[ka]; !ok {
+ buf = append(buf, fmt.Sprintf("new diagnostics, but no old ones, for %s",
+ ka))
+ }
+ }
+ if len(buf) > 0 {
+ log.Print("diagnostics differ:")
+ for _, s := range buf {
+ log.Print(s)
+ }
+ }
+}
+
+type direction int // what sort of message it is
+const (
+ // rpc from client to server have method and id
+ clrequest direction = iota
+ clresponse
+ // rpc from server have method and id
+ svrequest
+ svresponse
+ // notifications have method, but no id
+ toserver
+ toclient
+ reporterr // errors have method and id
+)
+
+// clrequest has method and id. toserver has method but no id, and svresponse has result (and id)
+type logmsg struct {
+ dir direction
+ method string
+ id string // for requests/responses. Client and server request ids overlap
+ elapsed string // for responses
+ hdr string // do we need to keep all these strings?
+ rest string // the unparsed result, with newlines or not
+ body interface{} // the parsed(?) result
+}
+
+// combined has all the fields of both Request and Response.
+// Unmarshal this and then work out which it is.
+type combined struct {
+ VersionTag jsonrpc2.VersionTag `json:"jsonrpc"`
+ ID *jsonrpc2.ID `json:"id,omitempty"`
+ // RPC name
+ Method string `json:"method"`
+ Params *json.RawMessage `json:"params,omitempty"`
+ Result *json.RawMessage `json:"result,omitempty"`
+ Error *jsonrpc2.Error `json:"error,omitempty"`
+}
+
+func (c *combined) dir() direction {
+ // Method, Params, ID => request
+ // Method, Params, no-ID => notification
+ // Error => error response
+ // Result, ID => response
+ if c.Error != nil {
+ return reporterr
+ }
+ if c.Params != nil && c.ID != nil {
+ // $/cancel could be either, cope someday
+ if parse.FromServer(c.Method) {
+ return svrequest
+ }
+ return clrequest
+ }
+ if c.Params != nil {
+ // we're receiving it, so it must be toclient
+ return toclient
+ }
+ if c.Result == nil {
+ if c.ID != nil {
+ return clresponse
+ }
+ log.Printf("%+v", *c)
+ panic("couldn't determine direction")
+ }
+ // we've received it, so it must be clresponse
+ return clresponse
+}
+
+func send(l *parse.Logmsg, stream jsonrpc2.Stream, id *jsonrpc2.ID) {
+ x, err := json.Marshal(l.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+ y := json.RawMessage(x)
+ if id == nil {
+ // need to use the number version of ID
+ n, err := strconv.Atoi(l.ID)
+ if err != nil {
+ n = 0
+ }
+ id = &jsonrpc2.ID{Number: int64(n)}
+ }
+ var r interface{}
+ switch l.Dir {
+ case parse.Clrequest:
+ r = jsonrpc2.WireRequest{
+ ID: id,
+ Method: l.Method,
+ Params: &y,
+ }
+ case parse.Svresponse:
+ r = jsonrpc2.WireResponse{
+ ID: id,
+ Result: &y,
+ }
+ case parse.Toserver:
+ r = jsonrpc2.WireRequest{
+ Method: l.Method,
+ Params: &y,
+ }
+ default:
+ log.Fatalf("sending %s", l.Dir)
+ }
+ data, err := json.Marshal(r)
+ if err != nil {
+ log.Fatal(err)
+ }
+ stream.Write(ctx, data)
+}
+
+func strID(x *jsonrpc2.ID) string {
+ if x.Name != "" {
+ log.Printf("strID returns %s", x.Name)
+ return x.Name
+ }
+ return strconv.Itoa(int(x.Number))
+}
+
+func respond(c *combined, stream jsonrpc2.Stream) {
+ // c is a server request
+ // pick out the id, and look for the response in msgs
+ id := strID(c.ID)
+ for _, l := range msgs {
+ if l.ID == id && l.Dir == parse.Svresponse {
+ // check that the methods match?
+ // need to send back the same ID we got.
+ send(l, stream, c.ID)
+ return
+ }
+ }
+ log.Fatalf("no response found %q %+v %+v", c.Method, c.ID, c)
+}
+
+func findgopls() string {
+ totry := [][]string{{"GOBIN", "/gopls"}, {"GOPATH", "/bin/gopls"}, {"HOME", "/go/bin/gopls"}}
+ // looks in the places go install would install:
+ // GOBIN, else GOPATH/bin, else HOME/go/bin
+ ok := func(s string) bool {
+ fd, err := os.Open(s)
+ if err != nil {
+ return false
+ }
+ fi, err := fd.Stat()
+ if err != nil {
+ return false
+ }
+ return fi.Mode()&0111 != 0
+ }
+ for _, t := range totry {
+ g := os.Getenv(t[0])
+ if g != "" && ok(g+t[1]) {
+ return g + t[1]
+ }
+ }
+ log.Fatal("could not find gopls")
+ return ""
+}
+
+func mimic() {
+ log.Printf("mimic %d", len(msgs))
+ if *command == "" {
+ *command = findgopls()
+ }
+ cmd := exec.Command(*command, "-logfile", "/tmp/seen", "-rpc.trace")
+ toServer, err := cmd.StdinPipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+ fromServer, err := cmd.StdoutPipe()
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = cmd.Start()
+ if err != nil {
+ log.Fatal(err)
+ }
+ stream := jsonrpc2.NewHeaderStream(fromServer, toServer)
+ rchan := make(chan *combined, 10) // do we need buffering?
+ rdr := func() {
+ for {
+ buf, _, err := stream.Read(ctx)
+ if err != nil {
+ rchan <- nil // close it instead?
+ return
+ }
+ msg := &combined{}
+ if err := json.Unmarshal(buf, msg); err != nil {
+ log.Fatal(err)
+ }
+ rchan <- msg
+ }
+ }
+ go rdr()
+ // send as many as possible: all clrequests and toservers up to a clresponse
+ // and loop
+ seenids := make(map[string]bool) // id's that have been responded toig:
+big:
+ for _, l := range msgs {
+ switch l.Dir {
+ case parse.Toserver: // just send these as we get to them
+ send(l, stream, nil)
+ case parse.Clrequest:
+ send(l, stream, nil) // for now, wait for a response, to make sure code is ok
+ fallthrough
+ case parse.Clresponse, parse.Reporterr: // don't go past these until they're received
+ if seenids[l.ID] {
+ break // onward, as it has been received already
+ }
+ done:
+ for {
+ x := <-rchan
+ if x == nil {
+ break big
+ }
+ // if it's svrequest, do something
+ // if it's clresponse or reporterr, add to seenids, and if it
+ // is l.id, break out of the loop, and continue the outer loop
+ switch x.dir() {
+ case svrequest:
+ respond(x, stream)
+ continue done // still waiting
+ case clresponse, reporterr:
+ id := strID(x.ID)
+ seenids[id] = true
+ if id == l.ID {
+ break done
+ }
+ case toclient:
+ continue
+ default:
+ log.Fatalf("%s", x.dir())
+ }
+ }
+ case parse.Svrequest: // not ours to send
+ continue
+ case parse.Svresponse: // sent by us, if the request arrives
+ continue
+ case parse.Toclient: // we don't send these
+ continue
+ }
+ }
+}
+
+func readLogs(fname string) []*logmsg {
+ byid := make(map[string]int)
+ msgs := []*logmsg{}
+ fd, err := os.Open(fname)
+ if err != nil {
+ log.Fatal(err)
+ }
+ logrdr = bufio.NewScanner(fd)
+ logrdr.Buffer(nil, 1<<25) // a large buffer, for safety
+ logrdr.Split(logRec)
+ for i := 0; logrdr.Scan(); i++ {
+ flds := strings.SplitN(logrdr.Text(), "\n", 2)
+ if len(flds) == 1 {
+ flds = append(flds, "") // for Errors
+ }
+ msg := parselog(flds[0], flds[1])
+ if msg == nil {
+ log.Fatalf("failed to parse %q", logrdr.Text())
+ continue
+ }
+ switch msg.dir {
+ case clrequest, svrequest:
+ v, err := msg.unmarshal(parse.Requests(msg.method))
+ if err != nil {
+ log.Fatal(err)
+ }
+ msg.body = v
+ case clresponse, svresponse:
+ v, err := msg.doresponse()
+ if err != nil {
+ log.Fatalf("%v %s", err, msg.method)
+ }
+ msg.body = v
+ case toserver, toclient:
+ v, err := msg.unmarshal(parse.Notifs(msg.method))
+ if err != nil {
+ log.Fatal(err)
+ }
+ msg.body = v
+ case reporterr:
+ msg.body = msg.id // cause?
+ }
+ byid[msg.id]++
+ msgs = append(msgs, msg)
+ }
+ if err = logrdr.Err(); err != nil {
+ log.Fatal(err)
+ return msgs
+ }
+ // there's 2 uses of id 1, and notifications have no id
+ for k, v := range byid {
+ if false && v != 2 {
+ log.Printf("ids %s:%d", k, v)
+ }
+ }
+ if false {
+ var m runtime.MemStats
+ runtime.ReadMemStats(&m)
+ log.Printf("%d msgs, alloc=%d HeapAlloc=%d", len(msgs), m.Alloc, m.HeapAlloc)
+ }
+ return msgs
+}
+
+func (d direction) String() string {
+ switch d {
+ case clrequest:
+ return "clrequest"
+ case clresponse:
+ return "clresponse"
+ case svrequest:
+ return "svrequest"
+ case svresponse:
+ return "svresponse"
+ case toserver:
+ return "toserver"
+ case toclient:
+ return "toclient"
+ case reporterr:
+ return "reporterr"
+ }
+ return fmt.Sprintf("dirname: %d unknown", d)
+}
+
+func (l *logmsg) Short() string {
+ return fmt.Sprintf("%s %s %s %s", l.dir, l.method, l.id, l.elapsed)
+}
+
+func (l *logmsg) unmarshal(p interface{}) (interface{}, error) {
+ r := []byte(l.rest)
+ if err := json.Unmarshal(r, p); err != nil {
+ // need general alternatives, but for now
+ // if p is *[]foo and rest is {}, return an empty p (or *p?)
+ // or, cheat:
+ if l.rest == "{}" {
+ return nil, nil
+ }
+ //log.Fatalf("%s/%s %T %q %v", l.method, l.id, p, l.rest, err)
+ return nil, err
+ }
+ return p, nil
+}
+
+func (l *logmsg) doresponse() (interface{}, error) {
+ for _, x := range parse.Responses(l.method) {
+ v, err := l.unmarshal(x)
+ if err == nil {
+ return v, nil
+ }
+ if x == nil {
+ return new(interface{}), nil
+ }
+ }
+ log.Fatalf("doresponse failed for %s", l.method)
+ return nil, nil
+}
+
+// parse a single log message, given first line, and the rest
+func parselog(first, rest string) *logmsg {
+ if strings.HasPrefix(rest, "Params: ") {
+ rest = rest[8:]
+ } else if strings.HasPrefix(rest, "Result: ") {
+ rest = rest[8:]
+ }
+ ans := &logmsg{hdr: first, rest: rest}
+ fixid := func(s string) string {
+ if s != "" && s[0] == '(' {
+ s = s[1 : len(s)-1]
+ }
+ return s
+ }
+ flds := strings.Fields(first)
+ chk := func(s string, n int) bool { return strings.Contains(first, s) && len(flds) == n }
+ // gopls and emacs differ in how they report elapsed time
+ switch {
+ case chk("Sending request", 9):
+ ans.dir = clrequest
+ ans.method = flds[6][1:]
+ ans.id = fixid(flds[8][:len(flds[8])-2])
+ clreq[ans.id] = ans
+ case chk("Received response", 11):
+ ans.dir = clresponse
+ ans.method = flds[6][1:]
+ ans.id = fixid(flds[8][:len(flds[8])-1])
+ ans.elapsed = flds[10]
+ clresp[ans.id] = ans
+ case chk("Received request", 9):
+ ans.dir = svrequest
+ ans.method = flds[6][1:]
+ ans.id = fixid(flds[8][:len(flds[8])-2])
+ svreq[ans.id] = ans
+ case chk("Sending response", 11), // gopls
+ chk("Sending response", 13): // emacs
+ ans.dir = svresponse
+ ans.method = flds[6][1:]
+ ans.id = fixid(flds[8][:len(flds[8])-1])
+ ans.elapsed = flds[10]
+ svresp[ans.id] = ans
+ case chk("Sending notification", 7):
+ ans.dir = toserver
+ ans.method = strings.Trim(flds[6], ".'")
+ if len(flds) == 9 {
+ log.Printf("len=%d method=%s %q", len(flds), ans.method, first)
+ }
+ case chk("Received notification", 7):
+ ans.dir = toclient
+ ans.method = flds[6][1 : len(flds[6])-2]
+ case strings.HasPrefix(first, "[Error - "):
+ ans.dir = reporterr
+ both := flds[5]
+ idx := strings.Index(both, "#") // relies on ID.Number
+ ans.method = both[:idx]
+ ans.id = fixid(both[idx+1:])
+ ans.rest = strings.Join(flds[6:], " ")
+ clreq[ans.id] = ans
+ default:
+ log.Fatalf("surprise, first=%q with %d flds", first, len(flds))
+ return nil
+ }
+ return ans
+}
+
+var recSep = regexp.MustCompile("\n\n\n|\r\n\r\n\r\n")
+
+// return start of next record, contents of record, error
+func logRec(b []byte, atEOF bool) (int, []byte, error) { //bufio.SplitFunc
+ got := recSep.FindIndex(b)
+ if got == nil {
+ if !atEOF {
+ return 0, nil, nil // need more
+ }
+ return 0, nil, io.EOF
+ }
+ return got[1], b[:got[0]], nil
+}