diff options
Diffstat (limited to 'src/cmd/vendor/github.com/google/pprof/internal')
27 files changed, 1290 insertions, 720 deletions
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go b/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go index 9a82cb8e92..390f952feb 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils.go @@ -175,20 +175,35 @@ func (bu *Binutils) Open(name string, start, limit, offset uint64) (plugin.ObjFi func (b *binrep) openMachO(name string, start, limit, offset uint64) (plugin.ObjFile, error) { of, err := macho.Open(name) if err != nil { - return nil, fmt.Errorf("Parsing %s: %v", name, err) + return nil, fmt.Errorf("error parsing %s: %v", name, err) } defer of.Close() + // Subtract the load address of the __TEXT section. Usually 0 for shared + // libraries or 0x100000000 for executables. You can check this value by + // running `objdump -private-headers <file>`. + + textSegment := of.Segment("__TEXT") + if textSegment == nil { + return nil, fmt.Errorf("could not identify base for %s: no __TEXT segment", name) + } + if textSegment.Addr > start { + return nil, fmt.Errorf("could not identify base for %s: __TEXT segment address (0x%x) > mapping start address (0x%x)", + name, textSegment.Addr, start) + } + + base := start - textSegment.Addr + if b.fast || (!b.addr2lineFound && !b.llvmSymbolizerFound) { - return &fileNM{file: file{b: b, name: name}}, nil + return &fileNM{file: file{b: b, name: name, base: base}}, nil } - return &fileAddr2Line{file: file{b: b, name: name}}, nil + return &fileAddr2Line{file: file{b: b, name: name, base: base}}, nil } func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFile, error) { ef, err := elf.Open(name) if err != nil { - return nil, fmt.Errorf("Parsing %s: %v", name, err) + return nil, fmt.Errorf("error parsing %s: %v", name, err) } defer ef.Close() @@ -215,9 +230,9 @@ func (b *binrep) openELF(name string, start, limit, offset uint64) (plugin.ObjFi } } - base, err := elfexec.GetBase(&ef.FileHeader, nil, stextOffset, start, limit, offset) + base, err := elfexec.GetBase(&ef.FileHeader, elfexec.FindTextProgHeader(ef), stextOffset, start, limit, offset) if err != nil { - return nil, fmt.Errorf("Could not identify base for %s: %v", name, err) + return nil, fmt.Errorf("could not identify base for %s: %v", name, err) } buildID := "" diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go b/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go index 989a290071..0317cf5126 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/binutils/binutils_test.go @@ -177,14 +177,20 @@ func TestSetFastSymbolization(t *testing.T) { func skipUnlessLinuxAmd64(t *testing.T) { if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" { - t.Skip("Disasm only tested on x86-64 linux") + t.Skip("This test only works on x86-64 Linux") + } +} + +func skipUnlessDarwinAmd64(t *testing.T) { + if runtime.GOOS != "darwin" || runtime.GOARCH != "amd64" { + t.Skip("This test only works on x86-64 Mac") } } func TestDisasm(t *testing.T) { skipUnlessLinuxAmd64(t) bu := &Binutils{} - insts, err := bu.Disasm(filepath.Join("testdata", "hello"), 0, math.MaxUint64) + insts, err := bu.Disasm(filepath.Join("testdata", "exe_linux_64"), 0, math.MaxUint64) if err != nil { t.Fatalf("Disasm: unexpected error %v", err) } @@ -199,42 +205,119 @@ func TestDisasm(t *testing.T) { } } +func findSymbol(syms []*plugin.Sym, name string) *plugin.Sym { + for _, s := range syms { + for _, n := range s.Name { + if n == name { + return s + } + } + } + return nil +} + func TestObjFile(t *testing.T) { skipUnlessLinuxAmd64(t) - bu := &Binutils{} - f, err := bu.Open(filepath.Join("testdata", "hello"), 0, math.MaxUint64, 0) - if err != nil { - t.Fatalf("Open: unexpected error %v", err) - } - defer f.Close() - syms, err := f.Symbols(regexp.MustCompile("main"), 0) - if err != nil { - t.Fatalf("Symbols: unexpected error %v", err) - } + for _, tc := range []struct { + desc string + start, limit, offset uint64 + addr uint64 + }{ + {"fake mapping", 0, math.MaxUint64, 0, 0x40052d}, + {"fixed load address", 0x400000, 0x4006fc, 0, 0x40052d}, + // True user-mode ASLR binaries are ET_DYN rather than ET_EXEC so this case + // is a bit artificial except that it approximates the + // vmlinux-with-kernel-ASLR case where the binary *is* ET_EXEC. + {"simulated ASLR address", 0x500000, 0x5006fc, 0, 0x50052d}, + } { + t.Run(tc.desc, func(t *testing.T) { + bu := &Binutils{} + f, err := bu.Open(filepath.Join("testdata", "exe_linux_64"), tc.start, tc.limit, tc.offset) + if err != nil { + t.Fatalf("Open: unexpected error %v", err) + } + defer f.Close() + syms, err := f.Symbols(regexp.MustCompile("main"), 0) + if err != nil { + t.Fatalf("Symbols: unexpected error %v", err) + } - find := func(name string) *plugin.Sym { - for _, s := range syms { - for _, n := range s.Name { - if n == name { - return s + m := findSymbol(syms, "main") + if m == nil { + t.Fatalf("Symbols: did not find main") + } + for _, addr := range []uint64{m.Start + f.Base(), tc.addr} { + gotFrames, err := f.SourceLine(addr) + if err != nil { + t.Fatalf("SourceLine: unexpected error %v", err) + } + wantFrames := []plugin.Frame{ + {Func: "main", File: "/tmp/hello.c", Line: 3}, + } + if !reflect.DeepEqual(gotFrames, wantFrames) { + t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, wantFrames) } } - } - return nil - } - m := find("main") - if m == nil { - t.Fatalf("Symbols: did not find main") - } - frames, err := f.SourceLine(m.Start) - if err != nil { - t.Fatalf("SourceLine: unexpected error %v", err) - } - expect := []plugin.Frame{ - {Func: "main", File: "/tmp/hello.c", Line: 3}, + }) } - if !reflect.DeepEqual(frames, expect) { - t.Fatalf("SourceLine for main: expect %v; got %v\n", expect, frames) +} + +func TestMachoFiles(t *testing.T) { + skipUnlessDarwinAmd64(t) + + t.Skip("Disabled because of issues with addr2line (see https://github.com/google/pprof/pull/313#issuecomment-364073010)") + + // Load `file`, pretending it was mapped at `start`. Then get the symbol + // table. Check that it contains the symbol `sym` and that the address + // `addr` gives the `expected` stack trace. + for _, tc := range []struct { + desc string + file string + start, limit, offset uint64 + addr uint64 + sym string + expected []plugin.Frame + }{ + {"normal mapping", "exe_mac_64", 0x100000000, math.MaxUint64, 0, + 0x100000f50, "_main", + []plugin.Frame{ + {Func: "main", File: "/tmp/hello.c", Line: 3}, + }}, + {"other mapping", "exe_mac_64", 0x200000000, math.MaxUint64, 0, + 0x200000f50, "_main", + []plugin.Frame{ + {Func: "main", File: "/tmp/hello.c", Line: 3}, + }}, + {"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0, + 0xfa0, "_bar", + []plugin.Frame{ + {Func: "bar", File: "/tmp/lib.c", Line: 6}, + }}, + } { + t.Run(tc.desc, func(t *testing.T) { + bu := &Binutils{} + f, err := bu.Open(filepath.Join("testdata", tc.file), tc.start, tc.limit, tc.offset) + if err != nil { + t.Fatalf("Open: unexpected error %v", err) + } + defer f.Close() + syms, err := f.Symbols(nil, 0) + if err != nil { + t.Fatalf("Symbols: unexpected error %v", err) + } + + m := findSymbol(syms, tc.sym) + if m == nil { + t.Fatalf("Symbols: could not find symbol %v", tc.sym) + } + gotFrames, err := f.SourceLine(tc.addr) + if err != nil { + t.Fatalf("SourceLine: unexpected error %v", err) + } + if !reflect.DeepEqual(gotFrames, tc.expected) { + t.Fatalf("SourceLine for main: got %v; want %v\n", gotFrames, tc.expected) + } + }) } } diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/disasm.go b/src/cmd/vendor/github.com/google/pprof/internal/binutils/disasm.go index 1a3b6f8d6a..28c89aa163 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/binutils/disasm.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/binutils/disasm.go @@ -34,24 +34,48 @@ var ( func findSymbols(syms []byte, file string, r *regexp.Regexp, address uint64) ([]*plugin.Sym, error) { // Collect all symbols from the nm output, grouping names mapped to // the same address into a single symbol. + + // The symbols to return. var symbols []*plugin.Sym + + // The current group of symbol names, and the address they are all at. names, start := []string{}, uint64(0) + buf := bytes.NewBuffer(syms) - for symAddr, name, err := nextSymbol(buf); err == nil; symAddr, name, err = nextSymbol(buf) { + + for { + symAddr, name, err := nextSymbol(buf) + if err == io.EOF { + // Done. If there was an unfinished group, append it. + if len(names) != 0 { + if match := matchSymbol(names, start, symAddr-1, r, address); match != nil { + symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1}) + } + } + + // And return the symbols. + return symbols, nil + } + if err != nil { + // There was some kind of serious error reading nm's output. return nil, err } - if start == symAddr { + + // If this symbol is at the same address as the current group, add it to the group. + if symAddr == start { names = append(names, name) continue } + + // Otherwise append the current group to the list of symbols. if match := matchSymbol(names, start, symAddr-1, r, address); match != nil { symbols = append(symbols, &plugin.Sym{Name: match, File: file, Start: start, End: symAddr - 1}) } + + // And start a new group. names, start = []string{name}, symAddr } - - return symbols, nil } // matchSymbol checks if a symbol is to be selected by checking its @@ -62,7 +86,7 @@ func matchSymbol(names []string, start, end uint64, r *regexp.Regexp, address ui return names } for _, name := range names { - if r.MatchString(name) { + if r == nil || r.MatchString(name) { return []string{name} } diff --git a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_linux_64 Binary files differindex d86dc7cdfc..d86dc7cdfc 100755 --- a/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/hello +++ b/src/cmd/vendor/github.com/google/pprof/internal/binutils/testdata/exe_linux_64 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) diff --git a/src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go b/src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go index 9b238c5b87..7e42c88d14 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/elfexec/elfexec.go @@ -259,3 +259,19 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6 } return 0, fmt.Errorf("Don't know how to handle FileHeader.Type %v", fh.Type) } + +// FindTextProgHeader finds the program segment header containing the .text +// section or nil if the segment cannot be found. +func FindTextProgHeader(f *elf.File) *elf.ProgHeader { + for _, s := range f.Sections { + if s.Name == ".text" { + // Find the LOAD segment containing the .text section. + for _, p := range f.Progs { + if p.Type == elf.PT_LOAD && p.Flags&elf.PF_X != 0 && s.Addr >= p.Vaddr && s.Addr < p.Vaddr+p.Memsz { + return &p.ProgHeader + } + } + } + } + return nil +} diff --git a/src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go b/src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go index 4e5d12f6cd..f3721e92f5 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/graph/dotgraph.go @@ -154,7 +154,7 @@ func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) { if flat != 0 { label = label + fmt.Sprintf(`%s (%s)`, flatValue, - strings.TrimSpace(percentage(flat, b.config.Total))) + strings.TrimSpace(measurement.Percentage(flat, b.config.Total))) } else { label = label + "0" } @@ -168,7 +168,7 @@ func (b *builder) addNode(node *Node, nodeID int, maxFlat float64) { cumValue = b.config.FormatValue(cum) label = label + fmt.Sprintf(`of %s (%s)`, cumValue, - strings.TrimSpace(percentage(cum, b.config.Total))) + strings.TrimSpace(measurement.Percentage(cum, b.config.Total))) } // Scale font sizes from 8 to 24 based on percentage of flat frequency. @@ -380,23 +380,6 @@ func dotColor(score float64, isBackground bool) string { return fmt.Sprintf("#%02x%02x%02x", uint8(r*255.0), uint8(g*255.0), uint8(b*255.0)) } -// percentage computes the percentage of total of a value, and encodes -// it as a string. At least two digits of precision are printed. -func percentage(value, total int64) string { - var ratio float64 - if total != 0 { - ratio = math.Abs(float64(value)/float64(total)) * 100 - } - switch { - case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05: - return " 100%" - case math.Abs(ratio) >= 1.0: - return fmt.Sprintf("%5.2f%%", ratio) - default: - return fmt.Sprintf("%5.2g%%", ratio) - } -} - func multilinePrintableName(info *NodeInfo) string { infoCopy := *info infoCopy.Name = strings.Replace(infoCopy.Name, "::", `\n`, -1) diff --git a/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement.go b/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement.go index 0a60435644..3e3bcb8c25 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/measurement/measurement.go @@ -17,6 +17,7 @@ package measurement import ( "fmt" + "math" "strings" "time" @@ -154,6 +155,23 @@ func ScaledLabel(value int64, fromUnit, toUnit string) string { return sv + u } +// Percentage computes the percentage of total of a value, and encodes +// it as a string. At least two digits of precision are printed. +func Percentage(value, total int64) string { + var ratio float64 + if total != 0 { + ratio = math.Abs(float64(value)/float64(total)) * 100 + } + switch { + case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05: + return " 100%" + case math.Abs(ratio) >= 1.0: + return fmt.Sprintf("%5.2f%%", ratio) + default: + return fmt.Sprintf("%5.2g%%", ratio) + } +} + // isMemoryUnit returns whether a name is recognized as a memory size // unit. func isMemoryUnit(unit string) bool { diff --git a/src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go b/src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go index 7f9dcab61a..03fac7e330 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/proftest/proftest.go @@ -19,6 +19,7 @@ package proftest import ( "encoding/json" "fmt" + "io" "io/ioutil" "os" "os/exec" @@ -80,11 +81,21 @@ type TestUI struct { Ignore int AllowRx string NumAllowRxMatches int + Input []string + index int } // ReadLine returns no input, as no input is expected during testing. func (ui *TestUI) ReadLine(_ string) (string, error) { - return "", fmt.Errorf("no input") + if ui.index >= len(ui.Input) { + return "", io.EOF + } + input := ui.Input[ui.index] + ui.index++ + if input == "**error**" { + return "", fmt.Errorf("Error: %s", input) + } + return input, nil } // Print messages are discarded by the test UI. diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/report.go b/src/cmd/vendor/github.com/google/pprof/internal/report/report.go index f434554dd9..e127f7fcec 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/report/report.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/report/report.go @@ -19,7 +19,6 @@ package report import ( "fmt" "io" - "math" "path/filepath" "regexp" "sort" @@ -425,7 +424,7 @@ func PrintAssembly(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFuncs int) e } fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", rpt.formatValue(flatSum), rpt.formatValue(cumSum), - percentage(cumSum, rpt.total)) + measurement.Percentage(cumSum, rpt.total)) function, file, line := "", "", 0 for _, n := range ns { @@ -704,7 +703,7 @@ func printTags(w io.Writer, rpt *Report) error { for _, t := range graph.SortTags(tags, true) { f, u := measurement.Scale(t.FlatValue(), o.SampleUnit, o.OutputUnit) if total > 0 { - fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, percentage(t.FlatValue(), total), t.Name) + fmt.Fprintf(tabw, " \t%.1f%s (%s):\t %s\n", f, u, measurement.Percentage(t.FlatValue(), total), t.Name) } else { fmt.Fprintf(tabw, " \t%.1f%s:\t %s\n", f, u, t.Name) } @@ -789,9 +788,9 @@ func printText(w io.Writer, rpt *Report) error { } flatSum += item.Flat fmt.Fprintf(w, "%10s %s %s %10s %s %s%s\n", - item.FlatFormat, percentage(item.Flat, rpt.total), - percentage(flatSum, rpt.total), - item.CumFormat, percentage(item.Cum, rpt.total), + item.FlatFormat, measurement.Percentage(item.Flat, rpt.total), + measurement.Percentage(flatSum, rpt.total), + item.CumFormat, measurement.Percentage(item.Cum, rpt.total), item.Name, inl) } return nil @@ -1030,17 +1029,17 @@ func printTree(w io.Writer, rpt *Report) error { inline = " (inline)" } fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(in.Weight), - percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline) + measurement.Percentage(in.Weight, cum), in.Src.Info.PrintableName(), inline) } // Print current node. flatSum += flat fmt.Fprintf(w, "%10s %s %s %10s %s | %s\n", rpt.formatValue(flat), - percentage(flat, rpt.total), - percentage(flatSum, rpt.total), + measurement.Percentage(flat, rpt.total), + measurement.Percentage(flatSum, rpt.total), rpt.formatValue(cum), - percentage(cum, rpt.total), + measurement.Percentage(cum, rpt.total), name) // Print outgoing edges. @@ -1051,7 +1050,7 @@ func printTree(w io.Writer, rpt *Report) error { inline = " (inline)" } fmt.Fprintf(w, "%50s %s | %s%s\n", rpt.formatValue(out.Weight), - percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline) + measurement.Percentage(out.Weight, cum), out.Dest.Info.PrintableName(), inline) } } if len(g.Nodes) > 0 { @@ -1083,23 +1082,6 @@ func printDOT(w io.Writer, rpt *Report) error { return nil } -// percentage computes the percentage of total of a value, and encodes -// it as a string. At least two digits of precision are printed. -func percentage(value, total int64) string { - var ratio float64 - if total != 0 { - ratio = math.Abs(float64(value)/float64(total)) * 100 - } - switch { - case math.Abs(ratio) >= 99.95 && math.Abs(ratio) <= 100.05: - return " 100%" - case math.Abs(ratio) >= 1.0: - return fmt.Sprintf("%5.2f%%", ratio) - default: - return fmt.Sprintf("%5.2g%%", ratio) - } -} - // ProfileLabels returns printable labels for a profile. func ProfileLabels(rpt *Report) []string { label := []string{} @@ -1131,7 +1113,7 @@ func ProfileLabels(rpt *Report) []string { totalNanos, totalUnit := measurement.Scale(rpt.total, o.SampleUnit, "nanoseconds") var ratio string if totalUnit == "ns" && totalNanos != 0 { - ratio = "(" + percentage(int64(totalNanos), prof.DurationNanos) + ")" + ratio = "(" + measurement.Percentage(int64(totalNanos), prof.DurationNanos) + ")" } label = append(label, fmt.Sprintf("Duration: %s, Total samples = %s %s", duration, rpt.formatValue(rpt.total), ratio)) } @@ -1162,7 +1144,7 @@ func reportLabels(rpt *Report, g *graph.Graph, origCount, droppedNodes, droppedE label = append(label, activeFilters...) } - label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total))) + label = append(label, fmt.Sprintf("Showing nodes accounting for %s, %s of %s total", rpt.formatValue(flatSum), strings.TrimSpace(measurement.Percentage(flatSum, rpt.total)), rpt.formatValue(rpt.total))) if rpt.total != 0 { if droppedNodes > 0 { diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/report_test.go b/src/cmd/vendor/github.com/google/pprof/internal/report/report_test.go index e05cf5ad08..c243e20c2b 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/report/report_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/report/report_test.go @@ -68,8 +68,8 @@ func TestSource(t *testing.T) { want: path + "source.dot", }, } { - b := bytes.NewBuffer(nil) - if err := Generate(b, tc.rpt, &binutils.Binutils{}); err != nil { + var b bytes.Buffer + if err := Generate(&b, tc.rpt, &binutils.Binutils{}); err != nil { t.Fatalf("%s: %v", tc.want, err) } diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/source.go b/src/cmd/vendor/github.com/google/pprof/internal/report/source.go index ce82ae55c4..9a98f7460b 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/report/source.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/report/source.go @@ -28,6 +28,7 @@ import ( "strings" "github.com/google/pprof/internal/graph" + "github.com/google/pprof/internal/measurement" "github.com/google/pprof/internal/plugin" ) @@ -99,7 +100,7 @@ func printSource(w io.Writer, rpt *Report) error { fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, filename) fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", rpt.formatValue(flatSum), rpt.formatValue(cumSum), - percentage(cumSum, rpt.total)) + measurement.Percentage(cumSum, rpt.total)) if err != nil { fmt.Fprintf(w, " Error: %v\n", err) @@ -337,13 +338,13 @@ func printHeader(w io.Writer, rpt *Report) { // printFunctionHeader prints a function header for a weblist report. func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) { - fmt.Fprintf(w, `<h1>%s</h1>%s + fmt.Fprintf(w, `<h2>%s</h2><p class="filename">%s</p> <pre onClick="pprof_toggle_asm(event)"> Total: %10s %10s (flat, cum) %s `, template.HTMLEscapeString(name), template.HTMLEscapeString(path), rpt.formatValue(flatSum), rpt.formatValue(cumSum), - percentage(cumSum, rpt.total)) + measurement.Percentage(cumSum, rpt.total)) } // printFunctionSourceLine prints a source line and the corresponding assembly. diff --git a/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go b/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go index 9a2b5a21c4..d45d4c5815 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/report/source_test.go @@ -25,8 +25,8 @@ func TestWebList(t *testing.T) { SampleValue: func(v []int64) int64 { return v[1] }, SampleUnit: cpu.SampleType[1].Unit, }) - buf := bytes.NewBuffer(nil) - if err := Generate(buf, rpt, &binutils.Binutils{}); err != nil { + var buf bytes.Buffer + if err := Generate(&buf, rpt, &binutils.Binutils{}); err != nil { t.Fatalf("could not generate weblist: %v", err) } output := buf.String() diff --git a/src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer.go b/src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer.go index 727b7e8c56..5994c11ac7 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/symbolizer/symbolizer.go @@ -168,6 +168,7 @@ func doLocalSymbolize(prof *profile.Profile, fast, force bool, obj plugin.ObjToo } l.Line = make([]profile.Line, len(stack)) + l.IsFolded = false for i, frame := range stack { if frame.Func != "" { m.HasFunctions = true diff --git a/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz.go b/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz.go index 34c119c4c2..086c0ccb04 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz.go @@ -63,10 +63,29 @@ func Symbolize(p *profile.Profile, force bool, sources plugin.MappingSources, sy return nil } +// Check whether path ends with one of the suffixes listed in +// pprof_remote_servers.html from the gperftools distribution +func hasGperftoolsSuffix(path string) bool { + suffixes := []string{ + "/pprof/heap", + "/pprof/growth", + "/pprof/profile", + "/pprof/pmuprofile", + "/pprof/contention", + } + for _, s := range suffixes { + if strings.HasSuffix(path, s) { + return true + } + } + return false +} + // symbolz returns the corresponding symbolz source for a profile URL. func symbolz(source string) string { if url, err := url.Parse(source); err == nil && url.Host != "" { - if strings.Contains(url.Path, "/debug/pprof/") { + // All paths in the net/http/pprof Go package contain /debug/pprof/ + if strings.Contains(url.Path, "/debug/pprof/") || hasGperftoolsSuffix(url.Path) { url.Path = path.Clean(url.Path + "/../symbol") } else { url.Path = "/symbolz" diff --git a/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz_test.go b/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz_test.go index 270a6198c9..2d13c889b6 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz_test.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/symbolz/symbolz_test.go @@ -34,6 +34,17 @@ func TestSymbolzURL(t *testing.T) { "http://host:8000/debug/pprof/profile?seconds=10": "http://host:8000/debug/pprof/symbol", "http://host:8000/debug/pprof/heap": "http://host:8000/debug/pprof/symbol", "http://some.host:8080/some/deeper/path/debug/pprof/endpoint?param=value": "http://some.host:8080/some/deeper/path/debug/pprof/symbol", + "http://host:8000/pprof/profile": "http://host:8000/pprof/symbol", + "http://host:8000/pprof/profile?seconds=15": "http://host:8000/pprof/symbol", + "http://host:8000/pprof/heap": "http://host:8000/pprof/symbol", + "http://host:8000/debug/pprof/block": "http://host:8000/debug/pprof/symbol", + "http://host:8000/debug/pprof/trace?seconds=5": "http://host:8000/debug/pprof/symbol", + "http://host:8000/debug/pprof/mutex": "http://host:8000/debug/pprof/symbol", + "http://host/whatever/pprof/heap": "http://host/whatever/pprof/symbol", + "http://host/whatever/pprof/growth": "http://host/whatever/pprof/symbol", + "http://host/whatever/pprof/profile": "http://host/whatever/pprof/symbol", + "http://host/whatever/pprof/pmuprofile": "http://host/whatever/pprof/symbol", + "http://host/whatever/pprof/contention": "http://host/whatever/pprof/symbol", } { if got := symbolz(try); got != want { t.Errorf(`symbolz(%s)=%s, want "%s"`, try, got, want) |
