blob: 4f7610c7e5475693886c1eabb40c85577af7f23e [file] [log] [blame]
// 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 (
"html/template"
"github.com/google/pprof/third_party/d3"
"github.com/google/pprof/third_party/d3flamegraph"
)
// addTemplates adds a set of template definitions to templates.
func addTemplates(templates *template.Template) {
template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`))
template.Must(templates.Parse(`{{define "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`))
template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`))
template.Must(templates.Parse(`
{{define "css"}}
<style type="text/css">
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
font-size: 13px;
line-height: 1.4;
display: flex;
flex-direction: column;
}
a {
color: #2a66d9;
}
.header {
display: flex;
align-items: center;
height: 44px;
min-height: 44px;
background-color: #eee;
color: #212121;
padding: 0 1rem;
}
.header > div {
margin: 0 0.125em;
}
.header .title h1 {
font-size: 1.75em;
margin-right: 1rem;
}
.header .title a {
color: #212121;
text-decoration: none;
}
.header .title a:hover {
text-decoration: underline;
}
.header .description {
width: 100%;
text-align: right;
white-space: nowrap;
}
@media screen and (max-width: 799px) {
.header input {
display: none;
}
}
#detailsbox {
display: none;
z-index: 1;
position: fixed;
top: 40px;
right: 20px;
background-color: #ffffff;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
line-height: 24px;
padding: 1em;
text-align: left;
}
.header input {
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' style='pointer-events:none;display:block;width:100%25;height:100%25;fill:%23757575'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61.0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 4px center/20px 20px;
border: 1px solid #d1d2d3;
border-radius: 2px 0 0 2px;
padding: 0.25em;
padding-left: 28px;
margin-left: 1em;
font-family: 'Roboto', 'Noto', sans-serif;
font-size: 1em;
line-height: 24px;
color: #212121;
}
.downArrow {
border-top: .36em solid #ccc;
border-left: .36em solid transparent;
border-right: .36em solid transparent;
margin-bottom: .05em;
margin-left: .5em;
transition: border-top-color 200ms;
}
.menu-item {
height: 100%;
text-transform: uppercase;
font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
position: relative;
}
.menu-item .menu-name:hover {
opacity: 0.75;
}
.menu-item .menu-name:hover .downArrow {
border-top-color: #666;
}
.menu-name {
height: 100%;
padding: 0 0.5em;
display: flex;
align-items: center;
justify-content: center;
}
.submenu {
display: none;
z-index: 1;
margin-top: -4px;
min-width: 10em;
position: absolute;
left: 0px;
background-color: white;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
font-size: 100%;
text-transform: none;
}
.menu-item, .submenu {
user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.submenu hr {
border: 0;
border-top: 2px solid #eee;
}
.submenu a {
display: block;
padding: .5em 1em;
text-decoration: none;
}
.submenu a:hover, .submenu a.active {
color: white;
background-color: #6b82d6;
}
.submenu a.disabled {
color: gray;
pointer-events: none;
}
.menu-check-mark {
position: absolute;
left: 2px;
}
.menu-delete-btn {
position: absolute;
right: 2px;
}
{{/* Used to disable events when a modal dialog is displayed */}}
#dialog-overlay {
display: none;
position: fixed;
left: 0px;
top: 0px;
width: 100%;
height: 100%;
background-color: rgba(1,1,1,0.1);
}
.dialog {
{{/* Displayed centered horizontally near the top */}}
display: none;
position: fixed;
margin: 0px;
top: 60px;
left: 50%;
transform: translateX(-50%);
z-index: 3;
font-size: 125%;
background-color: #ffffff;
box-shadow: 0 1px 5px rgba(0,0,0,.3);
}
.dialog-header {
font-size: 120%;
border-bottom: 1px solid #CCCCCC;
width: 100%;
text-align: center;
background: #EEEEEE;
user-select: none;
}
.dialog-footer {
border-top: 1px solid #CCCCCC;
width: 100%;
text-align: right;
padding: 10px;
}
.dialog-error {
margin: 10px;
color: red;
}
.dialog input {
margin: 10px;
font-size: inherit;
}
.dialog button {
margin-left: 10px;
font-size: inherit;
}
#save-dialog, #delete-dialog {
width: 50%;
max-width: 20em;
}
#delete-prompt {
padding: 10px;
}
#content {
overflow-y: scroll;
padding: 1em;
}
#top {
overflow-y: scroll;
}
#graph {
overflow: hidden;
}
#graph svg {
width: 100%;
height: auto;
padding: 10px;
}
#content.source .filename {
margin-top: 0;
margin-bottom: 1em;
font-size: 120%;
}
#content.source pre {
margin-bottom: 3em;
}
table {
border-spacing: 0px;
width: 100%;
padding-bottom: 1em;
white-space: nowrap;
}
table thead {
font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
}
table tr th {
position: sticky;
top: 0;
background-color: #ddd;
text-align: right;
padding: .3em .5em;
}
table tr td {
padding: .3em .5em;
text-align: right;
}
#top table tr th:nth-child(6),
#top table tr th:nth-child(7),
#top table tr td:nth-child(6),
#top table tr td:nth-child(7) {
text-align: left;
}
#top table tr td:nth-child(6) {
width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
cursor: ns-resize;
}
.hilite {
background-color: #ebf5fb;
font-weight: bold;
}
</style>
{{end}}
{{define "header"}}
<div class="header">
<div class="title">
<h1><a href="./">pprof</a></h1>
</div>
<div id="view" class="menu-item">
<div class="menu-name">
View
<i class="downArrow"></i>
</div>
<div class="submenu">
<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>
</div>
</div>
{{$sampleLen := len .SampleTypes}}
{{if gt $sampleLen 1}}
<div id="sample" class="menu-item">
<div class="menu-name">
Sample
<i class="downArrow"></i>
</div>
<div class="submenu">
{{range .SampleTypes}}
<a href="?si={{.}}" id="{{.}}">{{.}}</a>
{{end}}
</div>
</div>
{{end}}
<div id="refine" class="menu-item">
<div class="menu-name">
Refine
<i class="downArrow"></i>
</div>
<div class="submenu">
<a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
<a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
<a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
<a title="{{.Help.show}}" href="?" id="show">Show</a>
<a title="{{.Help.show_from}}" href="?" id="show-from">Show from</a>
<hr>
<a title="{{.Help.reset}}" href="?">Reset</a>
</div>
</div>
<div id="config" class="menu-item">
<div class="menu-name">
Config
<i class="downArrow"></i>
</div>
<div class="submenu">
<a title="{{.Help.save_config}}" id="save-config">Save as ...</a>
<hr>
{{range .Configs}}
<a href="{{.URL}}">
{{if .Current}}<span class="menu-check-mark">✓</span>{{end}}
{{.Name}}
{{if .UserConfig}}<span class="menu-delete-btn" data-config={{.Name}}>🗙</span>{{end}}
</a>
{{end}}
</div>
</div>
<div>
<input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
</div>
<div class="description">
<a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a>
<div id="detailsbox">
{{range .Legend}}<div>{{.}}</div>{{end}}
</div>
</div>
</div>
<div id="dialog-overlay"></div>
<div class="dialog" id="save-dialog">
<div class="dialog-header">Save options as</div>
<datalist id="config-list">
{{range .Configs}}{{if .UserConfig}}<option value="{{.Name}}" />{{end}}{{end}}
</datalist>
<input id="save-name" type="text" list="config-list" placeholder="New config" />
<div class="dialog-footer">
<span class="dialog-error" id="save-error"></span>
<button id="save-cancel">Cancel</button>
<button id="save-confirm">Save</button>
</div>
</div>
<div class="dialog" id="delete-dialog">
<div class="dialog-header" id="delete-dialog-title">Delete config</div>
<div id="delete-prompt"></div>
<div class="dialog-footer">
<span class="dialog-error" id="delete-error"></span>
<button id="delete-cancel">Cancel</button>
<button id="delete-confirm">Delete</button>
</div>
</div>
<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
{{end}}
{{define "graph" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
</head>
<body>
{{template "header" .}}
<div id="graph">
{{.HTMLBody}}
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
</body>
</html>
{{end}}
{{define "script"}}
<script>
// Make svg pannable and zoomable.
// Call clickHandler(t) if a click event is caught by the pan event handlers.
function initPanAndZoom(svg, clickHandler) {
'use strict';
// Current mouse/touch handling mode
const IDLE = 0;
const MOUSEPAN = 1;
const TOUCHPAN = 2;
const TOUCHZOOM = 3;
let mode = IDLE;
// State needed to implement zooming.
let currentScale = 1.0;
const initWidth = svg.viewBox.baseVal.width;
const initHeight = svg.viewBox.baseVal.height;
// State needed to implement panning.
let panLastX = 0; // Last event X coordinate
let panLastY = 0; // Last event Y coordinate
let moved = false; // Have we seen significant movement
let touchid = null; // Current touch identifier
// State needed for pinch zooming
let touchid2 = null; // Second id for pinch zooming
let initGap = 1.0; // Starting gap between two touches
let initScale = 1.0; // currentScale when pinch zoom started
let centerPoint = null; // Center point for scaling
// Convert event coordinates to svg coordinates.
function toSvg(x, y) {
const p = svg.createSVGPoint();
p.x = x;
p.y = y;
let m = svg.getCTM();
if (m == null) m = svg.getScreenCTM(); // Firefox workaround.
return p.matrixTransform(m.inverse());
}
// Change the scaling for the svg to s, keeping the point denoted
// by u (in svg coordinates]) fixed at the same screen location.
function rescale(s, u) {
// Limit to a good range.
if (s < 0.2) s = 0.2;
if (s > 10.0) s = 10.0;
currentScale = s;
// svg.viewBox defines the visible portion of the user coordinate
// system. So to magnify by s, divide the visible portion by s,
// which will then be stretched to fit the viewport.
const vb = svg.viewBox;
const w1 = vb.baseVal.width;
const w2 = initWidth / s;
const h1 = vb.baseVal.height;
const h2 = initHeight / s;
vb.baseVal.width = w2;
vb.baseVal.height = h2;
// We also want to adjust vb.baseVal.x so that u.x remains at same
// screen X coordinate. In other words, want to change it from x1 to x2
// so that:
// (u.x - x1) / w1 = (u.x - x2) / w2
// Simplifying that, we get
// (u.x - x1) * (w2 / w1) = u.x - x2
// x2 = u.x - (u.x - x1) * (w2 / w1)
vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1);
vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1);
}
function handleWheel(e) {
if (e.deltaY == 0) return;
// Change scale factor by 1.1 or 1/1.1
rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
toSvg(e.offsetX, e.offsetY));
}
function setMode(m) {
mode = m;
touchid = null;
touchid2 = null;
}
function panStart(x, y) {
moved = false;
panLastX = x;
panLastY = y;
}
function panMove(x, y) {
let dx = x - panLastX;
let dy = y - panLastY;
if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves
moved = true;
panLastX = x;
panLastY = y;
// Firefox workaround: get dimensions from parentNode.
const swidth = svg.clientWidth || svg.parentNode.clientWidth;
const sheight = svg.clientHeight || svg.parentNode.clientHeight;
// Convert deltas from screen space to svg space.
dx *= (svg.viewBox.baseVal.width / swidth);
dy *= (svg.viewBox.baseVal.height / sheight);
svg.viewBox.baseVal.x -= dx;
svg.viewBox.baseVal.y -= dy;
}
function handleScanStart(e) {
if (e.button != 0) return; // Do not catch right-clicks etc.
setMode(MOUSEPAN);
panStart(e.clientX, e.clientY);
e.preventDefault();
svg.addEventListener('mousemove', handleScanMove);
}
function handleScanMove(e) {
if (e.buttons == 0) {
// Missed an end event, perhaps because mouse moved outside window.
setMode(IDLE);
svg.removeEventListener('mousemove', handleScanMove);
return;
}
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
}
function handleScanEnd(e) {
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
setMode(IDLE);
svg.removeEventListener('mousemove', handleScanMove);
if (!moved) clickHandler(e.target);
}
// Find touch object with specified identifier.
function findTouch(tlist, id) {
for (const t of tlist) {
if (t.identifier == id) return t;
}
return null;
}
// Return distance between two touch points
function touchGap(t1, t2) {
const dx = t1.clientX - t2.clientX;
const dy = t1.clientY - t2.clientY;
return Math.hypot(dx, dy);
}
function handleTouchStart(e) {
if (mode == IDLE && e.changedTouches.length == 1) {
// Start touch based panning
const t = e.changedTouches[0];
setMode(TOUCHPAN);
touchid = t.identifier;
panStart(t.clientX, t.clientY);
e.preventDefault();
} else if (mode == TOUCHPAN && e.touches.length == 2) {
// Start pinch zooming
setMode(TOUCHZOOM);
const t1 = e.touches[0];
const t2 = e.touches[1];
touchid = t1.identifier;
touchid2 = t2.identifier;
initScale = currentScale;
initGap = touchGap(t1, t2);
centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
(t1.clientY + t2.clientY) / 2);
e.preventDefault();
}
}
function handleTouchMove(e) {
if (mode == TOUCHPAN) {
const t = findTouch(e.changedTouches, touchid);
if (t == null) return;
if (e.touches.length != 1) {
setMode(IDLE);
return;
}
panMove(t.clientX, t.clientY);
e.preventDefault();
} else if (mode == TOUCHZOOM) {
// Get two touches; new gap; rescale to ratio.
const t1 = findTouch(e.touches, touchid);
const t2 = findTouch(e.touches, touchid2);
if (t1 == null || t2 == null) return;
const gap = touchGap(t1, t2);
rescale(initScale * gap / initGap, centerPoint);
e.preventDefault();
}
}
function handleTouchEnd(e) {
if (mode == TOUCHPAN) {
const t = findTouch(e.changedTouches, touchid);
if (t == null) return;
panMove(t.clientX, t.clientY);
setMode(IDLE);
e.preventDefault();
if (!moved) clickHandler(t.target);
} else if (mode == TOUCHZOOM) {
setMode(IDLE);
e.preventDefault();
}
}
svg.addEventListener('mousedown', handleScanStart);
svg.addEventListener('mouseup', handleScanEnd);
svg.addEventListener('touchstart', handleTouchStart);
svg.addEventListener('touchmove', handleTouchMove);
svg.addEventListener('touchend', handleTouchEnd);
svg.addEventListener('wheel', handleWheel, true);
}
function initMenus() {
'use strict';
let activeMenu = null;
let activeMenuHdr = null;
function cancelActiveMenu() {
if (activeMenu == null) return;
activeMenu.style.display = 'none';
activeMenu = null;
activeMenuHdr = null;
}
// Set click handlers on every menu header.
for (const menu of document.getElementsByClassName('submenu')) {
const hdr = menu.parentElement;
if (hdr == null) return;
if (hdr.classList.contains('disabled')) return;
function showMenu(e) {
// menu is a child of hdr, so this event can fire for clicks
// inside menu. Ignore such clicks.
if (e.target.parentElement != hdr) return;
activeMenu = menu;
activeMenuHdr = hdr;
menu.style.display = 'block';
}
hdr.addEventListener('mousedown', showMenu);
hdr.addEventListener('touchstart', showMenu);
}
// If there is an active menu and a down event outside, retract the menu.
for (const t of ['mousedown', 'touchstart']) {
document.addEventListener(t, (e) => {
// Note: to avoid unnecessary flicker, if the down event is inside
// the active menu header, do not retract the menu.
if (activeMenuHdr != e.target.closest('.menu-item')) {
cancelActiveMenu();
}
}, { passive: true, capture: true });
}
// If there is an active menu and an up event inside, retract the menu.
document.addEventListener('mouseup', (e) => {
if (activeMenu == e.target.closest('.submenu')) {
cancelActiveMenu();
}
}, { passive: true, capture: true });
}
function sendURL(method, url, done) {
fetch(url.toString(), {method: method})
.then((response) => { done(response.ok); })
.catch((error) => { done(false); });
}
// Initialize handlers for saving/loading configurations.
function initConfigManager() {
'use strict';
// Initialize various elements.
function elem(id) {
const result = document.getElementById(id);
if (!result) console.warn('element ' + id + ' not found');
return result;
}
const overlay = elem('dialog-overlay');
const saveDialog = elem('save-dialog');
const saveInput = elem('save-name');
const saveError = elem('save-error');
const delDialog = elem('delete-dialog');
const delPrompt = elem('delete-prompt');
const delError = elem('delete-error');
let currentDialog = null;
let currentDeleteTarget = null;
function showDialog(dialog) {
if (currentDialog != null) {
overlay.style.display = 'none';
currentDialog.style.display = 'none';
}
currentDialog = dialog;
if (dialog != null) {
overlay.style.display = 'block';
dialog.style.display = 'block';
}
}
function cancelDialog(e) {
showDialog(null);
}
// Show dialog for saving the current config.
function showSaveDialog(e) {
saveError.innerText = '';
showDialog(saveDialog);
saveInput.focus();
}
// Commit save config.
function commitSave(e) {
const name = saveInput.value;
const url = new URL(document.URL);
// Set path relative to existing path.
url.pathname = new URL('./saveconfig', document.URL).pathname;
url.searchParams.set('config', name);
saveError.innerText = '';
sendURL('POST', url, (ok) => {
if (!ok) {
saveError.innerText = 'Save failed';
} else {
showDialog(null);
location.reload(); // Reload to show updated config menu
}
});
}
function handleSaveInputKey(e) {
if (e.key === 'Enter') commitSave(e);
}
function deleteConfig(e, elem) {
e.preventDefault();
const config = elem.dataset.config;
delPrompt.innerText = 'Delete ' + config + '?';
currentDeleteTarget = elem;
showDialog(delDialog);
}
function commitDelete(e, elem) {
if (!currentDeleteTarget) return;
const config = currentDeleteTarget.dataset.config;
const url = new URL('./deleteconfig', document.URL);
url.searchParams.set('config', config);
delError.innerText = '';
sendURL('DELETE', url, (ok) => {
if (!ok) {
delError.innerText = 'Delete failed';
return;
}
showDialog(null);
// Remove menu entry for this config.
if (currentDeleteTarget && currentDeleteTarget.parentElement) {
currentDeleteTarget.parentElement.remove();
}
});
}
// Bind event on elem to fn.
function bind(event, elem, fn) {
if (elem == null) return;
elem.addEventListener(event, fn);
if (event == 'click') {
// Also enable via touch.
elem.addEventListener('touchstart', fn);
}
}
bind('click', elem('save-config'), showSaveDialog);
bind('click', elem('save-cancel'), cancelDialog);
bind('click', elem('save-confirm'), commitSave);
bind('keydown', saveInput, handleSaveInputKey);
bind('click', elem('delete-cancel'), cancelDialog);
bind('click', elem('delete-confirm'), commitDelete);
// Activate deletion button for all config entries in menu.
for (const del of Array.from(document.getElementsByClassName('menu-delete-btn'))) {
bind('click', del, (e) => {
deleteConfig(e, del);
});
}
}
function viewer(baseUrl, nodes) {
'use strict';
// Elements
const search = document.getElementById('search');
const graph0 = document.getElementById('graph0');
const svg = (graph0 == null ? null : graph0.parentElement);
const toptable = document.getElementById('toptable');
let regexpActive = false;
let selected = new Map();
let origFill = new Map();
let searchAlarm = null;
let buttonsEnabled = true;
function handleDetails(e) {
e.preventDefault();
const detailsText = document.getElementById('detailsbox');
if (detailsText != null) {
if (detailsText.style.display === 'block') {
detailsText.style.display = 'none';
} else {
detailsText.style.display = 'block';
}
}
}
function handleKey(e) {
if (e.keyCode != 13) return;
setHrefParams(window.location, function (params) {
params.set('f', search.value);
});
e.preventDefault();
}
function handleSearch() {
// Delay expensive processing so a flurry of key strokes is handled once.
if (searchAlarm != null) {
clearTimeout(searchAlarm);
}
searchAlarm = setTimeout(selectMatching, 300);
regexpActive = true;
updateButtons();
}
function selectMatching() {
searchAlarm = null;
let re = null;
if (search.value != '') {
try {
re = new RegExp(search.value);
} catch (e) {
// TODO: Display error state in search box
return;
}
}
function match(text) {
return re != null && re.test(text);
}
// drop currently selected items that do not match re.
selected.forEach(function(v, n) {
if (!match(nodes[n])) {
unselect(n, document.getElementById('node' + 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));
}
}
}
updateButtons();
}
function toggleSvgSelect(elem) {
// Walk up to immediate child of graph0
while (elem != null && elem.parentElement != graph0) {
elem = elem.parentElement;
}
if (!elem) return;
// Disable regexp mode.
regexpActive = false;
const n = nodeId(elem);
if (n < 0) return;
if (selected.has(n)) {
unselect(n, elem);
} else {
select(n, elem);
}
updateButtons();
}
function unselect(n, elem) {
if (elem == null) return;
selected.delete(n);
setBackground(elem, false);
}
function select(n, elem) {
if (elem == null) return;
selected.set(n, true);
setBackground(elem, true);
}
function nodeId(elem) {
const id = elem.id;
if (!id) return -1;
if (!id.startsWith('node')) return -1;
const n = parseInt(id.slice(4), 10);
if (isNaN(n)) return -1;
if (n < 0 || n >= nodes.length) return -1;
return n;
}
function setBackground(elem, set) {
// Handle table row highlighting.
if (elem.nodeName == 'TR') {
elem.classList.toggle('hilite', set);
return;
}
// Handle svg element highlighting.
const p = findPolygon(elem);
if (p != null) {
if (set) {
origFill.set(p, p.style.fill);
p.style.fill = '#ccccff';
} else if (origFill.has(p)) {
p.style.fill = origFill.get(p);
}
}
}
function findPolygon(elem) {
if (elem.localName == 'polygon') return elem;
for (const c of elem.children) {
const p = findPolygon(c);
if (p != null) return p;
}
return null;
}
// convert a string to a regexp that matches that string.
function quotemeta(str) {
return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1');
}
function setSampleIndexLink(id) {
const elem = document.getElementById(id);
if (elem != null) {
setHrefParams(elem, function (params) {
params.set("si", id);
});
}
}
// Update id's href to reflect current selection whenever it is
// liable to be followed.
function makeSearchLinkDynamic(id) {
const elem = document.getElementById(id);
if (elem == null) return;
// Most links copy current selection into the 'f' parameter,
// but Refine menu links are different.
let param = 'f';
if (id == 'ignore') param = 'i';
if (id == 'hide') param = 'h';
if (id == 'show') param = 's';
if (id == 'show-from') param = 'sf';
// We update on mouseenter so middle-click/right-click work properly.
elem.addEventListener('mouseenter', updater);
elem.addEventListener('touchstart', updater);
function updater() {
// 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('|');
setHrefParams(elem, function (params) {
if (re != '') {
// For focus/show/show-from, forget old parameter. For others, add to re.
if (param != 'f' && param != 's' && param != 'sf' && params.has(param)) {
const old = params.get(param);
if (old != '') {
re += '|' + old;
}
}
params.set(param, re);
} else {
params.delete(param);
}
});
}
}
function setHrefParams(elem, paramSetter) {
let url = new URL(elem.href);
url.hash = '';
// Copy params from this page's URL.
const params = url.searchParams;
for (const p of new URLSearchParams(window.location.search)) {
params.set(p[0], p[1]);
}
// Give the params to the setter to modify.
paramSetter(params);
elem.href = url.toString();
}
function handleTopClick(e) {
// Walk back until we find TR and then get the Name column (index 5)
let elem = e.target;
while (elem != null && elem.nodeName != 'TR') {
elem = elem.parentElement;
}
if (elem == null || elem.children.length < 6) return;
e.preventDefault();
const tr = elem;
const td = elem.children[5];
if (td.nodeName != 'TD') return;
const name = td.innerText;
const index = nodes.indexOf(name);
if (index < 0) return;
// Disable regexp mode.
regexpActive = false;
if (selected.has(index)) {
unselect(index, elem);
} else {
select(index, elem);
}
updateButtons();
}
function updateButtons() {
const enable = (search.value != '' || selected.size != 0);
if (buttonsEnabled == enable) return;
buttonsEnabled = enable;
for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
const link = document.getElementById(id);
if (link != null) {
link.classList.toggle('disabled', !enable);
}
}
}
// Initialize button states
updateButtons();
// Setup event handlers
initMenus();
if (svg != null) {
initPanAndZoom(svg, toggleSvgSelect);
}
if (toptable != null) {
toptable.addEventListener('mousedown', handleTopClick);
toptable.addEventListener('touchstart', handleTopClick);
}
const ids = ['topbtn', 'graphbtn', 'flamegraph', 'peek', 'list', 'disasm',
'focus', 'ignore', 'hide', 'show', 'show-from'];
ids.forEach(makeSearchLinkDynamic);
const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}];
sampleIDs.forEach(setSampleIndexLink);
// Bind action to button with specified id.
function addAction(id, action) {
const btn = document.getElementById(id);
if (btn != null) {
btn.addEventListener('click', action);
btn.addEventListener('touchstart', action);
}
}
addAction('details', handleDetails);
initConfigManager();
search.addEventListener('input', handleSearch);
search.addEventListener('keydown', handleKey);
// Give initial focus to main container so it can be scrolled using keys.
const main = document.getElementById('bodycontainer');
if (main) {
main.focus();
}
}
</script>
{{end}}
{{define "top" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
<style type="text/css">
</style>
</head>
<body>
{{template "header" .}}
<div id="top">
<table id="toptable">
<thead>
<tr>
<th id="flathdr1">Flat</th>
<th id="flathdr2">Flat%</th>
<th>Sum%</th>
<th id="cumhdr1">Cum</th>
<th id="cumhdr2">Cum%</th>
<th id="namehdr">Name</th>
<th>Inlined?</th>
</tr>
</thead>
<tbody id="rows"></tbody>
</table>
</div>
{{template "script" .}}
<script>
function makeTopTable(total, entries) {
const rows = document.getElementById('rows');
if (rows == null) return;
// Store initial index in each entry so we have stable node ids for selection.
for (let i = 0; i < entries.length; i++) {
entries[i].Id = 'node' + i;
}
// Which column are we currently sorted by and in what order?
let currentColumn = '';
let descending = false;
sortBy('Flat');
function sortBy(column) {
// Update sort criteria
if (column == currentColumn) {
descending = !descending; // Reverse order
} else {
currentColumn = column;
descending = (column != 'Name');
}
// Sort according to current criteria.
function cmp(a, b) {
const av = a[currentColumn];
const bv = b[currentColumn];
if (av < bv) return -1;
if (av > bv) return +1;
return 0;
}
entries.sort(cmp);
if (descending) entries.reverse();
function addCell(tr, val) {
const td = document.createElement('td');
td.textContent = val;
tr.appendChild(td);
}
function percent(v) {
return (v * 100.0 / total).toFixed(2) + '%';
}
// Generate rows
const fragment = document.createDocumentFragment();
let sum = 0;
for (const row of entries) {
const tr = document.createElement('tr');
tr.id = row.Id;
sum += row.Flat;
addCell(tr, row.FlatFormat);
addCell(tr, percent(row.Flat));
addCell(tr, percent(sum));
addCell(tr, row.CumFormat);
addCell(tr, percent(row.Cum));
addCell(tr, row.Name);
addCell(tr, row.InlineLabel);
fragment.appendChild(tr);
}
rows.textContent = ''; // Remove old rows
rows.appendChild(fragment);
}
// Make different column headers trigger sorting.
function bindSort(id, column) {
const hdr = document.getElementById(id);
if (hdr == null) return;
const fn = function() { sortBy(column) };
hdr.addEventListener('click', fn);
hdr.addEventListener('touch', fn);
}
bindSort('flathdr1', 'Flat');
bindSort('flathdr2', 'Flat');
bindSort('cumhdr1', 'Cum');
bindSort('cumhdr2', 'Cum');
bindSort('namehdr', 'Name');
}
viewer(new URL(window.location.href), {{.Nodes}});
makeTopTable({{.Total}}, {{.Top}});
</script>
</body>
</html>
{{end}}
{{define "sourcelisting" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "weblistcss" .}}
{{template "weblistjs" .}}
</head>
<body>
{{template "header" .}}
<div id="content" class="source">
{{.HTMLBody}}
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), null);</script>
</body>
</html>
{{end}}
{{define "plaintext" -}}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
</head>
<body>
{{template "header" .}}
<div id="content">
<pre>
{{.TextBody}}
</pre>
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), null);</script>
</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: 15px 0 35px;
}
</style>
</head>
<body>
{{template "header" .}}
<div id="bodycontainer">
<div id="flamegraphdetails" class="flamegraph-details"></div>
<div class="flamegraph-content">
<div id="chart"></div>
</div>
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
<script>{{template "d3script" .}}</script>
<script>{{template "d3flamegraphscript" .}}</script>
<script>
var data = {{.FlameGraph}};
var width = document.getElementById('chart').clientWidth;
var flameGraph = d3.flamegraph()
.width(width)
.cellHeight(18)
.minFrameSize(1)
.transitionDuration(750)
.transitionEase(d3.easeCubic)
.inverted(true)
.sort(true)
.title('')
.tooltip(false)
.details(document.getElementById('flamegraphdetails'));
// <full name> (percentage, value)
flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')');
(function(flameGraph) {
var oldColorMapper = flameGraph.color();
function colorMapper(d) {
// Hack to force default color mapper to use 'warm' color scheme by not passing libtype
const { data, highlight } = d;
return oldColorMapper({ data: { n: data.n }, highlight });
}
flameGraph.color(colorMapper);
}(flameGraph));
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 search = document.getElementById('search');
var searchAlarm = null;
function selectMatching() {
searchAlarm = null;
if (search.value != '') {
flameGraph.search(search.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);
}
search.addEventListener('input', handleSearch);
</script>
</body>
</html>
{{end}}
`))
}