<!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 id="eventSummaryCard" style="margin-top: 12px; display: none;"></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 Relationship Graph -->
<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;">🕸️ Thread Relationship Graph</h2>
<div class="flex-gap-2">
<span class="badge badge-info" id="threadGraphEdgeCount">0 edges</span>
</div>
</div>
<div style="background: var(--bg2); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<div class="legend-item"><div class="legend-color" style="background: #10b981;"></div>Normal</div>
<div class="legend-item"><div class="legend-color" style="background: #f59e0b;"></div>Leak Risk</div>
<div class="legend-item"><div class="legend-color" style="background: #ef4444;"></div>High Risk</div>
<div class="legend-item"><div class="legend-color" style="background: #8b5cf6;"></div>Mixed</div>
<div class="legend-item" style="margin-left: 12px;"><div style="width: 20px; height: 2px; background: #3b82f6;"></div>Shared Type</div>
<div class="legend-item"><div style="width: 20px; height: 2px; background: #10b981; stroke-dasharray: 4,2;"></div>Shared Variable</div>
<div class="legend-item"><div style="width: 20px; height: 2px; background: #ef4444;"></div>Clone/Transfer</div>
<button class="btn-sm" onclick="Dashboard.threads.graph.resetZoom()">🔍 Reset</button>
</div>
</div>
<div class="graph-container" id="threadGraphContainer" style="border: 1px solid var(--border); border-radius: 8px; background: var(--bg); min-height: 300px;"></div>
</div>
<!-- Thread Detail Panel -->
<div id="threadDetailPanel" 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;" id="tdTitle">🧵 Thread Details</h3>
<button class="btn-sm" onclick="Dashboard.threads.hideDetail()">✕ Close</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px;">
<div class="detail-panel" id="tdMemByType"></div>
<div class="detail-panel" id="tdTopVariables"></div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="detail-panel" id="tdTopSources"></div>
<div class="detail-panel" id="tdUnsafeReports"></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>
<!-- Lifecycle Bars -->
<div class="card">
<div class="card-header">
<h2 class="card-title">⏳ Allocation Lifecycle Bars</h2>
<div class="flex-gap-2">
<select id="lcThreadFilter" onchange="Dashboard.timetravel.renderLifecycleTimeline()" class="input-sm">
<option value="all">All Threads</option>
</select>
<select id="lcTypeFilter" onchange="Dashboard.timetravel.renderLifecycleTimeline()" class="input-sm">
<option value="all">All Types</option>
</select>
<select id="lcStatusFilter" onchange="Dashboard.timetravel.renderLifecycleTimeline()" class="input-sm">
<option value="all">All Status</option>
<option value="active">Active Only</option>
<option value="leaked">Leaked Only</option>
<option value="freed">Freed Only</option>
</select>
<span class="badge badge-info" id="lcBarCount">0 bars</span>
</div>
</div>
<div id="lifecycleTimelineContainer" style="width: 100%; min-height: 300px; background: var(--bg); border-radius: 8px; overflow: hidden;"></div>
</div>
<!-- Time Slice Snapshots -->
<div class="card">
<div class="card-header">
<h2 class="card-title">⏱ Time Slice Snapshot</h2>
<span class="badge badge-info">Drag to explore memory at a point in time</span>
</div>
<div style="padding: 0 4px; margin-bottom: 12px;">
<input type="range" id="tsTimeSlider" min="0" max="100" value="100" oninput="Dashboard.timetravel.renderTimeSlice()" style="width: 100%; accent-color: var(--primary);">
<div class="flex-between" style="font-size: 0.75rem; color: var(--text2);">
<span id="tsTimeStart">T+0</span>
<span id="tsTimeCurrent">T+0</span>
<span id="tsTimeEnd">T+0</span>
</div>
</div>
<div id="tsStatsGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px;">
<div class="detail-panel" style="border-left: 4px solid #3b82f6; text-align: center;">
<div style="font-size: 0.7rem; color: var(--text2);">📦 Active Allocs</div>
<div class="stat-value" style="color: #3b82f6; font-size: 1.2rem;" id="tsAllocCount">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #10b981; text-align: center;">
<div style="font-size: 0.7rem; color: var(--text2);">💾 Live Bytes</div>
<div class="stat-value" style="color: #10b981; font-size: 1.2rem;" id="tsLiveBytes">0 B</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #f59e0b; text-align: center;">
<div style="font-size: 0.7rem; color: var(--text2);">📈 Peak Bytes</div>
<div class="stat-value" style="color: #f59e0b; font-size: 1.2rem;" id="tsPeakBytes">0 B</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #ef4444; text-align: center;">
<div style="font-size: 0.7rem; color: var(--text2);">🚨 Leak Candidates</div>
<div class="stat-value" style="color: #ef4444; font-size: 1.2rem;" id="tsLeakCandidates">0</div>
</div>
<div class="detail-panel" style="border-left: 4px solid #8b5cf6; text-align: center;">
<div style="font-size: 0.7rem; color: var(--text2);">⚠️ Unsafe Boundaries</div>
<div class="stat-value" style="color: #8b5cf6; font-size: 1.2rem;" id="tsUnsafeCount">0</div>
</div>
</div>
<div style="margin-top: 12px; display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
<div class="detail-panel" style="border-left: 4px solid #3b82f6;">
<div class="detail-label">💾 Memory by Thread</div>
<div id="tsMemByThread" style="font-size: 0.8rem; margin-top: 4px;"></div>
</div>
<div class="detail-panel" style="border-left: 4px solid #10b981;">
<div class="detail-label">📦 Memory by Type</div>
<div id="tsMemByType" style="font-size: 0.8rem; margin-top: 4px;"></div>
</div>
</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>#</th>
<th>Address</th>
<th>Variable</th>
<th>Size</th>
<th>Provenance</th>
<th>Evidence</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 class="detail-panel" style="border-left: 4px solid #8b5cf6;">
<div class="detail-label">Provenance</div>
<div id="ttProvenance" class="detail-value">-</div>
</div>
</div>
<h4 style="color: var(--text2); margin-bottom: 8px; margin-top: 12px;">📐 Type Layout</h4>
<div id="ttLayout" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; margin-bottom: 16px;"></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>
<!-- Unsafe Call-Stack View -->
<div class="card">
<div class="card-header">
<h2 class="card-title">📋 Unsafe Call-Stack View</h2>
<div class="flex-gap-2">
<span class="badge badge-danger" id="callStackGroupCount">0 groups</span>
<button class="btn-sm" onclick="toggleSection('callStackContainer', this)">▼ Expand</button>
</div>
</div>
<div style="background: linear-gradient(135deg, #ef444410 0%, #ef444420 100%); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid #ef444430;">
<div style="display: flex; align-items: center; gap: 12px;">
<span style="font-size: 1.5rem;">🪜</span>
<div style="color: var(--text2); font-size: 0.85rem;">
Unsafe reports grouped by source location. Each group shows the call-chain of boundary crossings and lifecycle events.
<a href="#" onclick="Dashboard.unsafe.renderCallStackView(); return false;" style="color: var(--primary);">Refresh</a>
</div>
</div>
</div>
<div id="callStackContainer" style="max-height: 450px; overflow-y: auto;"></div>
</div>
<!-- Enhanced FFI Boundary Flow -->
<div class="card" style="background: linear-gradient(135deg, #8b5cf610 0%, #8b5cf620 100%); border: 2px solid #8b5cf640;">
<div class="card-header">
<h2 class="card-title" style="color: #8b5cf6;">🔀 FFI Boundary Flow</h2>
<div class="flex-gap-2">
<span class="badge badge-info" id="ffiFlowCount">0 flows</span>
<span class="badge" style="background: #ef444430; color: #ef4444;" id="ffiMismatchCount">0 mismatches</span>
</div>
</div>
<div style="background: linear-gradient(135deg, #8b5cf610 0%, #8b5cf620 100%); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid #8b5cf640;">
<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;">
<span style="font-size: 1.5rem;">🛂</span>
<div style="color: var(--text2); font-size: 0.85rem; flex: 1;">
Memory crossing Rust ↔ FFI boundaries. Direction, ownership transfer, and mismatched allocation/deallocation sides are highlighted.
</div>
<button class="btn-sm" onclick="Dashboard.unsafe.renderFfiBoundaryFlow()">🔄 Refresh</button>
</div>
</div>
<div id="ffiBoundaryFlowContainer" style="min-height: 120px; background: var(--bg); border-radius: 8px; padding: 16px;"></div>
<div style="margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap; padding: 8px 0;" id="ffiFlowLegend"></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 DataIndex = (function() {
const idx = data.data_index || {};
const allocs = data.allocations || [];
const eventSummary = data.event_summary || {};
const sampling = data.sampling || {};
return {
allocationByPtr(ptr) {
const indices = idx.ptr_to_allocation && idx.ptr_to_allocation[ptr];
return indices && indices.length > 0 ? allocs[indices[0]] : null;
},
allocationsByThread(threadId) {
const indices = idx.thread_id_to_allocations && idx.thread_id_to_allocations[String(threadId)];
return indices ? indices.map(i => allocs[i]) : [];
},
allocationsByType(typeName) {
const indices = idx.type_name_to_allocations && idx.type_name_to_allocations[typeName];
return indices ? indices.map(i => allocs[i]) : [];
},
allocationsByVarName(varName) {
const indices = idx.var_name_to_allocations && idx.var_name_to_allocations[varName];
return indices ? indices.map(i => allocs[i]) : [];
},
allocationsBySource(file, line) {
const key = file + ':' + line;
const indices = idx.source_location_to_allocations && idx.source_location_to_allocations[key];
return indices ? indices.map(i => allocs[i]) : [];
},
eventsByThread(threadId) {
const indices = idx.thread_id_to_events && idx.thread_id_to_events[String(threadId)];
return indices ? indices.map(i => (data.events || [])[i]) : [];
},
passportByPtr(ptr) {
const indices = idx.allocation_ptr_to_passport && idx.allocation_ptr_to_passport[ptr];
return indices && indices.length > 0 ? indices.map(i => (data.passport_details || [])[i]) : [];
},
unsafeReportsByPtr(ptr) {
const indices = idx.allocation_ptr_to_unsafe_reports && idx.allocation_ptr_to_unsafe_reports[ptr];
return indices && indices.length > 0 ? indices.map(i => (data.unsafe_reports || [])[i]) : [];
}
};
})();
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');
function inferOwnershipModel(node) {
const tn = (node.typeName || '').toLowerCase();
if (node.isContainer) return 'Container';
if (tn.includes('arc<')) return 'Arc';
if (tn.includes('rc<')) return 'Rc';
if (tn.includes('box<')) return 'Box';
if (tn.includes('*const ') || tn.includes('*mut ') || tn.includes('raw') || tn.includes('ptr')) return 'RawPointer';
const alloc = DataIndex.allocationByPtr(node.ptr);
if (alloc && alloc.is_leaked) return 'Leaked';
return 'HeapOwner';
}
const ownerColorMap = { Container: '#f59e0b', Box: '#3b82f6', Rc: '#8b5cf6', Arc: '#8b5cf6', RawPointer: '#ef4444', Leaked: '#dc2626', HeapOwner: '#10b981' };
const ownerLabels = { Container: 'Container', Box: 'Box Owner', Rc: 'Rc Shared', Arc: 'Arc Shared', RawPointer: 'Raw Ptr', Leaked: '🚨 LEAKED', HeapOwner: 'Heap Owner' };
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());
nodes.forEach(n => { n.ownerModel = inferOwnershipModel(n); n.ownerColor = ownerColorMap[n.ownerModel] || '#64748b'; });
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 => d.ownerColor).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);">Ownership:</div><div style="color: ${d.ownerColor};">${ownerLabels[d.ownerModel] || d.ownerModel}</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)); },
selectNode(ptr) {
document.querySelectorAll('.mode-section').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.mode-tab').forEach(el => el.classList.remove('active'));
document.getElementById('mode-variable').classList.add('active');
const tab = document.querySelector('.mode-tab[onclick*="\'variable\'"]');
if (tab) tab.classList.add('active');
setTimeout(() => Dashboard.variableGraph.init(), 100);
setTimeout(() => {
if (this.g) this.g.selectAll('.node circle').attr('stroke', d => d.id === ptr ? '#f59e0b' : d.hasCycle ? '#ef4444' : 'none').attr('stroke-width', d => d.id === ptr ? 6 : d.hasCycle ? 3 : 0);
}, 400);
}
},
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>
`;
},
graph: {
simulation: null,
svg: null,
zoom: null,
g: null,
render() {
const container = document.getElementById('threadGraphContainer');
if (!container) return;
const threads = data.threads || [];
if (threads.length === 0) { container.innerHTML = '<div class="empty-state">No thread data for graph</div>'; return; }
container.innerHTML = '';
const allocs = data.allocations || [];
const unsafeReports = data.unsafe_reports || [];
const threadMap = {};
threads.forEach(t => { threadMap[String(t.thread_id)] = t; });
const leakByThread = {};
allocs.filter(a => a.is_leaked).forEach(a => { const tid = String(a.thread_id); leakByThread[tid] = (leakByThread[tid] || 0) + 1; });
const unsafeByThread = {};
unsafeReports.forEach(r => {
const linked = DataIndex.allocationByPtr(r.allocation_ptr);
if (linked) { const tid = String(linked.thread_id); unsafeByThread[tid] = (unsafeByThread[tid] || 0) + 1; }
});
const nodes = threads.map(t => {
const tid = String(t.thread_id);
const mem = t.current_memory_bytes || 0;
const leaks = leakByThread[tid] || 0;
const unsafes = unsafeByThread[tid] || 0;
let risk = 'normal';
if (leaks > 0 && unsafes > 0) risk = 'mixed';
else if (leaks > 0) risk = 'leak';
else if (unsafes > 0) risk = 'unsafe';
const colorMap = { normal: '#10b981', leak: '#f59e0b', unsafe: '#ef4444', mixed: '#8b5cf6' };
return { id: tid, label: 'T' + tid.slice(-4), memory: mem, allocCount: t.allocation_count || 0, leaks, unsafes, risk, color: colorMap[risk] || '#10b981' };
});
const memValues = nodes.map(n => n.memory);
const minMem = Math.min(...memValues) || 1;
const maxMem = Math.max(...memValues) || 1;
const edges = [];
const varByThread = {};
allocs.forEach(a => {
const tid = String(a.thread_id);
if (!varByThread[tid]) varByThread[tid] = new Set();
if (a.var_name) varByThread[tid].add(a.var_name);
});
const typeByThread = {};
allocs.forEach(a => {
const tid = String(a.thread_id);
if (!typeByThread[tid]) typeByThread[tid] = new Set();
if (a.type_name) typeByThread[tid].add(a.type_name);
});
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const tida = nodes[i].id, tidb = nodes[j].id;
const sharedVars = [...(varByThread[tida] || [])].filter(v => (varByThread[tidb] || []).has(v));
const sharedTypes = [...(typeByThread[tida] || [])].filter(v => (typeByThread[tidb] || []).has(v));
if (sharedVars.length > 0) edges.push({ source: tida, target: tidb, type: 'variable', label: 'Shared Var: ' + sharedVars.slice(0, 2).join(', '), strength: Math.min(1, sharedVars.length / 3), color: '#10b981', dash: '4,2' });
if (sharedTypes.length > 0) edges.push({ source: tida, target: tidb, type: 'type', label: 'Shared Type: ' + sharedTypes.slice(0, 2).join(', '), strength: Math.min(1, sharedTypes.length / 5), color: '#3b82f6', dash: null });
}
}
(data.relationships || []).forEach(r => {
const srcAlloc = DataIndex.allocationByPtr(r.source_ptr);
const tgtAlloc = DataIndex.allocationByPtr(r.target_ptr);
if (srcAlloc && tgtAlloc && String(srcAlloc.thread_id) !== String(tgtAlloc.thread_id)) {
edges.push({ source: String(srcAlloc.thread_id), target: String(tgtAlloc.thread_id), type: 'transfer', label: r.relationship_type || 'transfer', strength: (r.strength || 0.5), color: '#ef4444', dash: null });
}
});
document.getElementById('threadGraphEdgeCount').textContent = edges.length + ' edges';
const width = container.clientWidth || 700;
const height = 300;
Dashboard.threads.graph.svg = d3.select(container).append('svg').attr('width', '100%').attr('height', height).attr('viewBox', [0, 0, width, height]);
Dashboard.threads.graph.zoom = d3.zoom().on('zoom', (event) => { if (Dashboard.threads.graph.g) Dashboard.threads.graph.g.attr('transform', event.transform); });
Dashboard.threads.graph.svg.call(Dashboard.threads.graph.zoom);
Dashboard.threads.graph.g = Dashboard.threads.graph.svg.append('g');
Dashboard.threads.graph.svg.append('defs').append('marker').attr('id', 'threadArrow').attr('viewBox', '-0 -5 10 10').attr('refX', 25).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 link = Dashboard.threads.graph.g.append('g').selectAll('line').data(edges).join('line').attr('stroke', d => d.color).attr('stroke-width', d => 1 + d.strength * 2).attr('stroke-dasharray', d => d.dash || 'none').attr('stroke-opacity', 0.6).attr('marker-end', 'url(#threadArrow)');
link.append('title').text(d => d.label);
const node = Dashboard.threads.graph.g.append('g').selectAll('g').data(nodes).join('g').attr('class', 'node').style('cursor', 'pointer').on('click', (event, d) => Dashboard.threads.showDetail(d.id));
node.append('circle').attr('r', d => 10 + (d.memory / (maxMem || 1)) * 20).attr('fill', d => d.color).attr('stroke', '#fff').attr('stroke-width', 2).attr('opacity', 0.85);
node.append('text').attr('dx', d => 12 + (d.memory / (maxMem || 1)) * 20).attr('dy', 4).text(d => d.label).style('font-size', '11px').style('fill', 'var(--text)').style('font-weight', '600');
const tooltip = d3.select('#tooltip');
node.on('mouseover', (event, d) => {
tooltip.style('display', 'block').html(`<div style="min-width:150px;"><div style="font-weight:600;margin-bottom:4px;">Thread ${d.id}</div><table style="font-size:12px;border-collapse:collapse;"><tr><td style="color:var(--text2);padding:1px 4px;">Risk</td><td style="padding:1px 4px;">${d.risk}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Memory</td><td style="padding:1px 4px;">${Utils.formatBytes(d.memory)}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Allocs</td><td style="padding:1px 4px;">${d.allocCount}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Leaks</td><td style="padding:1px 4px;">${d.leaks}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Unsafe</td><td style="padding:1px 4px;">${d.unsafes}</td></tr></table></div>`).style('left', (event.pageX + 15) + 'px').style('top', (event.pageY - 10) + 'px');
}).on('mouseout', () => tooltip.style('display', 'none'));
Dashboard.threads.graph.simulation = d3.forceSimulation(nodes).force('link', d3.forceLink(edges).id(d => d.id).distance(120)).force('charge', d3.forceManyBody().strength(-300)).force('center', d3.forceCenter(width / 2, height / 2)).force('collision', d3.forceCollide().radius(40));
Dashboard.threads.graph.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 (Dashboard.threads.graph.svg && Dashboard.threads.graph.zoom) Dashboard.threads.graph.svg.transition().duration(500).call(Dashboard.threads.graph.zoom.transform, d3.zoomIdentity); }
},
showDetail(threadId) {
const panel = document.getElementById('threadDetailPanel');
if (!panel) return;
panel.style.display = 'block';
document.getElementById('tdTitle').textContent = '🧵 Thread ' + threadId + ' Details';
const allocs = DataIndex.allocationsByThread(threadId);
const typeMap = {};
allocs.forEach(a => { const cat = Dashboard.timetravel.classifyType(a.type_name); typeMap[cat] = (typeMap[cat] || 0) + (parseInt(a.size) || 0); });
const typeHtml = '<div class="detail-label">💾 Memory by Type</div>' + Object.entries(typeMap).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([cat, bytes]) => `<div class="flex-between" style="padding:2px 0;"><span style="color:${Dashboard.timetravel.getCategoryColor(cat)};">${cat}</span><span>${Utils.formatBytes(bytes)}</span></div>`).join('') || '<div style="color:var(--text2);">None</div>';
document.getElementById('tdMemByType').innerHTML = typeHtml;
const varMap = {};
allocs.forEach(a => { if (a.var_name) varMap[a.var_name] = (varMap[a.var_name] || 0) + (parseInt(a.size) || 0); });
const varHtml = '<div class="detail-label">📦 Top Variables</div>' + Object.entries(varMap).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([name, bytes]) => `<div class="flex-between" style="padding:2px 0;"><span>${name}</span><span>${Utils.formatBytes(bytes)}</span></div>`).join('') || '<div style="color:var(--text2);">None</div>';
document.getElementById('tdTopVariables').innerHTML = varHtml;
const srcMap = {};
allocs.forEach(a => { if (a.source_file) { const key = a.source_file + (a.source_line ? ':' + a.source_line : ''); srcMap[key] = (srcMap[key] || 0) + 1; } });
const srcHtml = '<div class="detail-label">📍 Top Source Locations</div>' + Object.entries(srcMap).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([loc, count]) => `<div class="flex-between" style="padding:2px 0;"><span style="font-size:0.75rem;">${loc}</span><span>${count}x</span></div>`).join('') || '<div style="color:var(--text2);">None</div>';
document.getElementById('tdTopSources').innerHTML = srcHtml;
const unsafeList = [];
(data.unsafe_reports || []).forEach(r => {
const linked = r.allocation_ptr ? DataIndex.allocationByPtr(r.allocation_ptr) : null;
if (linked && String(linked.thread_id) === threadId) unsafeList.push(r);
});
const unsafeHtml = '<div class="detail-label">⚠️ Unsafe Reports</div>' + (unsafeList.length > 0 ? unsafeList.slice(0, 5).map(r => `<div class="flex-between" style="padding:2px 0;"><span style="color:${r.risk_level === 'high' ? '#ef4444' : '#f59e0b'};">${r.var_name || 'unknown'}</span><span class="badge ${r.risk_level === 'high' ? 'badge-danger' : 'badge-warning'}">${r.risk_level}</span></div>`).join('') : '<div style="color:var(--text2);">None</div>');
document.getElementById('tdUnsafeReports').innerHTML = unsafeHtml;
},
hideDetail() { document.getElementById('threadDetailPanel').style.display = 'none'; },
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();
this.graph.render();
}
},
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 = 'none';
svgContainer.style.display = 'block';
countBadge.textContent = taskGraphData.nodes.length + ' tasks (graph)';
this.renderTree(taskGraphData);
return;
}
const asyncTasks = data.async_tasks || [];
if (asyncTasks.length > 0) {
emptyState.style.display = 'none';
svgContainer.style.display = 'block';
countBadge.textContent = asyncTasks.length + ' tasks (async)';
svgContainer.innerHTML = '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px;padding:12px;">' +
asyncTasks.slice(0, 50).map(t => {
const statusColor = t.is_completed ? '#10b981' : '#3b82f6';
const eff = (t.efficiency_score || 0) * 100;
return '<div style="background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:12px;border-left:4px solid ' + statusColor + ';">' +
'<div class="flex-between" style="margin-bottom:8px;"><span style="font-weight:600;color:var(--primary);">#' + t.task_id + '</span>' +
'<span class="badge" style="background:' + statusColor + '30;color:' + statusColor + ';">' + (t.is_completed ? 'Completed' : 'Active') + '</span></div>' +
'<div style="font-size:0.85rem;margin-bottom:8px;">' + (t.task_name || 'Unknown') + '</div>' +
'<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:0.8rem;">' +
'<div><span class="detail-label">Memory</span><div style="font-weight:600;">' + Utils.formatBytes(t.current_memory || 0) + '</div></div>' +
'<div><span class="detail-label">Peak</span><div style="font-weight:600;">' + Utils.formatBytes(t.peak_memory || 0) + '</div></div>' +
'<div><span class="detail-label">Allocs</span><div style="font-weight:600;">' + (t.total_allocations || 0) + '</div></div>' +
'<div><span class="detail-label">Dur.</span><div style="font-weight:600;">' + (t.duration_ms || 0).toFixed(1) + 'ms</div></div>' +
'<div><span class="detail-label">Eff.</span><div style="font-weight:600;">' + eff.toFixed(0) + '%</div></div>' +
'<div><span class="detail-label">Type</span><div style="font-weight:600;font-size:0.75rem;">' + (t.task_type || '—') + '</div></div>' +
'</div>' + (t.has_potential_leak ? '<div style="margin-top:8px;color:#f59e0b;font-size:0.8rem;">⚠️ Potential Leak</div>' : '') +
'</div>';
}).join('') + '</div>';
return;
}
emptyState.style.display = 'flex';
svgContainer.style.display = 'none';
countBadge.textContent = '0 tasks';
},
renderTree(graphData) {
const container = document.getElementById('taskGraphSvg');
if (!container) return;
const width = container.clientWidth || 800;
const height = Math.max(400, container.clientHeight || 600);
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;
}
const zoomGroup = svg.append('g').attr('class', 'zoom-group');
const zoom = d3.zoom()
.scaleExtent([0.1, 5])
.on('zoom', (event) => {
zoomGroup.attr('transform', event.transform);
});
svg.call(zoom);
svg.on('dblclick.zoom', (event) => {
event.preventDefault();
svg.transition().duration(500).call(
zoom.transform,
d3.zoomIdentity,
d3.pointer(event, svg.node())
);
});
this.renderSimpleTree(zoomGroup, trees, width, height, 0, 0);
setTimeout(() => {
const bbox = zoomGroup.node()?.getBBox();
if (bbox && bbox.width > 0 && bbox.height > 0) {
const pad = 40;
const scale = Math.min(
(width - pad * 2) / bbox.width,
(height - pad * 2) / bbox.height,
1.5
);
const tx = (width - bbox.width * scale) / 2 - bbox.x * scale;
const ty = (height - bbox.height * scale) / 2 - bbox.y * scale;
svg.transition().duration(500).call(
zoom.transform,
d3.zoomIdentity.translate(tx, ty).scale(scale)
);
}
}, 50);
},
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;
let clickStartPos = null;
const renderNode = (node, x, y) => {
const g = svg.append('g')
.attr('transform', `translate(${x}, ${y})`)
.style('cursor', 'pointer');
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`);
const drag = d3.drag()
.on('start', (event) => {
clickStartPos = { x: event.x, y: event.y };
})
.on('drag', (event) => {
const transform = g.attr('transform');
const match = transform.match(/translate\(([-\d.]+),\s*([-\d.]+)\)/);
let cx = x, cy = y;
if (match) { cx = parseFloat(match[1]); cy = parseFloat(match[2]); }
g.attr('transform', `translate(${cx + event.dx}, ${cy + event.dy})`);
})
.on('end', (event) => {
if (clickStartPos) {
const dist = Math.hypot(event.x - clickStartPos.x, event.y - clickStartPos.y);
clickStartPos = null;
if (dist < 5) {
this.showTaskDetails(node);
}
}
});
g.call(drag);
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('');
this.renderCallStackView();
this.renderFfiBoundaryFlow();
},
renderCallStackView() {
const container = document.getElementById('callStackContainer');
const countEl = document.getElementById('callStackGroupCount');
if (!container) return;
const reports = data.unsafe_reports || [];
const allocs = data.allocations || [];
const sourceMap = {};
reports.forEach(r => {
const alloc = DataIndex.allocationByPtr(r.allocation_ptr) || allocs.find(a => a.address === r.allocation_ptr);
const key = (alloc && alloc.source_file) ? `${alloc.source_file}:${alloc.source_line || 0}` : 'unknown:0';
if (!sourceMap[key]) sourceMap[key] = { file: alloc ? alloc.source_file : 'unknown', line: alloc ? alloc.source_line : 0, reports: [] };
sourceMap[key].reports.push(r);
});
const groups = Object.values(sourceMap).sort((a, b) => b.reports.length - a.reports.length);
if (countEl) countEl.textContent = groups.length + ' groups';
if (groups.length === 0) { container.innerHTML = '<div class="empty-state">No unsafe reports</div>'; return; }
const flowLegend = { 'RustToFfi': { icon: '→', color: '#f59e0b', label: 'Rust→FFI' }, 'FfiToRust': { icon: '←', color: '#10b981', label: 'FFI→Rust' }, 'OwnershipTransfer': { icon: '⇄', color: '#8b5cf6', label: 'Ownership Transfer' }, 'SharedAccess': { icon: '⇌', color: '#3b82f6', label: 'Shared Access' }, 'Unknown': { icon: '?', color: '#64748b', label: 'Unknown' } };
container.innerHTML = groups.map((g, gi) => `<div style="margin-bottom: 12px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;">
<div onclick="toggleTable('callStackGroup_${gi}', this)" style="cursor: pointer; display: flex; align-items: center; gap: 12px; padding: 10px 14px; background: var(--bg3); border-bottom: 1px solid var(--border);">
<span style="font-size: 1.2rem;">📂</span>
<div style="flex: 1;">
<div style="font-weight: 600; font-size: 0.9rem;">${g.file}:${g.line} <a href="#" onclick="jumpToSource('${g.file}', ${g.line}); return false;" style="color: var(--primary); font-size: 0.75rem; text-decoration: underline;">Jump</a></div>
<div style="color: var(--text2); font-size: 0.75rem;">${g.reports.length} report(s) | ${g.reports.filter(r => r.risk_level === 'high').length} high risk</div>
</div>
<span style="font-size: 0.8rem; color: var(--text2);">▼</span>
</div>
<div id="callStackGroup_${gi}" style="display: none; padding: 12px;">
${g.reports.map(r => `<div class="detail-panel" style="border-left: 4px solid ${r.risk_level === 'high' ? '#ef4444' : r.risk_level === 'medium' ? '#f59e0b' : '#10b981'}; margin-bottom: 8px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<div style="font-weight: 600; color: var(--text);">${r.var_name || 'unknown'}</div>
<div style="color: var(--text2); font-size: 0.8rem;">${r.type_name || 'Unknown'} | ${Utils.formatBytes(r.size_bytes || 0)} | <code style="font-size: 0.75rem;">${r.allocation_ptr || 'N/A'}</code></div>
</div>
<span class="badge ${r.risk_level === 'high' ? 'badge-danger' : r.risk_level === 'medium' ? 'badge-warning' : 'badge-success'}">${r.risk_level || 'unknown'}</span>
</div>
${r.risk_factors && r.risk_factors.length > 0 ? `<div style="margin-top: 6px;"><div style="font-size: 0.8rem; color: var(--text2);">Risk Factors:</div><ul style="margin: 4px 0 0 16px; font-size: 0.8rem; color: var(--text2);">${r.risk_factors.map(f => `<li>${f}</li>`).join('')}</ul></div>` : ''}
${r.cross_boundary_events && r.cross_boundary_events.length > 0 ? `<div style="margin-top: 6px;"><div style="font-size: 0.8rem; color: var(--text2);">Call Stack:</div><div style="display: flex; flex-direction: column; gap: 4px; margin-top: 4px;">${r.cross_boundary_events.map((e, ei) => `<div style="display: flex; align-items: center; gap: 6px; font-size: 0.75rem; padding: 4px 8px; background: var(--bg3); border-radius: 4px;"><span>${flowLegend[e.event_type]?.icon || '→'}</span><span style="color: ${flowLegend[e.event_type]?.color || '#64748b'};">${e.event_type || 'Unknown'}</span><span style="color: var(--text2);">${e.from_context || ''} → ${e.to_context || ''}</span></div>`).join('')}</div></div>` : ''}
<div style="margin-top: 6px; display: flex; gap: 8px; flex-wrap: wrap;">
<button class="btn-sm" onclick="Dashboard.timetravel.showDetail('${r.allocation_ptr}')">⏱ Timeline</button>
<button class="btn-sm" onclick="Dashboard.variableGraph.selectNode('${r.allocation_ptr}')">🔗 Variable Graph</button>
</div>
</div>`).join('')}
</div>
</div>`).join('');
},
renderFfiBoundaryFlow() {
const container = document.getElementById('ffiBoundaryFlowContainer');
const countEl = document.getElementById('ffiFlowCount');
const mismatchEl = document.getElementById('ffiMismatchCount');
const legendEl = document.getElementById('ffiFlowLegend');
if (!container) return;
const reports = data.unsafe_reports || [];
const flowEvents = [];
reports.forEach(r => { if (r.cross_boundary_events) r.cross_boundary_events.forEach(e => flowEvents.push({...e, var_name: r.var_name, allocation_ptr: r.allocation_ptr, is_leaked: r.is_leaked, risk_level: r.risk_level })); });
const distinctTypes = [...new Set(flowEvents.map(e => e.event_type))];
const flowTypeColors = { 'RustToFfi': '#f59e0b', 'FfiToRust': '#10b981', 'OwnershipTransfer': '#8b5cf6', 'SharedAccess': '#3b82f6', 'Unknown': '#64748b' };
const mismatches = flowEvents.filter(e => e.event_type === 'OwnershipTransfer' || (e.from_context && e.to_context && e.from_context.includes('alloc') !== e.to_context.includes('free')));
if (countEl) countEl.textContent = flowEvents.length + ' flows';
if (mismatchEl) mismatchEl.textContent = mismatches.length + ' mismatches';
if (legendEl) legendEl.innerHTML = Object.entries(flowTypeColors).map(([type, color]) => `<span style="display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; background: var(--bg3); border-radius: 4px; font-size: 0.75rem;"><span style="display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: ${color};"></span>${type}</span>`).join('');
if (flowEvents.length === 0) { container.innerHTML = '<div class="empty-state">No FFI boundary flows</div>'; return; }
const sortedEvents = flowEvents.sort((a, b) => (a.timestamp || 0) - (b.timestamp || 0));
container.innerHTML = '<div style="display: flex; flex-direction: column; gap: 8px;">' + sortedEvents.slice(0, 30).map(e => {
const color = flowTypeColors[e.event_type] || '#64748b';
const isMismatch = e.event_type === 'OwnershipTransfer' || (e.from_context && e.to_context && e.from_context.includes('alloc') !== e.to_context.includes('free'));
return `<div style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; background: var(--bg3); border-radius: 8px; border-left: 4px solid ${color}; ${isMismatch ? 'border: 2px solid #ef4444;' : ''}">
<span style="font-size: 1.2rem; color: ${color};">${e.event_type === 'RustToFfi' ? '🦀→🌐' : e.event_type === 'FfiToRust' ? '🌐→🦀' : e.event_type === 'OwnershipTransfer' ? '⇄' : e.event_type === 'SharedAccess' ? '⇌' : '→'}</span>
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 500; font-size: 0.85rem; color: ${color};">${e.event_type || 'Unknown'}</span>
${isMismatch ? '<span class="badge badge-danger">⚠ Mismatch</span>' : ''}
</div>
<div style="color: var(--text2); font-size: 0.75rem;">
${e.from_context || '?'} → ${e.to_context || '?'} | ${e.var_name || 'unknown'} | <code style="font-size: 0.7rem;">${e.allocation_ptr ? e.allocation_ptr.slice(-8) : 'N/A'}</code>
</div>
</div>
<span style="font-size: 0.7rem; color: var(--text2);">${Utils.formatTime(parseInt(e.timestamp || 0))}</span>
</div>`;
}).join('') + '</div>';
}
},
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>'; const genBadge = a.generation_id > 0 ? `<span class="badge" style="background:${a.generation_id > 1 ? '#ef444430' : '#3b82f630'};color:${a.generation_id > 1 ? '#ef4444' : '#3b82f6'};">G${a.generation_id}</span>` : ''; const provIcon = { allocator: '💠', clone: '📋', smart_pointer: '🔗', reallocation: '🔄', reallocated_then_leaked: '🚨' }[a.provenance] || '💠'; const confMap = { Confirmed: '🔴', Likely: '🟡', Possible: '🟢', Unknown: '⚪' }; const confEl = a.confidence ? `<span style="font-size:0.75rem;" title="Confidence: ${a.confidence}">${confMap[a.confidence] || '⚪'}</span>` : ''; return `<tr onclick="Dashboard.timetravel.showDetail('${a.address}')" style="cursor: pointer;"><td style="font-size:0.75rem;color:var(--text2);">${genBadge}</td><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.8rem;"><span title="Provenance: ${a.provenance || 'allocator'}">${provIcon} ${a.provenance || 'allocator'}</span></td><td style="font-size:0.75rem;">${confEl} ${a.evidence || ''}</td><td style="font-size: 0.85rem;">${sourceLoc}</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 = DataIndex.allocationByPtr(address) || 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 + (alloc.generation_id ? ` (G${alloc.generation_id})` : ''); 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>'; document.getElementById('ttProvenance').textContent = (alloc.provenance || 'allocator') + ' | Evidence: ' + (alloc.evidence || 'Unknown') + ' | Confidence: ' + (alloc.confidence || 'Unknown'); 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(''); const layoutEl = document.getElementById('ttLayout'); if (layoutEl) { const ls = alloc.layout_snapshot; if (ls) { const layoutCards = []; if (ls.size_of_t) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid var(--info);padding:8px;"><span class="detail-label">SizeOf(T)</span><div class="detail-value">${Utils.formatBytes(ls.size_of_t)}</div></div>`); if (ls.align_of_t) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid var(--warning);padding:8px;"><span class="detail-label">AlignOf(T)</span><div class="detail-value">${ls.align_of_t}</div></div>`); if (ls.layout_kind) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid var(--primary);padding:8px;"><span class="detail-label">Layout Kind</span><div class="detail-value">${ls.layout_kind}</div></div>`); if (ls.repr_hint && ls.repr_hint !== 'Unknown') layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #8b5cf6;padding:8px;"><span class="detail-label">Repr Hint</span><div class="detail-value">${ls.repr_hint}</div></div>`); if (ls.pointer_width && ls.pointer_width !== 'Unknown') layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #f59e0b;padding:8px;"><span class="detail-label">Ptr Width</span><div class="detail-value">${ls.pointer_width}</div></div>`); if (ls.element_size) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #10b981;padding:8px;"><span class="detail-label">Element Size</span><div class="detail-value">${ls.element_size} B</div></div>`); if (ls.container_len !== null && ls.container_len !== undefined) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #06b6d4;padding:8px;"><span class="detail-label">Length</span><div class="detail-value">${ls.container_len}</div></div>`); if (ls.container_capacity !== null && ls.container_capacity !== undefined) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #3b82f6;padding:8px;"><span class="detail-label">Capacity</span><div class="detail-value">${ls.container_capacity}</div></div>`); if (ls.strong_count !== null && ls.strong_count !== undefined) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #ef4444;padding:8px;"><span class="detail-label">Strong Ref</span><div class="detail-value">${ls.strong_count}</div></div>`); if (ls.weak_count !== null && ls.weak_count !== undefined) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #64748b;padding:8px;"><span class="detail-label">Weak Ref</span><div class="detail-value">${ls.weak_count}</div></div>`); if (ls.pointee_type) layoutCards.push(`<div class="detail-panel" style="border-left:4px solid #8b5cf6;padding:8px;"><span class="detail-label">Pointee</span><div class="detail-value" style="font-size:0.75rem;">${ls.pointee_type}</div></div>`); layoutEl.innerHTML = layoutCards.join('') || '<div style="color:var(--text2);font-size:0.85rem;">No type layout data</div>'; } else { layoutEl.innerHTML = '<div style="color:var(--text2);font-size:0.85rem;">No layout snapshot</div>'; } } },
renderTimeSlice() {
const slider = document.getElementById('tsTimeSlider');
if (!slider) return;
const allocs = data.allocations || [];
if (allocs.length === 0) return;
const minTime = d3.min(allocs, a => a.timestamp_alloc || 0);
const maxTime = d3.max(allocs, a => Math.max(a.timestamp_dealloc || 0, (a.timestamp_alloc || 0) + (a.lifetime_ms || 0) * 1000000));
const timeRange = (maxTime - minTime) || 1;
const pct = parseInt(slider.value) / 100;
const currentTime = minTime + pct * timeRange;
document.getElementById('tsTimeCurrent').textContent = 'T+' + Utils.formatTime(currentTime - minTime);
const active = allocs.filter(a => (a.timestamp_alloc || 0) <= currentTime && (!a.timestamp_dealloc || a.timestamp_dealloc > currentTime));
const liveBytes = active.reduce((s, a) => s + (parseInt(a.size) || 0), 0);
let peakBytes = 0;
for (let i = 0; i <= parseInt(slider.value); i++) {
const t = minTime + (i / 100) * timeRange;
const bAtT = allocs.filter(a => (a.timestamp_alloc || 0) <= t && (!a.timestamp_dealloc || a.timestamp_dealloc > t)).reduce((s, a) => s + (parseInt(a.size) || 0), 0);
if (bAtT > peakBytes) peakBytes = bAtT;
}
const leakCandidates = active.filter(a => a.is_leaked).length;
const unsafeReports = data.unsafe_reports || [];
const unsafeCount = unsafeReports.filter(r => r.allocation_ptr && active.some(a => a.address === r.allocation_ptr)).length;
document.getElementById('tsAllocCount').textContent = active.length;
document.getElementById('tsLiveBytes').textContent = Utils.formatBytes(liveBytes);
document.getElementById('tsPeakBytes').textContent = Utils.formatBytes(peakBytes);
document.getElementById('tsLeakCandidates').textContent = leakCandidates;
document.getElementById('tsUnsafeCount').textContent = unsafeCount;
const threadMap = {};
active.forEach(a => { const tid = a.thread_id || '?'; threadMap[tid] = (threadMap[tid] || 0) + (parseInt(a.size) || 0); });
const threadHtml = Object.entries(threadMap).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([tid, bytes]) => `<div class="flex-between" style="padding:2px 0;"><span>Thread ${tid}</span><span style="font-weight:600;">${Utils.formatBytes(bytes)}</span></div>`).join('') || '<div style="color:var(--text2);">None</div>';
document.getElementById('tsMemByThread').innerHTML = threadHtml;
const typeMap = {};
active.forEach(a => { const cat = this.classifyType(a.type_name); typeMap[cat] = (typeMap[cat] || 0) + (parseInt(a.size) || 0); });
const typeHtml = Object.entries(typeMap).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([cat, bytes]) => `<div class="flex-between" style="padding:2px 0;"><span style="color:${this.getCategoryColor(cat)};">${cat}</span><span style="font-weight:600;">${Utils.formatBytes(bytes)}</span></div>`).join('') || '<div style="color:var(--text2);">None</div>';
document.getElementById('tsMemByType').innerHTML = typeHtml;
},
classifyType(typeName) {
if (!typeName) return 'Unknown';
const t = typeName.toLowerCase();
if (t.includes('vec') || t.includes('string') || t.includes('hashmap') || t.includes('hashset') || t.includes('btreemap') || t.includes('btreeset') || t.includes('linkedlist') || t.includes('vecdeque')) return 'Container';
if (t.includes('arc<') || t.includes('rc<') || t.includes('box<')) return 'SmartPointer';
if (t.includes('mutex') || t.includes('rwlock') || t.includes('refcell') || t.includes('cell<') || t.includes('atomic') || t.includes('unsafecell')) return 'SyncPrimitive';
if (t.includes('fn ') || t.includes('closure') || t.includes('fn(')) return 'Function';
if (t.includes('i8') || t.includes('i16') || t.includes('i32') || t.includes('i64') || t.includes('u8') || t.includes('u16') || t.includes('u32') || t.includes('u64') || t.includes('usize') || t.includes('isize') || t.includes('f32') || t.includes('f64') || t.includes('bool') || t.includes('char')) return 'Primitive';
if (t.includes('*const') || t.includes('*mut') || t.includes('ptr')) return 'RawPointer';
if (t.includes('&') || t.includes('ref')) return 'Reference';
return 'Other';
},
getCategoryColor(category) {
const map = { 'Container': '#3b82f6', 'SmartPointer': '#8b5cf6', 'SyncPrimitive': '#f59e0b', 'Function': '#06b6d4', 'Primitive': '#10b981', 'RawPointer': '#ef4444', 'Reference': '#64748b', 'Other': '#94a3b8', 'Unknown': '#cbd5e1' };
return map[category] || '#94a3b8';
},
renderLifecycleTimeline() {
const container = document.getElementById('lifecycleTimelineContainer');
if (!container) return;
const threadFilter = document.getElementById('lcThreadFilter').value;
const typeFilter = document.getElementById('lcTypeFilter').value;
const statusFilter = document.getElementById('lcStatusFilter').value;
let allocs = data.allocations || [];
if (threadFilter !== 'all') allocs = allocs.filter(a => String(a.thread_id) === threadFilter);
if (typeFilter !== 'all') allocs = allocs.filter(a => this.classifyType(a.type_name) === typeFilter);
if (statusFilter === 'active') allocs = allocs.filter(a => !a.timestamp_dealloc && !a.is_leaked);
else if (statusFilter === 'leaked') allocs = allocs.filter(a => a.is_leaked);
else if (statusFilter === 'freed') allocs = allocs.filter(a => a.timestamp_dealloc > 0 && !a.is_leaked);
const bars = allocs.slice(0, 200);
document.getElementById('lcBarCount').textContent = bars.length + ' bars' + (allocs.length > 200 ? ' (showing 200)' : '');
container.innerHTML = '';
if (bars.length === 0) { container.innerHTML = '<div class="empty-state">No allocations match filters</div>'; return; }
const width = container.clientWidth || 800;
const height = Math.max(100, bars.length * 20 + 40);
container.style.height = height + 'px';
const svg = d3.select(container).append('svg').attr('width', '100%').attr('height', height).attr('viewBox', [0, 0, width, height]);
const minTime = d3.min(bars, a => a.timestamp_alloc || 0);
const maxTime = d3.max(bars, a => Math.max(a.timestamp_dealloc || 0, (a.timestamp_alloc || 0) + (a.lifetime_ms || 0) * 1000000));
const timeRange = (maxTime - minTime) || 1;
const padding = { top: 10, right: 20, bottom: 30, left: 120 };
const innerW = width - padding.left - padding.right;
const innerH = height - padding.top - padding.bottom;
const barH = Math.min(16, innerH / bars.length - 1);
const xScale = d3.scaleLinear().domain([minTime, maxTime + timeRange * 0.05]).range([0, innerW]);
const g = svg.append('g').attr('transform', `translate(${padding.left},${padding.top})`);
const tooltip = d3.select('#tooltip');
bars.forEach((a, i) => {
const cat = this.classifyType(a.type_name);
const color = this.getCategoryColor(cat);
const x0 = xScale(a.timestamp_alloc || 0);
const x1 = a.timestamp_dealloc > 0 ? xScale(a.timestamp_dealloc) : xScale(maxTime + timeRange * 0.05);
const y = i * (barH + 1);
g.append('rect').attr('x', x0).attr('y', y).attr('width', Math.max(2, x1 - x0)).attr('height', barH).attr('fill', a.is_leaked ? '#ef4444' : color).attr('opacity', a.is_leaked ? 0.9 : 0.7).attr('stroke', a.is_leaked ? '#dc2626' : 'none').attr('stroke-width', a.is_leaked ? 1.5 : 0).attr('stroke-dasharray', a.is_leaked ? '4,2' : 'none').attr('rx', 2).attr('ry', 2).style('cursor', 'pointer').on('mouseover', (event) => {
const lifetimeNs = a.lifetime_ms ? a.lifetime_ms * 1000000 : 0;
tooltip.style('display', 'block').html(`<div style="min-width: 230px;"><div style="font-weight:600;color:var(--primary);margin-bottom:4px;">${a.var_name || 'unknown'}</div><table style="font-size:12px;width:100%;border-collapse:collapse;"><tr><td style="color:var(--text2);padding:1px 4px;">Address</td><td style="padding:1px 4px;font-family:monospace;">${a.address || '0x0'}${a.generation_id ? ` <span style="color:#64748b;font-size:10px;">G${a.generation_id}</span>` : ''}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Type</td><td style="padding:1px 4px;">${a.type_name || 'Unknown'}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Category</td><td style="padding:1px 4px;">${cat}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Provenance</td><td style="padding:1px 4px;">${a.provenance || 'allocator'}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Evidence</td><td style="padding:1px 4px;">${a.evidence || 'Unknown'} — ${a.confidence || 'Unknown'}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Thread</td><td style="padding:1px 4px;">${a.thread_id || 'N/A'}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Size</td><td style="padding:1px 4px;">${Utils.formatBytes(parseInt(a.size) || 0)}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Lifetime</td><td style="padding:1px 4px;">${Utils.formatTime(lifetimeNs)}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Source</td><td style="padding:1px 4px;">${a.source_file || ''}${a.source_line ? ':' + a.source_line : ''}</td></tr><tr><td style="color:var(--text2);padding:1px 4px;">Status</td><td style="padding:1px 4px;">${a.is_leaked ? '<span style="color:#ef4444;font-weight:600;">LEAKED</span>' : a.timestamp_dealloc > 0 ? '<span style="color:#10b981;">Freed</span>' : '<span style="color:#3b82f6;">Active</span>'}</td></tr></table></div>`).style('left', (event.pageX + 15) + 'px').style('top', (event.pageY - 10) + 'px');
}).on('mouseout', () => tooltip.style('display', 'none')).on('click', () => this.showDetail(a.address));
g.append('text').attr('x', -4).attr('y', y + barH / 2 + 4).attr('text-anchor', 'end').attr('fill', 'var(--text2)').style('font-size', '10px').style('font-family', 'monospace').text((a.var_name || a.address || '').substring(0, 18));
});
g.append('g').attr('transform', `translate(0,${bars.length * (barH + 1)})`).call(d3.axisBottom(xScale).ticks(6).tickFormat(d => Utils.formatTime(d - minTime)));
},
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();
const threadSet = new Set((data.allocations || []).map(a => String(a.thread_id)));
const threadSel = document.getElementById('lcThreadFilter');
threadSet.forEach(tid => { const o = document.createElement('option'); o.value = tid; o.textContent = 'Thread ' + tid; threadSel.appendChild(o); });
const typeSet = new Set((data.allocations || []).map(a => this.classifyType(a.type_name)));
const typeSel = document.getElementById('lcTypeFilter');
typeSet.forEach(cat => { const o = document.createElement('option'); o.value = cat; o.textContent = cat; typeSel.appendChild(o); });
const allocs = data.allocations || [];
if (allocs.length > 0) {
const minT = d3.min(allocs, a => a.timestamp_alloc || 0);
const maxT = d3.max(allocs, a => Math.max(a.timestamp_dealloc || 0, (a.timestamp_alloc || 0) + (a.lifetime_ms || 0) * 1000000));
document.getElementById('tsTimeStart').textContent = 'T+0';
document.getElementById('tsTimeEnd').textContent = 'T+' + Utils.formatTime((maxT - minT));
}
this.renderTimeSlice();
this.renderLifecycleTimeline();
}
},
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 = DataIndex.allocationByPtr(address) || 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 provEl = document.getElementById('ttProvenance');
if (provEl) provEl.textContent = (alloc.provenance || 'allocator') + ' | Evidence: ' + (alloc.evidence || 'Unknown') + ' | Confidence: ' + (alloc.confidence || 'Unknown');
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('');
const layoutEl = document.getElementById('ttLayout');
if (layoutEl) {
const ls = alloc.layout_snapshot;
if (ls) {
const cards = [];
if (ls.size_of_t) cards.push(`<div class="detail-panel" style="border-left:4px solid var(--info);padding:8px;"><span class="detail-label">SizeOf(T)</span><div class="detail-value">${Utils.formatBytes(ls.size_of_t)}</div></div>`);
if (ls.align_of_t) cards.push(`<div class="detail-panel" style="border-left:4px solid var(--warning);padding:8px;"><span class="detail-label">AlignOf(T)</span><div class="detail-value">${ls.align_of_t}</div></div>`);
if (ls.layout_kind) cards.push(`<div class="detail-panel" style="border-left:4px solid var(--primary);padding:8px;"><span class="detail-label">Layout Kind</span><div class="detail-value">${ls.layout_kind}</div></div>`);
if (ls.repr_hint && ls.repr_hint !== 'Unknown') cards.push(`<div class="detail-panel" style="border-left:4px solid #8b5cf6;padding:8px;"><span class="detail-label">Repr Hint</span><div class="detail-value">${ls.repr_hint}</div></div>`);
if (ls.pointer_width && ls.pointer_width !== 'Unknown') cards.push(`<div class="detail-panel" style="border-left:4px solid #f59e0b;padding:8px;"><span class="detail-label">Ptr Width</span><div class="detail-value">${ls.pointer_width}</div></div>`);
if (ls.element_size) cards.push(`<div class="detail-panel" style="border-left:4px solid #10b981;padding:8px;"><span class="detail-label">Element Size</span><div class="detail-value">${ls.element_size} B</div></div>`);
if (ls.container_len !== null && ls.container_len !== undefined) cards.push(`<div class="detail-panel" style="border-left:4px solid #06b6d4;padding:8px;"><span class="detail-label">Length</span><div class="detail-value">${ls.container_len}</div></div>`);
if (ls.container_capacity !== null && ls.container_capacity !== undefined) cards.push(`<div class="detail-panel" style="border-left:4px solid #3b82f6;padding:8px;"><span class="detail-label">Capacity</span><div class="detail-value">${ls.container_capacity}</div></div>`);
if (ls.strong_count !== null && ls.strong_count !== undefined) cards.push(`<div class="detail-panel" style="border-left:4px solid #ef4444;padding:8px;"><span class="detail-label">Strong Ref</span><div class="detail-value">${ls.strong_count}</div></div>`);
if (ls.weak_count !== null && ls.weak_count !== undefined) cards.push(`<div class="detail-panel" style="border-left:4px solid #64748b;padding:8px;"><span class="detail-label">Weak Ref</span><div class="detail-value">${ls.weak_count}</div></div>`);
if (ls.pointee_type) cards.push(`<div class="detail-panel" style="border-left:4px solid #8b5cf6;padding:8px;"><span class="detail-label">Pointee</span><div class="detail-value" style="font-size:0.75rem;">${ls.pointee_type}</div></div>`);
layoutEl.innerHTML = cards.join('') || '<div style="color:var(--text2);font-size:0.85rem;">No type layout data</div>';
} else {
layoutEl.innerHTML = '<div style="color:var(--text2);font-size:0.85rem;">No layout snapshot</div>';
}
}
}
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();
}
function updateEventSummaryCard() {
const card = document.getElementById('eventSummaryCard');
if (!card) return;
const eventSummary = data.event_summary || {};
const sampling = data.sampling || {};
const events = data.events || [];
if (events.length === 0 && !sampling.is_sampled) { card.style.display = 'none'; return; }
card.style.display = 'block';
const totalEvents = eventSummary.total_event_count || events.length;
const exportedEvents = eventSummary.exported_event_count || events.length;
const isSampled = eventSummary.is_sampled || sampling.is_sampled;
const strategy = eventSummary.sampling_strategy || sampling.sampling_strategy || 'none';
card.innerHTML = '<div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 8px 12px; background: var(--bg3); border-radius: 8px; border: 1px solid var(--border);">' +
'<span style="font-size: 1.2rem;">📋</span>' +
'<span style="font-weight: 600; font-size: 0.85rem;">Events:</span>' +
'<span style="font-size: 0.85rem;">' + totalEvents + ' total</span>' +
(isSampled ? '<span class="badge badge-warning" title="Sampling strategy: ' + strategy + '">⚠️ Sampled (' + exportedEvents + ' displayed)</span>' : '<span class="badge badge-success">✅ Complete (' + exportedEvents + ' exported)</span>') +
'</div>';
}
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();
updateEventSummaryCard();
});
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>