d3-flame-graph
Version:
A d3.js library to produce flame graphs.
987 lines (986 loc) • 25.7 kB
JavaScript
import { select } from "d3-selection";
import { format } from "d3-format";
import { ascending } from "d3-array";
import { partition, hierarchy } from "d3-hierarchy";
import { scaleLinear } from "d3-scale";
import { easeCubic } from "d3-ease";
import "d3-transition";
import "d3-dispatch";
import './d3-flamegraph.css';function generateHash(name) {
const MAX_CHAR = 6;
let hash = 0;
let maxHash = 0;
let weight = 1;
const mod = 10;
if (name) {
for (let i = 0; i < name.length; i++) {
if (i > MAX_CHAR) {
break;
}
hash += weight * (name.charCodeAt(i) % mod);
maxHash += weight * (mod - 1);
weight *= 0.7;
}
if (maxHash > 0) {
hash = hash / maxHash;
}
}
return hash;
}
function generateColorVector(name) {
let vector = 0;
if (name) {
const nameArr = name.split("`");
if (nameArr.length > 1) {
name = nameArr[nameArr.length - 1];
}
name = name.split("(")[0];
vector = generateHash(name);
}
return vector;
}
function calculateColor(hue, vector) {
let r;
let g;
let b;
if (hue === "red") {
r = 200 + Math.round(55 * vector);
g = 50 + Math.round(80 * vector);
b = g;
} else if (hue === "orange") {
r = 190 + Math.round(65 * vector);
g = 90 + Math.round(65 * vector);
b = 0;
} else if (hue === "yellow") {
r = 175 + Math.round(55 * vector);
g = r;
b = 50 + Math.round(20 * vector);
} else if (hue === "green") {
r = 50 + Math.round(60 * vector);
g = 200 + Math.round(55 * vector);
b = r;
} else if (hue === "pastelgreen") {
r = 163 + Math.round(75 * vector);
g = 195 + Math.round(49 * vector);
b = 72 + Math.round(149 * vector);
} else if (hue === "blue") {
r = 91 + Math.round(126 * vector);
g = 156 + Math.round(76 * vector);
b = 221 + Math.round(26 * vector);
} else if (hue === "aqua") {
r = 50 + Math.round(60 * vector);
g = 165 + Math.round(55 * vector);
b = g;
} else if (hue === "cold") {
r = 0 + Math.round(55 * (1 - vector));
g = 0 + Math.round(230 * (1 - vector));
b = 200 + Math.round(55 * vector);
} else {
r = 200 + Math.round(55 * vector);
g = 0 + Math.round(230 * (1 - vector));
b = 0 + Math.round(55 * (1 - vector));
}
return "rgb(" + r + "," + g + "," + b + ")";
}
function defaultLabel(d) {
return d.data.name;
}
function defaultFlamegraphTooltip() {
var rootElement = select("body");
var tooltip2 = null;
var html = defaultLabel;
var text = defaultLabel;
var contentIsHTML = false;
function tip() {
tooltip2 = rootElement.append("div").style("display", "none").style("position", "absolute").style("opacity", 0).style("pointer-events", "none").attr("class", "d3-flame-graph-tip");
}
tip.show = function(event, d) {
if (contentIsHTML) {
tooltip2.html(html(d));
} else {
tooltip2.text(text(d));
}
var tipWidth = tooltip2.node().offsetWidth;
var tipHeight = tooltip2.node().offsetHeight;
var left = event.pageX + 5;
var top = event.pageY + 5;
if (left + tipWidth > window.innerWidth) {
left = event.pageX - tipWidth - 5;
}
if (left < 0) {
left = 5;
}
if (top + tipHeight > window.innerHeight) {
top = event.pageY - tipHeight - 5;
}
if (top < 0) {
top = 5;
}
tooltip2.style("display", "block").style("left", left + "px").style("top", top + "px").transition().duration(200).style("opacity", 1).style("pointer-events", "all");
return tip;
};
tip.hide = function() {
tooltip2.style("display", "none").transition().duration(200).style("opacity", 0).style("pointer-events", "none");
return tip;
};
tip.text = function(_) {
if (!arguments.length) return text;
text = _;
contentIsHTML = false;
return tip;
};
tip.html = function(_) {
if (!arguments.length) return html;
html = _;
contentIsHTML = true;
return tip;
};
tip.destroy = function() {
tooltip2.remove();
};
return tip;
}
const tooltip = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
defaultFlamegraphTooltip
}, Symbol.toStringTag, { value: "Module" }));
function pickHex(color1, color2, weight) {
const w1 = weight;
const w2 = 1 - w1;
const rgb = [
Math.round(color1[0] * w1 + color2[0] * w2),
Math.round(color1[1] * w1 + color2[1] * w2),
Math.round(color1[2] * w1 + color2[2] * w2)
];
return rgb;
}
function allocationColorMapper(d, originalColor) {
if (d.highlight) return originalColor;
const self = d.data.value;
const total = d.value;
const color = pickHex([0, 255, 40], [196, 245, 233], self / total);
return `rgb(${color.join()})`;
}
function offCpuColorMapper(d, originalColor) {
if (d.highlight) return originalColor;
const name = d.data.n || d.data.name;
const vector = generateColorVector(name);
const r = 0 + Math.round(55 * (1 - vector));
const g = 0 + Math.round(230 * (1 - vector));
const b = 200 + Math.round(55 * vector);
return "rgb(" + r + "," + g + "," + b + ")";
}
function nodeJsColorMapper(d, originalColor) {
let color = originalColor;
const { v8_jit: v8JIT, javascript, optimized } = d.data.extras || {};
if (v8JIT && !javascript) {
color = "#dadada";
}
if (javascript) {
let opt = (optimized || 0) / d.value;
let r = 255;
let g = 0;
let b = 0;
if (opt < 0.4) {
opt = opt * 2.5;
r = 240 - opt * 200;
} else if (opt < 0.9) {
opt = (opt - 0.4) * 2;
r = 0;
b = 200 - 200 * opt;
g = 100 * opt;
} else {
opt = (opt - 0.9) * 10;
r = 0;
b = 0;
g = 100 + 150 * opt;
}
color = `rgb(${r} , ${g}, ${b})`;
}
return color;
}
function differentialColorMapper(d, originalColor) {
if (d.highlight) return originalColor;
let r = 220;
let g = 220;
let b = 220;
const delta = d.delta || d.data.d || d.data.delta;
const unsignedDelta = Math.abs(delta);
let value = d.value || d.data.v || d.data.value;
if (value <= unsignedDelta) value = unsignedDelta;
const vector = unsignedDelta / value;
if (delta === value) {
r = 255;
g = 190;
b = 90;
} else if (delta > 0) {
b = Math.round(235 * (1 - vector));
g = b;
} else if (delta < 0) {
r = Math.round(235 * (1 - vector));
g = r;
}
return "rgb(" + r + "," + g + "," + b + ")";
}
const colorMapper = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
__proto__: null,
allocationColorMapper,
differentialColorMapper,
nodeJsColorMapper,
offCpuColorMapper
}, Symbol.toStringTag, { value: "Module" }));
function flamegraph() {
let w = 960;
let h = null;
let c = 18;
let selection = null;
let tooltip2 = null;
let title = "";
let transitionDuration = 750;
let transitionEase = easeCubic;
let sort = false;
let inverted = false;
let clickHandler = null;
let hoverHandler = null;
let minFrameSize = 0;
let detailsElement = null;
let searchDetails = null;
let selfValue = false;
let resetHeightOnZoom = false;
let scrollOnZoom = false;
let minHeight = null;
let computeDelta = false;
let colorHue = null;
let getName = function(d) {
return d.data.n || d.data.name;
};
let getValue = function(d) {
if ("v" in d) {
return d.v;
} else {
return d.value;
}
};
let getChildren = function(d) {
return d.c || d.children;
};
let getLibtype = function(d) {
return d.data.l || d.data.libtype;
};
let getDelta = function(d) {
if ("d" in d.data) {
return d.data.d;
} else {
return d.data.delta;
}
};
let searchHandler = function(searchResults, searchSum, totalValue) {
searchDetails = () => {
if (detailsElement) {
detailsElement.textContent = "search: " + searchSum + " of " + totalValue + " total samples ( " + format(".3f")(100 * (searchSum / totalValue), 3) + "%)";
}
};
searchDetails();
};
const originalSearchHandler = searchHandler;
let searchMatch = (d, term, ignoreCase = false) => {
if (!term) {
return false;
}
let label = getName(d);
if (ignoreCase) {
term = term.toLowerCase();
label = label.toLowerCase();
}
const re = new RegExp(term);
return typeof label !== "undefined" && label && label.match(re);
};
const originalSearchMatch = searchMatch;
let detailsHandler = function(d) {
if (detailsElement) {
if (d) {
detailsElement.textContent = d;
} else {
if (typeof searchDetails === "function") {
searchDetails();
} else {
detailsElement.textContent = "";
}
}
}
};
const originalDetailsHandler = detailsHandler;
let labelHandler = function(d) {
return getName(d) + " (" + format(".3f")(100 * (d.x1 - d.x0), 3) + "%, " + getValue(d) + " samples)";
};
let colorMapper2 = function(d) {
return d.highlight ? "#E600E6" : colorHash(getName(d), getLibtype(d));
};
const originalColorMapper = colorMapper2;
function colorHash(name, libtype) {
let hue = colorHue || "warm";
if (!colorHue && !(typeof libtype === "undefined" || libtype === "")) {
hue = "red";
if (typeof name !== "undefined" && name && name.match(/::/)) {
hue = "yellow";
}
if (libtype === "kernel") {
hue = "orange";
} else if (libtype === "jit") {
hue = "green";
} else if (libtype === "inlined") {
hue = "aqua";
}
}
const vector = generateColorVector(name);
return calculateColor(hue, vector);
}
function show(d) {
d.data.fade = false;
d.data.hide = false;
if (d.children) {
d.children.forEach(show);
}
}
function hideSiblings(node) {
let child = node;
let parent = child.parent;
let children, i, sibling;
while (parent) {
children = parent.children;
i = children.length;
while (i--) {
sibling = children[i];
if (sibling !== child) {
sibling.data.hide = true;
}
}
child = parent;
parent = child.parent;
}
}
function fadeAncestors(d) {
if (d.parent) {
d.parent.data.fade = true;
fadeAncestors(d.parent);
}
}
function zoom(d, node) {
if (tooltip2) tooltip2.hide();
hideSiblings(d);
show(d);
fadeAncestors(d);
update();
if (scrollOnZoom) {
const chartOffset = node.parentNode.offsetTop;
const maxFrames = (window.innerHeight - chartOffset) / c;
const frameOffset = (d.height - maxFrames + 10) * c;
window.scrollTo({
top: chartOffset + frameOffset,
left: 0,
behavior: "smooth"
});
}
if (typeof clickHandler === "function") {
clickHandler(d);
}
}
function searchTree(d, term) {
const results = [];
let sum = 0;
function searchInner(d2, foundParent) {
let found = false;
if (searchMatch(d2, term)) {
d2.highlight = true;
found = true;
if (!foundParent) {
sum += getValue(d2);
}
results.push(d2);
} else {
d2.highlight = false;
}
if (d2.children) {
d2.children.forEach(function(child) {
searchInner(child, foundParent || found);
});
}
}
searchInner(d, false);
return [results, sum];
}
function findTree(d, id) {
if (d.id === id) {
return d;
} else {
const children = d.children;
if (children) {
for (let i = 0; i < children.length; i++) {
const found = findTree(children[i], id);
if (found) {
return found;
}
}
}
}
}
function clear(d) {
d.highlight = false;
if (d.children) {
d.children.forEach(function(child) {
clear(child);
});
}
}
function doSort(a, b) {
if (typeof sort === "function") {
return sort(a, b);
} else if (sort) {
return ascending(getName(a), getName(b));
}
}
const p = partition();
function filterNodes(root) {
let nodeList = root.descendants();
if (minFrameSize > 0) {
const kx = w / (root.x1 - root.x0);
nodeList = nodeList.filter(function(el) {
return (el.x1 - el.x0) * kx > minFrameSize;
});
}
return nodeList;
}
function update() {
selection.each(function(root) {
const x = scaleLinear().range([0, w]);
const y = scaleLinear().range([0, c]);
if (sort) root.sort(doSort);
reappraiseNode(root);
p(root);
const kx = w / (root.x1 - root.x0);
function width(d) {
return (d.x1 - d.x0) * kx;
}
const descendants = filterNodes(root);
const svg = select(this).select("svg");
svg.attr("width", w);
let g = svg.selectAll("g").data(descendants, function(d) {
return d.id;
});
if (!h || resetHeightOnZoom) {
let maxDepth = 0;
for (let i = 0; i < descendants.length; ++i) {
if (descendants[i].depth > maxDepth) {
maxDepth = descendants[i].depth;
}
}
h = (maxDepth + 3) * c;
if (h < minHeight) h = minHeight;
svg.attr("height", h);
}
g.transition().duration(transitionDuration).ease(transitionEase).attr("transform", function(d) {
return "translate(" + x(d.x0) + "," + (inverted ? y(d.depth) : h - y(d.depth) - c) + ")";
});
g.select("rect").transition().duration(transitionDuration).ease(transitionEase).attr("width", width);
const node = g.enter().append("svg:g").attr("transform", function(d) {
return "translate(" + x(d.x0) + "," + (inverted ? y(d.depth) : h - y(d.depth) - c) + ")";
});
node.append("svg:rect").transition().delay(transitionDuration / 2).attr("width", width);
if (!tooltip2) {
node.append("svg:title");
}
node.append("foreignObject").append("xhtml:div");
g = svg.selectAll("g").data(descendants, function(d) {
return d.id;
});
g.attr("width", width).attr("height", function(_) {
return c;
}).attr("name", function(d) {
return getName(d);
}).attr("class", function(d) {
return d.data.fade ? "frame fade" : "frame";
});
g.select("rect").attr("height", function(_) {
return c;
}).attr("fill", function(d) {
return colorMapper2(d);
});
if (!tooltip2) {
g.select("title").text(labelHandler);
}
g.select("foreignObject").attr("width", width).attr("height", function(_) {
return c;
}).style("pointer-events", "none").select("div").attr("class", "d3-flame-graph-label").style("display", function(d) {
return width(d) < 35 ? "none" : "block";
}).transition().delay(transitionDuration).text(getName);
g.on("click", function(event, d) {
zoom(d, this);
});
g.exit().remove();
g.on("mouseover", function(event, d) {
if (tooltip2) tooltip2.show(event, d);
detailsHandler(labelHandler(d));
if (typeof hoverHandler === "function") {
hoverHandler(d);
}
}).on("mouseout", function() {
if (tooltip2) tooltip2.hide();
detailsHandler(null);
});
});
}
function merge(data, samples) {
samples.forEach(function(sample) {
const node = data.find(function(element) {
return element.name === sample.name;
});
if (node) {
node.value += sample.value;
if (sample.children) {
if (!node.children) {
node.children = [];
}
merge(node.children, sample.children);
}
} else {
data.push(sample);
}
});
}
function forEachNode(node, f) {
f(node);
let children = node.children;
if (children) {
const stack = [children];
let count, child, grandChildren;
while (stack.length) {
children = stack.pop();
count = children.length;
while (count--) {
child = children[count];
f(child);
grandChildren = child.children;
if (grandChildren) {
stack.push(grandChildren);
}
}
}
}
}
function adoptNode(node) {
let id = 0;
forEachNode(node, function(n) {
n.id = id++;
});
}
function reappraiseNode(root) {
let node, children, grandChildren, childrenValue, i, j, child, childValue;
const stack = [];
const included = [];
const excluded = [];
const compoundValue = !selfValue;
let item = root.data;
if (item.hide) {
root.value = 0;
children = root.children;
if (children) {
excluded.push(children);
}
} else {
root.value = item.fade ? 0 : getValue(item);
stack.push(root);
}
while (node = stack.pop()) {
children = node.children;
if (children && (i = children.length)) {
childrenValue = 0;
while (i--) {
child = children[i];
item = child.data;
if (item.hide) {
child.value = 0;
grandChildren = child.children;
if (grandChildren) {
excluded.push(grandChildren);
}
continue;
}
if (item.fade) {
child.value = 0;
} else {
childValue = getValue(item);
child.value = childValue;
childrenValue += childValue;
}
stack.push(child);
}
if (compoundValue && node.value) {
node.value -= childrenValue;
}
included.push(children);
}
}
i = included.length;
while (i--) {
children = included[i];
childrenValue = 0;
j = children.length;
while (j--) {
childrenValue += children[j].value;
}
children[0].parent.value += childrenValue;
}
while (excluded.length) {
children = excluded.pop();
j = children.length;
while (j--) {
child = children[j];
child.value = 0;
grandChildren = child.children;
if (grandChildren) {
excluded.push(grandChildren);
}
}
}
}
function processData() {
selection.datum((data) => {
if (data.constructor.name !== "Node") {
const root = hierarchy(data, getChildren);
adoptNode(root);
reappraiseNode(root);
root.originalValue = root.value;
if (computeDelta) {
root.eachAfter((node) => {
let sum = getDelta(node);
const children = node.children;
let i = children && children.length;
while (--i >= 0) sum += children[i].delta;
node.delta = sum;
});
}
return root;
}
});
}
function chart(s) {
if (!arguments.length) {
return chart;
}
selection = s;
processData();
selection.each(function(_) {
if (select(this).select("svg").size() === 0) {
const svg = select(this).append("svg:svg").attr("width", w).attr("class", "partition d3-flame-graph");
if (h) {
if (h < minHeight) h = minHeight;
svg.attr("height", h);
}
svg.append("svg:text").attr("class", "title").attr("text-anchor", "middle").attr("y", "25").attr("x", w / 2).attr("fill", "#808080").text(title);
if (tooltip2) svg.call(tooltip2);
}
});
update();
}
chart.height = function(_) {
if (!arguments.length) {
return h;
}
h = _;
return chart;
};
chart.minHeight = function(_) {
if (!arguments.length) {
return minHeight;
}
minHeight = _;
return chart;
};
chart.width = function(_) {
if (!arguments.length) {
return w;
}
w = _;
return chart;
};
chart.cellHeight = function(_) {
if (!arguments.length) {
return c;
}
c = _;
return chart;
};
chart.tooltip = function(_) {
if (!arguments.length) {
return tooltip2;
}
if (typeof _ === "function") {
tooltip2 = _;
}
return chart;
};
chart.title = function(_) {
if (!arguments.length) {
return title;
}
title = _;
return chart;
};
chart.transitionDuration = function(_) {
if (!arguments.length) {
return transitionDuration;
}
transitionDuration = _;
return chart;
};
chart.transitionEase = function(_) {
if (!arguments.length) {
return transitionEase;
}
transitionEase = _;
return chart;
};
chart.sort = function(_) {
if (!arguments.length) {
return sort;
}
sort = _;
return chart;
};
chart.inverted = function(_) {
if (!arguments.length) {
return inverted;
}
inverted = _;
return chart;
};
chart.computeDelta = function(_) {
if (!arguments.length) {
return computeDelta;
}
computeDelta = _;
return chart;
};
chart.setLabelHandler = function(_) {
if (!arguments.length) {
return labelHandler;
}
labelHandler = _;
return chart;
};
chart.label = chart.setLabelHandler;
chart.search = function(term) {
const searchResults = [];
let searchSum = 0;
let totalValue = 0;
selection.each(function(data) {
const res = searchTree(data, term);
searchResults.push(...res[0]);
searchSum += res[1];
totalValue += data.originalValue;
});
searchHandler(searchResults, searchSum, totalValue);
update();
};
chart.findById = function(id) {
if (typeof id === "undefined" || id === null) {
return null;
}
let found = null;
selection.each(function(data) {
if (found === null) {
found = findTree(data, id);
}
});
return found;
};
chart.clear = function() {
detailsHandler(null);
selection.each(function(root) {
clear(root);
update();
});
};
chart.zoomTo = function(d) {
zoom(d, selection.node());
};
chart.resetZoom = function() {
selection.each(function(root) {
zoom(root, selection.node());
});
};
chart.onClick = function(_) {
if (!arguments.length) {
return clickHandler;
}
clickHandler = _;
return chart;
};
chart.onHover = function(_) {
if (!arguments.length) {
return hoverHandler;
}
hoverHandler = _;
return chart;
};
chart.merge = function(data) {
if (!selection) {
return chart;
}
this.resetZoom();
searchDetails = null;
detailsHandler(null);
selection.datum((root) => {
merge([root.data], [data]);
return root.data;
});
processData();
update();
return chart;
};
chart.update = function(data) {
if (!selection) {
return chart;
}
if (data) {
selection.datum(data);
processData();
}
update();
return chart;
};
chart.destroy = function() {
if (!selection) {
return chart;
}
if (tooltip2) {
tooltip2.hide();
if (typeof tooltip2.destroy === "function") {
tooltip2.destroy();
}
}
selection.selectAll("svg").remove();
return chart;
};
chart.setColorMapper = function(_) {
if (!arguments.length) {
colorMapper2 = originalColorMapper;
return chart;
}
colorMapper2 = (d) => {
const originalColor = originalColorMapper(d);
return _(d, originalColor);
};
return chart;
};
chart.color = chart.setColorMapper;
chart.setColorHue = function(_) {
if (!arguments.length) {
colorHue = null;
return chart;
}
colorHue = _;
return chart;
};
chart.minFrameSize = function(_) {
if (!arguments.length) {
return minFrameSize;
}
minFrameSize = _;
return chart;
};
chart.setDetailsElement = function(_) {
if (!arguments.length) {
return detailsElement;
}
detailsElement = _;
return chart;
};
chart.details = chart.setDetailsElement;
chart.selfValue = function(_) {
if (!arguments.length) {
return selfValue;
}
selfValue = _;
return chart;
};
chart.resetHeightOnZoom = function(_) {
if (!arguments.length) {
return resetHeightOnZoom;
}
resetHeightOnZoom = _;
return chart;
};
chart.scrollOnZoom = function(_) {
if (!arguments.length) {
return scrollOnZoom;
}
scrollOnZoom = _;
return chart;
};
chart.getName = function(_) {
if (!arguments.length) {
return getName;
}
getName = _;
return chart;
};
chart.getValue = function(_) {
if (!arguments.length) {
return getValue;
}
getValue = _;
return chart;
};
chart.getChildren = function(_) {
if (!arguments.length) {
return getChildren;
}
getChildren = _;
return chart;
};
chart.getLibtype = function(_) {
if (!arguments.length) {
return getLibtype;
}
getLibtype = _;
return chart;
};
chart.getDelta = function(_) {
if (!arguments.length) {
return getDelta;
}
getDelta = _;
return chart;
};
chart.setSearchHandler = function(_) {
if (!arguments.length) {
searchHandler = originalSearchHandler;
return chart;
}
searchHandler = _;
return chart;
};
chart.setDetailsHandler = function(_) {
if (!arguments.length) {
detailsHandler = originalDetailsHandler;
return chart;
}
detailsHandler = _;
return chart;
};
chart.setSearchMatch = function(_) {
if (!arguments.length) {
searchMatch = originalSearchMatch;
return chart;
}
searchMatch = _;
return chart;
};
return chart;
}
export {
colorMapper,
flamegraph as default,
tooltip
};