mirror of
https://github.com/dutchcoders/transfer.sh.git
synced 2020-11-18 19:53:40 -08:00
1160 lines
30 KiB
Go
1160 lines
30 KiB
Go
// Copyright 2017 Google Inc. All Rights Reserved.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package driver
|
|
|
|
import "html/template"
|
|
|
|
import "github.com/google/pprof/third_party/d3"
|
|
import "github.com/google/pprof/third_party/d3flamegraph"
|
|
|
|
// addTemplates adds a set of template definitions to templates.
|
|
func addTemplates(templates *template.Template) {
|
|
template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`))
|
|
template.Must(templates.Parse(`{{define "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`))
|
|
template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`))
|
|
template.Must(templates.Parse(`
|
|
{{define "css"}}
|
|
<style type="text/css">
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html, body {
|
|
height: 100%;
|
|
}
|
|
body {
|
|
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
a {
|
|
color: #2a66d9;
|
|
}
|
|
.header {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 44px;
|
|
min-height: 44px;
|
|
background-color: #eee;
|
|
color: #212121;
|
|
padding: 0 1rem;
|
|
}
|
|
.header > div {
|
|
margin: 0 0.125em;
|
|
}
|
|
.header .title h1 {
|
|
font-size: 1.75em;
|
|
margin-right: 1rem;
|
|
}
|
|
.header .title a {
|
|
color: #212121;
|
|
text-decoration: none;
|
|
}
|
|
.header .title a:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.header .description {
|
|
width: 100%;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
@media screen and (max-width: 799px) {
|
|
.header input {
|
|
display: none;
|
|
}
|
|
}
|
|
#detailsbox {
|
|
display: none;
|
|
z-index: 1;
|
|
position: fixed;
|
|
top: 40px;
|
|
right: 20px;
|
|
background-color: #ffffff;
|
|
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
|
line-height: 24px;
|
|
padding: 1em;
|
|
text-align: left;
|
|
}
|
|
.header input {
|
|
background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' style='pointer-events:none;display:block;width:100%25;height:100%25;fill:#757575'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61.0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3C/svg%3E") no-repeat 4px center/20px 20px;
|
|
border: 1px solid #d1d2d3;
|
|
border-radius: 2px 0 0 2px;
|
|
padding: 0.25em;
|
|
padding-left: 28px;
|
|
margin-left: 1em;
|
|
font-family: 'Roboto', 'Noto', sans-serif;
|
|
font-size: 1em;
|
|
line-height: 24px;
|
|
color: #212121;
|
|
}
|
|
.downArrow {
|
|
border-top: .36em solid #ccc;
|
|
border-left: .36em solid transparent;
|
|
border-right: .36em solid transparent;
|
|
margin-bottom: .05em;
|
|
margin-left: .5em;
|
|
transition: border-top-color 200ms;
|
|
}
|
|
.menu-item {
|
|
height: 100%;
|
|
text-transform: uppercase;
|
|
font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
|
position: relative;
|
|
}
|
|
.menu-item .menu-name:hover {
|
|
opacity: 0.75;
|
|
}
|
|
.menu-item .menu-name:hover .downArrow {
|
|
border-top-color: #666;
|
|
}
|
|
.menu-name {
|
|
height: 100%;
|
|
padding: 0 0.5em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.submenu {
|
|
display: none;
|
|
z-index: 1;
|
|
margin-top: -4px;
|
|
min-width: 10em;
|
|
position: absolute;
|
|
left: 0px;
|
|
background-color: white;
|
|
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
|
font-size: 100%;
|
|
text-transform: none;
|
|
}
|
|
.menu-item, .submenu {
|
|
user-select: none;
|
|
-moz-user-select: none;
|
|
-ms-user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
.submenu hr {
|
|
border: 0;
|
|
border-top: 2px solid #eee;
|
|
}
|
|
.submenu a {
|
|
display: block;
|
|
padding: .5em 1em;
|
|
text-decoration: none;
|
|
}
|
|
.submenu a:hover, .submenu a.active {
|
|
color: white;
|
|
background-color: #6b82d6;
|
|
}
|
|
.submenu a.disabled {
|
|
color: gray;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#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 {
|
|
background-color: #ddd;
|
|
text-align: right;
|
|
padding: .3em .5em;
|
|
}
|
|
table tr td {
|
|
padding: .3em .5em;
|
|
text-align: right;
|
|
}
|
|
#top table tr th:nth-child(6),
|
|
#top table tr th:nth-child(7),
|
|
#top table tr td:nth-child(6),
|
|
#top table tr td:nth-child(7) {
|
|
text-align: left;
|
|
}
|
|
#top table tr td:nth-child(6) {
|
|
width: 100%;
|
|
text-overflow: ellipsis;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
}
|
|
#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr {
|
|
cursor: ns-resize;
|
|
}
|
|
.hilite {
|
|
background-color: #ebf5fb;
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
{{end}}
|
|
|
|
{{define "header"}}
|
|
<div class="header">
|
|
<div class="title">
|
|
<h1><a href="./">pprof</a></h1>
|
|
</div>
|
|
|
|
<div id="view" class="menu-item">
|
|
<div class="menu-name">
|
|
View
|
|
<i class="downArrow"></i>
|
|
</div>
|
|
<div class="submenu">
|
|
<a title="{{.Help.top}}" href="./top" id="topbtn">Top</a>
|
|
<a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a>
|
|
<a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a>
|
|
<a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a>
|
|
<a title="{{.Help.list}}" href="./source" id="list">Source</a>
|
|
<a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a>
|
|
</div>
|
|
</div>
|
|
|
|
{{$sampleLen := len .SampleTypes}}
|
|
{{if gt $sampleLen 1}}
|
|
<div id="sample" class="menu-item">
|
|
<div class="menu-name">
|
|
Sample
|
|
<i class="downArrow"></i>
|
|
</div>
|
|
<div class="submenu">
|
|
{{range .SampleTypes}}
|
|
<a href="?si={{.}}" id="{{.}}">{{.}}</a>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
{{end}}
|
|
|
|
<div id="refine" class="menu-item">
|
|
<div class="menu-name">
|
|
Refine
|
|
<i class="downArrow"></i>
|
|
</div>
|
|
<div class="submenu">
|
|
<a title="{{.Help.focus}}" href="?" id="focus">Focus</a>
|
|
<a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a>
|
|
<a title="{{.Help.hide}}" href="?" id="hide">Hide</a>
|
|
<a title="{{.Help.show}}" href="?" id="show">Show</a>
|
|
<a title="{{.Help.show_from}}" href="?" id="show-from">Show from</a>
|
|
<hr>
|
|
<a title="{{.Help.reset}}" href="?">Reset</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40>
|
|
</div>
|
|
|
|
<div class="description">
|
|
<a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a>
|
|
<div id="detailsbox">
|
|
{{range .Legend}}<div>{{.}}</div>{{end}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div>
|
|
{{end}}
|
|
|
|
{{define "graph" -}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
{{template "css" .}}
|
|
</head>
|
|
<body>
|
|
{{template "header" .}}
|
|
<div id="graph">
|
|
{{.HTMLBody}}
|
|
</div>
|
|
{{template "script" .}}
|
|
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "script"}}
|
|
<script>
|
|
// Make svg pannable and zoomable.
|
|
// Call clickHandler(t) if a click event is caught by the pan event handlers.
|
|
function initPanAndZoom(svg, clickHandler) {
|
|
'use strict';
|
|
|
|
// Current mouse/touch handling mode
|
|
const IDLE = 0;
|
|
const MOUSEPAN = 1;
|
|
const TOUCHPAN = 2;
|
|
const TOUCHZOOM = 3;
|
|
let mode = IDLE;
|
|
|
|
// State needed to implement zooming.
|
|
let currentScale = 1.0;
|
|
const initWidth = svg.viewBox.baseVal.width;
|
|
const initHeight = svg.viewBox.baseVal.height;
|
|
|
|
// State needed to implement panning.
|
|
let panLastX = 0; // Last event X coordinate
|
|
let panLastY = 0; // Last event Y coordinate
|
|
let moved = false; // Have we seen significant movement
|
|
let touchid = null; // Current touch identifier
|
|
|
|
// State needed for pinch zooming
|
|
let touchid2 = null; // Second id for pinch zooming
|
|
let initGap = 1.0; // Starting gap between two touches
|
|
let initScale = 1.0; // currentScale when pinch zoom started
|
|
let centerPoint = null; // Center point for scaling
|
|
|
|
// Convert event coordinates to svg coordinates.
|
|
function toSvg(x, y) {
|
|
const p = svg.createSVGPoint();
|
|
p.x = x;
|
|
p.y = y;
|
|
let m = svg.getCTM();
|
|
if (m == null) m = svg.getScreenCTM(); // Firefox workaround.
|
|
return p.matrixTransform(m.inverse());
|
|
}
|
|
|
|
// Change the scaling for the svg to s, keeping the point denoted
|
|
// by u (in svg coordinates]) fixed at the same screen location.
|
|
function rescale(s, u) {
|
|
// Limit to a good range.
|
|
if (s < 0.2) s = 0.2;
|
|
if (s > 10.0) s = 10.0;
|
|
|
|
currentScale = s;
|
|
|
|
// svg.viewBox defines the visible portion of the user coordinate
|
|
// system. So to magnify by s, divide the visible portion by s,
|
|
// which will then be stretched to fit the viewport.
|
|
const vb = svg.viewBox;
|
|
const w1 = vb.baseVal.width;
|
|
const w2 = initWidth / s;
|
|
const h1 = vb.baseVal.height;
|
|
const h2 = initHeight / s;
|
|
vb.baseVal.width = w2;
|
|
vb.baseVal.height = h2;
|
|
|
|
// We also want to adjust vb.baseVal.x so that u.x remains at same
|
|
// screen X coordinate. In other words, want to change it from x1 to x2
|
|
// so that:
|
|
// (u.x - x1) / w1 = (u.x - x2) / w2
|
|
// Simplifying that, we get
|
|
// (u.x - x1) * (w2 / w1) = u.x - x2
|
|
// x2 = u.x - (u.x - x1) * (w2 / w1)
|
|
vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1);
|
|
vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1);
|
|
}
|
|
|
|
function handleWheel(e) {
|
|
if (e.deltaY == 0) return;
|
|
// Change scale factor by 1.1 or 1/1.1
|
|
rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)),
|
|
toSvg(e.offsetX, e.offsetY));
|
|
}
|
|
|
|
function setMode(m) {
|
|
mode = m;
|
|
touchid = null;
|
|
touchid2 = null;
|
|
}
|
|
|
|
function panStart(x, y) {
|
|
moved = false;
|
|
panLastX = x;
|
|
panLastY = y;
|
|
}
|
|
|
|
function panMove(x, y) {
|
|
let dx = x - panLastX;
|
|
let dy = y - panLastY;
|
|
if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves
|
|
|
|
moved = true;
|
|
panLastX = x;
|
|
panLastY = y;
|
|
|
|
// Firefox workaround: get dimensions from parentNode.
|
|
const swidth = svg.clientWidth || svg.parentNode.clientWidth;
|
|
const sheight = svg.clientHeight || svg.parentNode.clientHeight;
|
|
|
|
// Convert deltas from screen space to svg space.
|
|
dx *= (svg.viewBox.baseVal.width / swidth);
|
|
dy *= (svg.viewBox.baseVal.height / sheight);
|
|
|
|
svg.viewBox.baseVal.x -= dx;
|
|
svg.viewBox.baseVal.y -= dy;
|
|
}
|
|
|
|
function handleScanStart(e) {
|
|
if (e.button != 0) return; // Do not catch right-clicks etc.
|
|
setMode(MOUSEPAN);
|
|
panStart(e.clientX, e.clientY);
|
|
e.preventDefault();
|
|
svg.addEventListener('mousemove', handleScanMove);
|
|
}
|
|
|
|
function handleScanMove(e) {
|
|
if (e.buttons == 0) {
|
|
// Missed an end event, perhaps because mouse moved outside window.
|
|
setMode(IDLE);
|
|
svg.removeEventListener('mousemove', handleScanMove);
|
|
return;
|
|
}
|
|
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
|
|
}
|
|
|
|
function handleScanEnd(e) {
|
|
if (mode == MOUSEPAN) panMove(e.clientX, e.clientY);
|
|
setMode(IDLE);
|
|
svg.removeEventListener('mousemove', handleScanMove);
|
|
if (!moved) clickHandler(e.target);
|
|
}
|
|
|
|
// Find touch object with specified identifier.
|
|
function findTouch(tlist, id) {
|
|
for (const t of tlist) {
|
|
if (t.identifier == id) return t;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Return distance between two touch points
|
|
function touchGap(t1, t2) {
|
|
const dx = t1.clientX - t2.clientX;
|
|
const dy = t1.clientY - t2.clientY;
|
|
return Math.hypot(dx, dy);
|
|
}
|
|
|
|
function handleTouchStart(e) {
|
|
if (mode == IDLE && e.changedTouches.length == 1) {
|
|
// Start touch based panning
|
|
const t = e.changedTouches[0];
|
|
setMode(TOUCHPAN);
|
|
touchid = t.identifier;
|
|
panStart(t.clientX, t.clientY);
|
|
e.preventDefault();
|
|
} else if (mode == TOUCHPAN && e.touches.length == 2) {
|
|
// Start pinch zooming
|
|
setMode(TOUCHZOOM);
|
|
const t1 = e.touches[0];
|
|
const t2 = e.touches[1];
|
|
touchid = t1.identifier;
|
|
touchid2 = t2.identifier;
|
|
initScale = currentScale;
|
|
initGap = touchGap(t1, t2);
|
|
centerPoint = toSvg((t1.clientX + t2.clientX) / 2,
|
|
(t1.clientY + t2.clientY) / 2);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
function handleTouchMove(e) {
|
|
if (mode == TOUCHPAN) {
|
|
const t = findTouch(e.changedTouches, touchid);
|
|
if (t == null) return;
|
|
if (e.touches.length != 1) {
|
|
setMode(IDLE);
|
|
return;
|
|
}
|
|
panMove(t.clientX, t.clientY);
|
|
e.preventDefault();
|
|
} else if (mode == TOUCHZOOM) {
|
|
// Get two touches; new gap; rescale to ratio.
|
|
const t1 = findTouch(e.touches, touchid);
|
|
const t2 = findTouch(e.touches, touchid2);
|
|
if (t1 == null || t2 == null) return;
|
|
const gap = touchGap(t1, t2);
|
|
rescale(initScale * gap / initGap, centerPoint);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
function handleTouchEnd(e) {
|
|
if (mode == TOUCHPAN) {
|
|
const t = findTouch(e.changedTouches, touchid);
|
|
if (t == null) return;
|
|
panMove(t.clientX, t.clientY);
|
|
setMode(IDLE);
|
|
e.preventDefault();
|
|
if (!moved) clickHandler(t.target);
|
|
} else if (mode == TOUCHZOOM) {
|
|
setMode(IDLE);
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
svg.addEventListener('mousedown', handleScanStart);
|
|
svg.addEventListener('mouseup', handleScanEnd);
|
|
svg.addEventListener('touchstart', handleTouchStart);
|
|
svg.addEventListener('touchmove', handleTouchMove);
|
|
svg.addEventListener('touchend', handleTouchEnd);
|
|
svg.addEventListener('wheel', handleWheel, true);
|
|
}
|
|
|
|
function initMenus() {
|
|
'use strict';
|
|
|
|
let activeMenu = null;
|
|
let activeMenuHdr = null;
|
|
|
|
function cancelActiveMenu() {
|
|
if (activeMenu == null) return;
|
|
activeMenu.style.display = 'none';
|
|
activeMenu = null;
|
|
activeMenuHdr = null;
|
|
}
|
|
|
|
// Set click handlers on every menu header.
|
|
for (const menu of document.getElementsByClassName('submenu')) {
|
|
const hdr = menu.parentElement;
|
|
if (hdr == null) return;
|
|
if (hdr.classList.contains('disabled')) return;
|
|
function showMenu(e) {
|
|
// menu is a child of hdr, so this event can fire for clicks
|
|
// inside menu. Ignore such clicks.
|
|
if (e.target.parentElement != hdr) return;
|
|
activeMenu = menu;
|
|
activeMenuHdr = hdr;
|
|
menu.style.display = 'block';
|
|
}
|
|
hdr.addEventListener('mousedown', showMenu);
|
|
hdr.addEventListener('touchstart', showMenu);
|
|
}
|
|
|
|
// If there is an active menu and a down event outside, retract the menu.
|
|
for (const t of ['mousedown', 'touchstart']) {
|
|
document.addEventListener(t, (e) => {
|
|
// Note: to avoid unnecessary flicker, if the down event is inside
|
|
// the active menu header, do not retract the menu.
|
|
if (activeMenuHdr != e.target.closest('.menu-item')) {
|
|
cancelActiveMenu();
|
|
}
|
|
}, { passive: true, capture: true });
|
|
}
|
|
|
|
// If there is an active menu and an up event inside, retract the menu.
|
|
document.addEventListener('mouseup', (e) => {
|
|
if (activeMenu == e.target.closest('.submenu')) {
|
|
cancelActiveMenu();
|
|
}
|
|
}, { passive: true, capture: true });
|
|
}
|
|
|
|
function viewer(baseUrl, nodes) {
|
|
'use strict';
|
|
|
|
// Elements
|
|
const search = document.getElementById('search');
|
|
const graph0 = document.getElementById('graph0');
|
|
const svg = (graph0 == null ? null : graph0.parentElement);
|
|
const toptable = document.getElementById('toptable');
|
|
|
|
let regexpActive = false;
|
|
let selected = new Map();
|
|
let origFill = new Map();
|
|
let searchAlarm = null;
|
|
let buttonsEnabled = true;
|
|
|
|
function handleDetails(e) {
|
|
e.preventDefault();
|
|
const detailsText = document.getElementById('detailsbox');
|
|
if (detailsText != null) {
|
|
if (detailsText.style.display === 'block') {
|
|
detailsText.style.display = 'none';
|
|
} else {
|
|
detailsText.style.display = 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleKey(e) {
|
|
if (e.keyCode != 13) return;
|
|
window.location.href =
|
|
updateUrl(new URL(window.location.href), 'f');
|
|
e.preventDefault();
|
|
}
|
|
|
|
function handleSearch() {
|
|
// Delay expensive processing so a flurry of key strokes is handled once.
|
|
if (searchAlarm != null) {
|
|
clearTimeout(searchAlarm);
|
|
}
|
|
searchAlarm = setTimeout(selectMatching, 300);
|
|
|
|
regexpActive = true;
|
|
updateButtons();
|
|
}
|
|
|
|
function selectMatching() {
|
|
searchAlarm = null;
|
|
let re = null;
|
|
if (search.value != '') {
|
|
try {
|
|
re = new RegExp(search.value);
|
|
} catch (e) {
|
|
// TODO: Display error state in search box
|
|
return;
|
|
}
|
|
}
|
|
|
|
function match(text) {
|
|
return re != null && re.test(text);
|
|
}
|
|
|
|
// drop currently selected items that do not match re.
|
|
selected.forEach(function(v, n) {
|
|
if (!match(nodes[n])) {
|
|
unselect(n, document.getElementById('node' + n));
|
|
}
|
|
})
|
|
|
|
// add matching items that are not currently selected.
|
|
for (let n = 0; n < nodes.length; n++) {
|
|
if (!selected.has(n) && match(nodes[n])) {
|
|
select(n, document.getElementById('node' + n));
|
|
}
|
|
}
|
|
|
|
updateButtons();
|
|
}
|
|
|
|
function toggleSvgSelect(elem) {
|
|
// Walk up to immediate child of graph0
|
|
while (elem != null && elem.parentElement != graph0) {
|
|
elem = elem.parentElement;
|
|
}
|
|
if (!elem) return;
|
|
|
|
// Disable regexp mode.
|
|
regexpActive = false;
|
|
|
|
const n = nodeId(elem);
|
|
if (n < 0) return;
|
|
if (selected.has(n)) {
|
|
unselect(n, elem);
|
|
} else {
|
|
select(n, elem);
|
|
}
|
|
updateButtons();
|
|
}
|
|
|
|
function unselect(n, elem) {
|
|
if (elem == null) return;
|
|
selected.delete(n);
|
|
setBackground(elem, false);
|
|
}
|
|
|
|
function select(n, elem) {
|
|
if (elem == null) return;
|
|
selected.set(n, true);
|
|
setBackground(elem, true);
|
|
}
|
|
|
|
function nodeId(elem) {
|
|
const id = elem.id;
|
|
if (!id) return -1;
|
|
if (!id.startsWith('node')) return -1;
|
|
const n = parseInt(id.slice(4), 10);
|
|
if (isNaN(n)) return -1;
|
|
if (n < 0 || n >= nodes.length) return -1;
|
|
return n;
|
|
}
|
|
|
|
function setBackground(elem, set) {
|
|
// Handle table row highlighting.
|
|
if (elem.nodeName == 'TR') {
|
|
elem.classList.toggle('hilite', set);
|
|
return;
|
|
}
|
|
|
|
// Handle svg element highlighting.
|
|
const p = findPolygon(elem);
|
|
if (p != null) {
|
|
if (set) {
|
|
origFill.set(p, p.style.fill);
|
|
p.style.fill = '#ccccff';
|
|
} else if (origFill.has(p)) {
|
|
p.style.fill = origFill.get(p);
|
|
}
|
|
}
|
|
}
|
|
|
|
function findPolygon(elem) {
|
|
if (elem.localName == 'polygon') return elem;
|
|
for (const c of elem.children) {
|
|
const p = findPolygon(c);
|
|
if (p != null) return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// convert a string to a regexp that matches that string.
|
|
function quotemeta(str) {
|
|
return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1');
|
|
}
|
|
|
|
function setSampleIndexLink(id) {
|
|
const elem = document.getElementById(id);
|
|
if (elem != null) {
|
|
setHrefParams(elem, function (params) {
|
|
params.set("si", id);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update id's href to reflect current selection whenever it is
|
|
// liable to be followed.
|
|
function makeSearchLinkDynamic(id) {
|
|
const elem = document.getElementById(id);
|
|
if (elem == null) return;
|
|
|
|
// Most links copy current selection into the 'f' parameter,
|
|
// but Refine menu links are different.
|
|
let param = 'f';
|
|
if (id == 'ignore') param = 'i';
|
|
if (id == 'hide') param = 'h';
|
|
if (id == 'show') param = 's';
|
|
if (id == 'show-from') param = 'sf';
|
|
|
|
// We update on mouseenter so middle-click/right-click work properly.
|
|
elem.addEventListener('mouseenter', updater);
|
|
elem.addEventListener('touchstart', updater);
|
|
|
|
function updater() {
|
|
// The selection can be in one of two modes: regexp-based or
|
|
// list-based. Construct regular expression depending on mode.
|
|
let re = regexpActive
|
|
? search.value
|
|
: Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|');
|
|
|
|
setHrefParams(elem, function (params) {
|
|
if (re != '') {
|
|
// For focus/show/show-from, forget old parameter. For others, add to re.
|
|
if (param != 'f' && param != 's' && param != 'sf' && params.has(param)) {
|
|
const old = params.get(param);
|
|
if (old != '') {
|
|
re += '|' + old;
|
|
}
|
|
}
|
|
params.set(param, re);
|
|
} else {
|
|
params.delete(param);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function setHrefParams(elem, paramSetter) {
|
|
let url = new URL(elem.href);
|
|
url.hash = '';
|
|
|
|
// Copy params from this page's URL.
|
|
const params = url.searchParams;
|
|
for (const p of new URLSearchParams(window.location.search)) {
|
|
params.set(p[0], p[1]);
|
|
}
|
|
|
|
// Give the params to the setter to modify.
|
|
paramSetter(params);
|
|
|
|
elem.href = url.toString();
|
|
}
|
|
|
|
function handleTopClick(e) {
|
|
// Walk back until we find TR and then get the Name column (index 5)
|
|
let elem = e.target;
|
|
while (elem != null && elem.nodeName != 'TR') {
|
|
elem = elem.parentElement;
|
|
}
|
|
if (elem == null || elem.children.length < 6) return;
|
|
|
|
e.preventDefault();
|
|
const tr = elem;
|
|
const td = elem.children[5];
|
|
if (td.nodeName != 'TD') return;
|
|
const name = td.innerText;
|
|
const index = nodes.indexOf(name);
|
|
if (index < 0) return;
|
|
|
|
// Disable regexp mode.
|
|
regexpActive = false;
|
|
|
|
if (selected.has(index)) {
|
|
unselect(index, elem);
|
|
} else {
|
|
select(index, elem);
|
|
}
|
|
updateButtons();
|
|
}
|
|
|
|
function updateButtons() {
|
|
const enable = (search.value != '' || selected.size != 0);
|
|
if (buttonsEnabled == enable) return;
|
|
buttonsEnabled = enable;
|
|
for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) {
|
|
const link = document.getElementById(id);
|
|
if (link != null) {
|
|
link.classList.toggle('disabled', !enable);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialize button states
|
|
updateButtons();
|
|
|
|
// Setup event handlers
|
|
initMenus();
|
|
if (svg != null) {
|
|
initPanAndZoom(svg, toggleSvgSelect);
|
|
}
|
|
if (toptable != null) {
|
|
toptable.addEventListener('mousedown', handleTopClick);
|
|
toptable.addEventListener('touchstart', handleTopClick);
|
|
}
|
|
|
|
const ids = ['topbtn', 'graphbtn', '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);
|
|
|
|
search.addEventListener('input', handleSearch);
|
|
search.addEventListener('keydown', handleKey);
|
|
|
|
// Give initial focus to main container so it can be scrolled using keys.
|
|
const main = document.getElementById('bodycontainer');
|
|
if (main) {
|
|
main.focus();
|
|
}
|
|
}
|
|
</script>
|
|
{{end}}
|
|
|
|
{{define "top" -}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
{{template "css" .}}
|
|
<style type="text/css">
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{{template "header" .}}
|
|
<div id="top">
|
|
<table id="toptable">
|
|
<thead>
|
|
<tr>
|
|
<th id="flathdr1">Flat</th>
|
|
<th id="flathdr2">Flat%</th>
|
|
<th>Sum%</th>
|
|
<th id="cumhdr1">Cum</th>
|
|
<th id="cumhdr2">Cum%</th>
|
|
<th id="namehdr">Name</th>
|
|
<th>Inlined?</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="rows"></tbody>
|
|
</table>
|
|
</div>
|
|
{{template "script" .}}
|
|
<script>
|
|
function makeTopTable(total, entries) {
|
|
const rows = document.getElementById('rows');
|
|
if (rows == null) return;
|
|
|
|
// Store initial index in each entry so we have stable node ids for selection.
|
|
for (let i = 0; i < entries.length; i++) {
|
|
entries[i].Id = 'node' + i;
|
|
}
|
|
|
|
// Which column are we currently sorted by and in what order?
|
|
let currentColumn = '';
|
|
let descending = false;
|
|
sortBy('Flat');
|
|
|
|
function sortBy(column) {
|
|
// Update sort criteria
|
|
if (column == currentColumn) {
|
|
descending = !descending; // Reverse order
|
|
} else {
|
|
currentColumn = column;
|
|
descending = (column != 'Name');
|
|
}
|
|
|
|
// Sort according to current criteria.
|
|
function cmp(a, b) {
|
|
const av = a[currentColumn];
|
|
const bv = b[currentColumn];
|
|
if (av < bv) return -1;
|
|
if (av > bv) return +1;
|
|
return 0;
|
|
}
|
|
entries.sort(cmp);
|
|
if (descending) entries.reverse();
|
|
|
|
function addCell(tr, val) {
|
|
const td = document.createElement('td');
|
|
td.textContent = val;
|
|
tr.appendChild(td);
|
|
}
|
|
|
|
function percent(v) {
|
|
return (v * 100.0 / total).toFixed(2) + '%';
|
|
}
|
|
|
|
// Generate rows
|
|
const fragment = document.createDocumentFragment();
|
|
let sum = 0;
|
|
for (const row of entries) {
|
|
const tr = document.createElement('tr');
|
|
tr.id = row.Id;
|
|
sum += row.Flat;
|
|
addCell(tr, row.FlatFormat);
|
|
addCell(tr, percent(row.Flat));
|
|
addCell(tr, percent(sum));
|
|
addCell(tr, row.CumFormat);
|
|
addCell(tr, percent(row.Cum));
|
|
addCell(tr, row.Name);
|
|
addCell(tr, row.InlineLabel);
|
|
fragment.appendChild(tr);
|
|
}
|
|
|
|
rows.textContent = ''; // Remove old rows
|
|
rows.appendChild(fragment);
|
|
}
|
|
|
|
// Make different column headers trigger sorting.
|
|
function bindSort(id, column) {
|
|
const hdr = document.getElementById(id);
|
|
if (hdr == null) return;
|
|
const fn = function() { sortBy(column) };
|
|
hdr.addEventListener('click', fn);
|
|
hdr.addEventListener('touch', fn);
|
|
}
|
|
bindSort('flathdr1', 'Flat');
|
|
bindSort('flathdr2', 'Flat');
|
|
bindSort('cumhdr1', 'Cum');
|
|
bindSort('cumhdr2', 'Cum');
|
|
bindSort('namehdr', 'Name');
|
|
}
|
|
|
|
viewer(new URL(window.location.href), {{.Nodes}});
|
|
makeTopTable({{.Total}}, {{.Top}});
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "sourcelisting" -}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
{{template "css" .}}
|
|
{{template "weblistcss" .}}
|
|
{{template "weblistjs" .}}
|
|
</head>
|
|
<body>
|
|
{{template "header" .}}
|
|
<div id="content" class="source">
|
|
{{.HTMLBody}}
|
|
</div>
|
|
{{template "script" .}}
|
|
<script>viewer(new URL(window.location.href), null);</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "plaintext" -}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
{{template "css" .}}
|
|
</head>
|
|
<body>
|
|
{{template "header" .}}
|
|
<div id="content">
|
|
<pre>
|
|
{{.TextBody}}
|
|
</pre>
|
|
</div>
|
|
{{template "script" .}}
|
|
<script>viewer(new URL(window.location.href), null);</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
|
|
{{define "flamegraph" -}}
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>{{.Title}}</title>
|
|
{{template "css" .}}
|
|
<style type="text/css">{{template "d3flamegraphcss" .}}</style>
|
|
<style type="text/css">
|
|
.flamegraph-content {
|
|
width: 90%;
|
|
min-width: 80%;
|
|
margin-left: 5%;
|
|
}
|
|
.flamegraph-details {
|
|
height: 1.2em;
|
|
width: 90%;
|
|
min-width: 90%;
|
|
margin-left: 5%;
|
|
padding: 15px 0 35px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
{{template "header" .}}
|
|
<div id="bodycontainer">
|
|
<div id="flamegraphdetails" class="flamegraph-details"></div>
|
|
<div class="flamegraph-content">
|
|
<div id="chart"></div>
|
|
</div>
|
|
</div>
|
|
{{template "script" .}}
|
|
<script>viewer(new URL(window.location.href), {{.Nodes}});</script>
|
|
<script>{{template "d3script" .}}</script>
|
|
<script>{{template "d3flamegraphscript" .}}</script>
|
|
<script>
|
|
var data = {{.FlameGraph}};
|
|
|
|
var width = document.getElementById('chart').clientWidth;
|
|
|
|
var flameGraph = d3.flamegraph()
|
|
.width(width)
|
|
.cellHeight(18)
|
|
.minFrameSize(1)
|
|
.transitionDuration(750)
|
|
.transitionEase(d3.easeCubic)
|
|
.inverted(true)
|
|
.title('')
|
|
.tooltip(false)
|
|
.details(document.getElementById('flamegraphdetails'));
|
|
|
|
// <full name> (percentage, value)
|
|
flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')');
|
|
|
|
(function(flameGraph) {
|
|
var oldColorMapper = flameGraph.color();
|
|
function colorMapper(d) {
|
|
// Hack to force default color mapper to use 'warm' color scheme by not passing libtype
|
|
const { data, highlight } = d;
|
|
return oldColorMapper({ data: { n: data.n }, highlight });
|
|
}
|
|
|
|
flameGraph.color(colorMapper);
|
|
}(flameGraph));
|
|
|
|
d3.select('#chart')
|
|
.datum(data)
|
|
.call(flameGraph);
|
|
|
|
function clear() {
|
|
flameGraph.clear();
|
|
}
|
|
|
|
function resetZoom() {
|
|
flameGraph.resetZoom();
|
|
}
|
|
|
|
window.addEventListener('resize', function() {
|
|
var width = document.getElementById('chart').clientWidth;
|
|
var graphs = document.getElementsByClassName('d3-flame-graph');
|
|
if (graphs.length > 0) {
|
|
graphs[0].setAttribute('width', width);
|
|
}
|
|
flameGraph.width(width);
|
|
flameGraph.resetZoom();
|
|
}, true);
|
|
|
|
var search = document.getElementById('search');
|
|
var searchAlarm = null;
|
|
|
|
function selectMatching() {
|
|
searchAlarm = null;
|
|
|
|
if (search.value != '') {
|
|
flameGraph.search(search.value);
|
|
} else {
|
|
flameGraph.clear();
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
// Delay expensive processing so a flurry of key strokes is handled once.
|
|
if (searchAlarm != null) {
|
|
clearTimeout(searchAlarm);
|
|
}
|
|
searchAlarm = setTimeout(selectMatching, 300);
|
|
}
|
|
|
|
search.addEventListener('input', handleSearch);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
{{end}}
|
|
`))
|
|
}
|