if o.Sym != nil {
sym = &internalSymbolizer{o.Sym}
}
+ var httpServer func(args *plugin.HTTPServerArgs) error
+ if o.HTTPServer != nil {
+ httpServer = func(args *plugin.HTTPServerArgs) error {
+ return o.HTTPServer(((*HTTPServerArgs)(args)))
+ }
+ }
return &plugin.Options{
- Writer: o.Writer,
- Flagset: o.Flagset,
- Fetch: o.Fetch,
- Sym: sym,
- Obj: obj,
- UI: o.UI,
+ Writer: o.Writer,
+ Flagset: o.Flagset,
+ Fetch: o.Fetch,
+ Sym: sym,
+ Obj: obj,
+ UI: o.UI,
+ HTTPServer: httpServer,
}
}
+// HTTPServerArgs contains arguments needed by an HTTP server that
+// is exporting a pprof web interface.
+type HTTPServerArgs plugin.HTTPServerArgs
+
// Options groups all the optional plugins into pprof.
type Options struct {
- Writer Writer
- Flagset FlagSet
- Fetch Fetcher
- Sym Symbolizer
- Obj ObjTool
- UI UI
+ Writer Writer
+ Flagset FlagSet
+ Fetch Fetcher
+ Sym Symbolizer
+ Obj ObjTool
+ UI UI
+ HTTPServer func(*HTTPServerArgs) error
}
// Writer provides a mechanism to write data under a certain name,
// interactive terminal (as opposed to being redirected to a file).
IsTerminal() bool
+ // WantBrowser indicates whether browser should be opened with the -http option.
+ WantBrowser() bool
+
// SetAutoComplete instructs the UI to call complete(cmd) to obtain
// the auto-completion of cmd, if the UI supports auto-completion at all.
SetAutoComplete(complete func(string) string)
rw lineReaderWriter
base uint64
- // nm holds an NM based addr2Liner which can provide
- // better full names compared to addr2line, which often drops
- // namespaces etc. from the names it returns.
+ // nm holds an addr2Liner using nm tool. Certain versions of addr2line
+ // produce incomplete names due to
+ // https://sourceware.org/bugzilla/show_bug.cgi?id=17541. As a workaround,
+ // the names from nm are used when they look more complete. See addrInfo()
+ // code below for the exact heuristic.
nm *addr2LinerNM
}
return nil, err
}
- // Get better name from nm if possible.
+ // Certain versions of addr2line produce incomplete names due to
+ // https://sourceware.org/bugzilla/show_bug.cgi?id=17541. Attempt to replace
+ // the name with a better one from nm.
if len(stack) > 0 && d.nm != nil {
nm, err := d.nm.addrInfo(addr)
if err == nil && len(nm) > 0 {
- // Last entry in frame list should match since
- // it is non-inlined. As a simple heuristic,
- // we only switch to the nm-based name if it
- // is longer.
+ // Last entry in frame list should match since it is non-inlined. As a
+ // simple heuristic, we only switch to the nm-based name if it is longer
+ // by 2 or more characters. We consider nm names that are longer by 1
+ // character insignificant to avoid replacing foo with _foo on MacOS (for
+ // unknown reasons read2line produces the former and nm produces the
+ // latter on MacOS even though both tools are asked to produce mangled
+ // names).
nmName := nm[len(nm)-1].Func
a2lName := stack[len(stack)-1].Func
- if len(nmName) > len(a2lName) {
+ if len(nmName) > len(a2lName)+1 {
stack[len(stack)-1].Func = nmName
}
}
bu.rep = r
}
+// String returns string representation of the binutils state for debug logging.
+func (bu *Binutils) String() string {
+ r := bu.get()
+ var llvmSymbolizer, addr2line, nm, objdump string
+ if r.llvmSymbolizerFound {
+ llvmSymbolizer = r.llvmSymbolizer
+ }
+ if r.addr2lineFound {
+ addr2line = r.addr2line
+ }
+ if r.nmFound {
+ nm = r.nm
+ }
+ if r.objdumpFound {
+ objdump = r.objdump
+ }
+ return fmt.Sprintf("llvm-symbolizer=%q addr2line=%q nm=%q objdump=%q fast=%t",
+ llvmSymbolizer, addr2line, nm, objdump, r.fast)
+}
+
// SetFastSymbolization sets a toggle that makes binutils use fast
// symbolization (using nm), which is much faster than addr2line but
// provides only symbol name information (no file/line).
defaultPath := paths[""]
b.llvmSymbolizer, b.llvmSymbolizerFound = findExe("llvm-symbolizer", append(paths["llvm-symbolizer"], defaultPath...))
b.addr2line, b.addr2lineFound = findExe("addr2line", append(paths["addr2line"], defaultPath...))
+ if !b.addr2lineFound {
+ // On MacOS, brew installs addr2line under gaddr2line name, so search for
+ // that if the tool is not found by its default name.
+ b.addr2line, b.addr2lineFound = findExe("gaddr2line", append(paths["addr2line"], defaultPath...))
+ }
b.nm, b.nmFound = findExe("nm", append(paths["nm"], defaultPath...))
b.objdump, b.objdumpFound = findExe("objdump", append(paths["objdump"], defaultPath...))
}
}
// fileAddr2Line implements the binutils.ObjFile interface, using
-// 'addr2line' to map addresses to symbols (with file/line number
-// information). It can be slow for large binaries with debug
-// information.
+// llvm-symbolizer, if that's available, or addr2line to map addresses to
+// symbols (with file/line number information). It can be slow for large
+// binaries with debug information.
type fileAddr2Line struct {
once sync.Once
file
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.
{"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0,
0xfa0, "_bar",
[]plugin.Frame{
- {Func: "bar", File: "/tmp/lib.c", Line: 6},
+ {Func: "bar", File: "/tmp/lib.c", Line: 5},
}},
} {
t.Run(tc.desc, func(t *testing.T) {
if err != nil {
t.Fatalf("Open: unexpected error %v", err)
}
+ t.Logf("binutils: %v", bu)
+ if runtime.GOOS == "darwin" && !bu.rep.addr2lineFound && !bu.rep.llvmSymbolizerFound {
+ // On OSX user needs to install gaddr2line or llvm-symbolizer with
+ // Homebrew, skip the test when the environment doesn't have it
+ // installed.
+ t.Skip("couldn't find addr2line or gaddr2line")
+ }
defer f.Close()
syms, err := f.Symbols(nil, 0)
if err != nil {
--- /dev/null
+#!/bin/bash -x
+
+# This is a script that generates the test MacOS executables in this directory.
+# It should be needed very rarely to run this script. It is mostly provided
+# as a future reference on how the original binary set was created.
+
+set -o errexit
+
+cat <<EOF >/tmp/hello.cc
+#include <stdio.h>
+
+int main() {
+ printf("Hello, world!\n");
+ return 0;
+}
+EOF
+
+cat <<EOF >/tmp/lib.c
+int foo() {
+ return 1;
+}
+
+int bar() {
+ return 2;
+}
+EOF
+
+cd $(dirname $0)
+rm -rf exe_mac_64* lib_mac_64*
+clang -g -o exe_mac_64 /tmp/hello.c
+clang -g -o lib_mac_64 -dynamiclib /tmp/lib.c
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.apple.xcode.dsym.exe_mac_64</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>dSYM</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ </dict>
+</plist>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+ <dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleIdentifier</key>
+ <string>com.apple.xcode.dsym.lib_mac_64</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>dSYM</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleShortVersionString</key>
+ <string>1.0</string>
+ <key>CFBundleVersion</key>
+ <string>1</string>
+ </dict>
+</plist>
}
if src.HTTPHostport != "" {
- return serveWebInterface(src.HTTPHostport, p, o, true)
+ return serveWebInterface(src.HTTPHostport, p, o)
}
return interactive(p, o)
}
// Output to specified file.
o.UI.PrintErr("Generating report in ", output)
- out, err := os.Create(output)
+ out, err := o.Writer.Open(output)
if err != nil {
return err
}
baseVars := pprofVariables
defer func() { pprofVariables = baseVars }()
for _, tc := range testcase {
- // Reset the pprof variables before processing
- pprofVariables = baseVars.makeCopy()
+ t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
+ // Reset the pprof variables before processing
+ pprofVariables = baseVars.makeCopy()
- f := baseFlags()
- f.args = []string{tc.source}
+ testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
- flags := strings.Split(tc.flags, ",")
+ f := baseFlags()
+ f.args = []string{tc.source}
- // Skip the output format in the first flag, to output to a proto
- addFlags(&f, flags[1:])
+ flags := strings.Split(tc.flags, ",")
- // Encode profile into a protobuf and decode it again.
- protoTempFile, err := ioutil.TempFile("", "profile_proto")
- if err != nil {
- t.Errorf("cannot create tempfile: %v", err)
- }
- defer os.Remove(protoTempFile.Name())
- defer protoTempFile.Close()
- f.strings["output"] = protoTempFile.Name()
-
- if flags[0] == "topproto" {
- f.bools["proto"] = false
- f.bools["topproto"] = true
- }
+ // Skip the output format in the first flag, to output to a proto
+ addFlags(&f, flags[1:])
- // First pprof invocation to save the profile into a profile.proto.
- o1 := setDefaults(nil)
- o1.Flagset = f
- o1.Fetch = testFetcher{}
- o1.Sym = testSymbolizer{}
- if err := PProf(o1); err != nil {
- t.Errorf("%s %q: %v", tc.source, tc.flags, err)
- continue
- }
- // Reset the pprof variables after the proto invocation
- pprofVariables = baseVars.makeCopy()
+ // Encode profile into a protobuf and decode it again.
+ protoTempFile, err := ioutil.TempFile("", "profile_proto")
+ if err != nil {
+ t.Errorf("cannot create tempfile: %v", err)
+ }
+ defer os.Remove(protoTempFile.Name())
+ defer protoTempFile.Close()
+ f.strings["output"] = protoTempFile.Name()
- // Read the profile from the encoded protobuf
- outputTempFile, err := ioutil.TempFile("", "profile_output")
- if err != nil {
- t.Errorf("cannot create tempfile: %v", err)
- }
- defer os.Remove(outputTempFile.Name())
- defer outputTempFile.Close()
- f.strings["output"] = outputTempFile.Name()
- f.args = []string{protoTempFile.Name()}
-
- var solution string
- // Apply the flags for the second pprof run, and identify name of
- // the file containing expected results
- if flags[0] == "topproto" {
- solution = solutionFilename(tc.source, &f)
- delete(f.bools, "topproto")
- f.bools["text"] = true
- } else {
- delete(f.bools, "proto")
- addFlags(&f, flags[:1])
- solution = solutionFilename(tc.source, &f)
- }
- // The add_comment flag is not idempotent so only apply it on the first run.
- delete(f.strings, "add_comment")
-
- // Second pprof invocation to read the profile from profile.proto
- // and generate a report.
- o2 := setDefaults(nil)
- o2.Flagset = f
- o2.Sym = testSymbolizeDemangler{}
- o2.Obj = new(mockObjTool)
-
- if err := PProf(o2); err != nil {
- t.Errorf("%s: %v", tc.source, err)
- }
- b, err := ioutil.ReadFile(outputTempFile.Name())
- if err != nil {
- t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
- }
+ if flags[0] == "topproto" {
+ f.bools["proto"] = false
+ f.bools["topproto"] = true
+ }
- // Read data file with expected solution
- solution = "testdata/" + solution
- sbuf, err := ioutil.ReadFile(solution)
- if err != nil {
- t.Errorf("reading solution file %s: %v", solution, err)
- continue
- }
- if runtime.GOOS == "windows" {
- sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1)
- sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte("\\path\\to\\"), -1)
- }
+ // First pprof invocation to save the profile into a profile.proto.
+ o1 := setDefaults(nil)
+ o1.Flagset = f
+ o1.Fetch = testFetcher{}
+ o1.Sym = testSymbolizer{}
+ o1.UI = testUI
+ if err := PProf(o1); err != nil {
+ t.Fatalf("%s %q: %v", tc.source, tc.flags, err)
+ }
+ // Reset the pprof variables after the proto invocation
+ pprofVariables = baseVars.makeCopy()
- if flags[0] == "svg" {
- b = removeScripts(b)
- sbuf = removeScripts(sbuf)
- }
+ // Read the profile from the encoded protobuf
+ outputTempFile, err := ioutil.TempFile("", "profile_output")
+ if err != nil {
+ t.Errorf("cannot create tempfile: %v", err)
+ }
+ defer os.Remove(outputTempFile.Name())
+ defer outputTempFile.Close()
+ f.strings["output"] = outputTempFile.Name()
+ f.args = []string{protoTempFile.Name()}
+
+ var solution string
+ // Apply the flags for the second pprof run, and identify name of
+ // the file containing expected results
+ if flags[0] == "topproto" {
+ solution = solutionFilename(tc.source, &f)
+ delete(f.bools, "topproto")
+ f.bools["text"] = true
+ } else {
+ delete(f.bools, "proto")
+ addFlags(&f, flags[:1])
+ solution = solutionFilename(tc.source, &f)
+ }
+ // The add_comment flag is not idempotent so only apply it on the first run.
+ delete(f.strings, "add_comment")
+
+ // Second pprof invocation to read the profile from profile.proto
+ // and generate a report.
+ o2 := setDefaults(nil)
+ o2.Flagset = f
+ o2.Sym = testSymbolizeDemangler{}
+ o2.Obj = new(mockObjTool)
+ o2.UI = testUI
+
+ if err := PProf(o2); err != nil {
+ t.Errorf("%s: %v", tc.source, err)
+ }
+ b, err := ioutil.ReadFile(outputTempFile.Name())
+ if err != nil {
+ t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err)
+ }
- if string(b) != string(sbuf) {
- t.Errorf("diff %s %s", solution, tc.source)
- d, err := proftest.Diff(sbuf, b)
+ // Read data file with expected solution
+ solution = "testdata/" + solution
+ sbuf, err := ioutil.ReadFile(solution)
if err != nil {
- t.Fatalf("diff %s %v", solution, err)
+ t.Fatalf("reading solution file %s: %v", solution, err)
}
- t.Errorf("%s\n%s\n", solution, d)
- if *updateFlag {
- err := ioutil.WriteFile(solution, b, 0644)
+ if runtime.GOOS == "windows" {
+ sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1)
+ sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte("\\path\\to\\"), -1)
+ }
+
+ if flags[0] == "svg" {
+ b = removeScripts(b)
+ sbuf = removeScripts(sbuf)
+ }
+
+ if string(b) != string(sbuf) {
+ t.Errorf("diff %s %s", solution, tc.source)
+ d, err := proftest.Diff(sbuf, b)
if err != nil {
- t.Errorf("failed to update the solution file %q: %v", solution, err)
+ t.Fatalf("diff %s %v", solution, err)
+ }
+ t.Errorf("%s\n%s\n", solution, d)
+ if *updateFlag {
+ err := ioutil.WriteFile(solution, b, 0644)
+ if err != nil {
+ t.Errorf("failed to update the solution file %q: %v", solution, err)
+ }
}
}
- }
+ })
}
}
if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
fileNames = append(fileNames, matches...)
}
+ fileNames = append(fileNames, filepath.Join(path, m.File, m.BuildID)) // perf path format
}
if m.File != "" {
// Try both the basename and the full path, to support the same directory
return nil, err
}
deferDeleteTempFile(profile.Name())
- cmd := exec.Command("perf_to_profile", perfPath, profile.Name())
+ cmd := exec.Command("perf_to_profile", "-i", perfPath, "-o", profile.Name(), "-f")
+ cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
profile.Close()
return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
}
f.args = tc.sources
- o := setDefaults(nil)
- o.Flagset = f
+ o := setDefaults(&plugin.Options{
+ UI: &proftest.TestUI{T: t, AllowRx: "Local symbolization failed|Some binary filenames not available"},
+ Flagset: f,
+ })
src, _, err := parseFlags(o)
if err != nil {
}()
defer l.Close()
- go func() {
- deadline := time.Now().Add(5 * time.Second)
- for time.Now().Before(deadline) {
- // Simulate a hotspot function. Spin in the inner loop for 100M iterations
- // to ensure we get most of the samples landed here rather than in the
- // library calls. We assume Go compiler won't elide the empty loop.
- for i := 0; i < 1e8; i++ {
- }
- runtime.Gosched()
- }
- }()
-
outputTempFile, err := ioutil.TempFile("", "profile_output")
if err != nil {
t.Fatalf("Failed to create tempfile: %v", err)
defer os.Remove(outputTempFile.Name())
defer outputTempFile.Close()
- address := "https+insecure://" + l.Addr().String() + "/debug/pprof/profile"
+ address := "https+insecure://" + l.Addr().String() + "/debug/pprof/goroutine"
s := &source{
Sources: []string{address},
Seconds: 10,
if len(p.SampleType) == 0 {
t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address)
}
- switch runtime.GOOS {
- case "plan9":
- // CPU profiling is not supported on Plan9; see golang.org/issues/22564.
- return
- case "darwin":
- if runtime.GOARCH == "arm" || runtime.GOARCH == "arm64" {
- // CPU profiling on iOS os not symbolized; see golang.org/issues/22612.
- return
- }
- }
if len(p.Function) == 0 {
t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address)
}
- if err := checkProfileHasFunction(p, "TestHttpsInsecure"); !badSigprofOS[runtime.GOOS] && err != nil {
+ if err := checkProfileHasFunction(p, "TestHttpsInsecure"); err != nil {
t.Fatalf("fetchProfiles(%s) %v", address, err)
}
}
-// Some operating systems don't trigger the profiling signal right.
-// See https://github.com/golang/go/issues/13841.
-var badSigprofOS = map[string]bool{
- "darwin": true,
- "netbsd": true,
-}
-
func checkProfileHasFunction(p *profile.Profile, fname string) error {
for _, f := range p.Function {
if strings.Contains(f.Name, fname) {
return
}
- ui.render(w, "/flamegraph", "flamegraph", rpt, errList, config.Labels, webArgs{
+ ui.render(w, "flamegraph", rpt, errList, config.Labels, webArgs{
FlameGraph: template.JS(b),
Nodes: nodeArr,
})
numLabelUnits := identifyNumLabelUnits(p, ui)
ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
if err == nil {
- ui.Print(strings.Join(report.ProfileLabels(report.New(p, ropt)), "\n"))
+ rpt := report.New(p, ropt)
+ ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
+ if rpt.Total() == 0 && len(p.SampleType) > 1 {
+ ui.Print(`No samples were found with the default sample value type.`)
+ ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n")
+ }
}
- ui.Print("Entering interactive mode (type \"help\" for commands, \"o\" for options)")
+ ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`)
}
// shortcuts represents composite commands that expand into a sequence
return false
}
+func (ui *stdUI) WantBrowser() bool {
+ return true
+}
+
func (ui *stdUI) SetAutoComplete(func(string) string) {
}
{{define "header"}}
<div class="header">
<div class="title">
- <h1><a href="/">pprof</a></h1>
+ <h1><a href="./">pprof</a></h1>
</div>
<div id="view" class="menu-item">
<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>
+ <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>
<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>
+ <a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
+ <a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
+ <a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
+ <a title="{{.Help.show}}" href="?" id="show">Show</a>
<hr>
- <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a>
+ <a title="{{.Help.reset}}" href="?">Reset</a>
</div>
</div>
{{.HTMLBody}}
</div>
{{template "script" .}}
- <script>viewer({{.BaseURL}}, {{.Nodes}});</script>
+ <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
</body>
</html>
{{end}}
function handleKey(e) {
if (e.keyCode != 13) return;
window.location.href =
- updateUrl(new URL({{.BaseURL}}, window.location.href), 'f');
+ updateUrl(new URL(window.location.href), 'f');
e.preventDefault();
}
bindSort('namehdr', 'Name');
}
- viewer({{.BaseURL}}, {{.Nodes}});
+ viewer(new URL(window.location.href), {{.Nodes}});
makeTopTable({{.Total}}, {{.Top}});
</script>
</body>
{{.HTMLBody}}
</div>
{{template "script" .}}
- <script>viewer({{.BaseURL}}, null);</script>
+ <script>viewer(new URL(window.location.href), null);</script>
</body>
</html>
{{end}}
</pre>
</div>
{{template "script" .}}
- <script>viewer({{.BaseURL}}, null);</script>
+ <script>viewer(new URL(window.location.href), null);</script>
</body>
</html>
{{end}}
<div id="flamegraphdetails" class="flamegraph-details"></div>
</div>
{{template "script" .}}
- <script>viewer({{.BaseURL}}, {{.Nodes}});</script>
+ <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
<script>{{template "d3script" .}}</script>
<script>{{template "d3tipscript" .}}</script>
<script>{{template "d3flamegraphscript" .}}</script>
// webArgs contains arguments passed to templates in webhtml.go.
type webArgs struct {
- BaseURL string
Title string
Errors []string
Total int64
FlameGraph template.JS
}
-func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, wantBrowser bool) error {
+func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options) error {
host, port, err := getHostAndPort(hostport)
if err != nil {
return err
},
}
- if wantBrowser {
+ if o.UI.WantBrowser() {
go openBrowser("http://"+args.Hostport, o)
}
return server(args)
}
h.ServeHTTP(w, req)
})
- s := &http.Server{Handler: handler}
+
+ // We serve the ui at /ui/ and redirect there from the root. This is done
+ // to surface any problems with serving the ui at a non-root early. See:
+ //
+ // https://github.com/google/pprof/pull/348
+ mux := http.NewServeMux()
+ mux.Handle("/ui/", http.StripPrefix("/ui", handler))
+ mux.Handle("/", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect))
+ s := &http.Server{Handler: mux}
return s.Serve(ln)
}
}
// render generates html using the named template based on the contents of data.
-func (ui *webInterface) render(w http.ResponseWriter, baseURL, tmpl string,
+func (ui *webInterface) render(w http.ResponseWriter, tmpl string,
rpt *report.Report, errList, legend []string, data webArgs) {
file := getFromLegend(legend, "File: ", "unknown")
profile := getFromLegend(legend, "Type: ", "unknown")
- data.BaseURL = baseURL
data.Title = file + " " + profile
data.Errors = errList
data.Total = rpt.Total()
nodes = append(nodes, n.Info.Name)
}
- ui.render(w, "/", "graph", rpt, errList, legend, webArgs{
+ ui.render(w, "graph", rpt, errList, legend, webArgs{
HTMLBody: template.HTML(string(svg)),
Nodes: nodes,
})
nodes = append(nodes, item.Name)
}
- ui.render(w, "/top", "top", rpt, errList, legend, webArgs{
+ ui.render(w, "top", rpt, errList, legend, webArgs{
Top: top,
Nodes: nodes,
})
}
legend := report.ProfileLabels(rpt)
- ui.render(w, "/disasm", "plaintext", rpt, errList, legend, webArgs{
+ ui.render(w, "plaintext", rpt, errList, legend, webArgs{
TextBody: out.String(),
})
}
legend := report.ProfileLabels(rpt)
- ui.render(w, "/source", "sourcelisting", rpt, errList, legend, webArgs{
+ ui.render(w, "sourcelisting", rpt, errList, legend, webArgs{
HTMLBody: template.HTML(body.String()),
})
}
}
legend := report.ProfileLabels(rpt)
- ui.render(w, "/peek", "plaintext", rpt, errList, legend, webArgs{
+ ui.render(w, "plaintext", rpt, errList, legend, webArgs{
TextBody: out.String(),
})
}
"testing"
"github.com/google/pprof/internal/plugin"
+ "github.com/google/pprof/internal/proftest"
"github.com/google/pprof/profile"
)
// Start server and wait for it to be initialized
go serveWebInterface("unused:1234", prof, &plugin.Options{
Obj: fakeObjTool{},
- UI: &stdUI{},
+ UI: &proftest.TestUI{},
HTTPServer: creator,
- }, false)
+ })
<-serverCreated
defer server.Close()
// So the base should be:
if stextOffset != nil && (start%pageSize) == (*stextOffset%pageSize) {
// perf uses the address of _stext as start. Some tools may
- // adjust for this before calling GetBase, in which case the the page
+ // adjust for this before calling GetBase, in which case the page
// alignment should be different from that of stextOffset.
return start - *stextOffset, nil
}
// interactive terminal (as opposed to being redirected to a file).
IsTerminal() bool
+ // WantBrowser indicates whether a browser should be opened with the -http option.
+ WantBrowser() bool
+
// SetAutoComplete instructs the UI to call complete(cmd) to obtain
// the auto-completion of cmd, if the UI supports auto-completion at all.
SetAutoComplete(complete func(string) string)
// implementation does. Without this Error() calls fmt.Sprintln() which
// _always_ adds spaces between arguments, unlike fmt.Sprint() which only
// adds them between arguments if neither is string.
- ui.T.Error(fmt.Sprint(args...))
+ ui.T.Error("unexpected error: " + fmt.Sprint(args...))
}
// IsTerminal indicates if the UI is an interactive terminal.
return false
}
+// WantBrowser indicates whether a browser should be opened with the -http option.
+func (ui *TestUI) WantBrowser() bool {
+ return false
+}
+
// SetAutoComplete is not supported by the test UI.
func (ui *TestUI) SetAutoComplete(_ func(string) string) {
}
}
// Scan each component of the path
- for _, dir := range strings.Split(searchPath, ":") {
+ for _, dir := range filepath.SplitList(searchPath) {
// Search up for every parent of each possible path.
for {
filename := filepath.Join(dir, path)
import (
"bytes"
+ "io/ioutil"
"os"
"path/filepath"
"regexp"
}
}
+func TestOpenSourceFile(t *testing.T) {
+ tempdir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+ const lsep = string(filepath.ListSeparator)
+ for _, tc := range []struct {
+ desc string
+ searchPath string
+ fs []string
+ path string
+ wantPath string // If empty, error is wanted.
+ }{
+ {
+ desc: "exact absolute path is found",
+ fs: []string{"foo/bar.txt"},
+ path: "$dir/foo/bar.txt",
+ wantPath: "$dir/foo/bar.txt",
+ },
+ {
+ desc: "exact relative path is found",
+ searchPath: "$dir",
+ fs: []string{"foo/bar.txt"},
+ path: "foo/bar.txt",
+ wantPath: "$dir/foo/bar.txt",
+ },
+ {
+ desc: "multiple search path",
+ searchPath: "some/path" + lsep + "$dir",
+ fs: []string{"foo/bar.txt"},
+ path: "foo/bar.txt",
+ wantPath: "$dir/foo/bar.txt",
+ },
+ {
+ desc: "relative path is found in parent dir",
+ searchPath: "$dir/foo/bar",
+ fs: []string{"bar.txt", "foo/bar/baz.txt"},
+ path: "bar.txt",
+ wantPath: "$dir/bar.txt",
+ },
+ {
+ desc: "error when not found",
+ path: "foo.txt",
+ },
+ } {
+ t.Run(tc.desc, func(t *testing.T) {
+ defer func() {
+ if err := os.RemoveAll(tempdir); err != nil {
+ t.Fatalf("failed to remove dir %q: %v", tempdir, err)
+ }
+ }()
+ for _, f := range tc.fs {
+ path := filepath.Join(tempdir, filepath.FromSlash(f))
+ dir := filepath.Dir(path)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ t.Fatalf("failed to create dir %q: %v", dir, err)
+ }
+ if err := ioutil.WriteFile(path, nil, 0644); err != nil {
+ t.Fatalf("failed to create file %q: %v", path, err)
+ }
+ }
+ tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
+ tc.path = filepath.FromSlash(strings.Replace(tc.path, "$dir", tempdir, 1))
+ tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1))
+ if file, err := openSourceFile(tc.path, tc.searchPath); err != nil && tc.wantPath != "" {
+ t.Errorf("openSourceFile(%q, %q) = err %v, want path %q", tc.path, tc.searchPath, err, tc.wantPath)
+ } else if err == nil {
+ defer file.Close()
+ gotPath := file.Name()
+ if tc.wantPath == "" {
+ t.Errorf("openSourceFile(%q, %q) = %q, want error", tc.path, tc.searchPath, gotPath)
+ } else if gotPath != tc.wantPath {
+ t.Errorf("openSourceFile(%q, %q) = %q, want path %q", tc.path, tc.searchPath, gotPath, tc.wantPath)
+ }
+ }
+ })
+ }
+}
+
func TestIndentation(t *testing.T) {
for _, c := range []struct {
str string
* ======================
*
* Given an unique existing element with id "viewport" (or when missing, the
- * first g-element), including the the library into any SVG adds the following
+ * first g-element), including the library into any SVG adds the following
* capabilities:
*
* - Mouse panning