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