Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flame Graph support on Web UI #188

Merged
merged 45 commits into from Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
06f02b6
feat: working flame graph web view
spiermar Aug 9, 2017
db1575c
feat: select flamegraph sample type
spiermar Aug 9, 2017
7f9e781
feat: profile details
spiermar Aug 9, 2017
8c11fce
feat: dynamic unit definition
spiermar Aug 9, 2017
b6e1efc
feat: button from main view
spiermar Aug 9, 2017
a55d5f9
feat: dynamic chart height
spiermar Aug 9, 2017
acfe4b9
feat: converting nanoseconds to seconds
spiermar Aug 9, 2017
faa772e
feat: selecting default metric based on sample_index variable
spiermar Aug 9, 2017
96445d3
Merge branch 'master' into feat_flamegraph
spiermar Aug 9, 2017
df97f08
feat: better cpu time conversion
spiermar Aug 10, 2017
5bd8ef3
feat: external resource integrity check
spiermar Aug 10, 2017
5d827ef
reafactor: dropping lodash script dependency
spiermar Aug 10, 2017
edb9617
feat: friendly error message if an external resource failed to load
spiermar Aug 10, 2017
0afa5df
refactor: serving javascript and css resources inline
spiermar Aug 15, 2017
a397cfc
refactor: removing bootstrap dependency
spiermar Aug 15, 2017
b110c5a
test: adding flamegraph tests
spiermar Aug 16, 2017
0c90f58
feat: graph button on flame graph view
spiermar Aug 16, 2017
0ef2abf
chore: upgrading to d3-flame-graph 1.0.5
spiermar Aug 18, 2017
5c523bf
feat: filtering small frames and using minified json
spiermar Aug 18, 2017
a61ecb7
Merge branch 'feat_flamegraph' of https://github.com/spiermar/pprof i…
spiermar Aug 18, 2017
ea28314
test: fix json assertion
spiermar Aug 21, 2017
7d50b81
feat: upgrading d3-flame-graph to 1.0.6
spiermar Aug 21, 2017
dc7fb88
Merge commit '57b9500a74c8000a864ef2dbcf157613a11df125' into feat_fla…
spiermar Aug 27, 2017
33b7a40
Merge commit 'cc3455886fdc155f3c51ee3e6ab146e768e92b2a' into feat_fla…
spiermar Aug 27, 2017
94b476f
fix: merge conflict leaked in
spiermar Aug 27, 2017
ffeb2ac
Merge branch 'master' into feat_flamegraph
spiermar Sep 19, 2017
335eff1
feat: default header on flame graph view [wip]
spiermar Sep 19, 2017
0e6f0f8
feat: flame graph filter functions
spiermar Sep 20, 2017
3b92869
fix: details text not being displayed
spiermar Sep 20, 2017
d8164c3
feat: flame graph search/highlight
spiermar Sep 20, 2017
39351b8
test: updating test case
spiermar Sep 20, 2017
2e3e9bc
fix: flame graph zoom issues
spiermar Sep 20, 2017
cf2aab6
Merge branch 'master' into feat_flamegraph
spiermar Sep 25, 2017
4a03397
chore: copyright update
spiermar Sep 26, 2017
2289cb4
Generate the flame graph data using existing graph.
aalexand Sep 29, 2017
46ed7a2
feat: percentage of total
spiermar Nov 19, 2017
370fc15
Merge branch 'master' into feat_flamegraph
spiermar Nov 19, 2017
0e1724b
refactor: style adjustments
spiermar Nov 19, 2017
b9de433
refactor: custom d3 build
spiermar Nov 19, 2017
7307bc6
Merge branch 'master' into feat_flamegraph
spiermar Nov 21, 2017
49a0976
refactor: use measurement package percentage function
spiermar Nov 21, 2017
c548705
refactor: remove template directive from third_party files
spiermar Nov 21, 2017
f465f8a
func: do not trim the tree so that the flame graph contains all funct…
spiermar Nov 21, 2017
1bd096a
docs: instructions on how to built the custom d3 bundle
spiermar Nov 21, 2017
7b1f77e
docs: style improvement on d3 readme
spiermar Nov 21, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CONTRIBUTORS
Expand Up @@ -11,4 +11,5 @@
Raul Silvera <rsilvera@google.com>
Tipp Moseley <tipp@google.com>
Hyoun Kyu Cho <netforce@google.com>
Martin Spier <spiermar@gmail.com>

105 changes: 105 additions & 0 deletions internal/driver/flamegraph.go
@@ -0,0 +1,105 @@
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package driver

import (
"encoding/json"
"fmt"
"html/template"
"math"
"net/http"
"strings"

"github.com/google/pprof/internal/graph"
"github.com/google/pprof/internal/report"
)

