summaryrefslogtreecommitdiff
path: root/internal/flamegraph/livehtml.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/flamegraph/livehtml.go')
-rw-r--r--internal/flamegraph/livehtml.go162
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, '&amp;')
+ .replace(/</g, '&lt;')
+ .replace(/>/g, '&gt;')
+ .replace(/"/g, '&quot;')
+ .replace(/'/g, '&apos;');
+ }
+
+ 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>