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>
diff --git a/internal/lsp/cache/cache.go b/internal/lsp/cache/cache.go
index cdb1d49..2f0e14d 100644
--- a/internal/lsp/cache/cache.go
+++ b/internal/lsp/cache/cache.go
@@ -8,31 +8,43 @@
"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 @@
// 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 }
diff --git a/internal/lsp/cache/session.go b/internal/lsp/cache/session.go
index 57735b2..920eac2 100644
--- a/internal/lsp/cache/session.go
+++ b/internal/lsp/cache/session.go
@@ -8,9 +8,13 @@
"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 @@
type session struct {
cache *cache
+ id string
// the logger to use to communicate back with the client
log xlog.Logger
@@ -40,6 +45,7 @@
}
s.views = nil
s.viewMap = nil
+ debug.DropSession(debugSession{s})
}
func (s *session) Cache() source.Cache {
@@ -47,12 +53,14 @@
}
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 @@
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 @@
}
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,
+ }
+}
diff --git a/internal/lsp/cache/view.go b/internal/lsp/cache/view.go
index 96983b1..b2bb3e6 100644
--- a/internal/lsp/cache/view.go
+++ b/internal/lsp/cache/view.go
@@ -15,12 +15,14 @@
"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 @@
v.cancel()
v.cancel = nil
}
+ debug.DropView(debugView{v})
}
// Ignore checks if the given URI is a URI we ignore.
@@ -395,3 +398,8 @@
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} }
diff --git a/internal/lsp/debug/serve.go b/internal/lsp/debug/serve.go
index c5a1bbd..3ea564e 100644
--- a/internal/lsp/debug/serve.go
+++ b/internal/lsp/debug/serve.go
@@ -7,29 +7,204 @@
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 @@
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 @@
{{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 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}}
`))