mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-12-24 13:48:04 +08:00
feat: add pprof
This commit is contained in:
279
plugin/debug/pkg/internal/driver/html/common.css
Normal file
279
plugin/debug/pkg/internal/driver/html/common.css
Normal 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; }
|
||||
714
plugin/debug/pkg/internal/driver/html/common.js
Normal file
714
plugin/debug/pkg/internal/driver/html/common.js
Normal 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') + '$';
|
||||
}
|
||||
7
plugin/debug/pkg/internal/driver/html/graph.css
Normal file
7
plugin/debug/pkg/internal/driver/html/graph.css
Normal file
@@ -0,0 +1,7 @@
|
||||
#graph {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
#graph:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
17
plugin/debug/pkg/internal/driver/html/graph.html
Normal file
17
plugin/debug/pkg/internal/driver/html/graph.html
Normal 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>
|
||||
119
plugin/debug/pkg/internal/driver/html/header.html
Normal file
119
plugin/debug/pkg/internal/driver/html/header.html
Normal 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 ⤇</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>
|
||||
18
plugin/debug/pkg/internal/driver/html/plaintext.html
Normal file
18
plugin/debug/pkg/internal/driver/html/plaintext.html
Normal 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>
|
||||
72
plugin/debug/pkg/internal/driver/html/source.html
Normal file
72
plugin/debug/pkg/internal/driver/html/source.html
Normal 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>
|
||||
89
plugin/debug/pkg/internal/driver/html/stacks.css
Normal file
89
plugin/debug/pkg/internal/driver/html/stacks.css
Normal 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;
|
||||
}
|
||||
36
plugin/debug/pkg/internal/driver/html/stacks.html
Normal file
36
plugin/debug/pkg/internal/driver/html/stacks.html
Normal 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>
|
||||
638
plugin/debug/pkg/internal/driver/html/stacks.js
Normal file
638
plugin/debug/pkg/internal/driver/html/stacks.js
Normal 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;
|
||||
}
|
||||
114
plugin/debug/pkg/internal/driver/html/top.html
Normal file
114
plugin/debug/pkg/internal/driver/html/top.html
Normal 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>
|
||||
Reference in New Issue
Block a user