| // Copyright 2017 Marc-Antoine Ruel. All rights reserved. | |
| // Use of this source code is governed under the Apache License, Version 2.0 | |
| // that can be found in the LICENSE file. | |
| // Render snapshots and aggregated goroutines as HTML with source links. | |
| //go:generate go run regen.go | |
| package stack | |
| import ( | |
| "fmt" | |
| "html/template" | |
| "io" | |
| "log" | |
| "net/url" | |
| "regexp" | |
| "runtime" | |
| "strings" | |
| "time" | |
| ) | |
| // ToHTML formats the aggregated buckets as HTML to the writer. | |
| // | |
| // Use footer to add custom HTML at the bottom of the page. | |
| func (a *Aggregated) ToHTML(w io.Writer, footer template.HTML) error { | |
| data := map[string]interface{}{ | |
| "Aggregated": a, | |
| "Footer": footer, | |
| "Snapshot": a.Snapshot, | |
| } | |
| return toHTML(w, data) | |
| } | |
| // ToHTML formats the snapshot as HTML to the writer. | |
| // | |
| // Use footer to add custom HTML at the bottom of the page. | |
| func (s *Snapshot) ToHTML(w io.Writer, footer template.HTML) error { | |
| data := map[string]interface{}{ | |
| "Footer": footer, | |
| "Snapshot": s, | |
| } | |
| return toHTML(w, data) | |
| } | |
| // Private stuff. | |
| func toHTML(w io.Writer, data map[string]interface{}) error { | |
| m := template.FuncMap{ | |
| "funcClass": funcClass, | |
| "minus": minus, | |
| "pkgURL": pkgURL, | |
| "srcURL": srcURL, | |
| "symbol": symbol, | |
| } | |
| data["Favicon"] = favicon | |
| data["GOMAXPROCS"] = runtime.GOMAXPROCS(0) | |
| data["Now"] = time.Now().Truncate(time.Second) | |
| data["Version"] = runtime.Version() | |
| t, err := template.New("t").Funcs(m).Parse(indexHTML) | |
| if err != nil { | |
| return err | |
| } | |
| return t.Execute(w, data) | |
| } | |
| var reMethodSymbol = regexp.MustCompile(`^\(\*?([^)]+)\)(\..+)$`) | |
| func funcClass(c *Call) template.HTML { | |
| if c.Func.IsPkgMain { | |
| return "FuncMain Exported" | |
| } | |
| s := c.Location.String() | |
| if c.Func.IsExported { | |
| s += " Exported" | |
| } | |
| /* #nosec G203 */ | |
| return template.HTML("Func") + template.HTML(template.HTMLEscapeString(s)) | |
| } | |
| func minus(i, j int) int { | |
| return i - j | |
| } | |
| // pkgURL returns a link to the godoc for the call. | |
| func pkgURL(c *Call) template.URL { | |
| imp := c.ImportPath | |
| // Check for vendored code first. | |
| if i := strings.Index(imp, "/vendor/"); i != -1 { | |
| imp = imp[i+8:] | |
| } | |
| ip := escape(imp) | |
| if ip == "" { | |
| return "" | |
| } | |
| url := template.URL("") | |
| if c.Location == Stdlib { | |
| // This always links to the latest release, past releases are not online. | |
| // That's somewhat unfortunate. | |
| url = "https://golang.org/pkg/" | |
| } else { | |
| // TODO(maruel): Leverage Location. | |
| // Use pkg.go.dev when there's a version (go module) and godoc.org when | |
| // there's none (implies branch master). | |
| _, branch := getSrcBranchURL(c) | |
| if branch == "master" || branch == "" { | |
| url = "https://godoc.org/" | |
| } else { | |
| url = "https://pkg.go.dev/" | |
| } | |
| } | |
| if c.Func.IsExported { | |
| return url + ip + template.URL("#") + symbol(&c.Func) | |
| } | |
| return url + ip | |
| } | |
| // srcURL returns an URL to the sources. | |
| // | |
| // TODO(maruel): Support custom local godoc server as it serves files too. | |
| func srcURL(c *Call) template.URL { | |
| url, _ := getSrcBranchURL(c) | |
| return url | |
| } | |
| func escape(s string) template.URL { | |
| // That's the only way I found to get the kind of escaping I wanted, where | |
| // '/' is not escaped. | |
| u := url.URL{Path: s} | |
| /* #nosec G203 */ | |
| return template.URL(u.EscapedPath()) | |
| } | |
| // getSrcBranchURL returns a link to the source on the web and the tag name for | |
| // the package version, if possible. | |
| func getSrcBranchURL(c *Call) (template.URL, template.URL) { | |
| tag := "" | |
| if c.Location == Stdlib { | |
| // TODO(maruel): This is not strictly speaking correct. The remote could be | |
| // running a different Go version from the current executable. | |
| ver := runtime.Version() | |
| const devel = "devel +" | |
| if strings.HasPrefix(ver, devel) { | |
| ver = ver[len(devel) : len(devel)+10] | |
| } | |
| tag = url.QueryEscape(ver) | |
| /* #nosec G203 */ | |
| return template.URL(fmt.Sprintf("https://github.com/golang/go/blob/%s/src/%s#L%d", tag, escape(c.RelSrcPath), c.Line)), template.URL(tag) | |
| } | |
| // TODO(maruel): Leverage Location. | |
| if rel := c.RelSrcPath; rel != "" { | |
| // Check for vendored code first. | |
| if i := strings.Index(rel, "/vendor/"); i != -1 { | |
| rel = rel[i+8:] | |
| } | |
| // Specialized support for github and golang.org. This will cover a fair | |
| // share of the URLs, but it'd be nice to support others too. Please submit | |
| // a PR (including a unit test that I was too lazy to add yet). | |
| switch host, rest := splitHost(rel); host { | |
| case "github.com": | |
| if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 { | |
| p, srcTag, tag := splitTag(parts[1]) | |
| url := fmt.Sprintf("https://github.com/%s/%s/blob/%s/%s#L%d", escape(parts[0]), p, srcTag, escape(parts[2]), c.Line) | |
| /* #nosec G203 */ | |
| return template.URL(url), tag | |
| } | |
| log.Printf("problematic github.com URL: %q", rel) | |
| case "golang.org": | |
| // https://github.com/golang/build/blob/HEAD/repos/repos.go lists all | |
| // the golang.org/x/<foo> packages. | |
| if parts := strings.SplitN(rest, "/", 3); len(parts) == 3 && parts[0] == "x" { | |
| // parts is: "x", <project@version>, <path inside the repo>. | |
| p, srcTag, tag := splitTag(parts[1]) | |
| // The source of truth is are actually go.googlesource.com, but | |
| // github.com has nicer syntax highlighting. | |
| url := fmt.Sprintf("https://github.com/golang/%s/blob/%s/%s#L%d", p, srcTag, escape(parts[2]), c.Line) | |
| /* #nosec G203 */ | |
| return template.URL(url), tag | |
| } | |
| log.Printf("problematic golang.org URL: %q", rel) | |
| default: | |
| // For example gopkg.in. In this case there's no known way to find the | |
| // link to the source files, but we can still try to extract the version | |
| // if fetched from a go module. | |
| // Do a best effort to find a version by searching for a '@'. | |
| if i := strings.IndexByte(rel, '@'); i != -1 { | |
| if j := strings.IndexByte(rel[i:], '/'); j != -1 { | |
| tag = rel[i+1 : i+j] | |
| } | |
| } | |
| } | |
| } | |
| if c.LocalSrcPath != "" { | |
| /* #nosec G203 */ | |
| return template.URL("file:///" + escape(c.LocalSrcPath)), template.URL(tag) | |
| } | |
| if c.RemoteSrcPath != "" { | |
| /* #nosec G203 */ | |
| return template.URL("file:///" + escape(c.RemoteSrcPath)), template.URL(tag) | |
| } | |
| return "", "" | |
| } | |
| func splitHost(s string) (string, string) { | |
| parts := strings.SplitN(s, "/", 2) | |
| if len(parts) != 2 { | |
| return parts[0], "" | |
| } | |
| return parts[0], parts[1] | |
| } | |
| // "v0.0.0-20200223170610-d5e6a3e2c0ae" | |
| var reVersion = regexp.MustCompile(`v\d+\.\d+\.\d+\-\d+\-([a-f0-9]+)`) | |
| func splitTag(s string) (string, string, template.URL) { | |
| // Default to branch master for non-versioned dependencies. It's not | |
| // optimal but it's better than nothing? | |
| // TODO(maruel): Replace with HEAD. | |
| i := strings.IndexByte(s, '@') | |
| if i == -1 { | |
| // No tag was found. | |
| return s, "master", "master" | |
| } | |
| // We got a versioned go module. | |
| tag := s[i+1:] | |
| srcTag := tag | |
| if m := reVersion.FindStringSubmatch(tag); len(m) != 0 { | |
| srcTag = m[1] | |
| } | |
| /* #nosec G203 */ | |
| return s[:i], url.QueryEscape(srcTag), template.URL(url.QueryEscape(tag)) | |
| } | |
| // symbol is the hashtag to use to refer to the symbol when looking at | |
| // documentation. | |
| // | |
| // All of godoc/gddo, pkg.go.dev and golang.org/godoc use the same symbol | |
| // reference format. | |
| func symbol(f *Func) template.URL { | |
| s := f.Name | |
| if reMethodSymbol.MatchString(s) { | |
| // Transform the method form. | |
| s = reMethodSymbol.ReplaceAllString(s, "$1$2") | |
| } | |
| /* #nosec G203 */ | |
| return template.URL(url.QueryEscape(s)) | |
| } |