internal/lsp: debug pages for sessions views and files

Change-Id: Id21f391bd66513615d274588ce7d1d1efe407074
Reviewed-on: https://go-review.googlesource.com/c/tools/+/179438
Run-TryBot: Ian Cottrell <iancottrell@google.com>
Reviewed-by: Rebecca Stambler <rstambler@golang.org>
This commit is contained in:
Ian Cottrell 2019-05-29 14:55:52 -04:00
parent 08e0b306e8
commit df5f646307
4 changed files with 324 additions and 5 deletions

View File

@ -8,31 +8,43 @@ import (
"crypto/sha1"
"fmt"
"go/token"
"strconv"
"sync/atomic"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span"
)
func New() source.Cache {
return &cache{
index := atomic.AddInt64(&cacheIndex, 1)
c := &cache{
id: strconv.FormatInt(index, 10),
fset: token.NewFileSet(),
}
debug.AddCache(debugCache{c})
return c
}
type cache struct {
nativeFileSystem
id string
fset *token.FileSet
}
func (c *cache) NewSession(log xlog.Logger) source.Session {
return &session{
index := atomic.AddInt64(&sessionIndex, 1)
s := &session{
cache: c,
id: strconv.FormatInt(index, 10),
log: log,
overlays: make(map[span.URI]*source.FileContent),
filesWatchMap: NewWatchMap(),
}
debug.AddSession(debugSession{s})
return s
}
func (c *cache) FileSet() *token.FileSet {
@ -44,3 +56,10 @@ func hashContents(contents []byte) string {
// This hash is used for internal identity detection only
return fmt.Sprintf("%x", sha1.Sum(contents))
}
var cacheIndex, sessionIndex, viewIndex int64
type debugCache struct{ *cache }
func (c *cache) ID() string { return c.id }
func (c debugCache) FileSet() *token.FileSet { return c.fset }

View File

@ -8,9 +8,13 @@ import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span"
@ -18,6 +22,7 @@ import (
type session struct {
cache *cache
id string
// the logger to use to communicate back with the client
log xlog.Logger
@ -40,6 +45,7 @@ func (s *session) Shutdown(ctx context.Context) {
}
s.views = nil
s.viewMap = nil
debug.DropSession(debugSession{s})
}
func (s *session) Cache() source.Cache {
@ -47,12 +53,14 @@ func (s *session) Cache() source.Cache {
}
func (s *session) NewView(name string, folder span.URI) source.View {
index := atomic.AddInt64(&viewIndex, 1)
s.viewMu.Lock()
defer s.viewMu.Unlock()
ctx := context.Background()
backgroundCtx, cancel := context.WithCancel(ctx)
v := &view{
session: s,
id: strconv.FormatInt(index, 10),
baseCtx: ctx,
backgroundCtx: backgroundCtx,
cancel: cancel,
@ -73,6 +81,7 @@ func (s *session) NewView(name string, folder span.URI) source.View {
s.views = append(s.views, v)
// we always need to drop the view map
s.viewMap = make(map[span.URI]source.View)
debug.AddView(debugView{v})
return v
}
@ -225,3 +234,58 @@ func (s *session) buildOverlay() map[string][]byte {
}
return overlays
}
type debugSession struct{ *session }
func (s debugSession) ID() string { return s.id }
func (s debugSession) Cache() debug.Cache { return debugCache{s.cache} }
func (s debugSession) Files() []*debug.File {
var files []*debug.File
seen := make(map[span.URI]*debug.File)
s.openFiles.Range(func(key interface{}, value interface{}) bool {
uri, ok := key.(span.URI)
if ok {
f := &debug.File{Session: s, URI: uri}
seen[uri] = f
files = append(files, f)
}
return true
})
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
for _, overlay := range s.overlays {
f, ok := seen[overlay.URI]
if !ok {
f = &debug.File{Session: s, URI: overlay.URI}
seen[overlay.URI] = f
files = append(files, f)
}
f.Data = string(overlay.Data)
f.Error = overlay.Error
f.Hash = overlay.Hash
}
sort.Slice(files, func(i int, j int) bool {
return files[i].URI < files[j].URI
})
return files
}
func (s debugSession) File(hash string) *debug.File {
s.overlayMu.Lock()
defer s.overlayMu.Unlock()
for _, overlay := range s.overlays {
if overlay.Hash == hash {
return &debug.File{
Session: s,
URI: overlay.URI,
Data: string(overlay.Data),
Error: overlay.Error,
Hash: overlay.Hash,
}
}
}
return &debug.File{
Session: s,
Hash: hash,
}
}

View File

@ -15,12 +15,14 @@ import (
"sync"
"golang.org/x/tools/go/packages"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/span"
)
type view struct {
session *session
id string
// mu protects all mutable state of the view.
mu sync.Mutex
@ -157,6 +159,7 @@ func (v *view) shutdown(context.Context) {
v.cancel()
v.cancel = nil
}
debug.DropView(debugView{v})
}
// Ignore checks if the given URI is a URI we ignore.
@ -395,3 +398,8 @@ func (v *view) mapFile(uri span.URI, f viewFile) {
v.filesByBase[basename] = append(v.filesByBase[basename], f)
}
}
type debugView struct{ *view }
func (v debugView) ID() string { return v.id }
func (v debugView) Session() debug.Session { return debugSession{v.session} }

View File

@ -7,29 +7,204 @@ package debug
import (
"bytes"
"context"
"go/token"
"html/template"
"log"
"net"
"net/http"
_ "net/http/pprof" // pull in the standard pprof handlers
"path"
"sync"
"golang.org/x/tools/internal/span"
)
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
}{}
)
func init() {
http.HandleFunc("/", Render(mainTmpl, nil))
http.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data }))
http.HandleFunc("/debug/", Render(debugTmpl, nil))
http.HandleFunc("/cache/", Render(cacheTmpl, getCache))
http.HandleFunc("/session/", Render(sessionTmpl, getSession))
http.HandleFunc("/view/", Render(viewTmpl, getView))
http.HandleFunc("/file/", Render(fileTmpl, getFile))
http.HandleFunc("/info", Render(infoTmpl, getInfo))
}
// 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(r *http.Request) interface{} {
buf := &bytes.Buffer{}
PrintVersionInfo(buf, true, HTML)
return template.HTML(buf.String())
}
// 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) error {
mu.Lock()
defer mu.Unlock()
if addr == "" {
return nil
}
@ -63,7 +238,7 @@ func Render(tmpl *template.Template, fun func(*http.Request) interface{}) func(h
var BaseTemplate = template.Must(template.New("").Parse(`
<html>
<head>
<title>{{template "title"}}</title>
<title>{{template "title" .}}</title>
<style>
.profile-name{
display:inline-block;
@ -82,11 +257,22 @@ 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}}
`))
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}}
`))
@ -100,6 +286,48 @@ var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls Debug pages{{end}}
{{define "body"}}
<A href="/debug/pprof">Profiling</A>
<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}}
`))