type treeNode struct {
Name string `json:"n"`
Cum int64 `json:"v"`
CumFormat string `json:"l"`
Percent string `json:"p"`
Children []*treeNode `json:"c"`
}

// percentage computes the percentage of total of a value, and encodes
// it as a string. At least two digits of precision are printed.
func percentage(value, total int64) string {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I I think we should do #265 and use measurement.Percentage in the code below so that this function can be dropped and so that the percentage formatting is enforced to be consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I wait until it gets merged?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I merged that, you can rebase.

var ratio float64
if total != 0 {
ratio = math.Abs(float64(value)/float64(total)) * 100
}
return fmt.Sprintf("%5.2f%%", ratio)
}

// flamegraph generates a web page containing a flamegraph.
func (ui *webInterface) flamegraph(w http.ResponseWriter, req *http.Request) {
// Force the call tree so that the graph is a tree.
rpt, errList := ui.makeReport(w, req, []string{"svg"}, "call_tree", "true")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this to

        // Force the call tree so that the graph is a tree. Also do not trim the tree
        // so that the flame graph contains all functions.
        rpt, errList := ui.makeReport(w, req, []string{"svg"}, "call_tree", "true", "trim", "false")

The GetDOT call trims the tree by default, and for the flame graph I think it's reasonable to override that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you. I prefer to have the flame graph contain everything, but that's not the default behavior for the directed graph, so I wasn't really sure what would the users prefer.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The graph does it by default because displaying the full graph for large programs becomes messy. Flame graph appears to be compact and readable enough on some larger profiles I tried so I think trimming the tree would be more confusing that useful here.

if rpt == nil {
return // error already reported
}

// Generate dot graph.
g, config := report.GetDOT(rpt)
var nodes []*treeNode
nroots := 0
rootValue := int64(0)
nodeMap := map[*graph.Node]*treeNode{}
// Make all nodes and the map, collect the roots.
for _, n := range g.Nodes {
v := n.CumValue()
node := &treeNode{
Name: n.Info.PrintableName(),
Cum: v,
CumFormat: config.FormatValue(v),
Percent: strings.TrimSpace(percentage(v, config.Total)),
}
nodes = append(nodes, node)
if len(n.In) == 0 {
nodes[nroots], nodes[len(nodes)-1] = nodes[len(nodes)-1], nodes[nroots]
nroots++
rootValue += v
}
nodeMap[n] = node
}
// Populate the child links.
for _, n := range g.Nodes {
node := nodeMap[n]
for child := range n.Out {
node.Children = append(node.Children, nodeMap[child])
}
}

rootNode := &treeNode{
Name: "root",
Cum: rootValue,
CumFormat: config.FormatValue(rootValue),
Percent: strings.TrimSpace(percentage(rootValue, config.Total)),
Children: nodes[0:nroots],
}

// JSON marshalling flame graph
b, err := json.Marshal(rootNode)
if err != nil {
http.Error(w, "error serializing flame graph", http.StatusInternalServerError)
ui.options.UI.PrintErr(err)
return
}

ui.render(w, "/flamegraph", "flamegraph", rpt, errList, config.Labels, webArgs{
FlameGraph: template.JS(b),
})
}
119 changes: 119 additions & 0 deletions internal/driver/webhtml.go
Expand Up @@ -15,9 +15,15 @@
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(d3.Source))
template.Must(templates.Parse(d3tip.Source))
template.Must(templates.Parse(d3flamegraph.Source))
template.Must(templates.Parse(`
{{define "css"}}
<style type="text/css">
Expand Down Expand Up @@ -193,6 +199,7 @@ View
<div class="menu">
<a title="{{.Help.top}}" href="/top" id="topbtn">Top</a>
<a title="{{.Help.graph}}" href="/" id="graphbtn">Graph</a>
<a title="{{.Help.flamegraph}}" href="/flamegraph" id="flamegraph">Flame Graph</a>
<a title="{{.Help.peek}}" href="/peek" id="peek">Peek</a>
<a title="{{.Help.list}}" href="/source" id="list">Source</a>
<a title="{{.Help.disasm}}" href="/disasm" id="disasm">Disassemble</a>
Expand Down Expand Up @@ -961,5 +968,117 @@ makeTopTable({{.Total}}, {{.Top}})
</body>
</html>
{{end}}

{{define "flamegraph" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
<style type="text/css">{{template "d3flamegraphcss" .}}</style>
<style type="text/css">
.flamegraph-content {
width: 90%;
min-width: 80%;
margin-left: 5%;
}
.flamegraph-details {
height: 1.2em;
width: 90%;
min-width: 90%;
margin-left: 5%;
padding-bottom: 41px;
}
</style>
</head>
<body>
{{template "header" .}}
<div id="bodycontainer">
<div class="flamegraph-content">
<div id="chart"></div>
</div>
<div id="flamegraphdetails" class="flamegraph-details"></div>
</div>
{{template "script" .}}
<script>viewer({{.BaseURL}}, {{.Nodes}})</script>
<script>{{template "d3script" .}}</script>
<script>{{template "d3tipscript" .}}</script>
<script>{{template "d3flamegraphscript" .}}</script>
<script type="text/javascript">
var data = {{.FlameGraph}};
var label = function(d) {
return d.data.n + " (" + d.data.p + ", " + d.data.l + ")";
};

var width = document.getElementById("chart").clientWidth;

var flameGraph = d3.flameGraph()
.width(width)
.cellHeight(18)
.minFrameSize(1)
.transitionDuration(750)
.transitionEase(d3.easeCubic)
.sort(true)
.title("")
.label(label)
.details(document.getElementById("flamegraphdetails"));

var tip = d3.tip()
.direction("s")
.offset([8, 0])
.attr('class', 'd3-flame-graph-tip')
.html(function(d) { return "name: " + d.data.n + ", value: " + d.data.l; });

flameGraph.tooltip(tip);

d3.select("#chart")
.datum(data)
.call(flameGraph);

function clear() {
flameGraph.clear();
}

function resetZoom() {
flameGraph.resetZoom();
}

window.addEventListener("resize", function() {
var width = document.getElementById("chart").clientWidth;
var graphs = document.getElementsByClassName("d3-flame-graph");
if (graphs.length > 0) {
graphs[0].setAttribute("width", width);
}
flameGraph.width(width);
flameGraph.resetZoom();
}, true);

var searchbox = document.getElementById("searchbox");
var searchAlarm = null;

function selectMatching() {
searchAlarm = null;

if (searchbox.value != "") {
flameGraph.search(searchbox.value);
} else {
flameGraph.clear();
}
}

function handleSearch() {
// Delay expensive processing so a flurry of key strokes is handled once.
if (searchAlarm != null) {
clearTimeout(searchAlarm);
}
searchAlarm = setTimeout(selectMatching, 300);
}

searchbox.addEventListener("input", handleSearch);
</script>
</body>
</html>
{{end}}
`))
}
32 changes: 17 additions & 15 deletions internal/driver/webui.go
Expand Up @@ -69,16 +69,17 @@ func (ec *errorCatcher) PrintErr(args ...interface{}) {

// webArgs contains arguments passed to templates in webhtml.go.
type webArgs struct {
BaseURL string
Title string
Errors []string
Total int64
Legend []string
Help map[string]string
Nodes []string
HTMLBody template.HTML
TextBody string
Top []report.TextItem
BaseURL string
Title string
Errors []string
Total int64
Legend []string
Help map[string]string
Nodes []string
HTMLBody template.HTML
TextBody string
Top []report.TextItem
FlameGraph template.JS
}

func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, wantBrowser bool) error {
Expand Down Expand Up @@ -115,11 +116,12 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, w
Host: host,
Port: port,
Handlers: map[string]http.Handler{
"/": http.HandlerFunc(ui.dot),
"/top": http.HandlerFunc(ui.top),
"/disasm": http.HandlerFunc(ui.disasm),
"/source": http.HandlerFunc(ui.source),
"/peek": http.HandlerFunc(ui.peek),
"/": http.HandlerFunc(ui.dot),
"/top": http.HandlerFunc(ui.top),
"/disasm": http.HandlerFunc(ui.disasm),
"/source": http.HandlerFunc(ui.source),
"/peek": http.HandlerFunc(ui.peek),
"/flamegraph": http.HandlerFunc(ui.flamegraph),
},
}

Expand Down
1 change: 1 addition & 0 deletions internal/driver/webui_test.go
Expand Up @@ -80,6 +80,7 @@ func TestWebInterface(t *testing.T) {
[]string{"300ms.*F1", "200ms.*300ms.*F2"}, false},
{"/disasm?f=" + url.QueryEscape("F[12]"),
[]string{"f1:asm", "f2:asm"}, false},
{"/flamegraph", []string{"File: testbin", "\"n\":\"root\"", "\"n\":\"F1\"", "function tip", "function flameGraph", "function hierarchy"}, false},
}
for _, c := range testcases {
if c.needDot && !haveDot {
Expand Down
27 changes: 27 additions & 0 deletions third_party/d3/LICENSE
@@ -0,0 +1,27 @@
Copyright 2010-2017 Mike Bostock
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the author nor the names of contributors may be used to
endorse or promote products derived from this software without specific prior
written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.