function renderFlameGraph(containerId, flameData) {
const container = document.getElementById(containerId);
if (!container) return;
if (!flameData || flameData.length === 0) {
container.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">No flame graph data available</div>';
return;
}
const maxLevel = Math.max(...flameData.map(d => d.value[0]));
const maxValue = Math.max(...flameData.map(d => d.value[2]));
const focusedInfo = window.currentFocusedFunction ? ` - Focused: ${window.currentFocusedFunction}` : '';
const resetDisabled = !window.currentFocusedFunction;
container.textContent = '';
const mainDiv = document.createElement('div');
mainDiv.className = 'h-full flex flex-col';
const header = document.createElement('div');
header.className = 'bg-gray-100 p-2 border-b flex justify-between items-center';
const title = document.createElement('div');
title.className = 'text-sm font-semibold text-gray-700';
title.textContent = `Flame Graph - ${flameData.length} functions${focusedInfo}`;
const controlsContainer = document.createElement('div');
controlsContainer.className = 'flex gap-2';
const resetButton = document.createElement('button');
resetButton.className = 'px-3 py-1 bg-gray-500 text-white text-xs rounded hover:bg-gray-600 disabled:opacity-50 disabled:cursor-not-allowed';
resetButton.textContent = 'Reset View';
resetButton.disabled = resetDisabled;
resetButton.setAttribute('onclick', 'resetFlameGraphView()');
const saveButton = document.createElement('button');
saveButton.className = 'px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600';
saveButton.textContent = 'Save as Image';
saveButton.setAttribute('onclick', 'saveFlameGraph()');
controlsContainer.appendChild(resetButton);
controlsContainer.appendChild(saveButton);
header.appendChild(title);
header.appendChild(controlsContainer);
const chartWrapper = document.createElement('div');
chartWrapper.className = 'flex-1 bg-white relative';
chartWrapper.id = 'flamegraph-chart-wrapper';
mainDiv.appendChild(header);
mainDiv.appendChild(chartWrapper);
container.appendChild(mainDiv);
const wrapperHeight = chartWrapper.offsetHeight || chartWrapper.clientHeight || 600;
console.log('Initial wrapper height:', chartWrapper);
chartWrapper.innerHTML = '';
chartWrapper.style.height = wrapperHeight + 'px';
chartWrapper.style.overflowY = 'scroll'; chartWrapper.style.overflowX = 'hidden'; chartWrapper.className = "bg-white relative";
const minBarHeight = 24;
const minRequiredHeight = (maxLevel + 1) * minBarHeight + 20; const chartHeight = minRequiredHeight;
console.log('Required height:', minRequiredHeight, 'Container height:', wrapperHeight);
const wrapperDiv = document.createElement('div');
wrapperDiv.style.width = '100%';
wrapperDiv.style.height = Math.max(wrapperHeight, chartHeight) + 'px';
wrapperDiv.style.position = 'relative';
console.log('Wrapper height:', Math.max(wrapperHeight, chartHeight), 'Container height:', wrapperHeight, 'Chart height:', chartHeight);
const chartDiv = document.createElement('div');
chartDiv.style.width = '100%';
chartDiv.style.height = chartHeight + 'px';
chartDiv.style.position = 'absolute';
chartDiv.style.bottom = '0';
chartDiv.style.left = '0';
chartDiv.style.right = '0';
wrapperDiv.appendChild(chartDiv);
chartWrapper.appendChild(wrapperDiv);
const chart = echarts.init(chartDiv);
const option = {
tooltip: {
trigger: 'item'
},
grid: {
top: 5,
bottom: 5,
left: 5,
right: 5,
containLabel: false
},
xAxis: {
show: false,
min: 0,
max: maxValue
},
yAxis: {
show: false,
min: -0.5,
max: maxLevel + 0.5,
inverse: false
},
series: [{
type: 'custom',
renderItem: renderFlameItem,
encode: {
x: [1, 2],
y: 0
},
data: flameData,
tooltip: {
formatter: (params) => {
const samples = params.value[2] - params.value[1];
const percentage = params.value[4];
const memoryFormatted = samples > 1024*1024 ?
`${(samples / (1024*1024)).toFixed(2)} MB` :
samples > 1024 ?
`${(samples / 1024).toFixed(2)} KB` :
`${samples} bytes`;
const rawFunctionName = params.data?.name || params.name || params.value[3] || 'Unknown';
const fullFunctionName = escapeHtml(rawFunctionName);
return `<b>${fullFunctionName}</b><br/>
Memory: ${memoryFormatted}<br/>
Percentage: ${percentage.toFixed(2)}%`;
}
}
}]
};
chart.setOption(option);
console.log('Chart wrapper scroll props:', {
scrollHeight: chartWrapper.scrollHeight,
clientHeight: chartWrapper.clientHeight,
offsetHeight: chartWrapper.offsetHeight,
scrollTop: chartWrapper.scrollTop
});
chartWrapper.scrollTop = chartWrapper.scrollHeight - chartWrapper.clientHeight;
console.log('After scrollTop attempt:', chartWrapper.scrollTop);
chart.on('click', (params) => {
if (params.data && params.data.name) {
console.log('Focusing on:', params.data.name);
focusFlameGraphFunction(params.data.name, flameData, chart);
}
});
window.currentFlameChart = chart;
if (!window.originalFlameData) {
window.originalFlameData = flameData;
}
window.currentFlameData = flameData;
const resizeHandler = () => chart.resize();
window.addEventListener('resize', resizeHandler);
container._chartCleanup = () => {
window.removeEventListener('resize', resizeHandler);
chart.dispose();
if (window.currentFlameChart === chart) {
window.currentFlameChart = null;
window.currentFlameData = null;
}
};
}
function renderFlameItem(params, api) {
const level = api.value(0);
const start = api.coord([api.value(1), level]);
const end = api.coord([api.value(2), level]);
const defaultHeight = ((api.size && api.size([0, 1])) || [0, 20])[1];
const width = end[0] - start[0];
const minHeight = 24;
const height = Math.max(minHeight, defaultHeight);
const minWidth = 10;
if (width < 0.5) return null;
const displayWidth = Math.max(minWidth, width);
return {
type: 'rect',
transition: ['shape'],
shape: {
x: start[0],
y: start[1] - height / 2,
width: displayWidth,
height: height - 2, r: 1
},
style: {
fill: api.visual('color'),
stroke: '#fff',
lineWidth: 0.5
},
textConfig: {
position: 'insideLeft'
},
textContent: {
style: {
text: displayWidth > 50 ? api.value(3) : '', fontFamily: 'Arial',
fontSize: Math.min(12, Math.max(8, displayWidth / 10)),
fill: '#000',
width: displayWidth - 4,
overflow: 'truncate'
}
}
};
}
function focusFlameGraphFunction(targetName, originalData, chart) {
console.log('Focusing on function:', targetName);
const filteredData = filterFlameData(originalData, targetName);
if (filteredData.length > 0) {
renderFlameGraph('flamegraph-fullscreen', filteredData);
window.currentFilteredData = filteredData;
window.currentFocusedFunction = targetName;
}
}
function filterFlameData(originalData, targetName) {
const filteredData = [];
let found = false;
let targetLevel = -1;
let targetStart = 0;
let targetWidth = 0;
for (const item of originalData) {
if (item.name === targetName && !found) {
found = true;
targetLevel = item.value[0];
targetStart = item.value[1];
targetWidth = item.value[2] - item.value[1];
const adjustedItem = { ...item };
adjustedItem.value = [0, 0, targetWidth, item.name, item.value[4]];
filteredData.push(adjustedItem);
} else if (found && item.value[0] > targetLevel) {
const itemStart = item.value[1];
const itemEnd = item.value[2];
if (itemStart >= targetStart && itemEnd <= (targetStart + targetWidth)) {
const adjustedItem = { ...item };
adjustedItem.value = [
item.value[0] - targetLevel, item.value[1] - targetStart, item.value[2] - targetStart, item.name,
item.value[4]
];
filteredData.push(adjustedItem);
}
} else if (found && item.value[0] <= targetLevel) {
continue;
}
}
return filteredData.length > 0 ? filteredData : originalData;
}
function resetFlameGraphView() {
if (window.currentFlameChart && window.originalFlameData) {
console.log('Resetting flame graph view...');
renderFlameGraph('flamegraph-fullscreen', window.originalFlameData);
window.currentFilteredData = null;
window.currentFocusedFunction = null;
console.log('Flame graph view reset to original');
}
}
function saveFlameGraph() {
if (window.currentFlameChart) {
try {
const imageDataURL = window.currentFlameChart.getDataURL({
type: 'png',
pixelRatio: 2,
backgroundColor: '#ffffff'
});
const link = document.createElement('a');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const focusedName = window.currentFocusedFunction || 'full';
const filename = `flamegraph-${focusedName}-${timestamp}.png`;
link.download = filename;
link.href = imageDataURL;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
console.log('Flame graph saved as:', filename);
} catch (error) {
console.error('Error saving flame graph:', error);
alert('Error saving flame graph. Please try again.');
}
}
}
function computeDifferentialProfile(actualData, baseData) {
if (!baseData || baseData.length === 0) {
return actualData; }
console.log('Computing differential profile...');
console.log('Actual data points:', actualData.length);
console.log('Base data points:', baseData.length);
const baseMemoryMap = new Map();
baseData.forEach(item => {
const functionName = item.name || item.value[3] || 'unknown';
const memory = item.value[2] - item.value[1];
if (baseMemoryMap.has(functionName)) {
baseMemoryMap.set(functionName, baseMemoryMap.get(functionName) + memory);
} else {
baseMemoryMap.set(functionName, memory);
}
});
console.log('Base functions mapped:', baseMemoryMap.size);
const differentialData = [];
actualData.forEach(item => {
const functionName = item.name || item.value[3] || 'unknown';
const actualMemory = item.value[2] - item.value[1];
const baseMemory = baseMemoryMap.get(functionName) || 0;
const diffMemory = Math.max(0, actualMemory - baseMemory);
if (diffMemory > 0) {
const newItem = {
...item,
value: [
item.value[0], item.value[1], item.value[1] + diffMemory, item.value[3], 0 ]
};
differentialData.push(newItem);
if (baseMemory > 0) {
baseMemoryMap.set(functionName, Math.max(0, baseMemory - actualMemory));
}
}
});
console.log('Differential data points:', differentialData.length);
if (differentialData.length === 0) {
console.log('No positive differential found - returning empty array');
return [];
}
return rebalanceFlameGraph(differentialData);
}
function rebalanceFlameGraph(data) {
if (data.length === 0) return data;
data.sort((a, b) => {
if (a.value[0] !== b.value[0]) return a.value[0] - b.value[0]; return a.value[1] - b.value[1]; });
const levels = new Map();
data.forEach(item => {
const level = item.value[0];
if (!levels.has(level)) {
levels.set(level, []);
}
levels.get(level).push(item);
});
let currentPosition = 0;
levels.forEach((levelItems, level) => {
if (level === 0) {
currentPosition = 0;
levelItems.forEach(item => {
const memory = item.value[2] - item.value[1];
item.value[1] = currentPosition;
item.value[2] = currentPosition + memory;
currentPosition += memory;
});
} else {
let levelPosition = 0;
levelItems.forEach(item => {
const memory = item.value[2] - item.value[1];
item.value[1] = levelPosition;
item.value[2] = levelPosition + memory;
levelPosition += memory;
});
}
});
return data;
}