diff options
Diffstat (limited to 'src/cmd/vendor/github.com/google/pprof/internal/driver')
12 files changed, 1025 insertions, 619 deletions
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/commands.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/commands.go index 66e5c86b9d..16b0b0a3b5 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/commands.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/commands.go @@ -28,7 +28,6 @@ import ( "github.com/google/pprof/internal/plugin" "github.com/google/pprof/internal/report" - "github.com/google/pprof/third_party/svg" ) // commands describes the commands accepted by pprof. @@ -398,7 +397,7 @@ func massageDotSVG() PostProcessor { if err := generateSVG(input, baseSVG, ui); err != nil { return err } - _, err := output.Write([]byte(svg.Massage(baseSVG.String()))) + _, err := output.Write([]byte(massageSVG(baseSVG.String()))) return err } } diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go index bc5f366128..c2b1cd082b 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver.go @@ -54,7 +54,7 @@ func PProf(eo *plugin.Options) error { } if src.HTTPHostport != "" { - return serveWebInterface(src.HTTPHostport, p, o) + return serveWebInterface(src.HTTPHostport, p, o, true) } return interactive(p, o) } diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go index 1289a096b8..0604da911c 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/driver_test.go @@ -1487,8 +1487,14 @@ func (m *mockFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) switch r.String() { case "line[13]": return []*plugin.Sym{ - {[]string{"line1000"}, m.name, 0x1000, 0x1003}, - {[]string{"line3000"}, m.name, 0x3000, 0x3004}, + { + Name: []string{"line1000"}, File: m.name, + Start: 0x1000, End: 0x1003, + }, + { + Name: []string{"line3000"}, File: m.name, + Start: 0x3000, End: 0x3004, + }, }, nil } return nil, fmt.Errorf("unimplemented") diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go index abce5b5c70..c80a0dbc1d 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/fetch_test.go @@ -423,16 +423,9 @@ func TestHttpsInsecure(t *testing.T) { Timeout: 10, Symbolize: "remote", } - rx := "Saved profile in" - if runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64") || - runtime.GOOS == "android" { - // On iOS, $HOME points to the app root directory and is not writable. - // On Android, $HOME points to / which is not writable. - rx += "|Could not use temp dir" - } o := &plugin.Options{ Obj: &binutils.Binutils{}, - UI: &proftest.TestUI{T: t, AllowRx: rx}, + UI: &proftest.TestUI{T: t, AllowRx: "Saved profile in"}, } o.Sym = &symbolizer.Symbolizer{Obj: o.Obj, UI: o.UI} p, err := fetchProfiles(s, o) diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/flamegraph.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/flamegraph.go new file mode 100644 index 0000000000..10588d6262 --- /dev/null +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/flamegraph.go @@ -0,0 +1,99 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "encoding/json" + "html/template" + "net/http" + "strings" + + "github.com/google/pprof/internal/graph" + "github.com/google/pprof/internal/measurement" + "github.com/google/pprof/internal/report" +) + +type treeNode struct { + Name string `json:"n"` + Cum int64 `json:"v"` + CumFormat string `json:"l"` + Percent string `json:"p"` + Children []*treeNode `json:"c"` +} + +// flamegraph generates a web page containing a flamegraph. +func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) { + // Force the call tree so that the graph is a tree. + // Also do not trim the tree so that the flame graph contains all functions. + rpt, errList := ui.makeReport(w, req, []string{"svg"}, "call_tree", "true", "trim", "false") + if rpt == nil { + return // error already reported + } + + // Generate dot graph. + g, config := report.GetDOT(rpt) + var nodes []*treeNode + nroots := 0 + rootValue := int64(0) + nodeArr := []string{} + nodeMap := map[*graph.Node]*treeNode{} + // Make all nodes and the map, collect the roots. + for _, n := range g.Nodes { + v := n.CumValue() + node := &treeNode{ + Name: n.Info.PrintableName(), + Cum: v, + CumFormat: config.FormatValue(v), + Percent: strings.TrimSpace(measurement.Percentage(v, config.Total)), + } + nodes = append(nodes, node) + if len(n.In) == 0 { + nodes[nroots], nodes[len(nodes)-1] = nodes[len(nodes)-1], nodes[nroots] + nroots++ + rootValue += v + } + nodeMap[n] = node + // Get all node names into an array. + nodeArr = append(nodeArr, n.Info.Name) + } + // Populate the child links. + for _, n := range g.Nodes { + node := nodeMap[n] + for child := range n.Out { + node.Children = append(node.Children, nodeMap[child]) + } + } + + rootNode := &treeNode{ + Name: "root", + Cum: rootValue, + CumFormat: config.FormatValue(rootValue), + Percent: strings.TrimSpace(measurement.Percentage(rootValue, config.Total)), + Children: nodes[0:nroots], + } + + // JSON marshalling flame graph + b, err := json.Marshal(rootNode) + if err != nil { + http.Error(w, "error serializing flame graph", http.StatusInternalServerError) + ui.options.UI.PrintErr(err) + return + } + + ui.render(w, "/flamegraph", "flamegraph", rpt, errList, config.Labels, webArgs{ + FlameGraph: template.JS(b), + Nodes: nodeArr, + }) +} diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go index 2c36b64cc7..b893697b62 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive.go @@ -42,6 +42,9 @@ func interactive(p *profile.Profile, o *plugin.Options) error { interactiveMode = true shortcuts := profileShortcuts(p) + // Get all groups in pprofVariables to allow for clearer error messages. + groups := groupOptions(pprofVariables) + greetings(p, o.UI) for { input, err := o.UI.ReadLine("(pprof) ") @@ -87,6 +90,9 @@ func interactive(p *profile.Profile, o *plugin.Options) error { o.UI.PrintErr(err) } continue + } else if okValues := groups[name]; okValues != nil { + o.UI.PrintErr(fmt.Errorf("Unrecognized value for %s: %q. Use one of %s", name, value, strings.Join(okValues, ", "))) + continue } } @@ -118,6 +124,23 @@ func interactive(p *profile.Profile, o *plugin.Options) error { } } +// groupOptions returns a map containing all non-empty groups +// mapped to an array of the option names in that group in +// sorted order. +func groupOptions(vars variables) map[string][]string { + groups := make(map[string][]string) + for name, option := range vars { + group := option.group + if group != "" { + groups[group] = append(groups[group], name) + } + } + for _, names := range groups { + sort.Strings(names) + } + return groups +} + var generateReportWrapper = generateReport // For testing purposes. // greetings prints a brief welcome and some overall profile diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive_test.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive_test.go index ba80741a35..db26862c7d 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/interactive_test.go @@ -16,12 +16,12 @@ package driver import ( "fmt" - "io" "math/rand" "strings" "testing" "github.com/google/pprof/internal/plugin" + "github.com/google/pprof/internal/proftest" "github.com/google/pprof/internal/report" "github.com/google/pprof/profile" ) @@ -70,6 +70,21 @@ func TestShell(t *testing.T) { t.Error("second shortcut attempt:", err) } + // Group with invalid value + pprofVariables = testVariables(savedVariables) + ui := &proftest.TestUI{ + T: t, + Input: []string{"cumulative=this"}, + AllowRx: `Unrecognized value for cumulative: "this". Use one of cum, flat`, + } + o.UI = ui + if err := interactive(p, o); err != nil { + t.Error("invalid group value:", err) + } + // Confirm error message written out once. + if ui.NumAllowRxMatches != 1 { + t.Errorf("want error message to be printed 1 time, got %v", ui.NumAllowRxMatches) + } // Verify propagation of IO errors pprofVariables = testVariables(savedVariables) o.UI = newUI(t, []string{"**error**"}) @@ -144,45 +159,10 @@ func makeShortcuts(input []string, seed int) (shortcuts, []string) { } func newUI(t *testing.T, input []string) plugin.UI { - return &testUI{ - t: t, - input: input, - } -} - -type testUI struct { - t *testing.T - input []string - index int -} - -func (ui *testUI) ReadLine(_ string) (string, error) { - if ui.index >= len(ui.input) { - return "", io.EOF + return &proftest.TestUI{ + T: t, + Input: input, } - input := ui.input[ui.index] - if input == "**error**" { - return "", fmt.Errorf("Error: %s", input) - } - ui.index++ - return input, nil -} - -func (ui *testUI) Print(args ...interface{}) { -} - -func (ui *testUI) PrintErr(args ...interface{}) { - output := fmt.Sprint(args) - if output != "" { - ui.t.Error(output) - } -} - -func (ui *testUI) IsTerminal() bool { - return false -} - -func (ui *testUI) SetAutoComplete(func(string) string) { } func checkValue(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error { diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/svg.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/svg.go new file mode 100644 index 0000000000..62767e726d --- /dev/null +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/svg.go @@ -0,0 +1,80 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package driver + +import ( + "regexp" + "strings" + + "github.com/google/pprof/third_party/svgpan" +) + +var ( + viewBox = regexp.MustCompile(`<svg\s*width="[^"]+"\s*height="[^"]+"\s*viewBox="[^"]+"`) + graphID = regexp.MustCompile(`<g id="graph\d"`) + svgClose = regexp.MustCompile(`</svg>`) +) + +// massageSVG enhances the SVG output from DOT to provide better +// panning inside a web browser. It uses the svgpan library, which is +// embedded into the svgpan.JSSource variable. +func massageSVG(svg string) string { + // Work around for dot bug which misses quoting some ampersands, + // resulting on unparsable SVG. + svg = strings.Replace(svg, "&;", "&;", -1) + + // Dot's SVG output is + // + // <svg width="___" height="___" + // viewBox="___" xmlns=...> + // <g id="graph0" transform="..."> + // ... + // </g> + // </svg> + // + // Change it to + // + // <svg width="100%" height="100%" + // xmlns=...> + + // <script type="text/ecmascript"><![CDATA[` ..$(svgpan.JSSource)... `]]></script>` + // <g id="viewport" transform="translate(0,0)"> + // <g id="graph0" transform="..."> + // ... + // </g> + // </g> + // </svg> + + if loc := viewBox.FindStringIndex(svg); loc != nil { + svg = svg[:loc[0]] + + `<svg width="100%" height="100%"` + + svg[loc[1]:] + } + + if loc := graphID.FindStringIndex(svg); loc != nil { + svg = svg[:loc[0]] + + `<script type="text/ecmascript"><![CDATA[` + string(svgpan.JSSource) + `]]></script>` + + `<g id="viewport" transform="scale(0.5,0.5) translate(0,0)">` + + svg[loc[0]:] + } + + if loc := svgClose.FindStringIndex(svg); loc != nil { + svg = svg[:loc[0]] + + `</g>` + + svg[loc[0]:] + } + + return svg +} diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.weblist b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.weblist index befc412db3..0284292745 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.weblist +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/testdata/pprof.cpu.flat.addresses.weblist @@ -61,7 +61,7 @@ function pprof_toggle_asm(e) { <div class="legend">File: testbinary<br> Type: cpu<br> -Duration: 10s, Total samples = 1.12s (11.20%)<br>Total: 1.12s</div><h1>line1000</h1>testdata/file1000.src +Duration: 10s, Total samples = 1.12s (11.20%)<br>Total: 1.12s</div><h2>line1000</h2><p class="filename">testdata/file1000.src</p> <pre onClick="pprof_toggle_asm(event)"> Total: 1.10s 1.10s (flat, cum) 98.21% <span class=line> 1</span> <span class=deadsrc> 1.10s 1.10s line1 </span><span class=asm> 1.10s 1.10s 1000: instruction one <span class=unimportant>file1000.src:1</span> @@ -77,7 +77,7 @@ Duration: 10s, Total samples = 1.12s (11.20%)<br>Total: 1.12s</div><h1>line1000< <span class=line> 6</span> <span class=nop> . . line6 </span> <span class=line> 7</span> <span class=nop> . . line7 </span> </pre> -<h1>line3000</h1>testdata/file3000.src +<h2>line3000</h2><p class="filename">testdata/file3000.src</p> <pre onClick="pprof_toggle_asm(event)"> Total: 10ms 1.12s (flat, cum) 100% <span class=line> 1</span> <span class=nop> . . line1 </span> diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go index 48f0fa1cfa..5d2821cd42 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go @@ -16,208 +16,267 @@ package driver import "html/template" +import "github.com/google/pprof/third_party/d3" +import "github.com/google/pprof/third_party/d3tip" +import "github.com/google/pprof/third_party/d3flamegraph" + // addTemplates adds a set of template definitions to templates. func addTemplates(templates *template.Template) { + template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`)) + template.Must(templates.Parse(`{{define "d3tipscript"}}` + d3tip.JSSource + `{{end}}`)) + template.Must(templates.Parse(`{{define "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`)) + template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`)) template.Must(templates.Parse(` {{define "css"}} <style type="text/css"> -html { - height: 100%; - min-height: 100%; - margin: 0px; +* { + margin: 0; + padding: 0; + box-sizing: border-box; } -body { - margin: 0px; - width: 100%; +html, body { height: 100%; - min-height: 100%; - overflow: hidden; } -#graphcontainer { +body { + font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 13px; + line-height: 1.4; display: flex; flex-direction: column; - height: 100%; - min-height: 100%; - width: 100%; - min-width: 100%; - margin: 0px; } -#graph { - flex: 1 1 auto; - overflow: hidden; +a { + color: #2a66d9; +} +.header { + display: flex; + align-items: center; + height: 44px; + min-height: 44px; + background-color: #eee; + color: #212121; + padding: 0 1rem; +} +.header > div { + margin: 0 0.125em; +} +.header .title h1 { + font-size: 1.75em; + margin-right: 1rem; +} +.header .title a { + color: #212121; + text-decoration: none; } -svg { +.header .title a:hover { + text-decoration: underline; +} +.header .description { width: 100%; - height: auto; + text-align: right; + white-space: nowrap; } -button { - margin-top: 5px; - margin-bottom: 5px; +@media screen and (max-width: 799px) { + .header input { + display: none; + } } -#detailtext { +#detailsbox { display: none; + z-index: 1; position: fixed; - top: 20px; - right: 10px; + top: 40px; + right: 20px; background-color: #ffffff; - min-width: 160px; - border: 1px solid #888; - box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); - z-index: 1; -} -#closedetails { - float: right; - margin: 2px; + box-shadow: 0 1px 5px rgba(0,0,0,.3); + line-height: 24px; + padding: 1em; + text-align: left; } -#home { - font-size: 14pt; - padding-left: 0.5em; - padding-right: 0.5em; - float: right; +.header input { + background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' style='pointer-events:none;display:block;width:100%25;height:100%25;fill:#757575'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61.0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 4px center/20px 20px; + border: 1px solid #d1d2d3; + border-radius: 2px 0 0 2px; + padding: 0.25em; + padding-left: 28px; + margin-left: 1em; + font-family: 'Roboto', 'Noto', sans-serif; + font-size: 1em; + line-height: 24px; + color: #212121; } -.menubar { - display: inline-block; - background-color: #f8f8f8; - border: 1px solid #ccc; - width: 100%; +.downArrow { + border-top: .36em solid #ccc; + border-left: .36em solid transparent; + border-right: .36em solid transparent; + margin-bottom: .05em; + margin-left: .5em; + transition: border-top-color 200ms; } -.menu-header { +.menu-item { + height: 100%; + text-transform: uppercase; + font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; position: relative; - display: inline-block; - padding: 2px 2px; - font-size: 14pt; } -.menu { +.menu-item .menu-name:hover { + opacity: 0.75; +} +.menu-item .menu-name:hover .downArrow { + border-top-color: #666; +} +.menu-name { + height: 100%; + padding: 0 0.5em; + display: flex; + align-items: center; + justify-content: center; +} +.submenu { display: none; - position: absolute; - background-color: #f8f8f8; - border: 1px solid #888; - box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); z-index: 1; - margin-top: 2px; + margin-top: -4px; + min-width: 10em; + position: absolute; left: 0px; - min-width: 5em; + background-color: white; + box-shadow: 0 1px 5px rgba(0,0,0,.3); + font-size: 100%; + text-transform: none; } -.menu-header, .menu { - cursor: default; +.menu-item, .submenu { user-select: none; -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; } -.menu hr { - background-color: #fff; - margin-top: 0px; - margin-bottom: 0px; +.submenu hr { + border: 0; + border-top: 2px solid #eee; } -.menu a, .menu button { +.submenu a { display: block; - width: 100%; - margin: 0px; - padding: 2px 0px 2px 0px; - text-align: left; + padding: .5em 1em; text-decoration: none; - color: #000; - background-color: #f8f8f8; - font-size: 12pt; - border: none; } -.menu-header:hover { - background-color: #ccc; +.submenu a:hover, .submenu a.active { + color: white; + background-color: #6b82d6; } -.menu a:hover, .menu button:hover { - background-color: #ccc; -} -.menu a.disabled { +.submenu a.disabled { color: gray; pointer-events: none; } -#searchbox { - margin-left: 10pt; + +#content { + overflow-y: scroll; + padding: 1em; +} +#top { + overflow-y: scroll; +} +#graph { + overflow: hidden; } -#bodycontainer { +#graph svg { width: 100%; - height: 100%; - max-height: 100%; - overflow: scroll; - padding-top: 5px; + height: auto; + padding: 10px; } -#toptable { +#content.source .filename { + margin-top: 0; + margin-bottom: 1em; + font-size: 120%; +} +#content.source pre { + margin-bottom: 3em; +} +table { border-spacing: 0px; width: 100%; padding-bottom: 1em; + white-space: nowrap; +} +table thead { + font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; } -#toptable tr th { - border-bottom: 1px solid black; +table tr th { + background-color: #ddd; text-align: right; - padding-left: 1em; - padding-top: 0.2em; - padding-bottom: 0.2em; + padding: .3em .5em; } -#toptable tr td { - padding-left: 1em; - font: monospace; +table tr td { + padding: .3em .5em; text-align: right; - white-space: nowrap; - cursor: default; } -#toptable tr th:nth-child(6), -#toptable tr th:nth-child(7), -#toptable tr td:nth-child(6), -#toptable tr td:nth-child(7) { +#top table tr th:nth-child(6), +#top table tr th:nth-child(7), +#top table tr td:nth-child(6), +#top table tr td:nth-child(7) { text-align: left; } -#toptable tr td:nth-child(6) { - max-width: 30em; // Truncate very long names +#top table tr td:nth-child(6) { + width: 100%; + text-overflow: ellipsis; overflow: hidden; + white-space: nowrap; } #flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr { cursor: ns-resize; } .hilite { - background-color: #ccf; + background-color: #ebf5fb; + font-weight: bold; } </style> {{end}} {{define "header"}} -<div id="detailtext"> -<button id="closedetails">Close</button> -{{range .Legend}}<div>{{.}}</div>{{end}} -</div> +<div class="header"> + <div class="title"> + <h1><a href="/">pprof</a></h1> + </div> -<div class="menubar"> + <div id="view" class="menu-item"> + <div class="menu-name"> + View + <i class="downArrow"></i> + </div> + <div class="submenu"> + <a title="{{.Help.top}}" href="/top" id="topbtn">Top</a> + <a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a> + <a title="{{.Help.flamegraph}}" href="/flamegraph" id="flamegraph">Flame Graph</a> + <a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a> + <a title="{{.Help.list}}" href="/source" id="list">Source</a> + <a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a> + </div> + </div> -<div class="menu-header"> -View -<div class="menu"> -<a title="{{.Help.top}}" href="/top" id="topbtn">Top</a> -<a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a> -<a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a> -<a title="{{.Help.list}}" href="/source" id="list">Source</a> -<a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a> -<hr> -<button title="{{.Help.details}}" id="details">Details</button> -</div> -</div> + <div id="refine" class="menu-item"> + <div class="menu-name"> + Refine + <i class="downArrow"></i> + </div> + <div class="submenu"> + <a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> + <a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> + <a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> + <a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> + <hr> + <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> + </div> + </div> -<div class="menu-header"> -Refine -<div class="menu"> -<a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> -<a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> -<a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> -<a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> -<hr> -<a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> -</div> -</div> + <div> + <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> + </div> -<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> - -<span id="home">{{.Title}}</span> - -</div> <!-- menubar --> + <div class="description"> + <a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a> + <div id="detailsbox"> + {{range .Legend}}<div>{{.}}</div>{{end}} + </div> + </div> +</div> <div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div> {{end}} @@ -226,21 +285,17 @@ Refine <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} </head> <body> - -{{template "header" .}} -<div id="graphcontainer"> -<div id="graph"> -{{.HTMLBody}} -</div> - -</div> -{{template "script" .}} -<script>viewer({{.BaseURL}}, {{.Nodes}})</script> + {{template "header" .}} + <div id="graph"> + {{.HTMLBody}} + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, {{.Nodes}});</script> </body> </html> {{end}} @@ -253,58 +308,58 @@ function initPanAndZoom(svg, clickHandler) { 'use strict'; // Current mouse/touch handling mode - const IDLE = 0 - const MOUSEPAN = 1 - const TOUCHPAN = 2 - const TOUCHZOOM = 3 - let mode = IDLE + const IDLE = 0; + const MOUSEPAN = 1; + const TOUCHPAN = 2; + const TOUCHZOOM = 3; + let mode = IDLE; // State needed to implement zooming. - let currentScale = 1.0 - const initWidth = svg.viewBox.baseVal.width - const initHeight = svg.viewBox.baseVal.height + let currentScale = 1.0; + const initWidth = svg.viewBox.baseVal.width; + const initHeight = svg.viewBox.baseVal.height; // State needed to implement panning. - let panLastX = 0 // Last event X coordinate - let panLastY = 0 // Last event Y coordinate - let moved = false // Have we seen significant movement - let touchid = null // Current touch identifier + let panLastX = 0; // Last event X coordinate + let panLastY = 0; // Last event Y coordinate + let moved = false; // Have we seen significant movement + let touchid = null; // Current touch identifier // State needed for pinch zooming - let touchid2 = null // Second id for pinch zooming - let initGap = 1.0 // Starting gap between two touches - let initScale = 1.0 // currentScale when pinch zoom started - let centerPoint = null // Center point for scaling + let touchid2 = null; // Second id for pinch zooming + let initGap = 1.0; // Starting gap between two touches + let initScale = 1.0; // currentScale when pinch zoom started + let centerPoint = null; // Center point for scaling // Convert event coordinates to svg coordinates. function toSvg(x, y) { - const p = svg.createSVGPoint() - p.x = x - p.y = y - let m = svg.getCTM() - if (m == null) m = svg.getScreenCTM() // Firefox workaround. - return p.matrixTransform(m.inverse()) + const p = svg.createSVGPoint(); + p.x = x; + p.y = y; + let m = svg.getCTM(); + if (m == null) m = svg.getScreenCTM(); // Firefox workaround. + return p.matrixTransform(m.inverse()); } // Change the scaling for the svg to s, keeping the point denoted // by u (in svg coordinates]) fixed at the same screen location. function rescale(s, u) { // Limit to a good range. - if (s < 0.2) s = 0.2 - if (s > 10.0) s = 10.0 + if (s < 0.2) s = 0.2; + if (s > 10.0) s = 10.0; - currentScale = s + currentScale = s; // svg.viewBox defines the visible portion of the user coordinate // system. So to magnify by s, divide the visible portion by s, // which will then be stretched to fit the viewport. - const vb = svg.viewBox - const w1 = vb.baseVal.width - const w2 = initWidth / s - const h1 = vb.baseVal.height - const h2 = initHeight / s - vb.baseVal.width = w2 - vb.baseVal.height = h2 + const vb = svg.viewBox; + const w1 = vb.baseVal.width; + const w2 = initWidth / s; + const h1 = vb.baseVal.height; + const h2 = initHeight / s; + vb.baseVal.width = w2; + vb.baseVal.height = h2; // We also want to adjust vb.baseVal.x so that u.x remains at same // screen X coordinate. In other words, want to change it from x1 to x2 @@ -313,154 +368,154 @@ function initPanAndZoom(svg, clickHandler) { // Simplifying that, we get // (u.x - x1) * (w2 / w1) = u.x - x2 // x2 = u.x - (u.x - x1) * (w2 / w1) - vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1) - vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1) + vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1); + vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1); } function handleWheel(e) { - if (e.deltaY == 0) return + if (e.deltaY == 0) return; // Change scale factor by 1.1 or 1/1.1 rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)), - toSvg(e.offsetX, e.offsetY)) + toSvg(e.offsetX, e.offsetY)); } function setMode(m) { - mode = m - touchid = null - touchid2 = null + mode = m; + touchid = null; + touchid2 = null; } function panStart(x, y) { - moved = false - panLastX = x - panLastY = y + moved = false; + panLastX = x; + panLastY = y; } function panMove(x, y) { - let dx = x - panLastX - let dy = y - panLastY - if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return // Ignore tiny moves + let dx = x - panLastX; + let dy = y - panLastY; + if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves - moved = true - panLastX = x - panLastY = y + moved = true; + panLastX = x; + panLastY = y; // Firefox workaround: get dimensions from parentNode. - const swidth = svg.clientWidth || svg.parentNode.clientWidth - const sheight = svg.clientHeight || svg.parentNode.clientHeight + const swidth = svg.clientWidth || svg.parentNode.clientWidth; + const sheight = svg.clientHeight || svg.parentNode.clientHeight; // Convert deltas from screen space to svg space. - dx *= (svg.viewBox.baseVal.width / swidth) - dy *= (svg.viewBox.baseVal.height / sheight) + dx *= (svg.viewBox.baseVal.width / swidth); + dy *= (svg.viewBox.baseVal.height / sheight); - svg.viewBox.baseVal.x -= dx - svg.viewBox.baseVal.y -= dy + svg.viewBox.baseVal.x -= dx; + svg.viewBox.baseVal.y -= dy; } function handleScanStart(e) { - if (e.button != 0) return // Do not catch right-clicks etc. - setMode(MOUSEPAN) - panStart(e.clientX, e.clientY) - e.preventDefault() - svg.addEventListener("mousemove", handleScanMove) + if (e.button != 0) return; // Do not catch right-clicks etc. + setMode(MOUSEPAN); + panStart(e.clientX, e.clientY); + e.preventDefault(); + svg.addEventListener('mousemove', handleScanMove); } function handleScanMove(e) { if (e.buttons == 0) { // Missed an end event, perhaps because mouse moved outside window. - setMode(IDLE) - svg.removeEventListener("mousemove", handleScanMove) - return + setMode(IDLE); + svg.removeEventListener('mousemove', handleScanMove); + return; } - if (mode == MOUSEPAN) panMove(e.clientX, e.clientY) + if (mode == MOUSEPAN) panMove(e.clientX, e.clientY); } function handleScanEnd(e) { - if (mode == MOUSEPAN) panMove(e.clientX, e.clientY) - setMode(IDLE) - svg.removeEventListener("mousemove", handleScanMove) - if (!moved) clickHandler(e.target) + if (mode == MOUSEPAN) panMove(e.clientX, e.clientY); + setMode(IDLE); + svg.removeEventListener('mousemove', handleScanMove); + if (!moved) clickHandler(e.target); } // Find touch object with specified identifier. function findTouch(tlist, id) { for (const t of tlist) { - if (t.identifier == id) return t + if (t.identifier == id) return t; } - return null + return null; } - // Return distance between two touch points + // Return distance between two touch points function touchGap(t1, t2) { - const dx = t1.clientX - t2.clientX - const dy = t1.clientY - t2.clientY - return Math.hypot(dx, dy) + const dx = t1.clientX - t2.clientX; + const dy = t1.clientY - t2.clientY; + return Math.hypot(dx, dy); } function handleTouchStart(e) { if (mode == IDLE && e.changedTouches.length == 1) { // Start touch based panning - const t = e.changedTouches[0] - setMode(TOUCHPAN) - touchid = t.identifier - panStart(t.clientX, t.clientY) - e.preventDefault() + const t = e.changedTouches[0]; + setMode(TOUCHPAN); + touchid = t.identifier; + panStart(t.clientX, t.clientY); + e.preventDefault(); } else if (mode == TOUCHPAN && e.touches.length == 2) { // Start pinch zooming - setMode(TOUCHZOOM) - const t1 = e.touches[0] - const t2 = e.touches[1] - touchid = t1.identifier - touchid2 = t2.identifier - initScale = currentScale - initGap = touchGap(t1, t2) + setMode(TOUCHZOOM); + const t1 = e.touches[0]; + const t2 = e.touches[1]; + touchid = t1.identifier; + touchid2 = t2.identifier; + initScale = currentScale; + initGap = touchGap(t1, t2); centerPoint = toSvg((t1.clientX + t2.clientX) / 2, - (t1.clientY + t2.clientY) / 2) - e.preventDefault() + (t1.clientY + t2.clientY) / 2); + e.preventDefault(); } } function handleTouchMove(e) { if (mode == TOUCHPAN) { - const t = findTouch(e.changedTouches, touchid) - if (t == null) return + const t = findTouch(e.changedTouches, touchid); + if (t == null) return; if (e.touches.length != 1) { - setMode(IDLE) - return + setMode(IDLE); + return; } - panMove(t.clientX, t.clientY) - e.preventDefault() + panMove(t.clientX, t.clientY); + e.preventDefault(); } else if (mode == TOUCHZOOM) { // Get two touches; new gap; rescale to ratio. - const t1 = findTouch(e.touches, touchid) - const t2 = findTouch(e.touches, touchid2) - if (t1 == null || t2 == null) return - const gap = touchGap(t1, t2) - rescale(initScale * gap / initGap, centerPoint) - e.preventDefault() + const t1 = findTouch(e.touches, touchid); + const t2 = findTouch(e.touches, touchid2); + if (t1 == null || t2 == null) return; + const gap = touchGap(t1, t2); + rescale(initScale * gap / initGap, centerPoint); + e.preventDefault(); } } function handleTouchEnd(e) { if (mode == TOUCHPAN) { - const t = findTouch(e.changedTouches, touchid) - if (t == null) return - panMove(t.clientX, t.clientY) - setMode(IDLE) - e.preventDefault() - if (!moved) clickHandler(t.target) + const t = findTouch(e.changedTouches, touchid); + if (t == null) return; + panMove(t.clientX, t.clientY); + setMode(IDLE); + e.preventDefault(); + if (!moved) clickHandler(t.target); } else if (mode == TOUCHZOOM) { - setMode(IDLE) - e.preventDefault() + setMode(IDLE); + e.preventDefault(); } } - svg.addEventListener("mousedown", handleScanStart) - svg.addEventListener("mouseup", handleScanEnd) - svg.addEventListener("touchstart", handleTouchStart) - svg.addEventListener("touchmove", handleTouchMove) - svg.addEventListener("touchend", handleTouchEnd) - svg.addEventListener("wheel", handleWheel, true) + svg.addEventListener('mousedown', handleScanStart); + svg.addEventListener('mouseup', handleScanEnd); + svg.addEventListener('touchstart', handleTouchStart); + svg.addEventListener('touchmove', handleTouchMove); + svg.addEventListener('touchend', handleTouchEnd); + svg.addEventListener('wheel', handleWheel, true); } function initMenus() { @@ -471,41 +526,42 @@ function initMenus() { function cancelActiveMenu() { if (activeMenu == null) return; - activeMenu.style.display = "none"; + activeMenu.style.display = 'none'; activeMenu = null; activeMenuHdr = null; } // Set click handlers on every menu header. - for (const menu of document.getElementsByClassName("menu")) { + for (const menu of document.getElementsByClassName('submenu')) { const hdr = menu.parentElement; if (hdr == null) return; + if (hdr.classList.contains('disabled')) return; function showMenu(e) { // menu is a child of hdr, so this event can fire for clicks // inside menu. Ignore such clicks. - if (e.target != hdr) return; + if (e.target.parentElement != hdr) return; activeMenu = menu; activeMenuHdr = hdr; - menu.style.display = "block"; + menu.style.display = 'block'; } - hdr.addEventListener("mousedown", showMenu); - hdr.addEventListener("touchstart", showMenu); + hdr.addEventListener('mousedown', showMenu); + hdr.addEventListener('touchstart', showMenu); } // If there is an active menu and a down event outside, retract the menu. - for (const t of ["mousedown", "touchstart"]) { + for (const t of ['mousedown', 'touchstart']) { document.addEventListener(t, (e) => { // Note: to avoid unnecessary flicker, if the down event is inside // the active menu header, do not retract the menu. - if (activeMenuHdr != e.target.closest(".menu-header")) { + if (activeMenuHdr != e.target.closest('.menu-item')) { cancelActiveMenu(); } }, { passive: true, capture: true }); } // If there is an active menu and an up event inside, retract the menu. - document.addEventListener("mouseup", (e) => { - if (activeMenu == e.target.closest(".menu")) { + document.addEventListener('mouseup', (e) => { + if (activeMenu == e.target.closest('.submenu')) { cancelActiveMenu(); } }, { passive: true, capture: true }); @@ -515,282 +571,283 @@ function viewer(baseUrl, nodes) { 'use strict'; // Elements - const search = document.getElementById("searchbox") - const graph0 = document.getElementById("graph0") - const svg = (graph0 == null ? null : graph0.parentElement) - const toptable = document.getElementById("toptable") + const search = document.getElementById('search'); + const graph0 = document.getElementById('graph0'); + const svg = (graph0 == null ? null : graph0.parentElement); + const toptable = document.getElementById('toptable'); - let regexpActive = false - let selected = new Map() - let origFill = new Map() - let searchAlarm = null - let buttonsEnabled = true + let regexpActive = false; + let selected = new Map(); + let origFill = new Map(); + let searchAlarm = null; + let buttonsEnabled = true; - function handleDetails() { - const detailsText = document.getElementById("detailtext") - if (detailsText != null) detailsText.style.display = "block" - } - - function handleCloseDetails() { - const detailsText = document.getElementById("detailtext") - if (detailsText != null) detailsText.style.display = "none" + function handleDetails(e) { + e.preventDefault(); + const detailsText = document.getElementById('detailsbox'); + if (detailsText != null) { + if (detailsText.style.display === 'block') { + detailsText.style.display = 'none'; + } else { + detailsText.style.display = 'block'; + } + } } function handleKey(e) { - if (e.keyCode != 13) return + if (e.keyCode != 13) return; window.location.href = - updateUrl(new URL({{.BaseURL}}, window.location.href), "f") - e.preventDefault() + updateUrl(new URL({{.BaseURL}}, window.location.href), 'f'); + e.preventDefault(); } function handleSearch() { // Delay expensive processing so a flurry of key strokes is handled once. if (searchAlarm != null) { - clearTimeout(searchAlarm) + clearTimeout(searchAlarm); } - searchAlarm = setTimeout(selectMatching, 300) + searchAlarm = setTimeout(selectMatching, 300); - regexpActive = true - updateButtons() + regexpActive = true; + updateButtons(); } function selectMatching() { - searchAlarm = null - let re = null - if (search.value != "") { + searchAlarm = null; + let re = null; + if (search.value != '') { try { - re = new RegExp(search.value) + re = new RegExp(search.value); } catch (e) { // TODO: Display error state in search box - return + return; } } function match(text) { - return re != null && re.test(text) + return re != null && re.test(text); } // drop currently selected items that do not match re. selected.forEach(function(v, n) { if (!match(nodes[n])) { - unselect(n, document.getElementById("node" + n)) + unselect(n, document.getElementById('node' + n)); } }) // add matching items that are not currently selected. for (let n = 0; n < nodes.length; n++) { if (!selected.has(n) && match(nodes[n])) { - select(n, document.getElementById("node" + n)) + select(n, document.getElementById('node' + n)); } } - updateButtons() + updateButtons(); } function toggleSvgSelect(elem) { // Walk up to immediate child of graph0 while (elem != null && elem.parentElement != graph0) { - elem = elem.parentElement + elem = elem.parentElement; } - if (!elem) return + if (!elem) return; // Disable regexp mode. - regexpActive = false + regexpActive = false; - const n = nodeId(elem) - if (n < 0) return + const n = nodeId(elem); + if (n < 0) return; if (selected.has(n)) { - unselect(n, elem) + unselect(n, elem); } else { - select(n, elem) + select(n, elem); } - updateButtons() + updateButtons(); } function unselect(n, elem) { - if (elem == null) return - selected.delete(n) - setBackground(elem, false) + if (elem == null) return; + selected.delete(n); + setBackground(elem, false); } function select(n, elem) { - if (elem == null) return - selected.set(n, true) - setBackground(elem, true) + if (elem == null) return; + selected.set(n, true); + setBackground(elem, true); } function nodeId(elem) { - const id = elem.id - if (!id) return -1 - if (!id.startsWith("node")) return -1 - const n = parseInt(id.slice(4), 10) - if (isNaN(n)) return -1 - if (n < 0 || n >= nodes.length) return -1 - return n + const id = elem.id; + if (!id) return -1; + if (!id.startsWith('node')) return -1; + const n = parseInt(id.slice(4), 10); + if (isNaN(n)) return -1; + if (n < 0 || n >= nodes.length) return -1; + return n; } function setBackground(elem, set) { // Handle table row highlighting. - if (elem.nodeName == "TR") { - elem.classList.toggle("hilite", set) - return + if (elem.nodeName == 'TR') { + elem.classList.toggle('hilite', set); + return; } // Handle svg element highlighting. - const p = findPolygon(elem) + const p = findPolygon(elem); if (p != null) { if (set) { - origFill.set(p, p.style.fill) - p.style.fill = "#ccccff" + origFill.set(p, p.style.fill); + p.style.fill = '#ccccff'; } else if (origFill.has(p)) { - p.style.fill = origFill.get(p) + p.style.fill = origFill.get(p); } } } function findPolygon(elem) { - if (elem.localName == "polygon") return elem + if (elem.localName == 'polygon') return elem; for (const c of elem.children) { - const p = findPolygon(c) - if (p != null) return p + const p = findPolygon(c); + if (p != null) return p; } - return null + return null; } // convert a string to a regexp that matches that string. function quotemeta(str) { - return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1') + return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1'); } // Update id's href to reflect current selection whenever it is // liable to be followed. function makeLinkDynamic(id) { - const elem = document.getElementById(id) - if (elem == null) return + const elem = document.getElementById(id); + if (elem == null) return; - // Most links copy current selection into the "f" parameter, + // Most links copy current selection into the 'f' parameter, // but Refine menu links are different. - let param = "f" - if (id == "ignore") param = "i" - if (id == "hide") param = "h" - if (id == "show") param = "s" + let param = 'f'; + if (id == 'ignore') param = 'i'; + if (id == 'hide') param = 'h'; + if (id == 'show') param = 's'; // We update on mouseenter so middle-click/right-click work properly. - elem.addEventListener("mouseenter", updater) - elem.addEventListener("touchstart", updater) + elem.addEventListener('mouseenter', updater); + elem.addEventListener('touchstart', updater); function updater() { - elem.href = updateUrl(new URL(elem.href), param) + elem.href = updateUrl(new URL(elem.href), param); } } // Update URL to reflect current selection. function updateUrl(url, param) { - url.hash = "" + url.hash = ''; // The selection can be in one of two modes: regexp-based or // list-based. Construct regular expression depending on mode. let re = regexpActive - ? search.value - : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join("|") + ? search.value + : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|'); // Copy params from this page's URL. - const params = url.searchParams + const params = url.searchParams; for (const p of new URLSearchParams(window.location.search)) { - params.set(p[0], p[1]) + params.set(p[0], p[1]); } - if (re != "") { + if (re != '') { // For focus/show, forget old parameter. For others, add to re. - if (param != "f" && param != "s" && params.has(param)) { - const old = params.get(param) - if (old != "") { - re += "|" + old + if (param != 'f' && param != 's' && params.has(param)) { + const old = params.get(param); + if (old != '') { + re += '|' + old; } } - params.set(param, re) + params.set(param, re); } else { - params.delete(param) + params.delete(param); } - return url.toString() + return url.toString(); } function handleTopClick(e) { // Walk back until we find TR and then get the Name column (index 5) - let elem = e.target - while (elem != null && elem.nodeName != "TR") { - elem = elem.parentElement + let elem = e.target; + while (elem != null && elem.nodeName != 'TR') { + elem = elem.parentElement; } - if (elem == null || elem.children.length < 6) return + if (elem == null || elem.children.length < 6) return; - e.preventDefault() - const tr = elem - const td = elem.children[5] - if (td.nodeName != "TD") return - const name = td.innerText - const index = nodes.indexOf(name) - if (index < 0) return + e.preventDefault(); + const tr = elem; + const td = elem.children[5]; + if (td.nodeName != 'TD') return; + const name = td.innerText; + const index = nodes.indexOf(name); + if (index < 0) return; // Disable regexp mode. - regexpActive = false + regexpActive = false; if (selected.has(index)) { - unselect(index, elem) + unselect(index, elem); } else { - select(index, elem) + select(index, elem); } - updateButtons() + updateButtons(); } function updateButtons() { - const enable = (search.value != "" || selected.size != 0) - if (buttonsEnabled == enable) return - buttonsEnabled = enable - for (const id of ["focus", "ignore", "hide", "show"]) { - const link = document.getElementById(id) + const enable = (search.value != '' || selected.size != 0); + if (buttonsEnabled == enable) return; + buttonsEnabled = enable; + for (const id of ['focus', 'ignore', 'hide', 'show']) { + const link = document.getElementById(id); if (link != null) { - link.classList.toggle("disabled", !enable) + link.classList.toggle('disabled', !enable); } } } // Initialize button states - updateButtons() + updateButtons(); // Setup event handlers - initMenus() + initMenus(); if (svg != null) { - initPanAndZoom(svg, toggleSvgSelect) + initPanAndZoom(svg, toggleSvgSelect); } if (toptable != null) { - toptable.addEventListener("mousedown", handleTopClick) - toptable.addEventListener("touchstart", handleTopClick) + toptable.addEventListener('mousedown', handleTopClick); + toptable.addEventListener('touchstart', handleTopClick); } - const ids = ["topbtn", "graphbtn", "peek", "list", "disasm", - "focus", "ignore", "hide", "show"] - ids.forEach(makeLinkDynamic) + const ids = ['topbtn', 'graphbtn', 'peek', 'list', 'disasm', + 'focus', 'ignore', 'hide', 'show']; + ids.forEach(makeLinkDynamic); // Bind action to button with specified id. function addAction(id, action) { - const btn = document.getElementById(id) + const btn = document.getElementById(id); if (btn != null) { - btn.addEventListener("click", action) - btn.addEventListener("touchstart", action) + btn.addEventListener('click', action); + btn.addEventListener('touchstart', action); } } - addAction("details", handleDetails) - addAction("closedetails", handleCloseDetails) + addAction('details', handleDetails); - search.addEventListener("input", handleSearch) - search.addEventListener("keydown", handleKey) + search.addEventListener('input', handleSearch); + search.addEventListener('keydown', handleKey); // Give initial focus to main container so it can be scrolled using keys. - const main = document.getElementById("bodycontainer") + const main = document.getElementById('bodycontainer'); if (main) { - main.focus() + main.focus(); } } </script> @@ -800,116 +857,115 @@ function viewer(baseUrl, nodes) { <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} -<style type="text/css"> -</style> + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} + <style type="text/css"> + </style> </head> <body> + {{template "header" .}} + <div id="top"> + <table id="toptable"> + <thead> + <tr> + <th id="flathdr1">Flat</th> + <th id="flathdr2">Flat%</th> + <th>Sum%</th> + <th id="cumhdr1">Cum</th> + <th id="cumhdr2">Cum%</th> + <th id="namehdr">Name</th> + <th>Inlined?</th> + </tr> + </thead> + <tbody id="rows"></tbody> + </table> + </div> + {{template "script" .}} + <script> + function makeTopTable(total, entries) { + const rows = document.getElementById('rows'); + if (rows == null) return; -{{template "header" .}} - -<div id="bodycontainer"> -<table id="toptable"> -<tr> -<th id="flathdr1">Flat -<th id="flathdr2">Flat% -<th>Sum% -<th id="cumhdr1">Cum -<th id="cumhdr2">Cum% -<th id="namehdr">Name -<th>Inlined?</tr> -<tbody id="rows"> -</tbody> -</table> -</div> + // Store initial index in each entry so we have stable node ids for selection. + for (let i = 0; i < entries.length; i++) { + entries[i].Id = 'node' + i; + } -{{template "script" .}} -<script> -function makeTopTable(total, entries) { - const rows = document.getElementById("rows") - if (rows == null) return + // Which column are we currently sorted by and in what order? + let currentColumn = ''; + let descending = false; + sortBy('Flat'); - // Store initial index in each entry so we have stable node ids for selection. - for (let i = 0; i < entries.length; i++) { - entries[i].Id = "node" + i - } + function sortBy(column) { + // Update sort criteria + if (column == currentColumn) { + descending = !descending; // Reverse order + } else { + currentColumn = column; + descending = (column != 'Name'); + } - // Which column are we currently sorted by and in what order? - let currentColumn = "" - let descending = false - sortBy("Flat") + // Sort according to current criteria. + function cmp(a, b) { + const av = a[currentColumn]; + const bv = b[currentColumn]; + if (av < bv) return -1; + if (av > bv) return +1; + return 0; + } + entries.sort(cmp); + if (descending) entries.reverse(); - function sortBy(column) { - // Update sort criteria - if (column == currentColumn) { - descending = !descending // Reverse order - } else { - currentColumn = column - descending = (column != "Name") - } + function addCell(tr, val) { + const td = document.createElement('td'); + td.textContent = val; + tr.appendChild(td); + } - // Sort according to current criteria. - function cmp(a, b) { - const av = a[currentColumn] - const bv = b[currentColumn] - if (av < bv) return -1 - if (av > bv) return +1 - return 0 - } - entries.sort(cmp) - if (descending) entries.reverse() + function percent(v) { + return (v * 100.0 / total).toFixed(2) + '%'; + } - function addCell(tr, val) { - const td = document.createElement('td') - td.textContent = val - tr.appendChild(td) - } + // Generate rows + const fragment = document.createDocumentFragment(); + let sum = 0; + for (const row of entries) { + const tr = document.createElement('tr'); + tr.id = row.Id; + sum += row.Flat; + addCell(tr, row.FlatFormat); + addCell(tr, percent(row.Flat)); + addCell(tr, percent(sum)); + addCell(tr, row.CumFormat); + addCell(tr, percent(row.Cum)); + addCell(tr, row.Name); + addCell(tr, row.InlineLabel); + fragment.appendChild(tr); + } - function percent(v) { - return (v * 100.0 / total).toFixed(2) + "%" - } + rows.textContent = ''; // Remove old rows + rows.appendChild(fragment); + } - // Generate rows - const fragment = document.createDocumentFragment() - let sum = 0 - for (const row of entries) { - const tr = document.createElement('tr') - tr.id = row.Id - sum += row.Flat - addCell(tr, row.FlatFormat) - addCell(tr, percent(row.Flat)) - addCell(tr, percent(sum)) - addCell(tr, row.CumFormat) - addCell(tr, percent(row.Cum)) - addCell(tr, row.Name) - addCell(tr, row.InlineLabel) - fragment.appendChild(tr) + // Make different column headers trigger sorting. + function bindSort(id, column) { + const hdr = document.getElementById(id); + if (hdr == null) return; + const fn = function() { sortBy(column) }; + hdr.addEventListener('click', fn); + hdr.addEventListener('touch', fn); + } + bindSort('flathdr1', 'Flat'); + bindSort('flathdr2', 'Flat'); + bindSort('cumhdr1', 'Cum'); + bindSort('cumhdr2', 'Cum'); + bindSort('namehdr', 'Name'); } - rows.textContent = '' // Remove old rows - rows.appendChild(fragment) - } - - // Make different column headers trigger sorting. - function bindSort(id, column) { - const hdr = document.getElementById(id) - if (hdr == null) return - const fn = function() { sortBy(column) } - hdr.addEventListener("click", fn) - hdr.addEventListener("touch", fn) - } - bindSort("flathdr1", "Flat") - bindSort("flathdr2", "Flat") - bindSort("cumhdr1", "Cum") - bindSort("cumhdr2", "Cum") - bindSort("namehdr", "Name") -} - -viewer({{.BaseURL}}, {{.Nodes}}) -makeTopTable({{.Total}}, {{.Top}}) -</script> + viewer({{.BaseURL}}, {{.Nodes}}); + makeTopTable({{.Total}}, {{.Top}}); + </script> </body> </html> {{end}} @@ -918,22 +974,19 @@ makeTopTable({{.Total}}, {{.Top}}) <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} -{{template "weblistcss" .}} -{{template "weblistjs" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} + {{template "weblistcss" .}} + {{template "weblistjs" .}} </head> <body> - -{{template "header" .}} - -<div id="bodycontainer"> -{{.HTMLBody}} -</div> - -{{template "script" .}} -<script>viewer({{.BaseURL}}, null)</script> + {{template "header" .}} + <div id="content" class="source"> + {{.HTMLBody}} + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, null);</script> </body> </html> {{end}} @@ -942,22 +995,131 @@ makeTopTable({{.Total}}, {{.Top}}) <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} +</head> +<body> + {{template "header" .}} + <div id="content"> + <pre> + {{.TextBody}} + </pre> + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, null);</script> +</body> +</html> +{{end}} + +{{define "flamegraph" -}} +<!DOCTYPE html> +<html> +<head> + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} + <style type="text/css">{{template "d3flamegraphcss" .}}</style> + <style type="text/css"> + .flamegraph-content { + width: 90%; + min-width: 80%; + margin-left: 5%; + } + .flamegraph-details { + height: 1.2em; + width: 90%; + min-width: 90%; + margin-left: 5%; + padding-bottom: 41px; + } + </style> </head> <body> + {{template "header" .}} + <div id="bodycontainer"> + <div class="flamegraph-content"> + <div id="chart"></div> + </div> + <div id="flamegraphdetails" class="flamegraph-details"></div> + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, {{.Nodes}});</script> + <script>{{template "d3script" .}}</script> + <script>{{template "d3tipscript" .}}</script> + <script>{{template "d3flamegraphscript" .}}</script> + <script type="text/javascript"> + var data = {{.FlameGraph}}; + var label = function(d) { + return d.data.n + ' (' + d.data.p + ', ' + d.data.l + ')'; + }; -{{template "header" .}} + var width = document.getElementById('chart').clientWidth; -<div id="bodycontainer"> -<pre> -{{.TextBody}} -</pre> -</div> + var flameGraph = d3.flameGraph() + .width(width) + .cellHeight(18) + .minFrameSize(1) + .transitionDuration(750) + .transitionEase(d3.easeCubic) + .sort(true) + .title('') + .label(label) + .details(document.getElementById('flamegraphdetails')); + + var tip = d3.tip() + .direction('s') + .offset([8, 0]) + .attr('class', 'd3-flame-graph-tip') + .html(function(d) { return 'name: ' + d.data.n + ', value: ' + d.data.l; }); + + flameGraph.tooltip(tip); + + d3.select('#chart') + .datum(data) + .call(flameGraph); + + function clear() { + flameGraph.clear(); + } + + function resetZoom() { + flameGraph.resetZoom(); + } + + window.addEventListener('resize', function() { + var width = document.getElementById('chart').clientWidth; + var graphs = document.getElementsByClassName('d3-flame-graph'); + if (graphs.length > 0) { + graphs[0].setAttribute('width', width); + } + flameGraph.width(width); + flameGraph.resetZoom(); + }, true); + + var search = document.getElementById('search'); + var searchAlarm = null; + + function selectMatching() { + searchAlarm = null; + + if (search.value != '') { + flameGraph.search(search.value); + } else { + flameGraph.clear(); + } + } + + function handleSearch() { + // Delay expensive processing so a flurry of key strokes is handled once. + if (searchAlarm != null) { + clearTimeout(searchAlarm); + } + searchAlarm = setTimeout(selectMatching, 300); + } -{{template "script" .}} -<script>viewer({{.BaseURL}}, null)</script> + search.addEventListener('input', handleSearch); + </script> </body> </html> {{end}} diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go index 67ae262882..20d4e025f4 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui.go @@ -69,31 +69,24 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) { // webArgs contains arguments passed to templates in webhtml.go. type webArgs struct { - BaseURL string - Title string - Errors []string - Total int64 - Legend []string - Help map[string]string - Nodes []string - HTMLBody template.HTML - TextBody string - Top []report.TextItem + BaseURL string + Title string + Errors []string + Total int64 + Legend []string + Help map[string]string + Nodes []string + HTMLBody template.HTML + TextBody string + Top []report.TextItem + FlameGraph template.JS } -func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error { - host, portStr, err := net.SplitHostPort(hostport) - if err != nil { - return fmt.Errorf("could not split http address: %v", err) - } - port, err := strconv.Atoi(portStr) +func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, wantBrowser bool) error { + host, port, err := getHostAndPort(hostport) if err != nil { - return fmt.Errorf("invalid port number: %v", err) - } - if host == "" { - host = "localhost" + return err } - interactiveMode = true ui := makeWebInterface(p, o) for n, c := range pprofCommands { @@ -111,22 +104,52 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) e server = defaultWebServer } args := &plugin.HTTPServerArgs{ - Hostport: net.JoinHostPort(host, portStr), + Hostport: net.JoinHostPort(host, strconv.Itoa(port)), Host: host, Port: port, Handlers: map[string]http.Handler{ - "/": http.HandlerFunc(ui.dot), - "/top": http.HandlerFunc(ui.top), - "/disasm": http.HandlerFunc(ui.disasm), - "/source": http.HandlerFunc(ui.source), - "/peek": http.HandlerFunc(ui.peek), + "/": http.HandlerFunc(ui.dot), + "/top": http.HandlerFunc(ui.top), + "/disasm": http.HandlerFunc(ui.disasm), + "/source": http.HandlerFunc(ui.source), + "/peek": http.HandlerFunc(ui.peek), + "/flamegraph": http.HandlerFunc(ui.flamegraph), }, } - go openBrowser("http://"+args.Hostport, o) + if wantBrowser { + go openBrowser("http://"+args.Hostport, o) + } return server(args) } +func getHostAndPort(hostport string) (string, int, error) { + host, portStr, err := net.SplitHostPort(hostport) + if err != nil { + return "", 0, fmt.Errorf("could not split http address: %v", err) + } + if host == "" { + host = "localhost" + } + var port int + if portStr == "" { + ln, err := net.Listen("tcp", net.JoinHostPort(host, "0")) + if err != nil { + return "", 0, fmt.Errorf("could not generate random port: %v", err) + } + port = ln.Addr().(*net.TCPAddr).Port + err = ln.Close() + if err != nil { + return "", 0, fmt.Errorf("could not generate random port: %v", err) + } + } else { + port, err = strconv.Atoi(portStr) + if err != nil { + return "", 0, fmt.Errorf("invalid port number: %v", err) + } + } + return host, port, nil +} func defaultWebServer(args *plugin.HTTPServerArgs) error { ln, err := net.Listen("tcp", args.Hostport) if err != nil { diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go index 96380a01b3..424752fd1f 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webui_test.go @@ -23,24 +23,15 @@ import ( "net/url" "os/exec" "regexp" + "runtime" "sync" "testing" - "time" - - "runtime" "github.com/google/pprof/internal/plugin" "github.com/google/pprof/profile" ) func TestWebInterface(t *testing.T) { - // This test starts a web browser in a background goroutine - // after a 500ms delay. Sometimes the test exits before it - // can run the browser, but sometimes the browser does open. - // That's obviously unacceptable. - defer time.Sleep(2 * time.Second) // to see the browser open - t.Skip("golang.org/issue/22651") - if runtime.GOOS == "nacl" { t.Skip("test assumes tcp available") } @@ -66,7 +57,7 @@ func TestWebInterface(t *testing.T) { Obj: fakeObjTool{}, UI: &stdUI{}, HTTPServer: creator, - }) + }, false) <-serverCreated defer server.Close() @@ -89,6 +80,7 @@ func TestWebInterface(t *testing.T) { []string{"300ms.*F1", "200ms.*300ms.*F2"}, false}, {"/disasm?f=" + url.QueryEscape("F[12]"), []string{"f1:asm", "f2:asm"}, false}, + {"/flamegraph", []string{"File: testbin", "\"n\":\"root\"", "\"n\":\"F1\"", "function tip", "function flameGraph", "function hierarchy"}, false}, } for _, c := range testcases { if c.needDot && !haveDot { @@ -127,14 +119,19 @@ func TestWebInterface(t *testing.T) { for count := 0; count < 2; count++ { wg.Add(1) go func() { - http.Get(path) - wg.Done() + defer wg.Done() + res, err := http.Get(path) + if err != nil { + t.Error("could not fetch", c.path, err) + return + } + if _, err = ioutil.ReadAll(res.Body); err != nil { + t.Error("could not read response", c.path, err) + } }() } } wg.Wait() - - time.Sleep(5 * time.Second) } // Implement fake object file support. @@ -153,9 +150,18 @@ func (f fakeObj) SourceLine(addr uint64) ([]plugin.Frame, error) { } func (f fakeObj) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { return []*plugin.Sym{ - {[]string{"F1"}, fakeSource, addrBase, addrBase + 10}, - {[]string{"F2"}, fakeSource, addrBase + 10, addrBase + 20}, - {[]string{"F3"}, fakeSource, addrBase + 20, addrBase + 30}, + { + Name: []string{"F1"}, File: fakeSource, + Start: addrBase, End: addrBase + 10, + }, + { + Name: []string{"F2"}, File: fakeSource, + Start: addrBase + 10, End: addrBase + 20, + }, + { + Name: []string{"F3"}, File: fakeSource, + Start: addrBase + 20, End: addrBase + 30, + }, }, nil } @@ -229,6 +235,41 @@ func makeFakeProfile() *profile.Profile { } } +func TestGetHostAndPort(t *testing.T) { + if runtime.GOOS == "nacl" { + t.Skip("test assumes tcp available") + } + + type testCase struct { + hostport string + wantHost string + wantPort int + wantRandomPort bool + } + + testCases := []testCase{ + {":", "localhost", 0, true}, + {":4681", "localhost", 4681, false}, + {"localhost:4681", "localhost", 4681, false}, + } + for _, tc := range testCases { + host, port, err := getHostAndPort(tc.hostport) + if err != nil { + t.Errorf("could not get host and port for %q: %v", tc.hostport, err) + } + if got, want := host, tc.wantHost; got != want { + t.Errorf("for %s, got host %s, want %s", tc.hostport, got, want) + continue + } + if !tc.wantRandomPort { + if got, want := port, tc.wantPort; got != want { + t.Errorf("for %s, got port %d, want %d", tc.hostport, got, want) + continue + } + } + } +} + func TestIsLocalHost(t *testing.T) { for _, s := range []string{"localhost:10000", "[::1]:10000", "127.0.0.1:10000"} { host, _, err := net.SplitHostPort(s) |
