1// Copyright 2017 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package driver 16 17import ( 18 "html/template" 19 20 "github.com/google/pprof/third_party/d3" 21 "github.com/google/pprof/third_party/d3flamegraph" 22) 23 24// addTemplates adds a set of template definitions to templates. 25func addTemplates(templates *template.Template) { 26 template.Must(templates.Parse(`{{define "d3script"}}` + d3.JSSource + `{{end}}`)) 27 template.Must(templates.Parse(`{{define "d3flamegraphscript"}}` + d3flamegraph.JSSource + `{{end}}`)) 28 template.Must(templates.Parse(`{{define "d3flamegraphcss"}}` + d3flamegraph.CSSSource + `{{end}}`)) 29 template.Must(templates.Parse(` 30{{define "css"}} 31<style type="text/css"> 32* { 33 margin: 0; 34 padding: 0; 35 box-sizing: border-box; 36} 37html, body { 38 height: 100%; 39} 40body { 41 font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 42 font-size: 13px; 43 line-height: 1.4; 44 display: flex; 45 flex-direction: column; 46} 47a { 48 color: #2a66d9; 49} 50.header { 51 display: flex; 52 align-items: center; 53 height: 44px; 54 min-height: 44px; 55 background-color: #eee; 56 color: #212121; 57 padding: 0 1rem; 58} 59.header > div { 60 margin: 0 0.125em; 61} 62.header .title h1 { 63 font-size: 1.75em; 64 margin-right: 1rem; 65 margin-bottom: 4px; 66} 67.header .title a { 68 color: #212121; 69 text-decoration: none; 70} 71.header .title a:hover { 72 text-decoration: underline; 73} 74.header .description { 75 width: 100%; 76 text-align: right; 77 white-space: nowrap; 78} 79@media screen and (max-width: 799px) { 80 .header input { 81 display: none; 82 } 83} 84#detailsbox { 85 display: none; 86 z-index: 1; 87 position: fixed; 88 top: 40px; 89 right: 20px; 90 background-color: #ffffff; 91 box-shadow: 0 1px 5px rgba(0,0,0,.3); 92 line-height: 24px; 93 padding: 1em; 94 text-align: left; 95} 96.header input { 97 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; 98 border: 1px solid #d1d2d3; 99 border-radius: 2px 0 0 2px; 100 padding: 0.25em; 101 padding-left: 28px; 102 margin-left: 1em; 103 font-family: 'Roboto', 'Noto', sans-serif; 104 font-size: 1em; 105 line-height: 24px; 106 color: #212121; 107} 108.downArrow { 109 border-top: .36em solid #ccc; 110 border-left: .36em solid transparent; 111 border-right: .36em solid transparent; 112 margin-bottom: .05em; 113 margin-left: .5em; 114 transition: border-top-color 200ms; 115} 116.menu-item { 117 height: 100%; 118 text-transform: uppercase; 119 font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 120 position: relative; 121} 122.menu-item .menu-name:hover { 123 opacity: 0.75; 124} 125.menu-item .menu-name:hover .downArrow { 126 border-top-color: #666; 127} 128.menu-name { 129 height: 100%; 130 padding: 0 0.5em; 131 display: flex; 132 align-items: center; 133 justify-content: center; 134} 135.menu-name a { 136 text-decoration: none; 137 color: #212121; 138} 139.submenu { 140 display: none; 141 z-index: 1; 142 margin-top: -4px; 143 min-width: 10em; 144 position: absolute; 145 left: 0px; 146 background-color: white; 147 box-shadow: 0 1px 5px rgba(0,0,0,.3); 148 font-size: 100%; 149 text-transform: none; 150} 151.menu-item, .submenu { 152 user-select: none; 153 -moz-user-select: none; 154 -ms-user-select: none; 155 -webkit-user-select: none; 156} 157.submenu hr { 158 border: 0; 159 border-top: 2px solid #eee; 160} 161.submenu a { 162 display: block; 163 padding: .5em 1em; 164 text-decoration: none; 165} 166.submenu a:hover, .submenu a.active { 167 color: white; 168 background-color: #6b82d6; 169} 170.submenu a.disabled { 171 color: gray; 172 pointer-events: none; 173} 174.menu-check-mark { 175 position: absolute; 176 left: 2px; 177} 178.menu-delete-btn { 179 position: absolute; 180 right: 2px; 181} 182 183{{/* Used to disable events when a modal dialog is displayed */}} 184#dialog-overlay { 185 display: none; 186 position: fixed; 187 left: 0px; 188 top: 0px; 189 width: 100%; 190 height: 100%; 191 background-color: rgba(1,1,1,0.1); 192} 193 194.dialog { 195 {{/* Displayed centered horizontally near the top */}} 196 display: none; 197 position: fixed; 198 margin: 0px; 199 top: 60px; 200 left: 50%; 201 transform: translateX(-50%); 202 203 z-index: 3; 204 font-size: 125%; 205 background-color: #ffffff; 206 box-shadow: 0 1px 5px rgba(0,0,0,.3); 207} 208.dialog-header { 209 font-size: 120%; 210 border-bottom: 1px solid #CCCCCC; 211 width: 100%; 212 text-align: center; 213 background: #EEEEEE; 214 user-select: none; 215} 216.dialog-footer { 217 border-top: 1px solid #CCCCCC; 218 width: 100%; 219 text-align: right; 220 padding: 10px; 221} 222.dialog-error { 223 margin: 10px; 224 color: red; 225} 226.dialog input { 227 margin: 10px; 228 font-size: inherit; 229} 230.dialog button { 231 margin-left: 10px; 232 font-size: inherit; 233} 234#save-dialog, #delete-dialog { 235 width: 50%; 236 max-width: 20em; 237} 238#delete-prompt { 239 padding: 10px; 240} 241 242#content { 243 overflow-y: scroll; 244 padding: 1em; 245} 246#top { 247 overflow-y: scroll; 248} 249#graph { 250 overflow: hidden; 251} 252#graph svg { 253 width: 100%; 254 height: auto; 255 padding: 10px; 256} 257#content.source .filename { 258 margin-top: 0; 259 margin-bottom: 1em; 260 font-size: 120%; 261} 262#content.source pre { 263 margin-bottom: 3em; 264} 265table { 266 border-spacing: 0px; 267 width: 100%; 268 padding-bottom: 1em; 269 white-space: nowrap; 270} 271table thead { 272 font-family: 'Roboto Medium', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 273} 274table tr th { 275 position: sticky; 276 top: 0; 277 background-color: #ddd; 278 text-align: right; 279 padding: .3em .5em; 280} 281table tr td { 282 padding: .3em .5em; 283 text-align: right; 284} 285#top table tr th:nth-child(6), 286#top table tr th:nth-child(7), 287#top table tr td:nth-child(6), 288#top table tr td:nth-child(7) { 289 text-align: left; 290} 291#top table tr td:nth-child(6) { 292 width: 100%; 293 text-overflow: ellipsis; 294 overflow: hidden; 295 white-space: nowrap; 296} 297#flathdr1, #flathdr2, #cumhdr1, #cumhdr2, #namehdr { 298 cursor: ns-resize; 299} 300.hilite { 301 background-color: #ebf5fb; 302 font-weight: bold; 303} 304</style> 305{{end}} 306 307{{define "header"}} 308<div class="header"> 309 <div class="title"> 310 <h1><a href="./">pprof</a></h1> 311 </div> 312 313 <div id="view" class="menu-item"> 314 <div class="menu-name"> 315 View 316 <i class="downArrow"></i> 317 </div> 318 <div class="submenu"> 319 <a title="{{.Help.top}}" href="./top" id="topbtn">Top</a> 320 <a title="{{.Help.graph}}" href="./" id="graphbtn">Graph</a> 321 <a title="{{.Help.flamegraph}}" href="./flamegraph" id="flamegraph">Flame Graph</a> 322 <a title="{{.Help.peek}}" href="./peek" id="peek">Peek</a> 323 <a title="{{.Help.list}}" href="./source" id="list">Source</a> 324 <a title="{{.Help.disasm}}" href="./disasm" id="disasm">Disassemble</a> 325 </div> 326 </div> 327 328 {{$sampleLen := len .SampleTypes}} 329 {{if gt $sampleLen 1}} 330 <div id="sample" class="menu-item"> 331 <div class="menu-name"> 332 Sample 333 <i class="downArrow"></i> 334 </div> 335 <div class="submenu"> 336 {{range .SampleTypes}} 337 <a href="?si={{.}}" id="{{.}}">{{.}}</a> 338 {{end}} 339 </div> 340 </div> 341 {{end}} 342 343 <div id="refine" class="menu-item"> 344 <div class="menu-name"> 345 Refine 346 <i class="downArrow"></i> 347 </div> 348 <div class="submenu"> 349 <a title="{{.Help.focus}}" href="?" id="focus">Focus</a> 350 <a title="{{.Help.ignore}}" href="?" id="ignore">Ignore</a> 351 <a title="{{.Help.hide}}" href="?" id="hide">Hide</a> 352 <a title="{{.Help.show}}" href="?" id="show">Show</a> 353 <a title="{{.Help.show_from}}" href="?" id="show-from">Show from</a> 354 <hr> 355 <a title="{{.Help.reset}}" href="?">Reset</a> 356 </div> 357 </div> 358 359 <div id="config" class="menu-item"> 360 <div class="menu-name"> 361 Config 362 <i class="downArrow"></i> 363 </div> 364 <div class="submenu"> 365 <a title="{{.Help.save_config}}" id="save-config">Save as ...</a> 366 <hr> 367 {{range .Configs}} 368 <a href="{{.URL}}"> 369 {{if .Current}}<span class="menu-check-mark">✓</span>{{end}} 370 {{.Name}} 371 {{if .UserConfig}}<span class="menu-delete-btn" data-config={{.Name}}></span>{{end}} 372 </a> 373 {{end}} 374 </div> 375 </div> 376 377 <div id="download" class="menu-item"> 378 <div class="menu-name"> 379 <a href="./download">Download</a> 380 </div> 381 </div> 382 383 <div> 384 <input id="search" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40> 385 </div> 386 387 <div class="description"> 388 <a title="{{.Help.details}}" href="#" id="details">{{.Title}}</a> 389 <div id="detailsbox"> 390 {{range .Legend}}<div>{{.}}</div>{{end}} 391 </div> 392 </div> 393</div> 394 395<div id="dialog-overlay"></div> 396 397<div class="dialog" id="save-dialog"> 398 <div class="dialog-header">Save options as</div> 399 <datalist id="config-list"> 400 {{range .Configs}}{{if .UserConfig}}<option value="{{.Name}}" />{{end}}{{end}} 401 </datalist> 402 <input id="save-name" type="text" list="config-list" placeholder="New config" /> 403 <div class="dialog-footer"> 404 <span class="dialog-error" id="save-error"></span> 405 <button id="save-cancel">Cancel</button> 406 <button id="save-confirm">Save</button> 407 </div> 408</div> 409 410<div class="dialog" id="delete-dialog"> 411 <div class="dialog-header" id="delete-dialog-title">Delete config</div> 412 <div id="delete-prompt"></div> 413 <div class="dialog-footer"> 414 <span class="dialog-error" id="delete-error"></span> 415 <button id="delete-cancel">Cancel</button> 416 <button id="delete-confirm">Delete</button> 417 </div> 418</div> 419 420<div id="errors">{{range .Errors}}<div>{{.}}</div>{{end}}</div> 421{{end}} 422 423{{define "graph" -}} 424<!DOCTYPE html> 425<html> 426<head> 427 <meta charset="utf-8"> 428 <title>{{.Title}}</title> 429 {{template "css" .}} 430</head> 431<body> 432 {{template "header" .}} 433 <div id="graph"> 434 {{.HTMLBody}} 435 </div> 436 {{template "script" .}} 437 <script>viewer(new URL(window.location.href), {{.Nodes}});</script> 438</body> 439</html> 440{{end}} 441 442{{define "script"}} 443<script> 444// Make svg pannable and zoomable. 445// Call clickHandler(t) if a click event is caught by the pan event handlers. 446function initPanAndZoom(svg, clickHandler) { 447 'use strict'; 448 449 // Current mouse/touch handling mode 450 const IDLE = 0; 451 const MOUSEPAN = 1; 452 const TOUCHPAN = 2; 453 const TOUCHZOOM = 3; 454 let mode = IDLE; 455 456 // State needed to implement zooming. 457 let currentScale = 1.0; 458 const initWidth = svg.viewBox.baseVal.width; 459 const initHeight = svg.viewBox.baseVal.height; 460 461 // State needed to implement panning. 462 let panLastX = 0; // Last event X coordinate 463 let panLastY = 0; // Last event Y coordinate 464 let moved = false; // Have we seen significant movement 465 let touchid = null; // Current touch identifier 466 467 // State needed for pinch zooming 468 let touchid2 = null; // Second id for pinch zooming 469 let initGap = 1.0; // Starting gap between two touches 470 let initScale = 1.0; // currentScale when pinch zoom started 471 let centerPoint = null; // Center point for scaling 472 473 // Convert event coordinates to svg coordinates. 474 function toSvg(x, y) { 475 const p = svg.createSVGPoint(); 476 p.x = x; 477 p.y = y; 478 let m = svg.getCTM(); 479 if (m == null) m = svg.getScreenCTM(); // Firefox workaround. 480 return p.matrixTransform(m.inverse()); 481 } 482 483 // Change the scaling for the svg to s, keeping the point denoted 484 // by u (in svg coordinates]) fixed at the same screen location. 485 function rescale(s, u) { 486 // Limit to a good range. 487 if (s < 0.2) s = 0.2; 488 if (s > 10.0) s = 10.0; 489 490 currentScale = s; 491 492 // svg.viewBox defines the visible portion of the user coordinate 493 // system. So to magnify by s, divide the visible portion by s, 494 // which will then be stretched to fit the viewport. 495 const vb = svg.viewBox; 496 const w1 = vb.baseVal.width; 497 const w2 = initWidth / s; 498 const h1 = vb.baseVal.height; 499 const h2 = initHeight / s; 500 vb.baseVal.width = w2; 501 vb.baseVal.height = h2; 502 503 // We also want to adjust vb.baseVal.x so that u.x remains at same 504 // screen X coordinate. In other words, want to change it from x1 to x2 505 // so that: 506 // (u.x - x1) / w1 = (u.x - x2) / w2 507 // Simplifying that, we get 508 // (u.x - x1) * (w2 / w1) = u.x - x2 509 // x2 = u.x - (u.x - x1) * (w2 / w1) 510 vb.baseVal.x = u.x - (u.x - vb.baseVal.x) * (w2 / w1); 511 vb.baseVal.y = u.y - (u.y - vb.baseVal.y) * (h2 / h1); 512 } 513 514 function handleWheel(e) { 515 if (e.deltaY == 0) return; 516 // Change scale factor by 1.1 or 1/1.1 517 rescale(currentScale * (e.deltaY < 0 ? 1.1 : (1/1.1)), 518 toSvg(e.offsetX, e.offsetY)); 519 } 520 521 function setMode(m) { 522 mode = m; 523 touchid = null; 524 touchid2 = null; 525 } 526 527 function panStart(x, y) { 528 moved = false; 529 panLastX = x; 530 panLastY = y; 531 } 532 533 function panMove(x, y) { 534 let dx = x - panLastX; 535 let dy = y - panLastY; 536 if (Math.abs(dx) <= 2 && Math.abs(dy) <= 2) return; // Ignore tiny moves 537 538 moved = true; 539 panLastX = x; 540 panLastY = y; 541 542 // Firefox workaround: get dimensions from parentNode. 543 const swidth = svg.clientWidth || svg.parentNode.clientWidth; 544 const sheight = svg.clientHeight || svg.parentNode.clientHeight; 545 546 // Convert deltas from screen space to svg space. 547 dx *= (svg.viewBox.baseVal.width / swidth); 548 dy *= (svg.viewBox.baseVal.height / sheight); 549 550 svg.viewBox.baseVal.x -= dx; 551 svg.viewBox.baseVal.y -= dy; 552 } 553 554 function handleScanStart(e) { 555 if (e.button != 0) return; // Do not catch right-clicks etc. 556 setMode(MOUSEPAN); 557 panStart(e.clientX, e.clientY); 558 e.preventDefault(); 559 svg.addEventListener('mousemove', handleScanMove); 560 } 561 562 function handleScanMove(e) { 563 if (e.buttons == 0) { 564 // Missed an end event, perhaps because mouse moved outside window. 565 setMode(IDLE); 566 svg.removeEventListener('mousemove', handleScanMove); 567 return; 568 } 569 if (mode == MOUSEPAN) panMove(e.clientX, e.clientY); 570 } 571 572 function handleScanEnd(e) { 573 if (mode == MOUSEPAN) panMove(e.clientX, e.clientY); 574 setMode(IDLE); 575 svg.removeEventListener('mousemove', handleScanMove); 576 if (!moved) clickHandler(e.target); 577 } 578 579 // Find touch object with specified identifier. 580 function findTouch(tlist, id) { 581 for (const t of tlist) { 582 if (t.identifier == id) return t; 583 } 584 return null; 585 } 586 587 // Return distance between two touch points 588 function touchGap(t1, t2) { 589 const dx = t1.clientX - t2.clientX; 590 const dy = t1.clientY - t2.clientY; 591 return Math.hypot(dx, dy); 592 } 593 594 function handleTouchStart(e) { 595 if (mode == IDLE && e.changedTouches.length == 1) { 596 // Start touch based panning 597 const t = e.changedTouches[0]; 598 setMode(TOUCHPAN); 599 touchid = t.identifier; 600 panStart(t.clientX, t.clientY); 601 e.preventDefault(); 602 } else if (mode == TOUCHPAN && e.touches.length == 2) { 603 // Start pinch zooming 604 setMode(TOUCHZOOM); 605 const t1 = e.touches[0]; 606 const t2 = e.touches[1]; 607 touchid = t1.identifier; 608 touchid2 = t2.identifier; 609 initScale = currentScale; 610 initGap = touchGap(t1, t2); 611 centerPoint = toSvg((t1.clientX + t2.clientX) / 2, 612 (t1.clientY + t2.clientY) / 2); 613 e.preventDefault(); 614 } 615 } 616 617 function handleTouchMove(e) { 618 if (mode == TOUCHPAN) { 619 const t = findTouch(e.changedTouches, touchid); 620 if (t == null) return; 621 if (e.touches.length != 1) { 622 setMode(IDLE); 623 return; 624 } 625 panMove(t.clientX, t.clientY); 626 e.preventDefault(); 627 } else if (mode == TOUCHZOOM) { 628 // Get two touches; new gap; rescale to ratio. 629 const t1 = findTouch(e.touches, touchid); 630 const t2 = findTouch(e.touches, touchid2); 631 if (t1 == null || t2 == null) return; 632 const gap = touchGap(t1, t2); 633 rescale(initScale * gap / initGap, centerPoint); 634 e.preventDefault(); 635 } 636 } 637 638 function handleTouchEnd(e) { 639 if (mode == TOUCHPAN) { 640 const t = findTouch(e.changedTouches, touchid); 641 if (t == null) return; 642 panMove(t.clientX, t.clientY); 643 setMode(IDLE); 644 e.preventDefault(); 645 if (!moved) clickHandler(t.target); 646 } else if (mode == TOUCHZOOM) { 647 setMode(IDLE); 648 e.preventDefault(); 649 } 650 } 651 652 svg.addEventListener('mousedown', handleScanStart); 653 svg.addEventListener('mouseup', handleScanEnd); 654 svg.addEventListener('touchstart', handleTouchStart); 655 svg.addEventListener('touchmove', handleTouchMove); 656 svg.addEventListener('touchend', handleTouchEnd); 657 svg.addEventListener('wheel', handleWheel, true); 658} 659 660function initMenus() { 661 'use strict'; 662 663 let activeMenu = null; 664 let activeMenuHdr = null; 665 666 function cancelActiveMenu() { 667 if (activeMenu == null) return; 668 activeMenu.style.display = 'none'; 669 activeMenu = null; 670 activeMenuHdr = null; 671 } 672 673 // Set click handlers on every menu header. 674 for (const menu of document.getElementsByClassName('submenu')) { 675 const hdr = menu.parentElement; 676 if (hdr == null) return; 677 if (hdr.classList.contains('disabled')) return; 678 function showMenu(e) { 679 // menu is a child of hdr, so this event can fire for clicks 680 // inside menu. Ignore such clicks. 681 if (e.target.parentElement != hdr) return; 682 activeMenu = menu; 683 activeMenuHdr = hdr; 684 menu.style.display = 'block'; 685 } 686 hdr.addEventListener('mousedown', showMenu); 687 hdr.addEventListener('touchstart', showMenu); 688 } 689 690 // If there is an active menu and a down event outside, retract the menu. 691 for (const t of ['mousedown', 'touchstart']) { 692 document.addEventListener(t, (e) => { 693 // Note: to avoid unnecessary flicker, if the down event is inside 694 // the active menu header, do not retract the menu. 695 if (activeMenuHdr != e.target.closest('.menu-item')) { 696 cancelActiveMenu(); 697 } 698 }, { passive: true, capture: true }); 699 } 700 701 // If there is an active menu and an up event inside, retract the menu. 702 document.addEventListener('mouseup', (e) => { 703 if (activeMenu == e.target.closest('.submenu')) { 704 cancelActiveMenu(); 705 } 706 }, { passive: true, capture: true }); 707} 708 709function sendURL(method, url, done) { 710 fetch(url.toString(), {method: method}) 711 .then((response) => { done(response.ok); }) 712 .catch((error) => { done(false); }); 713} 714 715// Initialize handlers for saving/loading configurations. 716function initConfigManager() { 717 'use strict'; 718 719 // Initialize various elements. 720 function elem(id) { 721 const result = document.getElementById(id); 722 if (!result) console.warn('element ' + id + ' not found'); 723 return result; 724 } 725 const overlay = elem('dialog-overlay'); 726 const saveDialog = elem('save-dialog'); 727 const saveInput = elem('save-name'); 728 const saveError = elem('save-error'); 729 const delDialog = elem('delete-dialog'); 730 const delPrompt = elem('delete-prompt'); 731 const delError = elem('delete-error'); 732 733 let currentDialog = null; 734 let currentDeleteTarget = null; 735 736 function showDialog(dialog) { 737 if (currentDialog != null) { 738 overlay.style.display = 'none'; 739 currentDialog.style.display = 'none'; 740 } 741 currentDialog = dialog; 742 if (dialog != null) { 743 overlay.style.display = 'block'; 744 dialog.style.display = 'block'; 745 } 746 } 747 748 function cancelDialog(e) { 749 showDialog(null); 750 } 751 752 // Show dialog for saving the current config. 753 function showSaveDialog(e) { 754 saveError.innerText = ''; 755 showDialog(saveDialog); 756 saveInput.focus(); 757 } 758 759 // Commit save config. 760 function commitSave(e) { 761 const name = saveInput.value; 762 const url = new URL(document.URL); 763 // Set path relative to existing path. 764 url.pathname = new URL('./saveconfig', document.URL).pathname; 765 url.searchParams.set('config', name); 766 saveError.innerText = ''; 767 sendURL('POST', url, (ok) => { 768 if (!ok) { 769 saveError.innerText = 'Save failed'; 770 } else { 771 showDialog(null); 772 location.reload(); // Reload to show updated config menu 773 } 774 }); 775 } 776 777 function handleSaveInputKey(e) { 778 if (e.key === 'Enter') commitSave(e); 779 } 780 781 function deleteConfig(e, elem) { 782 e.preventDefault(); 783 const config = elem.dataset.config; 784 delPrompt.innerText = 'Delete ' + config + '?'; 785 currentDeleteTarget = elem; 786 showDialog(delDialog); 787 } 788 789 function commitDelete(e, elem) { 790 if (!currentDeleteTarget) return; 791 const config = currentDeleteTarget.dataset.config; 792 const url = new URL('./deleteconfig', document.URL); 793 url.searchParams.set('config', config); 794 delError.innerText = ''; 795 sendURL('DELETE', url, (ok) => { 796 if (!ok) { 797 delError.innerText = 'Delete failed'; 798 return; 799 } 800 showDialog(null); 801 // Remove menu entry for this config. 802 if (currentDeleteTarget && currentDeleteTarget.parentElement) { 803 currentDeleteTarget.parentElement.remove(); 804 } 805 }); 806 } 807 808 // Bind event on elem to fn. 809 function bind(event, elem, fn) { 810 if (elem == null) return; 811 elem.addEventListener(event, fn); 812 if (event == 'click') { 813 // Also enable via touch. 814 elem.addEventListener('touchstart', fn); 815 } 816 } 817 818 bind('click', elem('save-config'), showSaveDialog); 819 bind('click', elem('save-cancel'), cancelDialog); 820 bind('click', elem('save-confirm'), commitSave); 821 bind('keydown', saveInput, handleSaveInputKey); 822 823 bind('click', elem('delete-cancel'), cancelDialog); 824 bind('click', elem('delete-confirm'), commitDelete); 825 826 // Activate deletion button for all config entries in menu. 827 for (const del of Array.from(document.getElementsByClassName('menu-delete-btn'))) { 828 bind('click', del, (e) => { 829 deleteConfig(e, del); 830 }); 831 } 832} 833 834function viewer(baseUrl, nodes) { 835 'use strict'; 836 837 // Elements 838 const search = document.getElementById('search'); 839 const graph0 = document.getElementById('graph0'); 840 const svg = (graph0 == null ? null : graph0.parentElement); 841 const toptable = document.getElementById('toptable'); 842 843 let regexpActive = false; 844 let selected = new Map(); 845 let origFill = new Map(); 846 let searchAlarm = null; 847 let buttonsEnabled = true; 848 849 function handleDetails(e) { 850 e.preventDefault(); 851 const detailsText = document.getElementById('detailsbox'); 852 if (detailsText != null) { 853 if (detailsText.style.display === 'block') { 854 detailsText.style.display = 'none'; 855 } else { 856 detailsText.style.display = 'block'; 857 } 858 } 859 } 860 861 function handleKey(e) { 862 if (e.keyCode != 13) return; 863 setHrefParams(window.location, function (params) { 864 params.set('f', search.value); 865 }); 866 e.preventDefault(); 867 } 868 869 function handleSearch() { 870 // Delay expensive processing so a flurry of key strokes is handled once. 871 if (searchAlarm != null) { 872 clearTimeout(searchAlarm); 873 } 874 searchAlarm = setTimeout(selectMatching, 300); 875 876 regexpActive = true; 877 updateButtons(); 878 } 879 880 function selectMatching() { 881 searchAlarm = null; 882 let re = null; 883 if (search.value != '') { 884 try { 885 re = new RegExp(search.value); 886 } catch (e) { 887 // TODO: Display error state in search box 888 return; 889 } 890 } 891 892 function match(text) { 893 return re != null && re.test(text); 894 } 895 896 // drop currently selected items that do not match re. 897 selected.forEach(function(v, n) { 898 if (!match(nodes[n])) { 899 unselect(n, document.getElementById('node' + n)); 900 } 901 }) 902 903 // add matching items that are not currently selected. 904 if (nodes) { 905 for (let n = 0; n < nodes.length; n++) { 906 if (!selected.has(n) && match(nodes[n])) { 907 select(n, document.getElementById('node' + n)); 908 } 909 } 910 } 911 912 updateButtons(); 913 } 914 915 function toggleSvgSelect(elem) { 916 // Walk up to immediate child of graph0 917 while (elem != null && elem.parentElement != graph0) { 918 elem = elem.parentElement; 919 } 920 if (!elem) return; 921 922 // Disable regexp mode. 923 regexpActive = false; 924 925 const n = nodeId(elem); 926 if (n < 0) return; 927 if (selected.has(n)) { 928 unselect(n, elem); 929 } else { 930 select(n, elem); 931 } 932 updateButtons(); 933 } 934 935 function unselect(n, elem) { 936 if (elem == null) return; 937 selected.delete(n); 938 setBackground(elem, false); 939 } 940 941 function select(n, elem) { 942 if (elem == null) return; 943 selected.set(n, true); 944 setBackground(elem, true); 945 } 946 947 function nodeId(elem) { 948 const id = elem.id; 949 if (!id) return -1; 950 if (!id.startsWith('node')) return -1; 951 const n = parseInt(id.slice(4), 10); 952 if (isNaN(n)) return -1; 953 if (n < 0 || n >= nodes.length) return -1; 954 return n; 955 } 956 957 function setBackground(elem, set) { 958 // Handle table row highlighting. 959 if (elem.nodeName == 'TR') { 960 elem.classList.toggle('hilite', set); 961 return; 962 } 963 964 // Handle svg element highlighting. 965 const p = findPolygon(elem); 966 if (p != null) { 967 if (set) { 968 origFill.set(p, p.style.fill); 969 p.style.fill = '#ccccff'; 970 } else if (origFill.has(p)) { 971 p.style.fill = origFill.get(p); 972 } 973 } 974 } 975 976 function findPolygon(elem) { 977 if (elem.localName == 'polygon') return elem; 978 for (const c of elem.children) { 979 const p = findPolygon(c); 980 if (p != null) return p; 981 } 982 return null; 983 } 984 985 // convert a string to a regexp that matches that string. 986 function quotemeta(str) { 987 return str.replace(/([\\\.?+*\[\](){}|^$])/g, '\\$1'); 988 } 989 990 function setSampleIndexLink(id) { 991 const elem = document.getElementById(id); 992 if (elem != null) { 993 setHrefParams(elem, function (params) { 994 params.set("si", id); 995 }); 996 } 997 } 998 999 // Update id's href to reflect current selection whenever it is 1000 // liable to be followed. 1001 function makeSearchLinkDynamic(id) { 1002 const elem = document.getElementById(id); 1003 if (elem == null) return; 1004 1005 // Most links copy current selection into the 'f' parameter, 1006 // but Refine menu links are different. 1007 let param = 'f'; 1008 if (id == 'ignore') param = 'i'; 1009 if (id == 'hide') param = 'h'; 1010 if (id == 'show') param = 's'; 1011 if (id == 'show-from') param = 'sf'; 1012 1013 // We update on mouseenter so middle-click/right-click work properly. 1014 elem.addEventListener('mouseenter', updater); 1015 elem.addEventListener('touchstart', updater); 1016 1017 function updater() { 1018 // The selection can be in one of two modes: regexp-based or 1019 // list-based. Construct regular expression depending on mode. 1020 let re = regexpActive 1021 ? search.value 1022 : Array.from(selected.keys()).map(key => quotemeta(nodes[key])).join('|'); 1023 1024 setHrefParams(elem, function (params) { 1025 if (re != '') { 1026 // For focus/show/show-from, forget old parameter. For others, add to re. 1027 if (param != 'f' && param != 's' && param != 'sf' && params.has(param)) { 1028 const old = params.get(param); 1029 if (old != '') { 1030 re += '|' + old; 1031 } 1032 } 1033 params.set(param, re); 1034 } else { 1035 params.delete(param); 1036 } 1037 }); 1038 } 1039 } 1040 1041 function setHrefParams(elem, paramSetter) { 1042 let url = new URL(elem.href); 1043 url.hash = ''; 1044 1045 // Copy params from this page's URL. 1046 const params = url.searchParams; 1047 for (const p of new URLSearchParams(window.location.search)) { 1048 params.set(p[0], p[1]); 1049 } 1050 1051 // Give the params to the setter to modify. 1052 paramSetter(params); 1053 1054 elem.href = url.toString(); 1055 } 1056 1057 function handleTopClick(e) { 1058 // Walk back until we find TR and then get the Name column (index 5) 1059 let elem = e.target; 1060 while (elem != null && elem.nodeName != 'TR') { 1061 elem = elem.parentElement; 1062 } 1063 if (elem == null || elem.children.length < 6) return; 1064 1065 e.preventDefault(); 1066 const tr = elem; 1067 const td = elem.children[5]; 1068 if (td.nodeName != 'TD') return; 1069 const name = td.innerText; 1070 const index = nodes.indexOf(name); 1071 if (index < 0) return; 1072 1073 // Disable regexp mode. 1074 regexpActive = false; 1075 1076 if (selected.has(index)) { 1077 unselect(index, elem); 1078 } else { 1079 select(index, elem); 1080 } 1081 updateButtons(); 1082 } 1083 1084 function updateButtons() { 1085 const enable = (search.value != '' || selected.size != 0); 1086 if (buttonsEnabled == enable) return; 1087 buttonsEnabled = enable; 1088 for (const id of ['focus', 'ignore', 'hide', 'show', 'show-from']) { 1089 const link = document.getElementById(id); 1090 if (link != null) { 1091 link.classList.toggle('disabled', !enable); 1092 } 1093 } 1094 } 1095 1096 // Initialize button states 1097 updateButtons(); 1098 1099 // Setup event handlers 1100 initMenus(); 1101 if (svg != null) { 1102 initPanAndZoom(svg, toggleSvgSelect); 1103 } 1104 if (toptable != null) { 1105 toptable.addEventListener('mousedown', handleTopClick); 1106 toptable.addEventListener('touchstart', handleTopClick); 1107 } 1108 1109 const ids = ['topbtn', 'graphbtn', 'flamegraph', 'peek', 'list', 'disasm', 1110 'focus', 'ignore', 'hide', 'show', 'show-from']; 1111 ids.forEach(makeSearchLinkDynamic); 1112 1113 const sampleIDs = [{{range .SampleTypes}}'{{.}}', {{end}}]; 1114 sampleIDs.forEach(setSampleIndexLink); 1115 1116 // Bind action to button with specified id. 1117 function addAction(id, action) { 1118 const btn = document.getElementById(id); 1119 if (btn != null) { 1120 btn.addEventListener('click', action); 1121 btn.addEventListener('touchstart', action); 1122 } 1123 } 1124 1125 addAction('details', handleDetails); 1126 initConfigManager(); 1127 1128 search.addEventListener('input', handleSearch); 1129 search.addEventListener('keydown', handleKey); 1130 1131 // Give initial focus to main container so it can be scrolled using keys. 1132 const main = document.getElementById('bodycontainer'); 1133 if (main) { 1134 main.focus(); 1135 } 1136} 1137</script> 1138{{end}} 1139 1140{{define "top" -}} 1141<!DOCTYPE html> 1142<html> 1143<head> 1144 <meta charset="utf-8"> 1145 <title>{{.Title}}</title> 1146 {{template "css" .}} 1147 <style type="text/css"> 1148 </style> 1149</head> 1150<body> 1151 {{template "header" .}} 1152 <div id="top"> 1153 <table id="toptable"> 1154 <thead> 1155 <tr> 1156 <th id="flathdr1">Flat</th> 1157 <th id="flathdr2">Flat%</th> 1158 <th>Sum%</th> 1159 <th id="cumhdr1">Cum</th> 1160 <th id="cumhdr2">Cum%</th> 1161 <th id="namehdr">Name</th> 1162 <th>Inlined?</th> 1163 </tr> 1164 </thead> 1165 <tbody id="rows"></tbody> 1166 </table> 1167 </div> 1168 {{template "script" .}} 1169 <script> 1170 function makeTopTable(total, entries) { 1171 const rows = document.getElementById('rows'); 1172 if (rows == null) return; 1173 1174 // Store initial index in each entry so we have stable node ids for selection. 1175 for (let i = 0; i < entries.length; i++) { 1176 entries[i].Id = 'node' + i; 1177 } 1178 1179 // Which column are we currently sorted by and in what order? 1180 let currentColumn = ''; 1181 let descending = false; 1182 sortBy('Flat'); 1183 1184 function sortBy(column) { 1185 // Update sort criteria 1186 if (column == currentColumn) { 1187 descending = !descending; // Reverse order 1188 } else { 1189 currentColumn = column; 1190 descending = (column != 'Name'); 1191 } 1192 1193 // Sort according to current criteria. 1194 function cmp(a, b) { 1195 const av = a[currentColumn]; 1196 const bv = b[currentColumn]; 1197 if (av < bv) return -1; 1198 if (av > bv) return +1; 1199 return 0; 1200 } 1201 entries.sort(cmp); 1202 if (descending) entries.reverse(); 1203 1204 function addCell(tr, val) { 1205 const td = document.createElement('td'); 1206 td.textContent = val; 1207 tr.appendChild(td); 1208 } 1209 1210 function percent(v) { 1211 return (v * 100.0 / total).toFixed(2) + '%'; 1212 } 1213 1214 // Generate rows 1215 const fragment = document.createDocumentFragment(); 1216 let sum = 0; 1217 for (const row of entries) { 1218 const tr = document.createElement('tr'); 1219 tr.id = row.Id; 1220 sum += row.Flat; 1221 addCell(tr, row.FlatFormat); 1222 addCell(tr, percent(row.Flat)); 1223 addCell(tr, percent(sum)); 1224 addCell(tr, row.CumFormat); 1225 addCell(tr, percent(row.Cum)); 1226 addCell(tr, row.Name); 1227 addCell(tr, row.InlineLabel); 1228 fragment.appendChild(tr); 1229 } 1230 1231 rows.textContent = ''; // Remove old rows 1232 rows.appendChild(fragment); 1233 } 1234 1235 // Make different column headers trigger sorting. 1236 function bindSort(id, column) { 1237 const hdr = document.getElementById(id); 1238 if (hdr == null) return; 1239 const fn = function() { sortBy(column) }; 1240 hdr.addEventListener('click', fn); 1241 hdr.addEventListener('touch', fn); 1242 } 1243 bindSort('flathdr1', 'Flat'); 1244 bindSort('flathdr2', 'Flat'); 1245 bindSort('cumhdr1', 'Cum'); 1246 bindSort('cumhdr2', 'Cum'); 1247 bindSort('namehdr', 'Name'); 1248 } 1249 1250 viewer(new URL(window.location.href), {{.Nodes}}); 1251 makeTopTable({{.Total}}, {{.Top}}); 1252 </script> 1253</body> 1254</html> 1255{{end}} 1256 1257{{define "sourcelisting" -}} 1258<!DOCTYPE html> 1259<html> 1260<head> 1261 <meta charset="utf-8"> 1262 <title>{{.Title}}</title> 1263 {{template "css" .}} 1264 {{template "weblistcss" .}} 1265 {{template "weblistjs" .}} 1266</head> 1267<body> 1268 {{template "header" .}} 1269 <div id="content" class="source"> 1270 {{.HTMLBody}} 1271 </div> 1272 {{template "script" .}} 1273 <script>viewer(new URL(window.location.href), null);</script> 1274</body> 1275</html> 1276{{end}} 1277 1278{{define "plaintext" -}} 1279<!DOCTYPE html> 1280<html> 1281<head> 1282 <meta charset="utf-8"> 1283 <title>{{.Title}}</title> 1284 {{template "css" .}} 1285</head> 1286<body> 1287 {{template "header" .}} 1288 <div id="content"> 1289 <pre> 1290 {{.TextBody}} 1291 </pre> 1292 </div> 1293 {{template "script" .}} 1294 <script>viewer(new URL(window.location.href), null);</script> 1295</body> 1296</html> 1297{{end}} 1298 1299{{define "flamegraph" -}} 1300<!DOCTYPE html> 1301<html> 1302<head> 1303 <meta charset="utf-8"> 1304 <title>{{.Title}}</title> 1305 {{template "css" .}} 1306 <style type="text/css">{{template "d3flamegraphcss" .}}</style> 1307 <style type="text/css"> 1308 .flamegraph-content { 1309 width: 90%; 1310 min-width: 80%; 1311 margin-left: 5%; 1312 } 1313 .flamegraph-details { 1314 height: 1.2em; 1315 width: 90%; 1316 min-width: 90%; 1317 margin-left: 5%; 1318 padding: 15px 0 35px; 1319 } 1320 </style> 1321</head> 1322<body> 1323 {{template "header" .}} 1324 <div id="bodycontainer"> 1325 <div id="flamegraphdetails" class="flamegraph-details"></div> 1326 <div class="flamegraph-content"> 1327 <div id="chart"></div> 1328 </div> 1329 </div> 1330 {{template "script" .}} 1331 <script>viewer(new URL(window.location.href), {{.Nodes}});</script> 1332 <script>{{template "d3script" .}}</script> 1333 <script>{{template "d3flamegraphscript" .}}</script> 1334 <script> 1335 var data = {{.FlameGraph}}; 1336 1337 var width = document.getElementById('chart').clientWidth; 1338 1339 var flameGraph = d3.flamegraph() 1340 .width(width) 1341 .cellHeight(18) 1342 .minFrameSize(1) 1343 .transitionDuration(750) 1344 .transitionEase(d3.easeCubic) 1345 .inverted(true) 1346 .sort(true) 1347 .title('') 1348 .tooltip(false) 1349 .details(document.getElementById('flamegraphdetails')); 1350 1351 // <full name> (percentage, value) 1352 flameGraph.label((d) => d.data.f + ' (' + d.data.p + ', ' + d.data.l + ')'); 1353 1354 (function(flameGraph) { 1355 var oldColorMapper = flameGraph.color(); 1356 function colorMapper(d) { 1357 // Hack to force default color mapper to use 'warm' color scheme by not passing libtype 1358 const { data, highlight } = d; 1359 return oldColorMapper({ data: { n: data.n }, highlight }); 1360 } 1361 1362 flameGraph.color(colorMapper); 1363 }(flameGraph)); 1364 1365 d3.select('#chart') 1366 .datum(data) 1367 .call(flameGraph); 1368 1369 function clear() { 1370 flameGraph.clear(); 1371 } 1372 1373 function resetZoom() { 1374 flameGraph.resetZoom(); 1375 } 1376 1377 window.addEventListener('resize', function() { 1378 var width = document.getElementById('chart').clientWidth; 1379 var graphs = document.getElementsByClassName('d3-flame-graph'); 1380 if (graphs.length > 0) { 1381 graphs[0].setAttribute('width', width); 1382 } 1383 flameGraph.width(width); 1384 flameGraph.resetZoom(); 1385 }, true); 1386 1387 var search = document.getElementById('search'); 1388 var searchAlarm = null; 1389 1390 function selectMatching() { 1391 searchAlarm = null; 1392 1393 if (search.value != '') { 1394 flameGraph.search(search.value); 1395 } else { 1396 flameGraph.clear(); 1397 } 1398 } 1399 1400 function handleSearch() { 1401 // Delay expensive processing so a flurry of key strokes is handled once. 1402 if (searchAlarm != null) { 1403 clearTimeout(searchAlarm); 1404 } 1405 searchAlarm = setTimeout(selectMatching, 300); 1406 } 1407 1408 search.addEventListener('input', handleSearch); 1409 </script> 1410</body> 1411</html> 1412{{end}} 1413`)) 1414} 1415