Commit 46047e64 authored by Hana (Hyang-Ah) Kim's avatar Hana (Hyang-Ah) Kim Committed by Hyang-Ah Hana Kim

cmd/vendor/.../pprof: update to 520140b6bf47519c766e8380e5f094576347b016

Change-Id: If431dfa59496b86f58f2ba2a83ca544a28a2a972
Reviewed-on: https://go-review.googlesource.com/112435Reviewed-by: default avatarIan Lance Taylor <iant@golang.org>
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
parent d540da10
......@@ -102,8 +102,8 @@ pprof can read `perf.data` files generated by the
## Further documentation
See [doc/pprof.md](doc/pprof.md) for more detailed end-user documentation.
See [doc/README.md](doc/README.md) for more detailed end-user documentation.
See [doc/developer/pprof.dev.md](doc/developer/pprof.dev.md) for developer documentation.
See [CONTRIBUTING.md](CONTRIBUTING.md) for contribution documentation.
See [doc/developer/profile.proto.md](doc/developer/profile.proto.md) for a description of the profile.proto format.
See [proto/README.md](proto/README.md) for a description of the profile.proto format.
......@@ -94,6 +94,8 @@ Some common pprof options are:
*regex*.
* **-ignore= _regex_:** Do not include samples that include a report entry matching
*regex*.
* **-show\_from= _regex_:** Do not show entries above the first one that
matches *regex*.
* **-show= _regex_:** Only show entries that match *regex*.
* **-hide= _regex_:** Do not show entries that match *regex*.
......
This is pprof's developer documentation. It discusses how to maintain and extend
pprof. It has yet to be written.
# How is pprof code structured?
Internal vs external packages.
# External interface
## Plugins
## Legacy formats
# Overview of internal packages
......@@ -316,5 +316,5 @@ var usageMsgVars = "\n\n" +
" PPROF_TOOLS Search path for object-level tools\n" +
" PPROF_BINARY_PATH Search path for local binary files\n" +
" default: $HOME/pprof/binaries\n" +
" finds binaries by $name and $buildid/$name\n" +
" searches $name, $path, $buildid/$name, $path/$buildid\n" +
" * On Windows, %USERPROFILE% is used instead of $HOME"
......@@ -135,14 +135,6 @@ var pprofVariables = variables{
"Ignore negative differences",
"Do not show any locations with values <0.")},
// Comparisons.
"positive_percentages": &variable{boolKind, "f", "", helpText(
"Ignore negative samples when computing percentages",
"Do not count negative samples when computing the total value",
"of the profile, used to compute percentages. If set, and the -base",
"option is used, percentages reported will be computed against the",
"main profile, ignoring the base profile.")},
// Graph handling options.
"call_tree": &variable{boolKind, "f", "", helpText(
"Create a context-sensitive call tree",
......@@ -161,6 +153,7 @@ var pprofVariables = variables{
"Using auto will scale each value independently to the most natural unit.")},
"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
"source_path": &variable{stringKind, "", "", "Search path for source files"},
"trim_path": &variable{stringKind, "", "", "Path to trim from source paths before search"},
// Filtering options
"nodecount": &variable{intKind, "-1", "", helpText(
......@@ -193,6 +186,10 @@ var pprofVariables = variables{
"Only show nodes matching regexp",
"If set, only show nodes that match this location.",
"Matching includes the function name, filename or object name.")},
"show_from": &variable{stringKind, "", "", helpText(
"Drops functions above the highest matched frame.",
"If set, all frames above the highest match are dropped from every sample.",
"Matching includes the function name, filename or object name.")},
"tagfocus": &variable{stringKind, "", "", helpText(
"Restricts to samples with tags in range or matched by regexp",
"Use name=value syntax to limit the matching to a specific tag.",
......
......@@ -150,11 +150,11 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
}
func applyCommandOverrides(cmd []string, v variables) variables {
trim, focus, tagfocus, hide := v["trim"].boolValue(), true, true, true
trim, tagfilter, filter := v["trim"].boolValue(), true, true
switch cmd[0] {
case "proto", "raw":
trim, focus, tagfocus, hide = false, false, false, false
trim, tagfilter, filter = false, false, false
v.set("addresses", "t")
case "callgrind", "kcachegrind":
trim = false
......@@ -163,7 +163,7 @@ func applyCommandOverrides(cmd []string, v variables) variables {
trim = false
v.set("addressnoinlines", "t")
case "peek":
trim, focus, hide = false, false, false
trim, filter = false, false
case "list":
v.set("nodecount", "0")
v.set("lines", "t")
......@@ -181,17 +181,16 @@ func applyCommandOverrides(cmd []string, v variables) variables {
v.set("nodefraction", "0")
v.set("edgefraction", "0")
}
if !focus {
v.set("focus", "")
v.set("ignore", "")
}
if !tagfocus {
if !tagfilter {
v.set("tagfocus", "")
v.set("tagignore", "")
}
if !hide {
if !filter {
v.set("focus", "")
v.set("ignore", "")
v.set("hide", "")
v.set("show", "")
v.set("show_from", "")
}
return v
}
......@@ -242,7 +241,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
}
var filters []string
for _, k := range []string{"focus", "ignore", "hide", "show", "tagfocus", "tagignore", "tagshow", "taghide"} {
for _, k := range []string{"focus", "ignore", "hide", "show", "show_from", "tagfocus", "tagignore", "tagshow", "taghide"} {
v := vars[k].value
if v != "" {
filters = append(filters, k+"="+v)
......@@ -253,7 +252,6 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
CumSort: vars["cum"].boolValue(),
CallTree: vars["call_tree"].boolValue(),
DropNegative: vars["drop_negative"].boolValue(),
PositivePercentages: vars["positive_percentages"].boolValue(),
CompactLabels: vars["compact_labels"].boolValue(),
Ratio: 1 / vars["divide_by"].floatValue(),
......@@ -273,6 +271,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
OutputUnit: vars["unit"].value,
SourcePath: vars["source_path"].stringValue(),
TrimPath: vars["trim_path"].stringValue(),
}
if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
......
......@@ -33,6 +33,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
hide, err := compileRegexOption("hide", v["hide"].value, err)
show, err := compileRegexOption("show", v["show"].value, err)
showfrom, err := compileRegexOption("show_from", v["show_from"].value, err)
tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
......@@ -46,6 +47,9 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
warnNoMatches(hide == nil || hm, "Hide", ui)
warnNoMatches(show == nil || hnm, "Show", ui)
sfm := prof.ShowFrom(showfrom)
warnNoMatches(showfrom == nil || sfm, "ShowFrom", ui)
tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore)
warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
......
......@@ -65,6 +65,7 @@ func TestParse(t *testing.T) {
{"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
{"tree,lines,cum,focus=[24]00", "heap"},
{"tree,relative_percentages,cum,focus=[24]00", "heap"},
{"tree,lines,cum,show_from=line2", "cpu"},
{"callgrind", "cpu"},
{"callgrind,call_tree", "cpu"},
{"callgrind", "heap"},
......@@ -261,6 +262,9 @@ func solutionFilename(source string, f *testFlags) string {
if f.strings["ignore"] != "" || f.strings["tagignore"] != "" {
name = append(name, "ignore")
}
if f.strings["show_from"] != "" {
name = append(name, "show_from")
}
name = addString(name, f, []string{"hide", "show"})
if f.strings["unit"] != "minimum" {
name = addString(name, f, []string{"unit"})
......
......@@ -599,9 +599,9 @@ var httpGet = func(source string, timeout time.Duration) (*http.Response, error)
client := &http.Client{
Transport: &http.Transport{
ResponseHeaderTimeout: timeout + 5*time.Second,
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: tlsConfig,
ResponseHeaderTimeout: timeout + 5*time.Second,
},
}
return client.Get(source)
......
......@@ -27,6 +27,7 @@ import (
type treeNode struct {
Name string `json:"n"`
FullName string `json:"f"`
Cum int64 `json:"v"`
CumFormat string `json:"l"`
Percent string `json:"p"`
......@@ -52,8 +53,10 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
// Make all nodes and the map, collect the roots.
for _, n := range g.Nodes {
v := n.CumValue()
fullName := n.Info.PrintableName()
node := &treeNode{
Name: n.Info.PrintableName(),
Name: getNodeShortName(fullName),
FullName: fullName,
Cum: v,
CumFormat: config.FormatValue(v),
Percent: strings.TrimSpace(measurement.Percentage(v, config.Total)),
......@@ -78,6 +81,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
rootNode := &treeNode{
Name: "root",
FullName: "root",
Cum: rootValue,
CumFormat: config.FormatValue(rootValue),
Percent: strings.TrimSpace(measurement.Percentage(rootValue, config.Total)),
......@@ -97,3 +101,19 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
Nodes: nodeArr,
})
}
// getNodeShortName builds a short node name from fullName.
func getNodeShortName(name string) string {
chunks := strings.SplitN(name, "(", 2)
head := chunks[0]
pathSep := strings.LastIndexByte(head, '/')
if pathSep == -1 || pathSep+1 >= len(head) {
return name
}
// Check if name is a stdlib package, i.e. doesn't have "." before "/"
if dot := strings.IndexByte(head, '.'); dot == -1 || dot > pathSep {
return name
}
// Trim package path prefix from node name
return name[pathSep+1:]
}
package driver
import "testing"
func TestGetNodeShortName(t *testing.T) {
type testCase struct {
name string
want string
}
testcases := []testCase{
{
"root",
"root",
},
{
"syscall.Syscall",
"syscall.Syscall",
},
{
"net/http.(*conn).serve",
"net/http.(*conn).serve",
},
{
"github.com/blah/foo.Foo",
"foo.Foo",
},
{
"github.com/blah/foo_bar.(*FooBar).Foo",
"foo_bar.(*FooBar).Foo",
},
{
"encoding/json.(*structEncoder).(encoding/json.encode)-fm",
"encoding/json.(*structEncoder).(encoding/json.encode)-fm",
},
{
"github.com/blah/blah/vendor/gopkg.in/redis.v3.(*baseClient).(github.com/blah/blah/vendor/gopkg.in/redis.v3.process)-fm",
"redis.v3.(*baseClient).(github.com/blah/blah/vendor/gopkg.in/redis.v3.process)-fm",
},
}
for _, tc := range testcases {
name := getNodeShortName(tc.name)
if got, want := name, tc.want; got != want {
t.Errorf("for %s, got %q, want %q", tc.name, got, want)
}
}
}
Active filters:
show_from=line2
Showing nodes accounting for 1.01s, 90.18% of 1.12s total
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
0 0% 0% 1.01s 90.18% | line2000 testdata/file2000.src:4
1.01s 100% | line2001 testdata/file2000.src:9 (inline)
----------------------------------------------------------+-------------
1.01s 100% | line2000 testdata/file2000.src:4 (inline)
0.01s 0.89% 0.89% 1.01s 90.18% | line2001 testdata/file2000.src:9
1s 99.01% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------
1s 100% | line2001 testdata/file2000.src:9
1s 89.29% 90.18% 1s 89.29% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------
......@@ -17,13 +17,11 @@ 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(`
......@@ -1031,49 +1029,51 @@ function viewer(baseUrl, nodes) {
width: 90%;
min-width: 90%;
margin-left: 5%;
padding-bottom: 41px;
padding: 15px 0 35px;
}
</style>
</head>
<body>
{{template "header" .}}
<div id="bodycontainer">
<div id="flamegraphdetails" class="flamegraph-details"></div>
<div class="flamegraph-content">
<div id="chart"></div>
</div>
<div id="flamegraphdetails" class="flamegraph-details"></div>
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
<script>{{template "d3script" .}}</script>
<script>{{template "d3tipscript" .}}</script>
<script>{{template "d3flamegraphscript" .}}</script>
<script type="text/javascript">
<script>
var data = {{.FlameGraph}};
var label = function(d) {
return d.data.n + ' (' + d.data.p + ', ' + d.data.l + ')';
};
var width = document.getElementById('chart').clientWidth;
var flameGraph = d3.flameGraph()
var flameGraph = d3.flamegraph()
.width(width)
.cellHeight(18)
.minFrameSize(1)
.transitionDuration(750)
.transitionEase(d3.easeCubic)
.sort(true)
.inverted(true)
.title('')
.label(label)
.tooltip(false)
.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; });
// <full name> (percentage, value)
flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')');
(function(flameGraph) {
var oldColorMapper = flameGraph.color();
function colorMapper(d) {
// Hack to force default color mapper to use 'warm' color scheme by not passing libtype
const { data, highlight } = d;
return oldColorMapper({ data: { n: data.n }, highlight });
}
flameGraph.tooltip(tip);
flameGraph.color(colorMapper);
}(flameGraph));
d3.select('#chart')
.datum(data)
......
......@@ -33,7 +33,7 @@ import (
)
func TestWebInterface(t *testing.T) {
if runtime.GOOS == "nacl" {
if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
t.Skip("test assumes tcp available")
}
......@@ -81,7 +81,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},
{"/flamegraph", []string{"File: testbin", "\"n\":\"root\"", "\"n\":\"F1\"", "var flamegraph = function", "function hierarchy"}, false},
}
for _, c := range testcases {
if c.needDot && !haveDot {
......
......@@ -208,13 +208,13 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6
if loadSegment.Vaddr == start-offset {
return offset, nil
}
if start >= loadSegment.Vaddr && limit > start && (offset == 0 || offset == pageOffsetPpc64) {
if start >= loadSegment.Vaddr && limit > start && (offset == 0 || offset == pageOffsetPpc64 || offset == start) {
// Some kernels look like:
// VADDR=0xffffffff80200000
// stextOffset=0xffffffff80200198
// Start=0xffffffff83200000
// Limit=0xffffffff84200000
// Offset=0 (0xc000000000000000 for PowerPC64)
// Offset=0 (0xc000000000000000 for PowerPC64) (== Start for ASLR kernel)
// So the base should be:
if stextOffset != nil && (start%pageSize) == (*stextOffset%pageSize) {
// perf uses the address of _stext as start. Some tools may
......
......@@ -55,7 +55,9 @@ func TestGetBase(t *testing.T) {
{"exec offset 2", fhExec, lsOffset, nil, 0x200000, 0x600000, 0, 0, false},
{"exec nomap", fhExec, nil, nil, 0, 0, 0, 0, false},
{"exec kernel", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0xffffffff82000198, 0xffffffff83000198, 0, 0x1000000, false},
{"exec PPC64 kernel", fhExec, ppc64KernelHeader, uint64p(0xc000000000000000), 0xc000000000000000, 0xd00000001a730000, 0xc000000000000000, 0x0, false},
{"exec kernel", fhExec, kernelHeader, uint64p(0xffffffff810002b8), 0xffffffff81000000, 0xffffffffa0000000, 0x0, 0x0, false},
{"exec kernel ASLR", fhExec, kernelHeader, uint64p(0xffffffff810002b8), 0xffffffff81000000, 0xffffffffa0000000, 0xffffffff81000000, 0x0, false},
{"exec PPC64 kernel", fhExec, ppc64KernelHeader, uint64p(0xc000000000000000), 0xc000000000000000, 0xd00000001a730000, 0x0, 0x0, false},
{"exec chromeos kernel", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0, 0x10197, 0, 0x7efffe68, false},
{"exec chromeos kernel 2", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0, 0x10198, 0, 0x7efffe68, false},
{"exec chromeos kernel 3", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0x198, 0x100000, 0, 0x7f000000, false},
......
......@@ -58,7 +58,6 @@ type Options struct {
CumSort bool
CallTree bool
DropNegative bool
PositivePercentages bool
CompactLabels bool
Ratio float64
Title string
......@@ -79,6 +78,7 @@ type Options struct {
Symbol *regexp.Regexp // Symbols to include on disassembly report.
SourcePath string // Search path for source files.
TrimPath string // Paths to trim from source file paths.
}
// Generate generates a report as directed by the Report.
......@@ -239,7 +239,7 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
// Clean up file paths using heuristics.
prof := rpt.prof
for _, f := range prof.Function {
f.Filename = trimPath(f.Filename)
f.Filename = trimPath(f.Filename, o.TrimPath, o.SourcePath)
}
// Removes all numeric tags except for the bytes tag prior
// to making graph.
......@@ -1192,7 +1192,7 @@ func New(prof *profile.Profile, o *Options) *Report {
}
return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit)
}
return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor, !o.PositivePercentages),
return &Report{prof, computeTotal(prof, o.SampleValue, o.SampleMeanDivisor),
o, format}
}
......@@ -1213,11 +1213,8 @@ func NewDefault(prof *profile.Profile, options Options) *Report {
}
// computeTotal computes the sum of all sample values. This will be
// used to compute percentages. If includeNegative is set, use use
// absolute values to provide a meaningful percentage for both
// negative and positive values. Otherwise only use positive values,
// which is useful when comparing profiles from different jobs.
func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64, includeNegative bool) int64 {
// used to compute percentages.
func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
var div, ret int64
for _, sample := range prof.Sample {
var d, v int64
......@@ -1225,13 +1222,11 @@ func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64, i
if meanDiv != nil {
d = meanDiv(sample.Value)
}
if v >= 0 {
if v < 0 {
v = -v
}
ret += v
div += d
} else if includeNegative {
ret -= v
div += d
}
}
if div != 0 {
return ret / div
......
......@@ -46,6 +46,7 @@ func TestSource(t *testing.T) {
&Options{
OutputFormat: List,
Symbol: regexp.MustCompile(`.`),
TrimPath: "/some/path",
SampleValue: sampleValue1,
SampleUnit: testProfile.SampleType[1].Unit,
......@@ -60,6 +61,7 @@ func TestSource(t *testing.T) {
OutputFormat: Dot,
CallTree: true,
Symbol: regexp.MustCompile(`.`),
TrimPath: "/some/path",
SampleValue: sampleValue1,
SampleUnit: testProfile.SampleType[1].Unit,
......@@ -119,7 +121,7 @@ var testF = []*profile.Function{
{
ID: 4,
Name: "tee",
Filename: "testdata/source2",
Filename: "/some/path/testdata/source2",
},
}
......
......@@ -63,7 +63,7 @@ func printSource(w io.Writer, rpt *Report) error {
}
sourcePath = wd
}
reader := newSourceReader(sourcePath)
reader := newSourceReader(sourcePath, o.TrimPath)
fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
for _, fn := range functions {
......@@ -146,7 +146,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er
}
sourcePath = wd
}
reader := newSourceReader(sourcePath)
reader := newSourceReader(sourcePath, o.TrimPath)
type fileFunction struct {
fileName, functionName string
......@@ -391,8 +391,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
continue
}
curCalls = nil
fname := trimPath(c.file)
fline, ok := reader.line(fname, c.line)
fline, ok := reader.line(c.file, c.line)
if !ok {
fline = ""
}
......@@ -400,7 +399,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
fmt.Fprintf(w, " %8s %10s %10s %8s <span class=inlinesrc>%s</span> <span class=unimportant>%s:%d</span>\n",
"", "", "", "",
template.HTMLEscapeString(fmt.Sprintf("%-80s", text)),
template.HTMLEscapeString(filepath.Base(fname)), c.line)
template.HTMLEscapeString(filepath.Base(c.file)), c.line)
}
curCalls = an.inlineCalls
text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
......@@ -426,7 +425,6 @@ func printPageClosing(w io.Writer) {
// file and annotates it with the samples in fns. Returns the sources
// as nodes, using the info.name field to hold the source code.
func getSourceFromFile(file string, reader *sourceReader, fns graph.Nodes, start, end int) (graph.Nodes, string, error) {
file = trimPath(file)
lineNodes := make(map[int]graph.Nodes)
// Collect source coordinates from profile.
......@@ -516,8 +514,13 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction
// sourceReader provides access to source code with caching of file contents.
type sourceReader struct {
// searchPath is a filepath.ListSeparator-separated list of directories where
// source files should be searched.
searchPath string
// trimPath is a filepath.ListSeparator-separated list of paths to trim.
trimPath string
// files maps from path name to a list of lines.
// files[*][0] is unused since line numbering starts at 1.
files map[string][]string
......@@ -527,9 +530,10 @@ type sourceReader struct {
errors map[string]error
}
func newSourceReader(searchPath string) *sourceReader {
func newSourceReader(searchPath, trimPath string) *sourceReader {
return &sourceReader{
searchPath,
trimPath,
make(map[string][]string),
make(map[string]error),
}
......@@ -544,7 +548,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
if !ok {
// Read and cache file contents.
lines = []string{""} // Skip 0th line
f, err := openSourceFile(path, reader.searchPath)
f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
if err != nil {
reader.errors[path] = err
} else {
......@@ -565,17 +569,20 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
return lines[lineno], true
}
// openSourceFile opens a source file from a name encoded in a
// profile. File names in a profile after often relative paths, so
// search them in each of the paths in searchPath (or CWD by default),
// and their parents.
func openSourceFile(path, searchPath string) (*os.File, error) {
// openSourceFile opens a source file from a name encoded in a profile. File
// names in a profile after can be relative paths, so search them in each of
// the paths in searchPath and their parents. In case the profile contains
// absolute paths, additional paths may be configured to trim from the source
// paths in the profile. This effectively turns the path into a relative path
// searching it using searchPath as usual).
func openSourceFile(path, searchPath, trim string) (*os.File, error) {
path = trimPath(path, trim, searchPath)
// If file is still absolute, require file to exist.
if filepath.IsAbs(path) {
f, err := os.Open(path)
return f, err
}
// Scan each component of the path
// Scan each component of the path.
for _, dir := range filepath.SplitList(searchPath) {
// Search up for every parent of each possible path.
for {
......@@ -595,18 +602,34 @@ func openSourceFile(path, searchPath string) (*os.File, error) {
}
// trimPath cleans up a path by removing prefixes that are commonly
// found on profiles.
func trimPath(path string) string {
basePaths := []string{
"/proc/self/cwd/./",
"/proc/self/cwd/",
// found on profiles plus configured prefixes.
// TODO(aalexand): Consider optimizing out the redundant work done in this
// function if it proves to matter.
func trimPath(path, trimPath, searchPath string) string {
// Keep path variable intact as it's used below to form the return value.
sPath, searchPath := filepath.ToSlash(path), filepath.ToSlash(searchPath)
if trimPath == "" {
// If the trim path is not configured, try to guess it heuristically:
// search for basename of each search path in the original path and, if
// found, strip everything up to and including the basename. So, for
// example, given original path "/some/remote/path/my-project/foo/bar.c"
// and search path "/my/local/path/my-project" the heuristic will return
// "/my/local/path/my-project/foo/bar.c".
for _, dir := range filepath.SplitList(searchPath) {
want := "/" + filepath.Base(dir) + "/"
if found := strings.Index(sPath, want); found != -1 {
return path[found+len(want):]
}
sPath := filepath.ToSlash(path)
for _, base := range basePaths {
if strings.HasPrefix(sPath, base) {
return filepath.FromSlash(sPath[len(base):])
}
}
// Trim configured trim prefixes.
trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/")
for _, trimPath := range trimPaths {
if !strings.HasSuffix(trimPath, "/") {
trimPath += "/"
}
if strings.HasPrefix(sPath, trimPath) {
return path[len(trimPath):]
}
}
return path
......
......@@ -48,40 +48,56 @@ func TestOpenSourceFile(t *testing.T) {
for _, tc := range []struct {
desc string
searchPath string
trimPath 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",
fs: []string{"foo/bar.cc"},
path: "$dir/foo/bar.cc",
wantPath: "$dir/foo/bar.cc",
},
{
desc: "exact relative path is found",
searchPath: "$dir",
fs: []string{"foo/bar.txt"},
path: "foo/bar.txt",
wantPath: "$dir/foo/bar.txt",
fs: []string{"foo/bar.cc"},
path: "foo/bar.cc",
wantPath: "$dir/foo/bar.cc",
},
{
desc: "multiple search path",
searchPath: "some/path" + lsep + "$dir",
fs: []string{"foo/bar.txt"},
path: "foo/bar.txt",
wantPath: "$dir/foo/bar.txt",
fs: []string{"foo/bar.cc"},
path: "foo/bar.cc",
wantPath: "$dir/foo/bar.cc",
},
{
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",
fs: []string{"bar.cc", "foo/bar/baz.cc"},
path: "bar.cc",
wantPath: "$dir/bar.cc",
},
{
desc: "trims configured prefix",
searchPath: "$dir",
trimPath: "some-path" + lsep + "/some/remote/path",
fs: []string{"my-project/foo/bar.cc"},
path: "/some/remote/path/my-project/foo/bar.cc",
wantPath: "$dir/my-project/foo/bar.cc",
},
{
desc: "trims heuristically",
searchPath: "$dir/my-project",
fs: []string{"my-project/foo/bar.cc"},
path: "/some/remote/path/my-project/foo/bar.cc",
wantPath: "$dir/my-project/foo/bar.cc",
},
{
desc: "error when not found",
path: "foo.txt",
path: "foo.cc",
},
} {
t.Run(tc.desc, func(t *testing.T) {
......@@ -103,15 +119,15 @@ func TestOpenSourceFile(t *testing.T) {
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)
if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" {
t.Errorf("openSourceFile(%q, %q, %q) = err %v, want path %q", tc.path, tc.searchPath, tc.trimPath, 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)
t.Errorf("openSourceFile(%q, %q, %q) = %q, want error", tc.path, tc.searchPath, tc.trimPath, gotPath)
} else if gotPath != tc.wantPath {
t.Errorf("openSourceFile(%q, %q) = %q, want path %q", tc.path, tc.searchPath, gotPath, tc.wantPath)
t.Errorf("openSourceFile(%q, %q, %q) = %q, want path %q", tc.path, tc.searchPath, tc.trimPath, gotPath, tc.wantPath)
}
}
})
......
......@@ -34,17 +34,22 @@ var (
symbolzRE = regexp.MustCompile(`(0x[[:xdigit:]]+)\s+(.*)`)
)
// Symbolize symbolizes profile p by parsing data returned by a
// symbolz handler. syms receives the symbolz query (hex addresses
// separated by '+') and returns the symbolz output in a string. If
// force is false, it will only symbolize locations from mappings
// not already marked as HasFunctions.
// Symbolize symbolizes profile p by parsing data returned by a symbolz
// handler. syms receives the symbolz query (hex addresses separated by '+')
// and returns the symbolz output in a string. If force is false, it will only
// symbolize locations from mappings not already marked as HasFunctions. Never
// attempts symbolization of addresses from unsymbolizable system
// mappings as those may look negative - e.g. "[vsyscall]".
func Symbolize(p *profile.Profile, force bool, sources plugin.MappingSources, syms func(string, string) ([]byte, error), ui plugin.UI) error {
for _, m := range p.Mapping {
if !force && m.HasFunctions {
// Only check for HasFunctions as symbolz only populates function names.
continue
}
// Skip well-known system mappings.
if m.Unsymbolizable() {
continue
}
mappingSources := sources[m.File]
if m.BuildID != "" {
mappingSources = append(mappingSources, sources[m.BuildID]...)
......
......@@ -19,13 +19,88 @@ package main
import (
"fmt"
"os"
"strings"
"github.com/chzyer/readline"
"github.com/google/pprof/driver"
)
func main() {
if err := driver.PProf(&driver.Options{}); err != nil {
if err := driver.PProf(&driver.Options{UI: newUI()}); err != nil {
fmt.Fprintf(os.Stderr, "pprof: %v\n", err)
os.Exit(2)
}
}
// readlineUI implements the driver.UI interface using the
// github.com/chzyer/readline library.
// This is contained in pprof.go to avoid adding the readline
// dependency in the vendored copy of pprof in the Go distribution,
// which does not use this file.
type readlineUI struct {
rl *readline.Instance
}
func newUI() driver.UI {
rl, err := readline.New("")
if err != nil {
fmt.Fprintf(os.Stderr, "readline: %v", err)
return nil
}
return &readlineUI{
rl: rl,
}
}
// Read returns a line of text (a command) read from the user.
// prompt is printed before reading the command.
func (r *readlineUI) ReadLine(prompt string) (string, error) {
r.rl.SetPrompt(prompt)
return r.rl.Readline()
}
// Print shows a message to the user.
// It is printed over stderr as stdout is reserved for regular output.
func (r *readlineUI) Print(args ...interface{}) {
text := fmt.Sprint(args...)
if !strings.HasSuffix(text, "\n") {
text += "\n"
}
fmt.Fprint(r.rl.Stderr(), text)
}
// Print shows a message to the user, colored in red for emphasis.
// It is printed over stderr as stdout is reserved for regular output.
func (r *readlineUI) PrintErr(args ...interface{}) {
text := fmt.Sprint(args...)
if !strings.HasSuffix(text, "\n") {
text += "\n"
}
fmt.Fprint(r.rl.Stderr(), colorize(text))
}
// colorize the msg using ANSI color escapes.
func colorize(msg string) string {
var red = 31
var colorEscape = fmt.Sprintf("\033[0;%dm", red)
var colorResetEscape = "\033[0m"
return colorEscape + msg + colorResetEscape
}
// IsTerminal returns whether the UI is known to be tied to an
// interactive terminal (as opposed to being redirected to a file).
func (r *readlineUI) IsTerminal() bool {
const stdout = 1
return readline.IsTerminal(stdout)
}
// Start a browser on interactive mode.
func (r *readlineUI) WantBrowser() bool {
return r.IsTerminal()
}
// SetAutoComplete instructs the UI to call complete(cmd) to obtain
// the auto-completion of cmd, if the UI supports auto-completion at all.
func (r *readlineUI) SetAutoComplete(complete func(string) string) {
// TODO: Implement auto-completion support.
}
......@@ -74,6 +74,71 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp)
return
}
// ShowFrom drops all stack frames above the highest matching frame and returns
// whether a match was found. If showFrom is nil it returns false and does not
// modify the profile.
//
// Example: consider a sample with frames [A, B, C, B], where A is the root.
// ShowFrom(nil) returns false and has frames [A, B, C, B].
// ShowFrom(A) returns true and has frames [A, B, C, B].
// ShowFrom(B) returns true and has frames [B, C, B].
// ShowFrom(C) returns true and has frames [C, B].
// ShowFrom(D) returns false and drops the sample because no frames remain.
func (p *Profile) ShowFrom(showFrom *regexp.Regexp) (matched bool) {
if showFrom == nil {
return false
}
// showFromLocs stores location IDs that matched ShowFrom.
showFromLocs := make(map[uint64]bool)
// Apply to locations.
for _, loc := range p.Location {
if filterShowFromLocation(loc, showFrom) {
showFromLocs[loc.ID] = true
matched = true
}
}
// For all samples, strip locations after the highest matching one.
s := make([]*Sample, 0, len(p.Sample))
for _, sample := range p.Sample {
for i := len(sample.Location) - 1; i >= 0; i-- {
if showFromLocs[sample.Location[i].ID] {
sample.Location = sample.Location[:i+1]
s = append(s, sample)
break
}
}
}
p.Sample = s
return matched
}
// filterShowFromLocation tests a showFrom regex against a location, removes
// lines after the last match and returns whether a match was found. If the
// mapping is matched, then all lines are kept.
func filterShowFromLocation(loc *Location, showFrom *regexp.Regexp) bool {
if m := loc.Mapping; m != nil && showFrom.MatchString(m.File) {
return true
}
if i := loc.lastMatchedLineIndex(showFrom); i >= 0 {
loc.Line = loc.Line[:i+1]
return true
}
return false
}
// lastMatchedLineIndex returns the index of the last line that matches a regex,
// or -1 if no match is found.
func (loc *Location) lastMatchedLineIndex(re *regexp.Regexp) int {
for i := len(loc.Line) - 1; i >= 0; i-- {
if fn := loc.Line[i].Function; fn != nil {
if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
return i
}
}
}
return -1
}
// FilterTagsByName filters the tags in a profile and only keeps
// tags that match show and not hide.
func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {
......@@ -142,6 +207,9 @@ func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line {
// matchedLines returns the lines in the location that match
// the regular expression.
func (loc *Location) matchedLines(re *regexp.Regexp) []Line {
if m := loc.Mapping; m != nil && re.MatchString(m.File) {
return loc.Line
}
var lines []Line
for _, ln := range loc.Line {
if fn := ln.Function; fn != nil {
......
This diff is collapsed.
......@@ -294,21 +294,12 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo {
}
// Check memoization tables.
bk, pk := src.key()
if src.BuildID != "" {
if m, ok := pm.mappings[bk]; ok {
mk := src.key()
if m, ok := pm.mappings[mk]; ok {
mi := mapInfo{m, int64(m.Start) - int64(src.Start)}
pm.mappingsByID[src.ID] = mi
return mi
}
}
if src.File != "" {
if m, ok := pm.mappings[pk]; ok {
mi := mapInfo{m, int64(m.Start) - int64(src.Start)}
pm.mappingsByID[src.ID] = mi
return mi
}
}
m := &Mapping{
ID: uint64(len(pm.p.Mapping) + 1),
Start: src.Start,
......@@ -324,21 +315,15 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo {
pm.p.Mapping = append(pm.p.Mapping, m)
// Update memoization tables.
if m.BuildID != "" {
pm.mappings[bk] = m
}
if m.File != "" {
pm.mappings[pk] = m
}
pm.mappings[mk] = m
mi := mapInfo{m, 0}
pm.mappingsByID[src.ID] = mi
return mi
}
// key generates encoded strings of Mapping to be used as a key for
// maps. The first key represents only the build id, while the second
// represents only the file path.
func (m *Mapping) key() (buildIDKey, pathKey mappingKey) {
// maps.
func (m *Mapping) key() mappingKey {
// Normalize addresses to handle address space randomization.
// Round up to next 4K boundary to avoid minor discrepancies.
const mapsizeRounding = 0x1000
......@@ -346,24 +331,27 @@ func (m *Mapping) key() (buildIDKey, pathKey mappingKey) {
size := m.Limit - m.Start
size = size + mapsizeRounding - 1
size = size - (size % mapsizeRounding)
buildIDKey = mappingKey{
size,
m.Offset,
m.BuildID,
key := mappingKey{
size: size,
offset: m.Offset,
}
pathKey = mappingKey{
size,
m.Offset,
m.File,
switch {
case m.BuildID != "":
key.buildIDOrFile = m.BuildID
case m.File != "":
key.buildIDOrFile = m.File
default:
// A mapping containing neither build ID nor file name is a fake mapping. A
// key with empty buildIDOrFile is used for fake mappings so that they are
// treated as the same mapping during merging.
}
return
return key
}
type mappingKey struct {
size, offset uint64
buildidIDOrFile string
buildIDOrFile string
}
func (pm *profileMerger) mapLine(src Line) Line {
......
// Copyright 2018 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 profile
import (
"testing"
)
func TestMapMapping(t *testing.T) {
pm := &profileMerger{
p: &Profile{},
mappings: make(map[mappingKey]*Mapping),
mappingsByID: make(map[uint64]mapInfo),
}
for _, tc := range []struct {
desc string
m1 Mapping
m2 Mapping
wantMerged bool
}{
{
desc: "same file name",
m1: Mapping{
ID: 1,
File: "test-file-1",
},
m2: Mapping{
ID: 2,
File: "test-file-1",
},
wantMerged: true,
},
{
desc: "same build ID",
m1: Mapping{
ID: 3,
BuildID: "test-build-id-1",
},
m2: Mapping{
ID: 4,
BuildID: "test-build-id-1",
},
wantMerged: true,
},
{
desc: "same fake mapping",
m1: Mapping{
ID: 5,
},
m2: Mapping{
ID: 6,
},
wantMerged: true,
},
{
desc: "different start",
m1: Mapping{
ID: 7,
Start: 0x1000,
Limit: 0x2000,
BuildID: "test-build-id-2",
},
m2: Mapping{
ID: 8,
Start: 0x3000,
Limit: 0x4000,
BuildID: "test-build-id-2",
},
wantMerged: true,
},
{
desc: "different file name",
m1: Mapping{
ID: 9,
File: "test-file-2",
},
m2: Mapping{
ID: 10,
File: "test-file-3",
},
},
{
desc: "different build id",
m1: Mapping{
ID: 11,
BuildID: "test-build-id-3",
},
m2: Mapping{
ID: 12,
BuildID: "test-build-id-4",
},
},
{
desc: "different size",
m1: Mapping{
ID: 13,
Start: 0x1000,
Limit: 0x3000,
BuildID: "test-build-id-5",
},
m2: Mapping{
ID: 14,
Start: 0x1000,
Limit: 0x5000,
BuildID: "test-build-id-5",
},
},
{
desc: "different offset",
m1: Mapping{
ID: 15,
Offset: 1,
BuildID: "test-build-id-6",
},
m2: Mapping{
ID: 16,
Offset: 2,
BuildID: "test-build-id-6",
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
info1 := pm.mapMapping(&tc.m1)
info2 := pm.mapMapping(&tc.m2)
gotM1, gotM2 := *info1.m, *info2.m
wantM1 := tc.m1
wantM1.ID = gotM1.ID
if gotM1 != wantM1 {
t.Errorf("first mapping got %v, want %v", gotM1, wantM1)
}
if tc.wantMerged {
if gotM1 != gotM2 {
t.Errorf("first mapping got %v, second mapping got %v, want equal", gotM1, gotM2)
}
if info1.offset != 0 {
t.Errorf("first mapping info got offset %d, want 0", info1.offset)
}
if wantOffset := int64(tc.m1.Start) - int64(tc.m2.Start); wantOffset != info2.offset {
t.Errorf("second mapping info got offset %d, want %d", info2.offset, wantOffset)
}
} else {
if gotM1.ID == gotM2.ID {
t.Errorf("first mapping got %v, second mapping got %v, want different IDs", gotM1, gotM2)
}
wantM2 := tc.m2
wantM2.ID = gotM2.ID
if gotM2 != wantM2 {
t.Errorf("second mapping got %v, want %v", gotM2, wantM2)
}
}
})
}
}
......@@ -20,7 +20,6 @@ import (
"io/ioutil"
"path/filepath"
"reflect"
"regexp"
"strings"
"sync"
"testing"
......@@ -902,101 +901,6 @@ func TestNormalizeIncompatibleProfiles(t *testing.T) {
}
}
func TestFilter(t *testing.T) {
// Perform several forms of filtering on the test profile.
type filterTestcase struct {
focus, ignore, hide, show *regexp.Regexp
fm, im, hm, hnm bool
}
for tx, tc := range []filterTestcase{
{
fm: true, // nil focus matches every sample
},
{
focus: regexp.MustCompile("notfound"),
},
{
ignore: regexp.MustCompile("foo.c"),
fm: true,
im: true,
},
{
hide: regexp.MustCompile("lib.so"),
fm: true,
hm: true,
},
{
show: regexp.MustCompile("foo.c"),
fm: true,
hnm: true,
},
{
show: regexp.MustCompile("notfound"),
fm: true,
},
} {
prof := *testProfile1.Copy()
gf, gi, gh, gnh := prof.FilterSamplesByName(tc.focus, tc.ignore, tc.hide, tc.show)
if gf != tc.fm {
t.Errorf("Filter #%d, got fm=%v, want %v", tx, gf, tc.fm)
}
if gi != tc.im {
t.Errorf("Filter #%d, got im=%v, want %v", tx, gi, tc.im)
}
if gh != tc.hm {
t.Errorf("Filter #%d, got hm=%v, want %v", tx, gh, tc.hm)
}
if gnh != tc.hnm {
t.Errorf("Filter #%d, got hnm=%v, want %v", tx, gnh, tc.hnm)
}
}
}
func TestTagFilter(t *testing.T) {
// Perform several forms of tag filtering on the test profile.
type filterTestcase struct {
include, exclude *regexp.Regexp
im, em bool
count int
}
countTags := func(p *Profile) map[string]bool {
tags := make(map[string]bool)
for _, s := range p.Sample {
for l := range s.Label {
tags[l] = true
}
for l := range s.NumLabel {
tags[l] = true
}
}
return tags
}
for tx, tc := range []filterTestcase{
{nil, nil, true, false, 3},
{regexp.MustCompile("notfound"), nil, false, false, 0},
{regexp.MustCompile("key1"), nil, true, false, 1},
{nil, regexp.MustCompile("key[12]"), true, true, 1},
} {
prof := testProfile1.Copy()
gim, gem := prof.FilterTagsByName(tc.include, tc.exclude)
if gim != tc.im {
t.Errorf("Filter #%d, got include match=%v, want %v", tx, gim, tc.im)
}
if gem != tc.em {
t.Errorf("Filter #%d, got exclude match=%v, want %v", tx, gem, tc.em)
}
if tags := countTags(prof); len(tags) != tc.count {
t.Errorf("Filter #%d, got %d tags[%v], want %d", tx, len(tags), tags, tc.count)
}
}
}
// locationHash constructs a string to use as a hashkey for a sample, based on its locations
func locationHash(s *Sample) string {
var tb string
......
The MIT License (MIT)
Copyright (c) 2013 Justin Palmer
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
// Tooltips for d3.js visualizations
// https://github.com/Caged/d3-tip
// Version 0.7.1
// See LICENSE file for license details
package d3tip
// JSSource returns the d3-tip.js file
const JSSource = `
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module with d3 as a dependency.
define(['d3'], factory)
} else if (typeof module === 'object' && module.exports) {
// CommonJS
var d3 = require('d3')
module.exports = factory(d3)
} else {
// Browser global.
root.d3.tip = factory(root.d3)
}
}(this, function (d3) {
// Public - contructs a new tooltip
//
// Returns a tip
return function() {
var direction = d3_tip_direction,
offset = d3_tip_offset,
html = d3_tip_html,
node = initNode(),
svg = null,
point = null,
target = null
function tip(vis) {
svg = getSVGNode(vis)
point = svg.createSVGPoint()
document.body.appendChild(node)
}
// Public - show the tooltip on the screen
//
// Returns a tip
tip.show = function() {
var args = Array.prototype.slice.call(arguments)
if(args[args.length - 1] instanceof SVGElement) target = args.pop()
var content = html.apply(this, args),
poffset = offset.apply(this, args),
dir = direction.apply(this, args),
nodel = getNodeEl(),
i = directions.length,
coords,
scrollTop = document.documentElement.scrollTop || document.body.scrollTop,
scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft
nodel.html(content)
.style('opacity', 1).style('pointer-events', 'all')
while(i--) nodel.classed(directions[i], false)
coords = direction_callbacks.get(dir).apply(this)
nodel.classed(dir, true)
.style('top', (coords.top + poffset[0]) + scrollTop + 'px')
.style('left', (coords.left + poffset[1]) + scrollLeft + 'px')
return tip;
};
// Public - hide the tooltip
//
// Returns a tip
tip.hide = function() {
var nodel = getNodeEl()
nodel.style('opacity', 0).style('pointer-events', 'none')
return tip
}
// Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value.
//
// n - name of the attribute
// v - value of the attribute
//
// Returns tip or attribute value
tip.attr = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().attr(n)
} else {
var args = Array.prototype.slice.call(arguments)
d3.selection.prototype.attr.apply(getNodeEl(), args)
}
return tip
}
// Public: Proxy style calls to the d3 tip container. Sets or gets a style value.
//
// n - name of the property
// v - value of the property
//
// Returns tip or style property value
tip.style = function(n, v) {
if (arguments.length < 2 && typeof n === 'string') {
return getNodeEl().style(n)
} else {
var args = Array.prototype.slice.call(arguments)
d3.selection.prototype.style.apply(getNodeEl(), args)
}
return tip
}
// Public: Set or get the direction of the tooltip
//
// v - One of n(north), s(south), e(east), or w(west), nw(northwest),
// sw(southwest), ne(northeast) or se(southeast)
//
// Returns tip or direction
tip.direction = function(v) {
if (!arguments.length) return direction
direction = v == null ? v : functor(v)
return tip
}
// Public: Sets or gets the offset of the tip
//
// v - Array of [x, y] offset
//
// Returns offset or
tip.offset = function(v) {
if (!arguments.length) return offset
offset = v == null ? v : functor(v)
return tip
}
// Public: sets or gets the html value of the tooltip
//
// v - String value of the tip
//
// Returns html value or tip
tip.html = function(v) {
if (!arguments.length) return html
html = v == null ? v : functor(v)
return tip
}
// Public: destroys the tooltip and removes it from the DOM
//
// Returns a tip
tip.destroy = function() {
if(node) {
getNodeEl().remove();
node = null;
}
return tip;
}
function d3_tip_direction() { return 'n' }
function d3_tip_offset() { return [0, 0] }
function d3_tip_html() { return ' ' }
var direction_callbacks = d3.map({
n: direction_n,
s: direction_s,
e: direction_e,
w: direction_w,
nw: direction_nw,
ne: direction_ne,
sw: direction_sw,
se: direction_se
}),
directions = direction_callbacks.keys()
function direction_n() {
var bbox = getScreenBBox()
return {
top: bbox.n.y - node.offsetHeight,
left: bbox.n.x - node.offsetWidth / 2
}
}
function direction_s() {
var bbox = getScreenBBox()
return {
top: bbox.s.y,
left: bbox.s.x - node.offsetWidth / 2
}
}
function direction_e() {
var bbox = getScreenBBox()
return {
top: bbox.e.y - node.offsetHeight / 2,
left: bbox.e.x
}
}
function direction_w() {
var bbox = getScreenBBox()
return {
top: bbox.w.y - node.offsetHeight / 2,
left: bbox.w.x - node.offsetWidth
}
}
function direction_nw() {
var bbox = getScreenBBox()
return {
top: bbox.nw.y - node.offsetHeight,
left: bbox.nw.x - node.offsetWidth
}
}
function direction_ne() {
var bbox = getScreenBBox()
return {
top: bbox.ne.y - node.offsetHeight,
left: bbox.ne.x
}
}
function direction_sw() {
var bbox = getScreenBBox()
return {
top: bbox.sw.y,
left: bbox.sw.x - node.offsetWidth
}
}
function direction_se() {
var bbox = getScreenBBox()
return {
top: bbox.se.y,
left: bbox.e.x
}
}
function initNode() {
var node = d3.select(document.createElement('div'));
node.style('position', 'absolute').style('top', 0).style('opacity', 0)
.style('pointer-events', 'none').style('box-sizing', 'border-box')
return node.node()
}
function getSVGNode(el) {
el = el.node()
if(el.tagName.toLowerCase() === 'svg')
return el
return el.ownerSVGElement
}
function getNodeEl() {
if(node === null) {
node = initNode();
// re-add node to DOM
document.body.appendChild(node);
};
return d3.select(node);
}
// Private - gets the screen coordinates of a shape
//
// Given a shape on the screen, will return an SVGPoint for the directions
// n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest),
// sw(southwest).
//
// +-+-+
// | |
// + +
// | |
// +-+-+
//
// Returns an Object {n, s, e, w, nw, sw, ne, se}
function getScreenBBox() {
var targetel = target || d3.event.target;
while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) {
targetel = targetel.parentNode;
}
var bbox = {},
matrix = targetel.getScreenCTM(),
tbbox = targetel.getBBox(),
width = tbbox.width,
height = tbbox.height,
x = tbbox.x,
y = tbbox.y
point.x = x
point.y = y
bbox.nw = point.matrixTransform(matrix)
point.x += width
bbox.ne = point.matrixTransform(matrix)
point.y += height
bbox.se = point.matrixTransform(matrix)
point.x -= width
bbox.sw = point.matrixTransform(matrix)
point.y -= height / 2
bbox.w = point.matrixTransform(matrix)
point.x += width
bbox.e = point.matrixTransform(matrix)
point.x -= width / 2
point.y -= height / 2
bbox.n = point.matrixTransform(matrix)
point.y += height
bbox.s = point.matrixTransform(matrix)
return bbox
}
// Private - replace D3JS 3.X d3.functor() function
function functor(v) {
return typeof v === "function" ? v : function() {
return v
}
}
return tip
};
}));
`
#!/usr/bin/env bash
set -eu
set -o pipefail
D3FLAMEGRAPH_REPO="https://raw.githubusercontent.com/spiermar/d3-flame-graph"
D3FLAMEGRAPH_VERSION="2.0.0-alpha4"
D3FLAMEGRAPH_JS="d3-flamegraph.js"
D3FLAMEGRAPH_CSS="d3-flamegraph.css"
cd $(dirname $0)
D3FLAMEGRAPH_DIR=d3flamegraph
generate_d3flamegraph_go() {
local d3_js=$(curl -s "${D3FLAMEGRAPH_REPO}/${D3FLAMEGRAPH_VERSION}/dist/${D3FLAMEGRAPH_JS}" | sed 's/`/`+"`"+`/g')
local d3_css=$(curl -s "${D3FLAMEGRAPH_REPO}/${D3FLAMEGRAPH_VERSION}/dist/${D3FLAMEGRAPH_CSS}")
cat <<-EOF > $D3FLAMEGRAPH_DIR/d3_flame_graph.go
// A D3.js plugin that produces flame graphs from hierarchical data.
// https://github.com/spiermar/d3-flame-graph
// Version $D3FLAMEGRAPH_VERSION
// See LICENSE file for license details
package d3flamegraph
// JSSource returns the $D3FLAMEGRAPH_JS file
const JSSource = \`
$d3_js
\`
// CSSSource returns the $D3FLAMEGRAPH_CSS file
const CSSSource = \`
$d3_css
\`
EOF
gofmt -w $D3FLAMEGRAPH_DIR/d3_flame_graph.go
}
get_license() {
curl -s -o $D3FLAMEGRAPH_DIR/LICENSE "${D3FLAMEGRAPH_REPO}/${D3FLAMEGRAPH_VERSION}/LICENSE"
}
mkdir -p $D3FLAMEGRAPH_DIR
get_license
generate_d3flamegraph_go
......@@ -9,8 +9,8 @@
{
"canonical": "github.com/google/pprof",
"local": "github.com/google/pprof",
"revision": "9e20b5b106e946f4cd1df94c1f6fe3f88456628d",
"revisionTime": "2017-11-08T17:47:23Z"
"revision": "520140b6bf47519c766e8380e5f094576347b016",
"revisionTime": "2018-05-08T15:00:43Z"
},
{
"canonical": "golang.org/x/arch/x86/x86asm",
......
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