feat: add pprof

This commit is contained in:
langhuihui
2024-12-16 20:06:39 +08:00
parent c1616740ec
commit b3a3e37429
220 changed files with 36494 additions and 56 deletions

View File

@@ -0,0 +1,279 @@
* {
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;
margin-bottom: 4px;
}
.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;
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;
}
.menu-name a {
text-decoration: none;
color: #212121;
}
.submenu {
display: none;
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;
white-space: nowrap;
}
.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;
}
.help {
padding-left: 1em;
}
{{/* 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%);
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;
}
/* stacking order */
.boxtext { z-index: 2; } /* flame graph box text */
#current-details { z-index: 2; } /* flame graph current box info */
#detailsbox { z-index: 3; } /* profile details */
.submenu { z-index: 4; }
.dialog { z-index: 5; }

View File

@@ -0,0 +1,714 @@
// 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);
});
}
}
// 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
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;
// 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');
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);
}
})
// 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);
}
}
}
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);
} else {
select(n);
}
updateButtons();
}
function unselect(n) {
if (setNodeHighlight(n, false)) selected.delete(n);
}
function select(n, elem) {
if (setNodeHighlight(n, true)) selected.set(n, 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;
}
// 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 true;
}
// 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);
}
}
return true;
}
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;
}
function setSampleIndexLink(si) {
const elem = document.getElementById('sampletype-' + si);
if (elem != null) {
setHrefParams(elem, function (params) {
params.set("si", si);
});
}
}
// 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(getSelection().keys()).map(key => pprofQuoteMeta(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 != '' || getSelection().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();
}
}
// convert a string to a regexp that matches exactly that string.
function pprofQuoteMeta(str) {
return '^' + str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1') + '$';
}

View File

@@ -0,0 +1,7 @@
#graph {
cursor: grab;
}
#graph:active {
cursor: grabbing;
}

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "graph_css" .}}
</head>
<body>
{{template "header" .}}
<div id="graph">
{{.HTMLBody}}
</div>
{{template "script" .}}
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<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="sampletype-{{.}}">{{.}}</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 id="download" class="menu-item">
<div class="menu-name">
<a href="./download">Download</a>
</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>
{{if .DocURL}}
<div class="menu-item">
<div class="help menu-name"><a title="Profile documentation" href="{{.DocURL}}" target="_blank">Help&nbsp;</a></div>
</div>
{{end}}
</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>

View File

@@ -0,0 +1,18 @@
<!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>

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{if not .Standalone}}{{template "css" .}}{{end}}
{{template "weblistcss" .}}
{{template "weblistjs" .}}
</head>
<body>{{"\n" -}}
{{/* emit different header in standalone mode */ -}}
{{if .Standalone}}{{"\n" -}}
<div class="legend">{{"" -}}
{{range $i, $e := .Legend -}}
{{if $i}}<br>{{"\n"}}{{end}}{{. -}}
{{end}}<br>Total: {{.Listing.Total -}}
</div>{{"" -}}
{{else -}}
{{template "header" .}}
<div id="content" class="source">{{"" -}}
{{end -}}
{{range .Listing.Files -}}
{{range .Funcs -}}
<h2>{{.Name}}</h2>{{"" -}}
<p class="filename">{{.File}}</p>{{"\n" -}}
<pre onClick="pprof_toggle_asm(event)">{{"\n" -}}
{{printf " Total: %10s %10s (flat, cum) %s" .Flat .Cumulative .Percent -}}
{{range .Lines -}}{{"\n" -}}
{{/* source line */ -}}
<span class=line>{{printf " %6d" .Line}}</span>{{" " -}}
<span class={{.HTMLClass}}>
{{- printf " %10s %10s %8s %s " .Flat .Cumulative "" .SrcLine -}}
</span>{{"" -}}
{{if .Instructions -}}
{{/* instructions for this source line */ -}}
<span class=asm>{{"" -}}
{{range .Instructions -}}
{{/* separate when we hit a new basic block */ -}}
{{if .NewBlock -}}{{printf " %8s %28s\n" "" "⋮"}}{{end -}}
{{/* inlined calls leading to this instruction */ -}}
{{range .InlinedCalls -}}
{{printf " %8s %10s %10s %8s " "" "" "" "" -}}
<span class=inlinesrc>{{.SrcLine}}</span>{{" " -}}
<span class=unimportant>{{.FileBase}}:{{.Line}}</span>{{"\n" -}}
{{end -}}
{{if not .Synthetic -}}
{{/* disassembled instruction */ -}}
{{printf " %8s %10s %10s %8x: %s " "" .Flat .Cumulative .Address .Disasm -}}
<span class=unimportant>{{.FileLine}}</span>{{"\n" -}}
{{end -}}
{{end -}}
</span>{{"" -}}
{{end -}}
{{/* end of line */ -}}
{{end}}{{"\n" -}}
</pre>{{"\n" -}}
{{/* end of function */ -}}
{{end -}}
{{/* end of file */ -}}
{{end -}}
{{if not .Standalone}}{{"\n " -}}
</div>{{"\n" -}}
{{template "script" .}}{{"\n" -}}
<script>viewer(new URL(window.location.href), null);</script>{{"" -}}
{{end}}
</body>
</html>

