<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}} - MemScope Memory Analysis</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
:root {
--primary: #3b82f6;
--secondary: #64748b;
--accent: #f59e0b;
--danger: #ef4444;
--success: #10b981;
--info: #3b82f6;
--warning: #f59e0b;
--cycle-edge: #ef4444;
--relationship-clone: #10b981;
--relationship-ownership: #dc2626;
--relationship-borrow: #3b82f6;
--relationship-arc: #8b5cf6;
--relationship-reference: #64748b;
--bg: #f8fafc;
--bg2: #ffffff;
--bg3: #f1f5f9;
--text: #1e293b;
--text2: #64748b;
--border: #e2e8f0;
}
[data-theme="dark"] {
--primary: #60a5fa;
--secondary: #94a3b8;
--accent: #fbbf24;
--danger: #f87171;
--success: #34d399;
--warning: #fbbf24;
--bg: #0f172a;
--bg2: #1e293b;
--bg3: #334155;
--text: #f1f5f9;
--text2: #94a3b8;
--border: #334155;
}
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg);
color: var(--text);
margin: 0;
padding: 0;
line-height: 1.6;
transition: background 0.3s, color 0.3s;
}
.container { max-width: 1600px; margin: 0 auto; padding: 16px; }
.header {
background: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 16px 0;
position: sticky;
top: 0;
z-index: 100;
transition: background 0.3s, border-color 0.3s;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.header h1 {
color: var(--primary);
font-size: 1.5rem;
font-weight: 700;
margin: 0;
}
.header-info { color: var(--text2); font-size: 0.85rem; }
.theme-toggle {
background: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 16px;
border-radius: 8px;
cursor: pointer;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
transition: all 0.2s;
}
.theme-toggle:hover { background: var(--primary); color: white; }
.mode-tabs {
display: flex;
gap: 4px;
background: var(--bg2);
padding: 4px;
border-radius: 8px;
margin: 16px 0;
overflow-x: auto;
border: 1px solid var(--border);
}
.mode-tab {
padding: 8px 16px;
border-radius: 6px;
border: none;
background: transparent;
color: var(--text2);
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
transition: all 0.2s;
}
.mode-tab:hover { background: var(--bg3); color: var(--text); }
.mode-tab.active { background: var(--primary); color: white; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.stat-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 16px;
text-align: center;
transition: transform 0.2s, border-color 0.2s, background 0.3s;
}
.stat-card:hover { transform: translateY(-2px); border-color: var(--primary); }
.stat-value { font-size: 1.75rem; font-weight: 700; color: var(--primary); }
.stat-label { color: var(--text2); font-size: 0.8rem; margin-top: 4px; }
.stat-card.danger .stat-value { color: var(--danger); }
.stat-card.warning .stat-value { color: var(--warning); }
.stat-card.success .stat-value { color: var(--success); }
.diagnosis-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 12px;
}
.diagnosis-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
background: var(--bg);
border-radius: 8px;
border-left: 4px solid var(--border);
}
.diagnosis-item.critical { border-left-color: var(--danger); background: rgba(239, 68, 68, 0.05); }
.diagnosis-item.warning { border-left-color: var(--warning); background: rgba(245, 158, 11, 0.05); }
.diagnosis-item.info { border-left-color: var(--info); background: rgba(59, 130, 246, 0.05); }
.diagnosis-item.success { border-left-color: var(--success); background: rgba(16, 185, 129, 0.05); }
.diagnosis-icon { font-size: 1.25rem; flex-shrink: 0; }
.diagnosis-content { flex: 1; }
.diagnosis-title { font-weight: 600; font-size: 0.9rem; margin-bottom: 2px; }
.diagnosis-item.critical .diagnosis-title { color: var(--danger); }
.diagnosis-item.warning .diagnosis-title { color: var(--warning); }
.diagnosis-item.info .diagnosis-title { color: var(--info); }
.diagnosis-item.success .diagnosis-title { color: var(--success); }
.diagnosis-desc { font-size: 0.8rem; color: var(--text2); }
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px;
margin-bottom: 20px;
transition: background 0.3s, border-color 0.3s;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
border-bottom: 2px solid var(--primary);
padding-bottom: 10px;
}
.card-title { color: var(--primary); font-size: 1.2rem; font-weight: 600; margin: 0; }
.mode-section { display: none; }
.mode-section.active { display: block; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th, td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
}
th { background: var(--bg); color: var(--text2); font-weight: 600; position: sticky; top: 0; }
tr:hover { background: rgba(59, 130, 246, 0.1); }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.badge-danger { background: rgba(239, 68, 68, 0.2); color: var(--danger); }
.badge-warning { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
.badge-success { background: rgba(16, 185, 129, 0.2); color: var(--success); }
.badge-info { background: rgba(59, 130, 246, 0.2); color: var(--primary); }
.chart-container { position: relative; height: 300px; margin: 16px 0; }
.chart-container.small { height: 200px; }
.detail-panel {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.detail-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid var(--border);
}
.detail-row:last-child { border-bottom: none; }
.detail-label { color: var(--text2); }
.detail-value { font-weight: 600; font-family: monospace; }
.timeline { position: relative; padding-left: 24px; }
.timeline::before {
content: '';
position: absolute;
left: 8px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
position: relative;
padding: 12px 0;
}
.timeline-item::before {
content: '';
position: absolute;
left: -20px;
top: 16px;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--primary);
}
.timeline-item.leaked::before { background: var(--danger); }
.timeline-item.active::before { background: var(--success); }
.search-box {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
color: var(--text);
width: 100%;
max-width: 300px;
margin-bottom: 12px;
}
.search-box:focus { outline: none; border-color: var(--primary); }
.table-wrapper { max-height: 500px; overflow-y: auto; border-radius: 8px; }
.empty-state { text-align: center; padding: 40px; color: var(--text2); }
.tab-nav {
display: flex;
gap: 4px;
margin-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.tab-btn {
padding: 8px 16px;
border: none;
background: transparent;
color: var(--text2);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
}
.tab-btn:hover { color: var(--text); }
.tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
.tab-content { display: none; }
.tab-content.active { display: block; }
.risk-low { color: var(--success); }
.risk-medium { color: var(--warning); }
.risk-high { color: var(--danger); }
.graph-container {
width: 100%;
height: 400px;
background: var(--bg);
border-radius: 8px;
overflow: hidden;
}
.node circle { cursor: pointer; transition: all 0.2s; }
.node circle:hover { filter: brightness(1.2); }
.node text { font-size: 10px; fill: var(--text); pointer-events: none; }
.link { stroke-opacity: 0.6; }
.tooltip {
position: absolute;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 12px;
font-size: 0.85rem;
pointer-events: none;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 8px;
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 50%;
}
.flex-center { display: flex; align-items: center; justify-content: center; }
.flex-between { display: flex; align-items: center; justify-content: space-between; }
.flex-gap-2 { display: flex; align-items: center; gap: 8px; }
.flex-gap-3 { display: flex; align-items: center; gap: 12px; }
.flex-gap-4 { display: flex; align-items: center; gap: 16px; }
.flex-wrap { flex-wrap: wrap; }
.mb-3 { margin-bottom: 12px; }
.mt-3 { margin-top: 12px; }
.mt-4 { margin-top: 16px; }
.gap-3 { gap: 12px; }
.gap-4 { gap: 16px; }
.gap-2 { gap: 8px; }
.grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
.grid-auto-sm { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.grid-auto-lg { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; }
.flex-float-start { display: flex; align-items: flex-start; }
.flex-1 { flex: 1; }
.text-primary { color: var(--primary); }
.font-bold { font-weight: 600; }
.mb-2 { margin-bottom: 8px; }
.mb-4 { margin-bottom: 16px; }
.mt-5 { margin-top: 20px; }
.text-sm { font-size: 0.9rem; }
.text-secondary { color: var(--text2); font-size: 0.9rem; }
.text-small { font-size: 0.85rem; }
.text-mono { font-family: monospace; }
.btn-sm { padding: 4px 12px; background: var(--bg3); border: 1px solid var(--border); border-radius: 6px; cursor: pointer; color: var(--text); }
.btn-sm:hover { background: var(--bg2); }
.input-sm { background: var(--bg3); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; color: var(--text); cursor: pointer; }
.table-scroll { max-height: 400px; overflow-y: auto; border-radius: 8px; }
.table-scroll-sm { max-height: 300px; overflow-y: auto; border-radius: 8px; }
.pagination { display: none; justify-content: center; align-items: center; gap: 8px; margin-top: 12px; padding: 12px; }
.panel-border { border: 1px solid var(--border); border-radius: 8px; padding: 16px; }
.panel-primary { border-left: 4px solid var(--primary); }
.panel-danger { border-left: 4px solid var(--danger); }
.grid-auto { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; }
.grid-auto-sm { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; }
.timeline-item { margin-bottom: 8px; padding: 8px; border-radius: 4px; }
.timeline-item.alloc { background: rgba(16, 185, 129, 0.1); border-left: 3px solid #10b981; }
.timeline-item.dealloc { background: rgba(59, 130, 246, 0.1); border-left: 3px solid #3b82f6; }
.timeline-item.leak { background: rgba(239, 68, 68, 0.1); border-left: 3px solid #ef4444; }
@media (max-width: 768px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.header-content { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<div class="header">
<div class="container header-content">
<div>
<h1>🧠 MemScope Memory Analysis</h1>
<div class="header-info">{{title}} | {{export_timestamp}}</div>
</div>
<div class="flex-gap-3">
<div class="header-info">{{os_name}} | {{architecture}} | {{cpu_cores}} cores</div>
<button class="theme-toggle" onclick="toggleTheme()">
<span id="theme-icon">🌙</span>
<span id="theme-text">Dark</span>
</button>
</div>
</div>
</div>
<div class="container">
<div class="mode-tabs">
<button class="mode-tab active" onclick="showMode('overview')">📊 Overview</button>
<button class="mode-tab" onclick="showMode('thread')">🧵 Thread View</button>
<button class="mode-tab" onclick="showMode('task')">⚡ Async View</button>
<button class="mode-tab" onclick="showMode('taskgraph')">🌳 Task Graph</button>
<button class="mode-tab" onclick="showMode('variable')">📦 Variable View</button>
<button class="mode-tab" onclick="showMode('passport')">🛂 Memory Passport</button>
<button class="mode-tab" onclick="showMode('timetravel')">⏪ Time Travel</button>
<button class="mode-tab" onclick="showMode('unsafe')">⚠️ Unsafe/FFI</button>
</div>
<div class="stats-grid">
<div class="stat-card" id="healthScoreCard" style="background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%); border: 2px solid var(--primary);">
<div class="stat-value" id="healthScore" style="font-size: 2.5rem;">{{health_score}}</div>
<div class="stat-label">🛡️ Health Score</div>
<div id="healthStatus" style="font-size: 0.75rem; margin-top: 4px; color: var(--text2);">{{health_status}}</div>
</div>
<div class="stat-card">
<div class="stat-value">{{total_allocations}}</div>
<div class="stat-label">📦 Allocations</div>
</div>
<div class="stat-card">
<div class="stat-value">{{total_memory}}</div>
<div class="stat-label">💾 Memory</div>
</div>
<div class="stat-card">
<div class="stat-value">{{peak_memory}}</div>
<div class="stat-label">📈 Peak</div>
</div>
<div class="stat-card danger" id="leakCard">
<div class="stat-value">{{leak_count}}</div>
<div class="stat-label">🔴 Leaks</div>
</div>
<div class="stat-card" style="background: linear-gradient(135deg, #8b5cf620 0%, #8b5cf640 100%); border: 1px solid #8b5cf6;">
<div class="stat-value" style="color: #8b5cf6;">{{passport_count}}</div>
<div class="stat-label">🛂 Passports</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{unsafe_count}}</div>
<div class="stat-label">⚠️ Unsafe</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ffi_count}}</div>
<div class="stat-label">🔗 FFI Calls</div>
</div>
</div>
<!-- Overview Mode -->
<div id="mode-overview" class="mode-section active">
<!-- Quick Health Dashboard -->
<div class="card" style="background: linear-gradient(135deg, #3b82f610 0%, #3b82f620 100%); border: 2px solid var(--primary); margin-bottom: 16px;">
<div class="card-header" style="margin-bottom: 12px;">
<h2 class="card-title">🎯 Quick Health Dashboard</h2>
<div class="flex-gap-2">
<span class="badge badge-info" id="quickStatusBadge">Analyzing...</span>
<span class="text-secondary" style="font-size: 0.85rem;">Last Updated: {{export_timestamp}}</span>
</div>
</div>
<!-- Critical Alerts -->
<div id="criticalAlerts" style="margin-bottom: 12px;"></div>
<!-- Compact Stats Grid -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 8px;">
<div class="stat-card" style="padding: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem;" id="quickHealthScore">{{health_score}}</div>
<div class="stat-label" style="font-size: 0.75rem;">🛡️ Health</div>
</div>
<div id="healthTrend" style="font-size: 1.5rem;">✅</div>
</div>
</div>
<div class="stat-card danger" style="padding: 8px;" id="quickLeakCard">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem;">{{leak_count}}</div>
<div class="stat-label" style="font-size: 0.75rem;">🔴 Leaks</div>
</div>
<div id="leakTrend" style="font-size: 1.5rem;">⚠️</div>
</div>
</div>
<div class="stat-card" style="padding: 8px; background: linear-gradient(135deg, #10b98120 0%, #10b98140 100%); border: 1px solid #10b981;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem; color: #10b981;" id="quickActiveTasks">0</div>
<div class="stat-label" style="font-size: 0.75rem;">🟢 Active Tasks</div>
</div>
<div style="font-size: 1.5rem;">⚡</div>
</div>
</div>
<div class="stat-card warning" style="padding: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem;">{{unsafe_count}}</div>
<div class="stat-label" style="font-size: 0.75rem;">⚠️ Unsafe</div>
</div>
<div id="unsafeTrend" style="font-size: 1.5rem;">⚡</div>
</div>
</div>
<div class="stat-card" style="padding: 8px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem;">{{ffi_count}}</div>
<div class="stat-label" style="font-size: 0.75rem;">🔗 FFI</div>
</div>
<div style="font-size: 1.5rem;">🌐</div>
</div>
</div>
<div class="stat-card" style="padding: 8px; background: linear-gradient(135deg, #8b5cf620 0%, #8b5cf640 100%); border: 1px solid #8b5cf6;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<div class="stat-value" style="font-size: 1.5rem; color: #8b5cf6;">{{passport_count}}</div>
<div class="stat-label" style="font-size: 0.75rem;">🛂 Passports</div>
</div>
<div style="font-size: 1.5rem;">📋</div>
</div>
</div>
</div>
</div>
<!-- Auto Diagnosis -->
<div class="card" style="background: linear-gradient(135deg, #fef2f220 0%, #fef2f240 100%); border: 2px solid #fef2f2;">
<div class="card-header">
<h2 class="card-title">🔬 Auto Diagnosis</h2>
<span class="badge" id="diagnosisBadge" style="background: var(--primary); color: white;">Analysis Complete</span>
</div>
<div id="diagnosisContent" class="diagnosis-grid">
<!-- Dynamic diagnosis content will be rendered here -->
</div>
</div>
<div class="card" style="background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%); border: 2px solid var(--primary);">
<div class="card-header">
<h2 class="card-title">🛡️ Memory Safety Analysis</h2>
<span class="badge badge-info" id="safetyBadge">{{safe_code_percent}}% Safe</span>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 16px;">
<div class="detail-panel" style="border-left: 4px solid var(--success);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.5rem;">✅</span>
<div>
<div style="font-weight: 600; color: var(--success);">Safe Operations</div>
<div id="safeOpsCount" style="font-size: 1.25rem; font-weight: 700;">{{safe_ops_count}}</div>
</div>
</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--warning);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.5rem;">⚡</span>
<div>
<div style="font-weight: 600; color: var(--warning);">Rust-FFI Crossings</div>
<div id="ffiCrossCount" style="font-size: 1.25rem; font-weight: 700;">{{ffi_count}}</div>
</div>
</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--danger);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 1.5rem;">🚨</span>
<div>
<div style="font-weight: 600; color: var(--danger);">Potential Issues</div>
<div id="potentialIssues" style="font-size: 1.25rem; font-weight: 700;">{{high_risk_count}}</div>
</div>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="memoryChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📊 Type Intelligence</h2>
<span class="text-secondary" style="font-size: 0.85rem;">Automatic type inference with confidence scores</span>
</div>
<div class="chart-container">
<canvas id="typeChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Active Allocations</h2>
<div class="flex-gap-3">
<span class="badge badge-info" id="allocCount">0 items</span>
<button class="btn-sm" onclick="Dashboard.allocations.toggle()" id="toggleAllocBtn">▼ Show All</button>
</div>
</div>
<input type="text" class="search-box" id="searchAllocations" placeholder="Search by name or type..." oninput="filterTable('allocationsTable', this.value)">
<div class="table-wrapper table-scroll" id="allocationsWrapper">
<table id="allocationsTable">
<thead>
<tr>
<th>Address</th>
<th>Variable</th>
<th>Type</th>
<th>Size</th>
<th>Source</th>
<th>Thread</th>
<th>Lifetime</th>
<th>Status</th>
</tr>
</thead>
<tbody id="allocationsBody"></tbody>
</table>
</div>
<div id="allocationsPagination" class="pagination">
<button class="btn-sm" onclick="Dashboard.allocations.prevPage()" id="prevAllocBtn">← Prev</button>
<span class="text-secondary" id="allocPageInfo">Page 1 of 1</span>
<button class="btn-sm" onclick="Dashboard.allocations.nextPage()" id="nextAllocBtn">Next →</button>
</div>
</div>
</div>
<!-- Thread Mode -->
<div id="mode-thread" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, #10b98110 0%, #10b98120 100%); border: 2px solid #10b98160;">
<div class="card-header">
<h2 class="card-title" style="color: #10b981;">🧵 Thread Activity Dashboard</h2>
<span class="badge" style="background: #10b98130; color: #10b981;" id="threadCount">0 threads</span>
</div>
<!-- Thread Activity Timeline -->
<div style="background: var(--bg2); border-radius: 8px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 2rem;">📊</span>
<div>
<div style="font-weight: 600; color: var(--primary);">Real-time Thread Activity</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Monitor memory allocation patterns across all threads. Each bar represents memory activity over time.
</div>
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div class="detail-panel" style="border-left: 4px solid #10b981; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🟢 Active Threads</div>
<div class="stat-value" style="color: #10b981; font-size: 1.5rem;" id="activeThreadCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #3b82f6; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">💾 Total Memory</div>
<div class="stat-value" style="color: #3b82f6; font-size: 1.5rem;" id="totalThreadMemory">0 B</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🏔️ Peak Memory</div>
<div class="stat-value" style="color: #f59e0b; font-size: 1.5rem;" id="peakThreadMemory">0 B</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #8b5cf6; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">📦 Total Allocs</div>
<div class="stat-value" style="color: #8b5cf6; font-size: 1.5rem;" id="totalThreadAllocs">0</div>
</div>
</div>
</div>
<!-- Thread Memory Heatmap -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🔥 Thread Memory Heatmap</h2>
<div class="flex-gap-2">
<button class="btn-sm" onclick="Dashboard.threads.toggleHeatmap('memory')">💾 Memory</button>
<button class="btn-sm" onclick="Dashboard.threads.toggleHeatmap('allocs')">📦 Allocations</button>
</div>
</div>
<div id="threadHeatmapContainer" style="min-height: 200px; background: var(--bg); border-radius: 8px; padding: 16px;"></div>
</div>
<!-- Thread Activity Timeline -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📅 Thread Activity Timeline</h2>
</div>
<div id="threadTimelineContainer" style="min-height: 250px; background: var(--bg); border-radius: 8px; padding: 16px;"></div>
</div>
<!-- Thread Distribution Chart -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📊 Memory Distribution</h2>
</div>
<div class="chart-container" style="height: 200px;">
<canvas id="threadChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Thread Details</h2>
<div class="flex-gap-2">
<label class="text-secondary">Sort by:</label>
<select id="threadSortOrder" onchange="Dashboard.threads.sort()" class="input-sm">
<option value="alloc-desc">Allocations ↓</option>
<option value="alloc-asc">Allocations ↑</option>
<option value="memory-desc">Memory ↓</option>
<option value="memory-asc">Memory ↑</option>
<option value="thread-id">Thread ID</option>
</select>
</div>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>Thread ID</th>
<th>Summary</th>
<th>Allocations</th>
<th>Current Memory</th>
<th>Peak Memory</th>
<th>Status</th>
</tr>
</thead>
<tbody id="threadTableBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Task Mode (Allocations) -->
<div id="mode-task" class="mode-section">
<!-- Async Overview Dashboard -->
<div class="card" style="background: linear-gradient(135deg, #3b82f610 0%, #3b82f620 100%); border: 2px solid var(--primary);">
<div class="card-header">
<h2 class="card-title">⚡ Async Task Dashboard</h2>
<span class="badge badge-info" id="asyncTaskCount">0 tasks</span>
</div>
<!-- Quick Stats -->
<div class="stats-grid" style="margin-bottom: 16px;">
<div class="stat-card">
<div class="stat-value" id="activeTaskCount">0</div>
<div class="stat-label">🟢 Active Tasks</div>
</div>
<div class="stat-card success">
<div class="stat-value" id="completedTaskCount">0</div>
<div class="stat-label">✅ Completed</div>
</div>
<div class="stat-card warning">
<div class="stat-value" id="leakTaskCount">0</div>
<div class="stat-label">⚠️ Potential Leaks</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgEfficiency">0%</div>
<div class="stat-label">📊 Avg Efficiency</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalAsyncMemory">0 B</div>
<div class="stat-label">💾 Total Memory</div>
</div>
<div class="stat-card">
<div class="stat-value" id="avgDuration">0ms</div>
<div class="stat-label">⏱️ Avg Duration</div>
</div>
</div>
</div>
<!-- Task Execution Timeline -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📅 Task Execution Timeline</h2>
<div class="flex-gap-2">
<button class="btn-sm" onclick="Dashboard.tasks.toggleView('timeline')">📊 Timeline</button>
<button class="btn-sm" onclick="Dashboard.tasks.toggleView('gantt')">📈 Gantt Chart</button>
</div>
</div>
<div id="taskTimelineContainer" style="min-height: 300px; background: var(--bg); border-radius: 8px; padding: 16px;">
<div id="taskTimeline"></div>
</div>
</div>
<!-- Resource Consumption Heatmap -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🔥 Resource Consumption Heatmap</h2>
</div>
<div class="chart-container" style="height: 250px;">
<canvas id="asyncHeatmapChart"></canvas>
</div>
</div>
<!-- Task Performance Analysis -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📊 Task Performance Analysis</h2>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div class="chart-container small">
<canvas id="asyncDurationChart"></canvas>
</div>
<div class="chart-container small">
<canvas id="asyncEfficiencyChart"></canvas>
</div>
</div>
</div>
<!-- Personalized Task Cards -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🎯 Task Details</h2>
<div class="flex-gap-2">
<label class="text-secondary">Sort by:</label>
<select id="taskSortOrder" onchange="Dashboard.tasks.sort()" class="input-sm">
<option value="memory-desc">Memory ↓</option>
<option value="memory-asc">Memory ↑</option>
<option value="peak-desc">Peak ↓</option>
<option value="efficiency-desc">Efficiency ↓</option>
<option value="duration-desc">Duration ↓</option>
<option value="allocations-desc">Allocs ↓</option>
</select>
<select id="taskFilterStatus" onchange="Dashboard.tasks.filter()" class="input-sm">
<option value="all">All Tasks</option>
<option value="active">Active Only</option>
<option value="completed">Completed Only</option>
<option value="leak">Potential Leaks</option>
</select>
</div>
</div>
<div id="taskCardsContainer" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 12px;"></div>
</div>
</div>
<!-- Task Graph Mode -->
<div id="mode-taskgraph" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, #10b98110 0%, #10b98120 100%); border: 2px solid #10b98160;">
<div class="card-header">
<h2 class="card-title" style="color: #10b981;">🌳 Task Relationship Graph</h2>
<span class="badge" style="background: #10b98130; color: #10b981;" id="taskGraphCount">0 tasks</span>
</div>
<div style="background: linear-gradient(135deg, #10b98110 0%, #10b98120 100%); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid #10b98140;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 1.5rem;">🌳</span>
<div>
<div style="font-weight: 600; color: #10b981;">Task Hierarchy Visualization</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Click on a task to see its memory usage • Tree view shows parent-child relationships
</div>
</div>
</div>
</div>
<div class="graph-container" id="taskGraphContainer" style="border: 1px solid var(--border); border-radius: 8px; background: var(--bg); min-height: 400px;">
<div id="taskGraphEmptyState" class="empty-state" style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; padding: 40px;">
<div style="font-size: 3rem; margin-bottom: 16px;">🌳</div>
<div style="font-size: 1.1rem; font-weight: 600; margin-bottom: 8px;">No Task Data Available</div>
<div style="color: var(--text2); font-size: 0.9rem; max-width: 400px; text-align: center;">
Task tracking requires integration with TaskIdRegistry. Use the API to spawn tasks and track memory allocations per task.
</div>
</div>
<div id="taskGraphSvg" style="display: none;"></div>
</div>
<div id="taskGraphDetails" class="card" style="display: none; margin-top: 16px; background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%);">
<div class="card-header">
<h2 class="card-title">📊 Task Memory Details</h2>
<button class="btn-sm" onclick="document.getElementById('taskGraphDetails').style.display='none'">✕ Close</button>
</div>
<div id="taskGraphDetailsContent"></div>
</div>
</div>
</div>
<!-- Variable Mode -->
<div id="mode-variable" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, var(--bg2) 0%, var(--bg3) 100%);">
<div class="card-header">
<h2 class="card-title">🔗 Variable Relationship Graph</h2>
<span class="badge badge-info" id="relationshipCount">0 relationships</span>
</div>
<div style="background: linear-gradient(135deg, #3b82f610 0%, #3b82f620 100%); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid #3b82f640;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 1.5rem;">🕸️</span>
<div>
<div style="font-weight: 600; color: var(--primary);">Ownership & Borrowing Visualization</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Drag nodes to explore • Red dashed lines = circular references • Node size = allocation size
</div>
</div>
</div>
</div>
<div class="flex-gap-4 flex-wrap mb-3">
<div class="legend-item"><div class="legend-color" style="background: #10b981; width: 12px; height: 12px; border-radius: 50%;"></div>Owner/Clone</div>
<div class="legend-item"><div class="legend-color" style="background: #3b82f6; width: 12px; height: 12px; border-radius: 50%;"></div>Immutable &</div>
<div class="legend-item"><div class="legend-color" style="background: #f59e0b; width: 12px; height: 12px; border-radius: 50%;"></div>Mutable &mut</div>
<div class="legend-item"><div class="legend-color" style="background: #8b5cf6; width: 12px; height: 12px; border-radius: 50%;"></div>Arc/Rc</div>
<div class="legend-item"><div class="legend-color" style="background: #06b6d4; width: 12px; height: 12px; border-radius: 50%;"></div>Variable Evolution</div>
<div class="legend-item"><div class="legend-color" style="background: #ef4444; width: 20px; height: 2px; border-style: dashed;"></div>Circular Ref</div>
</div>
<div class="graph-container" id="relationshipGraph" style="border: 1px solid var(--border); border-radius: 8px; background: var(--bg);"></div>
<div style="display: flex; gap: 8px; margin-top: 8px;">
<button class="btn-sm" onclick="Dashboard.variableGraph.resetZoom()">🔍 Reset View</button>
<button class="btn-sm" onclick="Dashboard.variableGraph.highlightCycles()">🔴 Highlight Cycles</button>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Relationship List</h2>
<div class="flex-gap-3">
<span class="badge badge-info" id="relationshipCount2">0 relationships</span>
<select id="relSortOrder" onchange="Dashboard.relationships.sort()" class="input-sm">
<option value="strength-desc">Strength ↓</option>
<option value="strength-asc">Strength ↑</option>
<option value="type">Type A-Z</option>
<option value="source">Source A-Z</option>
</select>
<button class="btn-sm" onclick="Dashboard.relationships.toggle()" id="toggleRelBtn">▼ Expand</button>
</div>
</div>
<div class="table-wrapper table-scroll-sm" id="relTableWrapper">
<table>
<thead>
<tr>
<th>Source</th>
<th>Target</th>
<th>Relationship</th>
<th>Type</th>
<th>Strength</th>
</tr>
</thead>
<tbody id="relationshipsBody"></tbody>
</table>
</div>
</div>
</div>
<!-- Passport Mode -->
<div id="mode-passport" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, #8b5cf610 0%, #8b5cf620 100%); border: 2px solid #8b5cf6;">
<div class="card-header">
<h2 class="card-title" style="color: #8b5cf6;">🛂 Memory Passport™</h2>
<span class="badge" style="background: #8b5cf630; color: #8b5cf6;" id="passportBadge">0 passports</span>
</div>
<div style="background: var(--bg2); border-radius: 8px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 2rem;">💡</span>
<div>
<div style="font-weight: 600; color: var(--primary);">What is Memory Passport?</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Every memory allocation gets a "passport" that tracks its entire lifecycle - from birth to death.
Like a visa, it records every border crossing between Rust and FFI, detecting potential leaks before they happen.
</div>
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div class="detail-panel" style="border-left: 4px solid #10b981; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">✅ Clean</div>
<div class="stat-value" style="color: #10b981; font-size: 1.5rem;" id="cleanPassCount">{{clean_passport_count}}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">⚡ Active</div>
<div class="stat-value" style="color: #f59e0b; font-size: 1.5rem;" id="activePassCount">{{active_passport_count}}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #ef4444; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🚨 Leaked</div>
<div class="stat-value" style="color: #ef4444; font-size: 1.5rem;" id="leakedPassCount">{{leaked_passport_count}}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #8b5cf6; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🔗 FFI Tracked</div>
<div class="stat-value" style="color: #8b5cf6; font-size: 1.5rem;" id="ffiPassCount">{{ffi_tracked_count}}</div>
</div>
</div>
<div class="tab-nav">
<button class="tab-btn active" onclick="showTab('passport-overview')">📊 Risk Analysis</button>
<button class="tab-btn" onclick="showTab('passport-list')">📋 Passport List</button>
<button class="tab-btn" onclick="showTab('passport-timeline')">⏱️ Timeline</button>
</div>
<div id="tab-passport-overview" class="tab-content active">
<div class="chart-container">
<canvas id="passportChart"></canvas>
</div>
<div class="grid-auto-lg gap-4 mt-4">
<div class="detail-panel" style="border-left: 4px solid var(--danger);">
<div class="detail-label">🔴 High Risk</div>
<div class="stat-value risk-high" id="highRiskCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--warning);">
<div class="detail-label">🟡 Medium Risk</div>
<div class="stat-value risk-medium" id="mediumRiskCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--success);">
<div class="detail-label">🟢 Low Risk</div>
<div class="stat-value risk-low" id="lowRiskCount">0</div>
</div>
</div>
</div>
<div id="tab-passport-list" class="tab-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span class="text-secondary" style="font-size: 0.85rem;">Click to expand each passport for details</span>
<button class="btn-sm" onclick="toggleSection('passportCardContainer', this)">▼ Expand All</button>
</div>
<div id="passportCardContainer" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 16px; max-height: 600px; overflow-y: auto;"></div>
</div>
<div id="tab-passport-timeline" class="tab-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<span class="text-secondary" style="font-size: 0.85rem;">Timeline of passport events</span>
<button class="btn-sm" onclick="toggleSection('passportTimeline', this)">▼ Expand All</button>
</div>
<div id="passportTimeline" class="timeline" style="max-height: 500px; overflow-y: auto;"></div>
</div>
</div>
</div>
<!-- Time Travel Mode -->
<div id="mode-timetravel" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, #8b5cf610 0%, #8b5cf620 100%); border: 2px solid #8b5cf660;">
<div class="card-header">
<h2 class="card-title" style="color: #8b5cf6;">⏪ Time Travel Debugging</h2>
<span class="badge" style="background: #8b5cf630; color: #8b5cf6;">Interactive Timeline</span>
</div>
<div style="background: var(--bg2); border-radius: 8px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 2rem;">🎯</span>
<div>
<div style="font-weight: 600; color: var(--primary);">Time Travel Debugging</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Navigate through memory events like a time machine. Use the slider to explore allocation history.
</div>
</div>
</div>
</div>
<!-- Stats Summary -->
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div class="detail-panel" style="border-left: 4px solid #10b981; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">📦 Total Events</div>
<div class="stat-value" style="color: #10b981; font-size: 1.25rem;" id="ttTotalEvents">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #3b82f6; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🟢 Active</div>
<div class="stat-value" style="color: #3b82f6; font-size: 1.25rem;" id="ttActiveCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">✅ Freed</div>
<div class="stat-value" style="color: #f59e0b; font-size: 1.25rem;" id="ttFreedCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #ef4444; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🚨 Leaked</div>
<div class="stat-value" style="color: #ef4444; font-size: 1.25rem;" id="ttLeakedCount">0</div>
</div>
</div>
</div>
<!-- Allocation Timeline Chart -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📈 Allocation Timeline</h2>
</div>
<div class="chart-container" style="height: 200px;">
<canvas id="ttTimelineChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">🔍 Event Browser</h2>
<div class="flex-gap-2">
<input type="text" class="search-box" placeholder="🔍 Search..." id="timetravelSearch" oninput="Dashboard.timetravel.filter()" style="width: 200px;">
<select id="ttSortOrder" onchange="Dashboard.timetravel.sort()" class="input-sm">
<option value="time-asc">⏱️ Oldest First</option>
<option value="time-desc">⏱️ Newest First</option>
<option value="size-desc">📦 Largest</option>
<option value="lifetime-desc">⏳ Longest Life</option>
</select>
<button class="btn-sm" onclick="toggleTable('timetravelTableWrapper', this)">▼ Expand</button>
</div>
</div>
<div class="table-wrapper" id="timetravelTableWrapper" style="max-height: 400px; overflow-y: auto;">
<table>
<thead>
<tr>
<th>Address</th>
<th>Variable</th>
<th>Size</th>
<th>Source</th>
<th>Lifetime</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody id="timetravelBody"></tbody>
</table>
</div>
</div>
<div id="timetravelDetail" class="card" style="display: none; background: linear-gradient(135deg, var(--bg) 0%, var(--bg3) 100%); border: 2px solid var(--primary);">
<div class="card-header">
<h3 style="color: var(--primary); margin: 0;">📍 Allocation Details: <span id="ttAddress"></span></h3>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; margin-bottom: 16px;">
<div class="detail-panel" style="border-left: 4px solid var(--primary);">
<div class="detail-label">Variable</div>
<div id="ttVarName" class="detail-value">-</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--info);">
<div class="detail-label">Type</div>
<div id="ttType" class="detail-value">-</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--warning);">
<div class="detail-label">Size</div>
<div id="ttSize" class="detail-value">-</div>
</div>
<div class="detail-panel" style="border-left: 4px solid var(--success);">
<div class="detail-label">Status</div>
<div id="ttStatus" class="detail-value">-</div>
</div>
</div>
<h4 style="color: var(--text2); margin-bottom: 12px;">🔄 Lifecycle Events</h4>
<div class="timeline" id="ttTimeline"></div>
</div>
</div>
<!-- Unsafe/FFI Mode -->
<div id="mode-unsafe" class="mode-section">
<div class="card" style="background: linear-gradient(135deg, #ef444410 0%, #ef444420 100%); border: 2px solid #ef444460;">
<div class="card-header">
<h2 class="card-title" style="color: #ef4444;">🛡️ Memory Safety Guardian</h2>
<span class="badge" style="background: #ef444430; color: #ef4444;" id="unsafeBadge">0 issues</span>
</div>
<div style="background: var(--bg2); border-radius: 8px; padding: 16px; margin-bottom: 16px; border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 2rem;">🔒</span>
<div>
<div style="font-weight: 600; color: var(--primary);">Rust's Promise: Memory Safety</div>
<div style="color: var(--text2); font-size: 0.85rem;">
Track every unsafe block and FFI call. Know exactly where your code steps outside Rust's safety guarantees.
</div>
</div>
</div>
</div>
<!-- Safety Score Gauge -->
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 16px; margin-bottom: 16px;">
<div style="background: var(--bg); border-radius: 8px; padding: 16px; text-align: center;">
<div style="font-size: 0.85rem; color: var(--text2); margin-bottom: 8px;">Safety Score</div>
<div id="safetyGauge" style="position: relative; width: 120px; height: 60px; margin: 0 auto;">
<svg viewBox="0 0 120 60" style="width: 100%; height: 100%;">
<path d="M 10 55 A 50 50 0 0 1 110 55" fill="none" stroke="#374151" stroke-width="8" stroke-linecap="round"/>
<path id="safetyArc" d="M 10 55 A 50 50 0 0 1 110 55" fill="none" stroke="#10b981" stroke-width="8" stroke-linecap="round" stroke-dasharray="157" stroke-dashoffset="0"/>
</svg>
<div style="position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); font-size: 1.5rem; font-weight: bold;" id="safetyScoreValue">100%</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;">
<div class="detail-panel" style="border-left: 4px solid #10b981; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">✅ Safe Code</div>
<div class="stat-value" style="color: #10b981; font-size: 1.25rem;" id="safeCodePercent">{{safe_code_percent}}%</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">⚠️ Unsafe Blocks</div>
<div class="stat-value" style="color: #f59e0b; font-size: 1.25rem;" id="unsafeBlockCount">{{unsafe_count}}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #3b82f6; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🔗 FFI Calls</div>
<div class="stat-value" style="color: #3b82f6; font-size: 1.25rem;" id="ffiCallCount">{{ffi_count}}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #ef4444; text-align: center;">
<div style="font-size: 0.75rem; color: var(--text2);">🚨 Critical</div>
<div class="stat-value" style="color: #ef4444; font-size: 1.25rem;" id="criticalIssueCount">{{high_risk_count}}</div>
</div>
</div>
</div>
</div>
<!-- Risk Heatmap -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🔥 Risk Heatmap</h2>
<div class="flex-gap-2">
<button class="btn-sm" onclick="Dashboard.unsafe.toggleHeatmap('risk')">⚠️ Risk Level</button>
<button class="btn-sm" onclick="Dashboard.unsafe.toggleHeatmap('type')">📦 By Type</button>
</div>
</div>
<div id="unsafeHeatmapContainer" style="min-height: 200px; background: var(--bg); border-radius: 8px; padding: 16px;"></div>
</div>
<!-- Cross-Boundary Trace -->
<div class="card">
<div class="card-header">
<h2 class="card-title">🔀 Cross-Boundary Trace</h2>
<span class="badge badge-info" id="boundaryEventCount">0 events</span>
</div>
<div style="background: linear-gradient(135deg, #8b5cf610 0%, #8b5cf620 100%); border-radius: 8px; padding: 12px; margin-bottom: 16px; border: 1px solid #8b5cf640;">
<div style="color: var(--text2); font-size: 0.85rem;">
<strong style="color: #8b5cf6;">VISA Flow:</strong> Track memory as it crosses between Rust ↔ FFI boundaries. Each crossing is logged like a visa stamp.
</div>
</div>
<div id="crossBoundaryTrace" style="min-height: 150px; background: var(--bg); border-radius: 8px; padding: 16px;">
<div style="display: flex; align-items: center; justify-content: space-around; height: 100%;">
<div style="text-align: center;">
<div style="font-size: 2rem;">🦀</div>
<div style="font-weight: 600; color: #f59e0b;">Rust</div>
<div style="font-size: 0.75rem; color: var(--text2);">Safe Zone</div>
</div>
<div style="flex: 1; display: flex; flex-direction: column; gap: 8px; padding: 0 20px;" id="boundaryEvents">
<div style="text-align: center; color: var(--text2); font-size: 0.85rem;">No boundary events</div>
</div>
<div style="text-align: center;">
<div style="font-size: 2rem;">🌐</div>
<div style="font-weight: 600; color: #3b82f6;">FFI</div>
<div style="font-size: 0.75rem; color: var(--text2);">External</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📊 Risk Distribution</h2>
</div>
<div class="chart-container" style="height: 200px;">
<canvas id="unsafeChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">🎯 Risk Assessment</h2>
</div>
<div class="chart-container small">
<canvas id="unsafeRiskChart"></canvas>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">🔄 Memory Lifecycle Flow</h2>
<span class="badge badge-info" id="lifecycleFlowCount">0 flows</span>
</div>
<div id="lifecycleFlowContainer" style="display: flex; flex-direction: column; gap: 12px;"></div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">🚨 High Risk Operations</h2>
<button class="btn-sm" onclick="toggleSection('unsafeReportsContainer', this)">▼ Expand</button>
</div>
<div id="unsafeReportsContainer" style="max-height: 400px; overflow-y: auto;"></div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 FFI Boundary Events</h2>
<button class="btn-sm" onclick="toggleTable('ffiTableWrapper', this)">▼ Expand</button>
</div>
<div class="table-wrapper" id="ffiTableWrapper" style="max-height: 300px; overflow-y: auto;">
<table>
<thead>
<tr>
<th>Type</th>
<th>From</th>
<th>To</th>
<th>Timestamp</th>
<th>Direction</th>
</tr>
</thead>
<tbody id="ffiTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="tooltip" id="tooltip" style="display: none;"></div>
<script>
const data = {{{json_data}}};
const Utils = {
formatBytes(bytes) {
if (!bytes || bytes === 0) 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(2)) + ' ' + sizes[i];
},
formatTime(ns) {
if (!ns || ns < 1000) return ns + ' ns';
if (ns < 1000000) return (ns / 1000).toFixed(2) + ' μs';
if (ns < 1000000000) return (ns / 1000000).toFixed(2) + ' ms';
return (ns / 1000000000).toFixed(2) + ' s';
},
formatThreadId(tid) {
if (!tid) return 'N/A';
const str = String(tid);
const match = str.match(/(\d+)/);
return match ? 'T' + match[1].slice(-4) : str.slice(-6);
},
getChartColors() {
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
return {
primary: isDark ? '#60a5fa' : '#2563eb',
text: isDark ? '#f1f5f9' : '#1f2937',
text2: isDark ? '#94a3b8' : '#6b7280',
grid: isDark ? '#334155' : '#e5e7eb'
};
}
};
const Dashboard = {
charts: {
instances: {},
init() {
Object.values(this.instances).forEach(chart => chart?.destroy?.());
this.instances = {};
const colors = Utils.getChartColors();
const allocations = data.allocations || [];
const threads = data.threads || [];
const passports = data.passport_details || [];
const unsafeReports = data.unsafe_reports || [];
const memoryCtx = document.getElementById('memoryChart');
if (memoryCtx) {
this.instances.memory = new Chart(memoryCtx, { type: 'bar', data: { labels: ['Active', 'Freed', 'Leaked'], datasets: [{ data: [data.active_allocations || 0, (data.total_allocations || 0) - (data.active_allocations || 0), data.leak_count || 0], backgroundColor: [colors.primary, '#10b981', '#ef4444'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } }, x: { grid: { display: false }, ticks: { color: colors.text2 } } } } });
}
const typeCtx = document.getElementById('typeChart');
if (typeCtx) {
const typeCount = {};
allocations.forEach(a => { const type = a.type_name || 'Unknown'; typeCount[type] = (typeCount[type] || 0) + 1; });
this.instances.type = new Chart(typeCtx, { type: 'doughnut', data: { labels: Object.keys(typeCount), datasets: [{ data: Object.values(typeCount), backgroundColor: Object.keys(typeCount).map((_, i) => `hsl(${i * 360 / Math.max(Object.keys(typeCount).length, 1)}, 70%, 50%)`) }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: colors.text } } } } });
}
const threadCtx = document.getElementById('threadChart');
if (threadCtx) {
const shortThreadIds = threads.map(t => {
const tid = t.thread_id || '?';
const match = String(tid).match(/(\d+)/);
return match ? 'T' + match[1].slice(-4) : String(tid).slice(-6);
});
this.instances.thread = new Chart(threadCtx, { type: 'bar', data: { labels: shortThreadIds, datasets: [{ label: 'Allocations', data: threads.map(t => parseInt(t.allocation_count) || 0), backgroundColor: colors.primary }] }, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', scales: { y: { grid: { display: false }, ticks: { color: colors.text2 } }, x: { grid: { color: colors.grid }, ticks: { color: colors.text2 } } } } });
}
const asyncCtx = document.getElementById('asyncChart');
if (asyncCtx) {
const asyncTasks = data.async_tasks || [];
const asyncSummary = data.async_summary || {};
if (asyncTasks.length > 0) {
const sortedTasks = [...asyncTasks].sort((a, b) => a.task_id - b.task_id);
const timeLabels = sortedTasks.map((t, i) => `T+${i * 100}ms`);
const cpuUsage = sortedTasks.map(t => Math.min(100, (t.total_allocations || 0) * 5));
const memoryUsage = sortedTasks.map(t => Math.min(100, ((t.peak_memory || 0) / 1024 / 1024) * 2));
const ioUsage = sortedTasks.map(t => Math.min(100, ((t.total_bytes || 0) / 1024) * 0.5));
this.instances.async = new Chart(asyncCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [
{ label: 'CPU Usage %', data: cpuUsage, borderColor: '#3b82f6', backgroundColor: '#3b82f633', fill: false, tension: 0.4 },
{ label: 'Memory %', data: memoryUsage, borderColor: '#10b981', backgroundColor: '#10b98133', fill: false, tension: 0.4 },
{ label: 'IO Usage %', data: ioUsage, borderColor: '#f59e0b', backgroundColor: '#f59e0b33', fill: false, tension: 0.4 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: true, position: 'top', labels: { color: colors.text } }, title: { display: true, text: `Resource Consumption Over Time | Tasks: ${asyncSummary.total_tasks || 0}`, color: colors.text2 } },
scales: { y: { min: 0, max: 100, grid: { color: colors.grid }, ticks: { color: colors.text2, callback: (v) => v + '%' }, title: { display: true, text: 'Resource Usage %', color: colors.text2 } }, x: { grid: { display: false }, ticks: { color: colors.text2 }, title: { display: true, text: 'Time', color: colors.text2 } } }
}
});
} else {
const allocData = allocations.slice(0, 15).map((a, i) => ({ label: a.var_name || a.type_name || `Alloc ${i}`, value: parseInt(a.size) || 0 }));
this.instances.async = new Chart(asyncCtx, { type: 'line', data: { labels: allocData.map(d => d.label), datasets: [{ label: 'Memory', data: allocData.map(d => d.value), borderColor: colors.primary, backgroundColor: colors.primary + '33', fill: true, tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } }, x: { grid: { display: false }, ticks: { color: colors.text2, maxRotation: 45 } } } } });
}
}
const unsafeCtx = document.getElementById('unsafeChart');
if (unsafeCtx) {
const riskCount = { high: 0, medium: 0, low: 0 };
unsafeReports.forEach(r => { const level = r.risk_level || 'low'; riskCount[level] = (riskCount[level] || 0) + 1; });
this.instances.unsafe = new Chart(unsafeCtx, { type: 'bar', data: { labels: ['High Risk', 'Medium Risk', 'Low Risk'], datasets: [{ data: [riskCount.high, riskCount.medium, riskCount.low], backgroundColor: ['#ef4444', '#f59e0b', '#10b981'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } }, x: { grid: { display: false }, ticks: { color: colors.text2 } } } } });
}
const unsafeRiskCtx = document.getElementById('unsafeRiskChart');
if (unsafeRiskCtx) {
const ffiCount = unsafeReports.filter(r => r.ffi_tracked).length;
this.instances.unsafeRisk = new Chart(unsafeRiskCtx, { type: 'doughnut', data: { labels: ['FFI Tracked', 'Native'], datasets: [{ data: [ffiCount, unsafeReports.length - ffiCount], backgroundColor: [colors.primary, '#10b981'] }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom', labels: { color: colors.text } } } } });
}
// Time Travel Timeline Chart
const ttTimelineCtx = document.getElementById('ttTimelineChart');
if (ttTimelineCtx && allocations.length > 0) {
// Sort allocations by timestamp
const sortedAllocs = [...allocations].sort((a, b) => (a.timestamp_alloc || 0) - (b.timestamp_alloc || 0));
// Group by time intervals (every 10% of time range)
const minTime = Math.min(...sortedAllocs.map(a => (a.timestamp_alloc || 0) / 1000000));
const maxTime = Math.max(...sortedAllocs.map(a => ((a.timestamp_alloc || 0) / 1000000) + (a.lifetime_ms || 0)));
const timeRange = maxTime - minTime || 1;
const interval = timeRange / 10;
const timeLabels = [];
const allocCounts = [];
const memoryBytes = [];
const freedCounts = [];
for (let i = 0; i <= 10; i++) {
const t = minTime + i * interval;
timeLabels.push(`T+${Math.round(t - minTime)}ms`);
let count = 0;
let mem = 0;
let freed = 0;
sortedAllocs.forEach(a => {
const allocMs = (a.timestamp_alloc || 0) / 1000000;
const deallocMs = (a.timestamp_dealloc || 0) / 1000000;
if (allocMs <= t) {
count++;
mem += parseInt(a.size) || 0;
}
if (deallocMs > 0 && deallocMs <= t) {
freed++;
}
});
allocCounts.push(count);
memoryBytes.push(mem / 1024); // KB
freedCounts.push(freed);
}
this.instances.ttTimeline = new Chart(ttTimelineCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [
{
label: 'Active Allocations',
data: allocCounts.map((a, i) => a - freedCounts[i]),
borderColor: '#3b82f6',
backgroundColor: '#3b82f633',
fill: false,
tension: 0.4,
yAxisID: 'y'
},
{
label: 'Memory (KB)',
data: memoryBytes,
borderColor: '#10b981',
backgroundColor: '#10b98133',
fill: false,
tension: 0.4,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: true, position: 'top', labels: { color: colors.text } },
title: { display: true, text: 'Memory Timeline', color: colors.text2 }
},
scales: {
y: {
type: 'linear',
display: true,
position: 'left',
grid: { color: colors.grid },
ticks: { color: colors.text2 },
title: { display: true, text: 'Allocations', color: colors.text2 }
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: { drawOnChartArea: false },
ticks: { color: colors.text2 },
title: { display: true, text: 'Memory (KB)', color: colors.text2 }
},
x: {
grid: { display: false },
ticks: { color: colors.text2 }
}
}
}
});
}
}
},
diagnosis: {
init() {
const container = document.getElementById('diagnosisContent');
if (!container) return;
const diagnoses = [];
const allocations = data.allocations || [];
const passports = data.passport_details || [];
const unsafeReports = data.unsafe_reports || [];
const relationships = data.relationships || [];
const leakCount = data.leak_count || 0;
const unsafeCount = unsafeReports.length;
const ffiCount = passports.filter(p => p.ffi_tracked).length;
const highRiskCount = unsafeReports.filter(r => r.risk_level === 'high').length;
if (leakCount > 0) {
diagnoses.push({
level: 'critical',
icon: '🔴',
title: 'Memory Leak Detected',
desc: `${leakCount} allocation${leakCount > 1 ? 's' : ''} not freed - potential memory leak`
});
}
const cycleCount = relationships.filter(r => r.is_part_of_cycle).length;
if (cycleCount > 0) {
diagnoses.push({
level: 'critical',
icon: '🔄',
title: 'Reference Cycle Detected',
desc: `${cycleCount} circular reference${cycleCount > 1 ? 's' : ''} found - may cause memory leak`
});
}
if (highRiskCount > 0) {
diagnoses.push({
level: 'warning',
icon: '⚠️',
title: 'High Risk Unsafe Operations',
desc: `${highRiskCount} high-risk unsafe operation${highRiskCount > 1 ? 's' : ''} detected`
});
}
const ffiPassports = passports.filter(p => p.ffi_tracked).length;
if (ffiPassports > 0) {
const leakedFfi = passports.filter(p => p.ffi_tracked && p.is_leaked).length;
if (leakedFfi > 0) {
diagnoses.push({
level: 'warning',
icon: '🔗',
title: 'FFI Memory Escape',
desc: `${leakedFfi} allocation${leakedFfi > 1 ? 's' : ''} leaked across FFI boundary`
});
} else {
diagnoses.push({
level: 'info',
icon: '🔗',
title: 'FFI Boundary Tracking Active',
desc: `${ffiPassports} allocation${ffiPassports > 1 ? 's' : ''} tracked across FFI boundaries`
});
}
}
const largeAllocs = allocations.filter(a => (parseInt(a.size) || 0) > 1024 * 1024);
if (largeAllocs.length > 0) {
diagnoses.push({
level: 'warning',
icon: '📦',
title: 'Large Allocations Found',
desc: `${largeAllocs.length} allocation${largeAllocs.length > 1 ? 's' : ''} larger than 1MB detected`
});
}
const healthScore = data.health_score || 0;
if (diagnoses.length === 0) {
if (healthScore >= 80) {
diagnoses.push({
level: 'success',
icon: '✅',
title: 'Memory Health Excellent',
desc: `Health score: ${healthScore} - No critical issues detected`
});
} else {
diagnoses.push({
level: 'info',
icon: '✅',
title: 'No Critical Issues',
desc: `Health score: ${healthScore} - Memory tracking looks healthy`
});
}
}
const badge = document.getElementById('diagnosisBadge');
const criticalCount = diagnoses.filter(d => d.level === 'critical').length;
if (criticalCount > 0) {
badge.textContent = `${criticalCount} Critical Issue${criticalCount > 1 ? 's' : ''}`;
badge.style.background = 'var(--danger)';
} else {
const warningCount = diagnoses.filter(d => d.level === 'warning').length;
if (warningCount > 0) {
badge.textContent = `${warningCount} Warning${warningCount > 1 ? 's' : ''}`;
badge.style.background = 'var(--warning)';
} else {
badge.textContent = 'All Clear';
badge.style.background = 'var(--success)';
}
}
container.innerHTML = diagnoses.map(d => `
<div class="diagnosis-item ${d.level}">
<span class="diagnosis-icon">${d.icon}</span>
<div class="diagnosis-content">
<div class="diagnosis-title">${d.title}</div>
<div class="diagnosis-desc">${d.desc}</div>
</div>
</div>
`).join('');
}
},
variableGraph: {
simulation: null,
svg: null,
zoom: null,
g: null,
highlightMode: false,
init() {
const container = document.getElementById('relationshipGraph');
if (!container) return;
container.innerHTML = '';
const relationships = data.relationships || [];
if (relationships.length === 0) { container.innerHTML = '<div class="empty-state">No relationship data</div>'; return; }
document.getElementById('relationshipCount').textContent = relationships.length + ' relationships';
const width = container.clientWidth;
const height = container.clientHeight || 400;
this.svg = d3.select(container).append('svg').attr('width', '100%').attr('height', height).attr('viewBox', [0, 0, width, height]);
this.zoom = d3.zoom().on('zoom', (event) => { this.g.attr('transform', event.transform); });
this.svg.call(this.zoom);
this.g = this.svg.append('g');
this.svg.append('defs').append('marker').attr('id', 'arrowhead').attr('viewBox', '-0 -5 10 10').attr('refX', 20).attr('refY', 0).attr('orient', 'auto').attr('markerWidth', 6).attr('markerHeight', 6).append('path').attr('d', 'M 0,-5 L 10,0 L 0,5').attr('fill', '#64748b');
const nodeMap = new Map();
relationships.forEach((r) => {
const sourceId = r.source_ptr;
const targetId = r.target_ptr;
const isSourceContainer = r.is_container_source || false;
const isTargetContainer = r.is_container_target || false;
if (!nodeMap.has(sourceId)) nodeMap.set(sourceId, { id: sourceId, ptr: sourceId, name: r.source_var_name || ('Ptr ' + sourceId.slice(-6)), type: r.relationship_type, typeName: r.type_name || 'unknown', isSource: true, hasCycle: false, inDegree: 0, outDegree: 0, isContainer: isSourceContainer });
if (!nodeMap.has(targetId)) nodeMap.set(targetId, { id: targetId, ptr: targetId, name: r.target_var_name || ('Ptr ' + targetId.slice(-6)), type: r.relationship_type, typeName: r.type_name || 'unknown', isSource: false, hasCycle: false, inDegree: 0, outDegree: 0, isContainer: isTargetContainer });
});
const nodes = Array.from(nodeMap.values());
const nodeIds = new Set(nodes.map(n => n.id));
const links = relationships.map(r => ({ source: r.source_ptr, target: r.target_ptr, type: r.relationship_type || 'reference', strength: r.strength || 0.5, typeName: r.type_name || 'unknown', isPartOfCycle: r.is_part_of_cycle || false })).filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
links.forEach(l => {
if (l.isPartOfCycle) {
const srcNode = nodeMap.get(l.source.id || l.source);
const tgtNode = nodeMap.get(l.target.id || l.target);
if (srcNode) srcNode.hasCycle = true;
if (tgtNode) tgtNode.hasCycle = true;
}
const srcId = l.source.id || l.source;
const tgtId = l.target.id || l.target;
const srcNode = nodeMap.get(srcId);
const tgtNode = nodeMap.get(tgtId);
if (srcNode) srcNode.outDegree++;
if (tgtNode) tgtNode.inDegree++;
});
const colorMap = { clone: '#10b981', ownership_transfer: '#dc2626', immutable_borrow: '#3b82f6', mutable_borrow: '#f59e0b', Arc: '#8b5cf6', Rc: '#8b5cf6', reference: '#64748b', evolution: '#06b6d4', contains: '#f59e0b' };
const typeLabels = { clone: 'Clone', ownership_transfer: 'Ownership Transfer', immutable_borrow: 'Immutable Borrow (&)', mutable_borrow: 'Mutable Borrow (&mut)', Arc: 'Arc Smart Pointer', Rc: 'Rc Smart Pointer', reference: 'Reference', evolution: 'Variable Evolution', contains: 'Contains' };
const getColor = (type) => colorMap[type] || '#64748b';
const getTypeLabel = (type) => typeLabels[type] || type;
const getLinkColor = (d) => d.isPartOfCycle ? '#ef4444' : getColor(d.type);
this.simulation = d3.forceSimulation(nodes).force('link', d3.forceLink(links).id(d => d.id).distance(100)).force('charge', d3.forceManyBody().strength(-300)).force('center', d3.forceCenter(width / 2, height / 2)).force('collision', d3.forceCollide().radius(35));
const link = this.g.append('g').selectAll('line').data(links).join('line').attr('class', 'link').attr('stroke', getLinkColor).attr('stroke-width', d => Math.max(1.5, d.strength * 3)).attr('stroke-dasharray', d => d.isPartOfCycle ? '6,3' : null).attr('marker-end', 'url(#arrowhead)');
const node = this.g.append('g').selectAll('g').data(nodes).join('g').attr('class', 'node').call(d3.drag().on('start', (event) => { if (!event.active) this.simulation.alphaTarget(0.3).restart(); event.subject.fx = event.subject.x || 0; event.subject.fy = event.subject.y || 0; }).on('drag', (event) => { event.subject.fx = event.x || 0; event.subject.fy = event.y || 0; }).on('end', (event) => { if (!event.active) this.simulation.alphaTarget(0); event.subject.fx = null; event.subject.fy = null; }));
node.append('circle').attr('r', d => d.hasCycle ? 18 : 14).attr('fill', d => getColor(d.type)).attr('stroke', d => d.hasCycle ? '#ef4444' : 'none').attr('stroke-width', 3);
node.append('text').attr('dx', 20).attr('dy', 4).text(d => { const name = d.name || 'unknown'; return name.length > 14 ? name.substring(0, 12) + '..' : name; }).style('font-size', '12px').style('fill', 'var(--text)').style('font-weight', '500');
const tooltip = document.getElementById('tooltip');
node.on('mouseover', (event, d) => {
tooltip.style.display = 'block';
const totalConnections = d.inDegree + d.outDegree;
tooltip.innerHTML = `<div style="min-width: 180px;">
<div style="font-weight: 600; font-size: 14px; margin-bottom: 8px; color: var(--primary);">${d.name || 'Unknown'}</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 4px; font-size: 12px;">
<div style="color: var(--text2);">Type:</div><div>${d.typeName || 'unknown'}</div>
<div style="color: var(--text2);">Node Kind:</div><div style="color: ${d.isContainer ? '#f59e0b' : '#64748b'};">${d.isContainer ? 'Container' : 'HeapOwner'}</div>
<div style="color: var(--text2);">Relation:</div><div style="color: ${getColor(d.type)};">${getTypeLabel(d.type)}</div>
<div style="color: var(--text2);">Incoming:</div><div>${d.inDegree} links</div>
<div style="color: var(--text2);">Outgoing:</div><div>${d.outDegree} links</div>
<div style="color: var(--text2);">Total:</div><div>${totalConnections} connections</div>
</div>
${d.hasCycle ? '<div style="margin-top: 8px; color: #ef4444; font-size: 11px;">⚠️ Part of reference cycle</div>' : ''}
</div>`;
tooltip.style.left = (event.pageX + 15) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}).on('mouseout', () => { tooltip.style.display = 'none'; });
this.simulation.on('tick', () => { link.attr('x1', d => d.source?.x || 0).attr('y1', d => d.source?.y || 0).attr('x2', d => d.target?.x || 0).attr('y2', d => d.target?.y || 0); node.attr('transform', d => `translate(${d.x || 0},${d.y || 0})`); });
},
resetZoom() { if (this.svg && this.zoom) this.svg.transition().duration(500).call(this.zoom.transform, d3.zoomIdentity); },
highlightCycles() { this.highlightMode = !this.highlightMode; if (!this.g) return; this.g.selectAll('.node circle').attr('stroke-width', d => this.highlightMode && d.hasCycle ? 5 : d.hasCycle ? 3 : 0); this.g.selectAll('.link').attr('stroke-width', d => this.highlightMode && d.isPartOfCycle ? 4 : Math.max(1.5, d.strength * 3)); }
},
allocations: {
showAll: false,
currentPage: 1,
pageSize: 100,
filteredData: [],
toggle() {
const wrapper = document.getElementById('allocationsWrapper');
const btn = document.getElementById('toggleAllocBtn');
const pagination = document.getElementById('allocationsPagination');
this.showAll = !this.showAll;
if (this.showAll) {
wrapper.style.maxHeight = 'none';
wrapper.style.overflow = 'visible';
btn.textContent = '▲ Collapse';
if (this.filteredData.length > this.pageSize) pagination.style.display = 'flex';
} else {
wrapper.style.maxHeight = '400px';
wrapper.style.overflow = 'auto';
btn.textContent = '▼ Expand';
pagination.style.display = 'none';
this.currentPage = 1;
}
this.render();
},
sort() {
const sortEl = document.getElementById('allocSortOrder');
if (!sortEl) return;
const sortBy = sortEl.value;
this.filteredData.sort((a, b) => {
switch (sortBy) {
case 'size-desc':
return (b.size || 0) - (a.size || 0);
case 'size-asc':
return (a.size || 0) - (b.size || 0);
case 'time-desc':
return (b.timestamp || 0) - (a.timestamp || 0);
case 'time-asc':
return (a.timestamp || 0) - (b.timestamp || 0);
case 'type':
return (a.type_name || '').localeCompare(b.type_name || '');
case 'var':
return (a.var_name || '').localeCompare(b.var_name || '');
default:
return 0;
}
});
this.renderPage(this.currentPage);
},
renderPage(page) {
const start = (page - 1) * this.pageSize;
const end = start + this.pageSize;
const pageData = this.filteredData.slice(start, end);
const tbody = document.getElementById('allocationsBody');
tbody.innerHTML = pageData.map(a =>
`<tr><td><a href="#" onclick="Dashboard.timetravel.showDetail('${a.address}'); return false;">${a.address}</a></td><td>${Utils.formatBytes(parseInt(a.size) || 0)}</td><td>${a.var_name || 'unknown'}</td><td>${a.type_name || 'Unknown'}</td><td><span class="badge ${a.is_leaked ? 'badge-danger' : 'badge-success'}">${a.is_leaked ? 'Leaked' : 'Normal'}</span></td><td>${Utils.formatTime(parseInt(a.timestamp) || 0)}</td></tr>`
).join('');
const totalPages = Math.ceil(this.filteredData.length / this.pageSize);
document.getElementById('allocPageInfo').textContent = `Page ${page} of ${totalPages} (${this.filteredData.length} total)`;
document.getElementById('allocPrevBtn').disabled = page <= 1;
document.getElementById('allocNextBtn').disabled = page >= totalPages;
},
prevPage() {
if (this.currentPage > 1) this.renderPage(this.currentPage - 1);
},
nextPage() {
const totalPages = Math.ceil(this.filteredData.length / this.pageSize);
if (this.currentPage < totalPages) this.renderPage(this.currentPage + 1);
},
render() {
this.renderPage(this.currentPage);
},
init() {
const allocations = data.allocations || [];
this.filteredData = [...allocations];
if (allocations.length === 0) {
document.getElementById('allocationsBody').innerHTML = '<tr><td colspan="6" class="empty-state">No allocation data</td></tr>';
return;
}
this.sort();
}
},
threads: {
filteredData: [],
heatmapType: 'memory',
sort() {
const sortEl = document.getElementById('threadSortOrder');
if (!sortEl) return;
const sortBy = sortEl.value;
this.filteredData.sort((a, b) => {
switch (sortBy) {
case 'alloc-desc':
return (b.allocation_count || 0) - (a.allocation_count || 0);
case 'alloc-asc':
return (a.allocation_count || 0) - (b.allocation_count || 0);
case 'memory-desc':
return (b.current_memory_bytes || 0) - (a.current_memory_bytes || 0);
case 'memory-asc':
return (a.current_memory_bytes || 0) - (b.current_memory_bytes || 0);
case 'thread-id':
return (a.thread_id || '0').localeCompare(b.thread_id || '0');
default:
return 0;
}
});
this.render();
},
render() {
const tbody = document.getElementById('threadTableBody');
tbody.innerHTML = this.filteredData.map(t => `<tr><td><code title="${t.thread_id || ''}">${Utils.formatThreadId(t.thread_id)}</code></td><td><span class="text-secondary">${t.thread_summary || ''}</span></td><td>${t.allocation_count || 0}</td><td>${t.current_memory || '0 B'}</td><td>${t.peak_memory || '0 B'}</td><td><span class="badge badge-success">active</span></td></tr>`).join('');
this.updateStats();
this.renderHeatmap();
this.renderTimeline();
},
updateStats() {
const threads = this.filteredData;
const activeCount = threads.length;
const totalMemory = threads.reduce((sum, t) => sum + (t.current_memory_bytes || 0), 0);
const peakMemory = threads.reduce((sum, t) => sum + (t.peak_memory_bytes || 0), 0);
const totalAllocs = threads.reduce((sum, t) => sum + (t.allocation_count || 0), 0);
const activeEl = document.getElementById('activeThreadCount');
const totalMemEl = document.getElementById('totalThreadMemory');
const peakMemEl = document.getElementById('peakThreadMemory');
const totalAllocsEl = document.getElementById('totalThreadAllocs');
if (activeEl) activeEl.textContent = activeCount;
if (totalMemEl) totalMemEl.textContent = Utils.formatBytes(totalMemory);
if (peakMemEl) peakMemEl.textContent = Utils.formatBytes(peakMemory);
if (totalAllocsEl) totalAllocsEl.textContent = totalAllocs.toLocaleString();
},
toggleHeatmap(type) {
this.heatmapType = type;
this.renderHeatmap();
},
renderHeatmap() {
const container = document.getElementById('threadHeatmapContainer');
if (!container || this.filteredData.length === 0) {
if (container) container.innerHTML = '<div class="empty-state">No thread data for heatmap</div>';
return;
}
const maxVal = Math.max(...this.filteredData.map(t =>
this.heatmapType === 'memory' ? (t.current_memory_bytes || 0) : (t.allocation_count || 0)
));
container.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px;">
${this.filteredData.map(t => {
const val = this.heatmapType === 'memory' ? (t.current_memory_bytes || 0) : (t.allocation_count || 0);
const intensity = maxVal > 0 ? val / maxVal : 0;
const hue = 120 - (intensity * 120);
const color = `hsl(${hue}, 70%, ${50 + intensity * 20}%)`;
const tid = Utils.formatThreadId(t.thread_id);
return `
<div style="background: ${color}; border-radius: 8px; padding: 12px; text-align: center; cursor: pointer;" title="${tid}: ${this.heatmapType === 'memory' ? Utils.formatBytes(val) : val + ' allocs'}">
<div style="font-weight: 600; font-size: 0.85rem;">${tid}</div>
<div style="font-size: 0.75rem; opacity: 0.9;">${this.heatmapType === 'memory' ? Utils.formatBytes(val) : val}</div>
</div>
`;
}).join('')}
</div>
`;
},
renderTimeline() {
const container = document.getElementById('threadTimelineContainer');
if (!container || this.filteredData.length === 0) {
if (container) container.innerHTML = '<div class="empty-state">No thread timeline data</div>';
return;
}
const maxAllocs = Math.max(...this.filteredData.map(t => t.allocation_count || 0));
container.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
${this.filteredData.slice(0, 10).map(t => {
const width = maxAllocs > 0 ? ((t.allocation_count || 0) / maxAllocs * 100) : 0;
const tid = Utils.formatThreadId(t.thread_id);
return `
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 60px; font-size: 0.85rem; font-weight: 500;">${tid}</div>
<div style="flex: 1; height: 24px; background: var(--bg3); border-radius: 4px; overflow: hidden;">
<div style="width: ${width}%; height: 100%; background: linear-gradient(90deg, #10b981, #3b82f6); border-radius: 4px;"></div>
</div>
<div style="width: 80px; font-size: 0.8rem; color: var(--text2);">${t.allocation_count || 0} allocs</div>
</div>
`;
}).join('')}
</div>
`;
},
init() {
const threads = data.threads || [];
this.filteredData = [...threads];
document.getElementById('threadCount').textContent = threads.length + ' threads';
const tbody = document.getElementById('threadTableBody');
if (threads.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No thread data</td></tr>';
return;
}
this.sort();
}
},
taskGraph: {
init() {
const container = document.getElementById('taskGraphContainer');
const emptyState = document.getElementById('taskGraphEmptyState');
const svgContainer = document.getElementById('taskGraphSvg');
const countBadge = document.getElementById('taskGraphCount');
if (!container) return;
let taskGraphData = null;
try {
if (typeof data.task_graph_json !== 'undefined' && data.task_graph_json) {
taskGraphData = JSON.parse(data.task_graph_json);
}
} catch (e) {
console.error('Failed to parse task_graph_json:', e);
}
if (!taskGraphData || !taskGraphData.nodes || taskGraphData.nodes.length === 0) {
emptyState.style.display = 'flex';
svgContainer.style.display = 'none';
countBadge.textContent = '0 tasks';
return;
}
emptyState.style.display = 'none';
svgContainer.style.display = 'block';
countBadge.textContent = taskGraphData.nodes.length + ' tasks';
this.renderTree(taskGraphData);
},
renderTree(graphData) {
const container = document.getElementById('taskGraphSvg');
if (!container) return;
const width = container.clientWidth || 800;
const height = 400;
container.innerHTML = '';
const svg = d3.select(container)
.append('svg')
.attr('width', '100%')
.attr('height', height)
.attr('viewBox', [0, 0, width, height]);
const nodes = graphData.nodes || [];
const edges = graphData.edges || [];
const childIds = new Set(edges.map(e => e.to));
const rootNodes = nodes.filter(n => !childIds.has(n.id));
const buildTree = (nodeId) => {
const node = nodes.find(n => n.id === nodeId);
if (!node) return null;
const children = edges
.filter(e => e.from === nodeId)
.map(e => buildTree(e.to))
.filter(n => n !== null);
return {
id: node.id,
name: node.name,
memory_usage: node.memory_usage || 0,
allocation_count: node.allocation_count || 0,
status: node.status,
children: children
};
};
const trees = rootNodes.map(n => buildTree(n.id)).filter(t => t !== null);
if (trees.length === 0) {
container.innerHTML = '<div class="empty-state">No valid task hierarchy</div>';
return;
}
this.renderSimpleTree(svg, trees, width, height, 0, 0);
},
renderSimpleTree(svg, trees, width, height, xOffset, yOffset) {
const nodeWidth = 200;
const nodeHeight = 60;
const horizontalGap = 40;
const verticalGap = 80;
let currentX = xOffset;
let currentY = yOffset;
const renderNode = (node, x, y) => {
const g = svg.append('g')
.attr('transform', `translate(${x}, ${y})`)
.style('cursor', 'pointer')
.on('click', () => this.showTaskDetails(node));
g.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 8)
.attr('fill', node.status === 'Running' ? '#10b981' : '#64748b')
.attr('opacity', 0.2);
g.append('rect')
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('rx', 8)
.attr('fill', 'none')
.attr('stroke', node.status === 'Running' ? '#10b981' : '#64748b')
.attr('stroke-width', 2);
g.append('text')
.attr('x', nodeWidth / 2)
.attr('y', 25)
.attr('text-anchor', 'middle')
.attr('fill', 'currentColor')
.style('font-size', '14px')
.style('font-weight', '600')
.text(node.name || `Task ${node.id}`);
g.append('text')
.attr('x', nodeWidth / 2)
.attr('y', 45)
.attr('text-anchor', 'middle')
.attr('fill', 'var(--text2)')
.style('font-size', '12px')
.text(`${Utils.formatBytes(node.memory_usage)} • ${node.allocation_count} allocs`);
return nodeWidth + horizontalGap;
};
const renderTreeRecursive = (node, x, y) => {
const nextX = renderNode(node, x, y);
if (node.children && node.children.length > 0) {
let childX = x;
let childY = y + nodeHeight + verticalGap;
node.children.forEach(child => {
svg.append('line')
.attr('x1', x + nodeWidth / 2)
.attr('y1', y + nodeHeight)
.attr('x2', childX + nodeWidth / 2)
.attr('y2', childY)
.attr('stroke', '#10b981')
.attr('stroke-width', 2)
.attr('stroke-dasharray', '5,5');
childX = renderTreeRecursive(child, childX, childY);
});
}
return nextX;
};
trees.forEach(tree => {
currentX = renderTreeRecursive(tree, currentX, currentY);
currentY += nodeHeight + verticalGap + 50;
});
},
showTaskDetails(node) {
const detailsPanel = document.getElementById('taskGraphDetails');
const detailsContent = document.getElementById('taskGraphDetailsContent');
if (!detailsPanel || !detailsContent) return;
detailsPanel.style.display = 'block';
detailsContent.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px;">
<div class="detail-panel" style="border-left: 4px solid #10b981;">
<div class="detail-label">Task ID</div>
<div class="stat-value">${node.id}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #3b82f6;">
<div class="detail-label">Task Name</div>
<div class="stat-value" style="font-size: 1.2rem;">${node.name || 'N/A'}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b;">
<div class="detail-label">Memory Usage</div>
<div class="stat-value">${Utils.formatBytes(node.memory_usage)}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #8b5cf6;">
<div class="detail-label">Allocations</div>
<div class="stat-value">${node.allocation_count}</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #10b981;">
<div class="detail-label">Status</div>
<div class="stat-value" style="color: ${node.status === 'Running' ? '#10b981' : '#64748b'};">${node.status}</div>
</div>
</div>
`;
}
},
relationships: {
showAll: false,
filteredData: [],
toggle() {
const wrapper = document.getElementById('relTableWrapper');
const btn = document.getElementById('toggleRelBtn');
this.showAll = !this.showAll;
if (this.showAll) {
wrapper.style.maxHeight = 'none';
wrapper.style.overflow = 'visible';
btn.textContent = '▲ Collapse';
} else {
wrapper.style.maxHeight = '300px';
wrapper.style.overflow = 'auto';
btn.textContent = '▼ Expand';
}
},
sort() {
const sortEl = document.getElementById('relSortOrder');
if (!sortEl) return;
const sortBy = sortEl.value;
this.filteredData.sort((a, b) => {
switch (sortBy) {
case 'strength-desc':
return (b.strength || 0.5) - (a.strength || 0.5);
case 'strength-asc':
return (a.strength || 0.5) - (b.strength || 0.5);
case 'type':
return (a.relationship_type || '').localeCompare(b.relationship_type || '');
case 'source':
return (a.source_var_name || '').localeCompare(b.source_var_name || '');
default:
return 0;
}
});
this.render();
},
render() {
const tbody = document.getElementById('relationshipsBody');
tbody.innerHTML = this.filteredData.map(r =>
`<tr><td>${r.source_var_name || ('Ptr ' + r.source_ptr) || 'N/A'}</td><td>${r.target_var_name || ('Ptr ' + r.target_ptr) || 'N/A'}</td><td><span class="badge badge-info">${r.relationship_type || 'reference'}</span></td><td>${r.type_name || 'Unknown'}</td><td>${(((r.strength || 0.5) * 100)).toFixed(0)}%</td></tr>`
).join('');
},
init() {
const relationships = data.relationships || [];
this.filteredData = [...relationships];
document.getElementById('relationshipCount2').textContent = relationships.length + ' relationships';
if (relationships.length === 0) {
document.getElementById('relationshipsBody').innerHTML = '<tr><td colspan="5" class="empty-state">No relationship data</td></tr>';
return;
}
this.sort();
}
},
passports: {
filteredData: [],
sort() {
const sortEl = document.getElementById('passportSortOrder');
if (!sortEl) return;
const sortBy = sortEl.value;
this.filteredData.sort((a, b) => {
switch (sortBy) {
case 'size-desc':
return (b.size_bytes || 0) - (a.size_bytes || 0);
case 'size-asc':
return (a.size_bytes || 0) - (b.size_bytes || 0);
case 'lifetime-desc':
return (b.lifetime_ms || 0) - (a.lifetime_ms || 0);
case 'lifetime-asc':
return (a.lifetime_ms || 0) - (b.lifetime_ms || 0);
default:
return 0;
}
});
this.render();
},
render() {
const container = document.getElementById('passportCardContainer');
const timeline = document.getElementById('passportTimeline');
const riskCount = { high: 0, medium: 0, low: 0 };
const passportCtx = document.getElementById('passportChart');
const chartColors = Utils.getChartColors();
container.innerHTML = this.filteredData.slice(0, 50).map(p => {
const risk = p.risk_level || 'low';
riskCount[risk] = (riskCount[risk] || 0) + 1;
const visaFlow = (p.cross_boundary_events && p.cross_boundary_events.length > 0) ? `<div style="margin-top: 12px; padding: 12px; background: linear-gradient(135deg, #1e3a5f10 0%, #1e3a5f20 100%); border-radius: 8px; border: 1px solid #1e3a5f40;"><div style="font-size: 0.75rem; color: #1e3a5f; margin-bottom: 8px; font-weight: 600; display: flex; align-items: center; gap: 6px;"><span style="font-size: 1rem;">🛂</span> FFI VISA STAMPS - Border Crossings</div><div style="display: flex; flex-wrap: wrap; gap: 8px;">${p.cross_boundary_events.map((e, idx) => `<div style="display: flex; flex-direction: column; align-items: center; padding: 8px 12px; border-radius: 8px; background: white; border: 2px dashed ${e.color || '#10b981'}; min-width: 80px;"><div style="font-size: 1.25rem;">${e.icon || '📍'}</div><div style="font-size: 0.7rem; font-weight: 600; color: #1e293b;">${e.event_type || 'Cross'}</div><div style="font-size: 0.65rem; color: #64748b;">${e.from_context || 'Rust'} → ${e.to_context || 'FFI'}</div></div>`).join('')}</div></div>` : '<div style="margin-top: 12px; padding: 8px 12px; background: var(--bg3); border-radius: 8px; border: 1px dashed var(--border); font-size: 0.8rem; color: var(--text2); display: flex; align-items: center; gap: 8px;"><span>🛂</span> No FFI boundary crossings - stayed in Rust territory</div>';
return `<div class="detail-panel" style="border-left: 4px solid ${p.is_leaked ? '#ef4444' : 'var(--primary)'}; position: relative; overflow: hidden;"><div style="position: absolute; top: 8px; right: 8px; font-size: 0.65rem; padding: 2px 8px; border-radius: 4px; background: ${p.is_leaked ? '#ef444420' : '#10b98120'}; color: ${p.is_leaked ? '#ef4444' : '#10b981'}; font-weight: 600;">${p.is_leaked ? '🔴 LEAKED' : '🟢 VALID'}</div><div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"><span style="font-size: 1.25rem;">🛂</span><h4 style="color: var(--primary); margin: 0;">${p.var_name || 'unknown'}</h4></div><div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 8px;"><div><span class="detail-label">Type</span><div class="detail-value" style="font-family: monospace; font-size: 0.8rem;">${(p.type_name || 'Unknown').substring(0, 25)}${(p.type_name || '').length > 25 ? '...' : ''}</div></div><div><span class="detail-label">Size</span><div class="detail-value">${Utils.formatBytes(parseInt(p.size_bytes) || 0)}</div></div><div><span class="detail-label">Status</span><div class="detail-value">${p.status || 'N/A'}</div></div><div><span class="detail-label">Risk</span><div class="detail-value ${p.risk_level === 'high' ? 'risk-high' : p.risk_level === 'medium' ? 'risk-medium' : ''}">${p.risk_level || 'low'}</div></div></div>${visaFlow}</div>`;
}).join('');
if (timeline) {
timeline.innerHTML = this.filteredData.slice(0, 10).map(p => `
<div class="timeline-item ${p.is_leaked ? 'leaked' : 'active'}">
<strong>${p.var_name || 'unknown'}</strong>
<span class="badge ${p.is_leaked ? 'badge-danger' : 'badge-success'}">${p.status || 'active'}</span>
<div style="color: var(--text2); font-size: 0.85rem;">
${Utils.formatBytes(parseInt(p.size_bytes) || 0)} | ${p.type_name || 'Unknown'}
</div>
</div>
`).join('');
}
document.getElementById('highRiskCount').textContent = riskCount.high;
document.getElementById('mediumRiskCount').textContent = riskCount.medium;
document.getElementById('lowRiskCount').textContent = riskCount.low;
if (passportCtx && !Dashboard.charts.instances.passport) {
Dashboard.charts.instances.passport = new Chart(passportCtx, {
type: 'radar',
data: {
labels: ['Size', 'Lifetime', 'Refs', 'Clones', 'Borrows'],
datasets: [{
label: 'Risk Metrics',
data: [
Math.min(100, this.filteredData.length * 5),
Math.min(100, riskCount.high * 20),
Math.min(100, riskCount.medium * 15),
Math.min(100, riskCount.low * 10),
50
],
borderColor: chartColors.primary,
backgroundColor: chartColors.primary + '20'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
grid: { color: chartColors.grid },
angleLines: { color: chartColors.grid },
ticks: { display: false }
}
},
plugins: { legend: { labels: { color: chartColors.text } } }
}
});
}
},
init() {
const passports = data.passport_details || [];
this.filteredData = [...passports];
document.getElementById('passportBadge').textContent = passports.length + ' passports';
if (passports.length === 0) {
document.getElementById('passportCardContainer').innerHTML = '<div class="empty-state">No passport data</div>';
return;
}
this.render();
}
},
unsafe: {
heatmapType: 'risk',
init() {
const container = document.getElementById('unsafeReportsContainer');
const unsafeReports = data.unsafe_reports || [];
document.getElementById('unsafeBadge').textContent = unsafeReports.length + ' reports';
if (unsafeReports.length === 0) {
container.innerHTML = '<div class="empty-state">✅ No high risk operations detected</div>';
document.getElementById('lifecycleFlowContainer').innerHTML = '<div class="empty-state">No lifecycle data available</div>';
document.getElementById('lifecycleFlowCount').textContent = '0 flows';
return;
}
const highRisk = unsafeReports.filter(r => r.risk_level === 'high').slice(0, 10);
if (highRisk.length === 0) container.innerHTML = '<div class="empty-state">✅ No high risk operations</div>';
else container.innerHTML = highRisk.map(r => `<div class="detail-panel" style="border-left: 4px solid var(--danger); margin-bottom: 12px;"><div style="display: flex; justify-content: space-between; align-items: center;"><h4 style="color: var(--danger); margin: 0;">⚠️ ${r.var_name || 'unknown'}</h4><span class="badge badge-danger">High Risk</span></div><div style="margin-top: 12px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px;"><div><span class="detail-label">Address</span><div class="detail-value">${r.allocation_ptr || 'N/A'}</div></div><div><span class="detail-label">Type</span><div class="detail-value">${r.type_name || 'Unknown'}</div></div><div><span class="detail-label">Size</span><div class="detail-value">${Utils.formatBytes(parseInt(r.size_bytes) || 0)}</div></div><div><span class="detail-label">Status</span><div class="detail-value">${r.status || 'unknown'}</div></div></div>${r.risk_factors && r.risk_factors.length > 0 ? `<div style="margin-top: 12px;"><span class="detail-label">Risk Factors:</span><ul style="margin: 8px 0 0 20px; color: var(--danger);">${r.risk_factors.map(f => `<li>${f}</li>`).join('')}</ul></div>` : ''}</div>`).join('');
const ffiBody = document.getElementById('ffiTableBody');
const ffiEvents = [];
const lifecycleFlows = [];
unsafeReports.forEach(r => { if (r.cross_boundary_events) r.cross_boundary_events.forEach(e => ffiEvents.push({...e, var_name: r.var_name, size_bytes: r.size_bytes})); if (r.lifecycle_events && r.lifecycle_events.length > 0) lifecycleFlows.push({var_name: r.var_name, allocation_ptr: r.allocation_ptr, size_bytes: r.size_bytes, events: r.lifecycle_events}); });
document.getElementById('lifecycleFlowCount').textContent = lifecycleFlows.length + ' flows';
const flowContainer = document.getElementById('lifecycleFlowContainer');
if (lifecycleFlows.length === 0) flowContainer.innerHTML = '<div class="empty-state">No lifecycle flow data available</div>';
else flowContainer.innerHTML = lifecycleFlows.slice(0, 10).map(flow => { const flowSteps = flow.events.map((e, idx) => { const isLast = idx === flow.events.length - 1; const stepColor = e.color || '#64748b'; return `<div style="display: flex; align-items: center; gap: 8px;"><div style="width: 32px; height: 32px; border-radius: 50%; background: ${stepColor}; display: flex; align-items: center; justify-content: center; font-size: 14px;">${e.icon || '•'}</div><div style="flex: 1;"><div style="font-weight: 600; color: var(--text);">${e.context || e.event_type}</div><div style="font-size: 0.8rem; color: var(--text2);">${flow.var_name} | ${Utils.formatBytes(flow.size_bytes || 0)}</div></div>${!isLast ? '<div style="width: 2px; height: 24px; background: var(--border);"></div>' : ''}</div>`; }).join(''); return `<div class="detail-panel" style="border: 1px solid var(--border); border-radius: 8px; padding: 16px;"><div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;"><h4 style="color: var(--primary); margin: 0;">${flow.var_name}</h4><code style="font-size: 0.8rem; color: var(--text2);">${flow.allocation_ptr || 'N/A'}</code></div><div style="display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap;">${flowSteps}</div></div>`; }).join('');
if (ffiEvents.length === 0) ffiBody.innerHTML = '<tr><td colspan="5" class="empty-state">No FFI events</td></tr>';
else ffiBody.innerHTML = ffiEvents.slice(0, 50).map(e => `<tr><td><span class="badge badge-warning">${e.event_type || 'N/A'}</span></td><td>${e.from_context || 'N/A'}</td><td>${e.to_context || 'N/A'}</td><td>${e.timestamp || 0}</td><td>${e.icon || '→'}</td></tr>`).join('');
this.updateSafetyGauge();
this.renderHeatmap();
this.renderCrossBoundaryTrace(ffiEvents);
},
updateSafetyGauge() {
const unsafeReports = data.unsafe_reports || [];
const highRisk = unsafeReports.filter(r => r.risk_level === 'high').length;
const total = unsafeReports.length || 1;
const safetyScore = Math.max(0, 100 - (highRisk / total * 100));
const arc = document.getElementById('safetyArc');
const valueEl = document.getElementById('safetyScoreValue');
if (arc) {
const dashOffset = 157 - (safetyScore / 100 * 157);
arc.style.strokeDashoffset = dashOffset;
arc.style.stroke = safetyScore >= 80 ? '#10b981' : safetyScore >= 50 ? '#f59e0b' : '#ef4444';
}
if (valueEl) {
valueEl.textContent = Math.round(safetyScore) + '%';
valueEl.style.color = safetyScore >= 80 ? '#10b981' : safetyScore >= 50 ? '#f59e0b' : '#ef4444';
}
},
toggleHeatmap(type) {
this.heatmapType = type;
this.renderHeatmap();
},
renderHeatmap() {
const container = document.getElementById('unsafeHeatmapContainer');
const unsafeReports = data.unsafe_reports || [];
if (!container || unsafeReports.length === 0) {
if (container) container.innerHTML = '<div class="empty-state">No unsafe operations data</div>';
return;
}
if (this.heatmapType === 'risk') {
const riskGroups = { high: [], medium: [], low: [] };
unsafeReports.forEach(r => {
const level = r.risk_level || 'low';
riskGroups[level].push(r);
});
container.innerHTML = `
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;">
<div style="background: #ef444420; border: 2px solid #ef4444; border-radius: 8px; padding: 16px;">
<div style="font-weight: 600; color: #ef4444; margin-bottom: 12px;">🚨 High Risk (${riskGroups.high.length})</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
${riskGroups.high.slice(0, 5).map(r => `
<div style="background: #ef444410; border-radius: 4px; padding: 8px; font-size: 0.85rem;">
<div style="font-weight: 500;">${r.var_name || 'unknown'}</div>
<div style="color: var(--text2); font-size: 0.75rem;">${r.type_name || 'Unknown'}</div>
</div>
`).join('')}
</div>
</div>
<div style="background: #f59e0b20; border: 2px solid #f59e0b; border-radius: 8px; padding: 16px;">
<div style="font-weight: 600; color: #f59e0b; margin-bottom: 12px;">⚠️ Medium Risk (${riskGroups.medium.length})</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
${riskGroups.medium.slice(0, 5).map(r => `
<div style="background: #f59e0b10; border-radius: 4px; padding: 8px; font-size: 0.85rem;">
<div style="font-weight: 500;">${r.var_name || 'unknown'}</div>
<div style="color: var(--text2); font-size: 0.75rem;">${r.type_name || 'Unknown'}</div>
</div>
`).join('')}
</div>
</div>
<div style="background: #10b98120; border: 2px solid #10b981; border-radius: 8px; padding: 16px;">
<div style="font-weight: 600; color: #10b981; margin-bottom: 12px;">✅ Low Risk (${riskGroups.low.length})</div>
<div style="display: flex; flex-direction: column; gap: 8px;">
${riskGroups.low.slice(0, 5).map(r => `
<div style="background: #10b98110; border-radius: 4px; padding: 8px; font-size: 0.85rem;">
<div style="font-weight: 500;">${r.var_name || 'unknown'}</div>
<div style="color: var(--text2); font-size: 0.75rem;">${r.type_name || 'Unknown'}</div>
</div>
`).join('')}
</div>
</div>
</div>
`;
} else {
const typeCount = {};
unsafeReports.forEach(r => {
const type = r.type_name || 'Unknown';
typeCount[type] = (typeCount[type] || 0) + 1;
});
const sortedTypes = Object.entries(typeCount).sort((a, b) => b[1] - a[1]);
const maxCount = sortedTypes[0]?.[1] || 1;
container.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
${sortedTypes.slice(0, 10).map(([type, count]) => {
const width = (count / maxCount) * 100;
return `
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 150px; font-size: 0.85rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="${type}">${type}</div>
<div style="flex: 1; height: 24px; background: var(--bg3); border-radius: 4px; overflow: hidden;">
<div style="width: ${width}%; height: 100%; background: linear-gradient(90deg, #f59e0b, #ef4444); border-radius: 4px;"></div>
</div>
<div style="width: 50px; font-size: 0.8rem; color: var(--text2);">${count}</div>
</div>
`;
}).join('')}
</div>
`;
}
},
renderCrossBoundaryTrace(ffiEvents) {
const container = document.getElementById('boundaryEvents');
const countEl = document.getElementById('boundaryEventCount');
if (!container) return;
if (countEl) countEl.textContent = ffiEvents.length + ' events';
if (ffiEvents.length === 0) {
container.innerHTML = '<div style="text-align: center; color: var(--text2); font-size: 0.85rem;">No boundary events</div>';
return;
}
container.innerHTML = ffiEvents.slice(0, 8).map(e => `
<div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg3); border-radius: 8px; font-size: 0.8rem;">
<span style="font-size: 1rem;">${e.icon || '→'}</span>
<div>
<div style="font-weight: 500;">${e.event_type || 'Cross'}</div>
<div style="color: var(--text2); font-size: 0.7rem;">${e.var_name || 'unknown'}</div>
</div>
</div>
`).join('');
}
},
timetravel: {
filteredData: [],
filter() { const query = document.getElementById('timetravelSearch').value.toLowerCase(); this.filteredData = (data.allocations || []).filter(a => (a.address || '').toLowerCase().includes(query) || (a.var_name || '').toLowerCase().includes(query)); this.sort(); },
sort() { const sortBy = document.getElementById('ttSortOrder').value; this.filteredData.sort((a, b) => { switch (sortBy) { case 'time-asc': return (a.timestamp_alloc || 0) - (b.timestamp_alloc || 0); case 'time-desc': return (b.timestamp_alloc || 0) - (a.timestamp_alloc || 0); case 'size-desc': return (b.size || 0) - (a.size || 0); case 'size-asc': return (a.size || 0) - (b.size || 0); case 'lifetime-desc': return (b.lifetime_ms || 0) - (a.lifetime_ms || 0); default: return 0; } }); this.render(); },
render() { const tbody = document.getElementById('timetravelBody'); tbody.innerHTML = this.filteredData.slice(0, 100).map(a => { const sourceLoc = (a.source_file && a.source_line) ? `<a href="#" onclick="jumpToSource('${a.source_file}', ${a.source_line}); return false;" title="Jump to source" style="color: var(--primary); text-decoration: underline;">${a.source_file}:${a.source_line}</a>` : '<span style="color: var(--text2);">—</span>'; return `<tr onclick="Dashboard.timetravel.showDetail('${a.address}')" style="cursor: pointer;"><td><code>${a.address || '0x0'}</code></td><td>${a.var_name || 'unknown'}</td><td>${Utils.formatBytes(parseInt(a.size) || 0)}</td><td style="font-size: 0.85rem;">${sourceLoc}</td><td>${a.timestamp_alloc || 0}</td><td>${Utils.formatTime(parseInt(a.lifetime_ms || 0) * 1000000)}</td><td>${a.is_leaked ? '<span class="badge badge-danger">Leaked</span>' : a.timestamp_dealloc > 0 ? '<span class="badge badge-success">Freed</span>' : '<span class="badge badge-info">Active</span>'}</td><td><button class="btn-link">View History</button></td></tr>`; }).join(''); this.updateStats(); },
updateStats() {
const allocations = this.filteredData;
const total = allocations.length;
const active = allocations.filter(a => !a.timestamp_dealloc).length;
const freed = allocations.filter(a => a.timestamp_dealloc > 0 && !a.is_leaked).length;
const leaked = allocations.filter(a => a.is_leaked).length;
const totalEl = document.getElementById('ttTotalEvents');
const activeEl = document.getElementById('ttActiveCount');
const freedEl = document.getElementById('ttFreedCount');
const leakedEl = document.getElementById('ttLeakedCount');
if (totalEl) totalEl.textContent = total;
if (activeEl) activeEl.textContent = active;
if (freedEl) freedEl.textContent = freed;
if (leakedEl) leakedEl.textContent = leaked;
},
showDetail(address) { const alloc = data.allocations?.find(a => a.address === address); if (!alloc) { alert('Allocation not found: ' + address); return; } document.getElementById('timetravelDetail').style.display = 'block'; document.getElementById('ttAddress').textContent = address; document.getElementById('ttVarName').textContent = alloc.var_name || 'unknown'; document.getElementById('ttType').textContent = alloc.type_name || 'Unknown'; document.getElementById('ttSize').textContent = Utils.formatBytes(parseInt(alloc.size) || 0); document.getElementById('ttStatus').innerHTML = alloc.is_leaked ? '<span class="badge badge-danger">Leaked</span>' : '<span class="badge badge-success">Normal</span>'; const timeline = document.getElementById('ttTimeline'); const events = [{ type: 'alloc', time: alloc.timestamp_alloc || 0, desc: 'Allocation', icon: '➕' }]; if (alloc.timestamp_dealloc > 0) events.push({ type: 'dealloc', time: alloc.timestamp_dealloc, desc: 'Deallocation', icon: '➖' }); if (alloc.is_leaked) events.push({ type: 'leak', time: (alloc.timestamp_alloc || 0) + parseInt(alloc.lifetime_ms || 0) * 1000000, desc: 'Leak Detected', icon: '⚠️' }); timeline.innerHTML = events.map(e => `<div class="timeline-item ${e.type}"><strong>${e.icon} ${e.desc}</strong><div style="color: var(--text2); font-size: 0.85rem;">Timestamp: ${e.time} | Delta: +${Utils.formatTime(e.time - events[0].time)}</div></div>`).join(''); },
init() {
const allocations = data.allocations || [];
this.filteredData = [...allocations];
if (allocations.length === 0) {
document.getElementById('timetravelBody').innerHTML = '<tr><td colspan="8" class="empty-state">No allocation data</td></tr>';
return;
}
this.sort();
}
},
tasks: {
filteredData: [],
currentView: 'timeline',
toggleView(view) {
this.currentView = view;
this.renderTimeline();
},
filter() {
const filterBy = document.getElementById('taskFilterStatus').value;
const asyncTasks = data.async_tasks || [];
switch(filterBy) {
case 'active':
this.filteredData = asyncTasks.filter(t => !t.is_completed);
break;
case 'completed':
this.filteredData = asyncTasks.filter(t => t.is_completed);
break;
case 'leak':
this.filteredData = asyncTasks.filter(t => t.has_potential_leak);
break;
default:
this.filteredData = [...asyncTasks];
}
this.render();
this.renderTimeline();
},
sort() {
const sortBy = document.getElementById('taskSortOrder').value;
this.filteredData.sort((a, b) => {
switch (sortBy) {
case 'memory-desc': return (b.current_memory || 0) - (a.current_memory || 0);
case 'memory-asc': return (a.current_memory || 0) - (b.current_memory || 0);
case 'peak-desc': return (b.peak_memory || 0) - (a.peak_memory || 0);
case 'efficiency-desc': return (b.efficiency_score || 0) - (a.efficiency_score || 0);
case 'duration-desc': return (b.duration_ms || 0) - (a.duration_ms || 0);
case 'allocations-desc': return (b.total_allocations || 0) - (a.total_allocations || 0);
default: return 0;
}
});
this.render();
},
render() {
const container = document.getElementById('taskCardsContainer');
if (this.filteredData.length === 0) {
container.innerHTML = '<div class="empty-state">No async task data</div>';
return;
}
container.innerHTML = this.filteredData.map(t => {
const statusClass = t.is_completed ? 'success' : 'info';
const statusText = t.is_completed ? 'Completed' : 'Running';
const leakBadge = t.has_potential_leak ? '<span class="badge badge-warning">⚠️ Leak?</span>' : '';
const efficiencyClass = (t.efficiency_score || 0) >= 0.8 ? 'success' : (t.efficiency_score || 0) >= 0.5 ? 'warning' : 'danger';
const taskType = t.task_type || 'Unknown';
return `
<div class="card" style="padding: 12px; margin: 0; border-left: 4px solid ${t.is_completed ? '#10b981' : '#3b82f6'};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div>
<span style="font-weight: 600; color: var(--primary);">#${t.task_id}</span>
<span style="margin-left: 8px; font-size: 0.9rem;">${t.task_name || 'Unknown'}</span>
</div>
<div>
<span class="badge badge-${statusClass}">${statusText}</span>
${leakBadge}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; font-size: 0.85rem;">
<div>
<div style="color: var(--text2); font-size: 0.75rem;">💾 Current Memory</div>
<div style="font-weight: 600;">${Utils.formatBytes(t.current_memory || 0)}</div>
</div>
<div>
<div style="color: var(--text2); font-size: 0.75rem;">🏔️ Peak Memory</div>
<div style="font-weight: 600;">${Utils.formatBytes(t.peak_memory || 0)}</div>
</div>
<div>
<div style="color: var(--text2); font-size: 0.75rem;">📊 Allocations</div>
<div style="font-weight: 600;">${t.total_allocations || 0}</div>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; font-size: 0.85rem;">
<div>
<div style="color: var(--text2); font-size: 0.75rem;">⏱️ Duration</div>
<div style="font-weight: 600;">${(t.duration_ms || 0).toFixed(1)}ms</div>
</div>
<div>
<div style="color: var(--text2); font-size: 0.75rem;">📈 Efficiency</div>
<div class="badge badge-${efficiencyClass}">${((t.efficiency_score || 0) * 100).toFixed(0)}%</div>
</div>
<div>
<div style="color: var(--text2); font-size: 0.75rem;">🏷️ Type</div>
<div style="font-weight: 600; font-size: 0.75rem;">${taskType}</div>
</div>
</div>
</div>
`;
}).join('');
},
renderTimeline() {
const container = document.getElementById('taskTimeline');
if (!container) return;
const tasks = this.filteredData;
if (tasks.length === 0) {
container.innerHTML = '<div class="empty-state">No task data for timeline</div>';
return;
}
if (this.currentView === 'gantt') {
this.renderGanttChart(container, tasks);
} else {
this.renderTimelineView(container, tasks);
}
},
renderTimelineView(container, tasks) {
const maxDuration = Math.max(...tasks.map(t => t.duration_ms || 0));
container.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 8px;">
${tasks.slice(0, 15).map((t, i) => {
const width = maxDuration > 0 ? ((t.duration_ms || 0) / maxDuration * 100) : 0;
const statusColor = t.is_completed ? '#10b981' : '#3b82f6';
const leakIndicator = t.has_potential_leak ? '⚠️' : '';
return `
<div style="display: flex; align-items: center; gap: 12px;">
<div style="width: 120px; font-size: 0.85rem; font-weight: 600; color: var(--primary);">
#${t.task_id} ${leakIndicator}
</div>
<div style="flex: 1; height: 24px; background: var(--bg3); border-radius: 4px; overflow: hidden; position: relative;">
<div style="height: 100%; width: ${width}%; background: ${statusColor}; border-radius: 4px; transition: width 0.3s;"></div>
<div style="position: absolute; top: 50%; left: 8px; transform: translateY(-50%); font-size: 0.75rem; color: white; font-weight: 600; text-shadow: 0 1px 2px rgba(0,0,0,0.5);">
${t.task_name || 'Unknown'} (${(t.duration_ms || 0).toFixed(1)}ms)
</div>
</div>
<div style="width: 80px; text-align: right; font-size: 0.75rem; color: var(--text2);">
${Utils.formatBytes(t.peak_memory || 0)}
</div>
</div>
`;
}).join('')}
</div>
`;
},
renderGanttChart(container, tasks) {
const sortedTasks = [...tasks].sort((a, b) => (a.created_at_ms || 0) - (b.created_at_ms || 0));
const minTime = Math.min(...sortedTasks.map(t => t.created_at_ms || 0));
const maxTime = Math.max(...sortedTasks.map(t => (t.created_at_ms || 0) + (t.duration_ms || 0)));
const timeRange = maxTime - minTime;
container.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 6px;">
${sortedTasks.slice(0, 15).map((t, i) => {
const start = ((t.created_at_ms || 0) - minTime) / timeRange * 100;
const width = (t.duration_ms || 0) / timeRange * 100;
const statusColor = t.is_completed ? '#10b981' : '#3b82f6';
const leakIndicator = t.has_potential_leak ? 'border: 2px solid #f59e0b;' : '';
return `
<div style="display: flex; align-items: center; gap: 12px; height: 28px;">
<div style="width: 100px; font-size: 0.8rem; font-weight: 600; color: var(--primary);">
#${t.task_id}
</div>
<div style="flex: 1; height: 20px; background: var(--bg3); border-radius: 4px; position: relative;">
<div style="position: absolute; left: ${start}%; width: ${Math.max(width, 2)}%; height: 100%; background: ${statusColor}; border-radius: 4px; ${leakIndicator} display: flex; align-items: center; padding: 0 8px;">
<span style="font-size: 0.7rem; color: white; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
${t.task_name || 'Task'}
</span>
</div>
</div>
</div>
`;
}).join('')}
</div>
`;
},
init() {
const asyncTasks = data.async_tasks || [];
const asyncSummary = data.async_summary || {};
this.filteredData = [...asyncTasks];
// Update stats
const activeCount = asyncTasks.filter(t => !t.is_completed).length;
const completedCount = asyncTasks.filter(t => t.is_completed).length;
const leakCount = asyncTasks.filter(t => t.has_potential_leak).length;
const avgEfficiency = asyncTasks.length > 0
? (asyncTasks.reduce((sum, t) => sum + (t.efficiency_score || 0), 0) / asyncTasks.length * 100).toFixed(0)
: 0;
const totalMemory = asyncTasks.reduce((sum, t) => sum + (t.current_memory || 0), 0);
const avgDuration = asyncTasks.length > 0
? (asyncTasks.reduce((sum, t) => sum + (t.duration_ms || 0), 0) / asyncTasks.length).toFixed(1)
: 0;
document.getElementById('asyncTaskCount').textContent = (asyncSummary.total_tasks || asyncTasks.length) + ' tasks';
document.getElementById('activeTaskCount').textContent = activeCount;
document.getElementById('completedTaskCount').textContent = completedCount;
document.getElementById('leakTaskCount').textContent = leakCount;
document.getElementById('avgEfficiency').textContent = avgEfficiency + '%';
document.getElementById('totalAsyncMemory').textContent = Utils.formatBytes(totalMemory);
document.getElementById('avgDuration').textContent = avgDuration + 'ms';
if (asyncTasks.length === 0) {
document.getElementById('taskCardsContainer').innerHTML = '<div class="empty-state">No async task data</div>';
document.getElementById('taskTimeline').innerHTML = '<div class="empty-state">No task data for timeline</div>';
return;
}
this.sort();
this.renderTimeline();
this.initCharts();
},
initCharts() {
const colors = Utils.getChartColors();
const tasks = this.filteredData.slice(0, 10);
// Duration Chart
const durationCtx = document.getElementById('asyncDurationChart');
if (durationCtx) {
new Chart(durationCtx, {
type: 'bar',
data: {
labels: tasks.map(t => t.task_name || `Task ${t.task_id}`),
datasets: [{
label: 'Duration (ms)',
data: tasks.map(t => t.duration_ms || 0),
backgroundColor: colors.primary + '80',
borderColor: colors.primary,
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { beginAtZero: true, grid: { color: colors.grid }, ticks: { color: colors.text2 } },
x: { grid: { display: false }, ticks: { color: colors.text2, maxRotation: 45 } }
},
plugins: { legend: { labels: { color: colors.text } } }
}
});
}
// Efficiency Chart
const efficiencyCtx = document.getElementById('asyncEfficiencyChart');
if (efficiencyCtx) {
new Chart(efficiencyCtx, {
type: 'doughnut',
data: {
labels: tasks.map(t => `#${t.task_id}`),
datasets: [{
data: tasks.map(t => (t.efficiency_score || 0) * 100),
backgroundColor: tasks.map(t =>
(t.efficiency_score || 0) >= 0.8 ? '#10b981' :
(t.efficiency_score || 0) >= 0.5 ? '#f59e0b' : '#ef4444'
)
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { color: colors.text, font: { size: 10 } } }
}
}
});
}
// Heatmap Chart
const heatmapCtx = document.getElementById('asyncHeatmapChart');
if (heatmapCtx) {
const taskLabels = tasks.map(t => t.task_name || `Task ${t.task_id}`);
new Chart(heatmapCtx, {
type: 'bar',
data: {
labels: taskLabels,
datasets: [
{
label: 'Memory %',
data: tasks.map(t => Math.min(100, ((t.peak_memory || 0) / 1024 / 1024) * 10)),
backgroundColor: '#3b82f680',
borderColor: '#3b82f6',
borderWidth: 1
},
{
label: 'CPU %',
data: tasks.map(t => Math.min(100, (t.total_allocations || 0) * 5)),
backgroundColor: '#10b98180',
borderColor: '#10b981',
borderWidth: 1
},
{
label: 'IO %',
data: tasks.map(t => Math.min(100, ((t.total_bytes || 0) / 1024) * 0.5)),
backgroundColor: '#f59e0b80',
borderColor: '#f59e0b',
borderWidth: 1
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
max: 100,
grid: { color: colors.grid },
ticks: { color: colors.text2, callback: v => v + '%' }
},
x: { grid: { display: false }, ticks: { color: colors.text2, maxRotation: 45 } }
},
plugins: { legend: { labels: { color: colors.text } } }
}
});
}
}
},
init() {
this.charts.init();
this.diagnosis.init();
this.allocations.init();
this.threads.init();
this.relationships.init();
this.passports.init();
this.unsafe.init();
this.timetravel.init();
this.tasks.init();
}
};
function jumpToSource(file, line) {
const vscodeUri = `vscode://file/${file}:${line}`;
const confirmed = confirm(`Open in VS Code?\n\n${file}:${line}\n\nClick OK to open in VS Code, or Cancel to copy to clipboard.`);
if (confirmed) {
window.open(vscodeUri, '_blank');
} else {
navigator.clipboard.writeText(`${file}:${line}`).then(() => {
alert('Source location copied to clipboard!');
});
}
}
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
html.setAttribute('data-theme', newTheme);
document.getElementById('theme-icon').textContent = newTheme === 'dark' ? '🌙' : '☀️';
document.getElementById('theme-text').textContent = newTheme === 'dark' ? 'Dark' : 'Light';
localStorage.setItem('theme', newTheme);
Dashboard.charts.init();
}
(function() {
const saved = localStorage.getItem('theme');
if (saved) {
document.documentElement.setAttribute('data-theme', saved);
document.getElementById('theme-icon').textContent = saved === 'dark' ? '🌙' : '☀️';
document.getElementById('theme-text').textContent = saved === 'dark' ? 'Dark' : 'Light';
} else {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
function showMode(mode) {
document.querySelectorAll('.mode-section').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.mode-tab').forEach(el => el.classList.remove('active'));
document.getElementById('mode-' + mode).classList.add('active');
event.target.classList.add('active');
if (mode === 'variable') {
setTimeout(() => Dashboard.variableGraph.init(), 100);
}
if (mode === 'taskgraph') {
setTimeout(() => Dashboard.taskGraph.init(), 100);
}
}
function showTab(tab) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById('tab-' + tab).classList.add('active');
event.target.classList.add('active');
}
function filterTable(tableId, query) {
const table = document.getElementById(tableId);
if (!table) return;
const rows = table.getElementsByTagName('tbody')[0]?.getElementsByTagName('tr') || [];
query = query.toLowerCase();
for (let row of rows) {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(query) ? '' : 'none';
}
}
function toggleSection(sectionId, btn) {
const section = document.getElementById(sectionId);
if (!section) return;
const isCollapsed = section.style.maxHeight === '200px';
if (isCollapsed) {
section.style.maxHeight = 'none';
btn.textContent = '▲ Collapse';
} else {
section.style.maxHeight = '200px';
btn.textContent = '▼ Expand All';
}
}
function toggleTable(wrapperId, btn) {
const wrapper = document.getElementById(wrapperId);
if (!wrapper) return;
const isCollapsed = wrapper.style.maxHeight === '200px';
if (isCollapsed) {
wrapper.style.maxHeight = 'none';
btn.textContent = '▲ Collapse';
} else {
wrapper.style.maxHeight = '200px';
btn.textContent = '▼ Expand';
}
}
let charts = {};
function initCharts() {
const colors = getChartColors();
const allocations = data.allocations || [];
const threads = data.threads || [];
const passports = data.passport_details || [];
const unsafeReports = data.unsafe_reports || [];
Object.values(charts).forEach(chart => chart?.destroy?.());
charts = {};
const memoryCtx = document.getElementById('memoryChart');
if (memoryCtx) {
charts.memory = new Chart(memoryCtx, {
type: 'bar',
data: {
labels: ['Active', 'Freed', 'Leaked'],
datasets: [{
data: [
data.active_allocations || 0,
(data.total_allocations || 0) - (data.active_allocations || 0),
data.leak_count || 0
],
backgroundColor: [colors.primary, '#10b981', '#ef4444']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } },
x: { grid: { display: false }, ticks: { color: colors.text2 } }
}
}
});
}
const typeCtx = document.getElementById('typeChart');
if (typeCtx) {
const typeCount = {};
allocations.forEach(a => {
const type = a.type_name || 'Unknown';
typeCount[type] = (typeCount[type] || 0) + 1;
});
charts.type = new Chart(typeCtx, {
type: 'doughnut',
data: {
labels: Object.keys(typeCount),
datasets: [{
data: Object.values(typeCount),
backgroundColor: Object.keys(typeCount).map((_, i) =>
`hsl(${i * 360 / Math.max(Object.keys(typeCount).length, 1)}, 70%, 50%)`)
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'right', labels: { color: colors.text } } }
}
});
}
const threadCtx = document.getElementById('threadChart');
if (threadCtx) {
const shortThreadIds = threads.map(t => {
const tid = t.thread_id || '?';
const match = String(tid).match(/(\d+)/);
return match ? 'T' + match[1].slice(-4) : String(tid).slice(-6);
});
charts.thread = new Chart(threadCtx, {
type: 'bar',
data: {
labels: shortThreadIds,
datasets: [{
label: 'Allocations',
data: threads.map(t => parseInt(t.allocation_count) || 0),
backgroundColor: colors.primary
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
indexAxis: 'y',
scales: {
y: { grid: { display: false }, ticks: { color: colors.text2 } },
x: { grid: { color: colors.grid }, ticks: { color: colors.text2 } }
}
}
});
}
const asyncCtx = document.getElementById('asyncChart');
if (asyncCtx) {
const asyncTasks = data.async_tasks || [];
const asyncSummary = data.async_summary || {};
if (asyncTasks.length > 0) {
const sortedTasks = [...asyncTasks].sort((a, b) => a.task_id - b.task_id);
const timeLabels = sortedTasks.map((t, i) => `T+${i * 100}ms`);
const cpuUsage = sortedTasks.map(t => Math.min(100, (t.total_allocations || 0) * 5));
const memoryUsage = sortedTasks.map(t => Math.min(100, ((t.peak_memory || 0) / 1024 / 1024) * 2));
const ioUsage = sortedTasks.map(t => Math.min(100, ((t.total_bytes || 0) / 1024) * 0.5));
charts.async = new Chart(asyncCtx, {
type: 'line',
data: {
labels: timeLabels,
datasets: [
{ label: 'CPU Usage %', data: cpuUsage, borderColor: '#3b82f6', backgroundColor: '#3b82f633', fill: false, tension: 0.4 },
{ label: 'Memory %', data: memoryUsage, borderColor: '#10b981', backgroundColor: '#10b98133', fill: false, tension: 0.4 },
{ label: 'IO Usage %', data: ioUsage, borderColor: '#f59e0b', backgroundColor: '#f59e0b33', fill: false, tension: 0.4 }
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { display: true, position: 'top', labels: { color: colors.text } }, title: { display: true, text: `Resource Consumption Over Time | Tasks: ${asyncSummary.total_tasks || 0}`, color: colors.text2 } },
scales: { y: { min: 0, max: 100, grid: { color: colors.grid }, ticks: { color: colors.text2, callback: (v) => v + '%' }, title: { display: true, text: 'Resource Usage %', color: colors.text2 } }, x: { grid: { display: false }, ticks: { color: colors.text2 }, title: { display: true, text: 'Time', color: colors.text2 } } }
}
});
} else {
const allocData = allocations.slice(0, 15).map((a, i) => ({
label: a.var_name || a.type_name || `Alloc ${i}`,
value: parseInt(a.size) || 0
}));
charts.async = new Chart(asyncCtx, {
type: 'line',
data: {
labels: allocData.map(t => t.label.substring(0, 20)),
datasets: [{
label: 'Memory Usage',
data: allocData.map(t => t.value),
borderColor: colors.primary,
fill: true,
backgroundColor: colors.primary + '20'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } },
x: { grid: { display: false }, ticks: { color: colors.text2, maxRotation: 45 } }
}
}
});
}
}
const passportCtx = document.getElementById('passportChart');
if (passportCtx) {
const riskCount = { high: 0, medium: 0, low: 0 };
passports.forEach(p => {
const risk = p.risk_level || 'low';
riskCount[risk] = (riskCount[risk] || 0) + 1;
});
document.getElementById('highRiskCount').textContent = riskCount.high;
document.getElementById('mediumRiskCount').textContent = riskCount.medium;
document.getElementById('lowRiskCount').textContent = riskCount.low;
charts.passport = new Chart(passportCtx, {
type: 'radar',
data: {
labels: ['Size', 'Lifetime', 'Refs', 'Clones', 'Borrows'],
datasets: [{
label: 'Risk Metrics',
data: [
Math.min(100, passports.length * 5),
Math.min(100, riskCount.high * 20),
Math.min(100, riskCount.medium * 15),
Math.min(100, riskCount.low * 10),
50
],
borderColor: colors.primary,
backgroundColor: colors.primary + '20'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
r: {
grid: { color: colors.grid },
angleLines: { color: colors.grid },
ticks: { display: false }
}
},
plugins: { legend: { labels: { color: colors.text } } }
}
});
}
const unsafeCtx = document.getElementById('unsafeChart');
if (unsafeCtx) {
charts.unsafe = new Chart(unsafeCtx, {
type: 'bar',
data: {
labels: ['Unsafe Ops', 'FFI Calls', 'Cross-Boundary'],
datasets: [{
data: [
data.unsafe_count || 0,
data.ffi_count || 0,
unsafeReports.reduce((sum, r) => sum + (r.cross_boundary_events?.length || 0), 0)
],
backgroundColor: ['#ef4444', '#f59e0b', '#3b82f6']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: { grid: { color: colors.grid }, ticks: { color: colors.text2 } },
x: { grid: { display: false }, ticks: { color: colors.text2 } }
}
}
});
}
const unsafeRiskCtx = document.getElementById('unsafeRiskChart');
if (unsafeRiskCtx) {
const riskGroups = { high: 0, medium: 0, low: 0 };
unsafeReports.forEach(r => {
const risk = r.risk_level || 'low';
riskGroups[risk] = (riskGroups[risk] || 0) + 1;
});
charts.unsafeRisk = new Chart(unsafeRiskCtx, {
type: 'pie',
data: {
labels: ['High Risk', 'Medium Risk', 'Low Risk'],
datasets: [{
data: [riskGroups.high, riskGroups.medium, riskGroups.low],
backgroundColor: ['#ef4444', '#f59e0b', '#10b981']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom', labels: { color: colors.text } } }
}
});
}
}
function initRelationshipGraph() {
const container = document.getElementById('relationshipGraph');
if (!container) return;
container.innerHTML = '';
const relationships = data.relationships || [];
if (relationships.length === 0) {
container.innerHTML = '<div class="empty-state">No relationship data available. Try using clones, borrows, or smart pointers (Arc/Rc) in your code.</div>';
return;
}
document.getElementById('relationshipCount').textContent = relationships.length + ' relationships';
const width = container.clientWidth;
const height = container.clientHeight || 400;
const svg = d3.select(container)
.append('svg')
.attr('width', width)
.attr('height', height);
const nodeMap = new Map();
relationships.forEach((r) => {
const sourceId = r.source_ptr;
const targetId = r.target_ptr;
if (!nodeMap.has(sourceId)) {
nodeMap.set(sourceId, {
id: sourceId,
ptr: sourceId,
name: r.source_var_name || ('Ptr ' + sourceId),
type: r.relationship_type,
isSource: true
});
}
if (!nodeMap.has(targetId)) {
nodeMap.set(targetId, {
id: targetId,
ptr: targetId,
name: r.target_var_name || ('Ptr ' + targetId),
type: r.relationship_type,
isSource: false
});
}
});
const nodes = Array.from(nodeMap.values());
const nodeIds = new Set(nodes.map(n => n.id));
const links = relationships
.map(r => ({
source: r.source_ptr,
target: r.target_ptr,
type: r.relationship_type || 'reference',
strength: r.strength || 0.5,
typeName: r.type_name || 'unknown'
}))
.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
const colorMap = {
'clone': '#10b981',
'ownership_transfer': '#dc2626',
'immutable_borrow': '#3b82f6',
'mutable_borrow': '#f59e0b',
'smart_pointer_arc': '#8b5cf6',
'smart_pointer_rc': '#8b5cf6',
'type_similarity': '#8b5cf6',
'Arc': '#8b5cf6',
'Rc': '#8b5cf6',
'contains': '#ec4899',
'Contains': '#ec4899',
'reference': '#64748b'
};
const getColor = (type) => colorMap[type] || '#64748b';
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(links).id(d => d.id).distance(120))
.force('charge', d3.forceManyBody().strength(-400))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(40));
const link = svg.append('g')
.selectAll('line')
.data(links)
.join('line')
.attr('class', 'link')
.attr('stroke', d => getColor(d.type))
.attr('stroke-width', d => Math.max(1, d.strength * 4));
const node = svg.append('g')
.selectAll('g')
.data(nodes)
.join('g')
.attr('class', 'node')
.call(d3.drag()
.on('start', dragstarted)
.on('drag', dragged)
.on('end', dragended));
node.append('circle')
.attr('r', 14)
.attr('fill', d => getColor(d.type));
node.append('text')
.attr('dx', 18)
.attr('dy', 4)
.text(d => {
const name = d.name || 'unknown';
return name.length > 12 ? name.substring(0, 10) + '..' : name;
})
.style('font-size', '11px')
.style('fill', 'var(--text)');
const tooltip = document.getElementById('tooltip');
node.on('mouseover', (event, d) => {
tooltip.style.display = 'block';
tooltip.innerHTML = `<strong>${d.name || 'Unknown'}</strong><br>Type: ${d.type || 'unknown'}<br>Ptr: ${d.ptr || 'N/A'}`;
tooltip.style.left = (event.pageX + 10) + 'px';
tooltip.style.top = (event.pageY - 10) + 'px';
}).on('mouseout', () => {
tooltip.style.display = 'none';
});
simulation.on('tick', () => {
link
.attr('x1', d => d.source?.x || 0)
.attr('y1', d => d.source?.y || 0)
.attr('x2', d => d.target?.x || 0)
.attr('y2', d => d.target?.y || 0);
node.attr('transform', d => `translate(${d.x || 0},${d.y || 0})`);
});
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x || 0;
event.subject.fy = event.subject.y || 0;
}
function dragged(event) {
event.subject.fx = event.x || 0;
event.subject.fy = event.y || 0;
}
function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}
}
const PAGE_SIZE = 100;
let allocShowAll = false;
let allocCurrentPage = 1;
let allocFilteredData = [];
function toggleAllocationsTable() {
const wrapper = document.getElementById('allocationsWrapper');
const btn = document.getElementById('toggleAllocBtn');
const pagination = document.getElementById('allocationsPagination');
allocShowAll = !allocShowAll;
if (allocShowAll) {
wrapper.style.maxHeight = 'none';
wrapper.style.overflow = 'visible';
btn.textContent = '▲ Collapse';
if (allocFilteredData.length > PAGE_SIZE) {
pagination.style.display = 'flex';
}
} else {
wrapper.style.maxHeight = '400px';
wrapper.style.overflow = 'auto';
btn.textContent = '▼ Show All';
pagination.style.display = 'none';
renderAllocPage(1);
}
}
function renderAllocPage(page) {
const tbody = document.getElementById('allocationsBody');
const start = (page - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
const pageData = allocFilteredData.slice(start, end);
tbody.innerHTML = pageData.map(a => `
<tr>
<td><code>${a.address || '0x0'}</code></td>
<td>${a.var_name || 'unknown'}</td>
<td>${a.type_name || 'Unknown'}</td>
<td>${Utils.formatBytes(parseInt(a.size) || 0)}</td>
<td>${a.thread_id || 'N/A'}</td>
<td>${Utils.formatTime(parseInt(a.lifetime_ms || 0) * 1000000)}</td>
<td>${a.is_leaked ? '<span class="badge badge-danger">Leaked</span>' :
a.timestamp_dealloc > 0 ? '<span class="badge badge-success">Freed</span>' :
'<span class="badge badge-info">Active</span>'}</td>
</tr>
`).join('');
allocCurrentPage = page;
const totalPages = Math.ceil(allocFilteredData.length / PAGE_SIZE);
document.getElementById('allocPageInfo').textContent = `Page ${page} of ${totalPages}`;
document.getElementById('prevAllocBtn').disabled = page <= 1;
document.getElementById('nextAllocBtn').disabled = page >= totalPages;
}
function prevAllocPage() {
if (allocCurrentPage > 1) {
renderAllocPage(allocCurrentPage - 1);
}
}
function nextAllocPage() {
const totalPages = Math.ceil(allocFilteredData.length / PAGE_SIZE);
if (allocCurrentPage < totalPages) {
renderAllocPage(allocCurrentPage + 1);
}
}
function populateAllocations() {
const tbody = document.getElementById('allocationsBody');
const data_list = data.allocations || [];
allocFilteredData = data_list;
document.getElementById('allocCount').textContent = data_list.length + ' items';
if (data_list.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No allocation data</td></tr>';
return;
}
if (data_list.length <= PAGE_SIZE) {
tbody.innerHTML = data_list.map(a => `
<tr>
<td><code>${a.address || '0x0'}</code></td>
<td>${a.var_name || 'unknown'}</td>
<td>${a.type_name || 'Unknown'}</td>
<td>${Utils.formatBytes(parseInt(a.size) || 0)}</td>
<td>${a.thread_id || 'N/A'}</td>
<td>${Utils.formatTime(parseInt(a.lifetime_ms || 0) * 1000000)}</td>
<td>${a.is_leaked ? '<span class="badge badge-danger">Leaked</span>' :
a.timestamp_dealloc > 0 ? '<span class="badge badge-success">Freed</span>' :
'<span class="badge badge-info">Active</span>'}</td>
</tr>
`).join('');
} else {
renderAllocPage(1);
}
}
let threadFilteredData = [];
function sortThreads() {
const sortBy = document.getElementById('threadSortOrder').value;
threadFilteredData.sort((a, b) => {
switch (sortBy) {
case 'alloc-desc': return (b.allocation_count || 0) - (a.allocation_count || 0);
case 'alloc-asc': return (a.allocation_count || 0) - (b.allocation_count || 0);
case 'memory-desc': return (b.current_memory_bytes || 0) - (a.current_memory_bytes || 0);
case 'memory-asc': return (a.current_memory_bytes || 0) - (b.current_memory_bytes || 0);
case 'thread-id': return (a.thread_id || '0').localeCompare(b.thread_id || '0');
default: return 0;
}
});
renderThreads();
}
function renderThreads() {
const tbody = document.getElementById('threadTableBody');
tbody.innerHTML = threadFilteredData.map(t => `
<tr>
<td><code>${t.thread_id || '0'}</code></td>
<td>${t.allocation_count || 0}</td>
<td>${t.current_memory || '0 B'}</td>
<td>${t.peak_memory || '0 B'}</td>
<td>${t.total_allocated || '0 B'}</td>
<td><span class="badge badge-success">active</span></td>
</tr>
`).join('');
}
function populateThreads() {
const threads = data.threads || [];
threadFilteredData = [...threads];
document.getElementById('threadCount').textContent = threads.length + ' threads';
if (threads.length === 0) {
document.getElementById('threadTableBody').innerHTML = '<tr><td colspan="6" class="empty-state">No thread data</td></tr>';
return;
}
sortThreads();
}
let relShowAll = false;
let relFilteredData = [];
function toggleRelTable() {
const wrapper = document.getElementById('relTableWrapper');
const btn = document.getElementById('toggleRelBtn');
relShowAll = !relShowAll;
if (relShowAll) {
wrapper.style.maxHeight = 'none';
wrapper.style.overflow = 'visible';
btn.textContent = '▲ Collapse';
} else {
wrapper.style.maxHeight = '300px';
wrapper.style.overflow = 'auto';
btn.textContent = '▼ Expand';
}
}
function sortRelationships() {
const sortBy = document.getElementById('relSortOrder').value;
relFilteredData.sort((a, b) => {
switch (sortBy) {
case 'strength-desc': return (b.strength || 0.5) - (a.strength || 0.5);
case 'strength-asc': return (a.strength || 0.5) - (b.strength || 0.5);
case 'type': return (a.relationship_type || '').localeCompare(b.relationship_type || '');
case 'source': return (a.source_var_name || '').localeCompare(b.source_var_name || '');
default: return 0;
}
});
renderRelationships();
}
function renderRelationships() {
const tbody = document.getElementById('relationshipsBody');
tbody.innerHTML = relFilteredData.map(r => `
<tr>
<td>${r.source_var_name || ('Ptr ' + r.source_ptr) || 'N/A'}</td>
<td>${r.target_var_name || ('Ptr ' + r.target_ptr) || 'N/A'}</td>
<td><span class="badge badge-info">${r.relationship_type || 'reference'}</span></td>
<td>${r.type_name || 'Unknown'}</td>
<td>${(((r.strength || 0.5) * 100)).toFixed(0)}%</td>
</tr>
`).join('');
}
function populateRelationships() {
const relationships = data.relationships || [];
relFilteredData = [...relationships];
document.getElementById('relationshipCount2').textContent = relationships.length + ' relationships';
if (relationships.length === 0) {
document.getElementById('relationshipsBody').innerHTML = '<tr><td colspan="5" class="empty-state">No relationship data</td></tr>';
return;
}
sortRelationships();
}
function populatePassports() {
const passports = data.passport_details || [];
document.getElementById('passportBadge').textContent = passports.length + ' passports';
if (passports.length === 0) {
const tbody = document.getElementById('passportTableBody');
if (tbody) tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No passport data</td></tr>';
return;
}
const container = document.getElementById('passportCardContainer');
if (container) {
container.innerHTML = passports.slice(0, 20).map(p => {
const riskClass = p.risk_level === 'high' ? 'risk-high' :
p.risk_level === 'medium' ? 'risk-medium' : 'risk-low';
return `<div class="detail-panel" style="border-left: 4px solid ${p.is_leaked ? '#ef4444' : 'var(--primary)'};">
<div style="display:flex;justify-content:space-between;align-items:center;">
<h4 style="color:var(--primary);margin:0;">${p.var_name || 'unknown'}</h4>
<span class="badge ${p.is_leaked ? 'badge-danger' : 'badge-success'}">${p.is_leaked ? '🔴 LEAKED' : '🟢 Active'}</span>
</div>
<div style="margin-top:8px;display:grid;grid-template-columns:repeat(auto-fit,minmax(100px,1fr));gap:8px;">
<div><span class="detail-label">Type</span><div class="detail-value">${p.type_name || 'Unknown'}</div></div>
<div><span class="detail-label">Size</span><div class="detail-value">${Utils.formatBytes(parseInt(p.size_bytes) || 0)}</div></div>
<div><span class="detail-label">Status</span><div class="detail-value">${p.status || 'N/A'}</div></div>
<div><span class="detail-label">Risk</span><div class="detail-value ${riskClass}">${p.risk_level || 'low'}</div></div>
</div>
</div>`;
}).join('');
}
const tbody = document.getElementById('passportTableBody');
if (tbody) {
tbody.innerHTML = passports.map(p => {
const riskClass = p.risk_level === 'high' ? 'risk-high' :
p.risk_level === 'medium' ? 'risk-medium' : 'risk-low';
return `<tr>
<td><code>${(p.passport_id || 'N/A').substring(0, 16)}...</code></td>
<td>${p.var_name || 'unknown'}</td>
<td>${p.type_name || 'Unknown'}</td>
<td>${Utils.formatBytes(parseInt(p.size_bytes) || 0)}</td>
<td>${p.is_leaked ? '<span class="badge badge-danger">Leaked</span>' : '<span class="badge badge-success">Normal</span>'}</td>
<td class="${riskClass}">${p.risk_level || 'low'}</td>
</tr>`;
}).join('');
}
const timeline = document.getElementById('passportTimeline');
timeline.innerHTML = passports.slice(0, 10).map(p => `
<div class="timeline-item ${p.is_leaked ? 'leaked' : 'active'}">
<strong>${p.var_name || 'unknown'}</strong>
<span class="badge ${p.is_leaked ? 'badge-danger' : 'badge-success'}">${p.status || 'active'}</span>
<div style="color: var(--text2); font-size: 0.85rem;">
${Utils.formatBytes(parseInt(p.size_bytes) || 0)} | ${p.type_name || 'Unknown'}
</div>
</div>
`).join('');
}
function populateUnsafeReports() {
const container = document.getElementById('unsafeReportsContainer');
const unsafeReports = data.unsafe_reports || [];
document.getElementById('unsafeBadge').textContent = unsafeReports.length + ' reports';
if (unsafeReports.length === 0) {
container.innerHTML = '<div class="empty-state">✅ No high risk operations detected</div>';
document.getElementById('lifecycleFlowContainer').innerHTML = '<div class="empty-state">No lifecycle data available</div>';
document.getElementById('lifecycleFlowCount').textContent = '0 flows';
return;
}
const highRisk = unsafeReports.filter(r => r.risk_level === 'high').slice(0, 10);
if (highRisk.length === 0) {
container.innerHTML = '<div class="empty-state">✅ No high risk operations</div>';
} else {
container.innerHTML = highRisk.map(r => `
<div class="detail-panel" style="border-left: 4px solid var(--danger); margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h4 style="color: var(--danger); margin: 0;">⚠️ ${r.var_name || 'unknown'}</h4>
<span class="badge badge-danger">High Risk</span>
</div>
<div style="margin-top: 12px; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px;">
<div><span class="detail-label">Address</span><div class="detail-value">${r.allocation_ptr || 'N/A'}</div></div>
<div><span class="detail-label">Type</span><div class="detail-value">${r.type_name || 'Unknown'}</div></div>
<div><span class="detail-label">Size</span><div class="detail-value">${Utils.formatBytes(parseInt(r.size_bytes) || 0)}</div></div>
<div><span class="detail-label">Status</span><div class="detail-value">${r.status || 'unknown'}</div></div>
</div>
${r.risk_factors && r.risk_factors.length > 0 ? `
<div style="margin-top: 12px;">
<span class="detail-label">Risk Factors:</span>
<ul style="margin: 8px 0 0 20px; color: var(--danger);">
${r.risk_factors.map(f => `<li>${f}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`).join('');
}
const ffiBody = document.getElementById('ffiTableBody');
const ffiEvents = [];
const lifecycleFlows = [];
unsafeReports.forEach(r => {
if (r.cross_boundary_events) {
r.cross_boundary_events.forEach(e => ffiEvents.push({...e, var_name: r.var_name, size_bytes: r.size_bytes}));
}
if (r.lifecycle_events && r.lifecycle_events.length > 0) {
lifecycleFlows.push({
var_name: r.var_name,
allocation_ptr: r.allocation_ptr,
size_bytes: r.size_bytes,
events: r.lifecycle_events
});
}
});
document.getElementById('lifecycleFlowCount').textContent = lifecycleFlows.length + ' flows';
const flowContainer = document.getElementById('lifecycleFlowContainer');
if (lifecycleFlows.length === 0) {
flowContainer.innerHTML = '<div class="empty-state">No lifecycle flow data available</div>';
} else {
flowContainer.innerHTML = lifecycleFlows.slice(0, 10).map(flow => {
const flowSteps = flow.events.map((e, idx) => {
const isLast = idx === flow.events.length - 1;
const stepColor = e.color || '#64748b';
return `
<div style="display: flex; align-items: center; gap: 8px;">
<div style="width: 32px; height: 32px; border-radius: 50%; background: ${stepColor}; display: flex; align-items: center; justify-content: center; font-size: 14px;">${e.icon || '•'}</div>
<div style="flex: 1;">
<div style="font-weight: 600; color: var(--text);">${e.context || e.event_type}</div>
<div style="font-size: 0.8rem; color: var(--text2);">${flow.var_name} | ${Utils.formatBytes(flow.size_bytes || 0)}</div>
</div>
${!isLast ? '<div style="width: 2px; height: 24px; background: var(--border);"></div>' : ''}
</div>
`;
}).join('');
return `
<div class="detail-panel" style="border: 1px solid var(--border); border-radius: 8px; padding: 16px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h4 style="color: var(--primary); margin: 0;">${flow.var_name}</h4>
<code style="font-size: 0.8rem; color: var(--text2);">${flow.allocation_ptr || 'N/A'}</code>
</div>
<div style="display: flex; align-items: flex-start; gap: 12px; flex-wrap: wrap;">
${flowSteps}
</div>
</div>
`;
}).join('');
}
if (ffiEvents.length === 0) {
ffiBody.innerHTML = '<tr><td colspan="5" class="empty-state">No FFI events</td></tr>';
} else {
ffiBody.innerHTML = ffiEvents.slice(0, 50).map(e => `
<tr>
<td><span class="badge badge-warning">${e.event_type || 'N/A'}</span></td>
<td>${e.from_context || 'N/A'}</td>
<td>${e.to_context || 'N/A'}</td>
<td>${e.timestamp || 0}</td>
<td>${e.icon || '→'}</td>
</tr>
`).join('');
}
}
let ttFilteredData = [];
function filterTimetravel() {
const query = document.getElementById('timetravelSearch').value.toLowerCase();
ttFilteredData = data.allocations?.filter(a =>
(a.address || '').toLowerCase().includes(query) ||
(a.var_name || '').toLowerCase().includes(query)
) || [];
sortTimetravel();
}
function sortTimetravel() {
const sortBy = document.getElementById('ttSortOrder').value;
ttFilteredData.sort((a, b) => {
switch (sortBy) {
case 'time-asc': return (a.timestamp_alloc || 0) - (b.timestamp_alloc || 0);
case 'time-desc': return (b.timestamp_alloc || 0) - (a.timestamp_alloc || 0);
case 'size-desc': return (b.size || 0) - (a.size || 0);
case 'size-asc': return (a.size || 0) - (b.size || 0);
case 'lifetime-desc': return (b.lifetime_ms || 0) - (a.lifetime_ms || 0);
default: return 0;
}
});
renderTimetravelTable();
}
function renderTimetravelTable() {
const tbody = document.getElementById('timetravelBody');
tbody.innerHTML = ttFilteredData.slice(0, 100).map(a => {
const sourceLoc = (a.source_file && a.source_line)
? `<a href="#" onclick="jumpToSource('${a.source_file}', ${a.source_line}); return false;" title="Jump to source" style="color: var(--primary); text-decoration: underline;">${a.source_file}:${a.source_line}</a>`
: '<span style="color: var(--text2);">—</span>';
return `
<tr onclick="showTimetravelDetail('${a.address}')" style="cursor: pointer;">
<td><code>${a.address || '0x0'}</code></td>
<td>${a.var_name || 'unknown'}</td>
<td>${Utils.formatBytes(parseInt(a.size) || 0)}</td>
<td style="font-size: 0.85rem;">${sourceLoc}</td>
<td>${Utils.formatTime(parseInt(a.timestamp_alloc) || 0)}</td>
<td>${Utils.formatTime(parseInt(a.lifetime_ms || 0) * 1000000)}</td>
<td>${a.is_leaked ? '<span class="badge badge-danger">Leaked</span>' :
a.timestamp_dealloc > 0 ? '<span class="badge badge-success">Freed</span>' :
'<span class="badge badge-info">Active</span>'}</td>
<td><button class="btn-link">View History</button></td>
</tr>
`}).join('');
}
function populateTimetravel() {
const allocations = data.allocations || [];
ttFilteredData = [...allocations];
if (allocations.length === 0) {
document.getElementById('timetravelBody').innerHTML = '<tr><td colspan="7" class="empty-state">No allocation data</td></tr>';
return;
}
sortTimetravel();
}
function showTimetravelDetail(address) {
const alloc = data.allocations?.find(a => a.address === address);
if (!alloc) {
alert('Allocation not found: ' + address);
return;
}
document.getElementById('timetravelDetail').style.display = 'block';
document.getElementById('ttAddress').textContent = address;
document.getElementById('ttVarName').textContent = alloc.var_name || 'unknown';
document.getElementById('ttType').textContent = alloc.type_name || 'Unknown';
document.getElementById('ttSize').textContent = Utils.formatBytes(parseInt(alloc.size) || 0);
document.getElementById('ttStatus').innerHTML = alloc.is_leaked ?
'<span class="badge badge-danger">Leaked</span>' :
'<span class="badge badge-success">Normal</span>';
const timeline = document.getElementById('ttTimeline');
const events = [
{ type: 'alloc', time: alloc.timestamp_alloc || 0, desc: 'Allocation', icon: '➕' },
];
if (alloc.timestamp_dealloc > 0) {
events.push({ type: 'dealloc', time: alloc.timestamp_dealloc, desc: 'Deallocation', icon: '➖' });
}
if (alloc.is_leaked) {
events.push({ type: 'leak', time: (alloc.timestamp_alloc || 0) + parseInt(alloc.lifetime_ms || 0) * 1000000, desc: 'Leak Detected', icon: '⚠️' });
}
timeline.innerHTML = events.map(e => `
<div class="timeline-item ${e.type}">
<strong>${e.icon} ${e.desc}</strong>
<div style="color: var(--text2); font-size: 0.85rem;">
Timestamp: ${e.time} | Delta: +${Utils.formatTime(e.time - events[0].time)}
</div>
</div>
`).join('');
}
let taskFilteredData = [];
function sortTasks() {
const sortBy = document.getElementById('taskSortOrder').value;
taskFilteredData.sort((a, b) => {
switch (sortBy) {
case 'memory-desc': return (b.current_memory || 0) - (a.current_memory || 0);
case 'memory-asc': return (a.current_memory || 0) - (b.current_memory || 0);
case 'peak-desc': return (b.peak_memory || 0) - (a.peak_memory || 0);
case 'peak-asc': return (a.peak_memory || 0) - (b.peak_memory || 0);
case 'allocations-desc': return (b.total_allocations || 0) - (a.total_allocations || 0);
case 'allocations-asc': return (a.total_allocations || 0) - (b.total_allocations || 0);
default: return 0;
}
});
renderTasks();
}
function renderTasks() {
const tbody = document.getElementById('taskTableBody');
if (taskFilteredData.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="empty-state">No async task data</td></tr>';
return;
}
tbody.innerHTML = taskFilteredData.map(t => `
<tr>
<td><code>#${t.task_id}</code></td>
<td>${t.task_name || 'Unknown'}</td>
<td>${Utils.formatBytes(t.current_memory || 0)}</td>
<td>${Utils.formatBytes(t.peak_memory || 0)}</td>
<td>${t.total_allocations || 0}</td>
<td>${t.is_completed ? '<span class="badge badge-success">Completed</span>' : '<span class="badge badge-info">Running</span>'}${t.has_potential_leak ? ' <span class="badge badge-warning">Leak?</span>' : ''}</td>
</tr>
`).join('');
}
function populateTasks() {
const asyncTasks = data.async_tasks || [];
taskFilteredData = [...asyncTasks];
if (asyncTasks.length === 0) {
document.getElementById('taskTableBody').innerHTML = '<tr><td colspan="6" class="empty-state">No async task data</td></tr>';
return;
}
sortTasks();
}
document.addEventListener('DOMContentLoaded', function() {
Dashboard.diagnosis.init();
Dashboard.charts.init();
Dashboard.allocations.init();
Dashboard.threads.init();
Dashboard.relationships.init();
Dashboard.passports.init();
Dashboard.unsafe.init();
Dashboard.timetravel.init();
Dashboard.tasks.init();
populatePassports();
// Initialize Quick Health Dashboard
updateQuickHealthDashboard();
updateCriticalAlerts();
});
function updateQuickHealthDashboard() {
const asyncTasks = data.async_tasks || [];
const activeCount = asyncTasks.filter(t => !t.is_completed).length;
const leakCount = data.leak_count || 0;
const unsafeCount = data.unsafe_count || 0;
// Update active tasks
const activeTaskEl = document.getElementById('quickActiveTasks');
if (activeTaskEl) {
activeTaskEl.textContent = activeCount;
}
// Update status badge
const statusBadge = document.getElementById('quickStatusBadge');
if (statusBadge) {
if (leakCount > 0) {
statusBadge.className = 'badge badge-danger';
statusBadge.textContent = `⚠️ ${leakCount} Leak${leakCount > 1 ? 's' : ''} Detected`;
} else if (unsafeCount > 0) {
statusBadge.className = 'badge badge-warning';
statusBadge.textContent = `⚡ ${unsafeCount} Unsafe Operation${unsafeCount > 1 ? 's' : ''}`;
} else {
statusBadge.className = 'badge badge-success';
statusBadge.textContent = '✅ Healthy';
}
}
// Update health trend
const healthScore = parseInt(data.health_score) || 0;
const healthTrend = document.getElementById('healthTrend');
if (healthTrend) {
if (healthScore >= 80) {
healthTrend.textContent = '✅';
} else if (healthScore >= 60) {
healthTrend.textContent = '⚠️';
} else {
healthTrend.textContent = '🚨';
}
}
}
function updateCriticalAlerts() {
const alertsContainer = document.getElementById('criticalAlerts');
if (!alertsContainer) return;
const alerts = [];
const leakCount = data.leak_count || 0;
const unsafeCount = data.unsafe_count || 0;
const ffiCount = data.ffi_count || 0;
const asyncTasks = data.async_tasks || [];
const leakyTasks = asyncTasks.filter(t => t.has_potential_leak).length;
if (leakCount > 0) {
alerts.push({
type: 'danger',
icon: '🔴',
message: `<strong>${leakCount} Memory Leak${leakCount > 1 ? 's' : ''}</strong> detected - Immediate attention required`,
action: `onclick="showMode('passport')" style="cursor: pointer;"`
});
}
if (leakyTasks > 0) {
alerts.push({
type: 'warning',
icon: '⚠️',
message: `<strong>${leakyTasks} Async Task${leakyTasks > 1 ? 's' : ''}</strong> with potential memory leaks`,
action: `onclick="showMode('task')" style="cursor: pointer;"`
});
}
if (unsafeCount > 10) {
alerts.push({
type: 'warning',
icon: '⚡',
message: `<strong>${unsafeCount} Unsafe Operations</strong> - Consider reviewing for safety`,
action: `onclick="showMode('unsafe')" style="cursor: pointer;"`
});
}
if (ffiCount > 20) {
alerts.push({
type: 'info',
icon: '🔗',
message: `<strong>${ffiCount} FFI Calls</strong> - High cross-boundary activity`,
action: `onclick="showMode('unsafe')" style="cursor: pointer;"`
});
}
if (alerts.length === 0) {
alertsContainer.innerHTML = `
<div style="background: rgba(16, 185, 129, 0.1); border-left: 4px solid #10b981; padding: 12px; border-radius: 4px;">
<span style="font-size: 1.2rem;">✅</span>
<span style="margin-left: 8px; font-weight: 600; color: #10b981;">All systems healthy - No critical issues detected</span>
</div>
`;
} else {
alertsContainer.innerHTML = alerts.map(alert => `
<div class="alert alert-${alert.type}" style="background: rgba(${alert.type === 'danger' ? '239, 68, 68' : alert.type === 'warning' ? '245, 158, 11' : '59, 130, 246'}, 0.1); border-left: 4px solid ${alert.type === 'danger' ? '#ef4444' : alert.type === 'warning' ? '#f59e0b' : '#3b82f6'}; padding: 12px; border-radius: 4px; margin-bottom: 8px;" ${alert.action}>
<span style="font-size: 1.2rem;">${alert.icon}</span>
<span style="margin-left: 8px;">${alert.message}</span>
</div>
`).join('');
}
}
</script>
</body>
</html>