From de76a06c78334b6db52bda5f1b97b2c765ddb821 Mon Sep 17 00:00:00 2001 From: Sanjay Ghemawat Date: Thu, 27 Jul 2023 08:59:29 -0700 Subject: [PATCH] Fix flamegraph display in the presence of --diff_base. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the flamegraph display was broken in the presence of `--diff_base` because it did not account for the fact that some stacks could have negative values. This change fixes the handling of negative values by tracking two numbers per displayed box that covers a set of stacks: 1. `sumpos`: the sum of the absolute values of increases in the stacks. 2. `sumneg`: the sum of the absolute values of decreases in the stacks. The width of the box is proportional to `sumpos + sumneg`. In addition, if a box has an overall decrease (`sumneg > sumpos`), the fraction of the box corresponding to the decrease is shaded green. (Similarly part of the box is shaded red if the box has an overall increase.) Changed the display of legends and tooltips to clearly show the added and removed samples. E.g., the following shows that the cost of `Function` changed from `1.44s` to `1.3s` as well as the net change and the corresponding percentages. ``` 0.14s (-1.8%) │ 1.44s (18.3%) 🠆 1.3s (16.5%) │ Function ``` Documented some of the Javascript types used for flamegraph rendering. --- doc/README.md | 9 ++ internal/driver/html/stacks.css | 3 + internal/driver/html/stacks.js | 185 +++++++++++++++++++++++--------- 3 files changed, 147 insertions(+), 50 deletions(-) diff --git a/doc/README.md b/doc/README.md index da481fc7..27fdeddf 100644 --- a/doc/README.md +++ b/doc/README.md @@ -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. diff --git a/internal/driver/html/stacks.css b/internal/driver/html/stacks.css index c03dab19..f5aeb985 100644 --- a/internal/driver/html/stacks.css +++ b/internal/driver/html/stacks.css @@ -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; diff --git a/internal/driver/html/stacks.js b/internal/driver/html/stacks.js index b0ae1905..be78edd5 100644 --- a/internal/driver/html/stacks.js +++ b/internal/driver/html/stacks.js @@ -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'); @@ -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); } @@ -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 @@ -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; @@ -322,11 +358,15 @@ 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); } @@ -334,12 +374,14 @@ function stackViewer(stacks, nodes) { // 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; @@ -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'); @@ -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'; @@ -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); @@ -492,7 +577,7 @@ function stackViewer(stacks, nodes) { } } } - return Number(v.toFixed(2)) + unit; + return sign + Number(v.toFixed(2)) + unit; } function find(name) {