/*jshint undef:true, browser:true, devel: true, jquery:true, strict:false, curly:false, indent:2 */ /*global profvis:true, d3, hljs */ profvis = (function() { var profvis = {}; profvis.render = function(el, message) { function generateStatusBarButton(id, caption, active) { var spacerImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAUCAYAAACnOeyiAAAAXklEQVR42mNgAIL///8zMYSGhjIDGYIMIiIMvECGMwMDN4M4kFEDUqIIZKwDMdSBjAsghj6Q8QPEMAAy/lOBoQekv4AYKkDGfgZeXl4RICOLQUtLiw3IUAJJMQIZ7AC2tU2tXJxOYgAAAABJRU5ErkJggg=='; var buttonHtml = '
' + caption + '
' + '
'; return buttonHtml; } function generateStatusBar(el, onToogle) { var $el = $(el); el.innerHTML = generateStatusBarButton('flameGraphButton', 'Flame Graph', true) + generateStatusBarButton('treetableButton', 'Data', false) + 'Options ▾'; $el.find("span.options-button").on("click", function(e) { e.preventDefault(); e.stopPropagation(); vis.optionsPanel.toggleVisibility(); }); var setStatusBarButtons = function(e) { $(".info-block").removeClass("result-block-active"); $(".info-block").addClass("result-block"); e.addClass("result-block-active"); }; $el.find("#flameGraphButton").on("click", function() { setStatusBarButtons($(this)); onToogle("flamegraph"); }); $el.find("#treetableButton").on("click", function() { setStatusBarButtons($(this)); onToogle("treetable"); }); return { el: el }; } function generateFooter(el, onToogle) { var $el = $(el); el.innerHTML = '
Sample Interval: ' + vis.interval + 'ms
' + '
' + // '' + (Math.round(vis.totalMem * 100) / 100) + 'MB' + // ' / ' + '' + vis.totalTime + 'ms' + '
'; return { el: el }; } function generateOptionsPanel(el, onOptionsChange) { var $el = $(el); el.innerHTML = '
' + '' + (vis.splitDir === "h" ? '☒' : '☐') + ' Split horizontally' + '
' + '
' + ' Hide internal function calls' + '
' + '
' + ' Hide lines of code with zero time' + '
' + '
' + ' Hide memory results' + '
'; // Toggle the appearance of a checkbox and return the new checked state. function toggleCheckbox($checkbox) { // Use attr() instead of data(), because the latter tries to coerce to // numbers, which complicates our comparisons. var checked = $checkbox.attr("data-checked"); if (checked === "0") { $checkbox.attr("data-checked", "1"); $checkbox.html("☒"); return true; } else { $checkbox.attr("data-checked", "0"); $checkbox.html("☐"); return false; } } $el.find(".split-horizontal") .on("click", function() { var checked = toggleCheckbox($(this).find(".options-checkbox")); onOptionsChange("split", checked); }); $el.find(".hide-internal") .on("click", function() { var checked = toggleCheckbox($(this).find(".options-checkbox")); onOptionsChange("internals", checked); }); $el.find(".hide-memory") .on("click", function() { var checked = toggleCheckbox($(this).find(".options-checkbox")); onOptionsChange("memory", checked); }); // Make the "hide internal" option available or unavailable to users function enableHideInternal() { $el.find(".hide-internal").css("display", ""); } function disableHideInternal() { $el.find(".hide-internal").css("display", "none"); } // By default, start with it unavailable; it's only relevant for Shiny // apps. disableHideInternal(); $el.find(".hide-zero-row") .on("click", function() { var checked = toggleCheckbox($(this).find(".options-checkbox")); if (checked) { vis.codeTable.hideZeroTimeRows(); } else { vis.codeTable.showZeroTimeRows(); } }); el.style.visibility = "hidden"; function toggleVisibility(offset) { if (el.style.visibility === "visible") { el.style.visibility = "hidden"; } else { el.style.visibility = "visible"; $(document).on("click", hideOnClickOutside); } } // Hide the panel when a click happens outside. This handler also removes // itself after it fires. function hideOnClickOutside(e) { var $el = $(el); if (!$el.is(e.target) && $el.has(e.target).length === 0) { el.style.visibility = "hidden"; // Unregister this event listener $(document).off("click", hideOnClickOutside); } } return { el: el, toggleVisibility: toggleVisibility, enableHideInternal: enableHideInternal, disableHideInternal: disableHideInternal }; } function notifySourceFileMessage(d, details) { if (window.parent.postMessage) { window.parent.postMessage({ source: "profvis", message: "sourcefile", file: d.filename, normpath: d.normpath ? d.normpath : getNormPath(vis.files, d.filename), line: d.linenum, details: details }, window.location.origin); } } function roundOneDecimalNum(number, decimals) { return Math.round(number * 10) / 10; } function roundOneDecimal(number, decimals) { if (!number) return 0; return roundOneDecimalNum(number).toFixed(1); } // Generate the code table ---------------------------------------- function generateCodeTable(el) { var useMemory = false; var content = d3.select(el); if (vis.fileLineStats.length === 0) { content.append("div") .attr("class", "profvis-message") .append("div") .text("(Sources not available)"); } // One table for each file var tables = content.selectAll("table") .data(vis.fileLineStats) .enter() .append("table") .attr("class", "profvis-table"); // Table headers var headerRows = tables.append("tr"); headerRows.append("th") .attr("colspan", "2") .attr("class", "filename") .text(function(d) { return d.filename; }); var percentTooltip = "Percentage of tracked execution time"; var percentMemTooltip = "Percentage of peak memory deallocation and allocation"; headerRows.append("th") .attr("class", "table-memory memory") .attr("colspan", "4") .text("Memory"); headerRows.append("th") .attr("class", "time") .attr("colspan", "2") .text("Time"); headerRows.append("th") .attr("class", "spacing") .attr("data-pseudo-content", "\u00a0"); // Insert each line of code var rows = tables.selectAll("tr.code-row") .data(function(d) { return d.lineData; }) .enter() .append("tr") .attr("class", "code-row"); // Use pseudo-content and CSS content rule to make text unselectable and // uncopyable. See https://danoc.me/blog/css-prevent-copy/ rows.append("td") .attr("class", "linenum") .attr("data-pseudo-content", function(d) { return d.linenum; }); rows.append("td") .attr("class", "code r") .text(function(d) { return d.content; }) .each(function() { hljs.highlightBlock(this); }); rows.append("td") .attr("class", "table-memory memory") .attr("title", "Memory deallocation (MB)") .attr("data-pseudo-content", function(d) { return roundOneDecimalNum(d.sumMemDealloc) !== 0 ? roundOneDecimal(d.sumMemDealloc) : ""; }); rows.append("td") .attr("class", "table-memory membar-left-cell") .append("div") .attr("class", "membar") .attr("title", percentMemTooltip) .style("width", function(d) { var p = Math.min(Math.abs(Math.min(Math.round(d.propMemDealloc * 100), 0)), 100); // 8% is the minimal size that looks visually appealing while drawing an almost empty bar p = roundOneDecimalNum(d.sumMemDealloc) !== 0 ? Math.max(p, 8) : 0; return p + "%"; }) // Add the equivalent of   to be added with CSS content .attr("data-pseudo-content", "\u00a0"); rows.append("td") .attr("class", "table-memory membar-right-cell") .append("div") .attr("class", "membar") .attr("title", percentMemTooltip) .style("width", function(d) { var p = Math.min(Math.max(Math.round(d.propMemAlloc * 100), 0), 100); // 4% is the minimal size that looks visually appealing while drawing an almost empty bar p = roundOneDecimalNum(d.sumMemAlloc) !== 0 ? Math.max(p, 4) : 0; return p + "%"; }) // Add the equivalent of   to be added with CSS content .attr("data-pseudo-content", "\u00a0"); rows.append("td") .attr("class", "table-memory memory memory-right") .attr("title", "Memory allocation (MB)") .attr("data-pseudo-content", function(d) { return roundOneDecimalNum(d.sumMemAlloc) !== 0 ? roundOneDecimal(d.sumMemAlloc) : ""; }); rows.append("td") .attr("class", "time") .attr("title", "Total time (ms)") .attr("data-pseudo-content", function(d) { return Math.round(d.sumTime * 100) !== 0 ? (Math.round(d.sumTime * 100) / 100) : ""; }); rows.append("td") .attr("class", "timebar-cell") .append("div") .attr("class", "timebar") .attr("title", percentTooltip) .style("width", function(d) { return Math.round(d.propTime * 100) + "%"; }) // Add the equivalent of   to be added with CSS content .attr("data-pseudo-content", "\u00a0"); rows.append("td") .attr("class", "spacing") .attr("data-pseudo-content", "\u00a0"); rows .on("click", function(d) { // Info box is only relevant when mousing over flamegraph vis.infoBox.hide(); highlighter.click(d); notifySourceFileMessage(d, "select"); }) .on("mouseover", function(d) { if (highlighter.isLocked()) return; // Info box is only relevant when mousing over flamegraph vis.infoBox.hide(); highlighter.hover(d); }) .on("mouseout", function(d) { if (highlighter.isLocked()) return; highlighter.hover(null); }) .on("dblclick", function(d) { notifySourceFileMessage(d, "open"); }); function hideZeroTimeRows() { rows .filter(function(d) { return d.sumTime === 0; }) .style("display", "none"); } function showZeroTimeRows() { rows .filter(function(d) { return d.sumTime === 0; }) .style("display", ""); } function addLockHighlight(d) { var target = d; rows .filter(function(d) { return d === target; } ) .classed({ locked: true }); } function clearLockHighlight() { rows .filter(".locked") .classed({ locked: false }); } function addActiveHighlight(d) { // If we have filename and linenum, search for cells that match, and // set them as "active". var target = d; if (target.filename && target.linenum) { var tr = rows .filter(function(d) { return d.linenum === target.linenum && d.filename === target.filename; }) .classed({ active: true }); tr.node().scrollIntoViewIfNeeded(); } } function clearActiveHighlight() { rows .filter(".active") .classed({ active: false }); } function enableScroll() { // TODO: implement this } function disableScroll() { } function useMemoryResults() { d3.selectAll(".table-memory").style("display", vis.hideMemory ? "none" : ""); } return { el: el, hideZeroTimeRows: hideZeroTimeRows, showZeroTimeRows: showZeroTimeRows, addLockHighlight: addLockHighlight, clearLockHighlight: clearLockHighlight, addActiveHighlight: addActiveHighlight, clearActiveHighlight: clearActiveHighlight, enableScroll: enableScroll, disableScroll: disableScroll, useMemoryResults: useMemoryResults }; } var highlighter = (function() { // D3 data objects for the currently locked and active items var lockItem = null; var activeItem = null; function isLocked() { return lockItem !== null; } function currentLock() { return lockItem; } function currentActive() { return activeItem; } // This is called when a flamegraph cell or a line of code is clicked on. // Clicks also should trigger hover events. function click(d) { // If d is null (background is clicked), or if locked and this click // is on the currently locked selection, just unlock and return. if (d === null || (lockItem && d === lockItem)) { lockItem = null; vis.flameGraph.clearLockHighlight(); vis.codeTable.clearLockHighlight(); return; } // If nothing currently locked, or if locked and this click is on // something other than the currently locked selection, then lock the // current selection. lockItem = d; vis.flameGraph.clearLockHighlight(); vis.codeTable.clearLockHighlight(); hover(null); vis.flameGraph.addLockHighlight(d); vis.codeTable.addLockHighlight(d); hover(d); } function hover(d) { activeItem = d; if (activeItem) { vis.flameGraph.addActiveHighlight(activeItem); vis.codeTable.addActiveHighlight(activeItem); return; } vis.flameGraph.clearActiveHighlight(); vis.codeTable.clearActiveHighlight(); } return { isLocked: isLocked, currentLock: currentLock, currentActive: currentActive, click: click, hover: hover }; })(); // Generate the flame graph ----------------------------------------------- function generateFlameGraph(el) { el.innerHTML = ""; var stackHeight = 15; // Height of each layer on the stack, in pixels var zoomMargin = 0.02; // Extra margin on sides when zooming to fit // Dimensions ----------------------------------------------------------- // Margin inside the svg where the plotting occurs var dims = { margin: { top: 0, right: 0, left: 0, bottom: 30 } }; dims.width = el.clientWidth - dims.margin.left - dims.margin.right; dims.height = el.clientHeight - dims.margin.top - dims.margin.bottom; var domains = { x: [ d3.min(vis.prof, function(d) { return d.startTime; }), d3.max(vis.prof, function(d) { return d.endTime; }) ], y: [ d3.min(vis.prof, function(d) { return d.depth; }) - 1, d3.max(vis.prof, function(d) { return d.depth; }) ] }; // Slightly expand x domain domains.x = expandRange(domains.x, zoomMargin); // Scales --------------------------------------------------------------- var scales = { x: d3.scale.linear() .domain(domains.x) .range([0, dims.width]), y: d3.scale.linear() .domain(domains.y) .range([dims.height, dims.height - (domains.y[1] - domains.y[0]) * stackHeight]), // This will be a function that, given a data point, returns the depth. // This function can change; sometimes it returns the original depth, // and sometimes it returns the collapsed depth. This isn't exactly a // scale function, but it's close enough for our purposes. getDepth: null }; function useCollapsedDepth() { scales.getDepth = function(d) { return d.depthCollapsed; }; } function useUncollapsedDepth() { scales.getDepth = function(d) { return d.depth; }; } useCollapsedDepth(); // SVG container objects ------------------------------------------------ var svg = d3.select(el).append('svg'); var clipRect = svg.append("clipPath") .attr("id", "clip-" + vis.el.id) .append("rect"); var container = svg.append('g') .attr("transform", "translate(" + dims.margin.left + "," + dims.margin.top + ")") .attr("clip-path", "url(" + urlNoHash() + "#clip-" + vis.el.id + ")"); // Add a background rect so we have something to grab for zooming/panning var backgroundRect = container.append("rect") .attr("class", "background"); // Axes ------------------------------------------------------------ var xAxis = d3.svg.axis() .scale(scales.x) .orient("bottom"); svg.append("g") .attr("class", "x axis") .call(xAxis); // Container sizing ----------------------------------------------------- // Update dimensions of various container elements, based on the overall // dimensions of the containing div. function updateContainerSize() { dims.width = el.clientWidth - dims.margin.left - dims.margin.right; dims.height = el.clientHeight - dims.margin.top - dims.margin.bottom; svg .attr('width', dims.width + dims.margin.left + dims.margin.right) .attr('height', dims.height + dims.margin.top + dims.margin.bottom); clipRect .attr("x", dims.margin.left) .attr("y", dims.margin.top) .attr("width", dims.width) .attr("height", dims.height); backgroundRect .attr("width", dims.width) .attr("height", dims.height); svg.select(".x.axis") .attr("transform", "translate(" + dims.margin.left + "," + dims.height + ")"); } // Redrawing ------------------------------------------------------------ // Redrawing is a little complicated. For performance reasons, the // flamegraph cells that are offscreen aren't rendered; they're removed // from the D3 selection of cells. However, when transitions are // involved, it may be necssary to add objects in their correct // off-screen starting locations before the transition, and then do the // transition. Similarly, it may be necssary to transition objects to // their correct off-screen ending positions. // // In order to handle this, whenever there's a transition, we need to // have the scales for before the transition, and after. When a function // invokes a transition, it will generally do the following: (1) save the // previous scales, (2) modify the current scales, (3) call a redraw // function. The redraw functions are customized for different types of // transitions, and they will use the saved previous scales to position // objects correctly for the transition. When there's no transition, the // previous scales aren't needed, and the redrawImmediate() function // should be used. // Cache cells for faster access (avoid a d3.select()) var cells; // For a data element, return identifying key function dataKey(d) { return d.depth + "-" + d.startTime + "-" + d.endTime; } // For transitions with animation, we need to have a copy of the previous // scales in addition to the current ones. var prevScales = {}; function savePrevScales() { prevScales = { x: scales.x.copy(), y: scales.y.copy(), getDepth: scales.getDepth }; } savePrevScales(); // Returns a D3 selection of the cells that are within the plotting // region, using a set of scales. function selectActiveCells(scales) { var xScale = scales.x; var yScale = scales.y; var depth = scales.getDepth; var width = dims.width; var height = dims.height; var data = vis.prof.filter(function(d) { var depthVal = depth(d); return !(xScale(d.endTime) < 0 || xScale(d.startTime) > width || depthVal === null || yScale(depthVal - 1) < 0 || yScale(depthVal) > height); }); cells = container.selectAll("g.cell").data(data, dataKey); return cells; } // Given an enter selection, add the rect and text objects, but don't // position them. Returns a selection of the new elements. // This should usually be called with addItems(sel.enter()) instead // of sel.enter().call(addItems), because the latter returns the original // enter selection, not the selection of elements, and can't be // used for chaining more function calls on the selection. function addItems(enterSelection) { var cells = enterSelection.append("g") .attr("class", "cell") .classed("highlighted", function(d) { return d.filename !== null; }) .call(addMouseEventHandlers); // Add CSS classes for highlighting cells with labels that match particular // regex patterns. var highlightPatterns = d3.entries(message.highlight); highlightPatterns.map(function(item) { var cssClass = item.key; var regexp = new RegExp(item.value); cells.classed(cssClass, function(d) { return d.label.search(regexp) !== -1; }); }); cells.append("rect") .attr("class", "rect"); cells.append("text") .attr("class", "profvis-label") .text(function(d) { return d.label; }); return cells; } // Given a selection, position the rects and labels, using a set of // scales. function positionItems(cells, scales) { var xScale = scales.x; var yScale = scales.y; var depth = scales.getDepth; cells.select("rect") .attr("width", function(d) { return xScale(d.endTime) - xScale(d.startTime); }) .attr("height", yScale(0) - yScale(1)) .attr("x", function(d) { return xScale(d.startTime); }) .attr("y", function(d) { return yScale(depth(d)); }); cells.select("text") .attr("x", function(d) { // To place the labels, check if there's enough space to fit the // label plus padding in the rect. (We already know the label fits // without padding if we got here.) // * If there's not enough space, simply center the label in the // rect. // * If there is enough space, keep the label within the rect, with // padding. Try to left-align, keeping the label within the // viewing area if possible. // Padding on left and right var pad = 2; var textWidth = getLabelWidth(this, d.label.length); var rectWidth = xScale(d.endTime) - xScale(d.startTime); if (textWidth + pad*2 > rectWidth) { return xScale(d.startTime) + (rectWidth - textWidth) / 2; } else { return Math.min( Math.max(0, xScale(d.startTime)) + pad, xScale(d.endTime) - textWidth - pad ); } }) .attr("y", function(d) { return yScale(depth(d) - 0.8); }); return cells; } // Redraw without a transition (regular panning and zooming) function redrawImmediate() { cells = selectActiveCells(scales); cells.exit().remove(); addItems(cells.enter()) .call(addLockHighlightSelection, highlighter.currentLock()) .call(addActiveHighlightSelection, highlighter.currentActive()); cells.call(positionItems, scales); cells.select('text') .call(updateLabelVisibility); svg.select(".x.axis").call(xAxis); } // Redraw for double-click zooming, where there's a transition function redrawZoom(duration) { // Figure out if we're zooming in or out. This will determine when we // recalculate the label visibility: before or after the transition. var prevExtent = prevScales.x.domain()[1] - prevScales.x.domain()[0]; var curExtent = scales.x.domain()[1] - scales.x.domain()[0]; var zoomIn = curExtent < prevExtent; cells = selectActiveCells(scales); // Phase 1 // Add the enter items, highlight them, and position them using the // previous scales addItems(cells.enter()) .call(addLockHighlightSelection, highlighter.currentLock()) .call(addActiveHighlightSelection, highlighter.currentActive()) .call(positionItems, prevScales); // If zooming out, update label visibility. This will hide some labels // now, before the transition, ensuring that they will never be larger // than the box. if (!zoomIn) { cells.select('text') .call(updateLabelVisibility); } // Phase 2 // Position the update (and enter) items using the new scales cells .transition().duration(duration) .call(positionItems, scales); // Position the exit items using the new scales cells.exit() .transition().duration(duration) .call(positionItems, scales); // Update x axis svg.select(".x.axis") .transition().duration(duration) .call(xAxis); // Phase 3 // If zooming in, update label visibility. This will hide some labels // now, after the transition, ensuring that they will never be larger // than the box. if (zoomIn) { cells.select('text') .transition().delay(duration) .call(updateLabelVisibility); } // Remove the exit items cells.exit() .transition().delay(duration) .remove(); } // Redraw when internal functions are hidden function redrawCollapse(exitDuration, updateDuration) { cells = selectActiveCells(scales); // There are two subsets of the exit items: // 1. Those that exit because depth is null. These should fade out. // 2. Those that exit because they move off screen. These should wait // for subset 1 to fade out, then move with a transition. var fadeOutCells = cells.exit() .filter(function(d) { return scales.getDepth(d) === null; }); var moveOutCells = cells.exit() .filter(function(d) { return scales.getDepth(d) !== null; }); // Phase 1 // Add the enter items, highlight them, and position them using the // previous scales addItems(cells.enter()) .call(addLockHighlightSelection, highlighter.currentLock()) .call(addActiveHighlightSelection, highlighter.currentActive()) .call(positionItems, prevScales); cells.select('text') .call(updateLabelVisibility); // Phase 2 // Fade out the items that have a null depth fadeOutCells .transition().duration(exitDuration) .style("opacity", 0); // Phase 3 // Position the update (and enter) items using the new scales cells .transition().delay(exitDuration).duration(updateDuration) .call(positionItems, scales); // Position the exit items that move out, using the new scales moveOutCells .transition().delay(exitDuration).duration(updateDuration) .call(positionItems, scales); // Phase 4 // Remove all the exit items cells.exit() .transition().delay(exitDuration + updateDuration) .remove(); } // Redraw when internal functions are un-hidden function redrawUncollapse(updateDuration, enterDuration) { cells = selectActiveCells(scales); var enterCells = addItems(cells.enter()); // There are two subsets of the enter items: // 1. Those that enter because they move on screen (but the previous // depth was not null). These should move with a transition. // 2. Those that enter because the previous depth was null. These // should wait for subset 1 to move, then fade in. var moveInCells = enterCells .filter(function(d) { return prevScales.getDepth(d) !== null; }); var fadeInCells = enterCells .filter(function(d) { return prevScales.getDepth(d) === null; }); // Phase 1 // Highlight and position the move-in items with the old scales moveInCells .call(addLockHighlightSelection, highlighter.currentLock()) .call(addActiveHighlightSelection, highlighter.currentActive()) .call(positionItems, prevScales); cells.select('text') .call(updateLabelVisibility); // Phase 2 // Position the move-in, update, and exit items with a transition moveInCells .transition().duration(updateDuration) .call(positionItems, scales); cells .transition().duration(updateDuration) .call(positionItems, scales); cells.exit() .transition().duration(updateDuration) .call(positionItems, scales); // Phase 3 // Highlight and position the fade-in items, then fade in fadeInCells .call(addLockHighlightSelection, highlighter.currentLock()) .call(addActiveHighlightSelection, highlighter.currentActive()) .call(positionItems, scales) .style("opacity", 0) .transition().delay(updateDuration).duration(enterDuration) .style("opacity", 1); // Phase 4 // Remove the exit items cells.exit() .transition().delay(updateDuration + enterDuration) .remove(); } // Calculate whether to display label in each cell ---------------------- // Finding the dimensions of SVG elements is expensive. We'll reduce the // calls getBoundingClientRect() by caching the dimensions. // Cache the width of labels. This is a lookup table which, given the // number of characters, gives the number of pixels. The label width // never changes, so we can keep it outside of updateLabelVisibility(). var labelWidthTable = {}; function getLabelWidth(el, nchar) { // Add entry if it doesn't already exist if (labelWidthTable[nchar] === undefined) { // If the text isn't displayed, then we can't get its width. Make // sure it's visible, get the width, and then restore original // display state. var oldDisplay = el.style.display; el.style.display = "inline"; labelWidthTable[nchar] = el.getBoundingClientRect().width; el.style.display = oldDisplay; } return labelWidthTable[nchar]; } // Show labels that fit in the corresponding rectangle, and hide others. function updateLabelVisibility(labels) { // Cache the width of rects. This is a lookup table which, given the // timespan (width in data), gives the number of pixels. The width of // rects changes with the x scale, so we have to rebuild the table each // time the scale changes. var rectWidthTable = {}; var x0 = scales.x(0); function getRectWidth(time) { // Add entry if it doesn't already exist if (rectWidthTable[time] === undefined) { rectWidthTable[time] = scales.x(time) - x0; } return rectWidthTable[time]; } // Now calculate text and rect width for each cell. labels.style("display", function(d) { var labelWidth = getLabelWidth(this, d.label.length); var boxWidth = getRectWidth(d.endTime - d.startTime); return (labelWidth <= boxWidth) ? "" : "none"; }); return labels; } function onResize() { updateContainerSize(); scales.x.range([0, dims.width]); zoom.x(scales.x); // Preserve distance from bottom, instead of from top (which is the // default behavior). scales.y.range([ dims.height, dims.height - (domains.y[1] - domains.y[0]) * stackHeight ]); redrawImmediate(); } // Attach mouse event handlers ------------------------------------ var dragging = false; function addMouseEventHandlers(cells) { cells .on("mouseup", function(d) { if (dragging) return; // If it wasn't a drag, treat it as a click vis.infoBox.show(d); highlighter.click(d); notifySourceFileMessage(d, "select"); }) .on("mouseover", function(d) { if (dragging) return; // If no label currently shown, display a tooltip var label = this.querySelector(".profvis-label"); if (label.style.display === "none") { var box = this.getBBox(); showTooltip( d.label, box.x + box.width / 2, box.y - box.height ); } if (!highlighter.isLocked()) { vis.infoBox.show(d); highlighter.hover(d); } }) .on("mouseout", function(d) { if (dragging) return; hideTooltip(); if (!highlighter.isLocked()) { vis.infoBox.hide(); highlighter.hover(null); } }) .on("dblclick.zoomcell", function(d) { // When a cell is double-clicked, zoom x to that cell's width. savePrevScales(); scales.x.domain(expandRange([d.startTime, d.endTime], zoomMargin)); zoom.x(scales.x); redrawZoom(250); notifySourceFileMessage(d, "open"); }); return cells; } // Tooltip -------------------------------------------------------- function showTooltip(label, x, y) { var tooltip = container.append("g").attr("class", "profvis-tooltip"); var tooltipRect = tooltip.append("rect"); var tooltipLabel = tooltip.append("text") .text(label) .attr("x", x) .attr("y", y + stackHeight * 0.2); // Shift down slightly for baseline // Add box around label var labelBox = tooltipLabel.node().getBBox(); var rectWidth = labelBox.width + 10; var rectHeight = labelBox.height + 4; tooltipRect .attr("width", rectWidth) .attr("height", rectHeight) .attr("x", x - rectWidth / 2) .attr("y", y - rectHeight / 2) .attr("rx", 4) // Rounded corners -- can't set this in CSS .attr("ry", 4); } function hideTooltip() { container.select("g.profvis-tooltip").remove(); } // Highlighting --------------------------------------------------------- function addLockHighlight(d) { addLockHighlightSelection(cells, d); } function clearLockHighlight() { cells .filter(".locked") .classed({ locked: false }); } function addActiveHighlight(d) { if (!d) return; addActiveHighlightSelection(cells, d); } function clearActiveHighlight() { cells .filter(".active") .classed({ active: false }); } // These are versions of addLockHighlight and addActiveHighlight which // are only internally visible. It must be passed a selection of cells to // perform the highlighting on. This can be more efficient because it can // operate on just an enter selection instead of all cells. function addLockHighlightSelection(selection, d) { if (!d) return; var target = d; selection .filter(function(d) { return d === target; } ) .classed({ locked: true }) .call(moveToFront); } function addActiveHighlightSelection(selection, d) { if (!d) return; var target = d; if (target.filename && target.linenum) { selection .filter(function(d) { // Check for filename and linenum match, and if provided, a label match. var match = d.filename === target.filename && d.linenum === target.linenum; if (!!target.label) { match = match && (d.label === target.label); } return match; }) .classed({ active: true }); } else if (target.label) { // Don't highlight blocks for these labels var exclusions = ["", "FUN"]; if (exclusions.some(function(x) { return target.label === x; })) { return; } // If we only have the label, search for cells that match, but make sure // to not select ones that have a filename and linenum. selection .filter(function(d) { return d.label === target.label && d.filename === null && d.linenum === null; }) .classed({ active: true }); } } // Move a D3 selection to front. If this is called on a selection, that // selection should have been created with a data indexing function (e.g. // data(data, function(d) { return ... })). Otherwise, the wrong object // may be moved to the front. function moveToFront(selection) { return selection.each(function() { this.parentNode.appendChild(this); }); } // Panning and zooming -------------------------------------------- // For panning and zooming x, d3.behavior.zoom does most of what we want // automatically. For panning y, we can't use d3.behavior.zoom becuase it // will also automatically add zooming, which we don't want. Instead, we // need to use d3.behavior.drag and set the y domain appropriately. var drag = d3.behavior.drag() .on("drag", function() { dragging = true; var y = scales.y; var ydom = y.domain(); var ydiff = y.invert(d3.event.dy) - y.invert(0); y.domain([ydom[0] - ydiff, ydom[1] - ydiff]); }); // For mousewheel zooming, we need to limit zoom amount. This is needed // because in Firefox, zoom increments are too big. To do this, we limit // scaleExtent before the first zoom event, and after each subsequent // one. // // When zooming out, there's an additional limit: never zoom out past // the original zoom span. The reason it's necessary to calculate this // each time, instead of simply setting the scaleExtent() so that the // lower bound is 1, is because other zoom events (like // dblclick.zoomcell) are able to change the domain of scales.x, without // changing the value of zoom.scale(). This means that the relationship // between the zoom.scale() does not have a fixed linear relationship to // the span of scales.x, and we have to recalculate it. var maxZoomPerStep = 1.1; function zoomOutLimit() { var span = scales.x.domain()[1] - scales.x.domain()[0]; var startSpan = domains.x[1] - domains.x[0]; return Math.min(maxZoomPerStep, startSpan/span); } var zoom = d3.behavior.zoom() .x(scales.x) .on("zoomstart", function() { zoom.scaleExtent([zoom.scale() / zoomOutLimit(), zoom.scale() * maxZoomPerStep]); }) .on("zoom", function(e) { redrawImmediate(); zoom.scaleExtent([zoom.scale() / zoomOutLimit(), zoom.scale() * maxZoomPerStep]); }); // Register drag before zooming, because we need the drag to set the y // scale before the zoom triggers a redraw. svg .on("mouseup", function(d) { dragging = false; }) .call(drag); // Unlock selection when background is clicked, and zoom out when // background is double-clicked. backgroundRect .on("mouseup", function(d) { if (dragging) return; // If it wasn't a drag, hide info box and unlock. vis.infoBox.hide(); highlighter.click(null); }) .on("dblclick.zoombackground", function() { savePrevScales(); scales.x.domain(domains.x); zoom.x(scales.x); redrawZoom(250); }); var zoomEnabled = false; function disableZoom() { if (zoomEnabled) { svg.on(".zoom", null); zoomEnabled = false; } } function enableZoom() { if (!zoomEnabled) { svg .call(zoom) .on("dblclick.zoom", null); // Disable zoom's built-in double-click behavior zoomEnabled = true; } } enableZoom(); onResize(); return { el: el, onResize: onResize, onUpdateInternals: onResize, redrawImmediate: redrawImmediate, redrawZoom: redrawZoom, redrawCollapse: redrawCollapse, redrawUncollapse: redrawUncollapse, savePrevScales: savePrevScales, useCollapsedDepth: useCollapsedDepth, useUncollapsedDepth: useUncollapsedDepth, addLockHighlight: addLockHighlight, clearLockHighlight: clearLockHighlight, addActiveHighlight: addActiveHighlight, clearActiveHighlight: clearActiveHighlight, disableZoom: disableZoom, enableZoom: enableZoom }; } // generateFlameGraph function initInfoBox(el) { function show(d) { var label = d.label ? d.label : ""; var ref = (d.filename && d.linenum) ? (d.filename + "#" + d.linenum) : "(source unavailable)"; el.style.visibility = ""; el.innerHTML = "" + "" + "" + "" + "" + "" + "" + "
Label" + escapeHTML(label) + "
Called from" + escapeHTML(ref) + "
Total time" + (d.endTime - d.startTime) + "ms
Memory" + roundOneDecimal(d.sumMemDealloc) + " / " + roundOneDecimal(d.sumMemAlloc) + " MB
Agg. total time" + vis.aggLabelTimes[label] + "ms
Call stack depth" + d.depth + "
"; } function hide() { el.style.visibility = "hidden"; } hide(); return { el: el, show: show, hide: hide }; } // Generate the tree table ---------------------------------------- function generateTreetable(el) { var content = d3.select(el); var table = content.append("table") .attr("class", "results") .attr("cellspacing", "0") .attr("cellpadding", "0"); table.append("col"); table.append("col") .style("width", "120px"); table.append("col") .style("width", "50px") .attr("class", "treetable-memory"); table.append("col") .style("width", "26px") .attr("class", "treetable-memory"); table.append("col") .style("width", "50px") .attr("class", "treetable-memory"); table.append("col") .style("width", "50px"); table.append("col") .style("width", "40px"); var tableBody = table.append("tbody"); var headerRows = tableBody.append("tr"); headerRows.append("th") .attr("class", "code-label") .text("Code"); headerRows.append("th") .attr("class", "path") .text("File"); headerRows.append("th") .attr("class", "treetable-memory memory") .attr("colspan", "3") .text("Memory (MB)"); headerRows.append("th") .attr("class", "time") .attr("colspan", "2") .text("Time (ms)"); // Retrieve all nodes (n), recursevely, where check(n) == true. function allTopNodes(nodes, check) { var included = []; nodes = nodes.slice(); while (nodes.length > 0) { var node = nodes.shift(); if (check(node)) included.push(node); else { node.sumChildren.forEach(function(c1) { nodes.unshift(c1); }); } } return included; } // Is there one node (n), including root, where check(n) == true? function oneNode(root, check) { var nodes = [root]; while (nodes.length > 0) { var n = nodes.shift(); if (check(n)) return true; n.sumChildren.forEach(function(x) { nodes.unshift(x); }); } return false; } function updateRowsDisplay(d) { if (vis.hideInternals && d.isInternal) return "none"; else if (!vis.hideInternals && d.isDescendant) return "none"; var collapsed = false; while (d.parent) { d = d.parent; if (d.collapsed) { collapsed = true; break; } } return collapsed ? "none" : ""; } function toggleTreeNode(d) { if (!d.canExpand) return; var collapsed = d.collapsed; if (collapsed === undefined) { // Create a copy since we might insert the same node twice: once // for the normal leaf the other one for a collapsed node. var sumChildren = d.sumChildren.map(function(x) { return jQuery.extend({}, x); }); var childNodes = sumChildren.filter(function(x) { return x.depthCollapsed !== null; }); childNodes.forEach(function(x) { x.isInternal = d.isInternal ? d.isInternal : false; x.isDescendant = d.isDescendant ? d.isDescendant : false; }); var internalChildNodes = sumChildren.filter(function(x) { return x.depthCollapsed === null; }); internalChildNodes.forEach(function(x) { x.isInternal = true; x.isDescendant = false; }); var notInternalDescendantNodes = []; if (!d.isInternal) { notInternalDescendantNodes = allTopNodes(internalChildNodes, function(x) { return x.depthCollapsed !== null && d.depth < x.depth; }); } notInternalDescendantNodes.forEach(function(x) { x.isInternal = false; x.isDescendant = true; }); childNodes = childNodes.concat(internalChildNodes); childNodes = childNodes.concat(notInternalDescendantNodes); childNodes.forEach(function(n) { n.visualDepth = d.visualDepth + 1; n.parent = d; }); vis.profTable = vis.profTable.concat(childNodes); d.collapsed = false; updateRows(); // Nodes are sorted "heaviest first" if (childNodes.length == 1) toggleTreeNode(childNodes[0]); } else { d.collapsed = !collapsed; updateRows(); } } function updateLabelCells(labelCell) { labelCell .attr("nowrap", "true") .style("padding-left", function(d) { return (8 + 15 * (d.visualDepth - 1)) + "px"; }) .on("click", toggleTreeNode) .attr("class", function(d) { d.canExpand = false; if (d.sumChildren) { d.sumChildren.forEach(function(c) { if (c.sumChildren.length > 0) { if (!vis.hideInternals || oneNode(c, function(c1) { return c1.depthCollapsed !== null; })) d.canExpand = true; } }); } var collapsedClass = ""; if (d.canExpand) collapsedClass = d.collapsed === undefined ? "treetable-expand" : d.collapsed ? "treetable-expand" : "treetable-collapse"; return "code-label " + (d.canExpand ? "label-pointer " + collapsedClass : ""); }); } function updateRows() { var rows = tableBody.selectAll("tr.treetable-row") .data(vis.profTable, function(d) { return d.id; }); rows.exit() .remove(); var updatedRows = rows .style("display", updateRowsDisplay); var updatedLabelCells = updatedRows.selectAll("td.code-label"); updateLabelCells(updatedLabelCells); var newRows = rows.enter() .append("tr") .filter(function(d) { if (vis.hideInternals && d.depthCollapsed === null) return false; return true; }) .on("click", function(d) { table.selectAll("tr") .style("background-color", null); this.style.backgroundColor = "rgb(241, 241, 241)"; notifySourceFileMessage(d, "select"); }) .style("display", updateRowsDisplay); newRows .attr("class", "treetable-row"); var labelCell = newRows.append("td"); updateLabelCells(labelCell); var cellWrapper = labelCell.append("div"); cellWrapper.append("div"); labelCell.append("div") .attr("class", "label-text") .text(function(d) { return d.label; }); newRows.append("td") .attr("class", "path") .text(function(d) { var lastSlash = d.filename ? d.filename.lastIndexOf("/") : -1; if (lastSlash >= 0) return d.filename.substr(lastSlash + 1); return d.filename; }); newRows.append("td") .attr("class", "treetable-memory memory-info") .text(function(d) { return roundOneDecimal(d.sumMemDealloc); }); var memoryBarContainer = newRows.append("td") .attr("class", "treetable-memory memory-bar-container"); var memoryLeftCell = memoryBarContainer.append("div") .attr("class", "memory-leftbar-wrapper"); memoryLeftCell.append("div") .attr("class", "memory-leftbar") .style("width", function(d) { return 1 + Math.min(Math.abs(Math.min(Math.round(d.propMemDealloc * 5), 0)), 5) + "px"; }); memoryBarContainer.append("div") .attr("class", "memory-rightbar") .style("width", function(d) { return 1 + Math.min(Math.max(Math.round(d.propMemAlloc * 13), 0), 13) + "px"; }); newRows.append("td") .attr("class", "treetable-memory memory-info-right") .text(function(d) { return roundOneDecimal(d.sumMemAlloc); }); newRows.append("td") .attr("class", "time-info") .text(function(d) { return d.sumTime; }); var timeCell = newRows.append("td") .attr("class", "time-bar-container"); timeCell.append("div") .attr("class", "timebar") .style("width", function(d) { return Math.round(d.propTime * 20) + "px"; }); var unorderedRows = d3.selectAll("tr.treetable-row") .data(vis.profTable, function(d) { return d.id; }); unorderedRows.sort(function(a,b) { return (a.id < b.id) ? -1 : (a.id == b.id ? 0 : 1); }); useMemoryResults(); } var buildProfTable = function (profTree) { var head = jQuery.extend({}, profTree); var nodes = [head]; var aggregateChildren = function(node) { var nameMap = {}; node.children.forEach(function(c) { var nameMapEntry = nameMap[c.label]; if (!nameMapEntry) { nameMapEntry = jQuery.extend({}, c); nameMapEntry.sumTime = c.endTime - c.startTime; nameMapEntry.sumChildren = []; nameMapEntry.children = []; nameMapEntry.parent = node; nameMapEntry.sumCount = 1; } else { nameMapEntry.sumMem = nameMapEntry.sumMem + c.sumMem; nameMapEntry.sumMemDealloc = nameMapEntry.sumMemDealloc + c.sumMemDealloc; nameMapEntry.sumMemAlloc = nameMapEntry.sumMemAlloc + c.sumMemAlloc; nameMapEntry.sumTime = nameMapEntry.sumTime + (c.endTime - c.startTime); nameMapEntry.sumCount = nameMapEntry.sumCount + 1; } nameMapEntry.propMem = nameMapEntry.sumMem / vis.totalMem; nameMapEntry.propMemDealloc = nameMapEntry.sumMemDealloc / vis.totalMem; nameMapEntry.propMemAlloc = nameMapEntry.sumMemAlloc / vis.totalMem; nameMapEntry.propTime = nameMapEntry.sumTime / vis.totalTime; c.children.forEach(function(e) { nameMapEntry.children.push(e); }); nameMap[c.label] = nameMapEntry; }); var childrenSum = []; for (var label in nameMap) { childrenSum.push(nameMap[label]); } // Sort by time descending childrenSum.sort(function(a, b) { return b.sumTime - a.sumTime }); return childrenSum; }; function addToNodesAt(c, i) { nodes.splice(i, 0, c); } var id = 0; while (nodes.length > 0) { var node = nodes.shift(); node.id = id; id = id + 1; node.sumChildren = aggregateChildren(node); // Processing in order is important to preserve order of IDs! node.sumChildren.forEach(addToNodesAt); } return head.sumChildren; }; function useMemoryResults() { d3.selectAll(".treetable-memory").style("display", vis.hideMemory ? "none" : ""); } vis.profTable = buildProfTable(vis.profTree); vis.profTable.forEach(function(e) { e.visualDepth = 1; }); updateRows(); return { el: el, onResize: updateRows, onOptionsChange: updateRows, onUpdateInternals: function() { }, useMemoryResults: useMemoryResults }; } function enableScroll() { vis.codeTable.enableScroll(); vis.flameGraph.enableZoom(); } function disableScroll() { vis.codeTable.disableScroll(); vis.flameGraph.disableZoom(); } // Set up resizing -------------------------------------------------------- // This is used as a jQuery event namespace so that we can remove the window // resize handler on subsequent calls to initResizing(). Not elegant, but it // gets the job done. var resizeCallbackNamespace = randomString(10); // Resize panel1 and panel2 to 50% of available space and add callback // for window resizing. function initResizing() { var $el = $(vis.el); var $panel1 = $el.children(".profvis-panel1"); var $panel2 = $el.children(".profvis-panel2"); var $splitBar = $el.children(".profvis-splitbar"); var $statusBar = $el.children(".profvis-status-bar"); // Clear any existing positioning that may have happened from previous // calls to this function and the callbacks that it sets up. $panel1.removeAttr("style"); $panel2.removeAttr("style"); $splitBar.removeAttr("style"); $statusBar.removeAttr("style"); // CSS class suffix for split direction var splitClass = (vis.splitDir === "h") ? "horizontal" : "vertical"; // Remove existing horizontal/vertical class and add the correct class back. $panel1.removeClass("profvis-panel1-horizontal profvis-panel1-vertical"); $panel2.removeClass("profvis-panel2-horizontal profvis-panel2-vertical"); $splitBar.removeClass("profvis-splitbar-horizontal profvis-splitbar-vertical"); $panel1.addClass("profvis-panel1-" + splitClass); $panel2.addClass("profvis-panel2-" + splitClass); $splitBar.addClass("profvis-splitbar-" + splitClass); var splitBarGap; var margin; // Record the proportions from the previous call to resizePanels. This is // needed when we resize the window to preserve the same proportions. var lastSplitProportion; if (vis.splitDir === "v") { // Record the gap between the split bar and the objects to left and right splitBarGap = { left: $splitBar.offset().left - offsetRight($panel1), right: $panel2.offset().left - offsetRight($splitBar) }; // Capture the initial distance from the left and right of container element margin = { left: $panel1.position().left, right: $el.innerWidth() - positionRight($panel2) }; } else if (vis.splitDir === "h") { splitBarGap = { top: $splitBar.offset().top - offsetBottom($panel1), bottom: $panel2.offset().top - offsetBottom($splitBar) }; margin = { top: $panel1.position().top, bottom: $el.innerWidth() - positionBottom($panel2) }; } // Resize the panels. splitProportion is a number from 0-1 representing the // horizontal position of the split bar. function resizePanels(splitProportion) { if (!splitProportion) splitProportion = lastSplitProportion; if (vis.splitDir === "v") { var innerWidth = offsetRight($panel2) - $panel1.offset().left; $splitBar.offset({ left: $panel1.offset().left + innerWidth * splitProportion - $splitBar.outerWidth()/2 }); // Size and position the panels $panel1.outerWidth($splitBar.position().left - splitBarGap.left - margin.left); $panel2.offset({ left: offsetRight($splitBar) + splitBarGap.right }); } else if (vis.splitDir === "h") { var innerHeight = offsetBottom($panel2) - $panel1.offset().top; $splitBar.offset({ top: $panel1.offset().top + innerHeight * splitProportion - $splitBar.outerHeight()/2 }); // Size and position the panels $panel1.outerHeight($splitBar.position().top - splitBarGap.top - margin.top); $panel2.offset({ top: offsetBottom($splitBar) + splitBarGap.bottom }); } lastSplitProportion = splitProportion; } // Initially, set widths to 50/50 // For the first sizing, we don't need to call vis.flameGraph.onResize() // because this happens before the flame graph is generated. resizePanels(0.5); var resizePanelsDebounced = debounce(function() { resizePanels(lastSplitProportion); vis.activeViews.forEach(function(e) { if (e.onResize) e.onResize(); }); }, 250); // Clear old resize handler and add new one. We use a namespace for this // visualization to make sure not to delete handlers for other profvis // visualizations on the same page (this can happen with Rmd documents). $(window).off("resize.profvis." + resizeCallbackNamespace); $(window).on("resize.profvis." + resizeCallbackNamespace, resizePanelsDebounced); // Get current proportional position of split bar function splitProportion() { var splitCenter; if (vis.splitDir === "v") { splitCenter = $splitBar.offset().left - $panel1.offset().left + $splitBar.outerWidth()/2; var innerWidth = offsetRight($panel2) - $panel1.offset().left; return splitCenter / innerWidth; } else if (vis.splitDir === "h") { splitCenter = $splitBar.offset().top - $panel1.offset().top + $splitBar.outerHeight()/2; var innerHeight = offsetBottom($panel2) - $panel1.offset().top; return splitCenter / innerHeight; } } function positionRight($el) { return $el.position().left + $el.outerWidth(); } function offsetRight($el) { return $el.offset().left + $el.outerWidth(); } function positionBottom($el) { return $el.position().top + $el.outerHeight(); } function offsetBottom($el) { return $el.offset().top + $el.outerHeight(); } // Enable dragging of the split bar --------------------------------------- (function() { var dragging = false; // For vertical split (left-right dragging) var startDragX; var startOffsetLeft; // For horizontal split (up-down dragging) var startDragY; var startOffsetTop; var stopDrag = function(e) { if (!dragging) return; dragging = false; document.removeEventListener("mousemove", drag); document.removeEventListener("mouseup", stopDrag); $splitBar.css("opacity", ""); if ((vis.splitDir === "v" && e.pageX - startDragX === 0) || (vis.splitDir === "h" && e.pageY - startDragY === 0)) { return; } resizePanels(splitProportion()); vis.flameGraph.onResize(); }; var startDrag = function(e) { // Don't start another drag if we're already in one. if (dragging) return; dragging = true; pauseEvent(e); $splitBar.css("opacity", 0.75); if (vis.splitDir === "v") { startDragX = e.pageX; startOffsetLeft = $splitBar.offset().left; } else { startDragY = e.pageY; startOffsetTop = $splitBar.offset().top; } document.addEventListener("mousemove", drag); document.addEventListener("mouseup", stopDrag); }; var drag = function(e) { if (!dragging) return; pauseEvent(e); if (vis.splitDir === "v") { var dx = e.pageX - startDragX; if (dx === 0) return; // Move the split bar $splitBar.offset({ left: startOffsetLeft + dx }); } else if (vis.splitDir === "h") { var dy = e.pageY - startDragY; if (dy === 0) return; // Move the split bar $splitBar.offset({ top: startOffsetTop + dy }); } }; // Stop propogation so that we don't select text while dragging function pauseEvent(e){ if(e.stopPropagation) e.stopPropagation(); if(e.preventDefault) e.preventDefault(); e.cancelBubble = true; e.returnValue = false; return false; } // Remove existing event listener from previous calls to initResizing(). $splitBar.off("mousedown.profvis"); $splitBar.on("mousedown.profvis", startDrag); })(); return { resizePanels: resizePanels }; } var prof = prepareProfData(message.prof, message.interval); var vis = { el: el, prof: prof, profTree: getProfTree(prof), interval: message.interval, totalTime: getTotalTime(prof), totalMem: getTotalMemory(prof), files: message.files, aggLabelTimes: getAggregatedLabelTimes(prof), fileLineStats: getFileLineStats(prof, message.files), profTable: [], // Objects representing each component statusBar: null, optionsPanel: null, codeTable: null, flameGraph: null, infoBox: null, treetable: null, activeViews: [], // Functions to enable/disable responding to scrollwheel events enableScroll: enableScroll, disableScroll: disableScroll, splitDir: message.split, hideInternals: true, hideMemory: false, resizePanels: null }; // Render the objects --------------------------------------------- var statusBarEl = document.createElement("div"); statusBarEl.className = "profvis-status-bar"; vis.el.appendChild(statusBarEl); // Container panels - top/bottom or left/right var panel1 = document.createElement("div"); panel1.className = "profvis-panel1"; vis.el.appendChild(panel1); var panel2 = document.createElement("div"); panel2.className = "profvis-panel2"; vis.el.appendChild(panel2); var splitBarEl = document.createElement("div"); splitBarEl.className = "profvis-splitbar"; vis.el.appendChild(splitBarEl); var footerEl = document.createElement("div"); footerEl.className = "profvis-footer"; vis.el.appendChild(footerEl); // Items in the panels var codeTableEl = document.createElement("div"); codeTableEl.className = "profvis-code"; panel1.appendChild(codeTableEl); var flameGraphEl = document.createElement("div"); flameGraphEl.className = "profvis-flamegraph"; panel2.appendChild(flameGraphEl); var infoBoxEl = document.createElement("div"); infoBoxEl.className = "profvis-infobox"; panel2.appendChild(infoBoxEl); var treetableEl = document.createElement("div"); treetableEl.className = "profvis-treetable"; treetableEl.style.display = "none"; vis.el.appendChild(treetableEl); var optionsPanelEl = document.createElement("div"); optionsPanelEl.className = "profvis-options-panel"; vis.el.appendChild(optionsPanelEl); // Efficient to properly size panels before the code + flamegraph are // rendered, so that we don't have to re-render. var resize = initResizing(); vis.resizePanels = resize.resizePanels; var hideViews = function() { splitBarEl.style.display = "none"; panel1.style.display = "none"; panel2.style.display = "none"; treetableEl.style.display = "none"; }; var toggleViews = function(view) { hideViews(); switch (view) { case "flamegraph": splitBarEl.style.display = "block"; panel1.style.display = "block"; panel2.style.display = "block"; vis.activeViews = [vis.flameGraph, vis.codeTable]; vis.resizePanels(); break; case "treetable": if (!vis.treetable) { vis.treetable = generateTreetable(treetableEl); } treetableEl.style.display = "block"; vis.activeViews = [vis.treetable]; break; } vis.activeViews.forEach(function(e) { if (e.onResize) e.onResize(); }); }; var onOptionsChange = function(option, checked) { switch (option) { case "split": { vis.splitDir = checked ? "h" : "v"; // Check that flame graph is visible if ($.inArray(vis.flameGraph, vis.activeViews) !== -1) { initResizing(); vis.flameGraph.onResize(); } break; } case "internals": { vis.flameGraph.savePrevScales(); vis.hideInternals = checked; if (checked) { vis.flameGraph.useCollapsedDepth(); vis.flameGraph.redrawCollapse(400, 400); } else { vis.flameGraph.useUncollapsedDepth(); vis.flameGraph.redrawUncollapse(400, 250); } vis.activeViews.forEach(function(e) { if (e.onOptionsChange) e.onOptionsChange(); }); break; } case "memory": { vis.hideMemory = checked; vis.activeViews.forEach(function(e) { if (e.useMemoryResults) e.useMemoryResults(); }); break; } } }; // Create the UI components vis.statusBar = generateStatusBar(statusBarEl, toggleViews); vis.footer = generateFooter(footerEl); vis.optionsPanel = generateOptionsPanel(optionsPanelEl, onOptionsChange); vis.codeTable = generateCodeTable(codeTableEl); vis.flameGraph = generateFlameGraph(flameGraphEl); vis.infoBox = initInfoBox(infoBoxEl); vis.treetable = null; vis.activeViews = [vis.flameGraph, vis.codeTable]; // If any depth collapsing occured, enable the "hide internal" checkbox. if (prof.some(function(d) { return d.depth !== d.depthCollapsed; })) { vis.optionsPanel.enableHideInternal(); } // Start with scrolling disabled because of mousewheel scrolling issue disableScroll(); // Make the vis object accessible via the DOM element $(el).data("profvis", vis); return vis; }; // profvis.render() // Calculate amount of time spent on each line of code. Returns nested objects // grouped by file, and then by line number. function getFileLineStats(prof, files) { // Drop entries with null or "" filename prof = prof.filter(function(row) { return row.filename !== null && row.filename !== ""; }); // Gather line-by-line file contents var fileLineStats = files.map(function(file) { // Create array of objects with info for each line of code. var lines = file.content.split("\n"); var lineData = []; var filename = file.filename; var normpath = file.normpath; for (var i=0; i