UNPKG

profile_flame

Version:

A javascript lib renders profile flame graph based on d3.js.

1,421 lines (1,317 loc) 56.5 kB
(function() { 'use strict'; function profileFlame () { // configuable variables var // size of whole svg size = [1024, 768], // click handlers on entries clickHandlers = [], // compare mode swicher, // an array of two data dict must be passed when in compare mode compare = false, // reverse two data dict while comparing compareReverse = false, // four thresholds to seperate each entry of compare data // into five categories, such as worst, worse, normal, better, best thresholds = [ -0.5, -0.1, 0.1, 0.5 ], // entries with val / total < cutoff will not be displayed. cutoff = 0.0005, // use internal or cumulative to compare compareMethod = 'cumulative', // only compare entry with percent larger than this arg // is dyed to red or blue compareValidThreshold = 0, // specified entries to show their chains as a flame specifiedEntries = [], // max stack depth to be applied when parsing data. maxDepth = 30, // focus event will highlight whole chain when triggered. focusChain = false; // internal variables var // height of rect of each entry levelHeight = 18, // auto increment field to sign each flame flameIndex = 0, // default color theme theme = 'hot', // global selection for containers selection = null, //height of header and bottom headerHeight = 0, footerHeight = 40, tooltipMargin = 3, tooltipLineHeight = 18, minWidth = 1024, // flame insts insts = {}, // partition object partition = d3.partition(), // scale arg svgScaleK = 1, // current and former value of x when draging svgDragX = {x: 0, ex: 0}, // hide text when it scales too big svgScaleTextThreshold = 2, // final flame height accroding to max stack height flameHeight = null; // theme color generators var colorBase = { hot: { r: function (v){ return 205 + parseInt(50 * v) }, g: function (v){ return parseInt(230 * v) }, b: function (v){ return parseInt(55 * v) } }, cold: { r: function (v){ return parseInt(55 * v) }, g: function (v){ return parseInt(230 * v) }, b: function (v){ return 205 + parseInt(50 * v) } } }; // colors for comparing var compareColors = { blank: '#DDD', darkred: '#B70707', red: '#ed5565', green: '#1ab394', lightblue: 'rgb(31, 179, 243)', blue: '#1c84c6' } ////// BUSINESS FUNCTIONS // put container svg and toolbar function placeContainer (current, inst) { d3.select(current).select('svg.profile-flame').remove(); d3.select(current).select('div.profile-flame-toolbar').remove(); var index = inst.index; var svg = d3.select(current) .append("svg:svg") .attr('class', 'profile-flame') .attr("width", size[0]) .attr("height", size[1]) .attr("class", "profile-flame") .attr('id', index); var mainG = svg .append("g") .attr('class', 'main-g'); svg.mainG = mainG; var svgRect = svg.node().getBoundingClientRect(); inst.svgRect = svgRect; setScale(svg); setGlobalEvent(svg); inst.svg = svg; return inst; } // set global event handler function setGlobalEvent(svg) { // the g elem can get events only if it was focused. var globalKeyupCallback = function(e) { var e = d3.event; if (!e) { return; } var alt = e.altKey; switch(e.code) { // flame focus history actions case 'ArrowLeft': alt && flame.backward(); break; case 'ArrowRight': alt && flame.forward(); break; default: break; } } var body = d3.select('body'); // register only one keydown cb, may be overwritted by other code. body.on('keyup', globalKeyupCallback); } // set scale and drag event handler function setScale(svg) { var mainG = svg.mainG; mainG.call( d3.zoom() // zoom times limited. .scaleExtent([1, 64]) .filter(function(){ // only zoom and drag when alt key pressed return d3.event.altKey }) .on("start", function(){ svgDragX.x = d3.event.transform.x; svgDragX.ex = d3.event.sourceEvent.x; }) .on("zoom", function() { var et = d3.event.sourceEvent.type; var t = d3.event.transform; // reset scale and position when scale to 1 if (t.k <= 1) { t.k = 1; t.x = t.y = 0; } var x = t.x; svgScaleK = t.k; // do not use event transform, it cannot fit scaled x if (et == "mousemove") { t.x = x = svgDragX.x - (svgDragX.ex - d3.event.sourceEvent.x) } // scale main g mainG.attr("transform", "translate({X}, 0) scale({K}, 1)" .replace("{X}", x) .replace("{K}", svgScaleK) ); if (et == "wheel") { // hide text when scale too big // auto drawing on the fly is tested // and showed a pool performce on big flame. if (t.k > svgScaleTextThreshold) { mainG.selectAll("text.gtext") .attr("class", function(d, i, e) { if (!e[0]) { return ""; } e[0].classList.add("hide"); return e[0].classList.value; }); } else { mainG.selectAll("text.gtext") .attr("class", function(d, i, e) { if (!e[0]) { return ""; } e[0].classList.remove("hide"); return e[0].classList.value; }); } } }) ); } // draw no data tip function drawNoData(inst){ inst.svg.append("text") .attr('x', size[0] / 2) .attr('y', 50) .attr('dy', '.35em') .style('font-size', '18px') .text('No Data.'); } // draw flame function draw() { var width = size[0], height = size[1]; selection.each(function(data){ var inst = insts[data._index]; if (inst.noData) { drawNoData(inst); return; } var tree = inst.tree.sum(_sum).sort(_sort); // var tree = inst.tree.sort(_sort); var cutoffDepth = 0; var nodes = partition(tree).descendants().filter(function(n){ // cutoff small entries and hide entries var cut = n.x1 - n.x0 < cutoff; if (!cut) { cutoffDepth = Math.max(cutoffDepth, n.depth); } return !cut; }); var totalWidth = width / (tree.x1 - tree.x0); var height = Math.min( tree.data.realDepth, maxDepth, cutoffDepth || 99999 ) + 1; // if flameHeight exists, use former value // to keep height when switch focus flameHeight = flameHeight || levelHeight * height; var height = flameHeight + headerHeight + footerHeight || height; size[1] = height; inst.svg.attr('height', height); var X = d3.scaleLinear().range([0, width]), Y = d3.scaleLinear().range([0, levelHeight]); var cbTranslate = function(n) { var x = X(n.x0), y = flameHeight - Y(n.depth) + headerHeight - levelHeight; return "translate({X}, {Y})" .replace('{X}', x) .replace('{Y}', y); } var cbDisplay = function(n) { return (( n.x1 - n.x0 ) * totalWidth < 30) || n.data.gap ? 'none': 'block'; } var cbWidth = function(n) { return (n.x1 - n.x0) * totalWidth;} var cbText = function(n) { if (n.data.gap) { return; } var entry = n.data.entry; n.data.entryInfo = formatComplexEntry(entry); entry = n.data.entryInfo.shortEntry; var fontWidth = 7, padding = 3; n.data._showHighlightFunction = false; var width = (n.x1 - n.x0) * totalWidth; if (width < 30) { return; } else { var maxLength = (width - padding) / fontWidth; if (maxLength <= 4) { return; } if (entry.length > maxLength) { return entry.substr(0, maxLength - 3) + '...'; } } if (n.data.entryInfo) { n.data._showHighlightFunction = true; return n.data.entryInfo.entryPrefix; } else { return entry; } } var cbFuncText = function(n) { if (n.data.entryInfo && n.data._showHighlightFunction) { return n.data.entryInfo.function; } return } var cbHeight = function(n) { return levelHeight; } var cbFill = function(n) { return n.data.color; } var cbVisible = function(n) { return n.data.gap || n.data.x1 - n.data.x0 == 0 ? 'hidden' : 'visible'; } var cbClass = function(n) { var class_ = 'node'; if (n.data.virtual) { class_ += ' virtual'; } if (specifiedEntries && specifiedEntries.indexOf(n.data.entry) !== -1) { class_ += ' specified'; } return class_; } var cbTextClass = function() { var c = "gtext"; if (svgScaleK > svgScaleTextThreshold) { c += " hide"; } return c; } var cbId = function(n) { return n.data.id; } inst.svg.mainG.selectAll('g.node').remove(); var g = inst.svg.mainG.selectAll('g.node').data(nodes); var node = g.enter().append('g') .attr('id', cbId) .attr('class', cbClass) .attr('visibility', cbVisible) .attr("transform", cbTranslate) .attr("width", cbWidth); node.append("rect") .attr("height", cbHeight) .attr("fill", cbFill) .attr("class", "grect") .attr("width", cbWidth) .attr('rx', 1.5) .attr('ry', 1.5); var text = node.append("text") .attr('x', 5) .attr('y', levelHeight / 2) .attr("class", cbTextClass) .attr('dy', '.35em') .style("display", cbDisplay) .text(cbText); text.append("tspan") .attr("class", function(n) { return cbTextClass(n) + " highlightFunction"; }) .text(cbFuncText); // node.on event cannot ensure n as a argument. // like mouseout // so pass exact n to cb in closures. node.each(function(n) { d3.select(this) .on('click', function() { nodeClick(n); }) .on('mouseover', function() { focus(n); }) .on('mouseout', function() { unfocus(n); }) .on('contextmenu', function() { pin(n); if (d3.event){ d3.event.preventDefault(); if (d3.event.sourceEvent){ d3.event.sourceEvent.stopPropagation(); } } }) }); g.exit().remove(); hideTooltip(inst); }); } function _parse(node, rawNode, depth){ if (depth == 0) { return; } for (var entry in rawNode) { var rawSubNodes = rawNode[entry][0]; var value = rawNode[entry][1]; var subNode = node.addChild(Node(entry, value)); _parse(subNode, rawSubNodes, depth-1); } } // parse raw profile to a tree function parse(flame, index){ if (!flame) { return; } var tree = Node(); _parse(tree, flame.tree, maxDepth); tree.realDepth = flame.height; return tree.init(index); } // parse two raw profile to a tree by compare them function parseCompare(flame, flameToCompare, index) { if (compareReverse) { var tmp = flame; flame = flameToCompare; flameToCompare = tmp; } var tree = parse(flame, index); var treeToCompare = parse(flameToCompare, index); var compareTree = function(n, _n) { n.compareValue = _n.value; n.comparePercent = _n.percent; n.internalCompareValue = _n.internalValue; n.internalComparePercent = _n.internalPercent; Object.keys(n.index).forEach(function(entry){ var c = n.index[entry]; var _nc = _n.index[entry]; if (c && _nc) { compareTree(c, _nc); } }); } compareTree(tree, treeToCompare); return tree.redyeAll(); } function cleanup(keeps) { keeps = keeps || []; for (var i in insts) { if (keeps.indexOf(i) != -1){ continue; } delete insts[i]; } } // build flame function flame(s) { if (!s) { return flame; } selection = s; var indexes = []; selection.each( function(data) { var index = 'flame' + flameIndex; data['_index'] = index; flameIndex += 1; indexes.push(index); var inst = insts[index] = { id: index, zoomHistory: gnrZoomHistory(), tree: null, title: null, noData: false, }; if (compare && (!data[1].flame || !data[1].flame.tree)){ compare = false; } if (!compare && data.flame === undefined) { // is array, set data to array[0] data = data[0]; } if ( compare ){ if ( !data[0] || !data[1] ){ var err = 'Compare mod needs an array contains two profiles'; throw err; return; } inst.title = 'Compare of ' + data[0].title + ' and ' + data[1].title; inst.tree = parseCompare( data[0].flame, data[1].flame, index ); if (!data[0].flame || data[0].flame.num === 0){ inst.noData = true; } inst.compareFlame = [data[0].flame, data[1].flame]; } else{ inst.title = data.title; inst.tree = parse(data.flame, index); if (!data.flame || data.flame.num === 0){ inst.noData = true; } } placeContainer(this, inst); inst.tree = d3.hierarchy(inst.tree); filterSpecifiedEntries(inst.tree); inst.zoomHistory.reset(inst.tree); return inst; }); cleanup(indexes); // reset flame height to fit new data flameHeight = null; draw(); return flame; } flame.width = function(w) { if (arguments.length) { if (w > minWidth) { size[0] = w - 10; } return flame; } else { return size[0]; } } flame.height = function(h) { if (arguments.length) { size[1] = h; return flame; } else { return size[1]; } } flame.cutoff = function(d) { if (arguments.length) { cutoff = d; return flame; } else { return cutoff; } } flame.maxDepth= function(d) { if (arguments.length) { maxDepth = d; return flame; } else { return maxDepth; } } flame.compare = function(t) { if (arguments.length) { compare = t; return flame; } else { return compare; } } flame.clickHandler = function(cb) { if (arguments.length){ clickHandlers.push(cb); return flame; } else{ return clickHandlers; } } flame.specifiedEntries = function(t) { if (arguments.length){ specifiedEntries = t return flame; } else{ return specifiedEntries; } } flame.search = function(kw) { return search(getLastInst(), kw); } flame.reset = function() { hardReset(getLastInst()); } flame.reverseCompare = function() { reverseCompare(getLastInst()); } flame.compareMethod = function(cb) { if (arguments.length){ compareMethod = cb return flame; } else{ return compareMethod; } } flame.backward = function() { var n = getLastInst().zoomHistory.backward(); if (n) { zoom(n, true); } } flame.forward = function() { var n = getLastInst().zoomHistory.forward(); if (n) { zoom(n, true); } } flame.historyPossible = function() { var zoomHistory = getLastInst().zoomHistory; return { backward: zoomHistory.canBackward(), forward: zoomHistory.canForward(), } } function getLastInst() { if (!insts) { return;} var index = 'flame' + (flameIndex - 1); return insts[index]; } function stripEntry(entry){ return entry.replace(/ /g, '-') .replace(/</g, '') .replace(/>/g, '') .replace(/:/g, '-') .replace(/./g, '-') } // simple data node class function Node(entry, value, percent, compareValue){ entry = entry || 'root'; return { entry: entry, // value to draw value: value, // compare value for comparing compareValue: compareValue, // a raw copy of value // to keep raw value when value changed while drawing raw: null, percent: percent, depth: 0, children: [], // index of nodes when parsing index: {}, // sum( children.value ) childrenSum: 0, addChild: function(n){ if (this.index[n.entry]){ var currentN = this.index[n.entry]; currentN.value += n.value; } else{ this.index[n.entry] = n; } // childrenSum contains no gap!!! this.childrenSum += n.value; return this.index[n.entry]; }, init: function(index, root, parent) { // init root node's value and percent if (this.entry === 'root') { this.value = this.childrenSum; root = this; parent = this; this.depth = 0; this.percent = 1; } else{ var total = root.value; this.depth = parent.depth + 1; this.percent = this.value / total; } this.internalValue = 0; this.internalPercent = 0; // gap node has no children and no need to dye. if (! this.gap) { this.shift(); this.dye(); for(var i=0; i<this.children.length; i++){ this.children[i].init(index, root, this); } // only non gap entry has internal this.internalValue = this.value - this.childrenSum; this.internalPercent = total ? this.internalValue / total : 0; } this.instIndex = index; this.id = getRandomString() + '-' + stripEntry(this.entry); this.raw = this.value; return this; }, shift: function() { // push children into children array and count percent for (var entry in this.index){ var child = this.index[entry]; this.children.push(child); child.relativePercent = child.value / this.value; } // fill gap only when children exist if (this.children.length && this.childrenSum < this.value){ var gapValue = this.value - this.childrenSum; this.children.push(Node( 'gap', gapValue, gapValue / this.value ).setGap()); } return this; }, setGap: function() { this.gap = true; return this; }, redyeAll: function() { this.dye(true); if (! this.gap) { for(var i=0; i<this.children.length; i++){ this.children[i].redyeAll(); } } return this; }, countCompare: function(){ var percent = this.internalPercent; var comparePercent = this.internalComparePercent; this.internalCompareDiff = (percent - comparePercent) / comparePercent; this.internalCompareValid = percent >= compareValidThreshold && comparePercent >= compareValidThreshold; var percent = this.percent; var comparePercent = this.comparePercent; this.compareDiff = (percent - comparePercent) / comparePercent; this.compareValid = percent >= compareValidThreshold && comparePercent >= compareValidThreshold; }, dye: function(redo) { if (this.color && !redo){ return this; } if (compare) { this.countCompare(); if (compareMethod == 'internal') { var compareDiff = this.internalCompareDiff; var compareValid = this.internalCompareValid; var comparePercent = this.internalComparePercent; } else{ var compareDiff = this.compareDiff; var compareValid = this.compareValid; var comparePercent = this.comparePercent; } if (comparePercent === null || comparePercent === undefined ) { compareDiff = null; } else if(!compareValid) { compareDiff = 0; } this.color = compareToColor(compareDiff); } else { var theme = 'hot'; if (isVmStack(this.entry)){ theme = 'cold'; } this.color = generateColor( hashEntry(this.entry), theme ); } return this; }, trim: function() { delete this.childrenSum; delete this.index; delete this.setGap; delete this.addChild; delete this.init; delete this.shift; delete this.dye; delete this.trim; } } } function isVmStack(entry) { if (entry.toLowerCase().indexOf('.py') != -1){ return true; } if (entry.indexOf('.c->') != -1){ return true; } if (entry.indexOf('.lua') != -1){ return true; } return false; } ////// ACTION FUNCTIONS // reverse two profiles for comparing function reverseCompare(inst) { if (!compare) { return; } compareReverse = !compareReverse; inst.svg.remove(); flame(selection); // cannot replay while after drawing, // svg cannot refreshed immediately //inst.searchInput.node().value = inst.searchKw; //search(inst); } // search entries by kw from searchInput function search(inst, kw) { if (kw === undefined || kw === null) { var searchInputNode = inst.searchInput.node(); var kw = inst.searchKw = searchInputNode.value.toLowerCase(); } var count = 0; var match = 0; var internalMatch = 0; travel(inst.tree, function(n){ if (searchMatch(n.data.entry, kw)){ n.data.onSearch = true; addClass( inst.svg.select("#" + n.data.id), 'on-search' ); count += 1; internalMatch += n.data.internalPercent; return true; } else if (n.data.onSearch) { n.data.onSearch = false; removeClass( inst.svg.select("#" + n.data.id), 'on-search' ); } return false; }, function(n){ match += n.data.percent; return false; }); return { count: count, match: match, internalMatch: internalMatch }; } function searchMatch(entry, kw){ if (entry == 'gap') { return false; } if (kw) { if (entry.toLowerCase().indexOf(kw) != -1) { return true; } var r = new RegExp(kw, "gi"); if (r.test(entry)) { return true; } } return false; } // clean search flag and highlight function cleanSearch(inst) { inst.svg.searchInputNode.value = ''; travel(inst.tree, function(n){ if (n.data.onSearch) { n.data.onSearch = false; removeClass( inst.svg.select("#" + n.data.id), 'on-search' ); } }); } // replay search highlight on entries with search flag function replaySearch(inst) { travel(inst.tree, function(n){ if (n.data.onSearch) { addClass( inst.svg.select("#" + n.data.id), 'on-search' ); } }); } // reset search and zoom function reset(index) { var inst = insts[index]; cleanSearch(inst); zoom(inst.tree); } // reset all contains reverse, rebuild svg function hardReset(inst){ svgScaleK = 1; compareReverse = false; inst.svg.remove(); flame(selection); } // make focus event static. function pin(n) { var inst = getInst(n); inst.pin = !inst.pin; } function nodeClick(n) { var e = d3.event; if (e.altKey) { specifyEntry(n); } else { zoom(n); } } function specifyEntry(n) { var entry = n.data.entry; var inst = getInst(n) specifiedEntries = [entry]; filterSpecifiedEntries(inst.tree); draw(); } // zoom to expand a entry function zoom(n, history) { var inst = getInst(n) inst.pin = false; hideBrothers(n); virtualParents(n); show(n); draw(); replaySearch(inst); if (!history) { inst.zoomHistory.set(n); } } // make parent virtual while expand a entry function virtualParents(n) { n = n.parent; while (n && n.data) { n.data.virtual = true; n = n.parent; } } // hide entries besides specifiedEntries function filterSpecifiedEntries(root) { if (! specifiedEntries || ! specifiedEntries.length) { return; } function _findSpecifiedEntries(n) { if (specifiedEntries.indexOf(n.data.entry) !== -1) { n.data.specified = true; return true; } if (n.children) { var found = false; for (var i=0; i<n.children.length; i++) { if (_findSpecifiedEntries(n.children[i])){ found = true; } else { n.children[i].data.value = 0; n.children[i].data.notSpecified = true; } } return found; } else{ return false; } } _findSpecifiedEntries(root); } // hide brothers recursively function hideBrothers(n) { var parent = n.parent; if (!parent) { return; } if (parent.children) { parent.children.forEach(function(child) { if (! equal(n, child)) { hide(child); } }); } if (parent.parent){ hideBrothers(parent); } } // set entry value to 0 to prevent it from display // recursively function hide(node) { // d3's sum get value from node.data node.data.value = 0; node.children && node.children.forEach(hide); } // reset entry value show it. // recursively function show(node) { if (node.data.value == 0 && node.data.notSpecified) { return; } node.data.value = node.data.raw; node.data.virtual = false; node.children && node.children.forEach(show); } // highlight entry and its ancestors when mouse on function focus(n) { var inst = getInst(n); if (inst.pin) { return; } if (focusChain) { var chain = getChain(n); chainElementApply(chain, function(e){ if (! hasClass(e, 'virtual')) { addClass(e, 'focus'); e.style('opacity', 1); } }); } else { var e = inst.svg.select("#" + n.data.id); if (! hasClass(e, 'virtual')) { addClass(e, 'focus'); e.style('opacity', 1); } } showTooltip(inst, n); } // unhighlight function unfocus(n) { var inst = getInst(n); if (inst.pin) { return; } if (focusChain) { var chain = getChain(n); chainElementApply(chain, function(ele){ removeClass(ele, 'focus'); }); } else { var e = inst.svg.select("#" + n.data.id); removeClass(e, 'focus'); } hideTooltip(inst); } function formatTooltip(n) { var dataDesc = parseDataDesc(n); var info = n.data.entryInfo || {}; var contentArray = [ "Entry : " + (info.shortEntry || info.entry || n.entry), dataDesc.internalContent, dataDesc.cumulativeContent, ] if (info.entry != info.function) { contentArray.push("Function : " + info.function); } if (info.entry != info.shortEntry) { contentArray.push("FullEntry : " + info.entry); } if (info.module) { contentArray.push("Module : " + info.module); } if (info.filepath) { contentArray.push("SourceFile : " + info.filepath); } if (info.lineNumber) { contentArray.push("SourceLine : " + info.lineNumber); } return contentArray; } function fitTooltipWidth(contents, charCntInLine=80) { var contentArray = []; for (var j=0; j<contents.length; j++) { var content = contents[j]; var n = parseInt( content.length / charCntInLine ) + 1; var i = 0; while (content) { // reserved 2 blank for tab var len = i==0 ? charCntInLine : (charCntInLine - 2); var line = content.substr(0, charCntInLine); if (line.trim()) { if (i > 0) { line = " " + line; } contentArray.push(line.replace(/ /g, "\u00A0")); } content = content.substr(charCntInLine); i += 1; } } return contentArray; } function countMaxTextWidth(inst, contentArray) { var textWidth = 0; var tmpG = inst.svg.append("g"); for (var i=0; i<contentArray.length; i++) { var tmpText = tmpG.append('text').text(contentArray[i]); var textWidth = Math.max( tmpText.node().getComputedTextLength(), textWidth ); tmpText.remove(); } tmpG.remove(); return textWidth; } function countTooltipBox(inst, e, n, textWidth, lines) { // use event.x, while event.y can be not correct when srcolled var offset = 4; var x = e.x + offset; x -= inst.svgRect.left; if (x < 0) { x = 0; } var nodeElement = inst.svg.select("#" + n.data.id); var et = nodeElement.attr('transform') .replace('translate(', '').replace(')', '').split(', '); var y = parseInt(et[1]) + levelHeight + offset; if (x == undefined || y == undefined) { return; } // count remain xrange after draw tip // if xrange < 0, means tip overflowed, count new x to fit var xrange = size[0] - (x + textWidth); if (xrange < 0){ x = Math.max( //size[0] - textWidth - offset - (size[0] - x) - 10, x - textWidth - offset - 10, 0 ); } var height = tooltipMargin * 2 + tooltipLineHeight * lines; // as well as xrange var yrange = size[1] - (y + height); if (yrange < 0) { y = Math.max( //size[1] - height - offset - (size[1] - y) - levelHeight, y - height - offset - levelHeight, 0 ); } return { x: x, y: y, height: height } } // draw a tooltip to describe entry function showTooltip(inst, n) { var contentArray = fitTooltipWidth( formatTooltip(n) ); if (!contentArray || contentArray.length == 0) { return; } var textWidth = countMaxTextWidth(inst, contentArray); var box = countTooltipBox(inst, d3.event, n, textWidth, contentArray.length); if (!box) { return; } var tip = inst.svg.append("g") .attr("class", "profile-flame-tooltip") tip.append('rect') .attr('width', textWidth + 8) .attr('height', box.height) .attr('x', box.x) .attr('y', box.y + 2) .attr('rx', 5) .attr('ry', 5) .attr('fill', '#333333') .attr('opacity', '.85'); for (var i=0; i<contentArray.length; i++) { var content = contentArray[i]; var yi = box.y + i * tooltipLineHeight; tip.append('text') .attr('x', box.x) .attr('y', yi) .attr('dx', '0.35em') .attr('dy', '1.5em') .attr('fill', '#FFFFFF') .text(content); } } function hideTooltip(inst) { inst.svg.selectAll("g.profile-flame-tooltip").remove(); } ////// CALLBACK FUNCTIONS // // sort callback, sort entry by is calls asc, gap is always behind. function _sort(a, b) { if (a.data.gap) { return 1; } else if (b.data.gap) { return -1; } return d3.ascending(a.value, b.value); } // sum callback // return 0 when n has children // to use only leaf value to count sum. function _sum(n) { return !n.children || n.children.length == 0 ? n.value : 0; } ////// TOOL FUNCTIONS var pathSeps = [ "\\", "/" ]; var getShortDemangled = function(d) { // py function and lua function if (d.indexOf('.py') != -1 || d.indexOf('.lua') != -1){ for(var i=0; i<pathSeps.length; i++) { var xIndex = d.lastIndexOf(pathSeps[i]) if (xIndex != -1) { d = d.substr(xIndex + 1); } } } // c/c++ function else { if (d.indexOf('<') == -1 && d.indexOf('(') == -1) { return d; } var dl = d.split(''); var indexes = getMatchedBracketIndexs(d, '<', '>'); for (var i=0; i<indexes.length; i++) { var indexPair = indexes[i]; for (var j=indexPair[0]; j<=indexPair[1]; j++) { dl[j] = ''; } } var indexes = getMatchedBracketIndexs(d, '(', ')'); for (var i=0; i<indexes.length; i++) { var indexPair = indexes[i]; for (var j=indexPair[0]+1; j<indexPair[1]; j++) { dl[j] = ''; } } d = dl.join(''); // remove () and const behind. d = d.split('(', 1)[0]; } return d; }; var formatComplexEntry = function(d) { var ret = { shortEntry: null, entryPrefix: null, function: null, entry: null, module: null, filepath: null, lineNumber: null, } if (d.indexOf('.py') != -1 || d.indexOf('.lua') != -1){ // vm stack ret.entry = ret.shortEntry = getShortDemangled(d); var items = d.split(":") // last one is line number, so it shoud be path:entry:line ret.lineNumber = parseInt(items[items.length-1]) || null; // path may have ':', so join remain items ret.filepath = items.slice(0, ret.lineNumber ? -2 : -1).join(":"); if (ret.lineNumber) { ret.shortEntry = ret.shortEntry.replace(":" + ret.lineNumber, ""); } } else { // common mtrace format ret.entry = d; ret.shortEntry = getShortDemangled(ret.entry); } var funcSep = ":" var index = ret.shortEntry.lastIndexOf(funcSep) if (index != -1) { ret.function = ret.shortEntry.substr(index+1); ret.entryPrefix = ret.shortEntry.substr(0, index+1); } else { ret.function = ret.shortEntry; } return ret; } var gnrZoomHistory = function() { return { current: null, forwardArray: [], backwardArray: [], size: 20, set: function(n) { if (this.current) { this.push(this.backwardArray, this.current); } this.current = n; }, push: function(a, n) { if (a[a.length-1] == n) { return; } a.push(n); while (a.length > this.size) { a.shift(); } }, forward: function() { var n = this.forwardArray.pop(); if (n) { if (this.current) { this.push(this.backwardArray, this.current); } this.current = n; } return n; }, backward: function() { var n = this.backwardArray.pop(); if (n) { if (this.current) { this.push(this.forwardArray, this.current); } this.current = n; } return n; }, canForward: function() { return this.forwardArray.length != 0; }, canBackward: function() { return this.backwardArray.length != 0; }, reset: function(r) { this.backwardArray.length = 0; this.forwardArray.length = 0; this.current = r; } } }; // node equal function equal(m, n) { return m.data.id == n.data.id; } // used to make entry id unique function getRandomString(len){ len = len || 8; var ignore = [91, 96]; var min = 65; var max = 122; var range = max - min; var string = ''; while (string.length < len){ var pos = parseInt(Math.random() * range) + min; if (ignore[0] <= pos && pos <= ignore[1]){ continue; } string += String.fromCharCode(pos); } return string; } // travel each node of tree and call cb // nestCb are only call when cb returns true // and travel to children when itself returns true function travel(tree, cb, nestCb){ var nextNestCb = nestCb; if (tree) { if (cb(tree) && nestCb){ if (!nestCb(tree)){ nextNestCb = null; } } if (tree.children) { for (var i=0; i<t