| // 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" |
| "fmt" |
| "go/token" |
| "html/template" |
| "io" |
| "log" |
| "net" |
| "net/http" |
| "net/http/pprof" |
| "os" |
| "path" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| rpprof "runtime/pprof" |
| "strconv" |
| "strings" |
| "sync" |
| "time" |
| |
| "golang.org/x/tools/internal/lsp/debug/tag" |
| "golang.org/x/tools/internal/lsp/protocol" |
| "golang.org/x/tools/internal/span" |
| "golang.org/x/tools/internal/telemetry/event" |
| "golang.org/x/tools/internal/telemetry/export" |
| "golang.org/x/tools/internal/telemetry/export/metric" |
| "golang.org/x/tools/internal/telemetry/export/ocagent" |
| "golang.org/x/tools/internal/telemetry/export/prometheus" |
| "golang.org/x/xerrors" |
| ) |
| |
| type instanceKeyType int |
| |
| const instanceKey = instanceKeyType(0) |
| |
| // An Instance holds all debug information associated with a gopls instance. |
| type Instance struct { |
| Logfile string |
| StartTime time.Time |
| ServerAddress string |
| DebugAddress string |
| ListenedDebugAddress string |
| Workdir string |
| OCAgentConfig string |
| |
| LogWriter io.Writer |
| |
| exporter event.Exporter |
| |
| ocagent *ocagent.Exporter |
| prometheus *prometheus.Exporter |
| rpcs *rpcs |
| traces *traces |
| State *State |
| } |
| |
| // State holds debugging information related to the server state. |
| type State struct { |
| mu sync.Mutex |
| caches objset |
| sessions objset |
| views objset |
| clients objset |
| servers objset |
| } |
| |
| type ider interface { |
| ID() string |
| } |
| |
| type objset struct { |
| objs []ider |
| } |
| |
| func (s *objset) add(elem ider) { |
| s.objs = append(s.objs, elem) |
| } |
| |
| func (s *objset) drop(elem ider) { |
| var newobjs []ider |
| for _, obj := range s.objs { |
| if obj.ID() != elem.ID() { |
| newobjs = append(newobjs, obj) |
| } |
| } |
| s.objs = newobjs |
| } |
| |
| func (s *objset) find(id string) ider { |
| for _, e := range s.objs { |
| if e.ID() == id { |
| return e |
| } |
| } |
| return nil |
| } |
| |
| // Caches returns the set of Cache objects currently being served. |
| func (st *State) Caches() []Cache { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| caches := make([]Cache, len(st.caches.objs)) |
| for i, c := range st.caches.objs { |
| caches[i] = c.(Cache) |
| } |
| return caches |
| } |
| |
| // Sessions returns the set of Session objects currently being served. |
| func (st *State) Sessions() []Session { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| sessions := make([]Session, len(st.sessions.objs)) |
| for i, s := range st.sessions.objs { |
| sessions[i] = s.(Session) |
| } |
| return sessions |
| } |
| |
| // Views returns the set of View objects currently being served. |
| func (st *State) Views() []View { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| views := make([]View, len(st.views.objs)) |
| for i, v := range st.views.objs { |
| views[i] = v.(View) |
| } |
| return views |
| } |
| |
| // Clients returns the set of Clients currently being served. |
| func (st *State) Clients() []Client { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| clients := make([]Client, len(st.clients.objs)) |
| for i, c := range st.clients.objs { |
| clients[i] = c.(Client) |
| } |
| return clients |
| } |
| |
| // Servers returns the set of Servers the instance is currently connected to. |
| func (st *State) Servers() []Server { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| servers := make([]Server, len(st.servers.objs)) |
| for i, s := range st.servers.objs { |
| servers[i] = s.(Server) |
| } |
| return servers |
| } |
| |
| // A Client is an incoming connection from a remote client. |
| type Client interface { |
| ID() string |
| Session() Session |
| DebugAddress() string |
| Logfile() string |
| ServerID() string |
| } |
| |
| // A Server is an outgoing connection to a remote LSP server. |
| type Server interface { |
| ID() string |
| DebugAddress() string |
| Logfile() string |
| ClientID() string |
| } |
| |
| // A Cache is an in-memory cache. |
| type Cache interface { |
| ID() string |
| FileSet() *token.FileSet |
| MemStats() map[reflect.Type]int |
| } |
| |
| // A Session is an LSP serving session. |
| type Session interface { |
| ID() string |
| Cache() Cache |
| Files() []*File |
| File(hash string) *File |
| } |
| |
| // A View is a root directory within a Session. |
| type View interface { |
| ID() string |
| Name() string |
| Folder() span.URI |
| Session() Session |
| } |
| |
| // A File is is a file within a session. |
| type File struct { |
| Session Session |
| URI span.URI |
| Data string |
| Error error |
| Hash string |
| } |
| |
| // AddCache adds a cache to the set being served. |
| func (st *State) AddCache(cache Cache) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.caches.add(cache) |
| } |
| |
| // DropCache drops a cache from the set being served. |
| func (st *State) DropCache(cache Cache) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.caches.drop(cache) |
| } |
| |
| // AddSession adds a session to the set being served. |
| func (st *State) AddSession(session Session) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.sessions.add(session) |
| } |
| |
| // DropSession drops a session from the set being served. |
| func (st *State) DropSession(session Session) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.sessions.drop(session) |
| } |
| |
| // AddView adds a view to the set being served. |
| func (st *State) AddView(view View) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.views.add(view) |
| } |
| |
| // DropView drops a view from the set being served. |
| func (st *State) DropView(view View) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.views.drop(view) |
| } |
| |
| // AddClient adds a client to the set being served. |
| func (st *State) AddClient(client Client) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.clients.add(client) |
| } |
| |
| // DropClient adds a client to the set being served. |
| func (st *State) DropClient(client Client) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.clients.drop(client) |
| } |
| |
| // AddServer adds a server to the set being queried. In practice, there should |
| // be at most one remote server. |
| func (st *State) AddServer(server Server) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.servers.add(server) |
| } |
| |
| // DropServer drops a server to the set being queried. |
| func (st *State) DropServer(server Server) { |
| st.mu.Lock() |
| defer st.mu.Unlock() |
| st.servers.drop(server) |
| } |
| |
| func (i *Instance) getCache(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| id := path.Base(r.URL.Path) |
| c, ok := i.State.caches.find(id).(Cache) |
| if !ok { |
| return nil |
| } |
| result := struct { |
| Cache |
| Sessions []Session |
| }{ |
| Cache: c, |
| } |
| |
| // now find all the views that belong to this session |
| for _, vd := range i.State.sessions.objs { |
| v := vd.(Session) |
| if v.Cache().ID() == id { |
| result.Sessions = append(result.Sessions, v) |
| } |
| } |
| return result |
| } |
| |
| func (i *Instance) getSession(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| id := path.Base(r.URL.Path) |
| s, ok := i.State.sessions.find(id).(Session) |
| if !ok { |
| return nil |
| } |
| result := struct { |
| Session |
| Views []View |
| }{ |
| Session: s, |
| } |
| // now find all the views that belong to this session |
| for _, vd := range i.State.views.objs { |
| v := vd.(View) |
| if v.Session().ID() == id { |
| result.Views = append(result.Views, v) |
| } |
| } |
| return result |
| } |
| |
| func (i Instance) getClient(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| id := path.Base(r.URL.Path) |
| c, ok := i.State.clients.find(id).(Client) |
| if !ok { |
| return nil |
| } |
| return c |
| } |
| |
| func (i Instance) getServer(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| id := path.Base(r.URL.Path) |
| s, ok := i.State.servers.find(id).(Server) |
| if !ok { |
| return nil |
| } |
| return s |
| } |
| |
| func (i Instance) getView(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| id := path.Base(r.URL.Path) |
| v, ok := i.State.views.find(id).(View) |
| if !ok { |
| return nil |
| } |
| return v |
| } |
| |
| func (i *Instance) getFile(r *http.Request) interface{} { |
| i.State.mu.Lock() |
| defer i.State.mu.Unlock() |
| hash := path.Base(r.URL.Path) |
| sid := path.Base(path.Dir(r.URL.Path)) |
| s, ok := i.State.sessions.find(sid).(Session) |
| if !ok { |
| return nil |
| } |
| return s.File(hash) |
| } |
| |
| func (i *Instance) getInfo(r *http.Request) interface{} { |
| buf := &bytes.Buffer{} |
| i.PrintServerInfo(r.Context(), buf) |
| return template.HTML(buf.String()) |
| } |
| |
| func getMemory(r *http.Request) interface{} { |
| var m runtime.MemStats |
| runtime.ReadMemStats(&m) |
| return m |
| } |
| |
| func init() { |
| event.SetExporter(makeGlobalExporter(os.Stderr)) |
| } |
| |
| func GetInstance(ctx context.Context) *Instance { |
| if ctx == nil { |
| return nil |
| } |
| v := ctx.Value(instanceKey) |
| if v == nil { |
| return nil |
| } |
| return v.(*Instance) |
| } |
| |
| // WithInstance creates debug instance ready for use using the supplied |
| // configuration and stores it in the returned context. |
| func WithInstance(ctx context.Context, workdir, agent string) context.Context { |
| i := &Instance{ |
| StartTime: time.Now(), |
| Workdir: workdir, |
| OCAgentConfig: agent, |
| } |
| i.LogWriter = os.Stderr |
| ocConfig := ocagent.Discover() |
| //TODO: we should not need to adjust the discovered configuration |
| ocConfig.Address = i.OCAgentConfig |
| i.ocagent = ocagent.Connect(ocConfig) |
| i.prometheus = prometheus.New() |
| i.rpcs = &rpcs{} |
| i.traces = &traces{} |
| i.State = &State{} |
| i.exporter = makeInstanceExporter(i) |
| return context.WithValue(ctx, instanceKey, i) |
| } |
| |
| // SetLogFile sets the logfile for use with this instance. |
| func (i *Instance) SetLogFile(logfile string) (func(), error) { |
| // TODO: probably a better solution for deferring closure to the caller would |
| // be for the debug instance to itself be closed, but this fixes the |
| // immediate bug of logs not being captured. |
| closeLog := func() {} |
| if logfile != "" { |
| if logfile == "auto" { |
| logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid())) |
| } |
| f, err := os.Create(logfile) |
| if err != nil { |
| return nil, fmt.Errorf("unable to create log file: %v", err) |
| } |
| closeLog = func() { |
| defer f.Close() |
| } |
| log.SetOutput(io.MultiWriter(os.Stderr, f)) |
| i.LogWriter = f |
| } |
| i.Logfile = logfile |
| return closeLog, nil |
| } |
| |
| // 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 (i *Instance) Serve(ctx context.Context) error { |
| if i.DebugAddress == "" { |
| return nil |
| } |
| listener, err := net.Listen("tcp", i.DebugAddress) |
| if err != nil { |
| return err |
| } |
| i.ListenedDebugAddress = listener.Addr().String() |
| |
| port := listener.Addr().(*net.TCPAddr).Port |
| if strings.HasSuffix(i.DebugAddress, ":0") { |
| log.Printf("debug server listening on port %d", port) |
| } |
| event.Print(ctx, "Debug serving", tag.Port.Of(port)) |
| go func() { |
| mux := http.NewServeMux() |
| mux.HandleFunc("/", render(mainTmpl, func(*http.Request) interface{} { return i })) |
| 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) |
| if i.prometheus != nil { |
| mux.HandleFunc("/metrics/", i.prometheus.Serve) |
| } |
| if i.rpcs != nil { |
| mux.HandleFunc("/rpc/", render(rpcTmpl, i.rpcs.getData)) |
| } |
| if i.traces != nil { |
| mux.HandleFunc("/trace/", render(traceTmpl, i.traces.getData)) |
| } |
| mux.HandleFunc("/cache/", render(cacheTmpl, i.getCache)) |
| mux.HandleFunc("/session/", render(sessionTmpl, i.getSession)) |
| mux.HandleFunc("/view/", render(viewTmpl, i.getView)) |
| mux.HandleFunc("/client/", render(clientTmpl, i.getClient)) |
| mux.HandleFunc("/server/", render(serverTmpl, i.getServer)) |
| mux.HandleFunc("/file/", render(fileTmpl, i.getFile)) |
| mux.HandleFunc("/info", render(infoTmpl, i.getInfo)) |
| mux.HandleFunc("/memory", render(memoryTmpl, getMemory)) |
| if err := http.Serve(listener, mux); err != nil { |
| event.Error(ctx, "Debug server failed", err) |
| return |
| } |
| event.Print(ctx, "Debug server finished") |
| }() |
| return nil |
| } |
| |
| // MonitorMemory starts recording memory statistics each second. |
| func (i *Instance) MonitorMemory(ctx context.Context) { |
| tick := time.NewTicker(time.Second) |
| nextThresholdGiB := uint64(1) |
| go func() { |
| for { |
| <-tick.C |
| var mem runtime.MemStats |
| runtime.ReadMemStats(&mem) |
| if mem.HeapAlloc < nextThresholdGiB*1<<30 { |
| continue |
| } |
| i.writeMemoryDebug(nextThresholdGiB) |
| event.Print(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir())) |
| nextThresholdGiB++ |
| } |
| }() |
| } |
| |
| func (i *Instance) writeMemoryDebug(threshold uint64) error { |
| fname := func(t string) string { |
| return fmt.Sprintf("gopls.%d-%dGiB-%s", os.Getpid(), threshold, t) |
| } |
| |
| f, err := os.Create(filepath.Join(os.TempDir(), fname("heap.pb.gz"))) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil { |
| return err |
| } |
| |
| f, err = os.Create(filepath.Join(os.TempDir(), fname("goroutines.txt"))) |
| if err != nil { |
| return err |
| } |
| defer f.Close() |
| if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func makeGlobalExporter(stderr io.Writer) event.Exporter { |
| return func(ctx context.Context, ev event.Event, tags event.TagMap) context.Context { |
| i := GetInstance(ctx) |
| |
| if ev.IsLog() { |
| // Don't log context cancellation errors. |
| if err := event.Err.Get(ev); xerrors.Is(err, context.Canceled) { |
| return ctx |
| } |
| // Make sure any log messages without an instance go to stderr. |
| if i == nil { |
| fmt.Fprintf(stderr, "%v\n", ev) |
| } |
| } |
| ctx = protocol.LogEvent(ctx, ev, tags) |
| if i == nil { |
| return ctx |
| } |
| return i.exporter(ctx, ev, tags) |
| } |
| } |
| |
| func makeInstanceExporter(i *Instance) event.Exporter { |
| exporter := func(ctx context.Context, ev event.Event, tags event.TagMap) context.Context { |
| if i.ocagent != nil { |
| ctx = i.ocagent.ProcessEvent(ctx, ev, tags) |
| } |
| if i.prometheus != nil { |
| ctx = i.prometheus.ProcessEvent(ctx, ev, tags) |
| } |
| if i.rpcs != nil { |
| ctx = i.rpcs.ProcessEvent(ctx, ev, tags) |
| } |
| if i.traces != nil { |
| ctx = i.traces.ProcessEvent(ctx, ev, tags) |
| } |
| return ctx |
| } |
| metrics := metric.Config{} |
| registerMetrics(&metrics) |
| exporter = metrics.Exporter(exporter) |
| exporter = export.Spans(exporter) |
| exporter = export.Labels(exporter) |
| return exporter |
| } |
| |
| 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 { |
| event.Error(context.Background(), "", err) |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| } |
| } |
| } |
| |
| 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 "clientlink"}}<a href="/client/{{.}}">Client {{.}}</a>{{end}} |
| {{define "serverlink"}}<a href="/server/{{.}}">Server {{.}}</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, |
| "localAddress": func(s string) string { |
| // Try to translate loopback addresses to localhost, both for cosmetics and |
| // because unspecified ipv6 addresses can break links on Windows. |
| // |
| // TODO(rfindley): In the future, it would be better not to assume the |
| // server is running on localhost, and instead construct this address using |
| // the remote host. |
| host, port, err := net.SplitHostPort(s) |
| if err != nil { |
| return s |
| } |
| ip := net.ParseIP(host) |
| if ip == nil { |
| return s |
| } |
| if ip.IsLoopback() || ip.IsUnspecified() { |
| return "localhost:" + port |
| } |
| return s |
| }, |
| }) |
| |
| var mainTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` |
| {{define "title"}}GoPls server information{{end}} |
| {{define "body"}} |
| <h2>Caches</h2> |
| <ul>{{range .State.Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul> |
| <h2>Sessions</h2> |
| <ul>{{range .State.Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul> |
| <h2>Views</h2> |
| <ul>{{range .State.Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul> |
| <h2>Clients</h2> |
| <ul>{{range .State.Clients}}<li>{{template "clientlink" .ID}}</li>{{end}}</ul> |
| <h2>Servers</h2> |
| <ul>{{range .State.Servers}}<li>{{template "serverlink" .ID}}</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> |
| <h2>memoize.Store entries</h2> |
| <ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul> |
| {{end}} |
| `)) |
| |
| var clientTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` |
| {{define "title"}}Client {{.ID}}{{end}} |
| {{define "body"}} |
| Using session: <b>{{template "sessionlink" .Session.ID}}</b><br> |
| {{if .DebugAddress}}Debug this client at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}} |
| Logfile: {{.Logfile}}<br> |
| Gopls Path: {{.GoplsPath}}<br> |
| {{end}} |
| `)) |
| |
| var serverTmpl = template.Must(template.Must(baseTemplate.Clone()).Parse(` |
| {{define "title"}}Server {{.ID}}{{end}} |
| {{define "body"}} |
| {{if .DebugAddress}}Debug this server at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}} |
| Logfile: {{.Logfile}}<br> |
| Gopls Path: {{.GoplsPath}}<br> |
| {{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}} |
| `)) |