profvis.js (87828B)
1 /*jshint 2 undef:true, 3 browser:true, 4 devel: true, 5 jquery:true, 6 strict:false, 7 curly:false, 8 indent:2 9 */ 10 /*global profvis:true, d3, hljs */ 11 12 profvis = (function() { 13 var profvis = {}; 14 15 profvis.render = function(el, message) { 16 17 function generateStatusBarButton(id, caption, active) { 18 var spacerImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAUCAYAAACnOeyiAAAAXklEQVR42mNgAIL///8zMYSGhjIDGYIMIiIMvECGMwMDN4M4kFEDUqIIZKwDMdSBjAsghj6Q8QPEMAAy/lOBoQekv4AYKkDGfgZeXl4RICOLQUtLiw3IUAJJMQIZ7AC2tU2tXJxOYgAAAABJRU5ErkJggg=='; 19 20 var buttonHtml = 21 '<div id="' + id + '" class="info-block ' + 22 (active ? 'result-block-active' : 'result-block') + 23 '"><span class="info-label">' + caption + '</span></div>' + 24 '<div class="separator-block"><img class="separator-image" src="' + spacerImage + '"></div>'; 25 26 return buttonHtml; 27 } 28 29 function generateStatusBar(el, onToogle) { 30 var $el = $(el); 31 32 el.innerHTML = 33 generateStatusBarButton('flameGraphButton', 'Flame Graph', true) + 34 generateStatusBarButton('treetableButton', 'Data', false) + 35 '<span role="button" class="options-button">Options ▾</span>'; 36 37 $el.find("span.options-button").on("click", function(e) { 38 e.preventDefault(); 39 e.stopPropagation(); 40 41 vis.optionsPanel.toggleVisibility(); 42 }); 43 44 var setStatusBarButtons = function(e) { 45 $(".info-block").removeClass("result-block-active"); 46 $(".info-block").addClass("result-block"); 47 e.addClass("result-block-active"); 48 }; 49 50 $el.find("#flameGraphButton").on("click", function() { 51 setStatusBarButtons($(this)); 52 onToogle("flamegraph"); 53 }); 54 55 $el.find("#treetableButton").on("click", function() { 56 setStatusBarButtons($(this)); 57 onToogle("treetable"); 58 }); 59 60 return { 61 el: el 62 }; 63 } 64 65 function generateFooter(el, onToogle) { 66 var $el = $(el); 67 68 el.innerHTML = 69 '<div class="info-block"><span class="info-label">Sample Interval: ' + 70 vis.interval + 'ms</span></div>' + 71 '<div class="info-block-right">' + 72 // '<span class="info-label" title="Peak memory allocation">' + (Math.round(vis.totalMem * 100) / 100) + 'MB</span>' + 73 // ' / ' + 74 '<span class="info-label" title="Total time">' + vis.totalTime + 'ms</span>' + 75 '</div>'; 76 77 return { 78 el: el 79 }; 80 } 81 82 function generateOptionsPanel(el, onOptionsChange) { 83 var $el = $(el); 84 85 el.innerHTML = 86 '<div role="button" class="split-horizontal">' + 87 '<span class="options-checkbox" data-checked="1">' + 88 (vis.splitDir === "h" ? '☒' : '☐') + 89 '</span> Split horizontally' + 90 '</div>' + 91 '<div role="button" class="hide-internal">' + 92 '<span class="options-checkbox" data-checked="1">☒</span> Hide internal function calls' + 93 '</div>' + 94 '<div role="button" class="hide-zero-row">' + 95 '<span class="options-checkbox" data-checked="0">☐</span> Hide lines of code with zero time' + 96 '</div>' + 97 '<div role="button" class="hide-memory">' + 98 '<span class="options-checkbox" data-checked="0">☐</span> Hide memory results' + 99 '</div>'; 100 101 // Toggle the appearance of a checkbox and return the new checked state. 102 function toggleCheckbox($checkbox) { 103 // Use attr() instead of data(), because the latter tries to coerce to 104 // numbers, which complicates our comparisons. 105 var checked = $checkbox.attr("data-checked"); 106 107 if (checked === "0") { 108 $checkbox.attr("data-checked", "1"); 109 $checkbox.html("☒"); 110 return true; 111 112 } else { 113 $checkbox.attr("data-checked", "0"); 114 $checkbox.html("☐"); 115 return false; 116 } 117 } 118 119 $el.find(".split-horizontal") 120 .on("click", function() { 121 var checked = toggleCheckbox($(this).find(".options-checkbox")); 122 onOptionsChange("split", checked); 123 }); 124 125 $el.find(".hide-internal") 126 .on("click", function() { 127 var checked = toggleCheckbox($(this).find(".options-checkbox")); 128 onOptionsChange("internals", checked); 129 }); 130 131 $el.find(".hide-memory") 132 .on("click", function() { 133 var checked = toggleCheckbox($(this).find(".options-checkbox")); 134 onOptionsChange("memory", checked); 135 }); 136 137 // Make the "hide internal" option available or unavailable to users 138 function enableHideInternal() { 139 $el.find(".hide-internal").css("display", ""); 140 } 141 function disableHideInternal() { 142 $el.find(".hide-internal").css("display", "none"); 143 } 144 // By default, start with it unavailable; it's only relevant for Shiny 145 // apps. 146 disableHideInternal(); 147 148 149 $el.find(".hide-zero-row") 150 .on("click", function() { 151 var checked = toggleCheckbox($(this).find(".options-checkbox")); 152 153 if (checked) { 154 vis.codeTable.hideZeroTimeRows(); 155 } else { 156 vis.codeTable.showZeroTimeRows(); 157 } 158 }); 159 160 el.style.visibility = "hidden"; 161 function toggleVisibility(offset) { 162 if (el.style.visibility === "visible") { 163 el.style.visibility = "hidden"; 164 } else { 165 el.style.visibility = "visible"; 166 $(document).on("click", hideOnClickOutside); 167 } 168 } 169 170 // Hide the panel when a click happens outside. This handler also removes 171 // itself after it fires. 172 function hideOnClickOutside(e) { 173 var $el = $(el); 174 if (!$el.is(e.target) && $el.has(e.target).length === 0) { 175 el.style.visibility = "hidden"; 176 // Unregister this event listener 177 $(document).off("click", hideOnClickOutside); 178 } 179 } 180 181 return { 182 el: el, 183 toggleVisibility: toggleVisibility, 184 enableHideInternal: enableHideInternal, 185 disableHideInternal: disableHideInternal 186 }; 187 } 188 189 function notifySourceFileMessage(d, details) { 190 if (window.parent.postMessage) { 191 window.parent.postMessage({ 192 source: "profvis", 193 message: "sourcefile", 194 file: d.filename, 195 normpath: d.normpath ? d.normpath : getNormPath(vis.files, d.filename), 196 line: d.linenum, 197 details: details 198 }, window.location.origin); 199 } 200 } 201 202 function roundOneDecimalNum(number, decimals) { 203 return Math.round(number * 10) / 10; 204 } 205 206 function roundOneDecimal(number, decimals) { 207 if (!number) return 0; 208 return roundOneDecimalNum(number).toFixed(1); 209 } 210 211 // Generate the code table ---------------------------------------- 212 function generateCodeTable(el) { 213 var useMemory = false; 214 var content = d3.select(el); 215 216 if (vis.fileLineStats.length === 0) { 217 content.append("div") 218 .attr("class", "profvis-message") 219 .append("div") 220 .text("(Sources not available)"); 221 } 222 223 // One table for each file 224 var tables = content.selectAll("table") 225 .data(vis.fileLineStats) 226 .enter() 227 .append("table") 228 .attr("class", "profvis-table"); 229 230 // Table headers 231 var headerRows = tables.append("tr"); 232 headerRows.append("th") 233 .attr("colspan", "2") 234 .attr("class", "filename") 235 .text(function(d) { return d.filename; }); 236 237 var percentTooltip = "Percentage of tracked execution time"; 238 var percentMemTooltip = "Percentage of peak memory deallocation and allocation"; 239 240 headerRows.append("th") 241 .attr("class", "table-memory memory") 242 .attr("colspan", "4") 243 .text("Memory"); 244 245 headerRows.append("th") 246 .attr("class", "time") 247 .attr("colspan", "2") 248 .text("Time"); 249 250 headerRows.append("th") 251 .attr("class", "spacing") 252 .attr("data-pseudo-content", "\u00a0"); 253 254 // Insert each line of code 255 var rows = tables.selectAll("tr.code-row") 256 .data(function(d) { return d.lineData; }) 257 .enter() 258 .append("tr") 259 .attr("class", "code-row"); 260 261 // Use pseudo-content and CSS content rule to make text unselectable and 262 // uncopyable. See https://danoc.me/blog/css-prevent-copy/ 263 rows.append("td") 264 .attr("class", "linenum") 265 .attr("data-pseudo-content", function(d) { return d.linenum; }); 266 267 rows.append("td") 268 .attr("class", "code r") 269 .text(function(d) { return d.content; }) 270 .each(function() { hljs.highlightElement(this); }); 271 272 rows.append("td") 273 .attr("class", "table-memory memory") 274 .attr("title", "Memory deallocation (MB)") 275 .attr("data-pseudo-content", 276 function(d) { return roundOneDecimalNum(d.sumMemDealloc) !== 0 ? roundOneDecimal(d.sumMemDealloc) : ""; }); 277 278 rows.append("td") 279 .attr("class", "table-memory membar-left-cell") 280 .append("div") 281 .attr("class", "membar") 282 .attr("title", percentMemTooltip) 283 .style("width", function(d) { 284 var p = Math.min(Math.abs(Math.min(Math.round(d.propMemDealloc * 100), 0)), 100); 285 286 // 8% is the minimal size that looks visually appealing while drawing an almost empty bar 287 p = roundOneDecimalNum(d.sumMemDealloc) !== 0 ? Math.max(p, 8) : 0; 288 return p + "%"; 289 }) 290 // Add the equivalent of to be added with CSS content 291 .attr("data-pseudo-content", "\u00a0"); 292 293 rows.append("td") 294 .attr("class", "table-memory membar-right-cell") 295 .append("div") 296 .attr("class", "membar") 297 .attr("title", percentMemTooltip) 298 .style("width", function(d) { 299 var p = Math.min(Math.max(Math.round(d.propMemAlloc * 100), 0), 100); 300 301 // 4% is the minimal size that looks visually appealing while drawing an almost empty bar 302 p = roundOneDecimalNum(d.sumMemAlloc) !== 0 ? Math.max(p, 4) : 0; 303 return p + "%"; 304 }) 305 // Add the equivalent of to be added with CSS content 306 .attr("data-pseudo-content", "\u00a0"); 307 308 rows.append("td") 309 .attr("class", "table-memory memory memory-right") 310 .attr("title", "Memory allocation (MB)") 311 .attr("data-pseudo-content", 312 function(d) { return roundOneDecimalNum(d.sumMemAlloc) !== 0 ? roundOneDecimal(d.sumMemAlloc) : ""; }); 313 314 rows.append("td") 315 .attr("class", "time") 316 .attr("title", "Total time (ms)") 317 .attr("data-pseudo-content", 318 function(d) { return Math.round(d.sumTime * 100) !== 0 ? (Math.round(d.sumTime * 100) / 100) : ""; }); 319 320 rows.append("td") 321 .attr("class", "timebar-cell") 322 .append("div") 323 .attr("class", "timebar") 324 .attr("title", percentTooltip) 325 .style("width", function(d) { 326 return Math.round(d.propTime * 100) + "%"; 327 }) 328 // Add the equivalent of to be added with CSS content 329 .attr("data-pseudo-content", "\u00a0"); 330 331 rows.append("td") 332 .attr("class", "spacing") 333 .attr("data-pseudo-content", "\u00a0"); 334 335 rows 336 .on("click", function(d) { 337 // Info box is only relevant when mousing over flamegraph 338 vis.infoBox.hide(); 339 highlighter.click(d); 340 notifySourceFileMessage(d, "select"); 341 }) 342 .on("mouseover", function(d) { 343 if (highlighter.isLocked()) return; 344 345 // Info box is only relevant when mousing over flamegraph 346 vis.infoBox.hide(); 347 highlighter.hover(d); 348 }) 349 .on("mouseout", function(d) { 350 if (highlighter.isLocked()) return; 351 352 highlighter.hover(null); 353 }) 354 .on("dblclick", function(d) { 355 notifySourceFileMessage(d, "open"); 356 }); 357 358 function hideZeroTimeRows() { 359 rows 360 .filter(function(d) { return d.sumTime === 0; }) 361 .style("display", "none"); 362 } 363 364 function showZeroTimeRows() { 365 rows 366 .filter(function(d) { return d.sumTime === 0; }) 367 .style("display", ""); 368 } 369 370 function addLockHighlight(d) { 371 var target = d; 372 rows 373 .filter(function(d) { return d === target; } ) 374 .classed({ locked: true }); 375 } 376 377 function clearLockHighlight() { 378 rows 379 .filter(".locked") 380 .classed({ locked: false }); 381 } 382 383 function addActiveHighlight(d) { 384 // If we have filename and linenum, search for cells that match, and 385 // set them as "active". 386 var target = d; 387 if (target.filename && target.linenum) { 388 var tr = rows 389 .filter(function(d) { 390 return d.linenum === target.linenum && 391 d.filename === target.filename; 392 }) 393 .classed({ active: true }); 394 395 tr.node().scrollIntoViewIfNeeded(); 396 } 397 } 398 399 function clearActiveHighlight() { 400 rows 401 .filter(".active") 402 .classed({ active: false }); 403 } 404 405 function enableScroll() { 406 // TODO: implement this 407 } 408 409 function disableScroll() { 410 } 411 412 function useMemoryResults() { 413 d3.selectAll(".table-memory").style("display", vis.hideMemory ? "none" : ""); 414 } 415 416 return { 417 el: el, 418 hideZeroTimeRows: hideZeroTimeRows, 419 showZeroTimeRows: showZeroTimeRows, 420 addLockHighlight: addLockHighlight, 421 clearLockHighlight: clearLockHighlight, 422 addActiveHighlight: addActiveHighlight, 423 clearActiveHighlight: clearActiveHighlight, 424 enableScroll: enableScroll, 425 disableScroll: disableScroll, 426 useMemoryResults: useMemoryResults 427 }; 428 } 429 430 431 var highlighter = (function() { 432 // D3 data objects for the currently locked and active items 433 var lockItem = null; 434 var activeItem = null; 435 436 function isLocked() { 437 return lockItem !== null; 438 } 439 440 function currentLock() { 441 return lockItem; 442 } 443 444 function currentActive() { 445 return activeItem; 446 } 447 448 449 // This is called when a flamegraph cell or a line of code is clicked on. 450 // Clicks also should trigger hover events. 451 function click(d) { 452 // If d is null (background is clicked), or if locked and this click 453 // is on the currently locked selection, just unlock and return. 454 if (d === null || (lockItem && d === lockItem)) { 455 lockItem = null; 456 vis.flameGraph.clearLockHighlight(); 457 vis.codeTable.clearLockHighlight(); 458 return; 459 } 460 461 // If nothing currently locked, or if locked and this click is on 462 // something other than the currently locked selection, then lock the 463 // current selection. 464 lockItem = d; 465 466 vis.flameGraph.clearLockHighlight(); 467 vis.codeTable.clearLockHighlight(); 468 hover(null); 469 470 vis.flameGraph.addLockHighlight(d); 471 vis.codeTable.addLockHighlight(d); 472 hover(d); 473 } 474 475 476 function hover(d) { 477 activeItem = d; 478 479 if (activeItem) { 480 vis.flameGraph.addActiveHighlight(activeItem); 481 vis.codeTable.addActiveHighlight(activeItem); 482 return; 483 } 484 485 vis.flameGraph.clearActiveHighlight(); 486 vis.codeTable.clearActiveHighlight(); 487 } 488 489 return { 490 isLocked: isLocked, 491 currentLock: currentLock, 492 currentActive: currentActive, 493 494 click: click, 495 hover: hover 496 }; 497 })(); 498 499 500 // Generate the flame graph ----------------------------------------------- 501 function generateFlameGraph(el) { 502 el.innerHTML = ""; 503 504 var stackHeight = 15; // Height of each layer on the stack, in pixels 505 var zoomMargin = 0.02; // Extra margin on sides when zooming to fit 506 507 // Dimensions ----------------------------------------------------------- 508 509 // Margin inside the svg where the plotting occurs 510 var dims = { 511 margin: { top: 0, right: 0, left: 0, bottom: 30 } 512 }; 513 dims.width = el.clientWidth - dims.margin.left - dims.margin.right; 514 dims.height = el.clientHeight - dims.margin.top - dims.margin.bottom; 515 516 var domains = { 517 x: [ 518 d3.min(vis.prof, function(d) { return d.startTime; }), 519 d3.max(vis.prof, function(d) { return d.endTime; }) 520 ], 521 y: [ 522 d3.min(vis.prof, function(d) { return d.depth; }) - 1, 523 d3.max(vis.prof, function(d) { return d.depth; }) 524 ] 525 }; 526 // Slightly expand x domain 527 domains.x = expandRange(domains.x, zoomMargin); 528 529 // Scales --------------------------------------------------------------- 530 var scales = { 531 x: d3.scale.linear() 532 .domain(domains.x) 533 .range([0, dims.width]), 534 535 y: d3.scale.linear() 536 .domain(domains.y) 537 .range([dims.height, dims.height - (domains.y[1] - domains.y[0]) * stackHeight]), 538 539 // This will be a function that, given a data point, returns the depth. 540 // This function can change; sometimes it returns the original depth, 541 // and sometimes it returns the collapsed depth. This isn't exactly a 542 // scale function, but it's close enough for our purposes. 543 getDepth: null 544 }; 545 546 function useCollapsedDepth() { 547 scales.getDepth = function(d) { return d.depthCollapsed; }; 548 } 549 function useUncollapsedDepth() { 550 scales.getDepth = function(d) { return d.depth; }; 551 } 552 553 useCollapsedDepth(); 554 555 // SVG container objects ------------------------------------------------ 556 var svg = d3.select(el).append('svg'); 557 558 var clipRect = svg.append("clipPath") 559 .attr("id", "clip-" + vis.el.id) 560 .append("rect"); 561 562 var container = svg.append('g') 563 .attr("transform", "translate(" + dims.margin.left + "," + dims.margin.top + ")") 564 .attr("clip-path", "url(" + urlNoHash() + "#clip-" + vis.el.id + ")"); 565 566 // Add a background rect so we have something to grab for zooming/panning 567 var backgroundRect = container.append("rect") 568 .attr("class", "background"); 569 570 // Axes ------------------------------------------------------------ 571 var xAxis = d3.svg.axis() 572 .scale(scales.x) 573 .orient("bottom"); 574 575 svg.append("g") 576 .attr("class", "x axis") 577 .call(xAxis); 578 579 // Container sizing ----------------------------------------------------- 580 // Update dimensions of various container elements, based on the overall 581 // dimensions of the containing div. 582 function updateContainerSize() { 583 dims.width = el.clientWidth - dims.margin.left - dims.margin.right; 584 dims.height = el.clientHeight - dims.margin.top - dims.margin.bottom; 585 586 svg 587 .attr('width', dims.width + dims.margin.left + dims.margin.right) 588 .attr('height', dims.height + dims.margin.top + dims.margin.bottom); 589 590 clipRect 591 .attr("x", dims.margin.left) 592 .attr("y", dims.margin.top) 593 .attr("width", dims.width) 594 .attr("height", dims.height); 595 596 backgroundRect 597 .attr("width", dims.width) 598 .attr("height", dims.height); 599 600 svg.select(".x.axis") 601 .attr("transform", "translate(" + dims.margin.left + "," + dims.height + ")"); 602 } 603 604 605 // Redrawing ------------------------------------------------------------ 606 607 // Redrawing is a little complicated. For performance reasons, the 608 // flamegraph cells that are offscreen aren't rendered; they're removed 609 // from the D3 selection of cells. However, when transitions are 610 // involved, it may be necssary to add objects in their correct 611 // off-screen starting locations before the transition, and then do the 612 // transition. Similarly, it may be necssary to transition objects to 613 // their correct off-screen ending positions. 614 // 615 // In order to handle this, whenever there's a transition, we need to 616 // have the scales for before the transition, and after. When a function 617 // invokes a transition, it will generally do the following: (1) save the 618 // previous scales, (2) modify the current scales, (3) call a redraw 619 // function. The redraw functions are customized for different types of 620 // transitions, and they will use the saved previous scales to position 621 // objects correctly for the transition. When there's no transition, the 622 // previous scales aren't needed, and the redrawImmediate() function 623 // should be used. 624 625 // Cache cells for faster access (avoid a d3.select()) 626 var cells; 627 628 // For a data element, return identifying key 629 function dataKey(d) { 630 return d.depth + "-" + d.startTime + "-" + d.endTime; 631 } 632 633 // For transitions with animation, we need to have a copy of the previous 634 // scales in addition to the current ones. 635 var prevScales = {}; 636 function savePrevScales() { 637 prevScales = { 638 x: scales.x.copy(), 639 y: scales.y.copy(), 640 getDepth: scales.getDepth 641 }; 642 } 643 savePrevScales(); 644 645 646 // Returns a D3 selection of the cells that are within the plotting 647 // region, using a set of scales. 648 function selectActiveCells(scales) { 649 var xScale = scales.x; 650 var yScale = scales.y; 651 var depth = scales.getDepth; 652 var width = dims.width; 653 var height = dims.height; 654 655 var data = vis.prof.filter(function(d) { 656 var depthVal = depth(d); 657 return !(xScale(d.endTime) < 0 || 658 xScale(d.startTime) > width || 659 depthVal === null || 660 yScale(depthVal - 1) < 0 || 661 yScale(depthVal) > height); 662 }); 663 664 cells = container.selectAll("g.cell").data(data, dataKey); 665 666 return cells; 667 } 668 669 // Given an enter selection, add the rect and text objects, but don't 670 // position them. Returns a selection of the new <g> elements. 671 // This should usually be called with addItems(sel.enter()) instead 672 // of sel.enter().call(addItems), because the latter returns the original 673 // enter selection, not the selection of <g> elements, and can't be 674 // used for chaining more function calls on the <g> selection. 675 function addItems(enterSelection) { 676 var cells = enterSelection.append("g") 677 .attr("class", "cell") 678 .classed("highlighted", function(d) { return d.filename !== null; }) 679 .call(addMouseEventHandlers); 680 681 // Add CSS classes for highlighting cells with labels that match particular 682 // regex patterns. 683 var highlightPatterns = d3.entries(message.highlight); 684 highlightPatterns.map(function(item) { 685 var cssClass = item.key; 686 var regexp = new RegExp(item.value); 687 688 cells.classed(cssClass, function(d) { 689 return d.label.search(regexp) !== -1; 690 }); 691 }); 692 693 cells.append("rect") 694 .attr("class", "rect"); 695 696 cells.append("text") 697 .attr("class", "profvis-label") 698 .text(function(d) { return d.label; }); 699 700 return cells; 701 } 702 703 // Given a selection, position the rects and labels, using a set of 704 // scales. 705 function positionItems(cells, scales) { 706 var xScale = scales.x; 707 var yScale = scales.y; 708 var depth = scales.getDepth; 709 710 cells.select("rect") 711 .attr("width", function(d) { 712 return xScale(d.endTime) - xScale(d.startTime); 713 }) 714 .attr("height", yScale(0) - yScale(1)) 715 .attr("x", function(d) { return xScale(d.startTime); }) 716 .attr("y", function(d) { return yScale(depth(d)); }); 717 718 cells.select("text") 719 .attr("x", function(d) { 720 // To place the labels, check if there's enough space to fit the 721 // label plus padding in the rect. (We already know the label fits 722 // without padding if we got here.) 723 // * If there's not enough space, simply center the label in the 724 // rect. 725 // * If there is enough space, keep the label within the rect, with 726 // padding. Try to left-align, keeping the label within the 727 // viewing area if possible. 728 729 // Padding on left and right 730 var pad = 2; 731 732 var textWidth = getLabelWidth(this, d.label.length); 733 var rectWidth = xScale(d.endTime) - xScale(d.startTime); 734 735 if (textWidth + pad*2 > rectWidth) { 736 return xScale(d.startTime) + (rectWidth - textWidth) / 2; 737 } else { 738 return Math.min( 739 Math.max(0, xScale(d.startTime)) + pad, 740 xScale(d.endTime) - textWidth - pad 741 ); 742 } 743 }) 744 .attr("y", function(d) { return yScale(depth(d) - 0.8); }); 745 746 return cells; 747 } 748 749 750 // Redraw without a transition (regular panning and zooming) 751 function redrawImmediate() { 752 cells = selectActiveCells(scales); 753 754 cells.exit().remove(); 755 addItems(cells.enter()) 756 .call(addLockHighlightSelection, highlighter.currentLock()) 757 .call(addActiveHighlightSelection, highlighter.currentActive()); 758 cells.call(positionItems, scales); 759 cells.select('text') 760 .call(updateLabelVisibility); 761 svg.select(".x.axis").call(xAxis); 762 } 763 764 // Redraw for double-click zooming, where there's a transition 765 function redrawZoom(duration) { 766 // Figure out if we're zooming in or out. This will determine when we 767 // recalculate the label visibility: before or after the transition. 768 var prevExtent = prevScales.x.domain()[1] - prevScales.x.domain()[0]; 769 var curExtent = scales.x.domain()[1] - scales.x.domain()[0]; 770 var zoomIn = curExtent < prevExtent; 771 772 cells = selectActiveCells(scales); 773 774 // Phase 1 775 // Add the enter items, highlight them, and position them using the 776 // previous scales 777 addItems(cells.enter()) 778 .call(addLockHighlightSelection, highlighter.currentLock()) 779 .call(addActiveHighlightSelection, highlighter.currentActive()) 780 .call(positionItems, prevScales); 781 782 // If zooming out, update label visibility. This will hide some labels 783 // now, before the transition, ensuring that they will never be larger 784 // than the box. 785 if (!zoomIn) { 786 cells.select('text') 787 .call(updateLabelVisibility); 788 } 789 790 // Phase 2 791 // Position the update (and enter) items using the new scales 792 cells 793 .transition().duration(duration) 794 .call(positionItems, scales); 795 796 // Position the exit items using the new scales 797 cells.exit() 798 .transition().duration(duration) 799 .call(positionItems, scales); 800 801 // Update x axis 802 svg.select(".x.axis") 803 .transition().duration(duration) 804 .call(xAxis); 805 806 // Phase 3 807 // If zooming in, update label visibility. This will hide some labels 808 // now, after the transition, ensuring that they will never be larger 809 // than the box. 810 if (zoomIn) { 811 cells.select('text') 812 .transition().delay(duration) 813 .call(updateLabelVisibility); 814 } 815 816 // Remove the exit items 817 cells.exit() 818 .transition().delay(duration) 819 .remove(); 820 } 821 822 // Redraw when internal functions are hidden 823 function redrawCollapse(exitDuration, updateDuration) { 824 cells = selectActiveCells(scales); 825 826 // There are two subsets of the exit items: 827 // 1. Those that exit because depth is null. These should fade out. 828 // 2. Those that exit because they move off screen. These should wait 829 // for subset 1 to fade out, then move with a transition. 830 var fadeOutCells = cells.exit() 831 .filter(function(d) { return scales.getDepth(d) === null; }); 832 var moveOutCells = cells.exit() 833 .filter(function(d) { return scales.getDepth(d) !== null; }); 834 835 // Phase 1 836 // Add the enter items, highlight them, and position them using the 837 // previous scales 838 addItems(cells.enter()) 839 .call(addLockHighlightSelection, highlighter.currentLock()) 840 .call(addActiveHighlightSelection, highlighter.currentActive()) 841 .call(positionItems, prevScales); 842 843 cells.select('text') 844 .call(updateLabelVisibility); 845 846 // Phase 2 847 // Fade out the items that have a null depth 848 fadeOutCells 849 .transition().duration(exitDuration) 850 .style("opacity", 0); 851 852 // Phase 3 853 // Position the update (and enter) items using the new scales 854 cells 855 .transition().delay(exitDuration).duration(updateDuration) 856 .call(positionItems, scales); 857 858 // Position the exit items that move out, using the new scales 859 moveOutCells 860 .transition().delay(exitDuration).duration(updateDuration) 861 .call(positionItems, scales); 862 863 // Phase 4 864 // Remove all the exit items 865 cells.exit() 866 .transition().delay(exitDuration + updateDuration) 867 .remove(); 868 } 869 870 // Redraw when internal functions are un-hidden 871 function redrawUncollapse(updateDuration, enterDuration) { 872 cells = selectActiveCells(scales); 873 874 var enterCells = addItems(cells.enter()); 875 // There are two subsets of the enter items: 876 // 1. Those that enter because they move on screen (but the previous 877 // depth was not null). These should move with a transition. 878 // 2. Those that enter because the previous depth was null. These 879 // should wait for subset 1 to move, then fade in. 880 var moveInCells = enterCells 881 .filter(function(d) { return prevScales.getDepth(d) !== null; }); 882 var fadeInCells = enterCells 883 .filter(function(d) { return prevScales.getDepth(d) === null; }); 884 885 // Phase 1 886 // Highlight and position the move-in items with the old scales 887 moveInCells 888 .call(addLockHighlightSelection, highlighter.currentLock()) 889 .call(addActiveHighlightSelection, highlighter.currentActive()) 890 .call(positionItems, prevScales); 891 892 cells.select('text') 893 .call(updateLabelVisibility); 894 895 // Phase 2 896 // Position the move-in, update, and exit items with a transition 897 moveInCells 898 .transition().duration(updateDuration) 899 .call(positionItems, scales); 900 cells 901 .transition().duration(updateDuration) 902 .call(positionItems, scales); 903 cells.exit() 904 .transition().duration(updateDuration) 905 .call(positionItems, scales); 906 907 // Phase 3 908 // Highlight and position the fade-in items, then fade in 909 fadeInCells 910 .call(addLockHighlightSelection, highlighter.currentLock()) 911 .call(addActiveHighlightSelection, highlighter.currentActive()) 912 .call(positionItems, scales) 913 .style("opacity", 0) 914 .transition().delay(updateDuration).duration(enterDuration) 915 .style("opacity", 1); 916 917 // Phase 4 918 // Remove the exit items 919 cells.exit() 920 .transition().delay(updateDuration + enterDuration) 921 .remove(); 922 } 923 924 925 // Calculate whether to display label in each cell ---------------------- 926 927 // Finding the dimensions of SVG elements is expensive. We'll reduce the 928 // calls getBoundingClientRect() by caching the dimensions. 929 930 // Cache the width of labels. This is a lookup table which, given the 931 // number of characters, gives the number of pixels. The label width 932 // never changes, so we can keep it outside of updateLabelVisibility(). 933 var labelWidthTable = {}; 934 function getLabelWidth(el, nchar) { 935 // Add entry if it doesn't already exist 936 if (labelWidthTable[nchar] === undefined) { 937 // If the text isn't displayed, then we can't get its width. Make 938 // sure it's visible, get the width, and then restore original 939 // display state. 940 var oldDisplay = el.style.display; 941 el.style.display = "inline"; 942 labelWidthTable[nchar] = el.getBoundingClientRect().width; 943 el.style.display = oldDisplay; 944 } 945 return labelWidthTable[nchar]; 946 } 947 948 // Show labels that fit in the corresponding rectangle, and hide others. 949 function updateLabelVisibility(labels) { 950 // Cache the width of rects. This is a lookup table which, given the 951 // timespan (width in data), gives the number of pixels. The width of 952 // rects changes with the x scale, so we have to rebuild the table each 953 // time the scale changes. 954 var rectWidthTable = {}; 955 var x0 = scales.x(0); 956 function getRectWidth(time) { 957 // Add entry if it doesn't already exist 958 if (rectWidthTable[time] === undefined) { 959 rectWidthTable[time] = scales.x(time) - x0; 960 } 961 return rectWidthTable[time]; 962 } 963 964 // Now calculate text and rect width for each cell. 965 labels.style("display", function(d) { 966 var labelWidth = getLabelWidth(this, d.label.length); 967 var boxWidth = getRectWidth(d.endTime - d.startTime); 968 969 return (labelWidth <= boxWidth) ? "" : "none"; 970 }); 971 972 return labels; 973 } 974 975 976 function onResize() { 977 updateContainerSize(); 978 979 scales.x.range([0, dims.width]); 980 zoom.x(scales.x); 981 982 // Preserve distance from bottom, instead of from top (which is the 983 // default behavior). 984 scales.y.range([ 985 dims.height, 986 dims.height - (domains.y[1] - domains.y[0]) * stackHeight 987 ]); 988 redrawImmediate(); 989 } 990 991 // Attach mouse event handlers ------------------------------------ 992 var dragging = false; 993 994 function addMouseEventHandlers(cells) { 995 cells 996 .on("mouseup", function(d) { 997 if (dragging) return; 998 999 // If it wasn't a drag, treat it as a click 1000 vis.infoBox.show(d); 1001 highlighter.click(d); 1002 notifySourceFileMessage(d, "select"); 1003 }) 1004 .on("mouseover", function(d) { 1005 if (dragging) return; 1006 1007 // If no label currently shown, display a tooltip 1008 var label = this.querySelector(".profvis-label"); 1009 if (label.style.display === "none") { 1010 var box = this.getBBox(); 1011 showTooltip( 1012 d.label, 1013 box.x + box.width / 2, 1014 box.y - box.height 1015 ); 1016 } 1017 1018 if (!highlighter.isLocked()) { 1019 vis.infoBox.show(d); 1020 highlighter.hover(d); 1021 } 1022 }) 1023 .on("mouseout", function(d) { 1024 if (dragging) return; 1025 1026 hideTooltip(); 1027 1028 if (!highlighter.isLocked()) { 1029 vis.infoBox.hide(); 1030 highlighter.hover(null); 1031 } 1032 }) 1033 .on("dblclick.zoomcell", function(d) { 1034 // When a cell is double-clicked, zoom x to that cell's width. 1035 savePrevScales(); 1036 1037 scales.x.domain(expandRange([d.startTime, d.endTime], zoomMargin)); 1038 zoom.x(scales.x); 1039 1040 redrawZoom(250); 1041 1042 notifySourceFileMessage(d, "open"); 1043 }); 1044 1045 return cells; 1046 } 1047 1048 // Tooltip -------------------------------------------------------- 1049 function showTooltip(label, x, y) { 1050 var tooltip = container.append("g").attr("class", "profvis-tooltip"); 1051 var tooltipRect = tooltip.append("rect"); 1052 var tooltipLabel = tooltip.append("text") 1053 .text(label) 1054 .attr("x", x) 1055 .attr("y", y + stackHeight * 0.2); // Shift down slightly for baseline 1056 1057 // Add box around label 1058 var labelBox = tooltipLabel.node().getBBox(); 1059 var rectWidth = labelBox.width + 10; 1060 var rectHeight = labelBox.height + 4; 1061 tooltipRect 1062 .attr("width", rectWidth) 1063 .attr("height", rectHeight) 1064 .attr("x", x - rectWidth / 2) 1065 .attr("y", y - rectHeight / 2) 1066 .attr("rx", 4) // Rounded corners -- can't set this in CSS 1067 .attr("ry", 4); 1068 } 1069 1070 function hideTooltip() { 1071 container.select("g.profvis-tooltip").remove(); 1072 } 1073 1074 1075 // Highlighting --------------------------------------------------------- 1076 1077 function addLockHighlight(d) { 1078 addLockHighlightSelection(cells, d); 1079 } 1080 1081 function clearLockHighlight() { 1082 cells 1083 .filter(".locked") 1084 .classed({ locked: false }); 1085 } 1086 1087 1088 function addActiveHighlight(d) { 1089 if (!d) return; 1090 addActiveHighlightSelection(cells, d); 1091 } 1092 1093 function clearActiveHighlight() { 1094 cells 1095 .filter(".active") 1096 .classed({ active: false }); 1097 } 1098 1099 // These are versions of addLockHighlight and addActiveHighlight which 1100 // are only internally visible. It must be passed a selection of cells to 1101 // perform the highlighting on. This can be more efficient because it can 1102 // operate on just an enter selection instead of all cells. 1103 function addLockHighlightSelection(selection, d) { 1104 if (!d) return; 1105 1106 var target = d; 1107 selection 1108 .filter(function(d) { return d === target; } ) 1109 .classed({ locked: true }) 1110 .call(moveToFront); 1111 } 1112 1113 function addActiveHighlightSelection(selection, d) { 1114 if (!d) return; 1115 1116 var target = d; 1117 if (target.filename && target.linenum) { 1118 selection 1119 .filter(function(d) { 1120 // Check for filename and linenum match, and if provided, a label match. 1121 var match = d.filename === target.filename && 1122 d.linenum === target.linenum; 1123 if (!!target.label) { 1124 match = match && (d.label === target.label); 1125 } 1126 return match; 1127 }) 1128 .classed({ active: true }); 1129 1130 } else if (target.label) { 1131 // Don't highlight blocks for these labels 1132 var exclusions = ["<Anonymous>", "FUN"]; 1133 if (exclusions.some(function(x) { return target.label === x; })) { 1134 return; 1135 } 1136 1137 // If we only have the label, search for cells that match, but make sure 1138 // to not select ones that have a filename and linenum. 1139 selection 1140 .filter(function(d) { 1141 return d.label === target.label && 1142 d.filename === null && 1143 d.linenum === null; 1144 }) 1145 .classed({ active: true }); 1146 } 1147 } 1148 1149 // Move a D3 selection to front. If this is called on a selection, that 1150 // selection should have been created with a data indexing function (e.g. 1151 // data(data, function(d) { return ... })). Otherwise, the wrong object 1152 // may be moved to the front. 1153 function moveToFront(selection) { 1154 return selection.each(function() { 1155 this.parentNode.appendChild(this); 1156 }); 1157 } 1158 1159 1160 // Panning and zooming -------------------------------------------- 1161 // For panning and zooming x, d3.behavior.zoom does most of what we want 1162 // automatically. For panning y, we can't use d3.behavior.zoom becuase it 1163 // will also automatically add zooming, which we don't want. Instead, we 1164 // need to use d3.behavior.drag and set the y domain appropriately. 1165 var drag = d3.behavior.drag() 1166 .on("drag", function() { 1167 dragging = true; 1168 var y = scales.y; 1169 var ydom = y.domain(); 1170 var ydiff = y.invert(d3.event.dy) - y.invert(0); 1171 y.domain([ydom[0] - ydiff, ydom[1] - ydiff]); 1172 }); 1173 1174 1175 // For mousewheel zooming, we need to limit zoom amount. This is needed 1176 // because in Firefox, zoom increments are too big. To do this, we limit 1177 // scaleExtent before the first zoom event, and after each subsequent 1178 // one. 1179 // 1180 // When zooming out, there's an additional limit: never zoom out past 1181 // the original zoom span. The reason it's necessary to calculate this 1182 // each time, instead of simply setting the scaleExtent() so that the 1183 // lower bound is 1, is because other zoom events (like 1184 // dblclick.zoomcell) are able to change the domain of scales.x, without 1185 // changing the value of zoom.scale(). This means that the relationship 1186 // between the zoom.scale() does not have a fixed linear relationship to 1187 // the span of scales.x, and we have to recalculate it. 1188 var maxZoomPerStep = 1.1; 1189 1190 function zoomOutLimit() { 1191 var span = scales.x.domain()[1] - scales.x.domain()[0]; 1192 var startSpan = domains.x[1] - domains.x[0]; 1193 return Math.min(maxZoomPerStep, startSpan/span); 1194 } 1195 1196 var zoom = d3.behavior.zoom() 1197 .x(scales.x) 1198 .on("zoomstart", function() { 1199 zoom.scaleExtent([zoom.scale() / zoomOutLimit(), zoom.scale() * maxZoomPerStep]); 1200 }) 1201 .on("zoom", function(e) { 1202 redrawImmediate(); 1203 zoom.scaleExtent([zoom.scale() / zoomOutLimit(), zoom.scale() * maxZoomPerStep]); 1204 }); 1205 1206 // Register drag before zooming, because we need the drag to set the y 1207 // scale before the zoom triggers a redraw. 1208 svg 1209 .on("mouseup", function(d) { 1210 dragging = false; 1211 }) 1212 .call(drag); 1213 1214 // Unlock selection when background is clicked, and zoom out when 1215 // background is double-clicked. 1216 backgroundRect 1217 .on("mouseup", function(d) { 1218 if (dragging) return; 1219 1220 // If it wasn't a drag, hide info box and unlock. 1221 vis.infoBox.hide(); 1222 highlighter.click(null); 1223 }) 1224 .on("dblclick.zoombackground", function() { 1225 savePrevScales(); 1226 1227 scales.x.domain(domains.x); 1228 zoom.x(scales.x); 1229 1230 redrawZoom(250); 1231 }); 1232 1233 1234 var zoomEnabled = false; 1235 function disableZoom() { 1236 if (zoomEnabled) { 1237 svg.on(".zoom", null); 1238 zoomEnabled = false; 1239 } 1240 } 1241 function enableZoom() { 1242 if (!zoomEnabled) { 1243 svg 1244 .call(zoom) 1245 .on("dblclick.zoom", null); // Disable zoom's built-in double-click behavior 1246 zoomEnabled = true; 1247 } 1248 } 1249 enableZoom(); 1250 1251 onResize(); 1252 1253 return { 1254 el: el, 1255 onResize: onResize, 1256 onUpdateInternals: onResize, 1257 redrawImmediate: redrawImmediate, 1258 redrawZoom: redrawZoom, 1259 redrawCollapse: redrawCollapse, 1260 redrawUncollapse: redrawUncollapse, 1261 savePrevScales: savePrevScales, 1262 useCollapsedDepth: useCollapsedDepth, 1263 useUncollapsedDepth: useUncollapsedDepth, 1264 addLockHighlight: addLockHighlight, 1265 clearLockHighlight: clearLockHighlight, 1266 addActiveHighlight: addActiveHighlight, 1267 clearActiveHighlight: clearActiveHighlight, 1268 disableZoom: disableZoom, 1269 enableZoom: enableZoom 1270 }; 1271 } // generateFlameGraph 1272 1273 1274 function initInfoBox(el) { 1275 1276 function show(d) { 1277 var label = d.label ? d.label : ""; 1278 var ref = (d.filename && d.linenum) ? 1279 (d.filename + "#" + d.linenum) : 1280 "(source unavailable)"; 1281 1282 el.style.visibility = ""; 1283 1284 el.innerHTML = 1285 "<table>" + 1286 "<tr><td class='infobox-title'>Label</td><td>" + escapeHTML(label) + "</td></tr>" + 1287 "<tr><td class='infobox-title'>Called from</td><td>" + escapeHTML(ref) + "</td></tr>" + 1288 "<tr><td class='infobox-title'>Total time</td><td>" + (d.endTime - d.startTime) + "ms</td></tr>" + 1289 "<tr><td class='infobox-title'>Memory</td><td>" + 1290 roundOneDecimal(d.sumMemDealloc) + " / " + roundOneDecimal(d.sumMemAlloc) + 1291 " MB</td></tr>" + 1292 "<tr><td class='infobox-title'>Agg. total time</td><td>" + vis.aggLabelTimes[label] + "ms</td></tr>" + 1293 "<tr><td class='infobox-title'>Call stack depth</td><td>" + d.depth + "</td></tr>" + 1294 "</table>"; 1295 } 1296 1297 function hide() { 1298 el.style.visibility = "hidden"; 1299 } 1300 1301 hide(); 1302 1303 return { 1304 el: el, 1305 show: show, 1306 hide: hide 1307 }; 1308 } 1309 1310 // Generate the tree table ---------------------------------------- 1311 function generateTreetable(el) { 1312 var content = d3.select(el); 1313 1314 var table = content.append("table") 1315 .attr("class", "results") 1316 .attr("cellspacing", "0") 1317 .attr("cellpadding", "0"); 1318 1319 table.append("col"); 1320 table.append("col") 1321 .style("width", "120px"); 1322 table.append("col") 1323 .style("width", "50px") 1324 .attr("class", "treetable-memory"); 1325 table.append("col") 1326 .style("width", "26px") 1327 .attr("class", "treetable-memory"); 1328 table.append("col") 1329 .style("width", "50px") 1330 .attr("class", "treetable-memory"); 1331 table.append("col") 1332 .style("width", "50px"); 1333 table.append("col") 1334 .style("width", "40px"); 1335 1336 var tableBody = table.append("tbody"); 1337 1338 var headerRows = tableBody.append("tr"); 1339 1340 headerRows.append("th") 1341 .attr("class", "code-label") 1342 .text("Code"); 1343 1344 headerRows.append("th") 1345 .attr("class", "path") 1346 .text("File"); 1347 1348 headerRows.append("th") 1349 .attr("class", "treetable-memory memory") 1350 .attr("colspan", "3") 1351 .text("Memory (MB)"); 1352 1353 headerRows.append("th") 1354 .attr("class", "time") 1355 .attr("colspan", "2") 1356 .text("Time (ms)"); 1357 1358 // Retrieve all nodes (n), recursevely, where check(n) == true. 1359 function allTopNodes(nodes, check) { 1360 var included = []; 1361 nodes = nodes.slice(); 1362 1363 while (nodes.length > 0) { 1364 var node = nodes.shift(); 1365 1366 if (check(node)) 1367 included.push(node); 1368 else { 1369 node.sumChildren.forEach(function(c1) { 1370 nodes.unshift(c1); 1371 }); 1372 } 1373 } 1374 return included; 1375 } 1376 1377 // Is there one node (n), including root, where check(n) == true? 1378 function oneNode(root, check) { 1379 var nodes = [root]; 1380 1381 while (nodes.length > 0) { 1382 var n = nodes.shift(); 1383 if (check(n)) 1384 return true; 1385 1386 n.sumChildren.forEach(function(x) { 1387 nodes.unshift(x); 1388 }); 1389 } 1390 1391 return false; 1392 } 1393 1394 function updateRowsDisplay(d) { 1395 if (vis.hideInternals && d.isInternal) 1396 return "none"; 1397 else if (!vis.hideInternals && d.isDescendant) 1398 return "none"; 1399 1400 var collapsed = false; 1401 while (d.parent) { 1402 d = d.parent; 1403 if (d.collapsed) { 1404 collapsed = true; 1405 break; 1406 } 1407 } 1408 return collapsed ? "none" : ""; 1409 } 1410 1411 function toggleTreeNode(d) { 1412 if (!d.canExpand) 1413 return; 1414 1415 var collapsed = d.collapsed; 1416 if (collapsed === undefined) { 1417 // Create a copy since we might insert the same node twice: once 1418 // for the normal leaf the other one for a collapsed node. 1419 var sumChildren = d.sumChildren.map(function(x) { 1420 return jQuery.extend({}, x); 1421 }); 1422 1423 var childNodes = sumChildren.filter(function(x) { 1424 return x.depthCollapsed !== null; 1425 }); 1426 1427 childNodes.forEach(function(x) { 1428 x.isInternal = d.isInternal ? d.isInternal : false; 1429 x.isDescendant = d.isDescendant ? d.isDescendant : false; 1430 }); 1431 1432 var internalChildNodes = sumChildren.filter(function(x) { 1433 return x.depthCollapsed === null; 1434 }); 1435 1436 internalChildNodes.forEach(function(x) { 1437 x.isInternal = true; 1438 x.isDescendant = false; 1439 }); 1440 1441 var notInternalDescendantNodes = []; 1442 if (!d.isInternal) { 1443 notInternalDescendantNodes = allTopNodes(internalChildNodes, function(x) { 1444 return x.depthCollapsed !== null && d.depth < x.depth; 1445 }); 1446 } 1447 1448 notInternalDescendantNodes.forEach(function(x) { 1449 x.isInternal = false; 1450 x.isDescendant = true; 1451 }); 1452 1453 childNodes = childNodes.concat(internalChildNodes); 1454 childNodes = childNodes.concat(notInternalDescendantNodes); 1455 1456 childNodes.forEach(function(n) { 1457 n.visualDepth = d.visualDepth + 1; 1458 n.parent = d; 1459 }); 1460 1461 vis.profTable = vis.profTable.concat(childNodes); 1462 d.collapsed = false; 1463 1464 updateRows(); 1465 1466 // Nodes are sorted "heaviest first" 1467 if (childNodes.length == 1) toggleTreeNode(childNodes[0]); 1468 } 1469 else { 1470 d.collapsed = !collapsed; 1471 updateRows(); 1472 } 1473 } 1474 1475 function updateLabelCells(labelCell) { 1476 labelCell 1477 .attr("nowrap", "true") 1478 .style("padding-left", function(d) { 1479 return (8 + 15 * (d.visualDepth - 1)) + "px"; 1480 }) 1481 .on("click", toggleTreeNode) 1482 .attr("class", function(d) { 1483 d.canExpand = false; 1484 if (d.sumChildren) { 1485 d.sumChildren.forEach(function(c) { 1486 if (c.sumChildren.length > 0) { 1487 if (!vis.hideInternals || oneNode(c, function(c1) { return c1.depthCollapsed !== null; })) 1488 d.canExpand = true; 1489 } 1490 }); 1491 } 1492 1493 var collapsedClass = ""; 1494 if (d.canExpand) 1495 collapsedClass = d.collapsed === undefined ? "treetable-expand" : d.collapsed ? "treetable-expand" : "treetable-collapse"; 1496 1497 return "code-label " + (d.canExpand ? "label-pointer " + collapsedClass : ""); 1498 }); 1499 } 1500 1501 function updateRows() { 1502 var rows = tableBody.selectAll("tr.treetable-row") 1503 .data(vis.profTable, function(d) { 1504 return d.id; 1505 }); 1506 1507 rows.exit() 1508 .remove(); 1509 1510 var updatedRows = rows 1511 .style("display", updateRowsDisplay); 1512 1513 var updatedLabelCells = updatedRows.selectAll("td.code-label"); 1514 updateLabelCells(updatedLabelCells); 1515 1516 var newRows = rows.enter() 1517 .append("tr") 1518 .filter(function(d) { 1519 if (vis.hideInternals && d.depthCollapsed === null) 1520 return false; 1521 1522 return true; 1523 }) 1524 .on("click", function(d) { 1525 table.selectAll("tr") 1526 .style("background-color", null); 1527 1528 this.style.backgroundColor = "rgb(241, 241, 241)"; 1529 notifySourceFileMessage(d, "select"); 1530 }) 1531 .style("display", updateRowsDisplay); 1532 1533 newRows 1534 .attr("class", "treetable-row"); 1535 1536 var labelCell = newRows.append("td"); 1537 updateLabelCells(labelCell); 1538 1539 var cellWrapper = labelCell.append("div"); 1540 cellWrapper.append("div"); 1541 1542 labelCell.append("div") 1543 .attr("class", "label-text") 1544 .text(function(d) { return d.label; }); 1545 1546 newRows.append("td") 1547 .attr("class", "path") 1548 .text(function(d) { 1549 var lastSlash = d.filename ? d.filename.lastIndexOf("/") : -1; 1550 if (lastSlash >= 0) 1551 return d.filename.substr(lastSlash + 1); 1552 1553 return d.filename; 1554 }); 1555 1556 newRows.append("td") 1557 .attr("class", "treetable-memory memory-info") 1558 .text(function(d) { 1559 return roundOneDecimal(d.sumMemDealloc); 1560 }); 1561 1562 var memoryBarContainer = newRows.append("td") 1563 .attr("class", "treetable-memory memory-bar-container"); 1564 1565 var memoryLeftCell = memoryBarContainer.append("div") 1566 .attr("class", "memory-leftbar-wrapper"); 1567 1568 memoryLeftCell.append("div") 1569 .attr("class", "memory-leftbar") 1570 .style("width", function(d) { 1571 return 1 + Math.min(Math.abs(Math.min(Math.round(d.propMemDealloc * 5), 0)), 5) + "px"; 1572 }); 1573 1574 memoryBarContainer.append("div") 1575 .attr("class", "memory-rightbar") 1576 .style("width", function(d) { 1577 return 1 + Math.min(Math.max(Math.round(d.propMemAlloc * 13), 0), 13) + "px"; 1578 }); 1579 1580 newRows.append("td") 1581 .attr("class", "treetable-memory memory-info-right") 1582 .text(function(d) { 1583 return roundOneDecimal(d.sumMemAlloc); 1584 }); 1585 1586 newRows.append("td") 1587 .attr("class", "time-info") 1588 .text(function(d) { 1589 return d.sumTime; 1590 }); 1591 1592 var timeCell = newRows.append("td") 1593 .attr("class", "time-bar-container"); 1594 1595 timeCell.append("div") 1596 .attr("class", "timebar") 1597 .style("width", function(d) { 1598 return Math.round(d.propTime * 20) + "px"; 1599 }); 1600 1601 var unorderedRows = d3.selectAll("tr.treetable-row") 1602 .data(vis.profTable, function(d) { 1603 return d.id; 1604 }); 1605 1606 unorderedRows.sort(function(a,b) { 1607 return (a.id < b.id) ? -1 : (a.id == b.id ? 0 : 1); 1608 }); 1609 1610 useMemoryResults(); 1611 } 1612 1613 var buildProfTable = function (profTree) { 1614 var head = jQuery.extend({}, profTree); 1615 var nodes = [head]; 1616 1617 var aggregateChildren = function(node) { 1618 var nameMap = {}; 1619 node.children.forEach(function(c) { 1620 var nameMapEntry = nameMap[c.label]; 1621 if (!nameMapEntry) { 1622 nameMapEntry = jQuery.extend({}, c); 1623 nameMapEntry.sumTime = c.endTime - c.startTime; 1624 nameMapEntry.sumChildren = []; 1625 nameMapEntry.children = []; 1626 nameMapEntry.parent = node; 1627 nameMapEntry.sumCount = 1; 1628 } 1629 else { 1630 nameMapEntry.sumMem = nameMapEntry.sumMem + c.sumMem; 1631 nameMapEntry.sumMemDealloc = nameMapEntry.sumMemDealloc + c.sumMemDealloc; 1632 nameMapEntry.sumMemAlloc = nameMapEntry.sumMemAlloc + c.sumMemAlloc; 1633 nameMapEntry.sumTime = nameMapEntry.sumTime + (c.endTime - c.startTime); 1634 nameMapEntry.sumCount = nameMapEntry.sumCount + 1; 1635 } 1636 1637 nameMapEntry.propMem = nameMapEntry.sumMem / vis.totalMem; 1638 nameMapEntry.propMemDealloc = nameMapEntry.sumMemDealloc / vis.totalMem; 1639 nameMapEntry.propMemAlloc = nameMapEntry.sumMemAlloc / vis.totalMem; 1640 nameMapEntry.propTime = nameMapEntry.sumTime / vis.totalTime; 1641 1642 c.children.forEach(function(e) { 1643 nameMapEntry.children.push(e); 1644 }); 1645 1646 nameMap[c.label] = nameMapEntry; 1647 }); 1648 1649 var childrenSum = []; 1650 for (var label in nameMap) { 1651 childrenSum.push(nameMap[label]); 1652 } 1653 1654 // Sort by time descending 1655 childrenSum.sort(function(a, b) { return b.sumTime - a.sumTime }); 1656 return childrenSum; 1657 }; 1658 1659 function addToNodesAt(c, i) { 1660 nodes.splice(i, 0, c); 1661 } 1662 1663 var id = 0; 1664 while (nodes.length > 0) { 1665 var node = nodes.shift(); 1666 1667 node.id = id; 1668 id = id + 1; 1669 1670 node.sumChildren = aggregateChildren(node); 1671 1672 // Processing in order is important to preserve order of IDs! 1673 node.sumChildren.forEach(addToNodesAt); 1674 } 1675 1676 return head.sumChildren; 1677 }; 1678 1679 function useMemoryResults() { 1680 d3.selectAll(".treetable-memory").style("display", vis.hideMemory ? "none" : ""); 1681 } 1682 1683 vis.profTable = buildProfTable(vis.profTree); 1684 vis.profTable.forEach(function(e) { 1685 e.visualDepth = 1; 1686 }); 1687 1688 updateRows(); 1689 1690 return { 1691 el: el, 1692 onResize: updateRows, 1693 onOptionsChange: updateRows, 1694 onUpdateInternals: function() { 1695 1696 }, 1697 useMemoryResults: useMemoryResults 1698 }; 1699 } 1700 1701 function enableScroll() { 1702 vis.codeTable.enableScroll(); 1703 vis.flameGraph.enableZoom(); 1704 } 1705 1706 function disableScroll() { 1707 vis.codeTable.disableScroll(); 1708 vis.flameGraph.disableZoom(); 1709 } 1710 1711 1712 // Set up resizing -------------------------------------------------------- 1713 1714 // This is used as a jQuery event namespace so that we can remove the window 1715 // resize handler on subsequent calls to initResizing(). Not elegant, but it 1716 // gets the job done. 1717 var resizeCallbackNamespace = randomString(10); 1718 1719 // Resize panel1 and panel2 to 50% of available space and add callback 1720 // for window resizing. 1721 function initResizing() { 1722 var $el = $(vis.el); 1723 var $panel1 = $el.children(".profvis-panel1"); 1724 var $panel2 = $el.children(".profvis-panel2"); 1725 var $splitBar = $el.children(".profvis-splitbar"); 1726 var $statusBar = $el.children(".profvis-status-bar"); 1727 1728 // Clear any existing positioning that may have happened from previous 1729 // calls to this function and the callbacks that it sets up. 1730 $panel1.removeAttr("style"); 1731 $panel2.removeAttr("style"); 1732 $splitBar.removeAttr("style"); 1733 $statusBar.removeAttr("style"); 1734 1735 // CSS class suffix for split direction 1736 var splitClass = (vis.splitDir === "h") ? "horizontal" : "vertical"; 1737 1738 // Remove existing horizontal/vertical class and add the correct class back. 1739 $panel1.removeClass("profvis-panel1-horizontal profvis-panel1-vertical"); 1740 $panel2.removeClass("profvis-panel2-horizontal profvis-panel2-vertical"); 1741 $splitBar.removeClass("profvis-splitbar-horizontal profvis-splitbar-vertical"); 1742 $panel1.addClass("profvis-panel1-" + splitClass); 1743 $panel2.addClass("profvis-panel2-" + splitClass); 1744 $splitBar.addClass("profvis-splitbar-" + splitClass); 1745 1746 1747 var splitBarGap; 1748 var margin; 1749 // Record the proportions from the previous call to resizePanels. This is 1750 // needed when we resize the window to preserve the same proportions. 1751 var lastSplitProportion; 1752 1753 if (vis.splitDir === "v") { 1754 // Record the gap between the split bar and the objects to left and right 1755 splitBarGap = { 1756 left: $splitBar.offset().left - offsetRight($panel1), 1757 right: $panel2.offset().left - offsetRight($splitBar) 1758 }; 1759 1760 // Capture the initial distance from the left and right of container element 1761 margin = { 1762 left: $panel1.position().left, 1763 right: $el.innerWidth() - positionRight($panel2) 1764 }; 1765 1766 } else if (vis.splitDir === "h") { 1767 splitBarGap = { 1768 top: $splitBar.offset().top - offsetBottom($panel1), 1769 bottom: $panel2.offset().top - offsetBottom($splitBar) 1770 }; 1771 1772 margin = { 1773 top: $panel1.position().top, 1774 bottom: $el.innerWidth() - positionBottom($panel2) 1775 }; 1776 } 1777 1778 // Resize the panels. splitProportion is a number from 0-1 representing the 1779 // horizontal position of the split bar. 1780 function resizePanels(splitProportion) { 1781 if (!splitProportion) 1782 splitProportion = lastSplitProportion; 1783 1784 if (vis.splitDir === "v") { 1785 var innerWidth = offsetRight($panel2) - $panel1.offset().left; 1786 1787 $splitBar.offset({ 1788 left: $panel1.offset().left + innerWidth * splitProportion - 1789 $splitBar.outerWidth()/2 1790 }); 1791 1792 // Size and position the panels 1793 $panel1.outerWidth($splitBar.position().left - splitBarGap.left - 1794 margin.left); 1795 $panel2.offset({ left: offsetRight($splitBar) + splitBarGap.right }); 1796 1797 } else if (vis.splitDir === "h") { 1798 var innerHeight = offsetBottom($panel2) - $panel1.offset().top; 1799 1800 $splitBar.offset({ 1801 top: $panel1.offset().top + innerHeight * splitProportion - 1802 $splitBar.outerHeight()/2 1803 }); 1804 1805 // Size and position the panels 1806 $panel1.outerHeight($splitBar.position().top - splitBarGap.top - 1807 margin.top); 1808 $panel2.offset({ top: offsetBottom($splitBar) + splitBarGap.bottom }); 1809 } 1810 1811 lastSplitProportion = splitProportion; 1812 } 1813 1814 // Initially, set widths to 50/50 1815 // For the first sizing, we don't need to call vis.flameGraph.onResize() 1816 // because this happens before the flame graph is generated. 1817 resizePanels(0.5); 1818 1819 var resizePanelsDebounced = debounce(function() { 1820 resizePanels(lastSplitProportion); 1821 vis.activeViews.forEach(function(e) { 1822 if (e.onResize) e.onResize(); 1823 }); 1824 }, 250); 1825 1826 // Clear old resize handler and add new one. We use a namespace for this 1827 // visualization to make sure not to delete handlers for other profvis 1828 // visualizations on the same page (this can happen with Rmd documents). 1829 $(window).off("resize.profvis." + resizeCallbackNamespace); 1830 $(window).on("resize.profvis." + resizeCallbackNamespace, resizePanelsDebounced); 1831 1832 // Get current proportional position of split bar 1833 function splitProportion() { 1834 var splitCenter; 1835 1836 if (vis.splitDir === "v") { 1837 splitCenter = $splitBar.offset().left - $panel1.offset().left + 1838 $splitBar.outerWidth()/2; 1839 var innerWidth = offsetRight($panel2) - $panel1.offset().left; 1840 return splitCenter / innerWidth; 1841 1842 } else if (vis.splitDir === "h") { 1843 splitCenter = $splitBar.offset().top - $panel1.offset().top + 1844 $splitBar.outerHeight()/2; 1845 var innerHeight = offsetBottom($panel2) - $panel1.offset().top; 1846 return splitCenter / innerHeight; 1847 } 1848 } 1849 1850 function positionRight($el) { 1851 return $el.position().left + $el.outerWidth(); 1852 } 1853 function offsetRight($el) { 1854 return $el.offset().left + $el.outerWidth(); 1855 } 1856 function positionBottom($el) { 1857 return $el.position().top + $el.outerHeight(); 1858 } 1859 function offsetBottom($el) { 1860 return $el.offset().top + $el.outerHeight(); 1861 } 1862 1863 // Enable dragging of the split bar --------------------------------------- 1864 (function() { 1865 var dragging = false; 1866 // For vertical split (left-right dragging) 1867 var startDragX; 1868 var startOffsetLeft; 1869 // For horizontal split (up-down dragging) 1870 var startDragY; 1871 var startOffsetTop; 1872 1873 var stopDrag = function(e) { 1874 if (!dragging) return; 1875 dragging = false; 1876 1877 document.removeEventListener("mousemove", drag); 1878 document.removeEventListener("mouseup", stopDrag); 1879 1880 $splitBar.css("opacity", ""); 1881 1882 if ((vis.splitDir === "v" && e.pageX - startDragX === 0) || 1883 (vis.splitDir === "h" && e.pageY - startDragY === 0)) { 1884 return; 1885 } 1886 1887 resizePanels(splitProportion()); 1888 vis.flameGraph.onResize(); 1889 }; 1890 1891 var startDrag = function(e) { 1892 // Don't start another drag if we're already in one. 1893 if (dragging) return; 1894 dragging = true; 1895 pauseEvent(e); 1896 1897 $splitBar.css("opacity", 0.75); 1898 1899 if (vis.splitDir === "v") { 1900 startDragX = e.pageX; 1901 startOffsetLeft = $splitBar.offset().left; 1902 } else { 1903 startDragY = e.pageY; 1904 startOffsetTop = $splitBar.offset().top; 1905 } 1906 1907 document.addEventListener("mousemove", drag); 1908 document.addEventListener("mouseup", stopDrag); 1909 }; 1910 1911 var drag = function(e) { 1912 if (!dragging) return; 1913 pauseEvent(e); 1914 1915 if (vis.splitDir === "v") { 1916 var dx = e.pageX - startDragX; 1917 if (dx === 0) 1918 return; 1919 1920 // Move the split bar 1921 $splitBar.offset({ left: startOffsetLeft + dx }); 1922 1923 } else if (vis.splitDir === "h") { 1924 var dy = e.pageY - startDragY; 1925 if (dy === 0) 1926 return; 1927 1928 // Move the split bar 1929 $splitBar.offset({ top: startOffsetTop + dy }); 1930 } 1931 }; 1932 1933 // Stop propogation so that we don't select text while dragging 1934 function pauseEvent(e){ 1935 if(e.stopPropagation) e.stopPropagation(); 1936 if(e.preventDefault) e.preventDefault(); 1937 e.cancelBubble = true; 1938 e.returnValue = false; 1939 return false; 1940 } 1941 1942 // Remove existing event listener from previous calls to initResizing(). 1943 $splitBar.off("mousedown.profvis"); 1944 $splitBar.on("mousedown.profvis", startDrag); 1945 })(); 1946 1947 1948 return { 1949 resizePanels: resizePanels 1950 }; 1951 } 1952 1953 1954 var prof = prepareProfData(message.prof, message.interval); 1955 1956 var vis = { 1957 el: el, 1958 prof: prof, 1959 profTree: getProfTree(prof), 1960 interval: message.interval, 1961 totalTime: getTotalTime(prof), 1962 totalMem: getTotalMemory(prof), 1963 files: message.files, 1964 aggLabelTimes: getAggregatedLabelTimes(prof), 1965 fileLineStats: getFileLineStats(prof, message.files), 1966 profTable: [], 1967 1968 // Objects representing each component 1969 statusBar: null, 1970 optionsPanel: null, 1971 codeTable: null, 1972 flameGraph: null, 1973 infoBox: null, 1974 treetable: null, 1975 activeViews: [], 1976 1977 // Functions to enable/disable responding to scrollwheel events 1978 enableScroll: enableScroll, 1979 disableScroll: disableScroll, 1980 1981 splitDir: message.split, 1982 hideInternals: true, 1983 hideMemory: false, 1984 1985 resizePanels: null 1986 }; 1987 1988 1989 // Render the objects --------------------------------------------- 1990 1991 var statusBarEl = document.createElement("div"); 1992 statusBarEl.className = "profvis-status-bar"; 1993 vis.el.appendChild(statusBarEl); 1994 1995 // Container panels - top/bottom or left/right 1996 var panel1 = document.createElement("div"); 1997 panel1.className = "profvis-panel1"; 1998 vis.el.appendChild(panel1); 1999 2000 var panel2 = document.createElement("div"); 2001 panel2.className = "profvis-panel2"; 2002 vis.el.appendChild(panel2); 2003 2004 var splitBarEl = document.createElement("div"); 2005 splitBarEl.className = "profvis-splitbar"; 2006 vis.el.appendChild(splitBarEl); 2007 2008 var footerEl = document.createElement("div"); 2009 footerEl.className = "profvis-footer"; 2010 vis.el.appendChild(footerEl); 2011 2012 // Items in the panels 2013 var codeTableEl = document.createElement("div"); 2014 codeTableEl.className = "profvis-code"; 2015 panel1.appendChild(codeTableEl); 2016 2017 var flameGraphEl = document.createElement("div"); 2018 flameGraphEl.className = "profvis-flamegraph"; 2019 panel2.appendChild(flameGraphEl); 2020 2021 var infoBoxEl = document.createElement("div"); 2022 infoBoxEl.className = "profvis-infobox"; 2023 panel2.appendChild(infoBoxEl); 2024 2025 var treetableEl = document.createElement("div"); 2026 treetableEl.className = "profvis-treetable"; 2027 treetableEl.style.display = "none"; 2028 vis.el.appendChild(treetableEl); 2029 2030 var optionsPanelEl = document.createElement("div"); 2031 optionsPanelEl.className = "profvis-options-panel"; 2032 vis.el.appendChild(optionsPanelEl); 2033 2034 // Efficient to properly size panels before the code + flamegraph are 2035 // rendered, so that we don't have to re-render. 2036 var resize = initResizing(); 2037 vis.resizePanels = resize.resizePanels; 2038 2039 var hideViews = function() { 2040 splitBarEl.style.display = "none"; 2041 panel1.style.display = "none"; 2042 panel2.style.display = "none"; 2043 treetableEl.style.display = "none"; 2044 }; 2045 2046 var toggleViews = function(view) { 2047 hideViews(); 2048 2049 switch (view) { 2050 case "flamegraph": 2051 splitBarEl.style.display = "block"; 2052 panel1.style.display = "block"; 2053 panel2.style.display = "block"; 2054 2055 vis.activeViews = [vis.flameGraph, vis.codeTable]; 2056 vis.resizePanels(); 2057 break; 2058 case "treetable": 2059 if (!vis.treetable) { 2060 vis.treetable = generateTreetable(treetableEl); 2061 } 2062 2063 treetableEl.style.display = "block"; 2064 2065 vis.activeViews = [vis.treetable]; 2066 break; 2067 } 2068 2069 vis.activeViews.forEach(function(e) { 2070 if (e.onResize) e.onResize(); 2071 }); 2072 }; 2073 2074 var onOptionsChange = function(option, checked) { 2075 switch (option) 2076 { 2077 case "split": { 2078 vis.splitDir = checked ? "h" : "v"; 2079 // Check that flame graph is visible 2080 if ($.inArray(vis.flameGraph, vis.activeViews) !== -1) { 2081 initResizing(); 2082 vis.flameGraph.onResize(); 2083 } 2084 break; 2085 } 2086 case "internals": { 2087 vis.flameGraph.savePrevScales(); 2088 2089 vis.hideInternals = checked; 2090 if (checked) { 2091 vis.flameGraph.useCollapsedDepth(); 2092 vis.flameGraph.redrawCollapse(400, 400); 2093 } else { 2094 vis.flameGraph.useUncollapsedDepth(); 2095 vis.flameGraph.redrawUncollapse(400, 250); 2096 } 2097 2098 vis.activeViews.forEach(function(e) { 2099 if (e.onOptionsChange) e.onOptionsChange(); 2100 }); 2101 2102 break; 2103 } 2104 case "memory": { 2105 vis.hideMemory = checked; 2106 vis.activeViews.forEach(function(e) { 2107 if (e.useMemoryResults) e.useMemoryResults(); 2108 }); 2109 break; 2110 } 2111 } 2112 }; 2113 2114 // Create the UI components 2115 vis.statusBar = generateStatusBar(statusBarEl, toggleViews); 2116 vis.footer = generateFooter(footerEl); 2117 vis.optionsPanel = generateOptionsPanel(optionsPanelEl, onOptionsChange); 2118 vis.codeTable = generateCodeTable(codeTableEl); 2119 vis.flameGraph = generateFlameGraph(flameGraphEl); 2120 vis.infoBox = initInfoBox(infoBoxEl); 2121 vis.treetable = null; 2122 vis.activeViews = [vis.flameGraph, vis.codeTable]; 2123 2124 // If any depth collapsing occured, enable the "hide internal" checkbox. 2125 if (prof.some(function(d) { return d.depth !== d.depthCollapsed; })) { 2126 vis.optionsPanel.enableHideInternal(); 2127 } 2128 2129 // Start with scrolling disabled because of mousewheel scrolling issue 2130 disableScroll(); 2131 2132 // Make the vis object accessible via the DOM element 2133 $(el).data("profvis", vis); 2134 2135 return vis; 2136 }; // profvis.render() 2137 2138 // Calculate amount of time spent on each line of code. Returns nested objects 2139 // grouped by file, and then by line number. 2140 function getFileLineStats(prof, files) { 2141 // Drop entries with null or "" filename 2142 prof = prof.filter(function(row) { 2143 return row.filename !== null && row.filename !== ""; 2144 }); 2145 2146 // Gather line-by-line file contents 2147 var fileLineStats = files.map(function(file) { 2148 // Create array of objects with info for each line of code. 2149 var lines = file.content.split("\n"); 2150 var lineData = []; 2151 var filename = file.filename; 2152 var normpath = file.normpath; 2153 for (var i=0; i<lines.length; i++) { 2154 lineData[i] = { 2155 filename: filename, 2156 normpath: normpath, 2157 linenum: i + 1, 2158 content: lines[i], 2159 sumTime: 0, 2160 sumMem: 0, 2161 sumMemAlloc: 0, 2162 sumMemDealloc: 0 2163 }; 2164 } 2165 2166 return { 2167 filename: filename, 2168 lineData: lineData 2169 }; 2170 }); 2171 2172 // Get timing data for each line 2173 var statsData = d3.nest() 2174 .key(function(d) { return d.filename; }) 2175 .key(function(d) { return d.linenum; }) 2176 .rollup(function(leaves) { 2177 var sumTime = leaves.reduce(function(sum, d) { 2178 // Add this node's time only if no ancestor node has the same 2179 // filename and linenum. This is to avoid double-counting times for 2180 // a line. 2181 var incTime = 0; 2182 if (!ancestorHasFilenameLinenum(d.filename, d.linenum, d.parent)) { 2183 incTime = d.endTime - d.startTime; 2184 } 2185 return sum + incTime; 2186 }, 0); 2187 2188 var sumMem = leaves.reduce(function(sum, d) { 2189 return sum + d.sumMem; 2190 }, 0); 2191 2192 var sumMemDealloc = leaves.reduce(function(sum, d) { 2193 return sum + d.sumMemDealloc; 2194 }, 0); 2195 2196 var sumMemAlloc = leaves.reduce(function(sum, d) { 2197 return sum + d.sumMemAlloc; 2198 }, 0); 2199 2200 return { 2201 filename: leaves[0].filename, 2202 linenum: leaves[0].linenum, 2203 sumTime: sumTime, 2204 sumMem: sumMem, 2205 sumMemAlloc: sumMemAlloc, 2206 sumMemDealloc: sumMemDealloc 2207 }; 2208 }) 2209 .entries(prof); 2210 2211 // Insert the sumTimes into line content data 2212 statsData.forEach(function(fileInfo) { 2213 // Find item in fileTimes that matches the file of this fileInfo object 2214 var fileLineData = fileLineStats.filter(function(d) { 2215 return d.filename === fileInfo.key; 2216 })[0].lineData; 2217 2218 fileInfo.values.forEach(function(lineInfo) { 2219 lineInfo = lineInfo.values; 2220 fileLineData[lineInfo.linenum - 1].sumTime = lineInfo.sumTime; 2221 fileLineData[lineInfo.linenum - 1].sumMem = lineInfo.sumMem; 2222 fileLineData[lineInfo.linenum - 1].sumMemDealloc = lineInfo.sumMemDealloc; 2223 fileLineData[lineInfo.linenum - 1].sumMemAlloc = lineInfo.sumMemAlloc; 2224 }); 2225 }); 2226 2227 // Calculate proportional times, relative to the longest time in the data 2228 // set. Modifies data in place. 2229 var fileMaxTimes = fileLineStats.map(function(lines) { 2230 var lineTimes = lines.lineData.map(function(x) { return x.sumTime; }); 2231 return d3.max(lineTimes); 2232 }); 2233 2234 var maxTime = d3.max(fileMaxTimes); 2235 2236 fileLineStats.map(function(lines) { 2237 lines.lineData.map(function(line) { 2238 line.propTime = line.sumTime / maxTime; 2239 }); 2240 }); 2241 2242 var totalMem = getTotalMemory(prof); 2243 2244 fileLineStats.map(function(lines) { 2245 lines.lineData.map(function(line) { 2246 line.propMem = line.sumMem / totalMem; 2247 line.propMemDealloc = line.sumMemDealloc / totalMem; 2248 line.propMemAlloc = line.sumMemAlloc / totalMem; 2249 }); 2250 }); 2251 2252 return fileLineStats; 2253 2254 // Returns true if the given node or one of its ancestors has the given 2255 // filename and linenum; false otherwise. 2256 function ancestorHasFilenameLinenum(filename, linenum, node) { 2257 if (!node) { 2258 return false; 2259 } 2260 if (node.filename === filename && node.linenum === linenum) { 2261 return true; 2262 } 2263 return ancestorHasFilenameLinenum(filename, linenum, node.parent); 2264 } 2265 } 2266 2267 function prepareProfData(prof, interval) { 2268 // Convert object-with-arrays format prof data to array-of-objects format 2269 var data = colToRows(prof); 2270 data = addParentChildLinks(data); 2271 data = consolidateRuns(data); 2272 data = applyInterval(data, interval); 2273 data = findCollapsedDepths(data); 2274 2275 return data; 2276 } 2277 2278 // Given the raw profiling data, convert `time` and `lastTime` fields to 2279 // `startTime` and `endTime`, and use the supplied interval. Modifies data 2280 // in place. 2281 function applyInterval(prof, interval) { 2282 prof.forEach(function(d) { 2283 d.startTime = interval * (d.time - 1); 2284 d.endTime = interval * (d.lastTime); 2285 delete d.time; 2286 delete d.lastTime; 2287 }); 2288 2289 return prof; 2290 } 2291 2292 // Find the total time spanned in the data 2293 function getTotalTime(prof) { 2294 return d3.max(prof, function(d) { return d.endTime; }) - 2295 d3.min(prof, function(d) { return d.startTime; }); 2296 } 2297 2298 // Find the total memory spanned in the data 2299 function getTotalMemory(prof) { 2300 return d3.max(prof, function(d) { return d.memalloc; }); 2301 } 2302 2303 // Calculate the total amount of time spent in each function label 2304 function getAggregatedLabelTimes(prof) { 2305 var labelTimes = {}; 2306 var tree = getProfTree(prof); 2307 calcLabelTimes(tree); 2308 2309 return labelTimes; 2310 2311 // Traverse the tree with the following strategy: 2312 // * Check if current label is used in an ancestor. 2313 // * If yes, don't add to times for that label. 2314 // * If no, do add to times for that label. 2315 // * Recurse into children. 2316 function calcLabelTimes(node) { 2317 var label = node.label; 2318 if (!ancestorHasLabel(label, node.parent)) { 2319 if (labelTimes[label] === undefined) 2320 labelTimes[label] = 0; 2321 2322 labelTimes[label] += node.endTime - node.startTime; 2323 } 2324 2325 node.children.forEach(calcLabelTimes); 2326 } 2327 2328 // Returns true if the given node or one of its ancestors has the given 2329 // label; false otherwise. 2330 function ancestorHasLabel(label, node) { 2331 if (node) { 2332 if (node.label === label) { 2333 return true; 2334 } 2335 return ancestorHasLabel(label, node.parent); 2336 } else { 2337 return false; 2338 } 2339 } 2340 } 2341 2342 2343 // Given profiling data, add parent and child links to indicate stack 2344 // relationships. 2345 function addParentChildLinks(prof) { 2346 var data = d3.nest() 2347 .key(function(d) { return d.time; }) 2348 .rollup(function(leaves) { 2349 leaves = leaves.sort(function(a, b) { return a.depth - b.depth; }); 2350 2351 leaves[0].parent = null; 2352 leaves[0].children = []; 2353 2354 for (var i=1; i<leaves.length; i++) { 2355 leaves[i-1].children.push(leaves[i]); 2356 leaves[i].parent = leaves[i-1]; 2357 leaves[i].children = []; 2358 } 2359 2360 return leaves; 2361 }) 2362 .map(prof); 2363 2364 // Convert data from object of arrays to array of arrays 2365 data = d3.map(data).values(); 2366 // Flatten data 2367 return d3.merge(data); 2368 } 2369 2370 2371 // Given profiling data, consolidate consecutive blocks for a flamegraph. 2372 // This function also assigns correct parent-child relationships to form a 2373 // tree of data objects, with a hidden root node at depth 0. 2374 function consolidateRuns(prof) { 2375 // Create a special top-level leaf whose only purpose is to point to its 2376 // children, the items at depth 1. 2377 var topLeaf = { 2378 depth: 0, 2379 parent: null, 2380 children: prof.filter(function(d) { return d.depth === 1; }) 2381 }; 2382 2383 var tree = consolidateTree(topLeaf); 2384 var data = treeToArray(tree); 2385 // Remove the root node from the flattened data 2386 data = data.filter(function(d) { return d.depth !== 0; }); 2387 return data; 2388 2389 function consolidateTree(tree) { 2390 var leaves = tree.children; 2391 leaves = leaves.sort(function(a, b) { return a.time - b.time; }); 2392 2393 // Collapse consecutive leaves, with some conditions 2394 var startLeaf = null; // leaf starting this run 2395 var lastLeaf = null; // The last leaf we've looked at 2396 var newLeaves = []; 2397 var collectedChildren = []; 2398 var sumMem = 0; 2399 var sumMemDealloc = 0; 2400 var sumMemAlloc = 0; 2401 2402 // This takes the start leaf, end leaf, and the set of children for the 2403 // new leaf, and creates a new leaf which copies all its properties from 2404 // the startLeaf, except lastTime and children. 2405 function addNewLeaf(startLeaf, endLeaf, newLeafChildren, sumMem, sumMemDealloc, sumMemAlloc) { 2406 var newLeaf = $.extend({}, startLeaf); 2407 newLeaf.lastTime = endLeaf.time; 2408 newLeaf.parent = tree; 2409 newLeaf.children = newLeafChildren; 2410 2411 // Recurse into children 2412 newLeaf = consolidateTree(newLeaf); 2413 2414 // Aggregate memory from this consolidation batch and their children 2415 aggregateMemory(newLeaf, sumMem, sumMemDealloc, sumMemAlloc); 2416 2417 newLeaves.push(newLeaf); 2418 } 2419 2420 function aggregateMemory(leaf, sumMem, sumMemDealloc, sumMemAlloc) { 2421 leaf.sumMem = sumMem; 2422 leaf.sumMemDealloc = sumMemDealloc; 2423 leaf.sumMemAlloc = sumMemAlloc; 2424 if (leaf.children) { 2425 leaf.children.forEach(function(child) { 2426 leaf.sumMem += child.sumMem ? child.sumMem : 0; 2427 leaf.sumMemDealloc += child.sumMemDealloc ? child.sumMemDealloc : 0; 2428 leaf.sumMemAlloc += child.sumMemAlloc ? child.sumMemAlloc : 0; 2429 }); 2430 } 2431 } 2432 2433 for (var i=0; i<leaves.length; i++) { 2434 var leaf = leaves[i]; 2435 2436 if (i === 0) { 2437 startLeaf = leaf; 2438 sumMem = sumMemAlloc = sumMemDealloc = 0; 2439 } else if (leaf.label !== startLeaf.label || 2440 leaf.filename !== startLeaf.filename || 2441 leaf.linenum !== startLeaf.linenum || 2442 leaf.depth !== startLeaf.depth) 2443 { 2444 addNewLeaf(startLeaf, lastLeaf, collectedChildren, sumMem, sumMemDealloc, sumMemAlloc); 2445 2446 collectedChildren = []; 2447 startLeaf = leaf; 2448 sumMem = sumMemAlloc = sumMemDealloc = 0; 2449 } 2450 2451 sumMem += leaf.meminc; 2452 sumMemDealloc += Math.min(leaf.meminc, 0); 2453 sumMemAlloc += Math.max(leaf.meminc, 0); 2454 collectedChildren = collectedChildren.concat(leaf.children); 2455 lastLeaf = leaf; 2456 } 2457 2458 // Add the last one, if there were any at all 2459 if (i !== 0) { 2460 addNewLeaf(startLeaf, lastLeaf, collectedChildren, sumMem, sumMemDealloc, sumMemAlloc); 2461 } 2462 2463 tree.children = newLeaves; 2464 return tree; 2465 } 2466 2467 // Given a tree, pull out all the leaves and put them in a flat array 2468 function treeToArray(tree) { 2469 var allLeaves = []; 2470 2471 function pushLeaves(leaf) { 2472 allLeaves.push(leaf); 2473 leaf.children.forEach(pushLeaves); 2474 } 2475 2476 pushLeaves(tree); 2477 return allLeaves; 2478 } 2479 } 2480 2481 2482 // Given profiling data with parent-child information, get the root node. 2483 function getProfTree(prof) { 2484 if (prof.length === 0) 2485 return null; 2486 2487 // Climb up to the top of the tree 2488 var node = prof[0]; 2489 while (node.parent) { 2490 node = node.parent; 2491 } 2492 return node; 2493 } 2494 2495 2496 // Given profiling data, find depth of items after hiding items between 2497 // items with labels "..stacktraceoff.." and "..stacktraceon..". Modifies 2498 // data in place. 2499 function findCollapsedDepths(data) { 2500 var tree = getProfTree(data); 2501 calculateDepths(tree, tree.depth, 0); 2502 return data; 2503 2504 function calculateDepths(node, curCollapsedDepth, stacktraceOffCount) { 2505 if (node.label === "..stacktraceoff..") { 2506 stacktraceOffCount++; 2507 } 2508 2509 if (stacktraceOffCount > 0) { 2510 node.depthCollapsed = null; 2511 } else { 2512 node.depthCollapsed = curCollapsedDepth; 2513 curCollapsedDepth++; 2514 } 2515 2516 if (node.label === "..stacktraceon..") { 2517 stacktraceOffCount--; 2518 } 2519 2520 // Recurse 2521 node.children.forEach(function(x) { 2522 calculateDepths(x, curCollapsedDepth, stacktraceOffCount); 2523 }); 2524 } 2525 } 2526 2527 2528 // Transform column-oriented data (an object with arrays) to row-oriented data 2529 // (an array of objects). 2530 function colToRows(x) { 2531 var colnames = d3.keys(x); 2532 if (colnames.length === 0) 2533 return []; 2534 2535 var newdata = []; 2536 for (var i=0; i < x[colnames[0]].length; i++) { 2537 var row = {}; 2538 for (var j=0; j < colnames.length; j++) { 2539 var colname = colnames[j]; 2540 row[colname] = x[colname][i]; 2541 } 2542 newdata[i] = row; 2543 } 2544 2545 return newdata; 2546 } 2547 2548 // Given an array with two values (a min and max), return an array with the 2549 // range expanded by `amount`. 2550 function expandRange(range, amount) { 2551 var adjust = amount * (range[1] - range[0]); 2552 return [ 2553 range[0] - adjust, 2554 range[1] + adjust 2555 ]; 2556 } 2557 2558 2559 // Escape an HTML string. 2560 function escapeHTML(text) { 2561 return text 2562 .replace(/&/g, "&") 2563 .replace(/</g, "<") 2564 .replace(/>/g, ">") 2565 .replace(/"/g, """) 2566 .replace(/'/g, "'"); 2567 } 2568 2569 // This returns the current page URL without any trailing hash. Should be 2570 // used in url() references in SVGs to avoid problems when there's a <base> 2571 // tag in the document. 2572 function urlNoHash() { 2573 return window.location.href.split("#")[0]; 2574 } 2575 2576 function debounce(f, delay) { 2577 var timer = null; 2578 return function() { 2579 var context = this; 2580 var args = arguments; 2581 clearTimeout(timer); 2582 timer = setTimeout(function () { 2583 f.apply(context, args); 2584 }, delay); 2585 }; 2586 } 2587 2588 function randomString(length) { 2589 var chars = 'abcdefghijklmnopqrstuvwxyz'; 2590 var result = ''; 2591 for (var i = length; i > 0; --i) 2592 result += chars[Math.floor(Math.random() * chars.length)]; 2593 return result; 2594 } 2595 2596 var getNormPath = function(files, filename) { 2597 var normpath = null; 2598 files.forEach(function(e) { 2599 if (e.filename == filename) { 2600 normpath = e.normpath; 2601 } 2602 }); 2603 return normpath; 2604 }; 2605 2606 2607 (function() { 2608 // Prevent unwanted scroll capturing. Based on the corresponding code in 2609 // https://github.com/rstudio/leaflet 2610 2611 // The rough idea is that we disable scroll wheel zooming inside each 2612 // profvis object, until the user moves the mouse cursor or clicks on the 2613 // visualization. This is trickier than just listening for mousemove, 2614 // because mousemove is fired when the page is scrolled, even if the user 2615 // did not physically move the mouse. We handle this by examining the 2616 // mousemove event's screenX and screenY properties; if they change, we know 2617 // it's a "true" move. 2618 // 2619 // There's a complication to this: when the mouse wheel is scrolled quickly, 2620 // on the step where the profvis DOM object overlaps the cursor, sometimes 2621 // the mousemove event happens before the mousewheel event, and sometimes 2622 // it's the reverse (at least on Chrome 46 on Linux). This means that we 2623 // can't rely on the mousemove handler disabling the profvis object's zoom 2624 // before a scroll event is triggered on the profvis object (cauzing 2625 // zooming). In order to deal with this, we start each profvis object with 2626 // zooming disabled, and also disable zooming when the cursor leaves the 2627 // profvis div. That way, even if a mousewheel event gets triggered on the 2628 // object before the mousemove, it won't cause zooming. 2629 2630 // lastScreen can never be null, but its x and y can. 2631 var lastScreen = { x: null, y: null }; 2632 2633 $(document) 2634 .on("mousewheel DOMMouseScroll", function(e) { 2635 // Any mousemove events at this screen position will be ignored. 2636 lastScreen = { x: e.originalEvent.screenX, y: e.originalEvent.screenY }; 2637 }) 2638 .on("mousemove", ".profvis", function(e) { 2639 // Did the mouse really move? 2640 if (lastScreen.x !== null && e.screenX !== lastScreen.x || e.screenY !== lastScreen.y) { 2641 $(this).data("profvis").flameGraph.enableZoom(); 2642 lastScreen = { x: null, y: null }; 2643 } 2644 }) 2645 .on("mousedown", ".profvis", function(e) { 2646 // Clicking always enables zooming. 2647 $(this).data("profvis").flameGraph.enableZoom(); 2648 lastScreen = { x: null, y: null }; 2649 }) 2650 .on("mouseleave", ".profvis", function(e) { 2651 $(this).data("profvis").flameGraph.disableZoom(); 2652 }); 2653 })(); 2654 2655 return profvis; 2656 })();