diff --git a/index.html b/index.html index 7780672..d7c05be 100644 --- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ - @@ -235,11 +236,21 @@

Python Profiler Viewer

Compare
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -248,7 +259,7 @@

Python Profiler Viewer

const workerCode = ` self.addEventListener("message", async function(e) { const file = e.data.file; - const CHUNK_SIZE = 1024 * 1024; + const CHUNK_SIZE = 2024 * 2024 ; let offset = 0; const decoder = new TextDecoder(); let leftover = ''; @@ -385,10 +396,31 @@

Python Profiler Viewer

if (tab.dataset.tab === "timelineTab") { setTimeout(() => Plotly.Plots.resize(document.getElementById('timeline')), 50); } + + if (tab.dataset.tab === "flamegraphTab") { + flamegraphActive = true; + renderFlamegraph(flamegraphRoot); + } else { + flamegraphActive = false; + } }); }); + + + + + + + resizeObserver = new ResizeObserver(() => { + if (flamegraphActive) { + renderFlamegraph(flamegraphRoot); + } + }); + resizeObserver.observe(document.getElementById("flamegraphView")); + + - + \ No newline at end of file diff --git a/profiler.js b/profiler.js index d2f155c..8b98a48 100644 --- a/profiler.js +++ b/profiler.js @@ -7,6 +7,81 @@ const renderEvery = 5; const MAX_NODES = 5000; const MAX_TIMELINE = 5000; + + + +function CallEntity(call_id, start,end, parent_call_id,children,type,expanded,functionName) { + this.call_id = call_id; + + this.start = start; + this.end=end; + + this.parent_call_id= parent_call_id; + this.children = children + + this.type=type; + this.expanded=expanded; + + this.function=functionName; + +} + + +// Build call map and tree structure +function buildCallMap(calls) { + const map = {}; + for (const id in calls) { + var cCall = calls[id]; + map[id] = new CallEntity(cCall["call_id"] ,cCall["start"],cCall["end"] ,cCall["parent_call_id"],[],cCall["type"], cCall?.expanded ?? false , cCall["function"]) + } + for (const id in map) { + const node = map[id]; + if (node.parent_call_id != null && map[node.parent_call_id]) { + map[node.parent_call_id].children.push(node); + } + } + return map; +} + + + +function finalizeProcessing() { + idMap = buildCallMap(calls); + treeRoot = buildTree(idMap); + renderTree(treeRoot); + renderTimeline(Object.values(calls)); + renderSummary(computeAggregates(Object.values(calls))); + + if (flamegraphActive) { + requestFlamegraphRender(); + } +} + +function renderSummary(aggregates) { + let html = ""; + html += ""; + const sorted = Object.entries(aggregates).sort((a, b) => b[1].total - a[1].total); + for (const [func, stat] of sorted) { + html += ``; + } + html += "
FunctionCallsTotal Time (ms)
${func}${stat.count}${stat.total}
"; + document.getElementById("summaryTable").innerHTML = html; +} + + + + +function parseTime(timeStr) { + const [h, m, s] = timeStr.split(':'); + const [sec, ms = "0"] = s.split('.'); + const date = new Date(); + date.setHours(parseInt(h), parseInt(m), parseInt(sec), parseInt(ms)); + return date; +} + + + + // Streaming batch processing function processBatch(batch) { for (const entry of batch) { @@ -25,6 +100,10 @@ function processBatch(batch) { calls[id].start = parseTime(entry.time); } else if (entry.event === "end") { calls[id].end = parseTime(entry.time); + // Update flamegraph only when both start & end exist + if (calls[id].start) { + addToFlamegraph(calls[id]); + } } } processedBatches++; @@ -33,45 +112,22 @@ function processBatch(batch) { function maybeRender() { if (processedBatches % renderEvery === 0) { renderPartial(); + + if (flamegraphActive) { + requestFlamegraphRender(); + } } } + function renderPartial() { const partialAgg = computeAggregates(Object.values(calls)); renderSummary(partialAgg); } -function finalizeProcessing() { - idMap = buildCallMap(calls); - treeRoot = buildTree(idMap); - renderTree(treeRoot); - renderTimeline(Object.values(calls)); - renderSummary(computeAggregates(Object.values(calls))); - renderFlamegraph(buildFlamegraphTree(calls)); - if (treeRoot) showFunctionDetails(treeRoot); -} -function parseTime(timeStr) { - const [h, m, s] = timeStr.split(':'); - const [sec, ms = "0"] = s.split('.'); - const date = new Date(); - date.setHours(parseInt(h), parseInt(m), parseInt(sec), parseInt(ms)); - return date; -} -function buildCallMap(calls) { - const map = {}; - for (const id in calls) { - map[id] = { ...calls[id], children: [], expanded: false }; - } - for (const id in map) { - const node = map[id]; - if (node.parent_call_id != null && map[node.parent_call_id]) { - map[node.parent_call_id].children.push(node); - } - } - return map; -} + function flattenVisibleTree(node, result = [], depth = 0) { result.push({ node, depth }); if (node.expanded) { @@ -187,16 +243,6 @@ function computeAggregates(callArray) { return agg; } -function renderSummary(aggregates) { - let html = ""; - html += ""; - const sorted = Object.entries(aggregates).sort((a, b) => b[1].total - a[1].total); - for (const [func, stat] of sorted) { - html += ``; - } - html += "
FunctionCallsTotal Time (ms)
${func}${stat.count}${stat.total}
"; - document.getElementById("summaryTable").innerHTML = html; -} function buildFlamegraphTree(calls) { const root = { name: "__ROOT__", value: 0, children: [] }; @@ -224,29 +270,161 @@ function buildFlamegraphTree(calls) { return root; } + + + +let flamegraphActive = false; +let flamegraphSVG = null; +let flamegraphG = null; +let flamegraphPendingRender = false; +let flamegraphRoot = { name: "__ROOT__", value: 0, children: [] }; +const color = d3.scaleOrdinal(d3.schemeTableau10); +let resizeObserver; + + +function getNodePath(d) { + return d.ancestors().map(n => n.data.name).reverse().join("/"); +} + function renderFlamegraph(data) { - document.getElementById("flamegraphView").innerHTML = ""; - const width = document.getElementById("flamegraphView").clientWidth; - const height = 400; + const container = document.getElementById("flamegraphView"); + const width = container.clientWidth; + const height = container.clientHeight; + if (width === 0 || height === 0) return; const partition = d3.partition().size([width, height]); const root = d3.hierarchy(data).sum(d => d.value); partition(root); - const color = d3.scaleOrdinal(d3.schemeTableau10); + // Initialize SVG & zoom once + if (!flamegraphSVG) { + flamegraphSVG = d3.select("#flamegraphView").append("svg") + .attr("width", width) + .attr("height", height); + + flamegraphG = flamegraphSVG.append("g"); - const svg = d3.select("#flamegraphView").append("svg") - .attr("width", width) - .attr("height", height); + const zoom = d3.zoom().scaleExtent([0.5, 10]) + .on("zoom", (event) => flamegraphG.attr("transform", event.transform)); + flamegraphSVG.call(zoom); - svg.selectAll("rect") - .data(root.descendants()) - .enter().append("rect") - .attr("x", d => d.x0) - .attr("y", d => d.y0) - .attr("width", d => d.x1 - d.x0) - .attr("height", d => d.y1 - d.y0) + // Add reset zoom button + const button = document.createElement("button"); + button.innerText = "Reset Zoom"; + button.style.position = "absolute"; + button.style.top = "10px"; + button.style.right = "20px"; + button.style.zIndex = 1000; + button.style.padding = "8px 15px"; + button.style.background = "orange"; + button.style.border = "none"; + button.style.color = "#fff"; + button.style.fontWeight = "bold"; + button.style.cursor = "pointer"; + document.body.appendChild(button); + button.onclick = () => { + flamegraphSVG.transition().duration(500).call(zoom.transform, d3.zoomIdentity); + }; + } else { + flamegraphSVG.attr("width", width).attr("height", height); + } + + const rects = flamegraphG.selectAll("g.node") + .data(root.descendants(), getNodePath); + + rects.exit().remove(); + + const enter = rects.enter().append("g").attr("class", "node"); + + enter.append("rect") + .attr("x", d => d.x0).attr("y", d => d.y0) + .attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0) .attr("fill", d => color(d.data.name)) - .append("title") - .text(d => `\${d.data.name}: \${Math.round(d.value)} ms`); + .on("click", (event, d) => zoomIntoNode(d, partition, root)) + .on("mouseover", function (event, d) { + d3.select(this).attr("stroke", "#fff").attr("stroke-width", 2); + showTooltip(event, d); + }) + .on("mouseout", function () { + d3.select(this).attr("stroke", null); + hideTooltip(); + }); + + enter.append("title") + .text(d => `${d.data.name}: ${Math.round(d.value)} ms`); + + const merged = enter.merge(rects); + merged.select("rect").transition().duration(200) + .attr("x", d => d.x0).attr("y", d => d.y0) + .attr("width", d => d.x1 - d.x0).attr("height", d => d.y1 - d.y0) + .attr("fill", d => color(d.data.name)); + + merged.select("title") + .text(d => `${d.data.name}: ${Math.round(d.value)} ms`); +} + +function zoomIntoNode(target, partition, root) { + const container = document.getElementById("flamegraphView"); + const width = container.clientWidth; + const height = container.clientHeight; + + const x = d3.scaleLinear().domain([target.x0, target.x1]).range([0, width]); + const y = d3.scaleLinear().domain([target.y0, root.y1]).range([0, height]); + + flamegraphG.selectAll("g.node").select("rect").transition().duration(750) + .attr("x", d => x(d.x0)) + .attr("y", d => y(d.y0)) + .attr("width", d => x(d.x1) - x(d.x0)) + .attr("height", d => y(d.y1) - y(d.y0)); } + +const tooltip = d3.select("body") + .append("div") + .style("position", "absolute") + .style("padding", "8px") + .style("background", "#333") + .style("color", "#fff") + .style("border-radius", "5px") + .style("visibility", "hidden"); + +function showTooltip(event, d) { + tooltip.style("visibility", "visible") + .html(`${d.data.name}
${Math.round(d.value)} ms`) + .style("top", (event.pageY - 10) + "px") + .style("left", (event.pageX + 10) + "px"); +} + +function hideTooltip() { + tooltip.style("visibility", "hidden"); +} + +function addToFlamegraph(call) { + let current = call; + const path = []; + while (current) { + path.unshift(current.function); + current = current.parent_call_id ? calls[current.parent_call_id] : null; + } + + let node = flamegraphRoot; + for (const func of path) { + let child = node.children.find(c => c.name === func); + if (!child) { + child = { name: func, value: 0, children: [] }; + node.children.push(child); + } + node = child; + } + node.value += call.end - call.start; +} + +let flamegraphRenderTimeout = null; + +function requestFlamegraphRender() { + if (flamegraphRenderTimeout) { + clearTimeout(flamegraphRenderTimeout); + } + flamegraphRenderTimeout = setTimeout(() => { + renderFlamegraph(flamegraphRoot); + }, 50); // slight delay to batch fast incoming updates +} \ No newline at end of file