dashboard.html•26.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CodeGraph Memory Profiler Dashboard</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/date-fns@2.29.3/index.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f0f;
color: #ffffff;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding: 20px;
background: linear-gradient(135deg, #1a1a2e, #16213e);
border-radius: 12px;
border: 1px solid #333;
}
.header h1 {
color: #00d4aa;
font-size: 2.5rem;
margin-bottom: 10px;
}
.header p {
color: #aaa;
font-size: 1.1rem;
}
.status-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.status-card {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: all 0.3s ease;
}
.status-card:hover {
border-color: #00d4aa;
transform: translateY(-2px);
}
.status-card h3 {
color: #00d4aa;
font-size: 1.8rem;
margin-bottom: 5px;
}
.status-card p {
color: #bbb;
font-size: 0.9rem;
}
.pressure-low { border-left: 4px solid #00d4aa; }
.pressure-medium { border-left: 4px solid #ffa500; }
.pressure-high { border-left: 4px solid #ff6b6b; }
.pressure-critical { border-left: 4px solid #ff1744; }
.dashboard-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
}
.chart-container h2 {
color: #00d4aa;
margin-bottom: 15px;
font-size: 1.3rem;
}
.chart {
position: relative;
height: 300px;
}
.side-panel {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 20px;
}
.leaks-list {
max-height: 300px;
overflow-y: auto;
}
.leak-item {
background: #2a2a2a;
border: 1px solid #ff6b6b;
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
}
.leak-item h4 {
color: #ff6b6b;
font-size: 0.9rem;
margin-bottom: 5px;
}
.leak-item p {
color: #ccc;
font-size: 0.8rem;
}
.recommendations {
margin-top: 30px;
}
.recommendation-item {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
border-left: 4px solid #00d4aa;
}
.recommendation-item.warning {
border-left-color: #ffa500;
}
.recommendation-item.critical {
border-left-color: #ff6b6b;
}
.recommendation-item h3 {
color: #00d4aa;
margin-bottom: 8px;
font-size: 1.1rem;
}
.recommendation-item p {
color: #bbb;
margin-bottom: 5px;
}
.recommendation-item .savings {
color: #00d4aa;
font-weight: bold;
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin-bottom: 20px;
}
.btn {
background: #00d4aa;
color: #000;
border: none;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s ease;
}
.btn:hover {
background: #00b894;
transform: translateY(-1px);
}
.btn-secondary {
background: #333;
color: #fff;
}
.btn-secondary:hover {
background: #555;
}
.connection-status {
position: fixed;
top: 20px;
right: 20px;
padding: 10px;
border-radius: 6px;
font-size: 0.9rem;
z-index: 1000;
}
.connection-status.connected {
background: #00d4aa;
color: #000;
}
.connection-status.disconnected {
background: #ff6b6b;
color: #fff;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #aaa;
}
.categories-chart {
max-height: 250px;
}
@media (max-width: 768px) {
.dashboard-grid {
grid-template-columns: 1fr;
}
.status-bar {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2rem;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧠 CodeGraph Memory Profiler</h1>
<p>Real-time memory allocation tracking and optimization recommendations</p>
</div>
<div class="connection-status" id="connectionStatus">
🔌 Connecting...
</div>
<div class="controls">
<button class="btn" onclick="refreshData()">🔄 Refresh</button>
<button class="btn btn-secondary" onclick="exportData('json')">📄 Export JSON</button>
<button class="btn btn-secondary" onclick="exportData('csv')">📊 Export CSV</button>
<button class="btn btn-secondary" onclick="detectLeaks()">🔍 Detect Leaks</button>
</div>
<div class="status-bar" id="statusBar">
<div class="status-card">
<h3 id="currentUsage">--</h3>
<p>Current Usage</p>
</div>
<div class="status-card">
<h3 id="peakUsage">--</h3>
<p>Peak Usage</p>
</div>
<div class="status-card">
<h3 id="allocationCount">--</h3>
<p>Total Allocations</p>
</div>
<div class="status-card" id="pressureCard">
<h3 id="memoryPressure">--</h3>
<p>Memory Pressure</p>
</div>
</div>
<div class="dashboard-grid">
<div class="chart-container">
<h2>📈 Memory Usage Over Time</h2>
<div class="chart">
<canvas id="memoryChart"></canvas>
</div>
</div>
<div class="side-panel">
<h2>🔥 Active Memory Leaks</h2>
<div class="leaks-list" id="leaksList">
<div class="loading">Loading leaks...</div>
</div>
</div>
</div>
<div class="chart-container">
<h2>🎯 Memory by Category</h2>
<div class="chart categories-chart">
<canvas id="categoriesChart"></canvas>
</div>
</div>
<div class="recommendations" id="recommendations">
<h2>💡 Optimization Recommendations</h2>
<div id="recommendationsList">
<div class="loading">Loading recommendations...</div>
</div>
</div>
</div>
<script>
class MemoryDashboard {
constructor() {
this.ws = null;
this.memoryChart = null;
this.categoriesChart = null;
this.isConnected = false;
this.init();
}
async init() {
this.setupWebSocket();
this.setupCharts();
await this.loadInitialData();
// Auto-refresh every 30 seconds
setInterval(() => this.refreshData(), 30000);
}
setupWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.isConnected = true;
this.updateConnectionStatus();
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handleWebSocketMessage(data);
};
this.ws.onclose = () => {
this.isConnected = false;
this.updateConnectionStatus();
// Attempt to reconnect after 5 seconds
setTimeout(() => this.setupWebSocket(), 5000);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.isConnected = false;
this.updateConnectionStatus();
};
}
updateConnectionStatus() {
const status = document.getElementById('connectionStatus');
if (this.isConnected) {
status.textContent = '🟢 Connected';
status.className = 'connection-status connected';
} else {
status.textContent = '🔴 Disconnected';
status.className = 'connection-status disconnected';
}
}
handleWebSocketMessage(data) {
switch (data.type) {
case 'LiveMetrics':
this.updateLiveMetrics(data);
break;
case 'Event':
this.handleProfilerEvent(data);
break;
}
}
updateLiveMetrics(data) {
this.updateStatusCards({
current_usage: data.current_usage,
memory_pressure: data.memory_pressure,
allocation_count: data.allocation_count || 0,
peak_usage: data.peak_usage || 0
});
// Update memory chart with new data point
if (this.memoryChart && data.current_usage) {
const chart = this.memoryChart;
const now = new Date();
// Add new data point
chart.data.labels.push(now.toLocaleTimeString());
chart.data.datasets[0].data.push(data.current_usage / (1024 * 1024)); // Convert to MB
// Keep only last 20 points
if (chart.data.labels.length > 20) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update('none');
}
}
handleProfilerEvent(event) {
console.log('Profiler event:', event);
if (event.type === 'LeakDetected') {
this.addLeakToList(event.leak);
} else if (event.type === 'RecommendationGenerated') {
this.addRecommendation(event.recommendation);
}
}
setupCharts() {
// Memory usage chart
const memoryCtx = document.getElementById('memoryChart').getContext('2d');
this.memoryChart = new Chart(memoryCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Memory Usage (MB)',
data: [],
borderColor: '#00d4aa',
backgroundColor: 'rgba(0, 212, 170, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: { color: '#ffffff' }
}
},
scales: {
x: {
ticks: { color: '#aaaaaa' },
grid: { color: '#333333' }
},
y: {
ticks: { color: '#aaaaaa' },
grid: { color: '#333333' }
}
}
}
});
// Categories chart
const categoriesCtx = document.getElementById('categoriesChart').getContext('2d');
this.categoriesChart = new Chart(categoriesCtx, {
type: 'doughnut',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
'#00d4aa', '#ff6b6b', '#ffa500', '#4ecdc4',
'#45b7d1', '#f9ca24', '#6c5ce7', '#a29bfe'
],
borderWidth: 2,
borderColor: '#1a1a1a'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#ffffff' }
}
}
}
});
}
async loadInitialData() {
try {
await Promise.all([
this.loadMetrics(),
this.loadLeaks(),
this.loadRecommendations(),
this.loadHistory()
]);
} catch (error) {
console.error('Error loading initial data:', error);
}
}
async loadMetrics() {
try {
const response = await fetch('/api/metrics');
const result = await response.json();
if (result.success) {
this.updateStatusCards(result.data);
this.updateCategoriesChart(result.data.categories);
}
} catch (error) {
console.error('Error loading metrics:', error);
}
}
async loadLeaks() {
try {
const response = await fetch('/api/leaks');
const result = await response.json();
if (result.success) {
this.updateLeaksList(result.data);
}
} catch (error) {
console.error('Error loading leaks:', error);
}
}
async loadRecommendations() {
try {
const response = await fetch('/api/recommendations');
const result = await response.json();
if (result.success) {
this.updateRecommendations(result.data);
}
} catch (error) {
console.error('Error loading recommendations:', error);
}
}
async loadHistory() {
try {
const response = await fetch('/api/history?hours=1&resolution=minute');
const result = await response.json();
if (result.success && result.data.length > 0) {
const labels = result.data.map(point =>
new Date(point.timestamp * 1000).toLocaleTimeString()
);
const data = result.data.map(point => point.total_usage / (1024 * 1024));
this.memoryChart.data.labels = labels;
this.memoryChart.data.datasets[0].data = data;
this.memoryChart.update();
}
} catch (error) {
console.error('Error loading history:', error);
}
}
updateStatusCards(metrics) {
document.getElementById('currentUsage').textContent =
this.formatBytes(metrics.current_usage);
document.getElementById('peakUsage').textContent =
this.formatBytes(metrics.peak_usage);
document.getElementById('allocationCount').textContent =
metrics.allocation_count?.toLocaleString() || '--';
const pressureCard = document.getElementById('pressureCard');
const pressureElement = document.getElementById('memoryPressure');
pressureElement.textContent = this.formatPressure(metrics.memory_pressure);
// Update pressure card styling
pressureCard.className = `status-card pressure-${metrics.memory_pressure?.toLowerCase() || 'low'}`;
}
updateCategoriesChart(categories) {
if (!categories || Object.keys(categories).length === 0) return;
const labels = Object.keys(categories);
const data = Object.values(categories).map(cat => cat.current / (1024 * 1024)); // Convert to MB
this.categoriesChart.data.labels = labels;
this.categoriesChart.data.datasets[0].data = data;
this.categoriesChart.update();
}
updateLeaksList(leaks) {
const container = document.getElementById('leaksList');
if (leaks.length === 0) {
container.innerHTML = '<p style="color: #00d4aa; text-align: center;">🎉 No memory leaks detected!</p>';
return;
}
container.innerHTML = leaks.map(leak => `
<div class="leak-item">
<h4>🚨 ${this.formatBytes(leak.size)} leak (${leak.age_seconds || 0}s old)</h4>
<p><strong>Category:</strong> ${leak.category}</p>
<p><strong>Impact:</strong> ${leak.estimated_impact}</p>
<p><strong>Stack:</strong> ${leak.stack_trace?.slice(0, 2).join(' → ') || 'No trace'}</p>
</div>
`).join('');
}
addLeakToList(leak) {
// Add new leak to the top of the list
const container = document.getElementById('leaksList');
const leakHtml = `
<div class="leak-item" style="animation: fadeIn 0.5s ease-in;">
<h4>🚨 ${this.formatBytes(leak.size)} leak (just detected)</h4>
<p><strong>Category:</strong> ${leak.category}</p>
<p><strong>Impact:</strong> ${leak.estimated_impact}</p>
</div>
`;
if (container.innerHTML.includes('No memory leaks detected')) {
container.innerHTML = leakHtml;
} else {
container.insertAdjacentHTML('afterbegin', leakHtml);
}
}
updateRecommendations(recommendations) {
const container = document.getElementById('recommendationsList');
if (recommendations.length === 0) {
container.innerHTML = '<p style="color: #00d4aa; text-align: center;">✅ No optimization recommendations at this time</p>';
return;
}
container.innerHTML = recommendations.map(rec => `
<div class="recommendation-item ${rec.severity?.toLowerCase() || 'info'}">
<h3>💡 ${rec.action || 'Optimization'}</h3>
<p>${rec.description}</p>
<p><strong>Category:</strong> ${rec.category}</p>
<p><strong>Estimated Savings:</strong> <span class="savings">${this.formatBytes(rec.estimated_savings || 0)}</span></p>
<p><strong>Difficulty:</strong> ${rec.implementation_difficulty || 'Unknown'}</p>
</div>
`).join('');
}
addRecommendation(recommendation) {
// Add new recommendation to the top of the list
const container = document.getElementById('recommendationsList');
const recHtml = `
<div class="recommendation-item ${recommendation.severity?.toLowerCase() || 'info'}" style="animation: fadeIn 0.5s ease-in;">
<h3>💡 ${recommendation.action || 'Optimization'} (New)</h3>
<p>${recommendation.description}</p>
<p><strong>Estimated Savings:</strong> <span class="savings">${this.formatBytes(recommendation.estimated_savings || 0)}</span></p>
</div>
`;
if (container.innerHTML.includes('No optimization recommendations')) {
container.innerHTML = recHtml;
} else {
container.insertAdjacentHTML('afterbegin', recHtml);
}
}
formatBytes(bytes) {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
formatPressure(pressure) {
if (!pressure) return 'Low';
return pressure.charAt(0).toUpperCase() + pressure.slice(1).toLowerCase();
}
async refreshData() {
await this.loadInitialData();
console.log('Data refreshed');
}
async exportData(format) {
try {
const response = await fetch(`/api/export?format=${format}`);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `memory_profile.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Export error:', error);
alert('Export failed. Please try again.');
}
}
async detectLeaks() {
try {
await this.loadLeaks();
console.log('Leak detection completed');
} catch (error) {
console.error('Leak detection error:', error);
}
}
}
// Global functions for button clicks
let dashboard;
function refreshData() {
dashboard.refreshData();
}
function exportData(format) {
dashboard.exportData(format);
}
function detectLeaks() {
dashboard.detectLeaks();
}
// Initialize dashboard when page loads
document.addEventListener('DOMContentLoaded', () => {
dashboard = new MemoryDashboard();
});
// Add fade-in animation
const style = document.createElement('style');
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
`;
document.head.appendChild(style);
</script>
</body>
</html>