| // 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" | |
| "errors" | |
| "fmt" | |
| "go/types" | |
| "html/template" | |
| "io" | |
| stdlog "log" | |
| "net" | |
| "net/http" | |
| "net/http/pprof" | |
| "os" | |
| "path" | |
| "path/filepath" | |
| "runtime" | |
| "strconv" | |
| "strings" | |
| "sync" | |
| "time" | |
| "golang.org/x/tools/gopls/internal/cache" | |
| "golang.org/x/tools/gopls/internal/cache/metadata" | |
| "golang.org/x/tools/gopls/internal/debug/log" | |
| "golang.org/x/tools/gopls/internal/file" | |
| label1 "golang.org/x/tools/gopls/internal/label" | |
| "golang.org/x/tools/gopls/internal/protocol" | |
| "golang.org/x/tools/gopls/internal/util/bug" | |
| versionpkg "golang.org/x/tools/gopls/internal/version" | |
| "golang.org/x/tools/internal/event" | |
| "golang.org/x/tools/internal/event/core" | |
| "golang.org/x/tools/internal/event/export" | |
| "golang.org/x/tools/internal/event/export/metric" | |
| "golang.org/x/tools/internal/event/export/otel" | |
| "golang.org/x/tools/internal/event/export/prometheus" | |
| "golang.org/x/tools/internal/event/keys" | |
| "golang.org/x/tools/internal/event/label" | |
| ) | |
| type contextKeyType int | |
| const ( | |
| instanceKey contextKeyType = iota | |
| traceKey | |
| ) | |
| // An Instance holds all debug information associated with a gopls instance. | |
| type Instance struct { | |
| Logfile string | |
| StartTime time.Time | |
| ServerAddress string | |
| LogWriter io.Writer | |
| exporter event.Exporter | |
| prometheus *prometheus.Exporter | |
| rpcs *Rpcs | |
| traces *traces | |
| otel *otel.Exporter | |
| State *State | |
| serveMu sync.Mutex | |
| debugAddress string | |
| listenedDebugAddress string | |
| } | |
| // State holds debugging information related to the server state. | |
| type State struct { | |
| mu sync.Mutex | |
| clients []*Client | |
| servers []*Server | |
| } | |
| func (st *State) Bugs() []bug.Bug { | |
| return bug.List() | |
| } | |
| // Caches returns the set of Cache objects currently being served. | |
| func (st *State) Caches() []*cache.Cache { | |
| var caches []*cache.Cache | |
| seen := make(map[string]struct{}) | |
| for _, client := range st.Clients() { | |
| cache := client.Session.Cache() | |
| if _, found := seen[cache.ID()]; found { | |
| continue | |
| } | |
| seen[cache.ID()] = struct{}{} | |
| caches = append(caches, cache) | |
| } | |
| return caches | |
| } | |
| // Cache returns the Cache that matches the supplied id. | |
| func (st *State) Cache(id string) *cache.Cache { | |
| for _, c := range st.Caches() { | |
| if c.ID() == id { | |
| return c | |
| } | |
| } | |
| return nil | |
| } | |
| // Analysis returns the global Analysis template value. | |
| func (st *State) Analysis() (_ analysisTmpl) { return } | |
| type analysisTmpl struct{} | |
| func (analysisTmpl) AnalyzerRunTimes() []cache.LabelDuration { return cache.AnalyzerRunTimes() } | |
| // Sessions returns the set of Session objects currently being served. | |
| func (st *State) Sessions() []*cache.Session { | |
| var sessions []*cache.Session | |
| for _, client := range st.Clients() { | |
| sessions = append(sessions, client.Session) | |
| } | |
| return sessions | |
| } | |
| // Session returns the Session that matches the supplied id. | |
| func (st *State) Session(id string) *cache.Session { | |
| for _, s := range st.Sessions() { | |
| if s.ID() == id { | |
| return s | |
| } | |
| } | |
| return nil | |
| } | |
| // Views returns the set of View objects currently being served. | |
| func (st *State) Views() []*cache.View { | |
| var views []*cache.View | |
| for _, s := range st.Sessions() { | |
| views = append(views, s.Views()...) | |
| } | |
| return views | |
| } | |
| // View returns the View that matches the supplied id. | |
| func (st *State) View(id string) *cache.View { | |
| for _, s := range st.Sessions() { | |
| if v, err := s.View(id); err == nil { | |
| return v | |
| } | |
| } | |
| return nil // not found | |
| } | |
| // 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)) | |
| copy(clients, st.clients) | |
| return clients | |
| } | |
| // Client returns the Client matching the supplied id. | |
| func (st *State) Client(id string) *Client { | |
| for _, c := range st.Clients() { | |
| if c.Session.ID() == id { | |
| return c | |
| } | |
| } | |
| return nil | |
| } | |
| // 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)) | |
| copy(servers, st.servers) | |
| return servers | |
| } | |
| // A Client is an incoming connection from a remote client. | |
| type Client struct { | |
| Session *cache.Session | |
| DebugAddress string | |
| Logfile string | |
| GoplsPath string | |
| ServerID string | |
| Service protocol.Server | |
| } | |
| // A Server is an outgoing connection to a remote LSP server. | |
| type Server struct { | |
| ID string | |
| DebugAddress string | |
| Logfile string | |
| GoplsPath string | |
| ClientID string | |
| } | |
| // addClient adds a client to the set being served. | |
| func (st *State) addClient(session *cache.Session) { | |
| st.mu.Lock() | |
| defer st.mu.Unlock() | |
| st.clients = append(st.clients, &Client{Session: session}) | |
| } | |
| // dropClient removes a client from the set being served. | |
| func (st *State) dropClient(session *cache.Session) { | |
| st.mu.Lock() | |
| defer st.mu.Unlock() | |
| for i, c := range st.clients { | |
| if c.Session == session { | |
| copy(st.clients[i:], st.clients[i+1:]) | |
| st.clients[len(st.clients)-1] = nil | |
| st.clients = st.clients[:len(st.clients)-1] | |
| return | |
| } | |
| } | |
| } | |
| // updateServer updates a server to the set being queried. In practice, there should | |
| // be at most one remote server. | |
| func (st *State) updateServer(server *Server) { | |
| st.mu.Lock() | |
| defer st.mu.Unlock() | |
| for i, existing := range st.servers { | |
| if existing.ID == server.ID { | |
| // Replace, rather than mutate, to avoid a race. | |
| newServers := make([]*Server, len(st.servers)) | |
| copy(newServers, st.servers[:i]) | |
| newServers[i] = server | |
| copy(newServers[i+1:], st.servers[i+1:]) | |
| st.servers = newServers | |
| return | |
| } | |
| } | |
| st.servers = append(st.servers, server) | |
| } | |
| // dropServer drops a server from the set being queried. | |
| func (st *State) dropServer(id string) { | |
| st.mu.Lock() | |
| defer st.mu.Unlock() | |
| for i, s := range st.servers { | |
| if s.ID == id { | |
| copy(st.servers[i:], st.servers[i+1:]) | |
| st.servers[len(st.servers)-1] = nil | |
| st.servers = st.servers[:len(st.servers)-1] | |
| return | |
| } | |
| } | |
| } | |
| // an http.ResponseWriter that filters writes | |
| type filterResponse struct { | |
| w http.ResponseWriter | |
| edit func([]byte) []byte | |
| } | |
| func (c filterResponse) Header() http.Header { | |
| return c.w.Header() | |
| } | |
| func (c filterResponse) Write(buf []byte) (int, error) { | |
| ans := c.edit(buf) | |
| return c.w.Write(ans) | |
| } | |
| func (c filterResponse) WriteHeader(n int) { | |
| c.w.WriteHeader(n) | |
| } | |
| // replace annoying nuls by spaces | |
| func cmdline(w http.ResponseWriter, r *http.Request) { | |
| fake := filterResponse{ | |
| w: w, | |
| edit: func(buf []byte) []byte { | |
| return bytes.ReplaceAll(buf, []byte{0}, []byte{' '}) | |
| }, | |
| } | |
| pprof.Cmdline(fake, r) | |
| } | |
| func (i *Instance) getCache(r *http.Request) any { | |
| return i.State.Cache(path.Base(r.URL.Path)) | |
| } | |
| func (i *Instance) getAnalysis(r *http.Request) any { | |
| return i.State.Analysis() | |
| } | |
| func (i *Instance) getSession(r *http.Request) any { | |
| return i.State.Session(path.Base(r.URL.Path)) | |
| } | |
| func (i *Instance) getClient(r *http.Request) any { | |
| return i.State.Client(path.Base(r.URL.Path)) | |
| } | |
| func (i *Instance) getServer(r *http.Request) any { | |
| i.State.mu.Lock() | |
| defer i.State.mu.Unlock() | |
| id := path.Base(r.URL.Path) | |
| for _, s := range i.State.servers { | |
| if s.ID == id { | |
| return s | |
| } | |
| } | |
| return nil | |
| } | |
| type FileWithKind interface { | |
| file.Handle | |
| Kind() file.Kind // (overlay files only) | |
| } | |
| // /file/{session}/{identifier}. Returns a [FileWithKind]. | |
| func (i *Instance) getFile(r *http.Request) any { | |
| s := i.State.Session(r.PathValue("session")) | |
| if s == nil { | |
| return nil | |
| } | |
| identifier := r.PathValue("identifier") | |
| for _, o := range s.Overlays() { | |
| // TODO(adonovan): understand and document this comparison. | |
| if o.Identity().Hash.String() == identifier { | |
| return o | |
| } | |
| } | |
| return nil | |
| } | |
| type MetadataInfo struct { | |
| SessionID string | |
| ViewID string | |
| Graph *metadata.Graph | |
| } | |
| // /metadata/{session}/{view}. Returns a [*MetadataInfo]. | |
| func (i *Instance) getMetadata(r *http.Request) any { | |
| session := i.State.Session(r.PathValue("session")) | |
| if session == nil { | |
| return nil | |
| } | |
| v, err := session.View(r.PathValue("view")) | |
| if err != nil { | |
| stdlog.Printf("/metadata: %v", err) | |
| return nil // no found | |
| } | |
| snapshot, release, err := v.Snapshot() | |
| if err != nil { | |
| stdlog.Printf("/metadata: failed to get latest snapshot: %v", err) | |
| return nil | |
| } | |
| defer release() | |
| return &MetadataInfo{ | |
| SessionID: session.ID(), | |
| ViewID: v.ID(), | |
| Graph: snapshot.MetadataGraph(), | |
| } | |
| } | |
| type PackageInfo struct { | |
| SessionID string | |
| ViewID string | |
| Package *cache.Package | |
| Diagnostics map[protocol.DocumentURI][]*cache.Diagnostic | |
| } | |
| // /package/{session}/{view}/{id...}. Returns a [*PackageInfo]. | |
| func (i *Instance) getPackage(r *http.Request) any { | |
| // TODO(adonovan): shouldn't we report an HTTP error in all | |
| // these early returns? Same for getMetadata. | |
| session := i.State.Session(r.PathValue("session")) | |
| if session == nil { | |
| return nil // not found | |
| } | |
| v, err := session.View(r.PathValue("view")) | |
| if err != nil { | |
| stdlog.Printf("/package: %v", err) | |
| return nil // not found | |
| } | |
| id := r.PathValue("id") | |
| snapshot, release, err := v.Snapshot() | |
| if err != nil { | |
| stdlog.Printf("/package: failed to get latest snapshot: %v", err) | |
| return nil | |
| } | |
| defer release() | |
| pkgs, err := snapshot.TypeCheck(r.Context(), cache.PackageID(id)) | |
| if err != nil { | |
| stdlog.Printf("/package: failed to typecheck package %q: %v", id, err) | |
| return nil | |
| } | |
| // (PackageDiagnostics is redundant w.r.t. TypeCheck but it's | |
| // the only way to access type errors in cache.Diagnostic form.) | |
| diags, err := snapshot.PackageDiagnostics(r.Context(), cache.PackageID(id)) | |
| if err != nil { | |
| stdlog.Printf("/package: failed to typecheck package %q: %v", id, err) | |
| return nil | |
| } | |
| return &PackageInfo{ | |
| SessionID: session.ID(), | |
| ViewID: v.ID(), | |
| Package: pkgs[0], | |
| Diagnostics: diags, | |
| } | |
| } | |
| func (i *Instance) getInfo(r *http.Request) any { | |
| buf := &bytes.Buffer{} | |
| i.writeServerInfo(buf) | |
| return template.HTML(buf.String()) | |
| } | |
| func (i *Instance) AddService(s protocol.Server, session *cache.Session) { | |
| for _, c := range i.State.clients { | |
| if c.Session == session { | |
| c.Service = s | |
| return | |
| } | |
| } | |
| stdlog.Printf("unable to find a Client to add the protocol.Server to") | |
| } | |
| func getMemory(_ *http.Request) any { | |
| 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, otelEndpoint string) context.Context { | |
| i := &Instance{ | |
| StartTime: time.Now(), | |
| } | |
| i.LogWriter = os.Stderr | |
| i.prometheus = prometheus.New() | |
| i.rpcs = &Rpcs{} | |
| i.traces = &traces{} | |
| i.State = &State{} | |
| if otelEndpoint != "" { | |
| i.otel = otel.NewExporter(ctx, | |
| otel.WithEndpoint(otelEndpoint), | |
| otel.WithServiceName("gopls"), | |
| otel.WithServiceVersion(versionpkg.Version())) | |
| } | |
| 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, isDaemon bool) (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" { | |
| if isDaemon { | |
| logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-daemon-%d.log", os.Getpid())) | |
| } else { | |
| 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: %w", err) | |
| } | |
| closeLog = func() { | |
| defer f.Close() | |
| } | |
| stdlog.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 on the given addr. | |
| // It also logs the port the server starts on, to allow for :0 auto assigned | |
| // ports. | |
| func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { | |
| stdlog.SetFlags(stdlog.Lshortfile) | |
| if addr == "" { | |
| return "", nil | |
| } | |
| i.serveMu.Lock() | |
| defer i.serveMu.Unlock() | |
| if i.listenedDebugAddress != "" { | |
| // Already serving. Return the bound address. | |
| return i.listenedDebugAddress, nil | |
| } | |
| i.debugAddress = addr | |
| 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") { | |
| stdlog.Printf("debug server listening at http://localhost:%d", port) | |
| } | |
| event.Log(ctx, "Debug serving", label1.Port.Of(port)) | |
| go func() { | |
| mux := http.NewServeMux() | |
| mux.HandleFunc("/", render(MainTmpl, func(*http.Request) any { return i })) | |
| mux.HandleFunc("/debug/", render(DebugTmpl, nil)) | |
| mux.HandleFunc("/debug/pprof/", pprof.Index) | |
| mux.HandleFunc("/debug/pprof/cmdline", cmdline) | |
| mux.HandleFunc("/debug/pprof/profile", pprof.Profile) | |
| mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) | |
| mux.HandleFunc("/debug/pprof/trace", pprof.Trace) | |
| if h, err := startFlightRecorder(); err != nil { | |
| stdlog.Printf("failed to start flight recorder: %v", err) // e.g. go1.24 | |
| } else { | |
| mux.HandleFunc("/flightrecorder", h) | |
| } | |
| 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("/analysis/", render(AnalysisTmpl, i.getAnalysis)) | |
| mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache)) | |
| mux.HandleFunc("/session/", render(SessionTmpl, i.getSession)) | |
| mux.HandleFunc("/client/", render(ClientTmpl, i.getClient)) | |
| mux.HandleFunc("/server/", render(ServerTmpl, i.getServer)) | |
| mux.HandleFunc("/file/{session}/{identifier}", render(FileTmpl, i.getFile)) | |
| mux.HandleFunc("/metadata/{session}/{view}/", render(MetadataTmpl, i.getMetadata)) | |
| mux.HandleFunc("/package/{session}/{view}/{id...}", render(PackageTmpl, i.getPackage)) | |
| mux.HandleFunc("/info", render(InfoTmpl, i.getInfo)) | |
| mux.HandleFunc("/memory", render(MemoryTmpl, getMemory)) | |
| // Internal debugging helpers. | |
| mux.HandleFunc("/gc", func(w http.ResponseWriter, r *http.Request) { | |
| runtime.GC() | |
| runtime.GC() | |
| runtime.GC() | |
| http.Redirect(w, r, "/memory", http.StatusTemporaryRedirect) | |
| }) | |
| mux.HandleFunc("/_makeabug", func(w http.ResponseWriter, r *http.Request) { | |
| bug.Report("bug here") | |
| http.Error(w, "made a bug", http.StatusOK) | |
| }) | |
| err := http.Serve(listener, mux) // always non-nil | |
| event.Error(ctx, "Debug server failed", err) | |
| }() | |
| return i.listenedDebugAddress, nil | |
| } | |
| func (i *Instance) DebugAddress() string { | |
| i.serveMu.Lock() | |
| defer i.serveMu.Unlock() | |
| return i.debugAddress | |
| } | |
| func (i *Instance) ListenedDebugAddress() string { | |
| i.serveMu.Lock() | |
| defer i.serveMu.Unlock() | |
| return i.listenedDebugAddress | |
| } | |
| func makeGlobalExporter(stderr io.Writer) event.Exporter { | |
| var ( | |
| pMu sync.Mutex | |
| p export.Printer | |
| ) | |
| return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { | |
| i := GetInstance(ctx) | |
| if event.IsLog(ev) { | |
| // Don't log context cancellation errors. | |
| if err := keys.Err.Get(ev); errors.Is(err, context.Canceled) { | |
| return ctx | |
| } | |
| // Make sure any log messages without an instance go to stderr. | |
| if i == nil { | |
| pMu.Lock() | |
| p.WriteEvent(stderr, ev, lm) | |
| pMu.Unlock() | |
| } | |
| level := log.LabeledLevel(lm) | |
| // Exclude trace logs from LSP logs. | |
| if level < log.Trace { | |
| ctx = protocol.LogEvent(ctx, ev, lm, messageType(level)) | |
| } | |
| } | |
| if i == nil { | |
| return ctx | |
| } | |
| return i.exporter(ctx, ev, lm) | |
| } | |
| } | |
| func messageType(l log.Level) protocol.MessageType { | |
| switch l { | |
| case log.Error: | |
| return protocol.Error | |
| case log.Warning: | |
| return protocol.Warning | |
| case log.Debug: | |
| return protocol.Debug | |
| case log.Info: | |
| return protocol.Info | |
| default: | |
| return protocol.Info | |
| } | |
| } | |
| func makeInstanceExporter(i *Instance) event.Exporter { | |
| exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context { | |
| if i.prometheus != nil { | |
| ctx = i.prometheus.ProcessEvent(ctx, ev, lm) | |
| } | |
| if i.rpcs != nil { | |
| ctx = i.rpcs.ProcessEvent(ctx, ev, lm) | |
| } | |
| if i.traces != nil { | |
| ctx = i.traces.ProcessEvent(ctx, ev, lm) | |
| } | |
| if i.otel != nil { | |
| ctx = i.otel.ProcessEvent(ctx, ev, lm) | |
| } | |
| if event.IsLog(ev) { | |
| if s := cache.KeyCreateSession.Get(ev); s != nil { | |
| i.State.addClient(s) | |
| } | |
| if sid := label1.NewServer.Get(ev); sid != "" { | |
| i.State.updateServer(&Server{ | |
| ID: sid, | |
| Logfile: label1.Logfile.Get(ev), | |
| DebugAddress: label1.DebugAddress.Get(ev), | |
| GoplsPath: label1.GoplsPath.Get(ev), | |
| ClientID: label1.ClientID.Get(ev), | |
| }) | |
| } | |
| if s := cache.KeyShutdownSession.Get(ev); s != nil { | |
| i.State.dropClient(s) | |
| } | |
| if sid := label1.EndServer.Get(ev); sid != "" { | |
| i.State.dropServer(sid) | |
| } | |
| if s := cache.KeyUpdateSession.Get(ev); s != nil { | |
| if c := i.State.Client(s.ID()); c != nil { | |
| c.DebugAddress = label1.DebugAddress.Get(ev) | |
| c.Logfile = label1.Logfile.Get(ev) | |
| c.ServerID = label1.ServerID.Get(ev) | |
| c.GoplsPath = label1.GoplsPath.Get(ev) | |
| } | |
| } | |
| } | |
| return ctx | |
| } | |
| // StdTrace must be above export.Spans below (by convention, export | |
| // middleware applies its wrapped exporter last). | |
| exporter = StdTrace(exporter) | |
| metrics := metric.Config{} | |
| registerMetrics(&metrics) | |
| exporter = metrics.Exporter(exporter) | |
| exporter = export.Spans(exporter) | |
| exporter = export.Labels(exporter) | |
| return exporter | |
| } | |
| type dataFunc func(*http.Request) any | |
| func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) { | |
| return func(w http.ResponseWriter, r *http.Request) { | |
| var data any | |
| 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)) | |
| } | |
| func fcontent(v []byte) string { | |
| return string(v) | |
| } | |
| 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.spans { | |
| font-family: monospace; | |
| font-size: 85%; | |
| } | |
| body { | |
| font-family: sans-serif; | |
| font-size: 1rem; | |
| line-height: normal; | |
| } | |
| </style> | |
| {{block "head" .}}{{end}} | |
| </head> | |
| <body> | |
| <a href="/">Main</a> | |
| <a href="/info">Info</a> | |
| <a href="/memory">Memory</a> | |
| <a href="/debug/pprof">Profiling</a> | |
| <a href="/metrics">Metrics</a> | |
| <a href="/rpc">RPC</a> | |
| <a href="/trace">Trace</a> | |
| <a href="/flightrecorder">Flight recorder</a> | |
| <a href="/analysis">Analysis</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}} | |
| `)).Funcs(template.FuncMap{ | |
| "fuint64": fuint64, | |
| "fuint32": fuint32, | |
| "fcontent": fcontent, | |
| "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 | |
| }, | |
| "objectString": func(obj types.Object) string { | |
| // Assume we're printing objects from the package of interest. | |
| this := obj.Pkg() | |
| qual := func(pkg *types.Package) string { | |
| if this == pkg { | |
| return "" // same package; unqualified | |
| } | |
| return pkg.Name() | |
| } | |
| return types.ObjectString(obj, qual) | |
| }, | |
| // TODO(rfindley): re-enable option inspection. | |
| // "options": func(s *cache.Session) []sessionOption { | |
| // return showOptions(s.Options()) | |
| // }, | |
| }) | |
| 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>Clients</h2> | |
| <ul>{{range .State.Clients}}<li>{{template "clientlink" .Session.ID}}</li>{{end}}</ul> | |
| <h2>Servers</h2> | |
| <ul>{{range .State.Servers}}<li>{{template "serverlink" .ID}}</li>{{end}}</ul> | |
| <h2>Bug reports</h2> | |
| <dl>{{range .State.Bugs}}<dt>{{.Key}}</dt><dd>{{.Description}}</dd>{{end}}</dl> | |
| {{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"}} | |
| <form action="/gc"><input type="submit" value="Run garbage collector"/></form> | |
| <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>memoize.Store entries</h2> | |
| <ul>{{range $k,$v := .MemStats}}<li>{{$k}} - {{$v}}</li>{{end}}</ul> | |
| <h2>File stats</h2> | |
| <p> | |
| {{- $stats := .FileStats -}} | |
| Total: <b>{{$stats.Total}}</b><br> | |
| Largest: <b>{{$stats.Largest}}</b><br> | |
| Errors: <b>{{$stats.Errs}}</b><br> | |
| </p> | |
| {{end}} | |
| `)) | |
| var AnalysisTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` | |
| {{define "title"}}Analysis{{end}} | |
| {{define "body"}} | |
| <h2>Analyzer.Run times</h2> | |
| <ul>{{range .AnalyzerRunTimes}}<li>{{.Duration}} {{.Label}}</li>{{end}}</ul> | |
| {{end}} | |
| `)) | |
| var ClientTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` | |
| {{define "title"}}Client {{.Session.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> | |
| {{- $session := . -}} | |
| <h2>Views</h2> | |
| <ul>{{range .Views}} | |
| {{- $envOverlay := .EnvOverlay -}} | |
| <li>ID: <b>{{.ID}}</b><br> | |
| Type: <b>{{.Type}}</b><br> | |
| Root: <b>{{.Root}}</b><br> | |
| {{- if $envOverlay}} | |
| Env overlay: <b>{{$envOverlay}})</b><br> | |
| {{end -}} | |
| Folder.Name: <b>{{.Folder.Name}}</b><br> | |
| Folder.Dir: <b>{{.Folder.Dir}}</b><br/> | |
| <a href="/metadata/{{$session.ID}}/{{.ID}}">Latest metadata</a><br/> | |
| Settings:<br/> | |
| <ul> | |
| {{range .Folder.Options.Debug}}<li>{{.}}</li> | |
| {{end}} | |
| </ul> | |
| {{end}}</ul> | |
| <h2>Overlays</h2> | |
| {{$session := .}} | |
| <ul>{{range .Overlays}} | |
| <li> | |
| <a href="/file/{{$session.ID}}/{{.Identity.Hash}}">{{.Identity.URI}}</a> | |
| </li>{{end}}</ul> | |
| {{end}} | |
| `)) | |
| // For /file endpoint; operand is [FileWithKind]. | |
| var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` | |
| {{define "title"}}Overlay {{.Identity.Hash}}{{end}} | |
| {{define "body"}} | |
| {{with .}} | |
| URI: <b>{{.URI}}</b><br> | |
| Identifier: <b>{{.Identity.Hash}}</b><br> | |
| Version: <b>{{.Version}}</b><br> | |
| Kind: <b>{{.Kind}}</b><br> | |
| {{end}} | |
| <h3>Contents</h3> | |
| <pre>{{fcontent .Content}}</pre> | |
| {{end}} | |
| `)) | |
| // For /metadata endpoint; operand is [*MetadataInfo]. | |
| var MetadataTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` | |
| {{define "title"}}Metadata graph{{end}} | |
| {{define "body"}} | |
| <p><a href='#hdr-Files'>↓ Index by file</a></p> | |
| <h3>Packages ({{len .Graph.Packages}})</h3> | |
| <ul> | |
| {{range $id, $pkg := .Graph.Packages}} | |
| <li id='{{$id}}'><b>{{$id}}</b> | |
| {{with $pkg}} | |
| <ul> | |
| <li>Name: {{.Name}}</li> | |
| <li>PkgPath: {{printf "%q" .PkgPath}}</li> | |
| {{if .Module}}<li>Module: {{printf "%#v" .Module}}</li>{{end}} | |
| <li><a href="/package/{{$.SessionID}}/{{$.ViewID}}/{{$id}}">Type information</a></li> | |
| {{if .ForTest}}<li>ForTest: {{.ForTest}}</li>{{end}} | |
| {{if .Standalone}}<li>Standalone</li>{{end}} | |
| {{if .Errors}}<li>Errors: {{.Errors}}</li>{{end}} | |
| {{if .DepsErrors}}<li>DepsErrors: {{.DepsErrors}}</li>{{end}} | |
| <li>LoadDir: {{.LoadDir}}</li> | |
| <li>DepsByImpPath | |
| <ul> | |
| {{range $path, $id := .DepsByImpPath}} | |
| <li>{{if $id}}<a href='#{{$id}}'>{{printf "%q" $path}}</a>{{else}}⚠️ {{printf "%q" $path}} missing{{end}}</li> | |
| {{end}} | |
| </ul> | |
| </li> | |
| {{if .GoFiles}}<li>GoFiles: <ul>{{range .GoFiles}}<li>{{.}}</li>{{end}}</ul></li>{{end}} | |
| {{if .CompiledGoFiles}}<li>CompiledGoFiles: <ul>{{range .CompiledGoFiles}}<li>{{.}}</li>{{end}}</ul></li>{{end}} | |
| {{if .IgnoredFiles}}<li>IgnoredFiles: <ul>{{range .IgnoredFiles}}<li>{{.}}</li>{{end}}</ul></li>{{end}} | |
| {{if .OtherFiles}}<li>OtherFiles: <ul>{{range .OtherFiles}}<li>{{.}}</li>{{end}}</ul></li>{{end}} | |
| <!-- skip DepsByPkgPath, ImportedBy (redundant indexes) --> | |
| </ul> | |
| {{end}} | |
| </li> | |
| {{end}} | |
| </ul> | |
| <h3 id='hdr-Files'>Files</h3> | |
| <ul> | |
| {{range $uri, $pkgs := .Graph.ForFile}}<li>{{$uri}} →{{range $pkgs}} <a href='#{{.ID}}'>{{.ID}}</a>{{end}}</li>{{end}} | |
| </ul> | |
| {{end}} | |
| `)) | |
| // For /package endpoint; operand is [*PackageInfo]. | |
| var PackageTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` | |
| {{define "title"}}Package {{.Package.Metadata.ID}}{{end}} | |
| {{define "body"}} | |
| <ul> | |
| <li><a href="/metadata/{{.SessionID}}/{{.ViewID}}#{{.Package.Metadata.ID}}">Metadata</a></li> | |
| </ul> | |
| <h2>Diagnostics for syntax and type (but not analysis) errors</h2> | |
| <ul> | |
| {{range $url, $diags := .Diagnostics}} | |
| <li>{{$url}} | |
| <ul> | |
| {{range $diag := $diags}} | |
| <li>{{$diag.Range}}: [{{$diag.Severity}}] {{$diag.Message}}<br/> | |
| <ul> | |
| <li>code {{$diag.Code}}</li> | |
| <li>code href {{$diag.CodeHref}}</li> | |
| <li>source {{$diag.Source}}</li> | |
| <li>tags {{$diag.Tags}}</li> | |
| <li>related {{$diag.Related}}</li> {{/*TODO: improve*/}} | |
| <li>bundled fixes {{$diag.BundledFixes}}</li> {{/*TODO: improve*/}} | |
| <li>fixes {{$diag.SuggestedFixes}}</li> {{/*TODO: improve*/}} | |
| </ul> | |
| {{end}} | |
| </ul> | |
| {{end}} | |
| </ul> | |
| <h2>Package-level symbols</h2> | |
| <ul> | |
| {{with $scope := .Package.Types.Scope}} | |
| {{range $name := $scope.Names}} | |
| {{with $obj := $scope.Lookup $name}} | |
| <li>{{objectString .}}<br/> | |
| {{end}} | |
| {{end}} | |
| {{end}} | |
| </ul> | |
| {{end}} | |
| {{/* | |
| TODO: | |
| - link to godoc (tricky: in server package) | |
| - show index info (xrefs, methodsets, tests) | |
| - call DiagnoseFile on each file? | |
| */}} | |
| `)) |