1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package debug
6
7import (
8	"archive/zip"
9	"bytes"
10	"context"
11	"fmt"
12	"html/template"
13	"io"
14	stdlog "log"
15	"net"
16	"net/http"
17	"net/http/pprof"
18	"os"
19	"path"
20	"path/filepath"
21	"runtime"
22	rpprof "runtime/pprof"
23	"strconv"
24	"strings"
25	"sync"
26	"time"
27
28	"golang.org/x/tools/internal/event"
29	"golang.org/x/tools/internal/event/core"
30	"golang.org/x/tools/internal/event/export"
31	"golang.org/x/tools/internal/event/export/metric"
32	"golang.org/x/tools/internal/event/export/ocagent"
33	"golang.org/x/tools/internal/event/export/prometheus"
34	"golang.org/x/tools/internal/event/keys"
35	"golang.org/x/tools/internal/event/label"
36	"golang.org/x/tools/internal/lsp/cache"
37	"golang.org/x/tools/internal/lsp/debug/log"
38	"golang.org/x/tools/internal/lsp/debug/tag"
39	"golang.org/x/tools/internal/lsp/protocol"
40	"golang.org/x/tools/internal/lsp/source"
41	errors "golang.org/x/xerrors"
42)
43
44type contextKeyType int
45
46const (
47	instanceKey contextKeyType = iota
48	traceKey
49)
50
51// An Instance holds all debug information associated with a gopls instance.
52type Instance struct {
53	Logfile              string
54	StartTime            time.Time
55	ServerAddress        string
56	DebugAddress         string
57	ListenedDebugAddress string
58	Workdir              string
59	OCAgentConfig        string
60
61	LogWriter io.Writer
62
63	exporter event.Exporter
64
65	ocagent    *ocagent.Exporter
66	prometheus *prometheus.Exporter
67	rpcs       *Rpcs
68	traces     *traces
69	State      *State
70}
71
72// State holds debugging information related to the server state.
73type State struct {
74	mu      sync.Mutex
75	clients []*Client
76	servers []*Server
77}
78
79// Caches returns the set of Cache objects currently being served.
80func (st *State) Caches() []*cache.Cache {
81	var caches []*cache.Cache
82	seen := make(map[string]struct{})
83	for _, client := range st.Clients() {
84		cache, ok := client.Session.Cache().(*cache.Cache)
85		if !ok {
86			continue
87		}
88		if _, found := seen[cache.ID()]; found {
89			continue
90		}
91		seen[cache.ID()] = struct{}{}
92		caches = append(caches, cache)
93	}
94	return caches
95}
96
97// Cache returns the Cache that matches the supplied id.
98func (st *State) Cache(id string) *cache.Cache {
99	for _, c := range st.Caches() {
100		if c.ID() == id {
101			return c
102		}
103	}
104	return nil
105}
106
107// Sessions returns the set of Session objects currently being served.
108func (st *State) Sessions() []*cache.Session {
109	var sessions []*cache.Session
110	for _, client := range st.Clients() {
111		sessions = append(sessions, client.Session)
112	}
113	return sessions
114}
115
116// Session returns the Session that matches the supplied id.
117func (st *State) Session(id string) *cache.Session {
118	for _, s := range st.Sessions() {
119		if s.ID() == id {
120			return s
121		}
122	}
123	return nil
124}
125
126// Views returns the set of View objects currently being served.
127func (st *State) Views() []*cache.View {
128	var views []*cache.View
129	for _, s := range st.Sessions() {
130		for _, v := range s.Views() {
131			if cv, ok := v.(*cache.View); ok {
132				views = append(views, cv)
133			}
134		}
135	}
136	return views
137}
138
139// View returns the View that matches the supplied id.
140func (st *State) View(id string) *cache.View {
141	for _, v := range st.Views() {
142		if v.ID() == id {
143			return v
144		}
145	}
146	return nil
147}
148
149// Clients returns the set of Clients currently being served.
150func (st *State) Clients() []*Client {
151	st.mu.Lock()
152	defer st.mu.Unlock()
153	clients := make([]*Client, len(st.clients))
154	copy(clients, st.clients)
155	return clients
156}
157
158// Client returns the Client matching the supplied id.
159func (st *State) Client(id string) *Client {
160	for _, c := range st.Clients() {
161		if c.Session.ID() == id {
162			return c
163		}
164	}
165	return nil
166}
167
168// Servers returns the set of Servers the instance is currently connected to.
169func (st *State) Servers() []*Server {
170	st.mu.Lock()
171	defer st.mu.Unlock()
172	servers := make([]*Server, len(st.servers))
173	copy(servers, st.servers)
174	return servers
175}
176
177// A Client is an incoming connection from a remote client.
178type Client struct {
179	Session      *cache.Session
180	DebugAddress string
181	Logfile      string
182	GoplsPath    string
183	ServerID     string
184}
185
186// A Server is an outgoing connection to a remote LSP server.
187type Server struct {
188	ID           string
189	DebugAddress string
190	Logfile      string
191	GoplsPath    string
192	ClientID     string
193}
194
195// AddClient adds a client to the set being served.
196func (st *State) addClient(session *cache.Session) {
197	st.mu.Lock()
198	defer st.mu.Unlock()
199	st.clients = append(st.clients, &Client{Session: session})
200}
201
202// DropClient removes a client from the set being served.
203func (st *State) dropClient(session source.Session) {
204	st.mu.Lock()
205	defer st.mu.Unlock()
206	for i, c := range st.clients {
207		if c.Session == session {
208			copy(st.clients[i:], st.clients[i+1:])
209			st.clients[len(st.clients)-1] = nil
210			st.clients = st.clients[:len(st.clients)-1]
211			return
212		}
213	}
214}
215
216// AddServer adds a server to the set being queried. In practice, there should
217// be at most one remote server.
218func (st *State) addServer(server *Server) {
219	st.mu.Lock()
220	defer st.mu.Unlock()
221	st.servers = append(st.servers, server)
222}
223
224// DropServer drops a server from the set being queried.
225func (st *State) dropServer(id string) {
226	st.mu.Lock()
227	defer st.mu.Unlock()
228	for i, s := range st.servers {
229		if s.ID == id {
230			copy(st.servers[i:], st.servers[i+1:])
231			st.servers[len(st.servers)-1] = nil
232			st.servers = st.servers[:len(st.servers)-1]
233			return
234		}
235	}
236}
237
238// an http.ResponseWriter that filters writes
239type filterResponse struct {
240	w    http.ResponseWriter
241	edit func([]byte) []byte
242}
243
244func (c filterResponse) Header() http.Header {
245	return c.w.Header()
246}
247
248func (c filterResponse) Write(buf []byte) (int, error) {
249	ans := c.edit(buf)
250	return c.w.Write(ans)
251}
252
253func (c filterResponse) WriteHeader(n int) {
254	c.w.WriteHeader(n)
255}
256
257// replace annoying nuls by spaces
258func cmdline(w http.ResponseWriter, r *http.Request) {
259	fake := filterResponse{
260		w: w,
261		edit: func(buf []byte) []byte {
262			return bytes.ReplaceAll(buf, []byte{0}, []byte{' '})
263		},
264	}
265	pprof.Cmdline(fake, r)
266}
267
268func (i *Instance) getCache(r *http.Request) interface{} {
269	return i.State.Cache(path.Base(r.URL.Path))
270}
271
272func (i *Instance) getSession(r *http.Request) interface{} {
273	return i.State.Session(path.Base(r.URL.Path))
274}
275
276func (i Instance) getClient(r *http.Request) interface{} {
277	return i.State.Client(path.Base(r.URL.Path))
278}
279
280func (i Instance) getServer(r *http.Request) interface{} {
281	i.State.mu.Lock()
282	defer i.State.mu.Unlock()
283	id := path.Base(r.URL.Path)
284	for _, s := range i.State.servers {
285		if s.ID == id {
286			return s
287		}
288	}
289	return nil
290}
291
292func (i Instance) getView(r *http.Request) interface{} {
293	return i.State.View(path.Base(r.URL.Path))
294}
295
296func (i *Instance) getFile(r *http.Request) interface{} {
297	identifier := path.Base(r.URL.Path)
298	sid := path.Base(path.Dir(r.URL.Path))
299	s := i.State.Session(sid)
300	if s == nil {
301		return nil
302	}
303	for _, o := range s.Overlays() {
304		if o.FileIdentity().Hash == identifier {
305			return o
306		}
307	}
308	return nil
309}
310
311func (i *Instance) getInfo(r *http.Request) interface{} {
312	buf := &bytes.Buffer{}
313	i.PrintServerInfo(r.Context(), buf)
314	return template.HTML(buf.String())
315}
316
317func getMemory(r *http.Request) interface{} {
318	var m runtime.MemStats
319	runtime.ReadMemStats(&m)
320	return m
321}
322
323func init() {
324	event.SetExporter(makeGlobalExporter(os.Stderr))
325}
326
327func GetInstance(ctx context.Context) *Instance {
328	if ctx == nil {
329		return nil
330	}
331	v := ctx.Value(instanceKey)
332	if v == nil {
333		return nil
334	}
335	return v.(*Instance)
336}
337
338// WithInstance creates debug instance ready for use using the supplied
339// configuration and stores it in the returned context.
340func WithInstance(ctx context.Context, workdir, agent string) context.Context {
341	i := &Instance{
342		StartTime:     time.Now(),
343		Workdir:       workdir,
344		OCAgentConfig: agent,
345	}
346	i.LogWriter = os.Stderr
347	ocConfig := ocagent.Discover()
348	//TODO: we should not need to adjust the discovered configuration
349	ocConfig.Address = i.OCAgentConfig
350	i.ocagent = ocagent.Connect(ocConfig)
351	i.prometheus = prometheus.New()
352	i.rpcs = &Rpcs{}
353	i.traces = &traces{}
354	i.State = &State{}
355	i.exporter = makeInstanceExporter(i)
356	return context.WithValue(ctx, instanceKey, i)
357}
358
359// SetLogFile sets the logfile for use with this instance.
360func (i *Instance) SetLogFile(logfile string, isDaemon bool) (func(), error) {
361	// TODO: probably a better solution for deferring closure to the caller would
362	// be for the debug instance to itself be closed, but this fixes the
363	// immediate bug of logs not being captured.
364	closeLog := func() {}
365	if logfile != "" {
366		if logfile == "auto" {
367			if isDaemon {
368				logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-daemon-%d.log", os.Getpid()))
369			} else {
370				logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid()))
371			}
372		}
373		f, err := os.Create(logfile)
374		if err != nil {
375			return nil, errors.Errorf("unable to create log file: %w", err)
376		}
377		closeLog = func() {
378			defer f.Close()
379		}
380		stdlog.SetOutput(io.MultiWriter(os.Stderr, f))
381		i.LogWriter = f
382	}
383	i.Logfile = logfile
384	return closeLog, nil
385}
386
387// Serve starts and runs a debug server in the background.
388// It also logs the port the server starts on, to allow for :0 auto assigned
389// ports.
390func (i *Instance) Serve(ctx context.Context) error {
391	stdlog.SetFlags(stdlog.Lshortfile)
392	if i.DebugAddress == "" {
393		return nil
394	}
395	listener, err := net.Listen("tcp", i.DebugAddress)
396	if err != nil {
397		return err
398	}
399	i.ListenedDebugAddress = listener.Addr().String()
400
401	port := listener.Addr().(*net.TCPAddr).Port
402	if strings.HasSuffix(i.DebugAddress, ":0") {
403		stdlog.Printf("debug server listening at http://localhost:%d", port)
404	}
405	event.Log(ctx, "Debug serving", tag.Port.Of(port))
406	go func() {
407		mux := http.NewServeMux()
408		mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i }))
409		mux.HandleFunc("/debug/", render(DebugTmpl, nil))
410		mux.HandleFunc("/debug/pprof/", pprof.Index)
411		mux.HandleFunc("/debug/pprof/cmdline", cmdline)
412		mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
413		mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
414		mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
415		if i.prometheus != nil {
416			mux.HandleFunc("/metrics/", i.prometheus.Serve)
417		}
418		if i.rpcs != nil {
419			mux.HandleFunc("/rpc/", render(RPCTmpl, i.rpcs.getData))
420		}
421		if i.traces != nil {
422			mux.HandleFunc("/trace/", render(TraceTmpl, i.traces.getData))
423		}
424		mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache))
425		mux.HandleFunc("/session/", render(SessionTmpl, i.getSession))
426		mux.HandleFunc("/view/", render(ViewTmpl, i.getView))
427		mux.HandleFunc("/client/", render(ClientTmpl, i.getClient))
428		mux.HandleFunc("/server/", render(ServerTmpl, i.getServer))
429		mux.HandleFunc("/file/", render(FileTmpl, i.getFile))
430		mux.HandleFunc("/info", render(InfoTmpl, i.getInfo))
431		mux.HandleFunc("/memory", render(MemoryTmpl, getMemory))
432		if err := http.Serve(listener, mux); err != nil {
433			event.Error(ctx, "Debug server failed", err)
434			return
435		}
436		event.Log(ctx, "Debug server finished")
437	}()
438	return nil
439}
440
441// MonitorMemory starts recording memory statistics each second.
442func (i *Instance) MonitorMemory(ctx context.Context) {
443	tick := time.NewTicker(time.Second)
444	nextThresholdGiB := uint64(1)
445	go func() {
446		for {
447			<-tick.C
448			var mem runtime.MemStats
449			runtime.ReadMemStats(&mem)
450			if mem.HeapAlloc < nextThresholdGiB*1<<30 {
451				continue
452			}
453			if err := i.writeMemoryDebug(nextThresholdGiB, true); err != nil {
454				event.Error(ctx, "writing memory debug info", err)
455			}
456			if err := i.writeMemoryDebug(nextThresholdGiB, false); err != nil {
457				event.Error(ctx, "writing memory debug info", err)
458			}
459			event.Log(ctx, fmt.Sprintf("Wrote memory usage debug info to %v", os.TempDir()))
460			nextThresholdGiB++
461		}
462	}()
463}
464
465func (i *Instance) writeMemoryDebug(threshold uint64, withNames bool) error {
466	suffix := "withnames"
467	if !withNames {
468		suffix = "nonames"
469	}
470
471	filename := fmt.Sprintf("gopls.%d-%dGiB-%s.zip", os.Getpid(), threshold, suffix)
472	zipf, err := os.OpenFile(filepath.Join(os.TempDir(), filename), os.O_CREATE|os.O_RDWR, 0644)
473	if err != nil {
474		return err
475	}
476	zipw := zip.NewWriter(zipf)
477
478	f, err := zipw.Create("heap.pb.gz")
479	if err != nil {
480		return err
481	}
482	if err := rpprof.Lookup("heap").WriteTo(f, 0); err != nil {
483		return err
484	}
485
486	f, err = zipw.Create("goroutines.txt")
487	if err != nil {
488		return err
489	}
490	if err := rpprof.Lookup("goroutine").WriteTo(f, 1); err != nil {
491		return err
492	}
493
494	for _, cache := range i.State.Caches() {
495		cf, err := zipw.Create(fmt.Sprintf("cache-%v.html", cache.ID()))
496		if err != nil {
497			return err
498		}
499		if _, err := cf.Write([]byte(cache.PackageStats(withNames))); err != nil {
500			return err
501		}
502	}
503
504	if err := zipw.Close(); err != nil {
505		return err
506	}
507	return zipf.Close()
508}
509
510func makeGlobalExporter(stderr io.Writer) event.Exporter {
511	p := export.Printer{}
512	var pMu sync.Mutex
513	return func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
514		i := GetInstance(ctx)
515
516		if event.IsLog(ev) {
517			// Don't log context cancellation errors.
518			if err := keys.Err.Get(ev); errors.Is(err, context.Canceled) {
519				return ctx
520			}
521			// Make sure any log messages without an instance go to stderr.
522			if i == nil {
523				pMu.Lock()
524				p.WriteEvent(stderr, ev, lm)
525				pMu.Unlock()
526			}
527			level := log.LabeledLevel(lm)
528			// Exclude trace logs from LSP logs.
529			if level < log.Trace {
530				ctx = protocol.LogEvent(ctx, ev, lm, messageType(level))
531			}
532		}
533		if i == nil {
534			return ctx
535		}
536		return i.exporter(ctx, ev, lm)
537	}
538}
539
540func messageType(l log.Level) protocol.MessageType {
541	switch l {
542	case log.Error:
543		return protocol.Error
544	case log.Warning:
545		return protocol.Warning
546	case log.Debug:
547		return protocol.Log
548	}
549	return protocol.Info
550}
551
552func makeInstanceExporter(i *Instance) event.Exporter {
553	exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context {
554		if i.ocagent != nil {
555			ctx = i.ocagent.ProcessEvent(ctx, ev, lm)
556		}
557		if i.prometheus != nil {
558			ctx = i.prometheus.ProcessEvent(ctx, ev, lm)
559		}
560		if i.rpcs != nil {
561			ctx = i.rpcs.ProcessEvent(ctx, ev, lm)
562		}
563		if i.traces != nil {
564			ctx = i.traces.ProcessEvent(ctx, ev, lm)
565		}
566		if event.IsLog(ev) {
567			if s := cache.KeyCreateSession.Get(ev); s != nil {
568				i.State.addClient(s)
569			}
570			if sid := tag.NewServer.Get(ev); sid != "" {
571				i.State.addServer(&Server{
572					ID:           sid,
573					Logfile:      tag.Logfile.Get(ev),
574					DebugAddress: tag.DebugAddress.Get(ev),
575					GoplsPath:    tag.GoplsPath.Get(ev),
576					ClientID:     tag.ClientID.Get(ev),
577				})
578			}
579			if s := cache.KeyShutdownSession.Get(ev); s != nil {
580				i.State.dropClient(s)
581			}
582			if sid := tag.EndServer.Get(ev); sid != "" {
583				i.State.dropServer(sid)
584			}
585			if s := cache.KeyUpdateSession.Get(ev); s != nil {
586				if c := i.State.Client(s.ID()); c != nil {
587					c.DebugAddress = tag.DebugAddress.Get(ev)
588					c.Logfile = tag.Logfile.Get(ev)
589					c.ServerID = tag.ServerID.Get(ev)
590					c.GoplsPath = tag.GoplsPath.Get(ev)
591				}
592			}
593		}
594		return ctx
595	}
596	// StdTrace must be above export.Spans below (by convention, export
597	// middleware applies its wrapped exporter last).
598	exporter = StdTrace(exporter)
599	metrics := metric.Config{}
600	registerMetrics(&metrics)
601	exporter = metrics.Exporter(exporter)
602	exporter = export.Spans(exporter)
603	exporter = export.Labels(exporter)
604	return exporter
605}
606
607type dataFunc func(*http.Request) interface{}
608
609func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) {
610	return func(w http.ResponseWriter, r *http.Request) {
611		var data interface{}
612		if fun != nil {
613			data = fun(r)
614		}
615		if err := tmpl.Execute(w, data); err != nil {
616			event.Error(context.Background(), "", err)
617			http.Error(w, err.Error(), http.StatusInternalServerError)
618		}
619	}
620}
621
622func commas(s string) string {
623	for i := len(s); i > 3; {
624		i -= 3
625		s = s[:i] + "," + s[i:]
626	}
627	return s
628}
629
630func fuint64(v uint64) string {
631	return commas(strconv.FormatUint(v, 10))
632}
633
634func fuint32(v uint32) string {
635	return commas(strconv.FormatUint(uint64(v), 10))
636}
637
638func fcontent(v []byte) string {
639	return string(v)
640}
641
642var BaseTemplate = template.Must(template.New("").Parse(`
643<html>
644<head>
645<title>{{template "title" .}}</title>
646<style>
647.profile-name{
648	display:inline-block;
649	width:6rem;
650}
651td.value {
652  text-align: right;
653}
654ul.events {
655	list-style-type: none;
656}
657
658</style>
659{{block "head" .}}{{end}}
660</head>
661<body>
662<a href="/">Main</a>
663<a href="/info">Info</a>
664<a href="/memory">Memory</a>
665<a href="/metrics">Metrics</a>
666<a href="/rpc">RPC</a>
667<a href="/trace">Trace</a>
668<hr>
669<h1>{{template "title" .}}</h1>
670{{block "body" .}}
671Unknown page
672{{end}}
673</body>
674</html>
675
676{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}}
677{{define "clientlink"}}<a href="/client/{{.}}">Client {{.}}</a>{{end}}
678{{define "serverlink"}}<a href="/server/{{.}}">Server {{.}}</a>{{end}}
679{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}}
680{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}}
681{{define "filelink"}}<a href="/file/{{.Session}}/{{.FileIdentity.Hash}}">{{.FileIdentity.URI}}</a>{{end}}
682`)).Funcs(template.FuncMap{
683	"fuint64":  fuint64,
684	"fuint32":  fuint32,
685	"fcontent": fcontent,
686	"localAddress": func(s string) string {
687		// Try to translate loopback addresses to localhost, both for cosmetics and
688		// because unspecified ipv6 addresses can break links on Windows.
689		//
690		// TODO(rfindley): In the future, it would be better not to assume the
691		// server is running on localhost, and instead construct this address using
692		// the remote host.
693		host, port, err := net.SplitHostPort(s)
694		if err != nil {
695			return s
696		}
697		ip := net.ParseIP(host)
698		if ip == nil {
699			return s
700		}
701		if ip.IsLoopback() || ip.IsUnspecified() {
702			return "localhost:" + port
703		}
704		return s
705	},
706	"options": func(s *cache.Session) []string {
707		return showOptions(s.Options())
708	},
709})
710
711var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
712{{define "title"}}GoPls server information{{end}}
713{{define "body"}}
714<h2>Caches</h2>
715<ul>{{range .State.Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul>
716<h2>Sessions</h2>
717<ul>{{range .State.Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul>
718<h2>Views</h2>
719<ul>{{range .State.Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul>
720<h2>Clients</h2>
721<ul>{{range .State.Clients}}<li>{{template "clientlink" .Session.ID}}</li>{{end}}</ul>
722<h2>Servers</h2>
723<ul>{{range .State.Servers}}<li>{{template "serverlink" .ID}}</li>{{end}}</ul>
724{{end}}
725`))
726
727var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
728{{define "title"}}GoPls version information{{end}}
729{{define "body"}}
730{{.}}
731{{end}}
732`))
733
734var MemoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
735{{define "title"}}GoPls memory usage{{end}}
736{{define "head"}}<meta http-equiv="refresh" content="5">{{end}}
737{{define "body"}}
738<h2>Stats</h2>
739<table>
740<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr>
741<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr>
742<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr>
743<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr>
744<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr>
745<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr>
746<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr>
747<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr>
748<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr>
749<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr>
750<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr>
751<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr>
752<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr>
753<tr><td class="label">GC metadata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr>
754<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr>
755</table>
756<h2>By size</h2>
757<table>
758<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr>
759{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}}
760</table>
761{{end}}
762`))
763
764var DebugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
765{{define "title"}}GoPls Debug pages{{end}}
766{{define "body"}}
767<a href="/debug/pprof">Profiling</a>
768{{end}}
769`))
770
771var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
772{{define "title"}}Cache {{.ID}}{{end}}
773{{define "body"}}
774<h2>memoize.Store entries</h2>
775<ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul>
776<h2>Per-package usage - not accurate, for guidance only</h2>
777{{.PackageStats true}}
778{{end}}
779`))
780
781var ClientTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
782{{define "title"}}Client {{.Session.ID}}{{end}}
783{{define "body"}}
784Using session: <b>{{template "sessionlink" .Session.ID}}</b><br>
785{{if .DebugAddress}}Debug this client at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}}
786Logfile: {{.Logfile}}<br>
787Gopls Path: {{.GoplsPath}}<br>
788{{end}}
789`))
790
791var ServerTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
792{{define "title"}}Server {{.ID}}{{end}}
793{{define "body"}}
794{{if .DebugAddress}}Debug this server at: <a href="http://{{localAddress .DebugAddress}}">{{localAddress .DebugAddress}}</a><br>{{end}}
795Logfile: {{.Logfile}}<br>
796Gopls Path: {{.GoplsPath}}<br>
797{{end}}
798`))
799
800var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
801{{define "title"}}Session {{.ID}}{{end}}
802{{define "body"}}
803From: <b>{{template "cachelink" .Cache.ID}}</b><br>
804<h2>Views</h2>
805<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul>
806<h2>Overlays</h2>
807<ul>{{range .Overlays}}<li>{{template "filelink" .}}</li>{{end}}</ul>
808<h2>Options</h2>
809{{range options .}}<p>{{.}}{{end}}
810{{end}}
811`))
812
813var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
814{{define "title"}}View {{.ID}}{{end}}
815{{define "body"}}
816Name: <b>{{.Name}}</b><br>
817Folder: <b>{{.Folder}}</b><br>
818From: <b>{{template "sessionlink" .Session.ID}}</b><br>
819<h2>Environment</h2>
820<ul>{{range .Options.Env}}<li>{{.}}</li>{{end}}</ul>
821{{end}}
822`))
823
824var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(`
825{{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}}
826{{define "body"}}
827{{with .}}
828	From: <b>{{template "sessionlink" .Session}}</b><br>
829	URI: <b>{{.URI}}</b><br>
830	Identifier: <b>{{.FileIdentity.Hash}}</b><br>
831	Version: <b>{{.Version}}</b><br>
832	Kind: <b>{{.Kind}}</b><br>
833{{end}}
834<h3>Contents</h3>
835<pre>{{fcontent .Read}}</pre>
836{{end}}
837`))
838