Skip to content

Commit

Permalink
Added alternative flamegraph implementation that can show callers. (#716
Browse files Browse the repository at this point in the history
)

Add an experimental flame-graph implementation. It can be selected in
pprof's web interface using the new "Flame (experimental)" menu entry.
At some point this new implementation may become the default.

The new view is similar to flame-graph view. But it can show caller
information as well. This should allow it to satisfy many users of
Graph and Peek views as well.

Let's illustrate with an example. Suppose we have profile data that
consists of the following stacks:

```
1000	main -> foo -> malloc
2000	main -> bar -> malloc
```

When main is selected, both the old and new views show:

```
[-------------------3000 main---------------------]
[---1000 foo-----] [----------2000 bar------------]
[---1000 malloc--] [----------2000 malloc---------]
```

But suppose now the user selects the right-most malloc slot.
The old view will show just the path leading to that malloc:

```
[----------2000 main-----------]
[----------2000 bar------------]
[----------2000 malloc---------]
```

The new view will however show a flame-graph view that grows
upwards that displays the call stacks leading to malloc:

```
[---1000 main----] [----------2000 main-----------]
[---1000 foo-----] [----------2000 bar------------]
[-------------------3000 malloc-------------------]
```

This caller display is useful when trying to determine expensive
callers of function.

A list of important differences between the new view and flame graphs:

New view pros:

1.  Callers are shown, e.g., all paths leading to malloc.
2.  Shows self-cost clearly with a different saturation.
3.  Font size is adjusted to fit more text into boxes.
4.  Highlighting on hover shows other occurrences of a function.
5.  Search works more like other views.
6.  Pivot changes are reflected in browser history (so back and forward
    buttons can be used to navigate between different selections).
7.  Allows eventual removal of the D3 dependency, which may make
    integrations into various environments easier.
8.  Colors provide higher contrast between foreground and background.

New view cons:

1.  There are small differences in how things look and feel.
2.  Color-scheme is very different.
3.  Change triggered by selecting a new entry is not animated.
  • Loading branch information
ghemawat committed Aug 16, 2022
1 parent a41b82a commit e6338ce
Show file tree
Hide file tree
Showing 18 changed files with 1,292 additions and 20 deletions.
34 changes: 34 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,40 @@ the user to interactively view profile data in multiple formats.

The top of the display is a header that contains some buttons and menus.

## View

The `View` menu allows the user to switch between different visualizations of
the profile.

Top
: Displays a tabular view of profile entries. The table can be sorted
interactively.

Graph
: Displays a scrollable/zoomable graph view; each function (or profile entry)
is represented by a node and edges connect callers to callees.

