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 "bytes" 9 "context" 10 "go/token" 11 "html/template" 12 "log" 13 "net" 14 "net/http" 15 "net/http/pprof" 16 _ "net/http/pprof" // pull in the standard pprof handlers 17 "path" 18 "runtime" 19 "strconv" 20 "sync" 21 22 "golang.org/x/tools/internal/span" 23) 24 25type Cache interface { 26 ID() string 27 FileSet() *token.FileSet 28} 29 30type Session interface { 31 ID() string 32 Cache() Cache 33 Files() []*File 34 File(hash string) *File 35} 36 37type View interface { 38 ID() string 39 Name() string 40 Folder() span.URI 41 Session() Session 42} 43 44type File struct { 45 Session Session 46 URI span.URI 47 Data string 48 Error error 49 Hash string 50} 51 52var ( 53 mu sync.Mutex 54 data = struct { 55 Caches []Cache 56 Sessions []Session 57 Views []View 58 }{} 59) 60 61// AddCache adds a cache to the set being served 62func AddCache(cache Cache) { 63 mu.Lock() 64 defer mu.Unlock() 65 data.Caches = append(data.Caches, cache) 66} 67 68// DropCache drops a cache from the set being served 69func DropCache(cache Cache) { 70 mu.Lock() 71 defer mu.Unlock() 72 //find and remove the cache 73 if i, _ := findCache(cache.ID()); i >= 0 { 74 copy(data.Caches[i:], data.Caches[i+1:]) 75 data.Caches[len(data.Caches)-1] = nil 76 data.Caches = data.Caches[:len(data.Caches)-1] 77 } 78} 79 80func findCache(id string) (int, Cache) { 81 for i, c := range data.Caches { 82 if c.ID() == id { 83 return i, c 84 } 85 } 86 return -1, nil 87} 88 89func getCache(r *http.Request) interface{} { 90 mu.Lock() 91 defer mu.Unlock() 92 id := path.Base(r.URL.Path) 93 result := struct { 94 Cache 95 Sessions []Session 96 }{} 97 _, result.Cache = findCache(id) 98 99 // now find all the views that belong to this session 100 for _, v := range data.Sessions { 101 if v.Cache().ID() == id { 102 result.Sessions = append(result.Sessions, v) 103 } 104 } 105 return result 106} 107 108func findSession(id string) Session { 109 for _, c := range data.Sessions { 110 if c.ID() == id { 111 return c 112 } 113 } 114 return nil 115} 116 117func getSession(r *http.Request) interface{} { 118 mu.Lock() 119 defer mu.Unlock() 120 id := path.Base(r.URL.Path) 121 result := struct { 122 Session 123 Views []View 124 }{ 125 Session: findSession(id), 126 } 127 // now find all the views that belong to this session 128 for _, v := range data.Views { 129 if v.Session().ID() == id { 130 result.Views = append(result.Views, v) 131 } 132 } 133 return result 134} 135 136func findView(id string) View { 137 for _, c := range data.Views { 138 if c.ID() == id { 139 return c 140 } 141 } 142 return nil 143} 144 145func getView(r *http.Request) interface{} { 146 mu.Lock() 147 defer mu.Unlock() 148 id := path.Base(r.URL.Path) 149 return findView(id) 150} 151 152func getFile(r *http.Request) interface{} { 153 mu.Lock() 154 defer mu.Unlock() 155 hash := path.Base(r.URL.Path) 156 sid := path.Base(path.Dir(r.URL.Path)) 157 session := findSession(sid) 158 return session.File(hash) 159} 160 161func getInfo(r *http.Request) interface{} { 162 buf := &bytes.Buffer{} 163 PrintVersionInfo(buf, true, HTML) 164 return template.HTML(buf.String()) 165} 166 167func getMemory(r *http.Request) interface{} { 168 var m runtime.MemStats 169 runtime.ReadMemStats(&m) 170 return m 171} 172 173// AddSession adds a session to the set being served 174func AddSession(session Session) { 175 mu.Lock() 176 defer mu.Unlock() 177 data.Sessions = append(data.Sessions, session) 178} 179 180// DropSession drops a session from the set being served 181func DropSession(session Session) { 182 mu.Lock() 183 defer mu.Unlock() 184 //find and remove the session 185} 186 187// AddView adds a view to the set being served 188func AddView(view View) { 189 mu.Lock() 190 defer mu.Unlock() 191 data.Views = append(data.Views, view) 192} 193 194// DropView drops a view from the set being served 195func DropView(view View) { 196 mu.Lock() 197 defer mu.Unlock() 198 //find and remove the view 199} 200 201// Serve starts and runs a debug server in the background. 202// It also logs the port the server starts on, to allow for :0 auto assigned 203// ports. 204func Serve(ctx context.Context, addr string) error { 205 mu.Lock() 206 defer mu.Unlock() 207 if addr == "" { 208 return nil 209 } 210 listener, err := net.Listen("tcp", addr) 211 if err != nil { 212 return err 213 } 214 log.Printf("Debug serving on port: %d", listener.Addr().(*net.TCPAddr).Port) 215 go func() { 216 mux := http.NewServeMux() 217 mux.HandleFunc("/", Render(mainTmpl, func(*http.Request) interface{} { return data })) 218 mux.HandleFunc("/debug/", Render(debugTmpl, nil)) 219 mux.HandleFunc("/debug/pprof/", pprof.Index) 220 mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 221 mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 222 mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 223 mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 224 mux.HandleFunc("/cache/", Render(cacheTmpl, getCache)) 225 mux.HandleFunc("/session/", Render(sessionTmpl, getSession)) 226 mux.HandleFunc("/view/", Render(viewTmpl, getView)) 227 mux.HandleFunc("/file/", Render(fileTmpl, getFile)) 228 mux.HandleFunc("/info", Render(infoTmpl, getInfo)) 229 mux.HandleFunc("/memory", Render(memoryTmpl, getMemory)) 230 if err := http.Serve(listener, mux); err != nil { 231 log.Printf("Debug server failed with %v", err) 232 return 233 } 234 log.Printf("Debug server finished") 235 }() 236 return nil 237} 238 239func Render(tmpl *template.Template, fun func(*http.Request) interface{}) func(http.ResponseWriter, *http.Request) { 240 return func(w http.ResponseWriter, r *http.Request) { 241 var data interface{} 242 if fun != nil { 243 data = fun(r) 244 } 245 if err := tmpl.Execute(w, data); err != nil { 246 log.Print(err) 247 } 248 } 249} 250 251func commas(s string) string { 252 for i := len(s); i > 3; { 253 i -= 3 254 s = s[:i] + "," + s[i:] 255 } 256 return s 257} 258 259func fuint64(v uint64) string { 260 return commas(strconv.FormatUint(v, 10)) 261} 262 263func fuint32(v uint32) string { 264 return commas(strconv.FormatUint(uint64(v), 10)) 265} 266 267var BaseTemplate = template.Must(template.New("").Parse(` 268<html> 269<head> 270<title>{{template "title" .}}</title> 271<style> 272.profile-name{ 273 display:inline-block; 274 width:6rem; 275} 276td.value { 277 text-align: right; 278} 279</style> 280{{block "head" .}}{{end}} 281</head> 282<body> 283<a href="/">Main</a> 284<a href="/info">Info</a> 285<a href="/memory">Memory</a> 286<a href="/debug/">Debug</a> 287<hr> 288<h1>{{template "title" .}}</h1> 289{{block "body" .}} 290Unknown page 291{{end}} 292</body> 293</html> 294 295{{define "cachelink"}}<a href="/cache/{{.}}">Cache {{.}}</a>{{end}} 296{{define "sessionlink"}}<a href="/session/{{.}}">Session {{.}}</a>{{end}} 297{{define "viewlink"}}<a href="/view/{{.}}">View {{.}}</a>{{end}} 298{{define "filelink"}}<a href="/file/{{.Session.ID}}/{{.Hash}}">{{.URI}}</a>{{end}} 299`)).Funcs(template.FuncMap{ 300 "fuint64": fuint64, 301 "fuint32": fuint32, 302}) 303 304var mainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 305{{define "title"}}GoPls server information{{end}} 306{{define "body"}} 307<h2>Caches</h2> 308<ul>{{range .Caches}}<li>{{template "cachelink" .ID}}</li>{{end}}</ul> 309<h2>Sessions</h2> 310<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}</li>{{end}}</ul> 311<h2>Views</h2> 312<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} from {{template "sessionlink" .Session.ID}} in {{.Folder}}</li>{{end}}</ul> 313{{end}} 314`)) 315 316var infoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 317{{define "title"}}GoPls version information{{end}} 318{{define "body"}} 319{{.}} 320{{end}} 321`)) 322 323var memoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 324{{define "title"}}GoPls memory usage{{end}} 325{{define "head"}}<meta http-equiv="refresh" content="5">{{end}} 326{{define "body"}} 327<h2>Stats</h2> 328<table> 329<tr><td class="label">Allocated bytes</td><td class="value">{{fuint64 .HeapAlloc}}</td></tr> 330<tr><td class="label">Total allocated bytes</td><td class="value">{{fuint64 .TotalAlloc}}</td></tr> 331<tr><td class="label">System bytes</td><td class="value">{{fuint64 .Sys}}</td></tr> 332<tr><td class="label">Heap system bytes</td><td class="value">{{fuint64 .HeapSys}}</td></tr> 333<tr><td class="label">Malloc calls</td><td class="value">{{fuint64 .Mallocs}}</td></tr> 334<tr><td class="label">Frees</td><td class="value">{{fuint64 .Frees}}</td></tr> 335<tr><td class="label">Idle heap bytes</td><td class="value">{{fuint64 .HeapIdle}}</td></tr> 336<tr><td class="label">In use bytes</td><td class="value">{{fuint64 .HeapInuse}}</td></tr> 337<tr><td class="label">Released to system bytes</td><td class="value">{{fuint64 .HeapReleased}}</td></tr> 338<tr><td class="label">Heap object count</td><td class="value">{{fuint64 .HeapObjects}}</td></tr> 339<tr><td class="label">Stack in use bytes</td><td class="value">{{fuint64 .StackInuse}}</td></tr> 340<tr><td class="label">Stack from system bytes</td><td class="value">{{fuint64 .StackSys}}</td></tr> 341<tr><td class="label">Bucket hash bytes</td><td class="value">{{fuint64 .BuckHashSys}}</td></tr> 342<tr><td class="label">GC metaata bytes</td><td class="value">{{fuint64 .GCSys}}</td></tr> 343<tr><td class="label">Off heap bytes</td><td class="value">{{fuint64 .OtherSys}}</td></tr> 344</table> 345<h2>By size</h2> 346<table> 347<tr><th>Size</th><th>Mallocs</th><th>Frees</th></tr> 348{{range .BySize}}<tr><td class="value">{{fuint32 .Size}}</td><td class="value">{{fuint64 .Mallocs}}</td><td class="value">{{fuint64 .Frees}}</td></tr>{{end}} 349</table> 350{{end}} 351`)) 352 353var debugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 354{{define "title"}}GoPls Debug pages{{end}} 355{{define "body"}} 356<a href="/debug/pprof">Profiling</a> 357<a href="/debug/rpcz">RPCz</a> 358<a href="/debug/tracez">Tracez</a> 359{{end}} 360`)) 361 362var cacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 363{{define "title"}}Cache {{.ID}}{{end}} 364{{define "body"}} 365<h2>Sessions</h2> 366<ul>{{range .Sessions}}<li>{{template "sessionlink" .ID}}</li>{{end}}</ul> 367{{end}} 368`)) 369 370var sessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 371{{define "title"}}Session {{.ID}}{{end}} 372{{define "body"}} 373From: <b>{{template "cachelink" .Cache.ID}}</b><br> 374<h2>Views</h2> 375<ul>{{range .Views}}<li>{{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}</li>{{end}}</ul> 376<h2>Files</h2> 377<ul>{{range .Files}}<li>{{template "filelink" .}}</li>{{end}}</ul> 378{{end}} 379`)) 380 381var viewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 382{{define "title"}}View {{.ID}}{{end}} 383{{define "body"}} 384Name: <b>{{.Name}}</b><br> 385Folder: <b>{{.Folder}}</b><br> 386From: <b>{{template "sessionlink" .Session.ID}}</b><br> 387<h2>Environment</h2> 388<ul>{{range .Env}}<li>{{.}}</li>{{end}}</ul> 389{{end}} 390`)) 391 392var fileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` 393{{define "title"}}File {{.Hash}}{{end}} 394{{define "body"}} 395From: <b>{{template "sessionlink" .Session.ID}}</b><br> 396URI: <b>{{.URI}}</b><br> 397Hash: <b>{{.Hash}}</b><br> 398Error: <b>{{.Error}}</b><br> 399<h3>Contents</h3> 400<pre>{{.Data}}</pre> 401{{end}} 402`)) 403