UNPKG

d3-flame-graph

Version:

A d3.js library to produce flame graphs.

987 lines (986 loc) 25.7 kB
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 };