internal/lsp: new tracing system

Change-Id: Id6e6b121e8c69d6324a15cb46b3b9937dfe7a1dd
Reviewed-on: https://go-review.googlesource.com/c/tools/+/186201
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index be64ab6..92c4075 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -21,6 +21,7 @@
 	"golang.org/x/tools/internal/lsp/telemetry/log"
 	"golang.org/x/tools/internal/lsp/telemetry/metric"
 	"golang.org/x/tools/internal/lsp/telemetry/tag"
+	"golang.org/x/tools/internal/lsp/telemetry/trace"
 	"golang.org/x/tools/internal/lsp/telemetry/worker"
 	"golang.org/x/tools/internal/span"
 )
@@ -219,6 +220,8 @@
 	metric.RegisterObservers(prometheus.observeMetric)
 	rpcs := rpcs{}
 	metric.RegisterObservers(rpcs.observeMetric)
+	traces := traces{}
+	trace.RegisterObservers(traces.export)
 	go func() {
 		mux := http.NewServeMux()
 		mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
@@ -230,6 +233,7 @@
 		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
 		mux.HandleFunc("/metrics/", prometheus.serve)
 		mux.HandleFunc("/rpc/", Render(rpcTmpl, rpcs.getData))
+		mux.HandleFunc("/trace/", Render(traceTmpl, traces.getData))
 		mux.HandleFunc("/cache/", Render(cacheTmpl, getCache))
 		mux.HandleFunc("/session/", Render(sessionTmpl, getSession))
 		mux.HandleFunc("/view/", Render(viewTmpl, getView))
@@ -290,6 +294,10 @@
 td.value {
   text-align: right;
 }
+ul.events {
+	list-style-type: none;
+}
+
 </style>
 {{block "head" .}}{{end}}
 </head>
@@ -299,6 +307,7 @@
 <a href="/memory">Memory</a>
 <a href="/metrics">Metrics</a>
 <a href="/rpc">RPC</a>
+<a href="/trace">Trace</a>
 <hr>
 <h1>{{template "title" .}}</h1>
 {{block "body" .}}
diff --git a/internal/lsp/debug/trace.go b/internal/lsp/debug/trace.go
new file mode 100644
index 0000000..19911ae
--- /dev/null
+++ b/internal/lsp/debug/trace.go
@@ -0,0 +1,172 @@
+// 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 (
+	"bytes"
+	"fmt"
+	"html/template"
+	"net/http"
+	"sort"
+	"strings"
+	"time"
+
+	"golang.org/x/tools/internal/lsp/telemetry/tag"
+	"golang.org/x/tools/internal/lsp/telemetry/trace"
+)
+
+var traceTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
+{{define "title"}}Trace Information{{end}}
+{{define "body"}}
+	{{range .Traces}}<a href="/trace/{{.Name}}">{{.Name}}</a> last: {{.Last.Duration}}, longest: {{.Longest.Duration}}<br>{{end}}
+	{{if .Selected}}
+		<H2>{{.Selected.Name}}</H2>
+		{{if .Selected.Last}}<H3>Last</H3><ul>{{template "details" .Selected.Last}}</ul>{{end}}
+		{{if .Selected.Longest}}<H3>Longest</H3><ul>{{template "details" .Selected.Longest}}</ul>{{end}}
+	{{end}}
+{{end}}
+{{define "details"}}
+	<li>{{.Offset}} {{.Name}} {{.Duration}} {{.Tags}}</li>
+	{{if .Events}}<ul class=events>{{range .Events}}<li>{{.Offset}} {{.Tags}}</li>{{end}}</ul>{{end}}
+	{{if .Children}}<ul>{{range .Children}}{{template "details" .}}{{end}}</ul>{{end}}
+{{end}}
+`))
+
+type traces struct {
+	sets       map[string]*traceSet
+	unfinished map[trace.SpanID]*traceData
+}
+
+type traceResults struct {
+	Traces   []*traceSet
+	Selected *traceSet
+}
+
+type traceSet struct {
+	Name    string
+	Last    *traceData
+	Longest *traceData
+}
+
+type traceData struct {
+	ID       trace.SpanID
+	ParentID trace.SpanID
+	Name     string
+	Start    time.Time
+	Finish   time.Time
+	Offset   time.Duration
+	Duration time.Duration
+	Tags     string
+	Events   []traceEvent
+	Children []*traceData
+}
+
+type traceEvent struct {
+	Time   time.Time
+	Offset time.Duration
+	Tags   string
+}
+
+func (t *traces) export(span *trace.Span) {
+	if t.sets == nil {
+		t.sets = make(map[string]*traceSet)
+		t.unfinished = make(map[trace.SpanID]*traceData)
+	}
+	// is this a completed span?
+	if span.Finish.IsZero() {
+		t.start(span)
+	} else {
+		t.finish(span)
+	}
+}
+
+func (t *traces) start(span *trace.Span) {
+	// just starting, add it to the unfinished map
+	td := &traceData{
+		ID:       span.SpanID,
+		ParentID: span.ParentID,
+		Name:     span.Name,
+		Start:    span.Start,
+		Tags:     renderTags(span.Tags),
+	}
+	t.unfinished[span.SpanID] = td
+	// and wire up parents if we have them
+	if !span.ParentID.IsValid() {
+		return
+	}
+	parent, found := t.unfinished[span.ParentID]
+	if !found {
+		// trace had an invalid parent, so it cannot itself be valid
+		return
+	}
+	parent.Children = append(parent.Children, td)
+
+}
+
+func (t *traces) finish(span *trace.Span) {
+	// finishing, must be already in the map
+	td, found := t.unfinished[span.SpanID]
+	if !found {
+		return // if this happens we are in a bad place
+	}
+	delete(t.unfinished, span.SpanID)
+
+	td.Finish = span.Finish
+	td.Duration = span.Finish.Sub(span.Start)
+	td.Events = make([]traceEvent, len(span.Events))
+	for i, event := range span.Events {
+		td.Events[i] = traceEvent{
+			Time: event.Time,
+			Tags: renderTags(event.Tags),
+		}
+	}
+
+	set, ok := t.sets[span.Name]
+	if !ok {
+		set = &traceSet{Name: span.Name}
+		t.sets[span.Name] = set
+	}
+	set.Last = td
+	if set.Longest == nil || set.Last.Duration > set.Longest.Duration {
+		set.Longest = set.Last
+	}
+	if !td.ParentID.IsValid() {
+		fillOffsets(td, td.Start)
+	}
+}
+
+func (t *traces) getData(req *http.Request) interface{} {
+	if len(t.sets) == 0 {
+		return nil
+	}
+	data := traceResults{}
+	data.Traces = make([]*traceSet, 0, len(t.sets))
+	for _, set := range t.sets {
+		data.Traces = append(data.Traces, set)
+	}
+	sort.Slice(data.Traces, func(i, j int) bool { return data.Traces[i].Name < data.Traces[j].Name })
+	if bits := strings.SplitN(req.URL.Path, "/trace/", 2); len(bits) > 1 {
+		data.Selected = t.sets[bits[1]]
+	}
+	return data
+}
+
+func fillOffsets(td *traceData, start time.Time) {
+	td.Offset = td.Start.Sub(start)
+	for i := range td.Events {
+		td.Events[i].Offset = td.Events[i].Time.Sub(start)
+	}
+	for _, child := range td.Children {
+		fillOffsets(child, start)
+	}
+}
+
+func renderTags(tags tag.List) string {
+	buf := &bytes.Buffer{}
+	for _, tag := range tags {
+		fmt.Fprintf(buf, "%v=%q ", tag.Key, tag.Value)
+	}
+	return buf.String()
+}
diff --git a/internal/lsp/telemetry/trace/id.go b/internal/lsp/telemetry/trace/id.go
new file mode 100644
index 0000000..c098532
--- /dev/null
+++ b/internal/lsp/telemetry/trace/id.go
@@ -0,0 +1,72 @@
+// 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 tag adds support for telemetry tracins.
+package trace
+
+import (
+	crand "crypto/rand"
+	"encoding/binary"
+	"fmt"
+	"math/rand"
+	"sync"
+	"sync/atomic"
+)
+
+type TraceID [16]byte
+type SpanID [8]byte
+
+func (t TraceID) String() string {
+	return fmt.Sprintf("%02x", t[:])
+}
+
+func (s SpanID) String() string {
+	return fmt.Sprintf("%02x", s[:])
+}
+
+func (s SpanID) IsValid() bool {
+	return s != SpanID{}
+}
+
+var (
+	generationMu sync.Mutex
+	nextSpanID   uint64
+	spanIDInc    uint64
+
+	traceIDAdd  [2]uint64
+	traceIDRand *rand.Rand
+)
+
+func initGenerator() {
+	var rngSeed int64
+	for _, p := range []interface{}{
+		&rngSeed, &traceIDAdd, &nextSpanID, &spanIDInc,
+	} {
+		binary.Read(crand.Reader, binary.LittleEndian, p)
+	}
+	traceIDRand = rand.New(rand.NewSource(rngSeed))
+	spanIDInc |= 1
+}
+
+func newTraceID() TraceID {
+	generationMu.Lock()
+	defer generationMu.Unlock()
+	if traceIDRand == nil {
+		initGenerator()
+	}
+	var tid [16]byte
+	binary.LittleEndian.PutUint64(tid[0:8], traceIDRand.Uint64()+traceIDAdd[0])
+	binary.LittleEndian.PutUint64(tid[8:16], traceIDRand.Uint64()+traceIDAdd[1])
+	return tid
+}
+
+func newSpanID() SpanID {
+	var id uint64
+	for id == 0 {
+		id = atomic.AddUint64(&nextSpanID, spanIDInc)
+	}
+	var sid [8]byte
+	binary.LittleEndian.PutUint64(sid[:], id)
+	return sid
+}
diff --git a/internal/lsp/telemetry/trace/trace.go b/internal/lsp/telemetry/trace/trace.go
index 2e73536..3ad8849 100644
--- a/internal/lsp/telemetry/trace/trace.go
+++ b/internal/lsp/telemetry/trace/trace.go
@@ -2,19 +2,139 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package tag adds support for telemetry tracing.
+// Package trace adds support for telemetry tracing.
 package trace
 
 import (
 	"context"
+	"fmt"
+	"time"
 
+	"golang.org/x/tools/internal/lsp/telemetry/log"
 	"golang.org/x/tools/internal/lsp/telemetry/tag"
+	"golang.org/x/tools/internal/lsp/telemetry/worker"
 )
 
+type Span struct {
+	Name     string
+	TraceID  TraceID
+	SpanID   SpanID
+	ParentID SpanID
+	Start    time.Time
+	Finish   time.Time
+	Tags     tag.List
+	Events   []Event
+
+	ready bool
+}
+
+type Event struct {
+	Time time.Time
+	Tags tag.List
+}
+
+type Observer func(*Span)
+
+func RegisterObservers(o ...Observer) {
+	worker.Do(func() {
+		if !registered {
+			registered = true
+			tag.Observe(tagObserver)
+			log.AddLogger(logger)
+		}
+		observers = append(observers, o...)
+	})
+}
+
 func StartSpan(ctx context.Context, name string, tags ...tag.Tag) (context.Context, func()) {
-	return tag.With(ctx, tags...), func() {}
+	span := &Span{
+		Name:  name,
+		Start: time.Now(),
+	}
+	if parent := fromContext(ctx); parent != nil {
+		span.TraceID = parent.TraceID
+		span.ParentID = parent.SpanID
+	} else {
+		span.TraceID = newTraceID()
+	}
+	span.SpanID = newSpanID()
+	ctx = context.WithValue(ctx, contextKey, span)
+	if len(tags) > 0 {
+		ctx = tag.With(ctx, tags...)
+	}
+	worker.Do(func() {
+		span.ready = true
+		for _, o := range observers {
+			o(span)
+		}
+	})
+	return ctx, span.close
+}
+
+func (s *Span) close() {
+	now := time.Now()
+	worker.Do(func() {
+		s.Finish = now
+		for _, o := range observers {
+			o(s)
+		}
+	})
+}
+
+func (s *Span) Format(f fmt.State, r rune) {
+	fmt.Fprintf(f, "%v %v:%v", s.Name, s.TraceID, s.SpanID)
+	if s.ParentID.IsValid() {
+		fmt.Fprintf(f, "[%v]", s.ParentID)
+	}
+	fmt.Fprintf(f, " %v->%v", s.Start, s.Finish)
+}
+
+type contextKeyType int
+
+var contextKey contextKeyType
+
+func fromContext(ctx context.Context) *Span {
+	v := ctx.Value(contextKey)
+	if v == nil {
+		return nil
+	}
+	return v.(*Span)
+}
+
+var (
+	observers  []Observer
+	registered bool
+)
+
+func tagObserver(ctx context.Context, at time.Time, tags tag.List) {
+	span := fromContext(ctx)
+	if span == nil {
+		return
+	}
+	if !span.ready {
+		span.Tags = append(span.Tags, tags...)
+		return
+	}
+	span.Events = append(span.Events, Event{
+		Time: at,
+		Tags: tags,
+	})
+}
+
+func logger(ctx context.Context, at time.Time, tags tag.List) bool {
+	span := fromContext(ctx)
+	if span == nil {
+		return false
+	}
+	span.Events = append(span.Events, Event{
+		Time: at,
+		Tags: tags,
+	})
+	return false
 }
 
 // Detach returns a context without an associated span.
 // This allows the creation of spans that are not children of the current span.
-func Detach(ctx context.Context) context.Context { return ctx }
+func Detach(ctx context.Context) context.Context {
+	return context.WithValue(ctx, contextKey, nil)
+}