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 ...@@ -102,8 +102,8 @@ pprof can read `perf.data` files generated by the
## Further documentation ## 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: ...@@ -94,6 +94,8 @@ Some common pprof options are:
*regex*. *regex*.
* **-ignore= _regex_:** Do not include samples that include a report entry matching * **-ignore= _regex_:** Do not include samples that include a report entry matching
*regex*. *regex*.
* **-show\_from= _regex_:** Do not show entries above the first one that
matches *regex*.
* **-show= _regex_:** Only show entries that match *regex*. * **-show= _regex_:** Only show entries that match *regex*.
* **-hide= _regex_:** Do not 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" + ...@@ -316,5 +316,5 @@ var usageMsgVars = "\n\n" +
" PPROF_TOOLS Search path for object-level tools\n" + " PPROF_TOOLS Search path for object-level tools\n" +
" PPROF_BINARY_PATH Search path for local binary files\n" + " PPROF_BINARY_PATH Search path for local binary files\n" +
" default: $HOME/pprof/binaries\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" " * On Windows, %USERPROFILE% is used instead of $HOME"
...@@ -135,14 +135,6 @@ var pprofVariables = variables{ ...@@ -135,14 +135,6 @@ var pprofVariables = variables{
"Ignore negative differences", "Ignore negative differences",
"Do not show any locations with values <0.")}, "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. // Graph handling options.
"call_tree": &variable{boolKind, "f", "", helpText( "call_tree": &variable{boolKind, "f", "", helpText(
"Create a context-sensitive call tree", "Create a context-sensitive call tree",
...@@ -161,6 +153,7 @@ var pprofVariables = variables{ ...@@ -161,6 +153,7 @@ var pprofVariables = variables{
"Using auto will scale each value independently to the most natural unit.")}, "Using auto will scale each value independently to the most natural unit.")},
"compact_labels": &variable{boolKind, "f", "", "Show minimal headers"}, "compact_labels": &variable{boolKind, "f", "", "Show minimal headers"},
"source_path": &variable{stringKind, "", "", "Search path for source files"}, "source_path": &variable{stringKind, "", "", "Search path for source files"},
"trim_path": &variable{stringKind, "", "", "Path to trim from source paths before search"},
// Filtering options // Filtering options
"nodecount": &variable{intKind, "-1", "", helpText( "nodecount": &variable{intKind, "-1", "", helpText(
...@@ -193,6 +186,10 @@ var pprofVariables = variables{ ...@@ -193,6 +186,10 @@ var pprofVariables = variables{
"Only show nodes matching regexp", "Only show nodes matching regexp",
"If set, only show nodes that match this location.", "If set, only show nodes that match this location.",
"Matching includes the function name, filename or object name.")}, "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( "tagfocus": &variable{stringKind, "", "", helpText(
"Restricts to samples with tags in range or matched by regexp", "Restricts to samples with tags in range or matched by regexp",
"Use name=value syntax to limit the matching to a specific tag.", "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. ...@@ -150,11 +150,11 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
} }
func applyCommandOverrides(cmd []string, v variables) variables { 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] { switch cmd[0] {
case "proto", "raw": case "proto", "raw":
trim, focus, tagfocus, hide = false, false, false, false trim, tagfilter, filter = false, false, false
v.set("addresses", "t") v.set("addresses", "t")
case "callgrind", "kcachegrind": case "callgrind", "kcachegrind":
trim = false trim = false
...@@ -163,7 +163,7 @@ func applyCommandOverrides(cmd []string, v variables) variables { ...@@ -163,7 +163,7 @@ func applyCommandOverrides(cmd []string, v variables) variables {
trim = false trim = false
v.set("addressnoinlines", "t") v.set("addressnoinlines", "t")
case "peek": case "peek":
trim, focus, hide = false, false, false trim, filter = false, false
case "list": case "list":
v.set("nodecount", "0") v.set("nodecount", "0")
v.set("lines", "t") v.set("lines", "t")
...@@ -181,17 +181,16 @@ func applyCommandOverrides(cmd []string, v variables) variables { ...@@ -181,17 +181,16 @@ func applyCommandOverrides(cmd []string, v variables) variables {
v.set("nodefraction", "0") v.set("nodefraction", "0")
v.set("edgefraction", "0") v.set("edgefraction", "0")
} }
if !focus { if !tagfilter {
v.set("focus", "")
v.set("ignore", "")
}
if !tagfocus {
v.set("tagfocus", "") v.set("tagfocus", "")
v.set("tagignore", "") v.set("tagignore", "")
} }
if !hide { if !filter {
v.set("focus", "")
v.set("ignore", "")
v.set("hide", "") v.set("hide", "")
v.set("show", "") v.set("show", "")
v.set("show_from", "")
} }
return v return v
} }
...@@ -242,7 +241,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var ...@@ -242,7 +241,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
} }
var filters []string 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 v := vars[k].value
if v != "" { if v != "" {
filters = append(filters, k+"="+v) filters = append(filters, k+"="+v)
...@@ -253,7 +252,6 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var ...@@ -253,7 +252,6 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
CumSort: vars["cum"].boolValue(), CumSort: vars["cum"].boolValue(),
CallTree: vars["call_tree"].boolValue(), CallTree: vars["call_tree"].boolValue(),
DropNegative: vars["drop_negative"].boolValue(), DropNegative: vars["drop_negative"].boolValue(),
PositivePercentages: vars["positive_percentages"].boolValue(),
CompactLabels: vars["compact_labels"].boolValue(), CompactLabels: vars["compact_labels"].boolValue(),
Ratio: 1 / vars["divide_by"].floatValue(), Ratio: 1 / vars["divide_by"].floatValue(),
...@@ -273,6 +271,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var ...@@ -273,6 +271,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
OutputUnit: vars["unit"].value, OutputUnit: vars["unit"].value,
SourcePath: vars["source_path"].stringValue(), SourcePath: vars["source_path"].stringValue(),
TrimPath: vars["trim_path"].stringValue(),
} }
if len(p.Mapping) > 0 && p.Mapping[0].File != "" { if len(p.Mapping) > 0 && p.Mapping[0].File != "" {
......
...@@ -33,6 +33,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab ...@@ -33,6 +33,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
ignore, err := compileRegexOption("ignore", v["ignore"].value, err) ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
hide, err := compileRegexOption("hide", v["hide"].value, err) hide, err := compileRegexOption("hide", v["hide"].value, err)
show, err := compileRegexOption("show", v["show"].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) tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err) tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, 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 ...@@ -46,6 +47,9 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
warnNoMatches(hide == nil || hm, "Hide", ui) warnNoMatches(hide == nil || hm, "Hide", ui)
warnNoMatches(show == nil || hnm, "Show", ui) warnNoMatches(show == nil || hnm, "Show", ui)
sfm := prof.ShowFrom(showfrom)
warnNoMatches(showfrom == nil || sfm, "ShowFrom", ui)
tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore) tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore)
warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui) warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
warnNoMatches(tagignore == nil || tim, "TagIgnore", ui) warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
......
...@@ -65,6 +65,7 @@ func TestParse(t *testing.T) { ...@@ -65,6 +65,7 @@ func TestParse(t *testing.T) {
{"topproto,lines,cum,hide=mangled[X3]0", "cpu"}, {"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
{"tree,lines,cum,focus=[24]00", "heap"}, {"tree,lines,cum,focus=[24]00", "heap"},
{"tree,relative_percentages,cum,focus=[24]00", "heap"}, {"tree,relative_percentages,cum,focus=[24]00", "heap"},
{"tree,lines,cum,show_from=line2", "cpu"},
{"callgrind", "cpu"}, {"callgrind", "cpu"},
{"callgrind,call_tree", "cpu"}, {"callgrind,call_tree", "cpu"},
{"callgrind", "heap"}, {"callgrind", "heap"},
...@@ -261,6 +262,9 @@ func solutionFilename(source string, f *testFlags) string { ...@@ -261,6 +262,9 @@ func solutionFilename(source string, f *testFlags) string {
if f.strings["ignore"] != "" || f.strings["tagignore"] != "" { if f.strings["ignore"] != "" || f.strings["tagignore"] != "" {
name = append(name, "ignore") name = append(name, "ignore")
} }
if f.strings["show_from"] != "" {
name = append(name, "show_from")
}
name = addString(name, f, []string{"hide", "show"}) name = addString(name, f, []string{"hide", "show"})
if f.strings["unit"] != "minimum" { if f.strings["unit"] != "minimum" {
name = addString(name, f, []string{"unit"}) name = addString(name, f, []string{"unit"})
......
...@@ -599,9 +599,9 @@ var httpGet = func(source string, timeout time.Duration) (*http.Response, error) ...@@ -599,9 +599,9 @@ var httpGet = func(source string, timeout time.Duration) (*http.Response, error)
client := &http.Client{ client := &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
ResponseHeaderTimeout: timeout + 5*time.Second,
Proxy: http.ProxyFromEnvironment, Proxy: http.ProxyFromEnvironment,
TLSClientConfig: tlsConfig, TLSClientConfig: tlsConfig,
ResponseHeaderTimeout: timeout + 5*time.Second,
}, },
} }
return client.Get(source) return client.Get(source)
......
...@@ -27,6 +27,7 @@ import ( ...@@ -27,6 +27,7 @@ import (
type treeNode struct { type treeNode struct {
Name string `json:"n"` Name string `json:"n"`
FullName string `json:"f"`
Cum int64 `json:"v"` Cum int64 `json:"v"`
CumFormat string `json:"l"` CumFormat string `json:"l"`
Percent string `json:"p"` Percent string `json:"p"`
...@@ -52,8 +53,10 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) { ...@@ -52,8 +53,10 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
// Make all nodes and the map, collect the roots. // Make all nodes and the map, collect the roots.
for _, n := range g.Nodes { for _, n := range g.Nodes {
v := n.CumValue() v := n.CumValue()
fullName := n.Info.PrintableName()
node := &treeNode{ node := &treeNode{
Name: n.Info.PrintableName(), Name: getNodeShortName(fullName),
FullName: fullName,
Cum: v, Cum: v,
CumFormat: config.FormatValue(v), CumFormat: config.FormatValue(v),
Percent: strings.TrimSpace(measurement.Percentage(v, config.Total)), Percent: strings.TrimSpace(measurement.Percentage(v, config.Total)),
...@@ -78,6 +81,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) { ...@@ -78,6 +81,7 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
rootNode := &treeNode{ rootNode := &treeNode{
Name: "root", Name: "root",
FullName: "root",
Cum: rootValue, Cum: rootValue,
CumFormat: config.FormatValue(rootValue), CumFormat: config.FormatValue(rootValue),
Percent: strings.TrimSpace(measurement.Percentage(rootValue, config.Total)), Percent: strings.TrimSpace(measurement.Percentage(rootValue, config.Total)),
...@@ -97,3 +101,19 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) { ...@@ -97,3 +101,19 @@ func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
Nodes: nodeArr, 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 ...@@ -17,13 +17,11 @@ package driver
import "html/template" import "html/template"
import "github.com/google/pprof/third_party/d3" import "github.com/google/pprof/third_party/d3"
import "github.com/google/pprof/third_party/d3tip"
import "github.com/google/pprof/third_party/d3flamegraph" import "github.com/google/pprof/third_party/d3flamegraph"
// addTemplates adds a set of template definitions to templates. // addTemplates adds a set of template definitions to templates.
func addTemplates(templates *template.Template) { func addTemplates(templates *template.Template) {
template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`)) 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 "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`))
template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`)) template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`))
template.Must(templates.Parse(` template.Must(templates.Parse(`
...@@ -1031,49 +1029,51 @@ function viewer(baseUrl, nodes) { ...@@ -1031,49 +1029,51 @@ function viewer(baseUrl, nodes) {
width: 90%; width: 90%;
min-width: 90%; min-width: 90%;
margin-left: 5%; margin-left: 5%;
padding-bottom: 41px; padding: 15px 0 35px;
} }
</style> </style>
</head> </head>
<body> <body>
{{template "header" .}} {{template "header" .}}
<div id="bodycontainer"> <div id="bodycontainer">
<div id="flamegraphdetails" class="flamegraph-details"></div>
<div class="flamegraph-content"> <div class="flamegraph-content">
<div id="chart"></div> <div id="chart"></div>
</div> </div>
<div id="flamegraphdetails" class="flamegraph-details"></div>
</div> </div>
{{template "script" .}} {{template "script" .}}
<script>viewer(new URL(window.location.href), {{.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 "d3flamegraphscript" .}}</script> <script>{{template "d3flamegraphscript" .}}</script>
<script type="text/javascript"> <script>
var data = {{.FlameGraph}}; var data = {{.FlameGraph}};
var label = function(d) {
return d.data.n + ' (' + d.data.p + ', ' + d.data.l + ')';
};
var width = document.getElementById('chart').clientWidth; var width = document.getElementById('chart').clientWidth;
var flameGraph = d3.flameGraph() var flameGraph = d3.flamegraph()
.width(width) .width(width)
.cellHeight(18) .cellHeight(18)
.minFrameSize(1) .minFrameSize(1)
.transitionDuration(750) .transitionDuration(750)
.transitionEase(d3.easeCubic) .transitionEase(d3.easeCubic)
.sort(true) .inverted(true)
.title('') .title('')
.label(label) .tooltip(false)
.details(document.getElementById('flamegraphdetails')); .details(document.getElementById('flamegraphdetails'));
var tip = d3.tip() // <full name> (percentage, value)
.direction('s') flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')');
.offset([8, 0])
.attr('class', 'd3-flame-graph-tip') (function(flameGraph) {
.html(function(d) { return 'name: ' + d.data.n + ', value: ' + d.data.l; }); 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') d3.select('#chart')
.datum(data) .datum(data)
......
...@@ -33,7 +33,7 @@ import ( ...@@ -33,7 +33,7 @@ import (
) )
func TestWebInterface(t *testing.T) { func TestWebInterface(t *testing.T) {
if runtime.GOOS == "nacl" { if runtime.GOOS == "nacl" || runtime.GOOS == "js" {
t.Skip("test assumes tcp available") t.Skip("test assumes tcp available")
} }
...@@ -81,7 +81,7 @@ func TestWebInterface(t *testing.T) { ...@@ -81,7 +81,7 @@ func TestWebInterface(t *testing.T) {
[]string{"300ms.*F1", "200ms.*300ms.*F2"}, false}, []string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
{"/disasm?f=" + url.QueryEscape("F[12]"), {"/disasm?f=" + url.QueryEscape("F[12]"),
[]string{"f1:asm", "f2:asm"}, false}, []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 { for _, c := range testcases {
if c.needDot && !haveDot { if c.needDot && !haveDot {
......
...@@ -208,13 +208,13 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6 ...@@ -208,13 +208,13 @@ func GetBase(fh *elf.FileHeader, loadSegment *elf.ProgHeader, stextOffset *uint6
if loadSegment.Vaddr == start-offset { if loadSegment.Vaddr == start-offset {
return offset, nil 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: // Some kernels look like:
// VADDR=0xffffffff80200000 // VADDR=0xffffffff80200000
// stextOffset=0xffffffff80200198 // stextOffset=0xffffffff80200198
// Start=0xffffffff83200000 // Start=0xffffffff83200000
// Limit=0xffffffff84200000 // Limit=0xffffffff84200000
// Offset=0 (0xc000000000000000 for PowerPC64) // Offset=0 (0xc000000000000000 for PowerPC64) (== Start for ASLR kernel)
// 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
......
...@@ -55,7 +55,9 @@ func TestGetBase(t *testing.T) { ...@@ -55,7 +55,9 @@ func TestGetBase(t *testing.T) {
{"exec offset 2", fhExec, lsOffset, nil, 0x200000, 0x600000, 0, 0, false}, {"exec offset 2", fhExec, lsOffset, nil, 0x200000, 0x600000, 0, 0, false},
{"exec nomap", fhExec, nil, nil, 0, 0, 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 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", 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 2", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0, 0x10198, 0, 0x7efffe68, false},
{"exec chromeos kernel 3", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0x198, 0x100000, 0, 0x7f000000, false}, {"exec chromeos kernel 3", fhExec, kernelHeader, uint64p(0xffffffff81000198), 0x198, 0x100000, 0, 0x7f000000, false},
......
...@@ -58,7 +58,6 @@ type Options struct { ...@@ -58,7 +58,6 @@ type Options struct {
CumSort bool CumSort bool
CallTree bool CallTree bool
DropNegative bool DropNegative bool
PositivePercentages bool
CompactLabels bool CompactLabels bool
Ratio float64 Ratio float64
Title string Title string
...@@ -79,6 +78,7 @@ type Options struct { ...@@ -79,6 +78,7 @@ type Options struct {
Symbol *regexp.Regexp // Symbols to include on disassembly report. Symbol *regexp.Regexp // Symbols to include on disassembly report.
SourcePath string // Search path for source files. 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. // Generate generates a report as directed by the Report.
...@@ -239,7 +239,7 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph { ...@@ -239,7 +239,7 @@ func (rpt *Report) newGraph(nodes graph.NodeSet) *graph.Graph {
// Clean up file paths using heuristics. // Clean up file paths using heuristics.
prof := rpt.prof prof := rpt.prof
for _, f := range prof.Function { 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 // Removes all numeric tags except for the bytes tag prior
// to making graph. // to making graph.
...@@ -1192,7 +1192,7 @@ func New(prof *profile.Profile, o *Options) *Report { ...@@ -1192,7 +1192,7 @@ func New(prof *profile.Profile, o *Options) *Report {
} }
return measurement.ScaledLabel(v, o.SampleUnit, o.OutputUnit) 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} o, format}
} }
...@@ -1213,11 +1213,8 @@ func NewDefault(prof *profile.Profile, options Options) *Report { ...@@ -1213,11 +1213,8 @@ func NewDefault(prof *profile.Profile, options Options) *Report {
} }
// computeTotal computes the sum of all sample values. This will be // computeTotal computes the sum of all sample values. This will be
// used to compute percentages. If includeNegative is set, use use // used to compute percentages.
// absolute values to provide a meaningful percentage for both func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64) int64 {
// 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 {
var div, ret int64 var div, ret int64
for _, sample := range prof.Sample { for _, sample := range prof.Sample {
var d, v int64 var d, v int64
...@@ -1225,13 +1222,11 @@ func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64, i ...@@ -1225,13 +1222,11 @@ func computeTotal(prof *profile.Profile, value, meanDiv func(v []int64) int64, i
if meanDiv != nil { if meanDiv != nil {
d = meanDiv(sample.Value) d = meanDiv(sample.Value)
} }
if v >= 0 { if v < 0 {
v = -v
}
ret += v ret += v
div += d div += d
} else if includeNegative {
ret -= v
div += d
}
} }
if div != 0 { if div != 0 {
return ret / div return ret / div
......
...@@ -46,6 +46,7 @@ func TestSource(t *testing.T) { ...@@ -46,6 +46,7 @@ func TestSource(t *testing.T) {
&Options{ &Options{
OutputFormat: List, OutputFormat: List,
Symbol: regexp.MustCompile(`.`), Symbol: regexp.MustCompile(`.`),
TrimPath: "/some/path",
SampleValue: sampleValue1, SampleValue: sampleValue1,
SampleUnit: testProfile.SampleType[1].Unit, SampleUnit: testProfile.SampleType[1].Unit,
...@@ -60,6 +61,7 @@ func TestSource(t *testing.T) { ...@@ -60,6 +61,7 @@ func TestSource(t *testing.T) {
OutputFormat: Dot, OutputFormat: Dot,
CallTree: true, CallTree: true,
Symbol: regexp.MustCompile(`.`), Symbol: regexp.MustCompile(`.`),
TrimPath: "/some/path",
SampleValue: sampleValue1, SampleValue: sampleValue1,
SampleUnit: testProfile.SampleType[1].Unit, SampleUnit: testProfile.SampleType[1].Unit,
...@@ -119,7 +121,7 @@ var testF = []*profile.Function{ ...@@ -119,7 +121,7 @@ var testF = []*profile.Function{
{ {
ID: 4, ID: 4,
Name: "tee", Name: "tee",
Filename: "testdata/source2", Filename: "/some/path/testdata/source2",
}, },
} }
......
...@@ -63,7 +63,7 @@ func printSource(w io.Writer, rpt *Report) error { ...@@ -63,7 +63,7 @@ func printSource(w io.Writer, rpt *Report) error {
} }
sourcePath = wd sourcePath = wd
} }
reader := newSourceReader(sourcePath) reader := newSourceReader(sourcePath, o.TrimPath)
fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total))
for _, fn := range functions { for _, fn := range functions {
...@@ -146,7 +146,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er ...@@ -146,7 +146,7 @@ func PrintWebList(w io.Writer, rpt *Report, obj plugin.ObjTool, maxFiles int) er
} }
sourcePath = wd sourcePath = wd
} }
reader := newSourceReader(sourcePath) reader := newSourceReader(sourcePath, o.TrimPath)
type fileFunction struct { type fileFunction struct {
fileName, functionName string fileName, functionName string
...@@ -391,8 +391,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns ...@@ -391,8 +391,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns
continue continue
} }
curCalls = nil curCalls = nil
fname := trimPath(c.file) fline, ok := reader.line(c.file, c.line)
fline, ok := reader.line(fname, c.line)
if !ok { if !ok {
fline = "" fline = ""
} }
...@@ -400,7 +399,7 @@ func printFunctionSourceLine(w io.Writer, fn *graph.Node, assembly []assemblyIns ...@@ -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", 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(fmt.Sprintf("%-80s", text)),
template.HTMLEscapeString(filepath.Base(fname)), c.line) template.HTMLEscapeString(filepath.Base(c.file)), c.line)
} }
curCalls = an.inlineCalls curCalls = an.inlineCalls
text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction text := strings.Repeat(" ", srcIndent+4+4*len(curCalls)) + an.instruction
...@@ -426,7 +425,6 @@ func printPageClosing(w io.Writer) { ...@@ -426,7 +425,6 @@ func printPageClosing(w io.Writer) {
// file and annotates it with the samples in fns. Returns the sources // file and annotates it with the samples in fns. Returns the sources
// as nodes, using the info.name field to hold the source code. // 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) { 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) lineNodes := make(map[int]graph.Nodes)
// Collect source coordinates from profile. // Collect source coordinates from profile.
...@@ -516,8 +514,13 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction ...@@ -516,8 +514,13 @@ func getMissingFunctionSource(filename string, asm map[int][]assemblyInstruction
// sourceReader provides access to source code with caching of file contents. // sourceReader provides access to source code with caching of file contents.
type sourceReader struct { type sourceReader struct {
// searchPath is a filepath.ListSeparator-separated list of directories where
// source files should be searched.
searchPath string 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 maps from path name to a list of lines.
// files[*][0] is unused since line numbering starts at 1. // files[*][0] is unused since line numbering starts at 1.
files map[string][]string files map[string][]string
...@@ -527,9 +530,10 @@ type sourceReader struct { ...@@ -527,9 +530,10 @@ type sourceReader struct {
errors map[string]error errors map[string]error
} }
func newSourceReader(searchPath string) *sourceReader { func newSourceReader(searchPath, trimPath string) *sourceReader {
return &sourceReader{ return &sourceReader{
searchPath, searchPath,
trimPath,
make(map[string][]string), make(map[string][]string),
make(map[string]error), make(map[string]error),
} }
...@@ -544,7 +548,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { ...@@ -544,7 +548,7 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
if !ok { if !ok {
// Read and cache file contents. // Read and cache file contents.
lines = []string{""} // Skip 0th line lines = []string{""} // Skip 0th line
f, err := openSourceFile(path, reader.searchPath) f, err := openSourceFile(path, reader.searchPath, reader.trimPath)
if err != nil { if err != nil {
reader.errors[path] = err reader.errors[path] = err
} else { } else {
...@@ -565,17 +569,20 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) { ...@@ -565,17 +569,20 @@ func (reader *sourceReader) line(path string, lineno int) (string, bool) {
return lines[lineno], true return lines[lineno], true
} }
// openSourceFile opens a source file from a name encoded in a // openSourceFile opens a source file from a name encoded in a profile. File
// profile. File names in a profile after often relative paths, so // names in a profile after can be relative paths, so search them in each of
// search them in each of the paths in searchPath (or CWD by default), // the paths in searchPath and their parents. In case the profile contains
// and their parents. // absolute paths, additional paths may be configured to trim from the source
func openSourceFile(path, searchPath string) (*os.File, error) { // 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) { if filepath.IsAbs(path) {
f, err := os.Open(path) f, err := os.Open(path)
return f, err return f, err
} }
// Scan each component of the path.
// Scan each component of the path
for _, dir := range filepath.SplitList(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 {
...@@ -595,18 +602,34 @@ func openSourceFile(path, searchPath string) (*os.File, error) { ...@@ -595,18 +602,34 @@ func openSourceFile(path, searchPath string) (*os.File, error) {
} }
// trimPath cleans up a path by removing prefixes that are commonly // trimPath cleans up a path by removing prefixes that are commonly
// found on profiles. // found on profiles plus configured prefixes.
func trimPath(path string) string { // TODO(aalexand): Consider optimizing out the redundant work done in this
basePaths := []string{ // function if it proves to matter.
"/proc/self/cwd/./", func trimPath(path, trimPath, searchPath string) string {
"/proc/self/cwd/", // 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) }
// Trim configured trim prefixes.
for _, base := range basePaths { trimPaths := append(filepath.SplitList(filepath.ToSlash(trimPath)), "/proc/self/cwd/./", "/proc/self/cwd/")
if strings.HasPrefix(sPath, base) { for _, trimPath := range trimPaths {
return filepath.FromSlash(sPath[len(base):]) if !strings.HasSuffix(trimPath, "/") {
trimPath += "/"
}
if strings.HasPrefix(sPath, trimPath) {
return path[len(trimPath):]
} }
} }
return path return path
......
...@@ -48,40 +48,56 @@ func TestOpenSourceFile(t *testing.T) { ...@@ -48,40 +48,56 @@ func TestOpenSourceFile(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
desc string desc string
searchPath string searchPath string
trimPath string
fs []string fs []string
path string path string
wantPath string // If empty, error is wanted. wantPath string // If empty, error is wanted.
}{ }{
{ {
desc: "exact absolute path is found", desc: "exact absolute path is found",
fs: []string{"foo/bar.txt"}, fs: []string{"foo/bar.cc"},
path: "$dir/foo/bar.txt", path: "$dir/foo/bar.cc",
wantPath: "$dir/foo/bar.txt", wantPath: "$dir/foo/bar.cc",
}, },
{ {
desc: "exact relative path is found", desc: "exact relative path is found",
searchPath: "$dir", searchPath: "$dir",
fs: []string{"foo/bar.txt"}, fs: []string{"foo/bar.cc"},
path: "foo/bar.txt", path: "foo/bar.cc",
wantPath: "$dir/foo/bar.txt", wantPath: "$dir/foo/bar.cc",
}, },
{ {
desc: "multiple search path", desc: "multiple search path",
searchPath: "some/path" + lsep + "$dir", searchPath: "some/path" + lsep + "$dir",
fs: []string{"foo/bar.txt"}, fs: []string{"foo/bar.cc"},
path: "foo/bar.txt", path: "foo/bar.cc",
wantPath: "$dir/foo/bar.txt", wantPath: "$dir/foo/bar.cc",
}, },
{ {
desc: "relative path is found in parent dir", desc: "relative path is found in parent dir",
searchPath: "$dir/foo/bar", searchPath: "$dir/foo/bar",
fs: []string{"bar.txt", "foo/bar/baz.txt"}, fs: []string{"bar.cc", "foo/bar/baz.cc"},
path: "bar.txt", path: "bar.cc",
wantPath: "$dir/bar.txt", 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", desc: "error when not found",
path: "foo.txt", path: "foo.cc",
}, },
} { } {
t.Run(tc.desc, func(t *testing.T) { t.Run(tc.desc, func(t *testing.T) {
...@@ -103,15 +119,15 @@ func TestOpenSourceFile(t *testing.T) { ...@@ -103,15 +119,15 @@ func TestOpenSourceFile(t *testing.T) {
tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1)) tc.searchPath = filepath.FromSlash(strings.Replace(tc.searchPath, "$dir", tempdir, -1))
tc.path = filepath.FromSlash(strings.Replace(tc.path, "$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)) tc.wantPath = filepath.FromSlash(strings.Replace(tc.wantPath, "$dir", tempdir, 1))
if file, err := openSourceFile(tc.path, tc.searchPath); err != nil && tc.wantPath != "" { if file, err := openSourceFile(tc.path, tc.searchPath, tc.trimPath); err != nil && tc.wantPath != "" {
t.Errorf("openSourceFile(%q, %q) = err %v, want path %q", tc.path, tc.searchPath, err, 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 { } else if err == nil {
defer file.Close() defer file.Close()
gotPath := file.Name() gotPath := file.Name()
if tc.wantPath == "" { 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 { } 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 ( ...@@ -34,17 +34,22 @@ var (
symbolzRE = regexp.MustCompile(`(0x[[:xdigit:]]+)\s+(.*)`) symbolzRE = regexp.MustCompile(`(0x[[:xdigit:]]+)\s+(.*)`)
) )
// Symbolize symbolizes profile p by parsing data returned by a // Symbolize symbolizes profile p by parsing data returned by a symbolz
// symbolz handler. syms receives the symbolz query (hex addresses // handler. syms receives the symbolz query (hex addresses separated by '+')
// separated by '+') and returns the symbolz output in a string. If // and returns the symbolz output in a string. If force is false, it will only
// force is false, it will only symbolize locations from mappings // symbolize locations from mappings not already marked as HasFunctions. Never
// not already marked as HasFunctions. // 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 { 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 { for _, m := range p.Mapping {
if !force && m.HasFunctions { if !force && m.HasFunctions {
// Only check for HasFunctions as symbolz only populates function names. // Only check for HasFunctions as symbolz only populates function names.
continue continue
} }
// Skip well-known system mappings.
if m.Unsymbolizable() {
continue
}
mappingSources := sources[m.File] mappingSources := sources[m.File]
if m.BuildID != "" { if m.BuildID != "" {
mappingSources = append(mappingSources, sources[m.BuildID]...) mappingSources = append(mappingSources, sources[m.BuildID]...)
......
...@@ -19,13 +19,88 @@ package main ...@@ -19,13 +19,88 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/chzyer/readline"
"github.com/google/pprof/driver" "github.com/google/pprof/driver"
) )
func main() { 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) fmt.Fprintf(os.Stderr, "pprof: %v\n", err)
os.Exit(2) 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) ...@@ -74,6 +74,71 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp)
return 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 // FilterTagsByName filters the tags in a profile and only keeps
// tags that match show and not hide. // tags that match show and not hide.
func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) { func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {
...@@ -142,6 +207,9 @@ func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line { ...@@ -142,6 +207,9 @@ func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line {
// matchedLines returns the lines in the location that match // matchedLines returns the lines in the location that match
// the regular expression. // the regular expression.
func (loc *Location) matchedLines(re *regexp.Regexp) []Line { func (loc *Location) matchedLines(re *regexp.Regexp) []Line {
if m := loc.Mapping; m != nil && re.MatchString(m.File) {
return loc.Line
}
var lines []Line var lines []Line
for _, ln := range loc.Line { for _, ln := range loc.Line {
if fn := ln.Function; fn != nil { if fn := ln.Function; fn != nil {
......
This diff is collapsed.
...@@ -294,21 +294,12 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo { ...@@ -294,21 +294,12 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo {
} }
// Check memoization tables. // Check memoization tables.
bk, pk := src.key() mk := src.key()
if src.BuildID != "" { if m, ok := pm.mappings[mk]; ok {
if m, ok := pm.mappings[bk]; ok {
mi := mapInfo{m, int64(m.Start) - int64(src.Start)} mi := mapInfo{m, int64(m.Start) - int64(src.Start)}
pm.mappingsByID[src.ID] = mi pm.mappingsByID[src.ID] = mi
return 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{ m := &Mapping{
ID: uint64(len(pm.p.Mapping) + 1), ID: uint64(len(pm.p.Mapping) + 1),
Start: src.Start, Start: src.Start,
...@@ -324,21 +315,15 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo { ...@@ -324,21 +315,15 @@ func (pm *profileMerger) mapMapping(src *Mapping) mapInfo {
pm.p.Mapping = append(pm.p.Mapping, m) pm.p.Mapping = append(pm.p.Mapping, m)
// Update memoization tables. // Update memoization tables.
if m.BuildID != "" { pm.mappings[mk] = m
pm.mappings[bk] = m
}
if m.File != "" {
pm.mappings[pk] = m
}
mi := mapInfo{m, 0} mi := mapInfo{m, 0}
pm.mappingsByID[src.ID] = mi pm.mappingsByID[src.ID] = mi
return mi return mi
} }
// key generates encoded strings of Mapping to be used as a key for // 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 // maps.
// represents only the file path. func (m *Mapping) key() mappingKey {
func (m *Mapping) key() (buildIDKey, pathKey mappingKey) {
// Normalize addresses to handle address space randomization. // Normalize addresses to handle address space randomization.
// Round up to next 4K boundary to avoid minor discrepancies. // Round up to next 4K boundary to avoid minor discrepancies.
const mapsizeRounding = 0x1000 const mapsizeRounding = 0x1000
...@@ -346,24 +331,27 @@ func (m *Mapping) key() (buildIDKey, pathKey mappingKey) { ...@@ -346,24 +331,27 @@ func (m *Mapping) key() (buildIDKey, pathKey mappingKey) {
size := m.Limit - m.Start size := m.Limit - m.Start
size = size + mapsizeRounding - 1 size = size + mapsizeRounding - 1
size = size - (size % mapsizeRounding) size = size - (size % mapsizeRounding)
key := mappingKey{
buildIDKey = mappingKey{ size: size,
size, offset: m.Offset,
m.Offset,
m.BuildID,
} }
pathKey = mappingKey{ switch {
size, case m.BuildID != "":
m.Offset, key.buildIDOrFile = m.BuildID
m.File, 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 { type mappingKey struct {
size, offset uint64 size, offset uint64
buildidIDOrFile string buildIDOrFile string
} }
func (pm *profileMerger) mapLine(src Line) Line { 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 ( ...@@ -20,7 +20,6 @@ import (
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"reflect" "reflect"
"regexp"
"strings" "strings"
"sync" "sync"
"testing" "testing"
...@@ -902,101 +901,6 @@ func TestNormalizeIncompatibleProfiles(t *testing.T) { ...@@ -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 // locationHash constructs a string to use as a hashkey for a sample, based on its locations
func locationHash(s *Sample) string { func locationHash(s *Sample) string {
var tb 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 @@ ...@@ -9,8 +9,8 @@
{ {
"canonical": "github.com/google/pprof", "canonical": "github.com/google/pprof",
"local": "github.com/google/pprof", "local": "github.com/google/pprof",
"revision": "9e20b5b106e946f4cd1df94c1f6fe3f88456628d", "revision": "520140b6bf47519c766e8380e5f094576347b016",
"revisionTime": "2017-11-08T17:47:23Z" "revisionTime": "2018-05-08T15:00:43Z"
}, },
{ {
"canonical": "golang.org/x/arch/x86/x86asm", "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