/*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 0) {
node.depthCollapsed = null;
} else {
node.depthCollapsed = curCollapsedDepth;
curCollapsedDepth++;
}
if (node.label === "..stacktraceon..") {
stacktraceOffCount--;
}
// Recurse
node.children.forEach(function(x) {
calculateDepths(x, curCollapsedDepth, stacktraceOffCount);
});
}
}
// Transform column-oriented data (an object with arrays) to row-oriented data
// (an array of objects).
function colToRows(x) {
var colnames = d3.keys(x);
if (colnames.length === 0)
return [];
var newdata = [];
for (var i=0; i < x[colnames[0]].length; i++) {
var row = {};
for (var j=0; j < colnames.length; j++) {
var colname = colnames[j];
row[colname] = x[colname][i];
}
newdata[i] = row;
}
return newdata;
}
// Given an array with two values (a min and max), return an array with the
// range expanded by `amount`.
function expandRange(range, amount) {
var adjust = amount * (range[1] - range[0]);
return [
range[0] - adjust,
range[1] + adjust
];
}
// Escape an HTML string.
function escapeHTML(text) {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// This returns the current page URL without any trailing hash. Should be
// used in url() references in SVGs to avoid problems when there's a
// tag in the document.
function urlNoHash() {
return window.location.href.split("#")[0];
}
function debounce(f, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
f.apply(context, args);
}, delay);
};
}
function randomString(length) {
var chars = 'abcdefghijklmnopqrstuvwxyz';
var result = '';
for (var i = length; i > 0; --i)
result += chars[Math.floor(Math.random() * chars.length)];
return result;
}
var getNormPath = function(files, filename) {
var normpath = null;
files.forEach(function(e) {
if (e.filename == filename) {
normpath = e.normpath;
}
});
return normpath;
};
(function() {
// Prevent unwanted scroll capturing. Based on the corresponding code in
// https://github.com/rstudio/leaflet
// The rough idea is that we disable scroll wheel zooming inside each
// profvis object, until the user moves the mouse cursor or clicks on the
// visualization. This is trickier than just listening for mousemove,
// because mousemove is fired when the page is scrolled, even if the user
// did not physically move the mouse. We handle this by examining the
// mousemove event's screenX and screenY properties; if they change, we know
// it's a "true" move.
//
// There's a complication to this: when the mouse wheel is scrolled quickly,
// on the step where the profvis DOM object overlaps the cursor, sometimes
// the mousemove event happens before the mousewheel event, and sometimes
// it's the reverse (at least on Chrome 46 on Linux). This means that we
// can't rely on the mousemove handler disabling the profvis object's zoom
// before a scroll event is triggered on the profvis object (cauzing
// zooming). In order to deal with this, we start each profvis object with
// zooming disabled, and also disable zooming when the cursor leaves the
// profvis div. That way, even if a mousewheel event gets triggered on the
// object before the mousemove, it won't cause zooming.
// lastScreen can never be null, but its x and y can.
var lastScreen = { x: null, y: null };
$(document)
.on("mousewheel DOMMouseScroll", function(e) {
// Any mousemove events at this screen position will be ignored.
lastScreen = { x: e.originalEvent.screenX, y: e.originalEvent.screenY };
})
.on("mousemove", ".profvis", function(e) {
// Did the mouse really move?
if (lastScreen.x !== null && e.screenX !== lastScreen.x || e.screenY !== lastScreen.y) {
$(this).data("profvis").flameGraph.enableZoom();
lastScreen = { x: null, y: null };
}
})
.on("mousedown", ".profvis", function(e) {
// Clicking always enables zooming.
$(this).data("profvis").flameGraph.enableZoom();
lastScreen = { x: null, y: null };
})
.on("mouseleave", ".profvis", function(e) {
$(this).data("profvis").flameGraph.disableZoom();
});
})();
if (!Element.prototype.scrollIntoViewIfNeeded) {
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
var parent = this.parentNode,
parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
}
return profvis;
})();