bookclub-advr

DSLC Advanced R Book Club
git clone https://git.eamoncaddigan.net/bookclub-advr.git
Log | Files | Refs | README | LICENSE

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 &#x25BE;</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" ? '&#x2612;' : '&#x2610;') +
     89           '</span> Split horizontally' +
     90         '</div>' +
     91         '<div role="button" class="hide-internal">' +
     92           '<span class="options-checkbox" data-checked="1">&#x2612;</span> Hide internal function calls' +
     93         '</div>' +
     94         '<div role="button" class="hide-zero-row">' +
     95           '<span class="options-checkbox" data-checked="0">&#x2610;</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">&#x2610;</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("&#x2612;");
    110           return true;
    111 
    112         } else {
    113           $checkbox.attr("data-checked", "0");
    114           $checkbox.html("&#x2610;");
    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 &nbsp; 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 &nbsp; 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 &nbsp; 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, "&amp;")
   2563       .replace(/</g, "&lt;")
   2564       .replace(/>/g, "&gt;")
   2565       .replace(/"/g, "&quot;")
   2566       .replace(/'/g, "&#039;");
   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 })();