blob: e1c93e2847cff016f97210d455b9912b65471f95 [file] [log] [blame]
// 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 (
"context"
"fmt"
"html/template"
"net/http"
"sort"
"sync"
"time"
"golang.org/x/tools/internal/lsp/debug/tag"
"golang.org/x/tools/internal/telemetry/event"
"golang.org/x/tools/internal/telemetry/export"
)
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}}{{if gt .Count 0}}<b>{{.Count}}</b> {{.Limit}} {{end}}{{end}}
<br>
<i>Received</i> {{.Received}} (avg. {{.ReceivedMean}})
<i>Sent</i> {{.Sent}} (avg. {{.SentMean}})
<br>
<i>Result codes</i> {{range .Codes}}{{.Key}}={{.Count}} {{end}}
</P>
{{end}}
{{end}}
`))
type rpcs struct {
mu sync.Mutex
Inbound []*rpcStats // stats for incoming lsp rpcs sorted by method name
Outbound []*rpcStats // stats for outgoing lsp rpcs sorted by method name
}
type rpcStats struct {
Method string
Started int64
Completed int64
Latency rpcTimeHistogram
Received byteUnits
Sent byteUnits
Codes []*rpcCodeBucket
}
type rpcTimeHistogram struct {
Sum timeUnits
Count int64
Min timeUnits
Max timeUnits
Values []rpcTimeBucket
}
type rpcTimeBucket struct {
Limit timeUnits
Count int64
}
type rpcCodeBucket struct {
Key string
Count int64
}
func (r *rpcs) ProcessEvent(ctx context.Context, ev event.Event, tagMap event.TagMap) context.Context {
r.mu.Lock()
defer r.mu.Unlock()
switch {
case ev.IsStartSpan():
if _, stats := r.getRPCSpan(ctx, ev); stats != nil {
stats.Started++
}
case ev.IsEndSpan():
span, stats := r.getRPCSpan(ctx, ev)
if stats != nil {
endRPC(ctx, ev, span, stats)
}
case ev.IsRecord():
sent := byteUnits(tag.SentBytes.Get(tagMap))
rec := byteUnits(tag.ReceivedBytes.Get(tagMap))
if sent != 0 || rec != 0 {
if _, stats := r.getRPCSpan(ctx, ev); stats != nil {
stats.Sent += sent
stats.Received += rec
}
}
}
return ctx
}
func endRPC(ctx context.Context, ev event.Event, span *export.Span, stats *rpcStats) {
// update the basic counts
stats.Completed++
// get and record the status code
if status := getStatusCode(span); status != "" {
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[j].Key
})
}
b.Count++
}
// calculate latency if this was an rpc span
elapsedTime := span.Finish().At.Sub(span.Start().At)
latencyMillis := timeUnits(elapsedTime) / timeUnits(time.Millisecond)
if stats.Latency.Count == 0 {
stats.Latency.Min = latencyMillis
stats.Latency.Max = latencyMillis
} else {
if stats.Latency.Min > latencyMillis {
stats.Latency.Min = latencyMillis
}
if stats.Latency.Max < latencyMillis {
stats.Latency.Max = latencyMillis
}
}
stats.Latency.Count++
stats.Latency.Sum += latencyMillis
for i := range stats.Latency.Values {
if stats.Latency.Values[i].Limit > latencyMillis {
stats.Latency.Values[i].Count++
break
}
}
}
func (r *rpcs) getRPCSpan(ctx context.Context, ev event.Event) (*export.Span, *rpcStats) {
// get the span
span := export.GetSpan(ctx)
if span == nil {
return nil, nil
}
// use the span start event look up the correct stats block
// we do this because it prevents us matching a sub span
return span, r.getRPCStats(span.Start())
}
func (r *rpcs) getRPCStats(tagMap event.TagMap) *rpcStats {
method := tag.Method.Get(tagMap)
if method == "" {
return nil
}
set := &r.Inbound
if tag.RPCDirection.Get(tagMap) != tag.Inbound {
set = &r.Outbound
}
// get the record for this method
index := sort.Search(len(*set), func(i int) bool {
return (*set)[i].Method >= method
})
if index < len(*set) && (*set)[index].Method == method {
return (*set)[index]
}
old := *set
*set = make([]*rpcStats, len(old)+1)
copy(*set, old[:index])
copy((*set)[index+1:], old[index:])
stats := &rpcStats{Method: method}
stats.Latency.Values = make([]rpcTimeBucket, len(millisecondsDistribution))
for i, m := range millisecondsDistribution {
stats.Latency.Values[i].Limit = timeUnits(m)
}
(*set)[index] = stats
return stats
}
func (s *rpcStats) InProgress() int64 { return s.Started - s.Completed }
func (s *rpcStats) SentMean() byteUnits { return s.Sent / byteUnits(s.Started) }
func (s *rpcStats) ReceivedMean() byteUnits { return s.Received / byteUnits(s.Started) }
func (h *rpcTimeHistogram) Mean() timeUnits { return h.Sum / timeUnits(h.Count) }
func getStatusCode(span *export.Span) string {
for _, ev := range span.Events() {
if status := tag.StatusCode.Get(ev); status != "" {
return status
}
}
return ""
}
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"})
}