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" "crypto/sha1"
"fmt" "fmt"
"go/token" "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/source"
"golang.org/x/tools/internal/lsp/xlog" "golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
func New() source.Cache { func New() source.Cache {
return &cache{ index := atomic.AddInt64(&cacheIndex, 1)
c := &cache{
id: strconv.FormatInt(index, 10),
fset: token.NewFileSet(), fset: token.NewFileSet(),
} }
debug.AddCache(debugCache{c})
return c
} }
type cache struct { type cache struct {
nativeFileSystem nativeFileSystem
id string
fset *token.FileSet fset *token.FileSet
} }
func (c *cache) NewSession(log xlog.Logger) source.Session { func (c *cache) NewSession(log xlog.Logger) source.Session {
return &session{ index := atomic.AddInt64(&sessionIndex, 1)
s := &session{
cache: c, cache: c,
id: strconv.FormatInt(index, 10),
log: log, log: log,
overlays: make(map[span.URI]*source.FileContent), overlays: make(map[span.URI]*source.FileContent),
filesWatchMap: NewWatchMap(), filesWatchMap: NewWatchMap(),
} }
debug.AddSession(debugSession{s})
return s
} }
func (c *cache) FileSet() *token.FileSet { func (c *cache) FileSet() *token.FileSet {
@ -44,3 +56,10 @@ func hashContents(contents []byte) string {
// This hash is used for internal identity detection only // This hash is used for internal identity detection only
return fmt.Sprintf("%x", sha1.Sum(contents)) 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" "context"
"fmt" "fmt"
"os" "os"
"sort"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"golang.org/x/tools/internal/lsp/debug"
"golang.org/x/tools/internal/lsp/source" "golang.org/x/tools/internal/lsp/source"
"golang.org/x/tools/internal/lsp/xlog" "golang.org/x/tools/internal/lsp/xlog"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
@ -18,6 +22,7 @@ import (
type session struct { type session struct {
cache *cache cache *cache
id string
// the logger to use to communicate back with the client // the logger to use to communicate back with the client
log xlog.Logger log xlog.Logger
@ -40,6 +45,7 @@ func (s *session) Shutdown(ctx context.Context) {
} }
s.views = nil s.views = nil
s.viewMap = nil s.viewMap = nil
debug.DropSession(debugSession{s})
} }
func (s *session) Cache() source.Cache { 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 { func (s *session) NewView(name string, folder span.URI) source.View {
index := atomic.AddInt64(&viewIndex, 1)
s.viewMu.Lock() s.viewMu.Lock()
defer s.viewMu.Unlock() defer s.viewMu.Unlock()
ctx := context.Background() ctx := context.Background()
backgroundCtx, cancel := context.WithCancel(ctx) backgroundCtx, cancel := context.WithCancel(ctx)
v := &view{ v := &view{
session: s, session: s,
id: strconv.FormatInt(index, 10),
baseCtx: ctx, baseCtx: ctx,
backgroundCtx: backgroundCtx, backgroundCtx: backgroundCtx,
cancel: cancel, cancel: cancel,
@ -73,6 +81,7 @@ func (s *session) NewView(name string, folder span.URI) source.View {
s.views = append(s.views, v) s.views = append(s.views, v)
// we always need to drop the view map // we always need to drop the view map
s.viewMap = make(map[span.URI]source.View) s.viewMap = make(map[span.URI]source.View)
debug.AddView(debugView{v})
return v return v
} }
@ -225,3 +234,58 @@ func (s *session) buildOverlay() map[string][]byte {
} }
return overlays 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" "sync"
"golang.org/x/tools/go/packages" "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/lsp/source"
"golang.org/x/tools/internal/span" "golang.org/x/tools/internal/span"
) )
type view struct { type view struct {
session *session session *session
id string
// mu protects all mutable state of the view. // mu protects all mutable state of the view.
mu sync.Mutex mu sync.Mutex
@ -157,6 +159,7 @@ func (v *view) shutdown(context.Context) {
v.cancel() v.cancel()
v.cancel = nil v.cancel = nil
} }
debug.DropView(debugView{v})
} }
// Ignore checks if the given URI is a URI we ignore. // 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) 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 ( import (
"bytes" "bytes"
"context" "context"
"go/token"
"html/template" "html/template"
"log" "log"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof" // pull in the standard pprof handlers _ "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() { 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("/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)) 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{} { func getInfo(r *http.Request) interface{} {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
PrintVersionInfo(buf, true, HTML) PrintVersionInfo(buf, true, HTML)
return template.HTML(buf.String()) 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. // 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 // It also logs the port the server starts on, to allow for :0 auto assigned
// ports. // ports.
func Serve(ctx context.Context, addr string) error { func Serve(ctx context.Context, addr string) error {
mu.Lock()
defer mu.Unlock()
if addr == "" { if addr == "" {
return nil 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(` var BaseTemplate = template.Must(template.New("").Parse(`
<html> <html>
<head> <head>
<title>{{template "title"}}</title> <title>{{template "title" .}}</title>
<style> <style>
.profile-name{ .profile-name{
display:inline-block; display:inline-block;
@ -82,11 +257,22 @@ Unknown page
{{end}} {{end}}
</body> </body>
</html> </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(` var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls server information{{end}} {{define "title"}}GoPls server information{{end}}
{{define "body"}} {{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}} {{end}}
`)) `))
@ -100,6 +286,48 @@ var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
{{define "title"}}GoPls Debug pages{{end}} {{define "title"}}GoPls Debug pages{{end}}
{{define "body"}} {{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}} {{end}}
`)) `))