Flame Graph
: Displays a [flame graph](https://www.brendangregg.com/flamegraphs.html).

Flame (experimental)
: Displays a view similar to a flame graph that can show the selected node's
callers and callees simultaneously.

NOTE: This view is currently experimental and may eventually replace the normal
Flame Graph view.

Peek
: Shows callers / callees per function in a simple textual forma.

Source
: Displays source code annotated with profile information. Clicking on a
source line can show the disassembled machine instructions for that line.

Disassemble
: Displays disassembled machine instructions annotated with profile
information.

## Config

The `Config` menu allows the user to save the current refinement
Expand Down
1 change: 1 addition & 0 deletions internal/driver/html/common.css
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ a {
box-shadow: 0 1px 5px rgba(0,0,0,.3);
font-size: 100%;
text-transform: none;
white-space: nowrap;
}
.menu-item, .submenu {
user-select: none;
Expand Down
57 changes: 38 additions & 19 deletions internal/driver/html/common.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,12 @@ function initConfigManager() {
}
}

function viewer(baseUrl, nodes) {
// options if present can contain:
// hiliter: function(Number, Boolean): Boolean
// Overridable mechanism for highlighting/unhighlighting specified node.
// current: function() Map[Number,Boolean]
// Overridable mechanism for fetching set of currently selected nodes.
function viewer(baseUrl, nodes, options) {
'use strict';

// Elements
Expand All @@ -403,6 +408,16 @@ function viewer(baseUrl, nodes) {
let searchAlarm = null;
let buttonsEnabled = true;

// Return current selection.
function getSelection() {
if (selected.size > 0) {
return selected;
} else if (options && options.current) {
return options.current();
}
return new Map();
}

function handleDetails(e) {
e.preventDefault();
const detailsText = document.getElementById('detailsbox');
Expand Down Expand Up @@ -453,15 +468,15 @@ function viewer(baseUrl, nodes) {
// drop currently selected items that do not match re.
selected.forEach(function(v, n) {
if (!match(nodes[n])) {
unselect(n, document.getElementById('node' + n));
unselect(n);
}
})

// add matching items that are not currently selected.
if (nodes) {
for (let n = 0; n < nodes.length; n++) {
if (!selected.has(n) && match(nodes[n])) {
select(n, document.getElementById('node' + n));
select(n);
}
}
}
Expand All @@ -482,23 +497,19 @@ function viewer(baseUrl, nodes) {
const n = nodeId(elem);
if (n < 0) return;
if (selected.has(n)) {
unselect(n, elem);
unselect(n);
} else {
select(n, elem);
select(n);
}
updateButtons();
}

function unselect(n, elem) {
if (elem == null) return;
selected.delete(n);
setBackground(elem, false);
function unselect(n) {
if (setNodeHighlight(n, false)) selected.delete(n);
}

function select(n, elem) {
if (elem == null) return;
selected.set(n, true);
setBackground(elem, true);
if (setNodeHighlight(n, true)) selected.set(n, true);
}

function nodeId(elem) {
Expand All @@ -511,11 +522,17 @@ function viewer(baseUrl, nodes) {
return n;
}

function setBackground(elem, set) {
// Change highlighting of node (returns true if node was found).
function setNodeHighlight(n, set) {
if (options && options.hiliter) return options.hiliter(n, set);

const elem = document.getElementById('node' + n);
if (!elem) return false;

// Handle table row highlighting.
if (elem.nodeName == 'TR') {
elem.classList.toggle('hilite', set);
return;
return true;
}

// Handle svg element highlighting.
Expand All @@ -528,6 +545,8 @@ function viewer(baseUrl, nodes) {
p.style.fill = origFill.get(p);
}
}

return true;
}

function findPolygon(elem) {
Expand Down Expand Up @@ -575,8 +594,8 @@ function viewer(baseUrl, nodes) {
// The selection can be in one of two modes: regexp-based or
// list-based. Construct regular expression depending on mode.
let re = regexpActive
? search.value
: Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|');
? search.value
: Array.from(getSelection().keys()).map(key => quotemeta(nodes[key])).join('|');

setHrefParams(elem, function (params) {
if (re != '') {
Expand Down Expand Up @@ -639,7 +658,7 @@ function viewer(baseUrl, nodes) {
}

function updateButtons() {
const enable = (search.value != '' || selected.size != 0);
const enable = (search.value != '' || getSelection().size != 0);
if (buttonsEnabled == enable) return;
buttonsEnabled = enable;
for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
Expand All @@ -663,8 +682,8 @@ function viewer(baseUrl, nodes) {
toptable.addEventListener('touchstart', handleTopClick);
}

const ids = ['topbtn', 'graphbtn', 'flamegraph', 'peek', 'list', 'disasm',
'focus', 'ignore', 'hide', 'show', 'show-from'];
const ids = ['topbtn', 'graphbtn', 'flamegraph', 'flamegraph2', 'peek', 'list',
'disasm', 'focus', 'ignore', 'hide', 'show', 'show-from'];
ids.forEach(makeSearchLinkDynamic);

const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}];
Expand Down
1 change: 1 addition & 0 deletions internal/driver/html/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ <h1><a href="./">pprof</a></h1>
<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.flamegraph2}}" href="./flamegraph2" id="flamegraph2">Flame Graph (new)</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
75 changes: 75 additions & 0 deletions internal/driver/html/stacks.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
body {
overflow: hidden; /* Want scrollbar not here, but in #stack-holder */
}
/* Scrollable container for flame graph */
#stack-holder {
width: 100%;
flex-grow: 1;
overflow-y: auto;
background: #eee; /* Light grey gives better contrast with boxes */
position: relative; /* Allows absolute positioning of child boxes */
}
/* Flame graph */
#stack-chart {
width: 100%;
position: relative; /* Allows absolute positioning of child boxes */
}
/* Shows details of frame that is under the mouse */
#current-details {
position: absolute;
top: 5px;
right: 5px;
z-index: 2;
font-size: 12pt;
}
/* Background of a single flame-graph frame */
.boxbg {
border-width: 0px;
position: absolute;
overflow: hidden;
}
/* Function name */
.boxtext {
position: absolute;
width: 100%;
padding-left: 2px;
line-height: 18px;
cursor: default;
font-family: "Google Sans", Arial, sans-serif;
font-size: 12pt;
z-index: 2;
}
/* Box highlighting via shadows to avoid size changes */
.hilite { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
.hilite2 { box-shadow: 0px 0px 0px 2px #000; z-index: 1; }
/* Self-cost region inside a box */
.self {
position: absolute;
background: rgba(0,0,0,0.25); /* Darker hue */
}
/* Gap left between callers and callees */
.separator {
position: absolute;
text-align: center;
font-size: 12pt;
font-weight: bold;
}
/* Ensure that pprof menu is above boxes */
.submenu { z-index: 3; }
/* Right-click menu */
#action-menu {
max-width: 15em;
}
/* Right-click menu title */
#action-title {
display: block;
padding: 0.5em 1em;
background: #888;
text-overflow: ellipsis;
overflow: hidden;
}
/* Internal canvas used to measure text size when picking fonts */
#textsizer {
position: absolute;
bottom: -100px;
}
32 changes: 32 additions & 0 deletions internal/driver/html/stacks.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "stacks_css"}}
</head>
<body>
{{template "header" .}}
<div id="stack-holder">
<div id="stack-chart"></div>
<div id="current-details"></div>
</div>
<div id="action-menu" class="submenu">
<span id="action-title"></span>
<hr>
<a title="{{.Help.list}}" id="action-source" href="./source">Show source code</a>
<a title="{{.Help.list}}" id="action-source-tab" href="./source" target="_blank">Show source in new tab</a>
<hr>
<a title="{{.Help.focus}}" id="action-focus" href="?">Focus</a>
<a title="{{.Help.ignore}}" id="action-ignore" href="?">Ignore</a>
<a title="{{.Help.hide}}" id="action-hide" href="?">Hide</a>
<a title="{{.Help.show_from}}" id="action-showfrom" href="?">Show from</a>
</div>
{{template "script" .}}
{{template "stacks_js"}}
<script>
stackViewer({{.Stacks}}, {{.Nodes}});
</script>
</body>
</html>
Loading

0 comments on commit e6338ce

Please sign in to comment.