Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix flamegraph display in the presence of --diff_base. #790

Merged
merged 1 commit into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,15 @@ Boxes are colored according to the name of the package in which the correspondin
function occurs. E.g., in C++ profiles all frames corresponding to `std::` functions
will be assigned the same color.

When using the **--diff_base** option, box width is proportional to the sum of
the increases and decreases in the sub-tree rooted at box. E.g., if the cost of
one child of box decreases by 150 and the cost of another child increases by
200, the box width will be proportional to 150+200. The net increase or decrease
(the preceding example has a net increase of 200-150, i.e., 50) is indicated by
a shaded region. The size of the shaded region is proportional to the net
increase or net decrease. The shading is red for a net increase, and green for a
net decrease.

Inlining is indicated by the absence of a horizontal border between a caller and
a callee. E.g., suppose X calls Y calls Z and the call from Y to Z is inlined into
Y. There will be a black border between X and Y, but no border between Y and Z.
Expand Down
3 changes: 3 additions & 0 deletions internal/driver/html/stacks.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ body {
position: absolute;
overflow: hidden;
box-sizing: border-box;
background: #d8d8d8;
}
.positive { position: absolute; background: #caa; }
.negative { position: absolute; background: #aca; }
/* Not-inlined frames are visually separated from their caller. */
.not-inlined {
border-top: 1px solid black;
Expand Down
185 changes: 135 additions & 50 deletions internal/driver/html/stacks.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,20 @@ function stackViewer(stacks, nodes) {
['hrs', 60*60]]]]);

// Fields
let shownTotal = 0; // Total value of all stacks
let pivots = []; // Indices of currently selected data.Sources entries.
let matches = new Set(); // Indices of sources that match search
let elems = new Map(); // Mapping from source index to display elements
let displayList = []; // List of boxes to display.
let actionMenuOn = false; // Is action menu visible?
let actionTarget = null; // Box on which action menu is operating.
let diff = false; // Are we displaying a diff?

for (const stack of stacks.Stacks) {
if (stack.Value < 0) {
diff = true;
break;
}
}

