<!DOCTYPE html> <html> <head> /INCLUDES/
<meta charset=utf-8 />
<title>Flame Graph of Page</title> <style>
.info {min-height: 50px; margin: 10px; } .legend div { display: block; float: left; width: 150px; margin: 0 8px 8px; padding: 4px; height: 50px; } .code { font-family: Consolas, Menlo, Monaco, "Lucida Console", "Liberation Mono", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Courier New", monospace; } #frameinfo table .sample-info { width: 200px; color: #333; font-size: 12px; } #frameinfo table { border-collapse: collapse; } #frameinfo table tr { border-bottom: 1px solid #eee; margin-top: 5px; } #frameinfo td { padding: 5px; } #frameinfo-wrapper { position: fixed; z-index: 1; opacity: 0.7; background-color: black; width: 100%; height: 100%; top: 0; left: 0; display: none; } #frameinfo { position: fixed; padding: 10px; z-index: 2; opacity: 1.0; top: 0; margin-top: 50px; margin-left: 40px; width: 80%; height: 80%; background-color: white; border: 2px solid #666; display: none; overflow: auto; }
</style> </head> <body>
<div class="graph"></div> <div class="info code"></div> <div class="legend"></div> <div id="frameinfo-wrapper"></div> <div id="frameinfo"> <h3>Frame Info</h3> <table> </table> </div> <script>
var data = /DATA/; var maxX = 0; var maxY = 0;
debounce = function(func, wait, trickle) {
var timeout; timeout = null; return function() { var args, context, currentWait, later; context = this; args = arguments; later = function() { timeout = null; return func.apply(context, args); }; if (timeout && trickle) { // already queued, let it through return; } if (typeof wait === "function") { currentWait = wait(); } else { currentWait = wait; } if (timeout) { clearTimeout(timeout); } timeout = setTimeout(later, currentWait); return timeout; };
};
var guessGem = function(frame) {
var split = frame.split('/gems/'); if(split.length == 1) { split = frame.split('/app/'); if(split.length == 1) { split = frame.split('/lib/'); } split = split[Math.max(split.length-2,0)].split('/'); return split[split.length-1].split(':')[0]; } else { return split[split.length -1].split('/')[0]; }
}
var guessMethod = function(frame) {
var split = frame.split('`'); if(split.length == 2) { var fullMethod = split[1].split("'")[0]; split = fullMethod.split("#"); if(split.length == 2) { return split[1]; } return split[0]; } return '?';
}
var guessFile = function(frame) {
var split = frame.split(".rb:"); if(split.length == 2) { split = split[0].split('/'); return split[split.length - 1]; } return "";
}
$.each(data, function(){
maxX = Math.max(maxX, this.x + this.width); maxY = Math.max(maxY, this.y); this.shortName = this.frame;
});
var width = $(window).width(); var height = $(window).height() / 1.2;
$('.graph').width(width).height(height);
var xScale = d3.scale.linear()
.domain([0, maxX]) .range([0, width]);
var yScale = d3.scale.linear()
.domain([0, maxY]) .range([0,height]);
var realHeight = 0; var debouncedHeightCheck = debounce(function(){
if (realHeight > 15) { svg.selectAll('text').attr('display','show'); } else { svg.selectAll('text').attr('display','none'); }
}, 200);
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")"); realHeight = yScale(1) * d3.event.scale; debouncedHeightCheck();
}
var svg = d3.select(“.graph”)
.append("svg") .attr("width", "100%") .attr("height", "100%") .attr("pointer-events", "all") .append('svg:g') .call(d3.behavior.zoom().on("zoom", zoom)) .append('svg:g');
// so zoom works everywhere svg.append(“rect”)
.attr("x",function(d) { return xScale(0); }) .attr("y",function(d) { return yScale(0);}) .attr("width", function(d){return xScale(maxX);}) .attr("height", yScale(maxY)) .attr("fill", "white");
var color = function() {
var r = parseInt(205 + Math.random() * 50); var g = parseInt(Math.random() * 230); var b = parseInt(Math.random() * 55); return "rgb(" + r + "," + g + "," + b + ")";
} var info = {};
// stackoverflow.com/questions/1960473/unique-values-in-an-array Array.prototype.getUnique = function() {
var o = {}, a = [] for (var i = 0; i < this.length; i++) o[this[i]] = 1 for (var e in o) a.push(e) return a
}
var samplePercent = function(samples, exclusive){
var info = " (" + samples + " sample" + (samples == 1 ? "" : "s") + " - " + ((samples / maxX) * 100).toFixed(2) + "%) "; if (exclusive) { info += " (" + exclusive + " exclusive - " + ((exclusive / maxX) * 100).toFixed(2) + "%) "; } return info;
}
var mouseover = function(d) {
var i = info[d.frame]; $('.info').text( d.frame + " " + samplePercent(i.samples.length, d.topFrame ? d.topFrame.exclusiveCount : 0)); d3.selectAll(i.nodes) .attr('opacity',0.5);
};
var mouseout = function(d) {
var i = info[d.frame]; $('.info').text(""); d3.selectAll(i.nodes) .attr('opacity',1);
};
var backtrace = function(frame){
for(var i=0; i<data.length; i++){ if(frame === data[i]){ break; } } frames = [frame]; var depth = frame.y; while(i > 0){ if(depth == -1) break; if(data[i].y === depth-1) { frames.push(data[i]); depth--; } i--; } return frames;
}
$('#frameinfo-wrapper').click(function(d){
$(this).hide(); $('#frameinfo').hide();
});
var click = function(d){
var trace = backtrace(d); var link = function(path, dest){ return path.replace(/[^\/]+:\d+/, function(x){ return "<a target='_blank' href='"+ dest +"'>" + x + "</a>"}) }; var linkify = function(path){ var split = path.split("/")[0].split("-"); if(["activerecord","actionpack","railties","activesupport", "rails"].indexOf(split[0]) > -1) { var github = "https://github.com/rails/rails/blob/"; var file = path.split(":")[0].split("/"); if(split[0] === "rails") { file.shift(); } else { file[0] = split[0]; } github += (split[1].length < 6 ? "v" : "") + split[1] + "/"; github += file.join("/"); github += "#L" + parseInt(path.split(":")[1]); return link(path, github); } return path; } var simplify = function(frame){ var split = frame.split('/gems/'); if(split.length > 1){ var path = linkify(split.pop()); return "<span class='full-location'>" + split.join('/gems/') + "/</span>" + path; } else { return frame; } } var table = trace.map(function(f){ var i = info[f.frame]; return "<tr><td class='code'>" + simplify(f.frame) + "</td><td class='sample-info'>" + samplePercent(i.samples.length, f.topFrame ? f.topFrame.exclusiveCount : 0) + "</td></tr>"; }).join("\n"); var table = $('#frameinfo table').html(table); table.find(".full-location").hide().after("<span class='expand'>… </span>"); table.find(".expand").css({cursor: "pointer"}).click(function(){ $(this).hide().parent().find(".full-location").show(); }); $('#frameinfo-wrapper').show(); $('#frameinfo').show();
};
// stackoverflow.com/a/7419630 var rainbow = function(numOfSteps, step) {
// This function generates vibrant, "evenly spaced" colours (i.e. no clustering). This is ideal for creating easily distiguishable vibrant markers in Google Maps and other apps. // Adam Cole, 2011-Sept-14 // HSV to RBG adapted from: http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript var r, g, b; var h = step / numOfSteps; var i = ~~(h * 6); var f = h * 6 - i; var q = 1 - f; switch(i % 6){ case 0: r = 1, g = f, b = 0; break; case 1: r = q, g = 1, b = 0; break; case 2: r = 0, g = 1, b = f; break; case 3: r = 0, g = q, b = 1; break; case 4: r = f, g = 0, b = 1; break; case 5: r = 1, g = 0, b = q; break; } var c = "#" + ("00" + (~ ~(r * 255)).toString(16)).slice(-2) + ("00" + (~ ~(g * 255)).toString(16)).slice(-2) + ("00" + (~ ~(b * 255)).toString(16)).slice(-2); return (c);
}
// assign some colors, analyze samples per gem var gemStats = {} var topFrames = {} var lastFrame = {frame: 'd52e04d-df28-41ed-a215-b6ec840a8ea5', x: -1}
$.each(data, function(){
var gem = guessGem(this.frame); var stat = gemStats[gem]; if(!stat) { gemStats[gem] = stat = {name: gem, samples: [], frames: []}; } stat.frames.push(this.frame); for(var j=0; j < this.width; j++){ stat.samples.push(this.x + j); } // This assumes the traversal is in order if (lastFrame.x != this.x) { var topFrame = topFrames[lastFrame.frame] if (!topFrame) { topFrames[lastFrame.frame] = topFrame = {exclusiveCount: 0} } topFrame.exclusiveCount += 1; lastFrame.topFrame = topFrame; } lastFrame = this;
});
var topFrame = topFrames if (!topFrame) {
topFrames[lastFrame.frame] = topFrame = {exclusiveCount: 0}
} topFrame.exclusiveCount += 1; lastFrame.topFrame = topFrame;
var totalGems = 0; $.each(gemStats, function(k,stat){
totalGems++; stat.samples = stat.samples.getUnique();
});
var gemsSorted = _(gemStats).pairs()
.sortBy(function(item){ return -item[1].samples.length; }) .map(function(item){return item[1]}) .value();
var currentIndex = 0; _.each(gemsSorted, function(stat){
stat.color = rainbow(totalGems, currentIndex); for(var x=0; x < stat.frames.length; x++) { info[stat.frames[x]] = {nodes: [], samples: [], color: stat.color}; } currentIndex += 1;
});
// see: bl.ocks.org/mundhradevang/1387786 function fontSize(d,i) {
var size = yScale(1) / 3; // var words = d.shortName.split(' '); var word = d.shortName; // words[0]; var width = xScale(d.width+100); var height = yScale(1); var length = 0; d3.select(this).style("font-size", size + "px").text(word); while((size > 12.1) && ((this.getBBox().width >= width) || (this.getBBox().height >= height))) { size -= 0.1; d3.select(this).style("font-size", size + "px"); } d3.select(this).attr("dy", size);
}
svg.selectAll(“g”)
.data(data) .enter() .append("g") .each(function(){ d3.select(this) .append("rect") .attr("x",function(d) { return xScale(d.x-1); }) .attr("y",function(d) { return yScale(maxY - d.y);}) .attr("width", function(d){return xScale(d.width);}) .attr("height", yScale(1)) .attr("fill", function(d){ var i = info[d.frame]; if(!i) { info[d.frame] = i = {nodes: [], samples: [], color: color()}; } i.nodes.push(this); for(var j=0; j < d.width; j++){ i.samples.push(d.x + j); } return i.color; }) .on("click", click) .on("mouseover", mouseover) .on("mouseout", mouseout) .attr("cursor", "pointer"); d3.select(this) .append("text") .attr("x",function(d) { return xScale(d.x - 0.98); }) .attr("y",function(d) { return yScale(maxY - d.y);}) .on("click", click) .on("mouseover", mouseover) .on("mouseout", mouseout) .each(fontSize) .attr("cursor", "pointer") .attr("display", "none"); });
// Samples may overlap on the same line for (var r in info) {
if (info[r].samples) { info[r].samples = info[r].samples.getUnique(); }
};
// render the legend _.each(gemsSorted, function(gem){
var node = $("<div></div>") .css("background-color", gem.color) .text(gem.name + " " + samplePercent(gem.samples.length)) ; $('.legend').append(node);
});
</script>
</body> </html>