diff options
Diffstat (limited to 'src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go')
| -rw-r--r-- | src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go | 1200 |
1 files changed, 681 insertions, 519 deletions
diff --git a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go index 48f0fa1cfa..5d2821cd42 100644 --- a/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go +++ b/src/cmd/vendor/github.com/google/pprof/internal/driver/webhtml.go @@ -16,208 +16,267 @@ package driver import "html/template" +import "github.com/google/pprof/third_party/d3" +import "github.com/google/pprof/third_party/d3tip" +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 "d3tipscript"}}` + d3tip.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"> -html { - height: 100%; - min-height: 100%; - margin: 0px; +* { + margin: 0; + padding: 0; + box-sizing: border-box; } -body { - margin: 0px; - width: 100%; +html, body { height: 100%; - min-height: 100%; - overflow: hidden; } -#graphcontainer { +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; - height: 100%; - min-height: 100%; - width: 100%; - min-width: 100%; - margin: 0px; } -#graph { - flex: 1 1 auto; - overflow: hidden; +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; } -svg { +.header .title a:hover { + text-decoration: underline; +} +.header .description { width: 100%; - height: auto; + text-align: right; + white-space: nowrap; } -button { - margin-top: 5px; - margin-bottom: 5px; +@media screen and (max-width: 799px) { + .header input { + display: none; + } } -#detailtext { +#detailsbox { display: none; + z-index: 1; position: fixed; - top: 20px; - right: 10px; + top: 40px; + right: 20px; background-color: #ffffff; - min-width: 160px; - border: 1px solid #888; - box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); - z-index: 1; -} -#closedetails { - float: right; - margin: 2px; + box-shadow: 0 1px 5px rgba(0,0,0,.3); + line-height: 24px; + padding: 1em; + text-align: left; } -#home { - font-size: 14pt; - padding-left: 0.5em; - padding-right: 0.5em; - float: right; +.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; } -.menubar { - display: inline-block; - background-color: #f8f8f8; - border: 1px solid #ccc; - width: 100%; +.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-header { +.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; - display: inline-block; - padding: 2px 2px; - font-size: 14pt; } -.menu { +.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; - position: absolute; - background-color: #f8f8f8; - border: 1px solid #888; - box-shadow: 4px 4px 4px 0px rgba(0,0,0,0.2); z-index: 1; - margin-top: 2px; + margin-top: -4px; + min-width: 10em; + position: absolute; left: 0px; - min-width: 5em; + background-color: white; + box-shadow: 0 1px 5px rgba(0,0,0,.3); + font-size: 100%; + text-transform: none; } -.menu-header, .menu { - cursor: default; +.menu-item, .submenu { user-select: none; -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; } -.menu hr { - background-color: #fff; - margin-top: 0px; - margin-bottom: 0px; +.submenu hr { + border: 0; + border-top: 2px solid #eee; } -.menu a, .menu button { +.submenu a { display: block; - width: 100%; - margin: 0px; - padding: 2px 0px 2px 0px; - text-align: left; + padding: .5em 1em; text-decoration: none; - color: #000; - background-color: #f8f8f8; - font-size: 12pt; - border: none; } -.menu-header:hover { - background-color: #ccc; +.submenu a:hover, .submenu a.active { + color: white; + background-color: #6b82d6; } -.menu a:hover, .menu button:hover { - background-color: #ccc; -} -.menu a.disabled { +.submenu a.disabled { color: gray; pointer-events: none; } -#searchbox { - margin-left: 10pt; + +#content { + overflow-y: scroll; + padding: 1em; +} +#top { + overflow-y: scroll; +} +#graph { + overflow: hidden; } -#bodycontainer { +#graph svg { width: 100%; - height: 100%; - max-height: 100%; - overflow: scroll; - padding-top: 5px; + height: auto; + padding: 10px; } -#toptable { +#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'; } -#toptable tr th { - border-bottom: 1px solid black; +table tr th { + background-color: #ddd; text-align: right; - padding-left: 1em; - padding-top: 0.2em; - padding-bottom: 0.2em; + padding: .3em .5em; } -#toptable tr td { - padding-left: 1em; - font: monospace; +table tr td { + padding: .3em .5em; text-align: right; - white-space: nowrap; - cursor: default; } -#toptable tr th:nth-child(6), -#toptable tr th:nth-child(7), -#toptable tr td:nth-child(6), -#toptable tr td:nth-child(7) { +#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; } -#toptable tr td:nth-child(6) { - max-width: 30em; // Truncate very long names +#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: #ccf; + background-color: #ebf5fb; + font-weight: bold; } </style> {{end}} {{define "header"}} -<div id="detailtext"> -<button id="closedetails">Close</button> -{{range .Legend}}<div>{{.}}</div>{{end}} -</div> +<div class="header"> + <div class="title"> + <h1><a href="/">pprof</a></h1> + </div> -<div class="menubar"> + <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> -<div class="menu-header"> -View -<div class="menu"> -<a title="{{.Help.top}}" href="/top" id="topbtn">Top</a> -<a title="{{.Help.graph}}" href="/" id="graphbtn">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> -<hr> -<button title="{{.Help.details}}" id="details">Details</button> -</div> -</div> + <div id="refine" class="menu-item"> + <div class="menu-name"> + Refine + <i class="downArrow"></i> + </div> + <div class="submenu"> + <a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> + <a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> + <a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> + <a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> + <hr> + <a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> + </div> + </div> -<div class="menu-header"> -Refine -<div class="menu"> -<a title="{{.Help.focus}}" href="{{.BaseURL}}" id="focus">Focus</a> -<a title="{{.Help.ignore}}" href="{{.BaseURL}}" id="ignore">Ignore</a> -<a title="{{.Help.hide}}" href="{{.BaseURL}}" id="hide">Hide</a> -<a title="{{.Help.show}}" href="{{.BaseURL}}" id="show">Show</a> -<hr> -<a title="{{.Help.reset}}" href="{{.BaseURL}}">Reset</a> -</div> -</div> + <div> + <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> + </div> -<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> - -<span id="home">{{.Title}}</span> - -</div> <!-- menubar --> + <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}} @@ -226,21 +285,17 @@ Refine <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} </head> <body> - -{{template "header" .}} -<div id="graphcontainer"> -<div id="graph"> -{{.HTMLBody}} -</div> - -</div> -{{template "script" .}} -<script>viewer({{.BaseURL}}, {{.Nodes}})</script> + {{template "header" .}} + <div id="graph"> + {{.HTMLBody}} + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, {{.Nodes}});</script> </body> </html> {{end}} @@ -253,58 +308,58 @@ 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 + 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 + 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 + 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 + 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()) + 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 + if (s < 0.2) s = 0.2; + if (s > 10.0) s = 10.0; - currentScale = s + 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 + 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 @@ -313,154 +368,154 @@ function initPanAndZoom(svg, clickHandler) { // 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) + 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 + 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)) + toSvg(e.offsetX, e.offsetY)); } function setMode(m) { - mode = m - touchid = null - touchid2 = null + mode = m; + touchid = null; + touchid2 = null; } function panStart(x, y) { - moved = false - panLastX = x - panLastY = 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 + 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 + 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 + 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) + dx *= (svg.viewBox.baseVal.width / swidth); + dy *= (svg.viewBox.baseVal.height / sheight); - svg.viewBox.baseVal.x -= dx - svg.viewBox.baseVal.y -= dy + 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) + 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 + setMode(IDLE); + svg.removeEventListener('mousemove', handleScanMove); + return; } - if (mode == MOUSEPAN) panMove(e.clientX, e.clientY) + 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) + 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 + if (t.identifier == id) return t; } - return null + return null; } - // Return distance between two touch points + // 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) + 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() + 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) + 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() + (t1.clientY + t2.clientY) / 2); + e.preventDefault(); } } function handleTouchMove(e) { if (mode == TOUCHPAN) { - const t = findTouch(e.changedTouches, touchid) - if (t == null) return + const t = findTouch(e.changedTouches, touchid); + if (t == null) return; if (e.touches.length != 1) { - setMode(IDLE) - return + setMode(IDLE); + return; } - panMove(t.clientX, t.clientY) - e.preventDefault() + 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() + 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) + 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() + 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) + 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() { @@ -471,41 +526,42 @@ function initMenus() { function cancelActiveMenu() { if (activeMenu == null) return; - activeMenu.style.display = "none"; + activeMenu.style.display = 'none'; activeMenu = null; activeMenuHdr = null; } // Set click handlers on every menu header. - for (const menu of document.getElementsByClassName("menu")) { + 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 != hdr) return; + if (e.target.parentElement != hdr) return; activeMenu = menu; activeMenuHdr = hdr; - menu.style.display = "block"; + menu.style.display = 'block'; } - hdr.addEventListener("mousedown", showMenu); - hdr.addEventListener("touchstart", showMenu); + 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"]) { + 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-header")) { + 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(".menu")) { + document.addEventListener('mouseup', (e) => { + if (activeMenu == e.target.closest('.submenu')) { cancelActiveMenu(); } }, { passive: true, capture: true }); @@ -515,282 +571,283 @@ function viewer(baseUrl, nodes) { 'use strict'; // Elements - const search = document.getElementById("searchbox") - const graph0 = document.getElementById("graph0") - const svg = (graph0 == null ? null : graph0.parentElement) - const toptable = document.getElementById("toptable") + 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 + let regexpActive = false; + let selected = new Map(); + let origFill = new Map(); + let searchAlarm = null; + let buttonsEnabled = true; - function handleDetails() { - const detailsText = document.getElementById("detailtext") - if (detailsText != null) detailsText.style.display = "block" - } - - function handleCloseDetails() { - const detailsText = document.getElementById("detailtext") - if (detailsText != null) detailsText.style.display = "none" + 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 + if (e.keyCode != 13) return; window.location.href = - updateUrl(new URL({{.BaseURL}}, window.location.href), "f") - e.preventDefault() + updateUrl(new URL({{.BaseURL}}, 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) + clearTimeout(searchAlarm); } - searchAlarm = setTimeout(selectMatching, 300) + searchAlarm = setTimeout(selectMatching, 300); - regexpActive = true - updateButtons() + regexpActive = true; + updateButtons(); } function selectMatching() { - searchAlarm = null - let re = null - if (search.value != "") { + searchAlarm = null; + let re = null; + if (search.value != '') { try { - re = new RegExp(search.value) + re = new RegExp(search.value); } catch (e) { // TODO: Display error state in search box - return + return; } } function match(text) { - return re != null && re.test(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)) + 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)) + select(n, document.getElementById('node' + n)); } } - updateButtons() + updateButtons(); } function toggleSvgSelect(elem) { // Walk up to immediate child of graph0 while (elem != null && elem.parentElement != graph0) { - elem = elem.parentElement + elem = elem.parentElement; } - if (!elem) return + if (!elem) return; // Disable regexp mode. - regexpActive = false + regexpActive = false; - const n = nodeId(elem) - if (n < 0) return + const n = nodeId(elem); + if (n < 0) return; if (selected.has(n)) { - unselect(n, elem) + unselect(n, elem); } else { - select(n, elem) + select(n, elem); } - updateButtons() + updateButtons(); } function unselect(n, elem) { - if (elem == null) return - selected.delete(n) - setBackground(elem, false) + 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) + 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 + 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 + if (elem.nodeName == 'TR') { + elem.classList.toggle('hilite', set); + return; } // Handle svg element highlighting. - const p = findPolygon(elem) + const p = findPolygon(elem); if (p != null) { if (set) { - origFill.set(p, p.style.fill) - p.style.fill = "#ccccff" + origFill.set(p, p.style.fill); + p.style.fill = '#ccccff'; } else if (origFill.has(p)) { - p.style.fill = origFill.get(p) + p.style.fill = origFill.get(p); } } } function findPolygon(elem) { - if (elem.localName == "polygon") return elem + if (elem.localName == 'polygon') return elem; for (const c of elem.children) { - const p = findPolygon(c) - if (p != null) return p + const p = findPolygon(c); + if (p != null) return p; } - return null + return null; } // convert a string to a regexp that matches that string. function quotemeta(str) { - return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1') + return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1'); } // Update id's href to reflect current selection whenever it is // liable to be followed. function makeLinkDynamic(id) { - const elem = document.getElementById(id) - if (elem == null) return + const elem = document.getElementById(id); + if (elem == null) return; - // Most links copy current selection into the "f" parameter, + // 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" + let param = 'f'; + if (id == 'ignore') param = 'i'; + if (id == 'hide') param = 'h'; + if (id == 'show') param = 's'; // We update on mouseenter so middle-click/right-click work properly. - elem.addEventListener("mouseenter", updater) - elem.addEventListener("touchstart", updater) + elem.addEventListener('mouseenter', updater); + elem.addEventListener('touchstart', updater); function updater() { - elem.href = updateUrl(new URL(elem.href), param) + elem.href = updateUrl(new URL(elem.href), param); } } // Update URL to reflect current selection. function updateUrl(url, param) { - url.hash = "" + url.hash = ''; // The selection can be in one of two modes: regexp-based or // list-based. Construct regular expression depending on mode. let re = regexpActive - ? search.value - : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join("|") + ? search.value + : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|'); // Copy params from this page's URL. - const params = url.searchParams + const params = url.searchParams; for (const p of new URLSearchParams(window.location.search)) { - params.set(p[0], p[1]) + params.set(p[0], p[1]); } - if (re != "") { + if (re != '') { // For focus/show, forget old parameter. For others, add to re. - if (param != "f" && param != "s" && params.has(param)) { - const old = params.get(param) - if (old != "") { - re += "|" + old + if (param != 'f' && param != 's' && params.has(param)) { + const old = params.get(param); + if (old != '') { + re += '|' + old; } } - params.set(param, re) + params.set(param, re); } else { - params.delete(param) + params.delete(param); } - return url.toString() + return 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 + let elem = e.target; + while (elem != null && elem.nodeName != 'TR') { + elem = elem.parentElement; } - if (elem == null || elem.children.length < 6) return + 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 + 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 + regexpActive = false; if (selected.has(index)) { - unselect(index, elem) + unselect(index, elem); } else { - select(index, elem) + select(index, elem); } - updateButtons() + updateButtons(); } function updateButtons() { - const enable = (search.value != "" || selected.size != 0) - if (buttonsEnabled == enable) return - buttonsEnabled = enable - for (const id of ["focus", "ignore", "hide", "show"]) { - const link = document.getElementById(id) + const enable = (search.value != '' || selected.size != 0); + if (buttonsEnabled == enable) return; + buttonsEnabled = enable; + for (const id of ['focus', 'ignore', 'hide', 'show']) { + const link = document.getElementById(id); if (link != null) { - link.classList.toggle("disabled", !enable) + link.classList.toggle('disabled', !enable); } } } // Initialize button states - updateButtons() + updateButtons(); // Setup event handlers - initMenus() + initMenus(); if (svg != null) { - initPanAndZoom(svg, toggleSvgSelect) + initPanAndZoom(svg, toggleSvgSelect); } if (toptable != null) { - toptable.addEventListener("mousedown", handleTopClick) - toptable.addEventListener("touchstart", handleTopClick) + toptable.addEventListener('mousedown', handleTopClick); + toptable.addEventListener('touchstart', handleTopClick); } - const ids = ["topbtn", "graphbtn", "peek", "list", "disasm", - "focus", "ignore", "hide", "show"] - ids.forEach(makeLinkDynamic) + const ids = ['topbtn', 'graphbtn', 'peek', 'list', 'disasm', + 'focus', 'ignore', 'hide', 'show']; + ids.forEach(makeLinkDynamic); // Bind action to button with specified id. function addAction(id, action) { - const btn = document.getElementById(id) + const btn = document.getElementById(id); if (btn != null) { - btn.addEventListener("click", action) - btn.addEventListener("touchstart", action) + btn.addEventListener('click', action); + btn.addEventListener('touchstart', action); } } - addAction("details", handleDetails) - addAction("closedetails", handleCloseDetails) + addAction('details', handleDetails); - search.addEventListener("input", handleSearch) - search.addEventListener("keydown", handleKey) + 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") + const main = document.getElementById('bodycontainer'); if (main) { - main.focus() + main.focus(); } } </script> @@ -800,116 +857,115 @@ function viewer(baseUrl, nodes) { <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} -<style type="text/css"> -</style> + <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; -{{template "header" .}} - -<div id="bodycontainer"> -<table id="toptable"> -<tr> -<th id="flathdr1">Flat -<th id="flathdr2">Flat% -<th>Sum% -<th id="cumhdr1">Cum -<th id="cumhdr2">Cum% -<th id="namehdr">Name -<th>Inlined?</tr> -<tbody id="rows"> -</tbody> -</table> -</div> + // 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; + } -{{template "script" .}} -<script> -function makeTopTable(total, entries) { - const rows = document.getElementById("rows") - if (rows == null) return + // Which column are we currently sorted by and in what order? + let currentColumn = ''; + let descending = false; + sortBy('Flat'); - // 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 - } + function sortBy(column) { + // Update sort criteria + if (column == currentColumn) { + descending = !descending; // Reverse order + } else { + currentColumn = column; + descending = (column != 'Name'); + } - // Which column are we currently sorted by and in what order? - let currentColumn = "" - let descending = false - sortBy("Flat") + // 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 sortBy(column) { - // Update sort criteria - if (column == currentColumn) { - descending = !descending // Reverse order - } else { - currentColumn = column - descending = (column != "Name") - } + function addCell(tr, val) { + const td = document.createElement('td'); + td.textContent = val; + tr.appendChild(td); + } - // 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 percent(v) { + return (v * 100.0 / total).toFixed(2) + '%'; + } - function addCell(tr, val) { - const td = document.createElement('td') - td.textContent = val - tr.appendChild(td) - } + // 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); + } - function percent(v) { - return (v * 100.0 / total).toFixed(2) + "%" - } + rows.textContent = ''; // Remove old rows + rows.appendChild(fragment); + } - // 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) + // 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'); } - 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({{.BaseURL}}, {{.Nodes}}) -makeTopTable({{.Total}}, {{.Top}}) -</script> + viewer({{.BaseURL}}, {{.Nodes}}); + makeTopTable({{.Total}}, {{.Top}}); + </script> </body> </html> {{end}} @@ -918,22 +974,19 @@ makeTopTable({{.Total}}, {{.Top}}) <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} -{{template "weblistcss" .}} -{{template "weblistjs" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} + {{template "weblistcss" .}} + {{template "weblistjs" .}} </head> <body> - -{{template "header" .}} - -<div id="bodycontainer"> -{{.HTMLBody}} -</div> - -{{template "script" .}} -<script>viewer({{.BaseURL}}, null)</script> + {{template "header" .}} + <div id="content" class="source"> + {{.HTMLBody}} + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, null);</script> </body> </html> {{end}} @@ -942,22 +995,131 @@ makeTopTable({{.Total}}, {{.Top}}) <!DOCTYPE html> <html> <head> -<meta charset="utf-8"> -<title>{{.Title}}</title> -{{template "css" .}} + <meta charset="utf-8"> + <title>{{.Title}}</title> + {{template "css" .}} +</head> +<body> + {{template "header" .}} + <div id="content"> + <pre> + {{.TextBody}} + </pre> + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, 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-bottom: 41px; + } + </style> </head> <body> + {{template "header" .}} + <div id="bodycontainer"> + <div class="flamegraph-content"> + <div id="chart"></div> + </div> + <div id="flamegraphdetails" class="flamegraph-details"></div> + </div> + {{template "script" .}} + <script>viewer({{.BaseURL}}, {{.Nodes}});</script> + <script>{{template "d3script" .}}</script> + <script>{{template "d3tipscript" .}}</script> + <script>{{template "d3flamegraphscript" .}}</script> + <script type="text/javascript"> + var data = {{.FlameGraph}}; + var label = function(d) { + return d.data.n + ' (' + d.data.p + ', ' + d.data.l + ')'; + }; -{{template "header" .}} + var width = document.getElementById('chart').clientWidth; -<div id="bodycontainer"> -<pre> -{{.TextBody}} -</pre> -</div> + var flameGraph = d3.flameGraph() + .width(width) + .cellHeight(18) + .minFrameSize(1) + .transitionDuration(750) + .transitionEase(d3.easeCubic) + .sort(true) + .title('') + .label(label) + .details(document.getElementById('flamegraphdetails')); + + var tip = d3.tip() + .direction('s') + .offset([8, 0]) + .attr('class', 'd3-flame-graph-tip') + .html(function(d) { return 'name: ' + d.data.n + ', value: ' + d.data.l; }); + + flameGraph.tooltip(tip); + + 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); + } -{{template "script" .}} -<script>viewer({{.BaseURL}}, null)</script> + search.addEventListener('input', handleSearch); + </script> </body> </html> {{end}} |
