diff options
Diffstat (limited to 'internal/flamegraph')
| -rw-r--r-- | internal/flamegraph/livehtml.go | 162 |
1 files changed, 160 insertions, 2 deletions
diff --git a/internal/flamegraph/livehtml.go b/internal/flamegraph/livehtml.go index 15ad7d6..bf26c3a 100644 --- a/internal/flamegraph/livehtml.go +++ b/internal/flamegraph/livehtml.go @@ -118,6 +118,12 @@ const liveHTML = `<!doctype html> (function () { var fg = { paused: false, + pendingData: null, + zoomRange: null, + searchQuery: '', + eventSource: null, + svg: document.getElementById('flamegraph'), + status: document.getElementById('status'), cfg: { width: 1200, frameHeight: 16, @@ -189,18 +195,170 @@ const liveHTML = `<!doctype html> } } + function fgEscape(value) { + return String(value || '') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + + function fgApplySearch() { + if (!fg.searchQuery) { + return; + } + var query = fg.searchQuery.toLowerCase(); + var frames = fg.svg.querySelectorAll('.frame'); + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + if (String(frame.getAttribute('data-name') || '').toLowerCase().indexOf(query) < 0) { + continue; + } + var rect = frame.querySelector('rect'); + if (rect) { + rect.setAttribute('fill', 'rgb(220,30,70)'); + } + } + } + + function fgApplyZoom() { + if (!fg.zoomRange) { + return; + } + var frames = fg.svg.querySelectorAll('.frame'); + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + if (frame.getAttribute('data-name') !== fg.zoomRange.name) { + continue; + } + frame.classList.add('zoom-target'); + break; + } + } + + function fgBindFrameEvents() { + var frames = fg.svg.querySelectorAll('.frame'); + for (var i = 0; i < frames.length; i++) { + frames[i].addEventListener('mouseenter', function () { + var name = this.getAttribute('data-name') || ''; + fg.status.textContent = fg.paused ? 'PAUSED | ' + name : 'LIVE | ' + name; + }); + frames[i].addEventListener('mouseleave', function () { + fg.status.textContent = fg.paused ? 'PAUSED' : 'LIVE'; + }); + frames[i].addEventListener('click', function () { + fg.zoomRange = { name: this.getAttribute('data-name') || '' }; + fgApplyZoom(); + }); + } + } + + function fgRender(treeData) { + if (!treeData || Number(treeData.t || 0) <= 0) { + fg.svg.innerHTML = ''; + return; + } + var rootTotal = Number(treeData.t || 0); + var maxDepth = fgMaxDepth(treeData, 0); + var canvasHeight = (fg.cfg.frameHeight * (maxDepth + 1)) + 80; + var frames = []; + fgBuildFrames(treeData, rootTotal, 0, 0, canvasHeight, true, frames); + + var parts = []; + parts.push('<text class="title" x="10" y="22">I/O Flame Graph (Live)</text>'); + for (var i = 0; i < frames.length; i++) { + var frame = frames[i]; + var textStyle = frame.w <= (fg.cfg.fontSize * 2) ? ' style="display:none"' : ''; + var title = fgEscape(frame.name + ' (' + frame.total + ', ' + frame.pct.toFixed(2) + '%)'); + parts.push('<g class="frame" data-name="' + fgEscape(frame.name) + '" data-x="' + frame.x.toFixed(3) + + '" data-w="' + frame.w.toFixed(3) + '" data-depth="' + frame.depth + + '" data-base-fill="' + frame.fill + '">'); + parts.push('<title>' + title + '</title>'); + parts.push('<rect x="' + frame.x.toFixed(3) + '" y="' + frame.y.toFixed(3) + '" width="' + frame.w.toFixed(3) + + '" height="' + frame.h.toFixed(3) + '" fill="' + frame.fill + '"></rect>'); + parts.push('<text x="' + (frame.x + 3).toFixed(3) + '" y="' + (frame.y + fg.cfg.fontSize).toFixed(3) + '"' + + textStyle + '>' + fgEscape(frame.name) + '</text>'); + parts.push('</g>'); + } + + fg.svg.setAttribute('viewBox', '0 0 ' + fg.cfg.width + ' ' + canvasHeight); + fg.svg.setAttribute('preserveAspectRatio', 'xMinYMin meet'); + fg.svg.innerHTML = parts.join(''); + fgBindFrameEvents(); + } + + function fgProcessUpdate(jsonStr) { + var treeData; + try { + treeData = JSON.parse(jsonStr); + } catch (err) { + fg.status.textContent = 'LIVE | parse error'; + return; + } + fgRender(treeData); + fgApplyZoom(); + fgApplySearch(); + } + + function fgConnect() { + fg.eventSource = new EventSource('/events'); + fg.eventSource.onmessage = function (e) { + if (fg.paused) { + fg.pendingData = e.data; + return; + } + requestAnimationFrame(function () { + fgProcessUpdate(e.data); + }); + }; + fg.eventSource.onerror = function () { + fg.status.textContent = fg.paused ? 'PAUSED' : 'LIVE | stream error'; + }; + } + var pauseBtn = document.getElementById("btn-pause"); - var status = document.getElementById("status"); + var searchBtn = document.getElementById("btn-search"); + var resetSearchBtn = document.getElementById("btn-reset-search"); pauseBtn.addEventListener("click", function () { fg.paused = !fg.paused; document.body.classList.toggle("paused", fg.paused); pauseBtn.textContent = fg.paused ? "Resume" : "Pause"; - status.textContent = fg.paused ? "PAUSED" : "LIVE"; + fg.status.textContent = fg.paused ? "PAUSED" : "LIVE"; + if (!fg.paused && fg.pendingData) { + var pending = fg.pendingData; + fg.pendingData = null; + requestAnimationFrame(function () { + fgProcessUpdate(pending); + }); + } }); + searchBtn.addEventListener('click', function () { + var query = window.prompt('Search frame substring:', fg.searchQuery || ''); + if (query === null) { + return; + } + fg.searchQuery = query; + fgApplySearch(); + }); + resetSearchBtn.addEventListener('click', function () { + fg.searchQuery = ''; + var rects = fg.svg.querySelectorAll('.frame rect'); + for (var i = 0; i < rects.length; i++) { + var baseFill = rects[i].parentElement.getAttribute('data-base-fill'); + if (baseFill) { + rects[i].setAttribute('fill', baseFill); + } + } + }); + + fgConnect(); window.fgFrameColor = fgFrameColor; window.fgBuildFrames = fgBuildFrames; window.fgMaxDepth = fgMaxDepth; + window.fgRender = fgRender; + window.fgProcessUpdate = fgProcessUpdate; window.liveFlamegraphState = fg; })(); </script> |