// Setup to allow measuring text width.
const textSizer = document.createElement('canvas');
Expand Down Expand Up @@ -177,9 +184,8 @@ function stackViewer(stacks, nodes) {
function handleEnter(box, div) {
if (actionMenuOn) return;
const src = stacks.Sources[box.src];
const d = details(box);
div.title = d + ' ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
detailBox.innerText = d;
div.title = details(box) + ' β”‚ ' + src.FullName + (src.Inlined ? "\n(inlined)" : "");
detailBox.innerText = summary(box.sumpos, box.sumneg);
// Highlight all boxes that have the same source as box.
toggleClass(box.src, 'hilite2', true);
}
Expand Down Expand Up @@ -228,16 +234,16 @@ function stackViewer(stacks, nodes) {
const width = chart.clientWidth;
elems.clear();
actionTarget = null;
const total = totalValue(places);
const [pos, neg] = totalValue(places);
const total = pos + neg;
const xscale = (width-2*PADDING) / total; // Converts from profile value to X pixels
const x = PADDING;
const y = 0;
shownTotal = total;

displayList.length = 0;
renderStacks(0, xscale, x, y, places, +1); // Callees
renderStacks(0, xscale, x, y-ROW, places, -1); // Callers (ROW left for separator)
display(displayList);
display(xscale, pos, neg, displayList);
}

// renderStacks creates boxes with top-left at x,y with children drawn as
Expand All @@ -256,29 +262,59 @@ function stackViewer(stacks, nodes) {
const groups = partitionPlaces(places);
for (const g of groups) {
renderGroup(depth, xscale, x, y, g, direction);
x += xscale*g.sum;
x += groupWidth(xscale, g);
}
}

// Some of the types used below:
//
// // Group represents a displayed (sub)tree.
// interface Group {
// name: string; // Full name of source
// src: number; // Index in stacks.Sources
// self: number; // Contribution as leaf (may be < 0 for diffs)
// sumpos: number; // Sum of |self| of positive nodes in tree (>= 0)
// sumneg: number; // Sum of |self| of negative nodes in tree (>= 0)
// places: Place[]; // Stack slots that contributed to this group
// }
//
// // Box is a rendered item.
// interface Box {
// x: number; // X coordinate of top-left
// y: number; // Y coordinate of top-left
// width: number; // Width of box to display
// src: number; // Index in stacks.Sources
// sumpos: number; // From corresponding Group
// sumneg: number; // From corresponding Group
// self: number; // From corresponding Group
// };

function groupWidth(xscale, g) {
return xscale * (g.sumpos + g.sumneg);
}

function renderGroup(depth, xscale, x, y, g, direction) {
// Skip if not wide enough.
const width = xscale * g.sum;
const width = groupWidth(xscale, g);
if (width < MIN_WIDTH) return;

// Draw the box for g.src (except for selected element in upwards direction
// since that duplicates the box we added in downwards direction).
if (depth != 0 || direction > 0) {
const box = {
x: x,
y: y,
src: g.src,
sum: g.sum,
selfValue: g.self,
width: xscale*g.sum,
selfWidth: (direction > 0) ? xscale*g.self : 0,
x: x,
y: y,
width: width,
src: g.src,
sumpos: g.sumpos,
sumneg: g.sumneg,
self: g.self,
};
displayList.push(box);
x += box.selfWidth;
if (direction > 0) {
// Leave gap on left hand side to indicate self contribution.
x += xscale*Math.abs(g.self);
}
}
y += direction * ROW;

Expand Down Expand Up @@ -322,24 +358,30 @@ function stackViewer(stacks, nodes) {
let group = groupMap.get(src);
if (!group) {
const name = stacks.Sources[src].FullName;
group = {name: name, src: src, sum: 0, self: 0, places: []};
group = {name: name, src: src, sumpos: 0, sumneg: 0, self: 0, places: []};
groupMap.set(src, group);
groups.push(group);
}
group.sum += stack.Value;
if (stack.Value < 0) {
group.sumneg += -stack.Value;
} else {
group.sumpos += stack.Value;
}
group.self += (place.Pos == stack.Sources.length-1) ? stack.Value : 0;
group.places.push(place);
}

// Order by decreasing cost (makes it easier to spot heavy functions).
// Though alphabetical ordering is a potential alternative that will make
// profile comparisons easier.
groups.sort(function(a, b) { return b.sum - a.sum; });
groups.sort(function(a, b) {
return (b.sumpos + b.sumneg) - (a.sumpos + a.sumneg);
});

return groups;
}

function display(list) {
function display(xscale, posTotal, negTotal, list) {
// Sort boxes so that text selection follows a predictable order.
list.sort(function(a, b) {
if (a.y != b.y) return a.y - b.y;
Expand All @@ -353,33 +395,48 @@ function stackViewer(stacks, nodes) {
const divs = [];
for (const box of list) {
box.y -= adjust;
divs.push(drawBox(box));
divs.push(drawBox(xscale, box));
}
divs.push(drawSep(-adjust));
divs.push(drawSep(-adjust, posTotal, negTotal));

const h = (list.length > 0 ? list[list.length-1].y : 0) + 4*ROW;
chart.style.height = h+'px';
chart.replaceChildren(...divs);
}

function drawBox(box) {
function drawBox(xscale, box) {
const srcIndex = box.src;
const src = stacks.Sources[srcIndex];

function makeRect(cl, x, y, w, h) {
const r = document.createElement('div');
r.style.left = x+'px';
r.style.top = y+'px';
r.style.width = w+'px';
r.style.height = h+'px';
r.classList.add(cl);
return r;
}

// Background
const w = box.width - 1; // Leave 1px gap
const r = document.createElement('div');
r.style.left = box.x + 'px';
r.style.top = box.y + 'px';
r.style.width = w + 'px';
r.style.height = ROW + 'px';
r.classList.add('boxbg');
r.style.background = makeColor(src.Color);
const r = makeRect('boxbg', box.x, box.y, w, ROW);
if (!diff) r.style.background = makeColor(src.Color);
addElem(srcIndex, r);
if (!src.Inlined) {
r.classList.add('not-inlined');
}

// Positive/negative indicator for diff mode.
if (diff) {
const delta = box.sumpos - box.sumneg;
const partWidth = xscale * Math.abs(delta);
if (partWidth >= MIN_WIDTH) {
r.appendChild(makeRect((delta < 0 ? 'negative' : 'positive'),
0, 0, partWidth, ROW-1));
}
}

// Label
if (box.width >= MIN_TEXT_WIDTH) {
const t = document.createElement('div');
Expand All @@ -395,11 +452,9 @@ function stackViewer(stacks, nodes) {
return r;
}

function drawSep(y) {
function drawSep(y, posTotal, negTotal) {
const m = document.createElement('div');
m.innerText = percent(shownTotal, stacks.Total) +
'\xa0\xa0\xa0\xa0' + // Some non-breaking spaces
valueString(shownTotal);
m.innerText = summary(posTotal, negTotal);
m.style.top = (y-ROW) + 'px';
m.style.left = PADDING + 'px';
m.style.width = (chart.clientWidth - PADDING*2) + 'px';
Expand Down Expand Up @@ -449,36 +504,66 @@ function stackViewer(stacks, nodes) {
t.innerText = text;
}

// totalValue returns the combined sum of the stacks listed in places.
// totalValue returns the positive and negative sums of the Values of stacks
// listed in places.
function totalValue(places) {
const seen = new Set();
let result = 0;
let pos = 0;
let neg = 0;
for (const place of places) {
if (seen.has(place.Stack)) continue; // Do not double-count stacks
seen.add(place.Stack);
const stack = stacks.Stacks[place.Stack];
result += stack.Value;
if (stack.Value < 0) {
neg += -stack.Value;
} else {
pos += stack.Value;
}
}
return result;
return [pos, neg];
}

function summary(pos, neg) {
// Examples:
// 6s (10%)
// 12s (20%) πŸ † 18s (30%)
return diff ? diffText(neg, pos) : percentText(pos);
}

function details(box) {
// E.g., 10% 7s
// or 10% 7s (3s self
let result = percent(box.sum, stacks.Total) + ' ' + valueString(box.sum);
if (box.selfValue > 0) {
result += ` (${valueString(box.selfValue)} self)`;
// Examples:
// 6s (10%)
// 6s (10%) β”‚ self 3s (5%)
// 6s (10%) β”‚ 12s (20%) πŸ † 18s (30%)
let result = percentText(box.sumpos - box.sumneg);
if (box.self != 0) {
result += " β”‚ self " + unitText(box.self);
}
if (diff && box.sumpos > 0 && box.sumneg > 0) {
result += " β”‚ " + diffText(box.sumneg, box.sumpos);
}
return result;
}

function percent(v, total) {
return Number(((100.0 * v) / total).toFixed(1)) + '%';
// diffText returns text that displays from and to alongside their percentages.
// E.g., 9s (45%) πŸ † 10s (50%)
function diffText(from, to) {
return percentText(from) + " πŸ † " + percentText(to);
}

// percentText returns text that displays v in appropriate units alongside its
// percentange.
function percentText(v) {
function percent(v, total) {
return Number(((100.0 * v) / total).toFixed(1)) + '%';
}
return unitText(v) + " (" + percent(v, stacks.Total) + ")";
}

// valueString returns a formatted string to display for value.
function valueString(value) {
let v = value * stacks.Scale;
// unitText returns a formatted string to display for value.
function unitText(value) {
const sign = (value < 0) ? "-" : "";
let v = Math.abs(value) * stacks.Scale;
// Rescale to appropriate display unit.
let unit = stacks.Unit;
const list = UNITS.get(unit);
Expand All @@ -492,7 +577,7 @@ function stackViewer(stacks, nodes) {
}
}
}
return Number(v.toFixed(2)) + unit;
return sign + Number(v.toFixed(2)) + unit;
}

function find(name) {
Expand Down
Loading