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