Commit 31db81d3 authored by Daniel Martí's avatar Daniel Martí Committed by Brad Fitzpatrick

cmd/vendor/.../pprof: update to 0f7d9ba1

In particular, to pull a few fixes that were causing some tests to be
flaky on our build dashboard.

Fixes #24405.
Fixes #24508.
Fixes #24611.

Change-Id: I713156ad11c924e4a4b603144d10395523d526ed
Reviewed-on: https://go-review.googlesource.com/105275
Run-TryBot: Daniel Martí <mvdan@mvdan.cc>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: default avatarBrad Fitzpatrick <bradfitz@golang.org>
parent 4acb305d
...@@ -41,6 +41,12 @@ func (o *Options) internalOptions() *plugin.Options { ...@@ -41,6 +41,12 @@ func (o *Options) internalOptions() *plugin.Options {
if o.Sym != nil { if o.Sym != nil {
sym = &internalSymbolizer{o.Sym} 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{ return &plugin.Options{
Writer: o.Writer, Writer: o.Writer,
Flagset: o.Flagset, Flagset: o.Flagset,
...@@ -48,9 +54,14 @@ func (o *Options) internalOptions() *plugin.Options { ...@@ -48,9 +54,14 @@ func (o *Options) internalOptions() *plugin.Options {
Sym: sym, Sym: sym,
Obj: obj, Obj: obj,
UI: o.UI, 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. // Options groups all the optional plugins into pprof.
type Options struct { type Options struct {
Writer Writer Writer Writer
...@@ -59,6 +70,7 @@ type Options struct { ...@@ -59,6 +70,7 @@ type Options struct {
Sym Symbolizer Sym Symbolizer
Obj ObjTool Obj ObjTool
UI UI UI UI
HTTPServer func(*HTTPServerArgs) error
} }
// Writer provides a mechanism to write data under a certain name, // Writer provides a mechanism to write data under a certain name,
...@@ -206,6 +218,9 @@ type UI interface { ...@@ -206,6 +218,9 @@ type UI interface {
// interactive terminal (as opposed to being redirected to a file). // interactive terminal (as opposed to being redirected to a file).
IsTerminal() bool 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 // SetAutoComplete instructs the UI to call complete(cmd) to obtain
// the auto-completion of cmd, if the UI supports auto-completion at all. // the auto-completion of cmd, if the UI supports auto-completion at all.
SetAutoComplete(complete func(string) string) SetAutoComplete(complete func(string) string)
......
...@@ -41,9 +41,11 @@ type addr2Liner struct { ...@@ -41,9 +41,11 @@ type addr2Liner struct {
rw lineReaderWriter rw lineReaderWriter
base uint64 base uint64
// nm holds an NM based addr2Liner which can provide // nm holds an addr2Liner using nm tool. Certain versions of addr2line
// better full names compared to addr2line, which often drops // produce incomplete names due to
// namespaces etc. from the names it returns. // 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 nm *addr2LinerNM
} }
...@@ -215,17 +217,22 @@ func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) { ...@@ -215,17 +217,22 @@ func (d *addr2Liner) addrInfo(addr uint64) ([]plugin.Frame, error) {
return nil, err 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 { if len(stack) > 0 && d.nm != nil {
nm, err := d.nm.addrInfo(addr) nm, err := d.nm.addrInfo(addr)
if err == nil && len(nm) > 0 { if err == nil && len(nm) > 0 {
// Last entry in frame list should match since // Last entry in frame list should match since it is non-inlined. As a
// it is non-inlined. As a simple heuristic, // simple heuristic, we only switch to the nm-based name if it is longer
// we only switch to the nm-based name if it // by 2 or more characters. We consider nm names that are longer by 1
// is longer. // 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 nmName := nm[len(nm)-1].Func
a2lName := stack[len(stack)-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 stack[len(stack)-1].Func = nmName
} }
} }
......
...@@ -81,6 +81,26 @@ func (bu *Binutils) update(fn func(r *binrep)) { ...@@ -81,6 +81,26 @@ func (bu *Binutils) update(fn func(r *binrep)) {
bu.rep = r 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 // SetFastSymbolization sets a toggle that makes binutils use fast
// symbolization (using nm), which is much faster than addr2line but // symbolization (using nm), which is much faster than addr2line but
// provides only symbol name information (no file/line). // provides only symbol name information (no file/line).
...@@ -111,6 +131,11 @@ func initTools(b *binrep, config string) { ...@@ -111,6 +131,11 @@ func initTools(b *binrep, config string) {
defaultPath := paths[""] defaultPath := paths[""]
b.llvmSymbolizer, b.llvmSymbolizerFound = findExe("llvm-symbolizer", append(paths["llvm-symbolizer"], defaultPath...)) b.llvmSymbolizer, b.llvmSymbolizerFound = findExe("llvm-symbolizer", append(paths["llvm-symbolizer"], defaultPath...))
b.addr2line, b.addr2lineFound = findExe("addr2line", append(paths["addr2line"], 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.nm, b.nmFound = findExe("nm", append(paths["nm"], defaultPath...))
b.objdump, b.objdumpFound = findExe("objdump", append(paths["objdump"], defaultPath...)) b.objdump, b.objdumpFound = findExe("objdump", append(paths["objdump"], defaultPath...))
} }
...@@ -306,9 +331,9 @@ func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) { ...@@ -306,9 +331,9 @@ func (f *fileNM) SourceLine(addr uint64) ([]plugin.Frame, error) {
} }
// fileAddr2Line implements the binutils.ObjFile interface, using // fileAddr2Line implements the binutils.ObjFile interface, using
// 'addr2line' to map addresses to symbols (with file/line number // llvm-symbolizer, if that's available, or addr2line to map addresses to
// information). It can be slow for large binaries with debug // symbols (with file/line number information). It can be slow for large
// information. // binaries with debug information.
type fileAddr2Line struct { type fileAddr2Line struct {
once sync.Once once sync.Once
file file
......
...@@ -265,8 +265,6 @@ func TestObjFile(t *testing.T) { ...@@ -265,8 +265,6 @@ func TestObjFile(t *testing.T) {
func TestMachoFiles(t *testing.T) { func TestMachoFiles(t *testing.T) {
skipUnlessDarwinAmd64(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 // Load `file`, pretending it was mapped at `start`. Then get the symbol
// table. Check that it contains the symbol `sym` and that the address // table. Check that it contains the symbol `sym` and that the address
// `addr` gives the `expected` stack trace. // `addr` gives the `expected` stack trace.
...@@ -291,7 +289,7 @@ func TestMachoFiles(t *testing.T) { ...@@ -291,7 +289,7 @@ func TestMachoFiles(t *testing.T) {
{"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0, {"lib normal mapping", "lib_mac_64", 0, math.MaxUint64, 0,
0xfa0, "_bar", 0xfa0, "_bar",
[]plugin.Frame{ []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) { t.Run(tc.desc, func(t *testing.T) {
...@@ -300,6 +298,13 @@ func TestMachoFiles(t *testing.T) { ...@@ -300,6 +298,13 @@ func TestMachoFiles(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Open: unexpected error %v", err) 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() defer f.Close()
syms, err := f.Symbols(nil, 0) syms, err := f.Symbols(nil, 0)
if err != nil { if err != nil {
......
#!/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
<?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>
<?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>
...@@ -54,7 +54,7 @@ func PProf(eo *plugin.Options) error { ...@@ -54,7 +54,7 @@ func PProf(eo *plugin.Options) error {
} }
if src.HTTPHostport != "" { if src.HTTPHostport != "" {
return serveWebInterface(src.HTTPHostport, p, o, true) return serveWebInterface(src.HTTPHostport, p, o)
} }
return interactive(p, o) return interactive(p, o)
} }
...@@ -138,7 +138,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin. ...@@ -138,7 +138,7 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
// Output to specified file. // Output to specified file.
o.UI.PrintErr("Generating report in ", output) o.UI.PrintErr("Generating report in ", output)
out, err := os.Create(output) out, err := o.Writer.Open(output)
if err != nil { if err != nil {
return err return err
} }
......
...@@ -96,9 +96,12 @@ func TestParse(t *testing.T) { ...@@ -96,9 +96,12 @@ func TestParse(t *testing.T) {
baseVars := pprofVariables baseVars := pprofVariables
defer func() { pprofVariables = baseVars }() defer func() { pprofVariables = baseVars }()
for _, tc := range testcase { for _, tc := range testcase {
t.Run(tc.flags+":"+tc.source, func(t *testing.T) {
// Reset the pprof variables before processing // Reset the pprof variables before processing
pprofVariables = baseVars.makeCopy() pprofVariables = baseVars.makeCopy()
testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"}
f := baseFlags() f := baseFlags()
f.args = []string{tc.source} f.args = []string{tc.source}
...@@ -126,9 +129,9 @@ func TestParse(t *testing.T) { ...@@ -126,9 +129,9 @@ func TestParse(t *testing.T) {
o1.Flagset = f o1.Flagset = f
o1.Fetch = testFetcher{} o1.Fetch = testFetcher{}
o1.Sym = testSymbolizer{} o1.Sym = testSymbolizer{}
o1.UI = testUI
if err := PProf(o1); err != nil { if err := PProf(o1); err != nil {
t.Errorf("%s %q: %v", tc.source, tc.flags, err) t.Fatalf("%s %q: %v", tc.source, tc.flags, err)
continue
} }
// Reset the pprof variables after the proto invocation // Reset the pprof variables after the proto invocation
pprofVariables = baseVars.makeCopy() pprofVariables = baseVars.makeCopy()
...@@ -164,6 +167,7 @@ func TestParse(t *testing.T) { ...@@ -164,6 +167,7 @@ func TestParse(t *testing.T) {
o2.Flagset = f o2.Flagset = f
o2.Sym = testSymbolizeDemangler{} o2.Sym = testSymbolizeDemangler{}
o2.Obj = new(mockObjTool) o2.Obj = new(mockObjTool)
o2.UI = testUI
if err := PProf(o2); err != nil { if err := PProf(o2); err != nil {
t.Errorf("%s: %v", tc.source, err) t.Errorf("%s: %v", tc.source, err)
...@@ -177,8 +181,7 @@ func TestParse(t *testing.T) { ...@@ -177,8 +181,7 @@ func TestParse(t *testing.T) {
solution = "testdata/" + solution solution = "testdata/" + solution
sbuf, err := ioutil.ReadFile(solution) sbuf, err := ioutil.ReadFile(solution)
if err != nil { if err != nil {
t.Errorf("reading solution file %s: %v", solution, err) t.Fatalf("reading solution file %s: %v", solution, err)
continue
} }
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1) sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte("testdata\\"), -1)
...@@ -204,6 +207,7 @@ func TestParse(t *testing.T) { ...@@ -204,6 +207,7 @@ func TestParse(t *testing.T) {
} }
} }
} }
})
} }
} }
......
...@@ -407,6 +407,7 @@ mapping: ...@@ -407,6 +407,7 @@ mapping:
if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil { if matches, err := filepath.Glob(filepath.Join(path, m.BuildID, "*")); err == nil {
fileNames = append(fileNames, matches...) fileNames = append(fileNames, matches...)
} }
fileNames = append(fileNames, filepath.Join(path, m.File, m.BuildID)) // perf path format
} }
if m.File != "" { if m.File != "" {
// Try both the basename and the full path, to support the same directory // Try both the basename and the full path, to support the same directory
...@@ -534,7 +535,8 @@ func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) { ...@@ -534,7 +535,8 @@ func convertPerfData(perfPath string, ui plugin.UI) (*os.File, error) {
return nil, err return nil, err
} }
deferDeleteTempFile(profile.Name()) 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 { if err := cmd.Run(); err != nil {
profile.Close() profile.Close()
return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err) return nil, fmt.Errorf("failed to convert perf.data file. Try github.com/google/perf_data_converter: %v", err)
......
...@@ -283,8 +283,10 @@ func TestFetchWithBase(t *testing.T) { ...@@ -283,8 +283,10 @@ func TestFetchWithBase(t *testing.T) {
} }
f.args = tc.sources f.args = tc.sources
o := setDefaults(nil) o := setDefaults(&plugin.Options{
o.Flagset = f UI: &proftest.TestUI{T: t, AllowRx: "Local symbolization failed|Some binary filenames not available"},
Flagset: f,
})
src, _, err := parseFlags(o) src, _, err := parseFlags(o)
if err != nil { if err != nil {
...@@ -397,18 +399,6 @@ func TestHttpsInsecure(t *testing.T) { ...@@ -397,18 +399,6 @@ func TestHttpsInsecure(t *testing.T) {
}() }()
defer l.Close() 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") outputTempFile, err := ioutil.TempFile("", "profile_output")
if err != nil { if err != nil {
t.Fatalf("Failed to create tempfile: %v", err) t.Fatalf("Failed to create tempfile: %v", err)
...@@ -416,7 +406,7 @@ func TestHttpsInsecure(t *testing.T) { ...@@ -416,7 +406,7 @@ func TestHttpsInsecure(t *testing.T) {
defer os.Remove(outputTempFile.Name()) defer os.Remove(outputTempFile.Name())
defer outputTempFile.Close() defer outputTempFile.Close()
address := "https+insecure://" + l.Addr().String() + "/debug/pprof/profile" address := "https+insecure://" + l.Addr().String() + "/debug/pprof/goroutine"
s := &source{ s := &source{
Sources: []string{address}, Sources: []string{address},
Seconds: 10, Seconds: 10,
...@@ -435,31 +425,14 @@ func TestHttpsInsecure(t *testing.T) { ...@@ -435,31 +425,14 @@ func TestHttpsInsecure(t *testing.T) {
if len(p.SampleType) == 0 { if len(p.SampleType) == 0 {
t.Fatalf("fetchProfiles(%s) got empty profile: len(p.SampleType)==0", address) 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 { if len(p.Function) == 0 {
t.Fatalf("fetchProfiles(%s) got non-symbolized profile: len(p.Function)==0", address) 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) 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 { func checkProfileHasFunction(p *profile.Profile, fname string) error {
for _, f := range p.Function { for _, f := range p.Function {
if strings.Contains(f.Name, fname) { if strings.Contains(f.Name, fname) {
......
...@@ -92,7 +92,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) { ...@@ -92,7 +92,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
return return
} }
ui.render(w, "/flamegraph", "flamegraph", rpt, errList, config.Labels, webArgs{ ui.render(w, "flamegraph", rpt, errList, config.Labels, webArgs{
FlameGraph: template.JS(b), FlameGraph: template.JS(b),
Nodes: nodeArr, Nodes: nodeArr,
}) })
......
...@@ -149,9 +149,14 @@ func greetings(p *profile.Profile, ui plugin.UI) { ...@@ -149,9 +149,14 @@ func greetings(p *profile.Profile, ui plugin.UI) {
numLabelUnits := identifyNumLabelUnits(p, ui) numLabelUnits := identifyNumLabelUnits(p, ui)
ropt, err := reportOptions(p, numLabelUnits, pprofVariables) ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
if err == nil { 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 // shortcuts represents composite commands that expand into a sequence
......
...@@ -128,6 +128,10 @@ func (ui *stdUI) IsTerminal() bool { ...@@ -128,6 +128,10 @@ func (ui *stdUI) IsTerminal() bool {
return false return false
} }
func (ui *stdUI) WantBrowser() bool {
return true
}
func (ui *stdUI) SetAutoComplete(func(string) string) { func (ui *stdUI) SetAutoComplete(func(string) string) {
} }
......
...@@ -233,7 +233,7 @@ table tr td { ...@@ -233,7 +233,7 @@ table tr td {
{{define "header"}} {{define "header"}}
<div class="header"> <div class="header">
<div class="title"> <div class="title">
<h1><a href="/">pprof</a></h1> <h1><a href="./">pprof</a></h1>
</div> </div>
<div id="view" class="menu-item"> <div id="view" class="menu-item">
...@@ -242,12 +242,12 @@ table tr td { ...@@ -242,12 +242,12 @@ table tr td {
<i class="downArrow"></i> <i class="downArrow"></i>
</div> </div>
<div class="submenu"> <div class="submenu">
<a title="{{.Help.top}}" href="/top" id="topbtn">Top</a> <a title="{{.Help.top}}" href="./top" id="topbtn">Top</a>
<a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a> <a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
<a title="{{.Help.flamegraph}}" href="/flamegraph" id="flamegraph">Flame 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.peek}}" href="./peek" id="peek">Peek</a>
<a title="{{.Help.list}}" href="/source" id="list">Source</a> <a title="{{.Help.list}}" href="./source" id="list">Source</a>
<a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a> <a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
</div> </div>
</div> </div>
...@@ -257,12 +257,12 @@ table tr td { ...@@ -257,12 +257,12 @@ table tr td {
<i class="downArrow"></i> <i class="downArrow"></i>
</div> </div>
<div class="submenu"> <div class="submenu">
<a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> <a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
<a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> <a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
<a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> <a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
<a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> <a title="{{.Help.show}}" href="?" id="show">Show</a>
<hr> <hr>
<a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> <a title="{{.Help.reset}}" href="?">Reset</a>
</div> </div>
</div> </div>
...@@ -295,7 +295,7 @@ table tr td { ...@@ -295,7 +295,7 @@ table tr td {
{{.HTMLBody}} {{.HTMLBody}}
</div> </div>
{{template "script" .}} {{template "script" .}}
<script>viewer({{.BaseURL}}, {{.Nodes}});</script> <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
</body> </body>
</html> </html>
{{end}} {{end}}
...@@ -597,7 +597,7 @@ function viewer(baseUrl, nodes) { ...@@ -597,7 +597,7 @@ function viewer(baseUrl, nodes) {
function handleKey(e) { function handleKey(e) {
if (e.keyCode != 13) return; if (e.keyCode != 13) return;
window.location.href = window.location.href =
updateUrl(new URL({{.BaseURL}}, window.location.href), 'f'); updateUrl(new URL(window.location.href), 'f');
e.preventDefault(); e.preventDefault();
} }
...@@ -963,7 +963,7 @@ function viewer(baseUrl, nodes) { ...@@ -963,7 +963,7 @@ function viewer(baseUrl, nodes) {
bindSort('namehdr', 'Name'); bindSort('namehdr', 'Name');
} }
viewer({{.BaseURL}}, {{.Nodes}}); viewer(new URL(window.location.href), {{.Nodes}});
makeTopTable({{.Total}}, {{.Top}}); makeTopTable({{.Total}}, {{.Top}});
</script> </script>
</body> </body>
...@@ -986,7 +986,7 @@ function viewer(baseUrl, nodes) { ...@@ -986,7 +986,7 @@ function viewer(baseUrl, nodes) {
{{.HTMLBody}} {{.HTMLBody}}
</div> </div>
{{template "script" .}} {{template "script" .}}
<script>viewer({{.BaseURL}}, null);</script> <script>viewer(new URL(window.location.href), null);</script>
</body> </body>
</html> </html>
{{end}} {{end}}
...@@ -1007,7 +1007,7 @@ function viewer(baseUrl, nodes) { ...@@ -1007,7 +1007,7 @@ function viewer(baseUrl, nodes) {
</pre> </pre>
</div> </div>
{{template "script" .}} {{template "script" .}}
<script>viewer({{.BaseURL}}, null);</script> <script>viewer(new URL(window.location.href), null);</script>
</body> </body>
</html> </html>
{{end}} {{end}}
...@@ -1044,7 +1044,7 @@ function viewer(baseUrl, nodes) { ...@@ -1044,7 +1044,7 @@ function viewer(baseUrl, nodes) {
<div id="flamegraphdetails" class="flamegraph-details"></div> <div id="flamegraphdetails" class="flamegraph-details"></div>
</div> </div>
{{template "script" .}} {{template "script" .}}
<script>viewer({{.BaseURL}}, {{.Nodes}});</script> <script>viewer(new URL(window.location.href), {{.Nodes}});</script>
<script>{{template "d3script" .}}</script> <script>{{template "d3script" .}}</script>
<script>{{template "d3tipscript" .}}</script> <script>{{template "d3tipscript" .}}</script>
<script>{{template "d3flamegraphscript" .}}</script> <script>{{template "d3flamegraphscript" .}}</script>
......
...@@ -69,7 +69,6 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) { ...@@ -69,7 +69,6 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) {
// webArgs contains arguments passed to templates in webhtml.go. // webArgs contains arguments passed to templates in webhtml.go.
type webArgs struct { type webArgs struct {
BaseURL string
Title string Title string
Errors []string Errors []string
Total int64 Total int64
...@@ -82,7 +81,7 @@ type webArgs struct { ...@@ -82,7 +81,7 @@ type webArgs struct {
FlameGraph template.JS 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) host, port, err := getHostAndPort(hostport)
if err != nil { if err != nil {
return err return err
...@@ -117,7 +116,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, w ...@@ -117,7 +116,7 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, w
}, },
} }
if wantBrowser { if o.UI.WantBrowser() {
go openBrowser("http://"+args.Hostport, o) go openBrowser("http://"+args.Hostport, o)
} }
return server(args) return server(args)
...@@ -172,7 +171,15 @@ func defaultWebServer(args *plugin.HTTPServerArgs) error { ...@@ -172,7 +171,15 @@ func defaultWebServer(args *plugin.HTTPServerArgs) error {
} }
h.ServeHTTP(w, req) 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) return s.Serve(ln)
} }
...@@ -248,11 +255,10 @@ func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request, ...@@ -248,11 +255,10 @@ func (ui *webInterface) makeReport(w http.ResponseWriter, req *http.Request,
} }
// render generates html using the named template based on the contents of data. // 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) { rpt *report.Report, errList, legend []string, data webArgs) {
file := getFromLegend(legend, "File: ", "unknown") file := getFromLegend(legend, "File: ", "unknown")
profile := getFromLegend(legend, "Type: ", "unknown") profile := getFromLegend(legend, "Type: ", "unknown")
data.BaseURL = baseURL
data.Title = file + " " + profile data.Title = file + " " + profile
data.Errors = errList data.Errors = errList
data.Total = rpt.Total() data.Total = rpt.Total()
...@@ -297,7 +303,7 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) { ...@@ -297,7 +303,7 @@ func (ui *webInterface) dot(w http.ResponseWriter, req *http.Request) {
nodes = append(nodes, n.Info.Name) 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)), HTMLBody: template.HTML(string(svg)),
Nodes: nodes, Nodes: nodes,
}) })
...@@ -332,7 +338,7 @@ func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) { ...@@ -332,7 +338,7 @@ func (ui *webInterface) top(w http.ResponseWriter, req *http.Request) {
nodes = append(nodes, item.Name) nodes = append(nodes, item.Name)
} }
ui.render(w, "/top", "top", rpt, errList, legend, webArgs{ ui.render(w, "top", rpt, errList, legend, webArgs{
Top: top, Top: top,
Nodes: nodes, Nodes: nodes,
}) })
...@@ -354,7 +360,7 @@ func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) { ...@@ -354,7 +360,7 @@ func (ui *webInterface) disasm(w http.ResponseWriter, req *http.Request) {
} }
legend := report.ProfileLabels(rpt) legend := report.ProfileLabels(rpt)
ui.render(w, "/disasm", "plaintext", rpt, errList, legend, webArgs{ ui.render(w, "plaintext", rpt, errList, legend, webArgs{
TextBody: out.String(), TextBody: out.String(),
}) })
...@@ -378,7 +384,7 @@ func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) { ...@@ -378,7 +384,7 @@ func (ui *webInterface) source(w http.ResponseWriter, req *http.Request) {
} }
legend := report.ProfileLabels(rpt) 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()), HTMLBody: template.HTML(body.String()),
}) })
} }
...@@ -399,7 +405,7 @@ func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) { ...@@ -399,7 +405,7 @@ func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) {
} }
legend := report.ProfileLabels(rpt) legend := report.ProfileLabels(rpt)
ui.render(w, "/peek", "plaintext", rpt, errList, legend, webArgs{ ui.render(w, "plaintext", rpt, errList, legend, webArgs{
TextBody: out.String(), TextBody: out.String(),
}) })
} }
......
...@@ -28,6 +28,7 @@ import ( ...@@ -28,6 +28,7 @@ import (
"testing" "testing"
"github.com/google/pprof/internal/plugin" "github.com/google/pprof/internal/plugin"
"github.com/google/pprof/internal/proftest"
"github.com/google/pprof/profile" "github.com/google/pprof/profile"
) )
...@@ -55,9 +56,9 @@ func TestWebInterface(t *testing.T) { ...@@ -55,9 +56,9 @@ func TestWebInterface(t *testing.T) {
// Start server and wait for it to be initialized // Start server and wait for it to be initialized
go serveWebInterface("unused:1234", prof, &plugin.Options{ go serveWebInterface("unused:1234", prof, &plugin.Options{
Obj: fakeObjTool{}, Obj: fakeObjTool{},
UI: &stdUI{}, UI: &proftest.TestUI{},
HTTPServer: creator, HTTPServer: creator,
}, false) })
<-serverCreated <-serverCreated
defer server.Close() defer server.Close()
......
...@@ -218,7 +218,7 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6 ...@@ -218,7 +218,7 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6
// So the base should be: // So the base should be:
if stextOffset != nil && (start%pageSize) == (*stextOffset%pageSize) { if stextOffset != nil && (start%pageSize) == (*stextOffset%pageSize) {
// perf uses the address of _stext as start. Some tools may // 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. // alignment should be different from that of stextOffset.
return start - *stextOffset, nil return start - *stextOffset, nil
} }
......
...@@ -192,6 +192,9 @@ type UI interface { ...@@ -192,6 +192,9 @@ type UI interface {
// interactive terminal (as opposed to being redirected to a file). // interactive terminal (as opposed to being redirected to a file).
IsTerminal() bool 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 // SetAutoComplete instructs the UI to call complete(cmd) to obtain
// the auto-completion of cmd, if the UI supports auto-completion at all. // the auto-completion of cmd, if the UI supports auto-completion at all.
SetAutoComplete(complete func(string) string) SetAutoComplete(complete func(string) string)
......
...@@ -122,7 +122,7 @@ func (ui *TestUI) PrintErr(args ...interface{}) { ...@@ -122,7 +122,7 @@ func (ui *TestUI) PrintErr(args ...interface{}) {
// implementation does. Without this Error() calls fmt.Sprintln() which // implementation does. Without this Error() calls fmt.Sprintln() which
// _always_ adds spaces between arguments, unlike fmt.Sprint() which only // _always_ adds spaces between arguments, unlike fmt.Sprint() which only
// adds them between arguments if neither is string. // 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. // IsTerminal indicates if the UI is an interactive terminal.
...@@ -130,6 +130,11 @@ func (ui *TestUI) IsTerminal() bool { ...@@ -130,6 +130,11 @@ func (ui *TestUI) IsTerminal() bool {
return false 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. // SetAutoComplete is not supported by the test UI.
func (ui *TestUI) SetAutoComplete(_ func(string) string) { func (ui *TestUI) SetAutoComplete(_ func(string) string) {
} }
...@@ -576,7 +576,7 @@ func openSourceFile(path, searchPath string) (*os.File, error) { ...@@ -576,7 +576,7 @@ func openSourceFile(path, searchPath string) (*os.File, error) {
} }
// Scan each component of the path // 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. // Search up for every parent of each possible path.
for { for {
filename := filepath.Join(dir, path) filename := filepath.Join(dir, path)
......
...@@ -2,6 +2,7 @@ package report ...@@ -2,6 +2,7 @@ package report
import ( import (
"bytes" "bytes"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
...@@ -38,6 +39,85 @@ func TestWebList(t *testing.T) { ...@@ -38,6 +39,85 @@ func TestWebList(t *testing.T) {
} }
} }
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) { func TestIndentation(t *testing.T) {
for _, c := range []struct { for _, c := range []struct {
str string str string
......
...@@ -12,7 +12,7 @@ const JSSource = ` ...@@ -12,7 +12,7 @@ const JSSource = `
* ====================== * ======================
* *
* Given an unique existing element with id "viewport" (or when missing, the * 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: * capabilities:
* *
* - Mouse panning * - Mouse panning
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment