summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-02-27 18:54:17 +0200
committerPaul Buetow <paul@buetow.org>2026-02-27 18:54:17 +0200
commit0887c715150fcf391191a924491737bd58b8af9c (patch)
tree26c82a02a8afc1a887e04e008333f927c2b7ca8c
parent5f23af510bd9031c515f2a3cc495bd996c795e69 (diff)
flamegraph: scale live view to viewport height
-rw-r--r--internal/flamegraph/livehtml.go64
-rw-r--r--internal/flamegraph/livehtml_browser_test.go18
2 files changed, 80 insertions, 2 deletions
diff --git a/internal/flamegraph/livehtml.go b/internal/flamegraph/livehtml.go
index 8ca74cd..2db5137 100644
--- a/internal/flamegraph/livehtml.go
+++ b/internal/flamegraph/livehtml.go
@@ -73,6 +73,7 @@ const liveHTML = `<!doctype html>
#flamegraph {
display: block;
width: 100%;
+ height: calc(100vh - 56px);
min-height: calc(100vh - 56px);
background: transparent;
}
@@ -120,6 +121,7 @@ const liveHTML = `<!doctype html>
var fg = {
paused: false,
resetting: false,
+ lastTreeData: null,
pendingData: null,
searchQuery: '',
zoomStack: [],
@@ -137,6 +139,7 @@ const liveHTML = `<!doctype html>
resetZoomBtn: document.getElementById('btn-reset-zoom'),
resetBaselineBtn: document.getElementById('btn-reset-baseline'),
cfg: {
+ baseFrameHeight: 16,
width: 1200,
frameHeight: 16,
fontSize: 12,
@@ -171,6 +174,46 @@ const liveHTML = `<!doctype html>
return maxDepth;
}
+ function fgDefaultCanvasHeight(maxDepth) {
+ return (fg.cfg.baseFrameHeight * (maxDepth + 1)) + 80;
+ }
+
+ function fgViewportLayout(maxDepth) {
+ var rows = Math.max(maxDepth + 1, 1);
+ var defaultCanvasHeight = fgDefaultCanvasHeight(maxDepth);
+ var viewportHeight = Number(window.innerHeight || 0);
+ if (viewportHeight <= 0) {
+ return {
+ frameHeight: fg.cfg.baseFrameHeight,
+ canvasHeight: defaultCanvasHeight
+ };
+ }
+
+ var controls = document.getElementById('controls');
+ var controlsHeight = 56;
+ if (controls && typeof controls.getBoundingClientRect === 'function') {
+ controlsHeight = Number(controls.getBoundingClientRect().height || controlsHeight);
+ }
+
+ var availableHeight = viewportHeight - controlsHeight;
+ if (availableHeight <= 0) {
+ return {
+ frameHeight: fg.cfg.baseFrameHeight,
+ canvasHeight: defaultCanvasHeight
+ };
+ }
+
+ var canvasHeight = Math.max(defaultCanvasHeight, availableHeight);
+ var frameHeight = (canvasHeight - 80) / rows;
+ if (frameHeight < fg.cfg.baseFrameHeight) {
+ frameHeight = fg.cfg.baseFrameHeight;
+ }
+ return {
+ frameHeight: frameHeight,
+ canvasHeight: canvasHeight
+ };
+ }
+
function fgBuildFrames(node, rootTotal, x, depth, canvasHeight, isRoot, out, path) {
if (!node || rootTotal <= 0) {
return;
@@ -522,6 +565,9 @@ const liveHTML = `<!doctype html>
function fgRender(treeData) {
if (!treeData || Number(treeData.t || 0) <= 0) {
+ var emptyLayout = fgViewportLayout(0);
+ fg.cfg.frameHeight = emptyLayout.frameHeight;
+ fg.svg.style.height = String(emptyLayout.canvasHeight) + 'px';
fg.frames = [];
fg.svg.innerHTML = '';
fgSetStatus('');
@@ -529,7 +575,9 @@ const liveHTML = `<!doctype html>
}
var rootTotal = Number(treeData.t || 0);
var maxDepth = fgMaxDepth(treeData, 0);
- var canvasHeight = (fg.cfg.frameHeight * (maxDepth + 1)) + 80;
+ var layout = fgViewportLayout(maxDepth);
+ fg.cfg.frameHeight = layout.frameHeight;
+ var canvasHeight = layout.canvasHeight;
var frames = [];
fgBuildFrames(treeData, rootTotal, 0, 0, canvasHeight, true, frames, '');
@@ -552,6 +600,7 @@ const liveHTML = `<!doctype html>
fg.svg.setAttribute('viewBox', '0 0 ' + fg.cfg.width + ' ' + canvasHeight);
fg.svg.setAttribute('preserveAspectRatio', 'xMinYMin meet');
+ fg.svg.style.height = String(canvasHeight) + 'px';
fg.svg.innerHTML = parts.join('');
fg.frames = Array.prototype.slice.call(fg.svg.querySelectorAll('g.frame'));
fg.rootWidth = fgDetectRootWidth();
@@ -569,6 +618,7 @@ const liveHTML = `<!doctype html>
fgSetStatus('parse error');
return;
}
+ fg.lastTreeData = treeData;
fgRender(treeData);
fgApplyZoom();
fgApplySearch();
@@ -590,6 +640,17 @@ const liveHTML = `<!doctype html>
};
}
+ function fgHandleResize() {
+ if (!fg.lastTreeData) {
+ return;
+ }
+ requestAnimationFrame(function () {
+ fgRender(fg.lastTreeData);
+ fgApplyZoom();
+ fgApplySearch();
+ });
+ }
+
function fgIsTextEntryTarget(target) {
if (!target) {
return false;
@@ -634,6 +695,7 @@ const liveHTML = `<!doctype html>
fg.resetZoomBtn.addEventListener('click', fgResetZoom);
fg.resetBaselineBtn.addEventListener('click', fgResetBaseline);
document.addEventListener('keydown', fgHandleKeydown);
+ window.addEventListener('resize', fgHandleResize);
fgSetStatus('');
fgConnect();
diff --git a/internal/flamegraph/livehtml_browser_test.go b/internal/flamegraph/livehtml_browser_test.go
index 7644084..c362ddf 100644
--- a/internal/flamegraph/livehtml_browser_test.go
+++ b/internal/flamegraph/livehtml_browser_test.go
@@ -23,6 +23,8 @@ type liveJSResult struct {
KnownFrames []jsFrame `json:"knownFrames"`
SVGHTML string `json:"svgHTML"`
ViewBox string `json:"viewBox"`
+ TallViewBox string `json:"tallViewBox"`
+ TallHeight string `json:"tallHeight"`
SingleCount int `json:"singleCount"`
DeepMaxDepth int `json:"deepMaxDepth"`
WideFrameCount int `json:"wideFrameCount"`
@@ -75,6 +77,12 @@ func TestLiveHTMLJSRenderingParity(t *testing.T) {
if got.ViewBox != "0 0 1200 128" {
t.Fatalf("viewBox = %q, want %q", got.ViewBox, "0 0 1200 128")
}
+ if got.TallViewBox != "0 0 1200 844" {
+ t.Fatalf("tall viewBox = %q, want %q", got.TallViewBox, "0 0 1200 844")
+ }
+ if got.TallHeight != "844px" {
+ t.Fatalf("tall style height = %q, want %q", got.TallHeight, "844px")
+ }
if got.SingleCount != 1 {
t.Fatalf("single-frame case count = %d, want 1", got.SingleCount)
@@ -127,6 +135,7 @@ function makeElement(id) {
attrs: {},
classList: { toggle: function(){}, add: function(){}, remove: function(){} },
addEventListener: function(){},
+ getBoundingClientRect: function() { return { height: id === "controls" ? 56 : 0 }; },
setAttribute: function(k, v) { this.attrs[k] = String(v); },
getAttribute: function(k) { return this.attrs[k] || ""; },
querySelectorAll: function() { return []; },
@@ -135,7 +144,7 @@ function makeElement(id) {
}
const elements = {};
-["flamegraph", "status", "btn-pause", "btn-search", "btn-reset-search", "btn-undo-zoom", "btn-reset-zoom", "btn-reset-baseline"].forEach((id) => {
+["controls", "flamegraph", "status", "btn-pause", "btn-search", "btn-reset-search", "btn-undo-zoom", "btn-reset-zoom", "btn-reset-baseline"].forEach((id) => {
elements[id] = makeElement(id);
});
elements["body"] = makeElement("body");
@@ -191,6 +200,11 @@ fgRender(knownTree);
const svgHTML = elements["flamegraph"].innerHTML;
const viewBox = elements["flamegraph"].attrs["viewBox"] || "";
+window.innerHeight = 900;
+fgRender(knownTree);
+const tallViewBox = elements["flamegraph"].attrs["viewBox"] || "";
+const tallHeight = elements["flamegraph"].style.height || "";
+
const singleTree = { n: "", v: 0, t: 1, c: [{ n: "only", v: 1, t: 1 }] };
const singleFrames = [];
const singleCanvas = (liveFlamegraphState.cfg.frameHeight * (fgMaxDepth(singleTree, 0) + 1)) + 80;
@@ -219,6 +233,8 @@ console.log(JSON.stringify({
knownFrames,
svgHTML,
viewBox,
+ tallViewBox,
+ tallHeight,
singleCount: singleFrames.length,
deepMaxDepth,
wideFrameCount: wideFrames.length,