1
0
mirror of https://github.com/golang/go synced 2024-10-01 09:28:37 -06:00
go/internal/lsp/debug/serve.go
Rob Findley 98c82cf1f4 internal/lsp: add server instance to debug info
When debugging multiple instances of gopls simultaneously, it is useful
to be able to inspect stateful debugging information for each server
instance, such as the location of logfiles and server startup
information.

This CL adds an additional section to the /info http handler, that
formats additional information related to the gopls instance handling
the request.

Updates golang/go#34111

Change-Id: I6cb8073800ce52b0645f1898461a19e1ac980d2b
Reviewed-on: https://go-review.googlesource.com/c/tools/+/214803
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
Run-TryBot: Robert Findley <rfindley@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
2020-01-15 14:58:21 +00:00

437 lines
11 KiB
Go

// 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"
"context"
"go/token"
"html/template"
stdlog "log"
"net"
"net/http"
"net/http/pprof"
_ "net/http/pprof" // pull in the standard pprof handlers
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
"golang.org/x/tools/internal/span"
"golang.org/x/tools/internal/telemetry/export"
"golang.org/x/tools/internal/telemetry/export/prometheus"
"golang.org/x/tools/internal/telemetry/log"
"golang.org/x/tools/internal/telemetry/tag"
)
type Instance interface {
Logfile() string
StartTime() time.Time
Address() string
Debug() string
Workdir() string
}
type Cache interface {
ID() string
FileSet() *token.FileSet
}
type Session interface {
ID() string
Cache() Cache
Files() []*File
File(hash string) *File
}
type View interface {
ID() string
Name() string
Folder() span.URI
Session() Session
}
type File struct {
Session Session
URI span.URI
Data string
Error error
Hash string
}
var (
mu sync.Mutex
data = struct {
Caches []Cache
Sessions []Session
Views []View
}{}
)
// AddCache adds a cache to the set being served
func AddCache(cache Cache) {
mu.Lock()
defer mu.Unlock()
data.Caches = append(data.Caches, cache)
}
// DropCache drops a cache from the set being served
func DropCache(cache Cache) {
mu.Lock()
defer mu.Unlock()
//find and remove the cache
if i, _ := findCache(cache.ID()); i >= 0 {
copy(data.Caches[i:], data.Caches[i+1:])
data.Caches[len(data.Caches)-1] = nil
data.Caches = data.Caches[:len(data.Caches)-1]
}
}
func findCache(id string) (int, Cache) {
for i, c := range data.Caches {
if c.ID() == id {
return i, c
}
}
return -1, nil
}
func getCache(r *http.Request) interface{} {
mu.Lock()
defer mu.Unlock()
id := path.Base(r.URL.Path)
result := struct {
Cache
Sessions []Session
}{}
_, result.Cache = findCache(id)
// now find all the views that belong to this session
for _, v := range data.Sessions {
if v.Cache().ID() == id {
result.Sessions = append(result.Sessions, v)
}
}
return result
}
func findSession(id string) Session {
for _, c := range data.Sessions {
if c.ID() == id {
return c
}
}
return nil
}
func getSession(r *http.Request) interface{} {
mu.Lock()
defer mu.Unlock()
id := path.Base(r.URL.Path)
result := struct {
Session
Views []View
}{
Session: findSession(id),
}
// now find all the views that belong to this session
for _, v := range data.Views {
if v.Session().ID() == id {
result.Views = append(result.Views, v)
}
}
return result
}
func findView(id string) View {
for _, c := range data.Views {
if c.ID() == id {
return c
}
}
return nil
}
func getView(r *http.Request) interface{} {
mu.Lock()
defer mu.Unlock()
id := path.Base(r.URL.Path)
return findView(id)
}
func getFile(r *http.Request) interface{} {
mu.Lock()
defer mu.Unlock()
hash := path.Base(r.URL.Path)
sid := path.Base(path.Dir(r.URL.Path))
session := findSession(sid)
return session.File(hash)
}
func getInfo(s Instance) dataFunc {
return func(r *http.Request) interface{} {
buf := &bytes.Buffer{}
PrintServerInfo(buf, s)
return template.HTML(buf.String())
}
}
func getMemory(r *http.Request) interface{} {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return m
}
// AddSession adds a session to the set being served
func AddSession(session Session) {
mu.Lock()
defer mu.Unlock()
data.Sessions = append(data.Sessions, session)
}
// DropSession drops a session from the set being served
func DropSession(session Session) {
mu.Lock()
defer mu.Unlock()
//find and remove the session
}
// AddView adds a view to the set being served
func AddView(view View) {
mu.Lock()
defer mu.Unlock()
data.Views = append(data.Views, view)
}
// DropView drops a view from the set being served
func DropView(view View) {
mu.Lock()
defer mu.Unlock()
//find and remove the view
}
// Serve starts and runs a debug server in the background.
// It also logs the port the server starts on, to allow for :0 auto assigned
// ports.
func Serve(ctx context.Context, addr string, instance Instance) error {
mu.Lock()
defer mu.Unlock()
if addr == "" {
return nil
}
listener, err := net.Listen("tcp", addr)
if err != nil {
return err
}
port := listener.Addr().(*net.TCPAddr).Port
if strings.HasSuffix(addr, ":0") {
stdlog.Printf("debug server listening on port %d", port)
}
log.Print(ctx, "Debug serving", tag.Of("Port", port))
prometheus := prometheus.New()
rpcs := &rpcs{}
traces := &traces{}
export.AddExporters(prometheus, rpcs, traces)
go func() {
mux := http.NewServeMux()
mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
mux.HandleFunc("/debug/", Render(debugTmpl, nil))
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
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("/trace/", Render(traceTmpl, traces.getData))
mux.HandleFunc("/cache/", Render(cacheTmpl, getCache))
mux.HandleFunc("/session/", Render(sessionTmpl, getSession))
mux.HandleFunc("/view/", Render(viewTmpl, getView))
mux.HandleFunc("/file/", Render(fileTmpl, getFile))
mux.HandleFunc("/info", Render(infoTmpl, getInfo(instance)))
mux.HandleFunc("/memory", Render(memoryTmpl, getMemory))
if err := http.Serve(listener, mux); err != nil {
log.Error(ctx, "Debug server failed", err)
return
}
log.Print(ctx, "Debug server finished")
}()
return nil
}
type dataFunc func(*http.Request) interface{}
func Render(tmpl *template.Template, fun dataFunc) 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.Error(context.Background(), "", err)
}
}
}
func commas(s string) string {
for i := len(s); i > 3; {
i -= 3
s = s[:i] + "," + s[i:]
}
return s
}
func fuint64(v uint64) string {
return commas(strconv.FormatUint(v, 10))
}
func fuint32(v uint32) string {
return commas(strconv.FormatUint(uint64(v), 10))
}
var BaseTemplate = template.Must(template.New("").Parse(`
<html>
<head>
<title>{{template "title" .}}</title>
<style>
.profile-name{
display:inline-block;
width:6rem;
}
td.value {
text-align: right;
}
ul.events {
list-style-type: none;
}
</style>
{{block "head" .}}{{end}}
</head>
<body>
<a href="/">Main</a>
<a href="/info">Info</a>
<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" .}}
Unknown page
{{end}}
</body>
</html>
{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}}
{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}}
`)).Funcs(template.FuncMap{
"fuint64": fuint64,
"fuint32": fuint32,
})
var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls server information{{end}}
{{define "body"}}
<h2>Caches</h2>
<ul>{{range .Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
<h2>Sessions</h2>
<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
<h2>Views</h2>
<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
{{end}}
`))
var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls version information{{end}}
{{define "body"}}
{{.}}
{{end}}
`))
var memoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls memory usage{{end}}
{{define "head"}}<meta http-equiv="refresh" content="5">{{end}}
{{define "body"}}
<h2>Stats</h2>
<table>
<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr>
<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr>
<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr>
<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr>
<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr>
<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr>
<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr>
<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr>
<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr>
<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr>
<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr>
<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr>
<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr>
<tr><td class="label">GC metadata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr>
<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr>
</table>
<h2>By size</h2>
<table>
<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr>
{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}}
</table>
{{end}}
`))
var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls Debug pages{{end}}
{{define "body"}}
<a href="/debug/pprof">Profiling</a>
{{end}}
`))
var cacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Cache {{.ID}}{{end}}
{{define "body"}}
<h2>Sessions</h2>
<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul>
{{end}}
`))
var sessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}Session {{.ID}}{{end}}
{{define "body"}}
From: <b>{{template "cachelink" .Cache.ID}}</b><br>
<h2>Views</h2>
<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul>
<h2>Files</h2>
<ul>{{range .Files}}<li>{{template "filelink" .}}</li>{{end}}</ul>
{{end}}
`))
var viewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}View {{.ID}}{{end}}
{{define "body"}}
Name: <b>{{.Name}}</b><br>
Folder: <b>{{.Folder}}</b><br>
From: <b>{{template "sessionlink" .Session.ID}}</b><br>
<h2>Environment</h2>
<ul>{{range .Env}}<li>{{.}}</li>{{end}}</ul>
{{end}}
`))
var fileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}File {{.Hash}}{{end}}
{{define "body"}}
From: <b>{{template "sessionlink" .Session.ID}}</b><br>
URI: <b>{{.URI}}</b><br>
Hash: <b>{{.Hash}}</b><br>
Error: <b>{{.Error}}</b><br>
<h3>Contents</h3>
<pre>{{.Data}}</pre>
{{end}}
`))