function base64Decode(str) {
try {
return atob(str);
} catch (e) {
console.error('Failed to decode base64:', e);
return 'Error: Could not decode content';
}
}
function escapeHtml(text) {
if (!text) return '';
return text.toString()
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function escapeJavaScript(text) {
if (!text) return '';
return text.toString()
.replace(/\\/g, '\\\\') .replace(/`/g, '\\`') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(/\n/g, '\\n') .replace(/\r/g, '\\r') .replace(/\t/g, '\\t') .replace(/\$/g, '\\$'); }
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.add('hidden');
});
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('border-blue-500', 'text-blue-600');
btn.classList.add('border-transparent', 'text-gray-500');
});
document.getElementById(tabName).classList.remove('hidden');
const activeButton = document.querySelector(`[data-tab="${tabName}"]`);
if (activeButton) {
activeButton.classList.remove('border-transparent', 'text-gray-500');
activeButton.classList.add('border-blue-500', 'text-blue-600');
}
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async function populateChartSelectors() {
const callgraphBaseSelect = document.getElementById('callgraph-base-select');
const callgraphActualSelect = document.getElementById('callgraph-actual-select');
const flamegraphBaseSelect = document.getElementById('flamegraph-base-select');
const flamegraphActualSelect = document.getElementById('flamegraph-actual-select');
[callgraphBaseSelect, flamegraphBaseSelect].forEach(select => {
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
});
[callgraphActualSelect, flamegraphActualSelect].forEach(select => {
while (select.children.length > 1) {
select.removeChild(select.lastChild);
}
});
const dumps = await DataAccess.getMemoryDumps();
if (dumps && dumps.length > 0) {
const sortedDumps = [...dumps].sort((a, b) => {
const timeA = a.created || parseInt(a.name.match(/(\d+)/)?.[1]) || 0;
const timeB = b.created || parseInt(b.name.match(/(\d+)/)?.[1]) || 0;
return timeB - timeA; });
sortedDumps.forEach((dump, index) => {
const dumpName = dump.name || `dump-${index}`;
const dumpSize = dump.size || 0;
const timestamp = dump.timestamp
? new Date(dump.timestamp * 1000).toLocaleString()
: 'Unknown time';
const displayText = `${timestamp} (${formatFileSize(dumpSize)})`;
const baseOption1 = document.createElement('option');
baseOption1.value = dumpName;
baseOption1.textContent = displayText;
callgraphBaseSelect.appendChild(baseOption1);
const baseOption2 = document.createElement('option');
baseOption2.value = dumpName;
baseOption2.textContent = displayText;
flamegraphBaseSelect.appendChild(baseOption2);
const actualOption1 = document.createElement('option');
actualOption1.value = dumpName;
actualOption1.textContent = displayText;
callgraphActualSelect.appendChild(actualOption1);
const actualOption2 = document.createElement('option');
actualOption2.value = dumpName;
actualOption2.textContent = displayText;
flamegraphActualSelect.appendChild(actualOption2);
});
}
}
async function updateCallGraph() {
const baseSelect = document.getElementById('callgraph-base-select');
const actualSelect = document.getElementById('callgraph-actual-select');
const container = document.getElementById('callgraph-fullscreen');
const actualDumpName = actualSelect.value;
if (!actualDumpName) {
container.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">Select a memory dump to generate call graph...</div>';
return;
}
container.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">Generating call graph...</div>';
let actualDump;
try {
actualDump = await DataAccess.getMemoryDump(actualDumpName);
} catch (error) {
container.innerHTML = '<div class="flex items-center justify-center h-full text-red-600">Failed to fetch memory dump</div>';
return;
}
try {
const parser = new BacktraceProcessor.HeapProfileParser();
const actualProfile = parser.parse(actualDump.data);
let profile = actualProfile;
const actualCallGraphData = BacktraceProcessor.StackProcessor.buildCallGraphData(actualProfile);
let callGraphData = actualCallGraphData;
const baseDumpName = baseSelect.value;
if (baseDumpName && baseDumpName !== 'none') {
let baseDump;
try {
baseDump = await DataAccess.getMemoryDump(baseDumpName);
} catch (error) {
console.error('Failed to fetch base dump:', error);
baseDump = null;
}
if (baseDump) {
const baseProfile = parser.parse(baseDump.data);
const baseCallGraphData = BacktraceProcessor.StackProcessor.buildCallGraphData(baseProfile);
callGraphData = computeCallGraphDifferential(actualCallGraphData, baseCallGraphData);
const titleElement = container.closest('.bg-white')?.querySelector('h2');
if (titleElement) {
titleElement.textContent = `Call Graph Analysis (Differential: ${actualDumpName} - ${baseDumpName})`;
}
} else {
console.warn('Base dump not found:', baseDumpName);
}
} else {
const titleElement = container.closest('.bg-white')?.querySelector('h2');
if (titleElement) {
titleElement.textContent = 'Call Graph Analysis';
}
}
await renderCallGraphWithVizJSData(container, callGraphData);
} catch (error) {
console.error('Error generating call graph:', error);
container.innerHTML = '<div class="flex items-center justify-center h-full text-red-600">Error generating call graph. Check console for details.</div>';
}
}
async function updateFlameGraph() {
const baseSelect = document.getElementById('flamegraph-base-select');
const actualSelect = document.getElementById('flamegraph-actual-select');
const container = document.getElementById('flamegraph-fullscreen');
const actualDumpName = actualSelect.value;
if (!actualDumpName) {
container.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">Select a memory dump to generate flame graph...</div>';
return;
}
container.innerHTML = '<div class="flex items-center justify-center h-full text-gray-500">Generating flame graph...</div>';
let actualDump;
try {
actualDump = await DataAccess.getMemoryDump(actualDumpName);
} catch (error) {
container.innerHTML = '<div class="flex items-center justify-center h-full text-red-600">Failed to fetch memory dump</div>';
return;
}
try {
const parser = new BacktraceProcessor.HeapProfileParser();
const actualProfile = parser.parse(actualDump.data);
let profile = actualProfile;
const collapsedStacks = BacktraceProcessor.StackProcessor.collapseStacks(profile);
const flameTree = BacktraceProcessor.StackProcessor.buildFlameTree(collapsedStacks);
let flameData = BacktraceProcessor.StackProcessor.convertToFlameData(flameTree);
const baseDumpName = baseSelect.value;
if (baseDumpName && baseDumpName !== 'none') {
let baseDump;
try {
baseDump = await DataAccess.getMemoryDump(baseDumpName);
} catch (error) {
console.error('Failed to fetch base dump:', error);
baseDump = null;
}
if (baseDump) {
const baseProfile = parser.parse(baseDump.data);
const baseCollapsedStacks = BacktraceProcessor.StackProcessor.collapseStacks(baseProfile);
const baseFlameTree = BacktraceProcessor.StackProcessor.buildFlameTree(baseCollapsedStacks);
const baseFlameData = BacktraceProcessor.StackProcessor.convertToFlameData(baseFlameTree);
flameData = computeDifferentialProfile(flameData, baseFlameData);
const titleElement = container.closest('.bg-white')?.querySelector('h2');
if (titleElement) {
titleElement.textContent = `Heap Flame Graph Analysis (Differential: ${actualDumpName} - ${baseDumpName})`;
}
} else {
console.warn('Base dump not found:', baseDumpName);
}
} else {
const titleElement = container.closest('.bg-white')?.querySelector('h2');
if (titleElement) {
titleElement.textContent = 'Heap Flame Graph Analysis';
}
}
container.textContent = ''; const chartId = 'flamegraph-chart-' + Date.now();
const chartDiv = document.createElement('div');
chartDiv.id = chartId;
chartDiv.className = 'w-full h-full';
container.appendChild(chartDiv);
setTimeout(() => {
renderFlameGraph(chartId, flameData);
}, 100);
} catch (error) {
console.error('Error generating flame graph:', error);
container.innerHTML = '<div class="flex items-center justify-center h-full text-red-600">Error generating flame graph. Check console for details.</div>';
}
}
function computeCallGraphDifferential(actualData, baseData) {
const baseNodeMap = new Map();
const baseLinkMap = new Map();
const baseReverseLinkMap = new Map();
baseData.nodes.forEach(node => {
baseNodeMap.set(node.id, node);
});
baseData.links?.forEach(link => {
const key = `${link.source}->${link.target}`;
baseLinkMap.set(key, link);
});
baseData.reverseLinks?.forEach(link => {
const key = `${link.source}->${link.target}`;
baseReverseLinkMap.set(key, link);
});
const differentialNodes = [];
actualData.nodes.forEach(actualNode => {
const baseNode = baseNodeMap.get(actualNode.id);
const actualMemory = actualNode.memory || 0;
const baseMemory = baseNode ? (baseNode.memory || 0) : 0;
const diffMemory = actualMemory - baseMemory;
if (Math.abs(diffMemory) > 1024 || !baseNode) {
const newNode = {
...actualNode,
memory: diffMemory,
calls: (actualNode.calls || 0) - (baseNode ? (baseNode.calls || 0) : 0),
isDifferential: true,
isNew: !baseNode,
originalMemory: actualMemory
};
differentialNodes.push(newNode);
if (baseMemory > 0) {
baseNodeMap.set(actualNode.id, {
...baseNode,
memory: Math.max(0, baseMemory - actualMemory)
});
}
}
});
const differentialNodeIds = new Set(differentialNodes.map(n => n.id));
const differentialLinks = [];
actualData.links?.forEach(actualLink => {
if (differentialNodeIds.has(actualLink.source) && differentialNodeIds.has(actualLink.target)) {
const key = `${actualLink.source}->${actualLink.target}`;
const baseLink = baseLinkMap.get(key);
const diffValue = (actualLink.value || 0) - (baseLink ? (baseLink.value || 0) : 0);
if (Math.abs(diffValue) > 1024 || !baseLink) {
differentialLinks.push({
...actualLink,
value: diffValue,
isDifferential: true,
isNew: !baseLink
});
}
}
});
const differentialReverseLinks = [];
actualData.reverseLinks?.forEach(actualLink => {
if (differentialNodeIds.has(actualLink.source) && differentialNodeIds.has(actualLink.target)) {
const key = `${actualLink.source}->${actualLink.target}`;
const baseLink = baseReverseLinkMap.get(key);
const diffValue = (actualLink.value || 0) - (baseLink ? (baseLink.value || 0) : 0);
if (Math.abs(diffValue) > 1024 || !baseLink) {
differentialReverseLinks.push({
...actualLink,
value: diffValue,
isDifferential: true,
isNew: !baseLink
});
}
}
});
if (differentialNodes.length === 0) {
return { nodes: [], links: [], reverseLinks: [] };
}
return {
nodes: differentialNodes,
links: differentialLinks,
reverseLinks: differentialReverseLinks
};
}
function showButtonSpinner(button) {
const textSpan = button.querySelector('.btn-text');
const spinner = button.querySelector('.btn-spinner');
if (textSpan && spinner) {
spinner.classList.remove('hidden');
button.disabled = true;
}
}
function hideButtonSpinner(button) {
const textSpan = button.querySelector('.btn-text');
const spinner = button.querySelector('.btn-spinner');
if (textSpan && spinner) {
spinner.classList.add('hidden');
button.disabled = false;
}
}
function showNotification(message, type = 'info') {
const notification = document.createElement('div');
const bgColor = type === 'error' ? 'bg-red-500' : type === 'success' ? 'bg-green-500' : 'bg-blue-500';
notification.className = `fixed top-4 right-4 ${bgColor} text-white px-6 py-3 rounded-lg shadow-lg z-50 max-w-md`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 4000);
}
async function updateProfilingStatusFromAPI() {
try {
const data = await fetchProfilingStatus();
updateProfilingStatus(data);
return data;
} catch (error) {
showNotification('Failed to fetch profiling status', 'error');
return null;
}
}
function updateProfilingStatus(status) {
const indicator = document.getElementById('profiling-status-indicator');
const text = document.getElementById('profiling-status-text');
const message = document.getElementById('profiling-status-message');
const startBtn = document.getElementById('start-profiling-btn');
const stopBtn = document.getElementById('stop-profiling-btn');
const dumpBtn = document.getElementById('trigger-dump-btn');
if (indicator && text && message && startBtn && stopBtn && dumpBtn) {
const isActive = status.profiling_active;
const isSupported = status.heap_dumps_available;
indicator.className = `w-3 h-3 rounded-full ${isActive ? 'bg-green-500' : 'bg-gray-400'}`;
text.textContent = isActive ? 'Active' : 'Inactive';
message.textContent = status.message || (isActive ? 'Memory profiling is active' : 'Memory profiling is inactive');
if (isSupported) {
startBtn.disabled = isActive;
stopBtn.disabled = !isActive;
dumpBtn.disabled = false;
} else {
startBtn.disabled = true;
stopBtn.disabled = true;
dumpBtn.disabled = true;
message.textContent = status.message || 'Memory profiling not supported on this platform';
}
}
}
async function handleStartProfiling() {
const button = document.getElementById('start-profiling-btn');
showButtonSpinner(button);
try {
await startProfiling();
showNotification('Memory profiling started successfully', 'success');
setTimeout(updateProfilingStatusFromAPI, 1000);
} catch (error) {
showNotification(error.message || 'Failed to start profiling', 'error');
} finally {
hideButtonSpinner(button);
}
}
async function handleStopProfiling() {
const button = document.getElementById('stop-profiling-btn');
showButtonSpinner(button);
try {
await stopProfiling();
showNotification('Memory profiling stopped successfully', 'success');
setTimeout(updateProfilingStatusFromAPI, 1000);
} catch (error) {
showNotification(error.message || 'Failed to stop profiling', 'error');
} finally {
hideButtonSpinner(button);
}
}
async function handleTriggerDump() {
const button = document.getElementById('trigger-dump-btn');
showButtonSpinner(button);
try {
await triggerDump();
showNotification('Memory dump created successfully', 'success');
setTimeout(refreshDashboardData, 200);
} catch (error) {
showNotification(error.message || 'Failed to create dump', 'error');
} finally {
hideButtonSpinner(button);
}
}
async function refreshDumpsDisplay() {
try {
const dumps = await listDumps();
await updateDumpsList(dumps);
return dumps;
} catch (error) {
console.error('Error in refreshDumpsDisplay:', error);
showNotification('Failed to list dumps', 'error');
return [];
}
}
async function updateDumpsList(dumps) {
const dumpsListElement = document.getElementById('dumps-list');
if (!dumpsListElement) {
console.error('dumps-list element not found in DOM');
return;
}
if (!dumps || dumps.length === 0) {
const noDumpsDiv = document.createElement('div');
noDumpsDiv.className = 'text-center text-gray-500';
noDumpsDiv.textContent = 'No memory dumps available';
dumpsListElement.innerHTML = '';
dumpsListElement.appendChild(noDumpsDiv);
await populateChartSelectors();
return;
}
const sortedDumps = [...dumps].sort((a, b) => {
const timeA = a.created || parseInt(a.name.match(/(\d+)/)?.[1]) || 0;
const timeB = b.created || parseInt(b.name.match(/(\d+)/)?.[1]) || 0;
return timeB - timeA; });
dumpsListElement.innerHTML = '';
sortedDumps.forEach((dump, index) => {
try {
const timestampDisplay = dump.timestamp
? `Created: ${new Date(dump.timestamp * 1000).toLocaleString()}`
: '';
const dumpElement = createDumpItem(
dump.name,
`Size: ${formatFileSize(dump.size)}`,
timestampDisplay
);
if (dumpElement) {
dumpsListElement.appendChild(dumpElement);
} else {
console.error(`Failed to create dump item ${index + 1}: createDumpItem returned null`);
}
} catch (error) {
console.error(`Error creating dump item ${index + 1}:`, error);
}
});
await populateChartSelectors();
}
async function handleDownloadDump(filename) {
try {
const blob = await downloadDump(filename);
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
showNotification(`Downloaded ${filename}`, 'success');
} catch (error) {
showNotification(error.message || 'Failed to download dump', 'error');
}
}
async function handleDeleteDump(filename) {
if (!confirm(`Are you sure you want to delete ${filename}?`)) {
return;
}
try {
await deleteDump(filename);
showNotification(`Deleted ${filename}`, 'success');
setTimeout(refreshDashboardData, 200);
} catch (error) {
showNotification(error.message || 'Failed to delete dump', 'error');
}
}
async function handleClearAllDumps() {
const button = document.querySelector('button[onclick="handleClearAllDumps()"]');
if (!button) return;
const confirmed = confirm('Are you sure you want to clear all heap dump files? This action cannot be undone.');
if (!confirmed) return;
showButtonSpinner(button);
try {
const result = await clearAllDumps();
console.log('Clear all dumps result:', result);
if (result.deleted_count > 0) {
alert(`Successfully deleted ${result.deleted_count} heap dump files.`);
} else {
alert('No heap dump files were found to delete.');
}
setTimeout(refreshDashboardData, 200);
} catch (error) {
console.error('Error clearing dumps:', error);
alert(`Error clearing dumps: ${error.message || 'Network error'}`);
} finally {
hideButtonSpinner(button);
}
}
let dumpPollingInterval = null;
let lastDumpCount = 0;
function startDumpPolling() {
if (!DataAccess.isDashboardMode()) {
return; }
if (dumpPollingInterval) {
clearInterval(dumpPollingInterval);
}
dumpPollingInterval = setInterval(async () => {
try {
const dumps = await DataAccess.getMemoryDumps();
const currentCount = dumps ? dumps.length : 0;
if (currentCount !== lastDumpCount) {
lastDumpCount = currentCount;
await refreshDumpsDisplay();
}
} catch (error) {
console.error('Error during dump polling:', error);
}
}, 3000);
}
function stopDumpPolling() {
if (dumpPollingInterval) {
clearInterval(dumpPollingInterval);
dumpPollingInterval = null;
}
}
async function refreshDashboardData() {
await refreshDumpsDisplay();
}
function initializeSummaryTab() {
updateProfilingStatusFromAPI();
refreshDumpsDisplay();
setInterval(updateProfilingStatusFromAPI, 5000);
}
document.addEventListener('DOMContentLoaded', async function() {
await initializeApplicationData();
});
window.addEventListener('beforeunload', function() {
stopDumpPolling();
});
async function initializeApplicationData() {
const loadingElements = {
system: document.getElementById('system-info-content'),
config: document.getElementById('router-config-content'),
schema: document.getElementById('schema-content')
};
Object.values(loadingElements).forEach(el => {
if (el) el.textContent = 'Loading...';
});
try {
const data = await loadAllData();
if (data.systemInfo && loadingElements.system) {
loadingElements.system.textContent = data.systemInfo;
}
if (data.routerConfig && loadingElements.config) {
loadingElements.config.textContent = data.routerConfig;
}
if (data.schema && loadingElements.schema) {
loadingElements.schema.textContent = data.schema;
}
if (!DataAccess.isDashboardMode()) {
const dashboardTab = document.getElementById('dashboard-tab');
if (dashboardTab) {
dashboardTab.style.display = 'none';
}
showTab('system');
}
await populateChartSelectors();
if (DataAccess.isDashboardMode()) {
initializeSummaryTab();
const dumps = await DataAccess.getMemoryDumps();
lastDumpCount = dumps.length;
startDumpPolling();
}
} catch (error) {
console.error('Failed to initialize application data:', error);
Object.values(loadingElements).forEach(el => {
if (el) el.textContent = 'Error loading data';
});
}
}