diff options
| author | Paul Buetow <paul@buetow.org> | 2026-02-27 18:54:17 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-02-27 18:54:17 +0200 |
| commit | 0887c715150fcf391191a924491737bd58b8af9c (patch) | |
| tree | 26c82a02a8afc1a887e04e008333f927c2b7ca8c | |
| parent | 5f23af510bd9031c515f2a3cc495bd996c795e69 (diff) | |
flamegraph: scale live view to viewport height
| -rw-r--r-- | internal/flamegraph/livehtml.go | 64 | ||||
| -rw-r--r-- | internal/flamegraph/livehtml_browser_test.go | 18 |
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, |