View File

@@ -0,0 +1,89 @@
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 */
}
/* Holder for current frame details. */
#current-details {
position: relative;
background: #eee; /* Light grey gives better contrast with boxes */
font-size: 12pt;
padding: 0 4px;
width: 100%;
}
/* Shows details of frame that is under the mouse */
#current-details-left {
float: left;
max-width: 60%;
white-space: nowrap;
overflow-x: hidden;
}
#current-details-right {
float: right;
max-width: 40%;
white-space: nowrap;
overflow-x: hidden;
}
/* Background of a single flame-graph frame */
.boxbg {
border-width: 0px;
position: absolute;
overflow: hidden;
box-sizing: border-box;
background: #d8d8d8;
}
.positive { position: absolute; background: #caa; }
.negative { position: absolute; background: #aca; }
/* Not-inlined frames are visually separated from their caller. */
.not-inlined {
border-top: 1px solid black;
}
/* 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; }
/* Gap left between callers and callees */
.separator {
position: absolute;
text-align: center;
font-size: 12pt;
font-weight: bold;
}
/* 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;
}

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>{{.Title}}</title>
{{template "css" .}}
{{template "stacks_css"}}
</head>
<body>
{{template "header" .}}
<div id="current-details">
<div id="current-details-left"></div>
<div id="current-details-right"> </div>
</div>
<div id="stack-holder">
<div id="stack-chart"></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>
pprofUnitDefs = {{.UnitDefs}};
stackViewer({{.Stacks}}, {{.Nodes}});
</script>
</body>
</html>

View File

@@ -0,0 +1,638 @@
// stackViewer displays a flame-graph like view (extended to show callers).
// stacks - report.StackSet
// nodes - List of names for each source in report.StackSet
function stackViewer(stacks, nodes) {
'use strict';
// Constants used in rendering.
const ROW = 20;
const PADDING = 2;
const MIN_WIDTH = 4;
const MIN_TEXT_WIDTH = 16;
const TEXT_MARGIN = 2;
const FONT_SIZE = 12;
const MIN_FONT_SIZE = 8;
// Fields
let pivots = []; // Indices of currently selected data.Sources entries.
let matches = new Set(); // Indices of sources that match search
let elems = new Map(); // Mapping from source index to display elements
let displayList = []; // List of boxes to display.
let actionMenuOn = false; // Is action menu visible?
let actionTarget = null; // Box on which action menu is operating.
let diff = false; // Are we displaying a diff?
let shown = 0; // How many profile values are being displayed?
for (const stack of stacks.Stacks) {
if (stack.Value < 0) {
diff = true;
break;
}
}
// Setup to allow measuring text width.
const textSizer = document.createElement('canvas');
textSizer.id = 'textsizer';
const textContext = textSizer.getContext('2d');
// Get DOM elements.
const chart = find('stack-chart');
const search = find('search');
const actions = find('action-menu');
const actionTitle = find('action-title');
const leftDetailBox = find('current-details-left');
const rightDetailBox = find('current-details-right');
window.addEventListener('resize', render);
window.addEventListener('popstate', render);
search.addEventListener('keydown', handleSearchKey);
// Withdraw action menu when clicking outside, or when item selected.
document.addEventListener('mousedown', (e) => {
if (!actions.contains(e.target)) {
hideActionMenu();
}
});
actions.addEventListener('click', hideActionMenu);
// Initialize menus and other general UI elements.
viewer(new URL(window.location.href), nodes, {
hiliter: (n, on) => { return hilite(n, on); },
current: () => {
let r = new Map();
if (pivots.length == 1 && pivots[0] == 0) {
// Not pivoting
} else {
for (let p of pivots) {
r.set(p, true);
}
}
return r;
}});
render();
clearDetails();
// Helper functions follow:
// hilite changes the highlighting of elements corresponding to specified src.
function hilite(src, on) {
if (on) {
matches.add(src);
} else {
matches.delete(src);
}
toggleClass(src, 'hilite', on);
return true;
}
// Display action menu (triggered by right-click on a frame)
function showActionMenu(e, box) {
if (box.src == 0) return; // No action menu for root
e.preventDefault(); // Disable browser context menu
const src = stacks.Sources[box.src];
actionTitle.innerText = src.Display[src.Display.length-1];
const menu = actions;
menu.style.display = 'block';
// Compute position so menu stays visible and near the mouse.
const x = Math.min(e.clientX - 10, document.body.clientWidth - menu.clientWidth);
const y = Math.min(e.clientY - 10, document.body.clientHeight - menu.clientHeight);
menu.style.left = x + 'px';
menu.style.top = y + 'px';
// Set menu links to operate on clicked box.
setHrefParam('action-source', 'f', box.src);
setHrefParam('action-source-tab', 'f', box.src);
setHrefParam('action-focus', 'f', box.src);
setHrefParam('action-ignore', 'i', box.src);
setHrefParam('action-hide', 'h', box.src);
setHrefParam('action-showfrom', 'sf', box.src);
toggleClass(box.src, 'hilite2', true);
actionTarget = box;
actionMenuOn = true;
}
function hideActionMenu() {
actions.style.display = 'none';
actionMenuOn = false;
if (actionTarget != null) {
toggleClass(actionTarget.src, 'hilite2', false);
}
}
// setHrefParam updates the specified parameter in the href of an <a>
// element to make it operate on the specified src.
function setHrefParam(id, param, src) {
const elem = document.getElementById(id);
if (!elem) return;
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]);
}
// Update params to include src.
// When `pprof` is invoked with `-lines`, FullName will be suffixed with `:<line>`,
// which we need to remove.
let v = pprofQuoteMeta(stacks.Sources[src].FullName.replace(/:[0-9]+$/, ''));
if (param != 'f' && param != 'sf') { // old f,sf values are overwritten
// Add new source to current parameter value.
const old = params.get(param);
if (old && old != '') {
v += '|' + old;
}
}
params.set(param, v);
elem.href = url.toString();
}
// Capture Enter key in the search box to make it pivot instead of focus.
function handleSearchKey(e) {
if (e.key != 'Enter') return;
e.stopImmediatePropagation(); // Disable normal enter key handling
const val = search.value;
try {
new RegExp(search.value);
} catch (error) {
return; // TODO: Display error state in search box
}
switchPivots(val);
}
function switchPivots(regexp) {
// Switch URL without hitting the server.
const url = new URL(document.URL);
if (regexp === '' || regexp === '^$') {
url.searchParams.delete('p'); // Not pivoting
} else {
url.searchParams.set('p', regexp);
}
history.pushState('', '', url.toString()); // Makes back-button work
matches = new Set();
search.value = '';
render();
}
function handleEnter(box, div) {
if (actionMenuOn) return;
const src = stacks.Sources[box.src];
div.title = details(box) + ' │ ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
leftDetailBox.innerText = src.FullName + (src.Inlined ? " (inlined)" : "");
let timing = summary(box.sumpos, box.sumneg);
if (box.self != 0) {
timing = "self " + unitText(box.self) + " │ " + timing;
}
rightDetailBox.innerText = timing;
// Highlight all boxes that have the same source as box.
toggleClass(box.src, 'hilite2', true);
}
function handleLeave(box) {
if (actionMenuOn) return;
clearDetails();
toggleClass(box.src, 'hilite2', false);
}
function clearDetails() {
leftDetailBox.innerText = '';
rightDetailBox.innerText = percentText(shown);
}
// Return list of sources that match the regexp given by the 'p' URL parameter.
function urlPivots() {
const pivots = [];
const params = (new URL(document.URL)).searchParams;
const val = params.get('p');
if (val !== null && val != '') {
try {
const re = new RegExp(val);
for (let i = 0; i < stacks.Sources.length; i++) {
const src = stacks.Sources[i];
if (re.test(src.UniqueName) || re.test(src.FileName)) {
pivots.push(i);
}
}
} catch (error) {}
}
if (pivots.length == 0) {
pivots.push(0);
}
return pivots;
}
// render re-generates the stack display.
function render() {
pivots = urlPivots();
// Get places where pivots occur.
let places = [];
for (let pivot of pivots) {
const src = stacks.Sources[pivot];
for (let p of src.Places) {
places.push(p);
}
}
const width = chart.clientWidth;
elems.clear();
actionTarget = null;
const [pos, neg] = totalValue(places);
const total = pos + neg;
const xscale = (width-2*PADDING) / total; // Converts from profile value to X pixels
const x = PADDING;
const y = 0;
// Show summary for pivots if we are actually pivoting.
const showPivotSummary = !(pivots.length == 1 && pivots[0] == 0);
shown = pos + neg;
displayList.length = 0;
renderStacks(0, xscale, x, y, places, +1); // Callees
renderStacks(0, xscale, x, y-ROW, places, -1); // Callers (ROW left for separator)
display(xscale, pos, neg, displayList, showPivotSummary);
}
// renderStacks creates boxes with top-left at x,y with children drawn as
// nested stacks (below or above based on the sign of direction).
// Returns the largest y coordinate filled.
function renderStacks(depth, xscale, x, y, places, direction) {
// Example: suppose we are drawing the following stacks:
// a->b->c
// a->b->d
// a->e->f
// After rendering a, we will call renderStacks, with places pointing to
// the preceding stacks.
//
// We first group all places with the same leading entry. In this example
// we get [b->c, b->d] and [e->f]. We render the two groups side-by-side.
const groups = partitionPlaces(places);
for (const g of groups) {
renderGroup(depth, xscale, x, y, g, direction);
x += groupWidth(xscale, g);
}
}
// Some of the types used below:
//
// // Group represents a displayed (sub)tree.
// interface Group {
// name: string; // Full name of source
// src: number; // Index in stacks.Sources
// self: number; // Contribution as leaf (may be < 0 for diffs)
// sumpos: number; // Sum of |self| of positive nodes in tree (>= 0)
// sumneg: number; // Sum of |self| of negative nodes in tree (>= 0)
// places: Place[]; // Stack slots that contributed to this group
// }
//
// // Box is a rendered item.
// interface Box {
// x: number; // X coordinate of top-left
// y: number; // Y coordinate of top-left
// width: number; // Width of box to display
// src: number; // Index in stacks.Sources
// sumpos: number; // From corresponding Group
// sumneg: number; // From corresponding Group
// self: number; // From corresponding Group
// };
function groupWidth(xscale, g) {
return xscale * (g.sumpos + g.sumneg);
}
function renderGroup(depth, xscale, x, y, g, direction) {
// Skip if not wide enough.
const width = groupWidth(xscale, g);
if (width < MIN_WIDTH) return;
// Draw the box for g.src (except for selected element in upwards direction
// since that duplicates the box we added in downwards direction).
if (depth != 0 || direction > 0) {
const box = {
x: x,
y: y,
width: width,
src: g.src,
sumpos: g.sumpos,
sumneg: g.sumneg,
self: g.self,
};
displayList.push(box);
if (direction > 0) {
// Leave gap on left hand side to indicate self contribution.
x += xscale*Math.abs(g.self);
}
}
y += direction * ROW;
// Find child or parent stacks.
const next = [];
for (const place of g.places) {
const stack = stacks.Stacks[place.Stack];
const nextSlot = place.Pos + direction;
if (nextSlot >= 0 && nextSlot < stack.Sources.length) {
next.push({Stack: place.Stack, Pos: nextSlot});
}
}
renderStacks(depth+1, xscale, x, y, next, direction);
}
// partitionPlaces partitions a set of places into groups where each group
// contains places with the same source. If a stack occurs multiple times
// in places, only the outer-most occurrence is kept.
function partitionPlaces(places) {
// Find outer-most slot per stack (used later to elide duplicate stacks).
const stackMap = new Map(); // Map from stack index to outer-most slot#
for (const place of places) {
const prevSlot = stackMap.get(place.Stack);
if (prevSlot && prevSlot <= place.Pos) {
// We already have a higher slot in this stack.
} else {
stackMap.set(place.Stack, place.Pos);
}
}
// Now partition the stacks.
const groups = []; // Array of Group {name, src, sum, self, places}
const groupMap = new Map(); // Map from Source to Group
for (const place of places) {
if (stackMap.get(place.Stack) != place.Pos) {
continue;
}
const stack = stacks.Stacks[place.Stack];
const src = stack.Sources[place.Pos];
let group = groupMap.get(src);
if (!group) {
const name = stacks.Sources[src].FullName;
group = {name: name, src: src, sumpos: 0, sumneg: 0, self: 0, places: []};
groupMap.set(src, group);
groups.push(group);
}
if (stack.Value < 0) {
group.sumneg += -stack.Value;
} else {
group.sumpos += stack.Value;
}
group.self += (place.Pos == stack.Sources.length-1) ? stack.Value : 0;
group.places.push(place);
}
// Order by decreasing cost (makes it easier to spot heavy functions).
// Though alphabetical ordering is a potential alternative that will make
// profile comparisons easier.
groups.sort(function(a, b) {
return (b.sumpos + b.sumneg) - (a.sumpos + a.sumneg);
});
return groups;
}
function display(xscale, posTotal, negTotal, list, showPivotSummary) {
// Sort boxes so that text selection follows a predictable order.
list.sort(function(a, b) {
if (a.y != b.y) return a.y - b.y;
return a.x - b.x;
});
// Adjust Y coordinates so that zero is at top.
let adjust = (list.length > 0) ? list[0].y : 0;
const divs = [];
for (const box of list) {
box.y -= adjust;
divs.push(drawBox(xscale, box));
}
if (showPivotSummary) {
divs.push(drawSep(-adjust, posTotal, negTotal));
}
const h = (list.length > 0 ? list[list.length-1].y : 0) + 4*ROW;
chart.style.height = h+'px';
chart.replaceChildren(...divs);
}
function drawBox(xscale, box) {
const srcIndex = box.src;
const src = stacks.Sources[srcIndex];
function makeRect(cl, x, y, w, h) {
const r = document.createElement('div');
r.style.left = x+'px';
r.style.top = y+'px';
r.style.width = w+'px';
r.style.height = h+'px';
r.classList.add(cl);
return r;
}
// Background
const w = box.width - 1; // Leave 1px gap
const r = makeRect('boxbg', box.x, box.y, w, ROW);
if (!diff) r.style.background = makeColor(src.Color);
addElem(srcIndex, r);
if (!src.Inlined) {
r.classList.add('not-inlined');
}
// Positive/negative indicator for diff mode.
if (diff) {
const delta = box.sumpos - box.sumneg;
const partWidth = xscale * Math.abs(delta);
if (partWidth >= MIN_WIDTH) {
r.appendChild(makeRect((delta < 0 ? 'negative' : 'positive'),
0, 0, partWidth, ROW-1));
}
}
// Label
if (box.width >= MIN_TEXT_WIDTH) {
const t = document.createElement('div');
t.classList.add('boxtext');
fitText(t, box.width-2*TEXT_MARGIN, src.Display);
r.appendChild(t);
}
onClick(r, () => { switchPivots(pprofQuoteMeta(src.UniqueName)); });
r.addEventListener('mouseenter', () => { handleEnter(box, r); });
r.addEventListener('mouseleave', () => { handleLeave(box); });
r.addEventListener('contextmenu', (e) => { showActionMenu(e, box); });
return r;
}
// Handle clicks, but only if the mouse did not move during the click.
function onClick(target, handler) {
// Disable click if mouse moves more than threshold pixels since mousedown.
const threshold = 3;
let [x, y] = [-1, -1];
target.addEventListener('mousedown', (e) => {
[x, y] = [e.clientX, e.clientY];
});
target.addEventListener('click', (e) => {
if (Math.abs(e.clientX - x) <= threshold &&
Math.abs(e.clientY - y) <= threshold) {
handler();
}
});
}
function drawSep(y, posTotal, negTotal) {
const m = document.createElement('div');
m.innerText = summary(posTotal, negTotal);
m.style.top = (y-ROW) + 'px';
m.style.left = PADDING + 'px';
m.style.width = (chart.clientWidth - PADDING*2) + 'px';
m.classList.add('separator');
return m;
}
// addElem registers an element that belongs to the specified src.
function addElem(src, elem) {
let list = elems.get(src);
if (!list) {
list = [];
elems.set(src, list);
}
list.push(elem);
elem.classList.toggle('hilite', matches.has(src));
}
// Adds or removes cl from classList of all elements for the specified source.
function toggleClass(src, cl, value) {
const list = elems.get(src);
if (list) {
for (const elem of list) {
elem.classList.toggle(cl, value);
}
}
}
// fitText sets text and font-size clipped to the specified width w.
function fitText(t, avail, textList) {
// Find first entry in textList that fits.
let width = avail;
textContext.font = FONT_SIZE + 'pt Arial';
for (let i = 0; i < textList.length; i++) {
let text = textList[i];
width = textContext.measureText(text).width;
if (width <= avail) {
t.innerText = text;
return;
}
}
// Try to fit by dropping font size.
let text = textList[textList.length-1];
const fs = Math.max(MIN_FONT_SIZE, FONT_SIZE * (avail / width));
t.style.fontSize = fs + 'pt';
t.innerText = text;
}
// totalValue returns the positive and negative sums of the Values of stacks
// listed in places.
function totalValue(places) {
const seen = new Set();
let pos = 0;
let neg = 0;
for (const place of places) {
if (seen.has(place.Stack)) continue; // Do not double-count stacks
seen.add(place.Stack);
const stack = stacks.Stacks[place.Stack];
if (stack.Value < 0) {
neg += -stack.Value;
} else {
pos += stack.Value;
}
}
return [pos, neg];
}
function summary(pos, neg) {
// Examples:
// 6s (10%)
// 12s (20%) 🠆 18s (30%)
return diff ? diffText(neg, pos) : percentText(pos);
}
function details(box) {
// Examples:
// 6s (10%)
// 6s (10%) │ self 3s (5%)
// 6s (10%) │ 12s (20%) 🠆 18s (30%)
let result = percentText(box.sumpos - box.sumneg);
if (box.self != 0) {
result += " │ self " + unitText(box.self);
}
if (diff && box.sumpos > 0 && box.sumneg > 0) {
result += " │ " + diffText(box.sumneg, box.sumpos);
}
return result;
}
// diffText returns text that displays from and to alongside their percentages.
// E.g., 9s (45%) 🠆 10s (50%)
function diffText(from, to) {
return percentText(from) + " 🠆 " + percentText(to);
}
// percentText returns text that displays v in appropriate units alongside its
// percentange.
function percentText(v) {
function percent(v, total) {
return Number(((100.0 * v) / total).toFixed(1)) + '%';
}
return unitText(v) + " (" + percent(v, stacks.Total) + ")";
}
// unitText returns a formatted string to display for value.
function unitText(value) {
return pprofUnitText(value*stacks.Scale, stacks.Unit);
}
function find(name) {
const elem = document.getElementById(name);
if (!elem) {
throw 'element not found: ' + name
}
return elem;
}
function makeColor(index) {
// Rotate hue around a circle. Multiple by phi to spread things
// out better. Use 50% saturation to make subdued colors, and
// 80% lightness to have good contrast with black foreground text.
const PHI = 1.618033988;
const hue = (index+1) * PHI * 2 * Math.PI; // +1 to avoid 0
const hsl = `hsl(${hue}rad 50% 80%)`;
return hsl;
}
}
// pprofUnitText returns a formatted string to display for value in the specified unit.
function pprofUnitText(value, unit) {
const sign = (value < 0) ? "-" : "";
let v = Math.abs(value);
// Rescale to appropriate display unit.
let list = null;
for (const def of pprofUnitDefs) {
if (def.DefaultUnit.CanonicalName == unit) {
list = def.Units;
v *= def.DefaultUnit.Factor;
break;
}
}
if (list) {
// Stop just before entry that is too large.
for (let i = 0; i < list.length; i++) {
if (i == list.length-1 || list[i+1].Factor > v) {
v /= list[i].Factor;
unit = list[i].CanonicalName;
break;
}
}
}
return sign + Number(v.toFixed(2)) + unit;
}

View File

@@ -0,0 +1,114 @@
<!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>