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}}
 `))