internal/lsp: add an rpc summary debug page using the metrics

Change-Id: I92481b0e7996747d70081d575a47768222219506
Reviewed-on: https://go-review.googlesource.com/c/tools/+/185986
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
diff --git a/internal/lsp/debug/rpc.go b/internal/lsp/debug/rpc.go
new file mode 100644
index 0000000..87624ae
--- /dev/null
+++ b/internal/lsp/debug/rpc.go
@@ -0,0 +1,209 @@
+// 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 debug
+
+import (
+	"fmt"
+	"html/template"
+	"log"
+	"net/http"
+	"sort"
+
+	"golang.org/x/tools/internal/lsp/telemetry"
+	"golang.org/x/tools/internal/lsp/telemetry/metric"
+)
+
+var rpcTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+{{define "title"}}RPC Information{{end}}
+{{define "body"}}
+	<H2>Inbound</H2>
+	{{template "rpcSection" .Inbound}}
+	<H2>Outbound</H2>
+	{{template "rpcSection" .Outbound}}
+{{end}}
+{{define "rpcSection"}}
+	{{range .}}<P>
+		<b>{{.Method}}</b> {{.Started}} <a href="/trace/{{.Method}}">traces</a> ({{.InProgress}} in progress)
+		<br>
+		<i>Latency</i> {{with .Latency}}{{.Mean}} ({{.Min}}<{{.Max}}){{end}}
+		<i>By bucket</i> 0s {{range .Latency.Values}}<b>{{.Count}}</b> {{.Limit}} {{end}}
+		<br>
+		<i>Received</i> {{with .Received}}{{.Mean}} ({{.Min}}<{{.Max}}){{end}}
+		<i>Sent</i> {{with .Sent}}{{.Mean}} ({{.Min}}<{{.Max}}){{end}}
+		<br>
+		<i>Result codes</i> {{range .Codes}}{{.Key}}={{.Count}} {{end}}
+		</P>
+	{{end}}
+{{end}}
+`))
+
+type rpcs struct {
+	Inbound  []*rpcStats
+	Outbound []*rpcStats
+}
+
+type rpcStats struct {
+	Method     string
+	Started    int64
+	Completed  int64
+	InProgress int64
+	Latency    rpcTimeHistogram
+	Received   rpcBytesHistogram
+	Sent       rpcBytesHistogram
+	Codes      []*rpcCodeBucket
+}
+
+type rpcTimeHistogram struct {
+	Sum    timeUnits
+	Count  int64
+	Mean   timeUnits
+	Min    timeUnits
+	Max    timeUnits
+	Values []rpcTimeBucket
+}
+
+type rpcTimeBucket struct {
+	Limit timeUnits
+	Count int64
+}
+
+type rpcBytesHistogram struct {
+	Sum    byteUnits
+	Count  int64
+	Mean   byteUnits
+	Min    byteUnits
+	Max    byteUnits
+	Values []rpcBytesBucket
+}
+
+type rpcBytesBucket struct {
+	Limit byteUnits
+	Count int64
+}
+
+type rpcCodeBucket struct {
+	Key   string
+	Count int64
+}
+
+func (r *rpcs) observeMetric(data metric.Data) {
+	for i, group := range data.Groups() {
+		set := &r.Inbound
+		if group.Get(telemetry.RPCDirection) == telemetry.Outbound {
+			set = &r.Outbound
+		}
+		method, ok := group.Get(telemetry.Method).(string)
+		if !ok {
+			log.Printf("Not a method... %v", group)
+			continue
+		}
+		index := sort.Search(len(*set), func(i int) bool {
+			return (*set)[i].Method >= method
+		})
+		if index >= len(*set) || (*set)[index].Method != method {
+			old := *set
+			*set = make([]*rpcStats, len(old)+1)
+			copy(*set, old[:index])
+			copy((*set)[index+1:], old[index:])
+			(*set)[index] = &rpcStats{Method: method}
+		}
+		stats := (*set)[index]
+		switch data.Handle() {
+		case started:
+			stats.Started = data.(*metric.Int64Data).Rows[i]
+		case completed:
+			status, ok := group.Get(telemetry.StatusCode).(string)
+			if !ok {
+				log.Printf("Not status... %v", group)
+				continue
+			}
+			var b *rpcCodeBucket
+			for c, entry := range stats.Codes {
+				if entry.Key == status {
+					b = stats.Codes[c]
+					break
+				}
+			}
+			if b == nil {
+				b = &rpcCodeBucket{Key: status}
+				stats.Codes = append(stats.Codes, b)
+				sort.Slice(stats.Codes, func(i int, j int) bool {
+					return stats.Codes[i].Key < stats.Codes[i].Key
+				})
+			}
+			b.Count = data.(*metric.Int64Data).Rows[i]
+		case latency:
+			data := data.(*metric.HistogramFloat64Data)
+			row := data.Rows[i]
+			stats.Latency.Count = row.Count
+			stats.Latency.Sum = timeUnits(row.Sum)
+			stats.Latency.Min = timeUnits(row.Min)
+			stats.Latency.Max = timeUnits(row.Max)
+			stats.Latency.Mean = timeUnits(row.Sum) / timeUnits(row.Count)
+			stats.Latency.Values = make([]rpcTimeBucket, len(data.Info.Buckets))
+			last := int64(0)
+			for i, b := range data.Info.Buckets {
+				stats.Latency.Values[i].Limit = timeUnits(b)
+				stats.Latency.Values[i].Count = row.Values[i] - last
+				last = row.Values[i]
+			}
+		case sentBytes:
+			data := data.(*metric.HistogramInt64Data)
+			row := data.Rows[i]
+			stats.Sent.Count = row.Count
+			stats.Sent.Sum = byteUnits(row.Sum)
+			stats.Sent.Min = byteUnits(row.Min)
+			stats.Sent.Max = byteUnits(row.Max)
+			stats.Sent.Mean = byteUnits(row.Sum) / byteUnits(row.Count)
+		case receivedBytes:
+			data := data.(*metric.HistogramInt64Data)
+			row := data.Rows[i]
+			stats.Received.Count = row.Count
+			stats.Received.Sum = byteUnits(row.Sum)
+			stats.Sent.Min = byteUnits(row.Min)
+			stats.Sent.Max = byteUnits(row.Max)
+			stats.Received.Mean = byteUnits(row.Sum) / byteUnits(row.Count)
+		}
+	}
+
+	for _, set := range [][]*rpcStats{r.Inbound, r.Outbound} {
+		for _, stats := range set {
+			stats.Completed = 0
+			for _, b := range stats.Codes {
+				stats.Completed += b.Count
+			}
+			stats.InProgress = stats.Started - stats.Completed
+		}
+	}
+}
+
+func (r *rpcs) getData(req *http.Request) interface{} {
+	return r
+}
+
+func units(v float64, suffixes []string) string {
+	s := ""
+	for _, s = range suffixes {
+		n := v / 1000
+		if n < 1 {
+			break
+		}
+		v = n
+	}
+	return fmt.Sprintf("%.2f%s", v, s)
+}
+
+type timeUnits float64
+
+func (v timeUnits) String() string {
+	v = v * 1000 * 1000
+	return units(float64(v), []string{"ns", "μs", "ms", "s"})
+}
+
+type byteUnits float64
+
+func (v byteUnits) String() string {
+	return units(float64(v), []string{"B", "KB", "MB", "GB", "TB"})
+}
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index 26d8b55..2427cdc 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -20,6 +20,7 @@
 	"sync"
 
 	"golang.org/x/tools/internal/lsp/telemetry/metric"
+	"golang.org/x/tools/internal/lsp/telemetry/worker"
 	"golang.org/x/tools/internal/span"
 )
 
@@ -215,6 +216,8 @@
 	log.Printf("Debug serving on port: %d", listener.Addr().(*net.TCPAddr).Port)
 	prometheus := prometheus{}
 	metric.RegisterObservers(prometheus.observeMetric)
+	rpcs := rpcs{}
+	metric.RegisterObservers(rpcs.observeMetric)
 	go func() {
 		mux := http.NewServeMux()
 		mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
@@ -225,6 +228,7 @@
 		mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
 		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
 		mux.HandleFunc("/metrics/", prometheus.serve)
+		mux.HandleFunc("/rpc/", Render(rpcTmpl, rpcs.getData))
 		mux.HandleFunc("/cache/", Render(cacheTmpl, getCache))
 		mux.HandleFunc("/session/", Render(sessionTmpl, getSession))
 		mux.HandleFunc("/view/", Render(viewTmpl, getView))
@@ -242,13 +246,18 @@
 
 func Render(tmpl *template.Template, fun func(*http.Request) interface{}) func(http.ResponseWriter, *http.Request) {
 	return func(w http.ResponseWriter, r *http.Request) {
-		var data interface{}
-		if fun != nil {
-			data = fun(r)
-		}
-		if err := tmpl.Execute(w, data); err != nil {
-			log.Print(err)
-		}
+		done := make(chan struct{})
+		worker.Do(func() {
+			defer close(done)
+			var data interface{}
+			if fun != nil {
+				data = fun(r)
+			}
+			if err := tmpl.Execute(w, data); err != nil {
+				log.Print(err)
+			}
+		})
+		<-done
 	}
 }
 
@@ -288,6 +297,7 @@
 <a href="/info">Info</a>
 <a href="/memory">Memory</a>
 <a href="/metrics">Metrics</a>
+<a href="/rpc">RPC</a>
 <hr>
 <h1>{{template "title" .}}</h1>
 {{block "body" .}}
@@ -358,8 +368,6 @@
 {{define "title"}}GoPls Debug pages{{end}}
 {{define "body"}}
 <a href="/debug/pprof">Profiling</a>
-<a href="/debug/rpcz">RPCz</a>
-<a href="/debug/tracez">Tracez</a>
 {{end}}
 `))