<!DOCTYPE html>
<html class="dark" data-theme="amber" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>{{title}} — MemScope V2</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7.8.5/dist/d3.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700;800&family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<style>
html[data-theme="amber"] {
--primary: #F59E0B;
--primary-soft: #ffb95f;
--secondary: #ffb95f;
--tertiary: #e7c365;
--accent-amber: #F59E0B;
--warning: #F59E0B;
--success: #10B981;
--error: #ef4444;
--ke-primary: #F59E0B;
--ke-accent: #ffb95f;
--ke-accent-glow: rgba(245, 158, 11, 0.20);
--ke-on-accent: #3e2e00;
--ke-edge: #211f24;
--glow-color: rgba(245, 158, 11, 0.18);
--execution-tint: rgba(245, 158, 11, 0.18);
}
html[data-theme="indigo"] {
--primary: #cfbcff;
--primary-soft: #b39ddb;
--secondary: #b39ddb;
--tertiary: #9575cd;
--accent-amber: #cfbcff;
--warning: #b39ddb;
--success: #10B981;
--error: #ef4444;
--ke-primary: #cfbcff;
--ke-accent: #6750a4;
--ke-accent-glow: rgba(103, 80, 164, 0.22);
--ke-on-accent: #381e72;
--ke-edge: #2b2950;
--glow-color: rgba(103, 80, 164, 0.20);
--execution-tint: rgba(103, 80, 164, 0.18);
}
.text-primary { color: var(--primary) !important; }
.bg-primary { background-color: var(--primary) !important; }
.border-primary { border-color: var(--primary) !important; }
.text-secondary { color: var(--secondary) !important; }
.bg-secondary { background-color: var(--secondary) !important; }
.border-secondary { border-color: var(--secondary) !important; }
.text-tertiary { color: var(--tertiary) !important; }
.bg-tertiary { background-color: var(--tertiary) !important; }
.border-tertiary { border-color: var(--tertiary) !important; }
.text-accent-amber { color: var(--accent-amber) !important; }
.bg-accent-amber { background-color: var(--accent-amber) !important; }
.border-accent-amber { border-color: var(--accent-amber) !important; }
.text-warning { color: var(--warning) !important; }
.bg-warning { background-color: var(--warning) !important; }
.border-warning { border-color: var(--warning) !important; }
.bg-primary\/60 { background-color: color-mix(in srgb, var(--primary) 60%, transparent) !important; }
.bg-primary\/20 { background-color: color-mix(in srgb, var(--primary) 20%, transparent) !important; }
.bg-accent-amber\/20 { background-color: color-mix(in srgb, var(--accent-amber) 20%, transparent) !important; }
.bg-accent-amber\/40 { background-color: color-mix(in srgb, var(--accent-amber) 40%, transparent) !important; }
.bg-accent-amber\/60 { background-color: color-mix(in srgb, var(--accent-amber) 60%, transparent) !important; }
.border-l-4.border-primary { border-left-color: var(--primary) !important; }
.border-l-4.border-secondary { border-left-color: var(--secondary) !important; }
.border-l-4.border-tertiary { border-left-color: var(--tertiary) !important; }
.border-l-4.border-accent-amber { border-left-color: var(--accent-amber) !important; }
.border-l-4.border-success { border-left-color: var(--success) !important; }
.border-l-4.border-error { border-left-color: var(--error) !important; }
.border-l-4.border-warning { border-left-color: var(--warning) !important; }
.ke-side-item.active { background: #211f24; color: #e6e0e9; border-left: 2px solid var(--primary); }
.amber-glow { box-shadow: 0 0 15px -3px var(--glow-color); }
.indigo-glow { box-shadow: 0 0 15px 0 var(--glow-color); }
.execution-line { width: 1px; background: linear-gradient(to bottom, transparent, var(--execution-tint), transparent); }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #0f0d13; }
::-webkit-scrollbar-thumb { background: #36343a; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #494551; }
.rim-light { box-shadow: inset 0 1px 0 0 rgba(255, 255, 255, 0.05); }
.mode-section { display: none; }
.mode-section.active { display: block; }
.kpi-card { transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease; cursor: pointer; }
.kpi-card:hover { transform: translateY(-2px); box-shadow: 0 6px 18px -8px rgba(0,0,0,0.6); }
.kpi-card:active { transform: translateY(0); }
.kpi-flash { animation: kpi-flash .6s ease-out; }
@keyframes kpi-flash { 0% { box-shadow: 0 0 0 0 var(--primary); } 100% { box-shadow: 0 0 0 12px transparent; } }
.kpi-toast { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); background: #211f24; color: #e6e0e9; border: 1px solid var(--primary); border-radius: 8px; padding: 10px 16px; font-family: "JetBrains Mono", monospace; font-size: 12px; z-index: 100; opacity: 0; transition: opacity .18s ease; pointer-events: none; max-width: 80vw; }
.kpi-toast.visible { opacity: 1; }
.dashed-pulse { stroke-dasharray: 8 4; animation: dash 1s linear infinite; }
@keyframes dash { to { stroke-dashoffset: -12; } }
.chart-interactive { cursor: pointer; transition: opacity .15s ease, filter .15s ease, transform .15s ease; transform-box: fill-box; transform-origin: center; }
.chart-interactive:hover { opacity: 0.85; filter: brightness(1.25); transform: scale(1.03); }
.chart-tooltip { position: fixed; pointer-events: none; background: #211f24; color: #e6e0e9; border: 1px solid var(--primary); border-radius: 6px; padding: 8px 12px; font-family: "JetBrains Mono", monospace; font-size: 11px; z-index: 200; opacity: 0; transition: opacity .12s ease; max-width: 280px; box-shadow: 0 4px 16px -4px rgba(0,0,0,0.6); }
.chart-tooltip.visible { opacity: 1; }
.chart-tooltip .tt-label { color: var(--primary); font-weight: 700; letter-spacing: 0.05em; margin-bottom: 4px; }
.chart-tooltip .tt-row { display: flex; justify-content: space-between; gap: 12px; }
.chart-tooltip .tt-key { color: #948e9c; }
table tbody tr.chart-interactive:hover { background-color: color-mix(in srgb, var(--primary) 12%, transparent) !important; }
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"primary-container": "#6750a4",
"inverse-surface": "#e6e0e9",
"surface-tint": "#cfbcff",
"surface-dim": "#141218",
"on-tertiary": "#3e2e00",
"on-error": "#690005",
"on-tertiary-container": "#503d00",
"on-surface-variant": "#cbc4d2",
"background": "#141218",
"secondary-fixed": "#ffddb8",
"primary-fixed": "#e9ddff",
"on-primary-fixed": "#22005d",
"inverse-primary": "#6750a4",
"on-tertiary-fixed": "#241a00",
"on-secondary": "#472a00",
"on-secondary-fixed-variant": "#653e00",
"on-error-container": "#ffdad6",
"surface-variant": "#36343a",
"secondary-container": "#ee9800",
"surface": "#141218",
"on-surface": "#e6e0e9",
"on-secondary-fixed": "#2a1700",
"on-primary-fixed-variant": "#4f378a",
"error-container": "#93000a",
"outline": "#948e9c",
"on-secondary-container": "#5b3800",
"surface-container-high": "#2b292f",
"secondary": "#ffb95f",
"on-tertiary-fixed-variant": "#594400",
"primary": "#cfbcff",
"tertiary-container": "#c9a74d",
"surface-container-low": "#1d1b20",
"tertiary-fixed-dim": "#e7c365",
"on-primary": "#381e72",
"inverse-on-surface": "#322f35",
"surface-container": "#211f24",
"primary-fixed-dim": "#cfbcff",
"outline-variant": "#494551",
"error": "#ffb4ab",
"surface-container-lowest": "#0f0d13",
"secondary-fixed-dim": "#ffb95f",
"on-primary-container": "#e0d2ff",
"on-background": "#e6e0e9",
"tertiary-fixed": "#ffdf93",
"surface-bright": "#3b383e",
"tertiary": "#e7c365",
"surface-container-highest": "#36343a",
"accent-amber": "#F59E0B",
"brick-brown": "#8E443D",
"success": "#10B981",
"warning": "#F59E0B"
},
"borderRadius": { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px" },
"spacing": { "container-max": "1440px", "margin-mobile": "16px", "base": "4px", "margin-desktop": "32px", "gutter": "16px" },
"fontFamily": {
"headline-md": ["JetBrains Mono"], "code-sm": ["JetBrains Mono"], "label-caps": ["JetBrains Mono"],
"headline-lg-mobile": ["JetBrains Mono"], "headline-lg": ["JetBrains Mono"],
"body-lg": ["JetBrains Mono"], "body-sm": ["JetBrains Mono"], "data-mono": ["JetBrains Mono"]
},
"fontSize": {
"headline-md": ["20px", {"lineHeight": "28px", "fontWeight": "600"}],
"code-sm": ["13px", {"lineHeight": "18px", "fontWeight": "400"}],
"label-caps": ["12px", {"lineHeight": "16px", "letterSpacing": "0.05em", "fontWeight": "700"}],
"headline-lg-mobile": ["24px", {"lineHeight": "32px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"headline-lg": ["32px", {"lineHeight": "40px", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-lg": ["16px", {"lineHeight": "24px", "fontWeight": "400"}],
"body-sm": ["14px", {"lineHeight": "20px", "fontWeight": "400"}]
}
}
}
}
</script>
</head>
<body class="bg-surface-container-lowest text-on-surface font-body-sm selection:bg-primary-container selection:text-on-primary-container">
<div class="flex min-h-screen">
<nav class="fixed left-0 top-0 h-full flex flex-col bg-surface-container-lowest border-r border-outline-variant w-64 z-50">
<div class="p-6 flex flex-col gap-1">
<span class="font-label-caps text-label-caps text-on-surface-variant tracking-widest uppercase">memscope-rs</span>
<span class="font-body-sm text-body-sm text-outline">{{os_name}}</span>
</div>
<div class="flex-grow py-4">
<div class="ke-side-item active flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('overview', this)">
<span class="material-symbols-outlined">dashboard</span><span class="font-body-sm">Overview</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('thread', this)">
<span class="material-symbols-outlined">segment</span><span class="font-body-sm">Threads</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('task', this)">
<span class="material-symbols-outlined">query_stats</span><span class="font-body-sm">Async Tasks</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('taskgraph', this)">
<span class="material-symbols-outlined">account_tree</span><span class="font-body-sm">Task Graph</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('variable', this)">
<span class="material-symbols-outlined">link</span><span class="font-body-sm">Variables</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('passport', this)">
<span class="material-symbols-outlined">badge</span><span class="font-body-sm">Passports</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('ffi', this)">
<span class="material-symbols-outlined">extension</span><span class="font-body-sm">FFI Bridge</span>
</div>
<div class="ke-side-item flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high hover:text-on-surface transition-all cursor-pointer" onclick="showMode('unsafe', this)">
<span class="material-symbols-outlined">warning</span><span class="font-body-sm">Unsafe / Time</span>
</div>
</div>
<div class="p-4 flex flex-col gap-2 border-t border-outline-variant">
<div class="flex items-center gap-2 text-[10px] font-label-caps text-on-surface-variant">
<span class="w-1.5 h-1.5 rounded-full bg-primary animate-pulse"></span>NODE_READY
</div>
<div class="text-[10px] text-outline">UP: {{system_uptime_formatted}} · {{cpu_cores}} cores</div>
<div class="flex items-center gap-1 mt-2 pt-2 border-t border-outline-variant/50">
<span class="text-[10px] font-label-caps text-on-surface-variant mr-1">THEME</span>
<button id="theme-amber" onclick="switchTheme('amber')" title="Amber accent" class="flex-1 py-1 rounded border border-accent-amber bg-accent-amber/20 text-accent-amber font-label-caps text-[10px] hover:bg-accent-amber/40 transition-colors">amber</button>
<button id="theme-indigo" onclick="switchTheme('indigo')" title="Indigo accent" class="flex-1 py-1 rounded border border-outline-variant text-on-surface-variant font-label-caps text-[10px] hover:bg-surface-container-high transition-colors">indigo</button>
</div>
</div>
</nav>
<main class="ml-64 flex-grow p-margin-desktop max-w-container-max">
<header class="w-full h-16 border-b border-outline-variant bg-surface sticky top-0 z-40 flex justify-between items-center px-margin-desktop">
<div class="flex items-center gap-8">
<span class="font-headline-md text-headline-md font-bold text-primary tracking-tighter">memscope-rs</span>
</div>
<div class="flex items-center gap-4">
<span class="font-data-mono text-xs text-on-surface-variant">{{export_timestamp}}</span>
</div>
</header>
<div id="mode-overview" class="mode-section active">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">System Overview Dashboard</h1>
<p class="text-on-surface-variant font-body-sm">{{title}} · {{architecture}} · {{os_name}}</p>
</div>
<div class="flex gap-4">
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
<span class="font-label-caps text-label-caps text-primary">LIVE MONITORING</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light indigo-glow kpi-card" data-kpi="health_score" title="Health score — composite of leak/unsafe/FFI/passport risk. Click to drill." onclick="drillKpi('health_score','HEALTH_SCORE','{{health_score}}/100 — {{health_status}}')">
<div class="flex justify-between items-start mb-4">
<span class="font-label-caps text-label-caps text-on-surface-variant">HEALTH_SCORE</span>
<span class="material-symbols-outlined text-primary">shield</span>
</div>
<div class="flex items-end gap-2">
<span class="font-headline-lg text-headline-lg text-primary">{{health_score}}</span>
<span class="text-outline font-body-sm mb-2">/100</span>
</div>
<p class="text-[10px] text-outline mt-4 font-label-caps uppercase">{{health_status}}</p>
<div class="mt-4 w-full bg-surface h-1 rounded-full overflow-hidden">
<div class="bg-primary h-full" style="width: {{health_score}}%"></div>
</div>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="total_allocations" title="Total allocations captured since boot. Click to drill." onclick="drillKpi('total_allocations','TOTAL_ALLOCATIONS','{{total_allocations}} total · {{active_allocations}} active')">
<div class="flex justify-between items-start mb-4">
<span class="font-label-caps text-label-caps text-on-surface-variant">TOTAL_ALLOCATIONS</span>
<span class="material-symbols-outlined text-tertiary">database</span>
</div>
<div class="flex items-end gap-2">
<span class="font-headline-lg text-headline-lg text-tertiary">{{total_allocations}}</span>
</div>
<p class="text-[10px] text-outline mt-4 font-label-caps uppercase">Active: {{active_allocations}}</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="live_memory" title="Current live heap memory. Click to drill." onclick="drillKpi('live_memory','LIVE_MEMORY','{{total_memory}} live · peak {{peak_memory}}')">
<div class="flex justify-between items-start mb-4">
<span class="font-label-caps text-label-caps text-on-surface-variant">LIVE_MEMORY</span>
<span class="material-symbols-outlined text-secondary">memory</span>
</div>
<div class="flex items-end gap-2">
<span class="font-headline-lg text-headline-lg text-secondary">{{total_memory}}</span>
</div>
<p class="text-[10px] text-outline mt-4 font-label-caps uppercase">Peak: {{peak_memory}}</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="leak_count" title="Detected leaks + unsafe/FFI breakdown. Click to drill." onclick="drillKpi('leak_count','LEAK_DETECTED','{{leak_count}} leaks · {{unsafe_count}} unsafe · {{ffi_count}} FFI · safe {{safe_code_percent}}%')">
<div class="flex justify-between items-start mb-4">
<span class="font-label-caps text-label-caps text-on-surface-variant">LEAK_DETECTED</span>
<span class="material-symbols-outlined text-error">emergency</span>
</div>
<div class="flex items-end gap-2">
<span class="font-headline-lg text-headline-lg text-error">{{leak_count}}</span>
</div>
<p class="text-[10px] text-outline mt-4 font-label-caps uppercase">Unsafe: {{unsafe_count}} · FFI: {{ffi_count}}</p>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">Safe ops: {{safe_ops_count}} · High risk: {{high_risk_count}}</p>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">Safe code: {{safe_code_percent}}%</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-gutter mb-8">
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">TYPE_INTELLIGENCE</p>
<div class="h-48"><canvas id="typeChart"></canvas></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">AUTO_DIAGNOSIS</p>
<div id="diagnosisContent" class="font-data-mono text-xs space-y-3">
<div>
<p class="font-label-caps text-[10px] text-accent-amber mb-1">TOP_ALLOCATION_SITES</p>
<ul class="space-y-1">
{{#each top_allocation_sites}}
<li class="flex justify-between"><span>{{name}}</span><span class="text-primary">{{allocation_count}}</span></li>
{{else}}
<li class="text-outline">No allocation sites.</li>
{{/each}}
</ul>
</div>
<div>
<p class="font-label-caps text-[10px] text-error mb-1">TOP_LEAKED</p>
<ul class="space-y-1">
{{#each top_leaked_allocations}}
<li class="flex justify-between"><span class="truncate">{{type_name}}</span><span class="text-error">{{size}}</span></li>
{{else}}
<li class="text-outline">No leaked allocations.</li>
{{/each}}
</ul>
</div>
<div>
<p class="font-label-caps text-[10px] text-secondary mb-1">TOP_TEMPORARY_CHURN</p>
<ul class="space-y-1">
{{#each top_temporary_churn}}
<li class="flex justify-between"><span class="truncate">{{name}}</span><span class="text-secondary">{{allocation_count}}</span></li>
{{else}}
<li class="text-outline">No churn samples.</li>
{{/each}}
</ul>
</div>
<div>
<p class="font-label-caps text-[10px] text-tertiary mb-1">CIRCULAR_REFERENCES · {{circular_references.has_cycles}}</p>
<ul class="space-y-1">
<li class="flex justify-between"><span>cycles</span><span class="text-tertiary">{{circular_references.count}}</span></li>
<li class="flex justify-between"><span>pointers in cycles</span><span class="text-tertiary">{{circular_references.pointers_in_cycles}} / {{circular_references.total_smart_pointers}}</span></li>
<li class="flex justify-between"><span>est. leaked</span><span class="text-error">{{circular_references.total_leaked_memory}}</span></li>
</ul>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-gutter mb-8">
<div class="lg:col-span-4 bg-surface-container border border-outline-variant rounded-lg p-6 flex flex-col">
<p class="font-label-caps text-on-surface-variant mb-3">HEAP_LATTICE</p>
<div class="grid grid-cols-10 gap-1 flex-1 min-h-[120px]" id="heapGrid"></div>
<div class="flex justify-between mt-3 font-data-mono text-[10px] text-on-surface-variant">
<span>low</span><span>→ allocation size →</span><span>high</span>
</div>
</div>
<div class="lg:col-span-8 bg-surface-container border border-outline-variant rounded-lg p-6 flex flex-col">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">MEMORY_FLAMEGRAPH</p>
<span class="font-data-mono text-[10px] text-primary" id="flameNodeCount">0 frames</span>
</div>
<div class="flex-1 min-h-[160px] relative" id="flameWrap">
<svg class="w-full h-full" id="flameSvg" viewBox="0 0 800 200" preserveAspectRatio="none"></svg>
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">ALLOCATIONS_STREAM</p>
<span class="font-data-mono text-[10px] text-primary">{{allocations_count}} items · peak {{peak_memory}}</span>
</div>
<div class="h-32 mb-4"><canvas id="allocTrendChart"></canvas></div>
<div class="overflow-auto max-h-64">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-2 px-2">ADDR</th><th class="text-left py-2 px-2">TYPE</th>
<th class="text-left py-2 px-2">VAR</th>
<th class="text-right py-2 px-2">SIZE</th>
<th class="text-right py-2 px-2">TID</th>
<th class="text-left py-2 px-2">SRC</th>
<th class="text-left py-2 px-2">STATUS</th>
</tr></thead>
<tbody>
{{#each allocations}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high alloc-row {{#if is_leaked}}bg-error/5{{/if}}" data-idx="{{@index}}">
<td class="py-2 px-2 text-accent-amber whitespace-nowrap">{{address}}</td>
<td class="py-2 px-2 truncate max-w-[160px]">{{type_name}}</td>
<td class="py-2 px-2 truncate max-w-[100px] text-on-surface-variant">{{#if var_name}}{{var_name}}{{else}}—{{/if}}</td>
<td class="py-2 px-2 text-right whitespace-nowrap">{{size}}</td>
<td class="py-2 px-2 text-right whitespace-nowrap text-on-surface-variant">{{thread_id}}</td>
<td class="py-2 px-2 truncate max-w-[160px] text-on-surface-variant">{{source_file}}:{{source_line}}</td>
<td class="py-2 px-2 whitespace-nowrap {{#if is_leaked}}text-error{{else}}text-primary{{/if}}">{{allocation_type}}{{#if is_leaked}} ⚠{{/if}}</td>
</tr>
{{else}}
<tr><td colspan="7" class="py-4 text-center text-outline">No allocations.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
<div id="mode-thread" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Thread Affinity Monitor</h1>
<p class="text-on-surface-variant font-body-sm">{{thread_count}} threads · {{cpu_cores}} cores</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-accent-amber animate-pulse"></span>
<span class="font-label-caps text-label-caps text-accent-amber">LIVE</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
<div class="bg-surface-container-high border border-accent-amber p-5 rounded-lg rim-light amber-glow kpi-card" data-kpi="thread_count" title="Active hardware threads. Click to drill." onclick="drillKpi('thread_count','THREAD_COUNT','{{thread_count}} threads on {{cpu_cores}} cores')">
<span class="font-label-caps text-on-surface-variant">THREAD_COUNT</span>
<h3 class="font-headline-lg text-headline-lg text-accent-amber">{{thread_count}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">SATURATION: nominal</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="scheduler_lag" title="Scheduler latency observed between wake and poll. Click to drill." onclick="drillKpi('scheduler_lag','SCHEDULER_LAG','{{scheduler_lag_ms}}ms mean · migration {{migration_rate_pct}}%')">
<span class="font-label-caps text-on-surface-variant">SCHEDULER_LAG</span>
<h3 class="font-headline-lg text-headline-lg text-primary">{{scheduler_lag_ms}}ms</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">MIG: {{migration_rate_pct}}%</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="uptime" title="Process uptime since boot. Click to drill." onclick="drillKpi('uptime','SYSTEM_UPTIME','up {{system_uptime_formatted}} · {{cpu_cores}} cores')">
<span class="font-label-caps text-on-surface-variant">SYSTEM_UPTIME</span>
<h3 class="font-headline-lg text-headline-lg text-tertiary">{{system_uptime_formatted}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">SINCE_BOOT</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card overflow-hidden" data-kpi="thread_memory_total" title="Total live memory across all threads. Click to drill." onclick="drillKpi('thread_memory_total','THREAD_MEMORY_TOTAL','{{thread_memory_total_fmt}} across {{thread_count}} threads')">
<span class="font-label-caps text-on-surface-variant">THREAD_MEMORY</span>
<h3 class="font-headline-lg text-headline-lg text-secondary truncate">{{thread_memory_total_fmt}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">TOTAL · {{thread_count}} threads</p>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="font-label-caps text-label-caps text-accent-amber">THREAD_LOAD_HEATMAP</h2>
<span class="font-data-mono text-[10px] text-primary">{{len threads}} threads</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-3" id="loadHeatmap"></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4 pb-2 border-b border-outline-variant/30">
<h3 class="font-label-caps text-label-caps text-on-surface uppercase tracking-widest">THREAD_EVENT_LOG [TRACE_LVL: FINE]</h3>
<div class="flex items-center gap-2"><div class="w-2 h-2 rounded-full bg-accent-amber animate-pulse"></div><span class="font-data-mono text-[10px] text-on-surface-variant">STREAMING LIVE</span></div>
</div>
<div class="font-data-mono text-data-mono text-on-surface-variant space-y-1 h-48 overflow-y-auto" id="threadEventLog"></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">THREAD_DETAILS</p>
<span class="font-data-mono text-[10px] text-primary">{{thread_count}} threads</span>
</div>
<div class="overflow-auto max-h-64">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-2">ID</th><th class="text-left py-2">NAME</th>
<th class="text-right py-2">ALLOCS</th>
<th class="text-right py-2">CURRENT</th>
<th class="text-right py-2">PEAK</th>
<th class="text-right py-2">TOTAL</th>
<th class="text-left py-2">STATUS</th>
</tr></thead>
<tbody>
{{#each threads}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high thread-row" data-idx="{{@index}}">
<td class="py-2 text-accent-amber whitespace-nowrap">{{thread_id}}</td>
<td class="py-2 truncate max-w-[200px]">{{thread_summary}}</td>
<td class="py-2 text-right text-primary whitespace-nowrap">{{allocation_count}}</td>
<td class="py-2 text-right text-secondary whitespace-nowrap">{{current_memory}}</td>
<td class="py-2 text-right text-tertiary whitespace-nowrap">{{peak_memory}}</td>
<td class="py-2 text-right text-on-surface-variant whitespace-nowrap">{{total_allocated}}</td>
<td class="py-2 whitespace-nowrap {{#if is_active}}text-primary{{else}}text-outline{{/if}}">{{status}}</td>
</tr>
{{else}}
<tr><td colspan="7" class="py-4 text-center text-outline">No threads.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-gutter mb-8">
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">THREAD_POLICIES · {{len thread_policies}}</p>
<ul class="space-y-1 font-data-mono text-xs">
{{#each thread_policies}}
<li class="flex justify-between thread-policy-item" data-idx="{{@index}}"><span>{{name}}</span><span class="{{#if enabled}}text-success{{else}}text-outline{{/if}}">{{#if enabled}}ON{{else}}OFF{{/if}}</span></li>
{{else}}
<li class="text-outline">No policies configured.</li>
{{/each}}
</ul>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">SCHEDULER_LAG_BARS · {{len scheduler_lag_bars}} samples</p>
<div class="flex items-end gap-1 h-32" id="schedulerLagBars">
{{#each scheduler_lag_bars}}
<div class="flex-1 bg-primary/60 rounded-t sched-lag-bar" data-idx="{{@index}}" data-val="{{this}}" style="height: {{this}}%"></div>
{{else}}
<div class="text-outline text-xs">No lag samples.</div>
{{/each}}
</div>
<p class="text-[9px] font-data-mono text-center mt-2 text-on-surface-variant">MEAN: {{scheduler_lag_ms}}ms</p>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">THREAD_AFFINITY_GRID · {{len thread_affinity_grid}} cells</p>
<div class="grid grid-cols-8 gap-1" id="threadAffinityGrid">
{{#each thread_affinity_grid}}
<div class="aspect-square rounded border border-outline-variant/50 flex items-center justify-center text-[8px] font-data-mono text-on-surface-variant affinity-cell" data-idx="{{@index}}" data-state="{{this}}">{{this}}</div>
{{else}}
<div class="col-span-full text-outline text-xs">No affinity samples.</div>
{{/each}}
</div>
</div>
</div>
</div>
<div id="mode-task" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Async Task Topology</h1>
<p class="text-on-surface-variant font-body-sm">Tokio runtime · {{async_summary.total_tasks}} tasks · poll {{#if poll_latency_mean_ms}}{{poll_latency_mean_ms_fmt}}ms{{else}}—{{/if}}</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-secondary animate-pulse"></span>
<span class="font-label-caps text-label-caps text-secondary">ASYNC</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
<div class="bg-surface-container-high border border-secondary p-5 rounded-lg rim-light kpi-card" data-kpi="async_total_tasks" title="Total async tasks spawned since runtime start. Click to drill." onclick="drillKpi('async_total_tasks','TOTAL_TASKS','{{async_summary.total_tasks}} total · {{async_summary.active_tasks}} active')">
<span class="font-label-caps text-on-surface-variant">TOTAL_TASKS</span>
<h3 class="font-headline-lg text-headline-lg text-secondary">{{async_summary.total_tasks}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">ACTIVE: {{async_summary.active_tasks}}</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="async_completed" title="Completed tasks and success rate. Click to drill." onclick="drillKpi('async_completed','COMPLETED','{{async_summary.completed}} completed · {{async_summary.success_rate}}% success')">
<span class="font-label-caps text-on-surface-variant">COMPLETED</span>
<h3 class="font-headline-lg text-headline-lg text-primary">{{async_summary.completed}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">SUCCESS_RATE: {{async_summary.success_rate}}%</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card" data-kpi="async_leaked" title="Leaked and zombie tasks — potential runtime bugs. Click to drill." onclick="drillKpi('async_leaked','LEAKED_TASKS','{{async_summary.leaked}} leaked · {{async_summary.zombie}} zombie')">
<span class="font-label-caps text-on-surface-variant">LEAKED_TASKS</span>
<h3 class="font-headline-lg text-headline-lg text-error">{{async_summary.leaked}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">ZOMBIE: {{async_summary.zombie}}</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light kpi-card overflow-hidden" data-kpi="async_poll_latency" title="Mean async poll latency in ms. Click to drill." onclick="drillKpi('async_poll_latency','POLL_LATENCY','{{#if poll_latency_mean_ms}}{{poll_latency_mean_ms_fmt}}ms{{else}}—{{/if}} mean')">
<span class="font-label-caps text-on-surface-variant">POLL_LATENCY</span>
<h3 class="font-headline-lg text-headline-lg text-tertiary truncate">{{#if poll_latency_mean_ms}}{{poll_latency_mean_ms_fmt}}ms{{else}}—{{/if}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">MEAN</p>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-gutter mb-8">
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 flex flex-col">
<p class="font-label-caps text-on-surface-variant mb-3">WAKER_EFFICIENCY_HEATGRID</p>
<div class="grid grid-cols-10 gap-1 flex-1 min-h-[100px]" id="wakerHeatgrid"></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 flex flex-col">
<p class="font-label-caps text-on-surface-variant mb-3">POLL_LATENCY · <span id="pollLatencyCount">{{len poll_latency_samples}}</span> samples</p>
<div id="pollLatencyContainer" class="w-full h-40 relative">
<svg class="w-full h-full" viewBox="0 0 200 80" preserveAspectRatio="none" id="pollLatencySvg"></svg>
</div>
<p class="text-[9px] font-data-mono text-center mt-2 text-on-surface-variant">{{#if poll_latency_mean_ms}}{{poll_latency_mean_ms_fmt}}ms{{else}}—{{/if}} MEAN · hover for per-task detail</p>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<p class="font-label-caps text-on-surface-variant mb-3">EXECUTION_TIMELINE</p>
<div class="space-y-3" id="taskTimelineRows"></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4 mb-8">
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 font-data-mono text-xs">
<div class="async-stat" data-stat="total_allocations"><span class="text-on-surface-variant text-[10px] font-label-caps">ALLOCATIONS</span><div class="text-primary text-lg">{{async_summary.total_allocations}}</div></div>
<div class="async-stat" data-stat="total_memory"><span class="text-on-surface-variant text-[10px] font-label-caps">TOTAL_MEM</span><div class="text-primary text-lg">{{async_summary.total_memory_bytes}}B</div></div>
<div class="async-stat" data-stat="peak_memory"><span class="text-on-surface-variant text-[10px] font-label-caps">PEAK_MEM</span><div class="text-accent-amber text-lg">{{async_summary.peak_memory_bytes}}B</div></div>
<div class="async-stat" data-stat="active"><span class="text-on-surface-variant text-[10px] font-label-caps">ACTIVE</span><div class="text-warning text-lg">{{async_summary.active_tasks}}</div></div>
<div class="async-stat" data-stat="completed"><span class="text-on-surface-variant text-[10px] font-label-caps">COMPLETED</span><div class="text-success text-lg">{{async_summary.completed}}</div></div>
<div class="async-stat" data-stat="leaked"><span class="text-on-surface-variant text-[10px] font-label-caps">LEAKED</span><div class="text-error text-lg">{{async_summary.leaked}}</div></div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">ASYNC_TASK_DETAILS</p>
<span class="font-data-mono text-[10px] text-primary">{{async_summary.total_tasks}} tasks</span>
</div>
<div class="overflow-auto max-h-64">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-2">ID</th><th class="text-left py-2">NAME</th>
<th class="text-left py-2">TYPE</th>
<th class="text-right py-2">DURATION</th>
<th class="text-right py-2">ALLOCS</th>
<th class="text-right py-2">MEM</th>
<th class="text-right py-2">EFFICIENCY</th>
<th class="text-left py-2">STATUS</th>
</tr></thead>
<tbody>
{{#each async_tasks}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high async-task-row" data-idx="{{@index}}">
<td class="py-2 text-accent-amber whitespace-nowrap">{{task_id}}</td>
<td class="py-2 truncate max-w-[180px]">{{task_name}}</td>
<td class="py-2 truncate max-w-[100px] text-on-surface-variant">{{task_type}}</td>
<td class="py-2 text-right text-secondary whitespace-nowrap">{{duration_ms}}ms</td>
<td class="py-2 text-right text-on-surface-variant">{{total_allocations}}</td>
<td class="py-2 text-right text-on-surface-variant">{{current_memory}}B</td>
<td class="py-2 text-right {{#if (eq efficiency_score 0.0)}}text-error{{else}}text-primary{{/if}}">{{efficiency_score}}</td>
<td class="py-2 {{#if has_potential_leak}}text-error{{else}}text-primary{{/if}}">{{status}}{{#if has_potential_leak}} ⚠{{/if}}</td>
</tr>
{{else}}
<tr><td colspan="8" class="py-4 text-center text-outline">No async tasks.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
<div id="mode-taskgraph" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Task Topology Graph</h1>
<p class="text-on-surface-variant font-body-sm">Async task DAG · {{task_topology_nodes_count}} nodes · {{task_topology_edges_count}} edges</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-primary animate-pulse"></span>
<span class="font-label-caps text-label-caps text-primary">LIVE_TRACE</span>
</div>
</div>
<div class="grid grid-cols-12 gap-gutter mb-8">
<div class="col-span-12 lg:col-span-8 bg-surface-container border border-outline-variant rounded-lg p-6 relative overflow-hidden min-h-[450px]">
<div id="taskTopologyCanvas" class="absolute inset-0 pt-10 pl-4 pr-4 pb-20"></div>
</div>
<div class="col-span-12 lg:col-span-4 flex flex-col gap-gutter">
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<h4 class="font-label-caps text-[10px] text-on-surface-variant mb-3">STREAMING_TOPOLOGY</h4>
<div class="space-y-2 font-data-mono text-[10px] text-on-surface-variant">
<div class="flex justify-between"><span>GRAPH_NODES</span><span class="text-white">{{task_topology_nodes_count}}</span></div>
<div class="flex justify-between"><span>GRAPH_EDGES</span><span class="text-white">{{task_topology_edges_count}}</span></div>
<div class="flex justify-between"><span>SAMPLING_RATE</span><span class="text-white">{{streaming_topology_stats.sampling_rate_ms}}ms</span></div>
<div class="flex justify-between"><span>WAKER_LOCKS</span><span class="text-primary">{{streaming_topology_stats.waker_locks_status}}</span></div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4 flex-1">
<h4 class="font-label-caps text-[10px] text-on-surface-variant mb-2">TRACE_LOG_WINDOW</h4>
<div class="font-data-mono text-[10px] space-y-1 overflow-y-auto h-[200px]" id="traceLogWindow"></div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-gutter mb-8">
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">TASK_TOPOLOGY_NODES · {{task_topology_nodes_count}}</p>
<div class="overflow-auto max-h-48">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-1 px-2">ID</th><th class="text-left py-1 px-2">NAME</th>
<th class="text-left py-1 px-2">STATUS</th><th class="text-right py-1 px-2">DUR</th>
</tr></thead>
<tbody>
{{#each task_topology_nodes}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high topo-node-row" data-idx="{{@index}}">
<td class="py-1 px-2 text-accent-amber">{{task_id}}</td>
<td class="py-1 px-2 truncate">{{name}}</td>
<td class="py-1 px-2">{{status}}</td>
<td class="py-1 px-2 text-right text-primary">{{duration_ms}}ms</td>
</tr>
{{else}}
<tr><td colspan="4" class="py-2 px-2 text-outline">No topology nodes.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6">
<p class="font-label-caps text-on-surface-variant mb-3">TASK_TOPOLOGY_EDGES · {{task_topology_edges_count}}</p>
<div class="overflow-auto max-h-48">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-1 px-2">SOURCE</th><th class="text-left py-1 px-2">TARGET</th>
<th class="text-left py-1 px-2">ACTIVE</th>
</tr></thead>
<tbody>
{{#each task_topology_edges}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high topo-edge-row" data-idx="{{@index}}">
<td class="py-1 px-2 text-accent-amber">{{source}}</td>
<td class="py-1 px-2 text-accent-amber">{{target}}</td>
<td class="py-1 px-2 {{#if is_active}}text-success{{else}}text-outline{{/if}}">{{#if is_active}}yes{{else}}no{{/if}}</td>
</tr>
{{else}}
<tr><td colspan="3" class="py-2 px-2 text-outline">No topology edges.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<p class="font-label-caps text-on-surface-variant mb-3">DEPENDENCY_GRAPH_NODES</p>
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2 font-data-mono text-xs">
{{#each dependency_graph_nodes}}
<div class="bg-surface-container-low border border-outline-variant/50 rounded p-2 dep-graph-node" data-idx="{{@index}}" style="opacity: {{opacity}};">
<div class="text-accent-amber truncate">{{name}}</div>
<div class="text-[10px] text-on-surface-variant">{{position}}{{#if status}} · {{status}}{{/if}}</div>
</div>
{{else}}
<div class="col-span-full text-outline">No peripheral dependency nodes.</div>
{{/each}}
</div>
</div>
</div>
<div id="mode-variable" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Variable Dependency Graph</h1>
<p class="text-on-surface-variant font-body-sm">{{relationships_count}} relationships · {{ownership_graph.total_nodes}} nodes</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-tertiary"></span>
<span class="font-label-caps text-label-caps text-tertiary">D3_FORCE</span>
</div>
</div>
<div class="grid grid-cols-12 gap-gutter mb-8">
<div class="col-span-12 lg:col-span-8 bg-black border border-outline-variant rounded-lg p-4 relative min-h-[400px]">
<p class="font-label-caps text-on-surface-variant mb-3 absolute top-4 left-4 z-10">VARIABLE_DEPENDENCY_GRAPH</p>
<div class="w-full h-full pt-8" id="variableGraphContainer"></div>
</div>
<div class="col-span-12 lg:col-span-4 flex flex-col gap-gutter">
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<h4 class="font-label-caps text-[10px] text-on-surface-variant mb-1">SELECTED_NODE</h4>
<p class="font-headline-md text-on-surface" id="detailNodeName">{{#if selected_node_detail}}{{selected_node_detail.node_name}}{{else}}—{{/if}}</p>
<div class="flex items-center gap-2 mt-1"><div class="w-2 h-2 rounded-full bg-primary"></div><span class="font-data-mono text-xs text-primary" id="detailNodeStatus">{{#if selected_node_detail}}{{selected_node_detail.status_badge}} · {{selected_node_detail.current_status}}{{else}}—{{/if}}</span></div>
<p class="font-data-mono text-[10px] text-outline mt-2" id="detailNodeUuid">UUID: {{#if selected_node_detail}}{{selected_node_detail.uuid}}{{else}}—{{/if}}</p>
<p class="font-data-mono text-[10px] text-accent-amber mt-1" id="detailNodeType">{{#if selected_node_detail.type_name}}TYPE: {{selected_node_detail.type_name}}{{/if}}</p>
<p class="font-data-mono text-[10px] mt-1" id="detailNodeCycle"></p>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<p class="font-label-caps text-[10px] text-on-surface-variant mb-2">EXECUTION_TIME</p>
<p class="font-data-mono text-xl text-on-surface" id="detailNodeExecTime">{{#if selected_node_detail}}{{selected_node_detail.execution_time_ms}}ms{{else}}—{{/if}}</p>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<p class="font-label-caps text-[10px] text-on-surface-variant mb-2">UPSTREAM_DEPS</p>
<p class="font-data-mono text-xl text-on-surface" id="detailNodeUpstream">{{#if selected_node_detail}}{{selected_node_detail.upstream_deps}}{{else}}—{{/if}}</p>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4">
<p class="font-label-caps text-[10px] text-on-surface-variant mb-2">EXECUTION_TRACE</p>
<div class="font-data-mono text-[11px] space-y-1" id="detailNodeTrace">
{{#each selected_node_detail.exec_trace}}
<div class="text-on-surface-variant">{{this}}</div>
{{else}}
<div class="text-outline">Click a node in the graph to inspect.</div>
{{/each}}
</div>
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-4 mb-8">
<div class="flex justify-between items-center mb-4">
<span class="font-label-caps text-label-caps text-on-surface-variant">NEIGHBOR_DENSITY_HISTOGRAM</span>
<span class="font-data-mono text-xs text-primary">{{allocations_count}} objects</span>
</div>
<div class="flex items-end gap-1 h-20 px-2" id="neighborHistogram"></div>
<div class="flex justify-between mt-2 font-data-mono text-[9px] text-on-surface-variant"><span>0ms</span><span>500ms</span><span>1000ms</span><span>1500ms</span><span>2000ms</span></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">VARIABLE_RELATIONSHIPS</p>
<span class="font-data-mono text-[10px] text-primary">{{relationships_count}} relationships</span>
</div>
<div class="overflow-auto max-h-64">
<table class="w-full font-data-mono text-xs border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-2">FROM</th><th class="text-left py-2">TO</th>
<th class="text-left py-2">RELATION</th><th class="text-left py-2">SCOPE</th>
<th class="text-right py-2">STRENGTH</th><th class="text-center py-2">CYCLE</th>
</tr></thead>
<tbody>
{{#each relationships}}
<tr class="border-b border-outline-variant/30 hover:bg-surface-container-high {{#if is_part_of_cycle}}bg-error/5{{/if}}">
<td class="py-2 text-accent-amber truncate max-w-[120px]">{{source_var_name}}</td>
<td class="py-2 text-secondary truncate max-w-[120px]">{{target_var_name}}</td>
<td class="py-2 text-primary whitespace-nowrap">{{relationship_type}}</td>
<td class="py-2 truncate max-w-[100px] text-on-surface-variant">{{type_name}}</td>
<td class="py-2 text-right whitespace-nowrap text-on-surface-variant">{{strength}}</td>
<td class="py-2 text-center">{{#if is_part_of_cycle}}<span class="text-error" title="Part of retain cycle">⚠</span>{{else}}<span class="text-outline">·</span>{{/if}}</td>
</tr>
{{else}}
<tr><td colspan="6" class="py-4 text-center text-outline">No relationships.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
{{#if total_smart_pointers}}
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
<div class="bg-surface-container-high border border-tertiary p-5 rounded-lg rim-light">
<span class="font-label-caps text-on-surface-variant">SMART_PTR_COUNT</span>
<h3 class="font-headline-lg text-headline-lg text-tertiary">{{total_smart_pointers}}</h3>
<p class="text-[10px] text-outline mt-2 font-label-caps uppercase">OF {{allocations_count}} ALLOCATIONS</p>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light col-span-1">
<span class="font-label-caps text-on-surface-variant mb-2 block">TYPE_DISTRIBUTION</span>
<div class="space-y-2 font-data-mono text-xs">
{{#each smart_pointer_breakdown}}
<div class="flex justify-between items-center">
<span class="text-primary">{{@key}}</span>
<span class="text-on-surface">{{this}}</span>
</div>
{{else}}
<div class="text-outline">—</div>
{{/each}}
</div>
</div>
<div class="bg-surface-container-high border border-outline-variant p-5 rounded-lg rim-light col-span-2">
<span class="font-label-caps text-on-surface-variant mb-2 block">SMART_PTR_ALLOCATIONS</span>
<div class="overflow-auto max-h-32 font-data-mono text-xs">
<table class="w-full border-collapse">
<thead><tr class="text-on-surface-variant border-b border-outline-variant">
<th class="text-left py-1">TYPE</th><th class="text-left py-1">ADDR</th><th class="text-right py-1">SIZE</th>
</tr></thead>
<tbody>
{{#each allocations}}
{{#if is_smart_pointer}}
<tr class="border-b border-outline-variant/30">
<td class="py-1 text-tertiary">{{smart_pointer_type}}</td>
<td class="py-1 text-accent-amber">{{addr}}</td>
<td class="py-1 text-right">{{size}}</td>
</tr>
{{/if}}
{{else}}
<tr><td colspan="3" class="py-2 text-outline">No smart pointers.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
{{/if}}
</div>
<div id="mode-passport" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Memory Passport Center</h1>
<p class="text-on-surface-variant font-body-sm">{{passport_count}} passports · {{ffi_tracked_count}} FFI tracked</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="w-2 h-2 rounded-full bg-primary"></span>
<span class="font-label-caps text-label-caps text-primary">PASSPORT</span>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-gutter mb-8">
<div class="bg-surface-container-high border border-success p-5 rounded-lg rim-light kpi-card" data-kpi="passport_clean" title="Passports with clean lifecycle — properly freed. Click to drill." onclick="drillKpi('passport_clean','CLEAN','{{clean_passport_count}} clean passports')">
<span class="font-label-caps text-on-surface-variant">CLEAN</span>
<h3 class="font-headline-lg text-headline-lg text-success">{{clean_passport_count}}</h3>
</div>
<div class="bg-surface-container-high border border-accent-amber p-5 rounded-lg rim-light kpi-card" data-kpi="passport_active" title="Active passports still in custody. Click to drill." onclick="drillKpi('passport_active','ACTIVE','{{active_passport_count}} active passports')">
<span class="font-label-caps text-on-surface-variant">ACTIVE</span>
<h3 class="font-headline-lg text-headline-lg text-accent-amber">{{active_passport_count}}</h3>
</div>
<div class="bg-surface-container-high border border-error p-5 rounded-lg rim-light kpi-card" data-kpi="passport_leaked" title="Leaked passports — memory not returned. Click to drill." onclick="drillKpi('passport_leaked','LEAKED','{{leaked_passport_count}} leaked passports')">
<span class="font-label-caps text-on-surface-variant">LEAKED</span>
<h3 class="font-headline-lg text-headline-lg text-error">{{leaked_passport_count}}</h3>
</div>
<div class="bg-surface-container-high border border-secondary p-5 rounded-lg rim-light kpi-card" data-kpi="passport_ffi" title="Passports crossing FFI boundaries. Click to drill." onclick="drillKpi('passport_ffi','FFI_TRACKED','{{ffi_tracked_count}} FFI-tracked passports')">
<span class="font-label-caps text-on-surface-variant">FFI_TRACKED</span>
<h3 class="font-headline-lg text-headline-lg text-secondary">{{ffi_tracked_count}}</h3>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<p class="font-label-caps text-on-surface-variant">PASSPORT_CARDS</p>
<span class="font-data-mono text-[10px] text-primary">{{passport_count}} cards</span>
</div>
<div id="passportCards" class="grid grid-cols-1 md:grid-cols-3 gap-4">
{{#each passport_details}}
<div class="bg-surface-container-low border border-outline-variant rounded-lg p-4 passport-card" data-idx="{{@index}}">
<div class="flex justify-between items-start mb-2">
<span class="font-data-mono text-xs text-accent-amber">{{passport_id}}</span>
<span class="font-label-caps text-[9px] px-1.5 py-0.5 rounded border {{risk_label status}}">{{status}}</span>
</div>
<p class="font-data-mono text-sm text-on-surface">{{#if var_name}}{{var_name}}{{else}}{{type_name}}{{/if}}</p>
<p class="text-[10px] text-on-surface-variant mt-1">TYPE: {{type_name}} · SIZE: {{size_bytes}}B</p>
<p class="text-[10px] text-on-surface-variant">SRC: {{source_location}}</p>
<p class="text-[10px] text-outline mt-1">PTR: {{allocation_ptr}}</p>
<div class="flex flex-wrap gap-1 mt-2">
<span class="text-[9px] px-1 py-0.5 rounded border {{risk_class risk_level}}">RISK:{{risk_level}}</span>
{{#if ffi_tracked}}<span class="text-[9px] px-1 py-0.5 rounded bg-accent-amber/20 text-accent-amber border border-accent-amber/40">FFI</span>{{/if}}
{{#if is_active}}<span class="text-[9px] px-1 py-0.5 rounded bg-warning/20 text-warning border border-warning/40">ACTIVE</span>{{/if}}
{{#if is_leaked}}<span class="text-[9px] px-1 py-0.5 rounded bg-error/20 text-error border border-error/40">LEAKED</span>{{/if}}
<span class="text-[9px] px-1 py-0.5 rounded bg-surface-container-highest text-on-surface-variant border border-outline-variant">{{risk_confidence}}</span>
</div>
<div class="mt-2 pt-2 border-t border-outline-variant/30 flex justify-between text-[9px] font-data-mono text-on-surface-variant">
<span>LIFE: {{len lifecycle_events}}ev</span>
<span>FFI: {{len cross_boundary_events}}x</span>
</div>
</div>
{{else}}
<div class="col-span-full text-outline text-xs">No passport details available.</div>
{{/each}}
</div>
</div>
</div>
<div id="mode-ffi" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">FFI Bridge Analysis</h1>
<p class="text-on-surface-variant font-body-sm">{{ffi_count}} FFI ops · {{unsafe_count}} unsafe · integrity {{stack_integrity.pointers_checked_pct}}%</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="material-symbols-outlined text-accent-amber">security</span>
<span class="font-label-caps text-label-caps text-accent-amber">HARDENED</span>
</div>
</div>
<div class="grid grid-cols-12 gap-gutter mb-8">
<div class="col-span-12 lg:col-span-8 bg-surface-container border border-outline-variant rounded-lg p-6 relative overflow-hidden min-h-[400px] amber-glow">
<div class="flex justify-between items-center mb-6">
<h2 class="font-label-caps text-on-surface">CALL_MAPPING_TOPOLOGY</h2>
<div class="flex gap-2"><span class="w-2 h-2 rounded-full bg-accent-amber animate-pulse"></span><span class="font-data-mono text-[10px] text-on-surface-variant">LIVE_TRACE</span></div>
</div>
<svg class="absolute inset-0 w-full h-full" id="ffiTopoSvg"></svg>
</div>
<div class="col-span-12 lg:col-span-4 space-y-gutter">
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 min-h-[192px] flex flex-col justify-between">
<h2 class="font-label-caps text-on-surface mb-4">STACK_INTEGRITY</h2>
<div class="space-y-3">
<div class="flex justify-between items-end border-b border-outline-variant pb-2"><span class="font-data-mono text-on-surface-variant">Pointers Checked</span><span class="font-data-mono text-primary">{{stack_integrity.pointers_checked_pct}}%</span></div>
<div class="flex justify-between items-end border-b border-outline-variant pb-2"><span class="font-data-mono text-on-surface-variant">Memory Violations</span><span class="font-data-mono {{#if stack_integrity.memory_violations}}text-error{{else}}text-success{{/if}}">{{stack_integrity.memory_violations}}</span></div>
<div class="flex justify-between items-end"><span class="font-data-mono text-on-surface-variant">Unwinding Strategy</span><span class="font-data-mono">{{stack_integrity.unwinding_strategy}}</span></div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 min-h-[192px]">
<h2 class="font-label-caps text-on-surface mb-4">RESOURCES</h2>
<div class="space-y-3" id="ffiResourceBars">
{{#each resource_bars}}
<div>
<div class="flex justify-between text-[10px] font-label-caps text-on-surface-variant mb-1"><span>{{label}}</span><span>{{pct}}%</span></div>
<div class="w-full h-1 bg-surface-container-highest rounded-full overflow-hidden"><div class="h-full bg-{{color_class}}" style="width: {{pct}}%"></div></div>
</div>
{{/each}}
</div>
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg overflow-hidden mb-8">
<div class="p-4 border-b border-outline-variant flex justify-between items-center">
<h2 class="font-label-caps text-on-surface">SYMBOL_TABLE_ANALYSIS</h2>
<span class="font-data-mono text-[10px] text-primary">{{symbol_table_count}} symbols</span>
</div>
<div class="overflow-x-auto">
<table class="w-full text-left font-data-mono">
<thead class="bg-surface-container-low border-b border-outline-variant">
<tr><th class="p-4 font-label-caps text-on-surface-variant">HEX_ADDR</th><th class="p-4 font-label-caps text-on-surface-variant">SYMBOL_NAME</th><th class="p-4 font-label-caps text-on-surface-variant">STATUS</th><th class="p-4 font-label-caps text-on-surface-variant">CALL_COUNT</th><th class="p-4 font-label-caps text-on-surface-variant">TIME_AVG</th></tr>
</thead>
<tbody class="divide-y divide-outline-variant/30">
{{#each symbol_table}}
<tr class="hover:bg-surface-container-high transition-colors symbol-row" data-idx="{{@index}}">
<td class="p-4 text-xs text-accent-amber">{{hex_addr}}</td>
<td class="p-4 text-xs">{{#if symbol_name}}{{symbol_name}}{{else}}—{{/if}}</td>
<td class="p-4 text-xs {{#if is_hot}}text-error{{else if (eq status 'HOT')}}text-error{{else}}text-success{{/if}}">{{status}}{{#if is_hot}} 🔥{{/if}}</td>
<td class="p-4 text-xs text-primary">{{call_count}}</td>
<td class="p-4 text-xs text-secondary">{{time_avg_us}}µs</td>
</tr>
{{else}}
<tr><td colspan="5" class="p-4 text-center text-outline">No symbols.</td></tr>
{{/each}}
</tbody>
</table>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-6">
<h2 class="font-label-caps text-on-surface">THREAD_EXECUTION_TIMELINE</h2>
<span class="font-data-mono text-[10px] text-primary">{{thread_timeline_count}} rows</span>
</div>
<div class="space-y-4" id="ffiThreadTimeline"></div>
</div>
</div>
<div id="mode-unsafe" class="mode-section">
<div class="flex justify-between items-end mb-8 pt-6">
<div>
<h1 class="font-headline-lg text-headline-lg text-on-background mb-1">Unsafe / FFI Boundary & Time Travel</h1>
<p class="text-on-surface-variant font-body-sm">{{unsafe_count}} unsafe · {{ffi_count}} FFI · {{leak_count}} leaks</p>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-surface-container border border-outline-variant rounded-lg">
<span class="material-symbols-outlined text-error">warning</span>
<span class="font-label-caps text-label-caps text-error">UNSAFE</span>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-gutter mb-8">
<div class="bg-surface-container border border-warning p-4 rounded-lg">
<p class="font-label-caps text-on-surface-variant mb-3">UNSAFE_OPERATIONS · {{unsafe_count}} reports</p>
<div class="space-y-2 font-data-mono text-xs" id="unsafeOpsList">
{{#each unsafe_reports}}
<div class="border-b border-outline-variant/30 pb-2 unsafe-op-row" data-idx="{{@index}}">
<div class="flex justify-between items-center">
<span class="text-accent-amber">{{#if var_name}}{{var_name}}{{else}}{{type_name}}{{/if}}</span>
<span class="text-[9px] px-1.5 py-0.5 rounded border {{risk_class risk_level}}">{{risk_level}}</span>
</div>
<div class="text-on-surface-variant text-[10px] mt-1">{{description}}</div>
<div class="flex flex-wrap gap-1 mt-1">
{{#each risk_factors}}<span class="text-[9px] px-1 py-0.5 rounded bg-surface-container-highest text-on-surface-variant">{{this}}</span>{{/each}}
</div>
<div class="text-outline text-[9px] mt-1">{{passport_id}} · {{size_bytes}}B · {{type_name}}</div>
</div>
{{else}}
<div class="text-outline">No unsafe operations.</div>
{{/each}}
</div>
</div>
<div class="bg-surface-container border border-tertiary p-4 rounded-lg">
<p class="font-label-caps text-on-surface-variant mb-3">FFI_CROSSINGS · {{ffi_count}} edges</p>
<div class="space-y-2 font-data-mono text-xs" id="ffiCrossingsList">
{{#each ffi_call_topology.edges}}
<div class="border-b border-outline-variant/30 pb-2 ffi-crossing-row" data-idx="{{@index}}">
<div class="flex justify-between"><span class="text-secondary truncate max-w-[200px]">{{from_name}}</span><span class="text-accent-amber whitespace-nowrap">→ {{to_name}}</span></div>
<div class="text-on-surface-variant text-[10px] mt-1 truncate">{{label}}</div>
</div>
{{else}}
<div class="text-outline">No FFI crossings.</div>
{{/each}}
</div>
</div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="font-label-caps text-on-surface">OWNERSHIP_GRAPH</h2>
<span class="font-data-mono text-[10px] text-primary">{{ownership_graph.total_nodes}} nodes · {{ownership_graph.total_edges}} edges · {{ownership_graph.total_cycles}} cycles</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-4">
<div class="text-center p-3 bg-surface-container-low rounded border-l-4 border-primary own-stat-card" data-stat="nodes"><div class="font-headline-md text-primary">{{ownership_graph.total_nodes}}</div><div class="font-label-caps text-[10px] text-on-surface-variant">NODES</div></div>
<div class="text-center p-3 bg-surface-container-low rounded border-l-4 border-secondary own-stat-card" data-stat="edges"><div class="font-headline-md text-secondary">{{ownership_graph.total_edges}}</div><div class="font-label-caps text-[10px] text-on-surface-variant">EDGES</div></div>
<div class="text-center p-3 bg-surface-container-low rounded border-l-4 border-error own-stat-card" data-stat="cycles"><div class="font-headline-md text-error">{{ownership_graph.total_cycles}}</div><div class="font-label-caps text-[10px] text-on-surface-variant">CYCLES</div></div>
<div class="text-center p-3 bg-surface-container-low rounded border-l-4 border-success own-stat-card" data-stat="rc_clones"><div class="font-headline-md text-success">{{ownership_graph.rc_clone_count}}</div><div class="font-label-caps text-[10px] text-on-surface-variant">RC_CLONES</div></div>
<div class="text-center p-3 bg-surface-container-low rounded border-l-4 border-accent-amber own-stat-card" data-stat="arc_clones"><div class="font-headline-md text-accent-amber">{{ownership_graph.arc_clone_count}}</div><div class="font-label-caps text-[10px] text-on-surface-variant">ARC_CLONES</div></div>
</div>
{{#if ownership_graph.has_issues}}
<div class="border-t border-outline-variant pt-4">
<p class="font-label-caps text-[10px] text-error mb-2">⚠ ISSUES_DETECTED</p>
<ul class="space-y-1 font-data-mono text-xs">
{{#each ownership_graph.issues}}<li class="text-on-surface-variant flex gap-2"><span class="text-error">▸</span><span>{{this}}</span></li>{{/each}}
</ul>
{{#if ownership_graph.root_cause}}<p class="text-[10px] text-warning mt-2 font-data-mono">ROOT_CAUSE: {{ownership_graph.root_cause}}</p>{{/if}}
</div>
{{else}}
<div class="border-t border-outline-variant pt-4 flex items-center gap-2">
<span class="material-symbols-outlined text-success text-base">check_circle</span>
<span class="font-data-mono text-xs text-success">No ownership issues detected · graph is acyclic</span>
</div>
{{/if}}
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">TIME_TRAVEL_ANALYSIS</p>
<span class="font-data-mono text-[10px] text-primary">{{allocations_count}} events</span>
</div>
<div class="h-48"><canvas id="timelineChart"></canvas></div>
</div>
<div class="bg-surface-container border border-outline-variant rounded-lg p-6 mb-8">
<div class="flex justify-between items-center mb-3">
<p class="font-label-caps text-on-surface-variant">UNSAFE_SOURCE_HEATMAP</p>
<span class="font-data-mono text-[10px] text-primary">{{unsafe_count}} operations</span>
</div>
<div id="unsafeHeatmap" class="grid grid-cols-2 md:grid-cols-3 gap-2"></div>
</div>
</div>
</main>
</div>
<footer class="fixed bottom-0 left-0 ml-64 w-[calc(100%-16rem)] h-6 bg-surface-container-lowest border-t border-outline-variant z-50 flex justify-between items-center px-4">
<div class="flex items-center gap-4">
<div class="flex items-center gap-1"><span class="w-1.5 h-1.5 bg-primary rounded-full animate-pulse"></span><span class="font-data-mono text-[9px] uppercase text-on-surface-variant">Telemetry Linked</span></div>
<div class="flex items-center gap-1 border-l border-outline-variant pl-4"><span class="font-data-mono text-[9px] text-on-surface-variant">CPU: {{system_resources.cpu_usage_pct}}%</span></div>
<div class="flex items-center gap-1 border-l border-outline-variant pl-4"><span class="font-data-mono text-[9px] text-on-surface-variant">MEM: {{system_resources.used_physical}} / {{system_resources.total_physical}}</span></div>
</div>
<div class="font-data-mono text-[9px] text-on-surface-variant">UTC: <span id="clock">{{export_timestamp}}</span></div>
</footer>
<script id="dashboard-json-data" type="application/json">{{{json_data}}}</script>
<script>
function showMode(mode, el) {
document.querySelectorAll('.mode-section').forEach(s => s.classList.remove('active'));
const target = document.getElementById('mode-' + mode);
if (target) target.classList.add('active');
document.querySelectorAll('.ke-side-item').forEach(i => i.classList.remove('active'));
if (el) el.classList.add('active');
}
function drillKpi(metric, label, value) {
const card = document.querySelector('[data-kpi="' + metric + '"]');
if (card) {
card.classList.remove('kpi-flash');
void card.offsetWidth; card.classList.add('kpi-flash');
}
const toast = document.getElementById('kpiToast');
if (toast) {
toast.textContent = label + ': ' + value;
toast.classList.add('visible');
clearTimeout(window.__kpiToastTimer);
window.__kpiToastTimer = setTimeout(() => toast.classList.remove('visible'), 1800);
}
}
const __themeAwareCharts = [];
function registerThemeAware(name, fn) { __themeAwareCharts.push({ name, fn }); }
function rerenderThemeAwareCharts() {
__themeAwareCharts.forEach(c => {
try { c.fn(); } catch (e) { console.warn('rerender failed:', c.name, e); }
});
}
function switchTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
try { localStorage.setItem('memscope.theme', theme); } catch (e) {}
const amberBtn = document.getElementById('theme-amber');
const indigoBtn = document.getElementById('theme-indigo');
if (amberBtn && indigoBtn) {
const activeClass = 'border-primary bg-primary/20 text-primary';
const idleClass = 'border-outline-variant text-on-surface-variant bg-transparent';
const amberActive = theme === 'amber';
amberBtn.className = 'flex-1 py-1 rounded border ' + (amberActive ? activeClass : idleClass) + ' font-label-caps text-[10px] hover:bg-primary/20 transition-colors';
indigoBtn.className = 'flex-1 py-1 rounded border ' + (!amberActive ? activeClass : idleClass) + ' font-label-caps text-[10px] hover:bg-primary/20 transition-colors';
}
if (typeof rerenderThemeAwareCharts === 'function') rerenderThemeAwareCharts();
}
function applyInitialTheme() {
let saved = null;
try { saved = localStorage.getItem('memscope.theme'); } catch (e) {}
switchTheme(saved === 'indigo' ? 'indigo' : 'amber');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyInitialTheme);
} else {
applyInitialTheme();
}
function getOrCreateTooltip() {
let tt = document.getElementById('chartTooltip');
if (!tt) {
tt = document.createElement('div');
tt.id = 'chartTooltip';
tt.className = 'chart-tooltip';
document.body.appendChild(tt);
}
return tt;
}
let _lastTooltipKey = '';
function showChartTooltip(event, label, rows) {
const tt = getOrCreateTooltip();
const contentKey = (label || '') + '|' + (rows || []).map(r => (r[0] || '') + '=' + (r[1] !== undefined ? r[1] : '')).join('|');
if (contentKey !== _lastTooltipKey) {
_lastTooltipKey = contentKey;
let html = '<div class="tt-label">' + (label || 'DETAIL') + '</div>';
(rows || []).forEach(r => {
html += '<div class="tt-row"><span class="tt-key">' + (r[0] || '') + '</span><span>' + (r[1] !== undefined ? r[1] : '') + '</span></div>';
});
tt.innerHTML = html;
}
tt.classList.add('visible');
moveChartTooltip(event);
}
function moveChartTooltip(event) {
const tt = getOrCreateTooltip();
const x = (event.clientX || (event.touches && event.touches[0].clientX) || 0) + 14;
const y = (event.clientY || (event.touches && event.touches[0].clientY) || 0) + 14;
const w = tt.offsetWidth || 200;
const h = tt.offsetHeight || 60;
tt.style.left = Math.min(x, window.innerWidth - w - 8) + 'px';
tt.style.top = Math.min(y, window.innerHeight - h - 8) + 'px';
}
function hideChartTooltip() {
const tt = document.getElementById('chartTooltip');
if (tt) tt.classList.remove('visible');
}
function bindInteractive(el, label, rows, drillLabel, drillValue) {
if (!el) return;
el.classList.add('chart-interactive');
el.addEventListener('mousemove', e => { showChartTooltip(e, label, rows); });
el.addEventListener('mouseleave', () => { hideChartTooltip(); });
el.addEventListener('click', () => { drillKpi(label, drillLabel, drillValue); });
}
let DATA = {};
try {
const raw = document.getElementById('dashboard-json-data').textContent;
DATA = JSON.parse(raw);
} catch (e) {
console.warn('Failed to parse dashboard JSON:', e);
}
function themeVar(name) {
return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
}
function primaryMix(alpha) {
return 'color-mix(in srgb, var(--primary) ' + (alpha * 100).toFixed(0) + '%, transparent)';
}
function renderHeapGrid() {
const grid = document.getElementById('heapGrid');
if (!grid) return;
const sizes = (DATA.allocations || []).map(a => a.size || 0).filter(s => s > 0);
if (sizes.length === 0) {
grid.innerHTML = '<div class="col-span-full text-outline text-xs px-2 py-4">No allocation samples.</div>';
return;
}
const max = Math.max(1, ...sizes);
grid.innerHTML = '';
for (let i = 0; i < 100; i++) {
const cell = document.createElement('div');
const sz = sizes[i % sizes.length];
const intensity = sz / max;
cell.className = 'rounded-sm chart-interactive';
cell.style.background = primaryMix(0.1 + intensity * 0.7);
bindInteractive(cell, 'HEAP_CELL', [['cell', '#' + i], ['size', sz + 'B'], ['intensity', (intensity * 100).toFixed(0) + '%']], 'HEAP_CELL', 'cell #' + i + ' · size ' + sz + 'B');
grid.appendChild(cell);
}
}
registerThemeAware('heapGrid', renderHeapGrid);
renderHeapGrid();
function renderAllocTrend() {
const canvas = document.getElementById('allocTrendChart');
if (!canvas) return;
const ctx = canvas.getContext('2d');
const allocs = DATA.allocations || [];
if (allocs.length < 2) { ctx.clearRect(0, 0, canvas.width, canvas.height); return; }
const rect = canvas.parentElement.getBoundingClientRect();
canvas.width = rect.width || 600;
canvas.height = 128;
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
const bins = 60;
const bucket = new Array(bins).fill(0);
const minT = allocs[0].timestamp_alloc;
const maxT = allocs[allocs.length - 1].timestamp_alloc;
const span = Math.max(maxT - minT, 1);
for (const a of allocs) {
const idx = Math.min(Math.floor((a.timestamp_alloc - minT) / span * bins), bins - 1);
bucket[idx] += a.size;
}
const maxV = Math.max(1, ...bucket);
ctx.strokeStyle = '#494551'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, H - 20); ctx.lineTo(W, H - 20); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, H - 20); ctx.stroke();
const barW = (W - 10) / bins;
ctx.strokeStyle = '#cfbcff'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
const pts = [];
for (let i = 0; i < bins; i++) {
const x = 5 + i * barW + barW / 2;
const y = H - 20 - (bucket[i] / maxV) * (H - 30);
pts.push({ x, y });
}
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (let i = 0; i < pts.length - 1; i++) {
const mx = (pts[i].x + pts[i + 1].x) / 2;
const my = (pts[i].y + pts[i + 1].y) / 2;
ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
}
const last = pts[pts.length - 1];
ctx.lineTo(last.x, last.y);
ctx.stroke();
const grad = ctx.createLinearGradient(0, 0, 0, H - 20);
grad.addColorStop(0, 'rgba(207,188,255,0.15)');
grad.addColorStop(1, 'rgba(207,188,255,0.01)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(5 + barW / 2, H - 20);
for (let i = 0; i < bins; i++) {
const x = 5 + i * barW + barW / 2;
const y = H - 20 - (bucket[i] / maxV) * (H - 30);
ctx.lineTo(x, y);
}
ctx.lineTo(5 + (bins - 1) * barW + barW / 2, H - 20);
ctx.closePath();
ctx.fill();
ctx.fillStyle = '#948e9c'; ctx.font = '9px JetBrains Mono';
ctx.fillText('0', 4, H - 6);
const maxLabel = maxV >= 1_000_000 ? (maxV / 1_000_000).toFixed(1) + 'MB' : maxV >= 1_000 ? (maxV / 1_000).toFixed(1) + 'KB' : maxV + 'B';
ctx.fillText(maxLabel, 4, 10);
ctx.fillText(span + 'ns', W - 60, H - 6);
}
window.addEventListener('resize', () => { setTimeout(renderAllocTrend, 200); });
renderAllocTrend();
function renderLoadHeatmap() {
const grid = document.getElementById('loadHeatmap');
if (!grid) return;
const threads = DATA.threads || [];
if (threads.length === 0) {
grid.innerHTML = '<div class="col-span-full text-outline text-xs py-4">No thread data.</div>';
return;
}
const maxAlloc = Math.max(...threads.map(t => t.allocation_count || 0), 1);
const maxMem = Math.max(...threads.map(t => t.current_memory_bytes || 0), 1);
grid.innerHTML = '';
threads.forEach((t, i) => {
const cell = document.createElement('div');
const ratio = (t.allocation_count || 0) / maxAlloc;
const intensity = 0.15 + ratio * 0.75;
cell.className = 'aspect-square rounded border border-outline-variant/50 flex flex-col items-center justify-center text-[8px] font-data-mono chart-interactive p-1';
cell.style.background = primaryMix(intensity);
cell.innerHTML = '<span class="text-white font-bold">' + (t.thread_id || '').replace('Thread-', 'T') + '</span><span class="text-on-surface-variant mt-1">' + (t.allocation_count || 0) + '</span>';
cell.title = t.thread_id + ' · ' + t.allocation_count + ' allocs · ' + (t.current_memory || '0 B');
bindInteractive(cell, 'THREAD_' + i, [['id', t.thread_id], ['allocs', t.allocation_count], ['mem', t.current_memory]], 'THREAD_LOAD', t.thread_id + ' · ' + t.allocation_count + ' allocs');
grid.appendChild(cell);
});
}
const loadEl = document.getElementById('loadHeatmap');
if (loadEl) renderLoadHeatmap();
function renderUnsafeHeatmap() {
const grid = document.getElementById('unsafeHeatmap');
if (!grid) return;
const fileCount = {};
(DATA.unsafe_reports || []).forEach(r => {
const f = r.source_file || 'unknown';
fileCount[f] = (fileCount[f] || 0) + 1;
});
(DATA.allocations || []).forEach(a => {
if (a.is_leaked) {
const f = a.source_file || 'unknown';
fileCount[f] = (fileCount[f] || 0) + 1;
}
});
const entries = Object.entries(fileCount);
if (entries.length === 0) {
grid.innerHTML = '<div class="col-span-full text-outline text-xs py-4">No unsafe or leaked sources.</div>';
return;
}
const maxCnt = Math.max(1, ...entries.map(e => e[1]));
grid.innerHTML = '';
entries.sort((a, b) => b[1] - a[1]);
entries.forEach(([file, count]) => {
const cell = document.createElement('div');
const ratio = count / maxCnt;
const intensity = 0.15 + ratio * 0.75;
cell.className = 'rounded border border-outline-variant/50 p-2 font-data-mono text-[10px] chart-interactive overflow-hidden';
cell.style.background = `rgba(239,68,68,${intensity})`;
const short = file.split('/').pop() || file;
cell.innerHTML = '<div class="truncate text-white font-bold">' + short + '</div><div class="text-on-surface-variant">' + count + ' ops</div>';
cell.title = file + ' · ' + count + ' operations';
bindInteractive(cell, 'UNSAFE_SRC_' + entries.indexOf([file, count]), [['file', file], ['count', count]], 'UNSAFE_SOURCE', file + ' · ' + count + ' ops');
grid.appendChild(cell);
});
}
const unsafeEl = document.getElementById('unsafeHeatmap');
if (unsafeEl) renderUnsafeHeatmap();
function renderWakerHeatgrid() {
const grid = document.getElementById('wakerHeatgrid');
if (!grid) return;
const bins = DATA.waker_efficiency_grid || [];
if (bins.length === 0) {
grid.innerHTML = '<div class="col-span-full text-outline text-xs px-2 py-4">No waker samples.</div>';
return;
}
const max = Math.max(1, ...bins);
grid.innerHTML = '';
bins.forEach((v, i) => {
const cell = document.createElement('div');
cell.className = 'rounded-sm chart-interactive';
const intensity = v / max;
cell.style.background = primaryMix(0.1 + intensity * 0.8);
bindInteractive(cell, 'WAKER_BIN_' + i, [['bin', '#' + i], ['efficiency', v.toFixed(3)], ['intensity', (intensity * 100).toFixed(0) + '%']], 'WAKER', 'bin #' + i + ' · efficiency ' + v.toFixed(3));
grid.appendChild(cell);
});
}
registerThemeAware('wakerHeatgrid', renderWakerHeatgrid);
renderWakerHeatgrid();
function renderPollLatencyCurve() {
const svg = document.getElementById('pollLatencySvg');
if (!svg) return;
const samples = (DATA.poll_latency_samples || []).slice(0, 60);
const W = 200, H = 80, pad = 4;
const mean = DATA.poll_latency_mean_ms || 0;
if (samples.length === 0) {
svg.innerHTML = '<text x="100" y="42" text-anchor="middle" fill="var(--on-surface-variant)" font-family="JetBrains Mono" font-size="7">NO POLL_LATENCY SAMPLES</text>';
return;
}
const maxV = Math.max(...samples, mean, 0.001);
const xStep = (W - pad * 2) / Math.max(1, samples.length - 1);
const pts = samples.map((v, i) => {
const x = pad + i * xStep;
const y = H - pad - (v / maxV) * (H - pad * 2);
return [x, y, v, i];
});
const linePath = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
const areaPath = linePath + ' L' + (W - pad) + ' ' + (H - pad) + ' L' + pad + ' ' + (H - pad) + ' Z';
const meanY = H - pad - (mean / maxV) * (H - pad * 2);
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--ke-accent').trim() || '#F59E0B';
let html = '';
html += '<defs><linearGradient id="pollGrad" x1="0" y1="0" x2="0" y2="1">';
html += '<stop offset="0%" stop-color="' + primaryColor + '" stop-opacity="0.35"/>';
html += '<stop offset="100%" stop-color="' + primaryColor + '" stop-opacity="0.02"/>';
html += '</linearGradient></defs>';
html += '<path d="' + areaPath + '" fill="url(#pollGrad)" stroke="none"/>';
html += '<line x1="' + pad + '" y1="' + meanY.toFixed(1) + '" x2="' + (W - pad) + '" y2="' + meanY.toFixed(1) + '" stroke="var(--on-surface-variant)" stroke-width="0.4" stroke-dasharray="2 2" opacity="0.6"/>';
html += '<path d="' + linePath + '" fill="none" stroke="' + primaryColor + '" stroke-width="1.2" stroke-linejoin="round"/>';
pts.forEach(p => {
html += '<circle class="poll-pt" cx="' + p[0].toFixed(1) + '" cy="' + p[1].toFixed(1) + '" r="1.8" fill="' + primaryColor + '" stroke="var(--on-background)" stroke-width="0.5" style="cursor:pointer"/>';
});
svg.innerHTML = html;
svg.querySelectorAll('.poll-pt').forEach((c, idx) => {
const p = pts[idx];
const task = (DATA.async_tasks || [])[p[3]];
const taskName = task ? task.task_name : ('sample #' + p[3]);
c.setAttribute('title', taskName + ' · ' + p[2].toFixed(2) + 'ms');
c.classList.add('chart-interactive');
c.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'POLL_LATENCY', [
['task', taskName],
['duration', p[2].toFixed(2) + 'ms'],
['mean', mean.toFixed(2) + 'ms']
]);
});
c.addEventListener('mouseleave', hideChartTooltip);
c.addEventListener('click', function() {
drillKpi('poll_sample', 'POLL_LATENCY', taskName + ' · ' + p[2].toFixed(2) + 'ms (mean ' + mean.toFixed(2) + 'ms)');
});
});
const lineEl = svg.querySelectorAll('path')[1];
if (lineEl) {
lineEl.style.cursor = 'pointer';
lineEl.classList.add('chart-interactive');
lineEl.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'POLL_LATENCY', [['samples', samples.length], ['mean', mean.toFixed(2) + 'ms'], ['max', maxV.toFixed(2) + 'ms']]);
});
lineEl.addEventListener('mouseleave', hideChartTooltip);
lineEl.addEventListener('click', function() {
drillKpi('poll_curve', 'POLL_LATENCY', samples.length + ' samples · mean ' + mean.toFixed(2) + 'ms · max ' + maxV.toFixed(2) + 'ms');
});
}
}
registerThemeAware('pollLatency', renderPollLatencyCurve);
renderPollLatencyCurve();
function renderNeighborHistogram() {
const hist = document.getElementById('neighborHistogram');
if (!hist) return;
const bins = DATA.neighbor_density_histogram || [];
if (bins.length === 0) {
hist.innerHTML = '<div class="text-outline text-xs px-1 py-2">No neighbor density samples.</div>';
return;
}
const max = Math.max(1, ...bins.map(b => (b && b.count) || 0));
hist.innerHTML = '';
bins.forEach((b, i) => {
const bar = document.createElement('div');
bar.className = 'flex-1 chart-interactive';
bar.style.background = primaryMix(0.6);
const v = (b && b.count) || 0;
const rangeLabel = (b && b.range_label) || '';
bar.style.height = Math.max(2, (v / max) * 100) + '%';
bindInteractive(bar, 'DENSITY_BIN_' + i, [['bin', '#' + i], ['count', v], ['range', rangeLabel]], 'DENSITY', 'bin #' + i + ' · ' + v + ' neighbors · ' + rangeLabel);
hist.appendChild(bar);
});
}
registerThemeAware('neighborHistogram', renderNeighborHistogram);
renderNeighborHistogram();
(function renderThreadEventLog() {
const log = document.getElementById('threadEventLog');
if (!log) return;
const events = DATA.thread_event_log || [];
if (events.length === 0) { log.innerHTML = '<div class="text-outline">No thread events recorded.</div>'; return; }
events.slice(0, 50).forEach((e, idx) => {
const row = document.createElement('div');
row.className = 'chart-interactive';
row.style.cursor = 'pointer';
row.innerHTML = `<span class="text-outline">${e.time || ''}</span> <span class="text-accent-amber">${e.level || ''}</span> <span class="text-on-surface">${e.message || ''}</span>`;
const stacks = (e.stack_traces || []).slice(0, 4);
bindInteractive(row, 'THREAD_EVENT_' + idx, [
['time', e.time || '—'],
['level', e.level || '—'],
['message', e.message || '—'],
['stack_traces', stacks.join(' || ') || '—']
], 'THREAD_EVENT', (e.level || 'EVENT') + ' @ ' + (e.time || '?') + ' · ' + (e.message || ''));
log.appendChild(row);
});
})();
(function renderTraceLog() {
const log = document.getElementById('traceLogWindow');
if (!log) return;
const traces = DATA.trace_logs || [];
if (traces.length === 0) { log.innerHTML = '<div class="text-outline">No traces.</div>'; return; }
traces.slice(0, 30).forEach((t, idx) => {
const row = document.createElement('div');
row.className = 'text-on-surface-variant chart-interactive';
row.style.cursor = 'pointer';
row.textContent = `${t.timestamp || ''} ${t.message || ''}`;
bindInteractive(row, 'TRACE_' + idx, [
['timestamp', t.timestamp || '—'],
['level', t.level || '—'],
['message', t.message || '—']
], 'TRACE_LOG', (t.level || 'TRACE') + ' @ ' + (t.timestamp || '?') + ' · ' + (t.message || ''));
log.appendChild(row);
});
})();
(function renderTaskTimeline() {
const rows = document.getElementById('taskTimelineRows');
if (!rows) return;
const tasks = DATA.async_tasks || [];
if (tasks.length === 0) { rows.innerHTML = '<div class="text-outline text-xs">No async tasks.</div>'; return; }
tasks.slice(0, 10).forEach((t, idx) => {
const row = document.createElement('div');
row.className = 'flex items-center gap-3 font-data-mono text-xs chart-interactive';
row.style.cursor = 'pointer';
row.innerHTML = `<span class="text-accent-amber w-16">${t.task_id || ''}</span><span class="text-on-surface w-32 truncate">${t.task_name || ''}</span><div class="flex-1 h-2 bg-surface-container-highest rounded-full overflow-hidden"><div class="h-full bg-primary" style="width: ${Math.min(100, (t.duration_ms || 0))}%"></div></div><span class="text-secondary w-16 text-right">${t.duration_ms || 0}ms</span>`;
bindInteractive(row, 'TASK_TIMELINE_' + idx, [
['task_id', t.task_id != null ? t.task_id : '?'],
['name', t.task_name || '—'],
['type', t.task_type || '—'],
['duration', (t.duration_ms != null ? t.duration_ms : '?') + 'ms'],
['allocations', t.total_allocations != null ? t.total_allocations : '?'],
['current_mem', (t.current_memory != null ? t.current_memory : '?') + 'B'],
['peak_mem', (t.peak_memory != null ? t.peak_memory : '?') + 'B'],
['status', t.status || '—'],
['leak', t.has_potential_leak ? 'YES ⚠' : 'no']
], 'TASK_DURATION', (t.task_name || '?') + ' · ' + (t.duration_ms != null ? t.duration_ms : '?') + 'ms · ' + (t.status || '?'));
rows.appendChild(row);
});
})();
(function renderPassportCards() {
const cards = document.getElementById('passportCards');
if (!cards) return;
if (cards.children.length > 0 && cards.children[0].tagName !== 'SCRIPT') return;
const passports = DATA.passport_details || [];
passports.slice(0, 12).forEach(p => {
const card = document.createElement('div');
card.className = 'bg-surface-container-low border border-outline-variant rounded-lg p-4';
card.innerHTML = `<div class="flex justify-between items-start mb-2"><span class="font-data-mono text-xs text-accent-amber">${p.passport_id || ''}</span><span class="font-label-caps text-[9px] text-success">${p.status || 'CLEAN'}</span></div><p class="font-data-mono text-sm text-on-surface">${p.type_name || ''}</p><p class="text-[10px] text-on-surface-variant mt-1">SIZE: ${p.size_bytes || 0} bytes</p>`;
cards.appendChild(card);
});
})();
function renderTypeChart() {
const canvas = document.getElementById('typeChart');
if (!canvas || typeof Chart === 'undefined') return;
const allocs = DATA.allocations || [];
const typeMap = {};
allocs.forEach(a => {
const t = a.type_name || 'unknown';
typeMap[t] = (typeMap[t] || 0) + 1;
});
const labels = Object.keys(typeMap).slice(0, 8);
const values = labels.map(l => typeMap[l]);
if (labels.length === 0) {
canvas.parentElement.innerHTML = '<div class="text-outline text-xs px-2 py-4">No type samples.</div>';
return;
}
const palette = [themeVar('--primary'), themeVar('--secondary'), themeVar('--tertiary'), themeVar('--accent-amber'), '#10B981', '#ef4444', '#8b5cf6', '#06b6d4'];
const existing = Chart.getChart(canvas);
if (existing) existing.destroy();
new Chart(canvas, {
type: 'doughnut',
data: { labels: labels, datasets: [{ data: values, backgroundColor: palette, borderColor: '#141218', borderWidth: 2 }] },
options: {
responsive: true, maintainAspectRatio: false, cutout: '62%',
plugins: {
legend: { position: 'right', labels: { color: '#cbc4d2', font: { family: 'JetBrains Mono', size: 10 }, boxWidth: 10, padding: 6 } },
tooltip: { callbacks: { label: ctx => ctx.label + ': ' + ctx.parsed + ' allocs' } }
},
onClick: function(event, elements) {
if (elements.length > 0) {
const idx = elements[0].index;
const label = labels[idx];
const count = values[idx];
const totalAllocs = DATA.allocations || [];
const typeAllocs = totalAllocs.filter(a => (a.type_name || 'unknown') === label);
const totalBytes = typeAllocs.reduce((s, a) => s + (a.size || 0), 0);
drillKpi('type_chart', 'TYPE_DETAIL', label + ' — ' + count + ' allocs, ' + totalBytes + 'B total');
}
},
onHover: function(event, elements) {
if (elements.length > 0) {
const idx = elements[0].index;
event.native.target.style.cursor = 'pointer';
showChartTooltip(event.native, 'TYPE_DETAIL', [['type', labels[idx]], ['allocs', values[idx]]]);
} else {
hideChartTooltip();
}
}
}
});
}
registerThemeAware('typeChart', renderTypeChart);
renderTypeChart();
function renderTimelineChart() {
const canvas = document.getElementById('timelineChart');
if (!canvas || typeof Chart === 'undefined') return;
const allocs = DATA.allocations || [];
if (allocs.length === 0) {
canvas.parentElement.innerHTML = '<div class="text-outline text-xs px-2 py-4">No timeline samples.</div>';
return;
}
const sorted = allocs.slice().sort((a, b) => (a.timestamp_alloc || 0) - (b.timestamp_alloc || 0));
const labels = sorted.map((_, i) => 'T' + i);
const sizes = sorted.map(a => a.size || 0);
const cumulative = [];
let running = 0;
sizes.forEach(s => { running += s; cumulative.push(running); });
const primaryHex = themeVar('--primary');
const secondaryHex = themeVar('--secondary');
const existing = Chart.getChart(canvas);
if (existing) existing.destroy();
new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Alloc Size (bytes)', data: sizes, borderColor: primaryHex, backgroundColor: primaryMix(0.08), borderWidth: 1.5, pointRadius: 0, tension: 0.3, yAxisID: 'y' },
{ label: 'Cumulative (bytes)', data: cumulative, borderColor: secondaryHex, backgroundColor: 'rgba(207,188,255,0.06)', borderWidth: 1.5, pointRadius: 0, tension: 0.3, fill: true, yAxisID: 'y1' }
]
},
options: {
responsive: true, maintainAspectRatio: false, animation: false,
plugins: { legend: { labels: { color: '#cbc4d2', font: { family: 'JetBrains Mono', size: 10 } } } },
scales: {
x: { ticks: { color: '#948e9c', font: { family: 'JetBrains Mono', size: 9 }, maxTicksLimit: 12 }, grid: { color: 'rgba(148,142,156,0.08)' } },
y: { position: 'left', ticks: { color: primaryHex, font: { family: 'JetBrains Mono', size: 9 } }, grid: { color: 'rgba(148,142,156,0.08)' } },
y1: { position: 'right', ticks: { color: secondaryHex, font: { family: 'JetBrains Mono', size: 9 } }, grid: { drawOnChartArea: false } }
},
onClick: function(event, elements) {
if (elements.length > 0) {
const idx = elements[0].index;
const a = sorted[idx];
if (a) {
drillKpi('timeline', 'ALLOC_INSPECT', '#' + idx + ' ' + (a.type_name || '?') + ' ' + (a.size || 0) + 'B @ ' + (a.address || '?'));
}
}
},
onHover: function(event, elements) {
if (elements.length > 0) {
const idx = elements[0].index;
const a = sorted[idx];
event.native.target.style.cursor = 'pointer';
showChartTooltip(event.native, 'ALLOC_INSPECT', [['#', idx], ['type', a.type_name || '?'], ['size', (a.size || 0) + 'B'], ['addr', a.address || '?'], ['leaked', a.is_leaked ? 'yes' : 'no']]);
} else {
hideChartTooltip();
}
}
}
});
}
registerThemeAware('timelineChart', renderTimelineChart);
renderTimelineChart();
(function renderFlamegraph() {
const svg = document.getElementById('flameSvg');
const wrap = document.getElementById('flameNodeCount');
if (!svg) return;
const allocs = DATA.allocations || [];
const fileMap = {};
allocs.forEach(a => {
const f = a.source_file || 'unknown';
if (!fileMap[f]) fileMap[f] = { count: 0, size: 0 };
fileMap[f].count++;
fileMap[f].size += (a.size || 0);
});
const entries = Object.entries(fileMap).sort((a, b) => b[1].size - a[1].size).slice(0, 12);
if (entries.length === 0) {
svg.innerHTML = '<text x="10" y="100" fill="#948e9c" font-family="JetBrains Mono" font-size="11">No flame samples.</text>';
return;
}
if (wrap) wrap.textContent = entries.length + ' frames';
const maxLen = Math.max(...entries.map(e => e[1].size));
const barH = 14;
const gap = 2;
const W = 800;
let html = '';
entries.forEach((entry, i) => {
const file = entry[0];
const info = entry[1];
const w = Math.max(2, (info.size / maxLen) * (W - 180));
const y = i * (barH + gap) + 4;
const label = file.split('/').pop() || file;
const shortLabel = label.length > 22 ? label.slice(0, 22) + '…' : label;
html += `<rect x="0" y="${y}" width="${W}" height="${barH}" fill="#211f24" rx="2"/>`;
const fillAlpha = (0.3 + (info.size/maxLen)*0.6).toFixed(2);
html += `<rect x="0" y="${y}" width="${w}" height="${barH}" style="fill: color-mix(in srgb, var(--primary) ${(0.3 + (info.size/maxLen)*0.6)*100}% , transparent)" rx="2" class="flame-bar chart-interactive" data-file="${label}" data-count="${info.count}" data-size="${info.size}"/>`;
html += `<text x="6" y="${y + 10}" fill="#e6e0e9" font-family="JetBrains Mono" font-size="9">${shortLabel}</text>`;
html += `<text x="${W - 6}" y="${y + 10}" style="fill: var(--accent-amber)" font-family="JetBrains Mono" font-size="9" text-anchor="end">${info.size}B</text>`;
});
svg.innerHTML = html;
svg.querySelectorAll('.flame-bar').forEach(bar => {
const file = bar.getAttribute('data-file');
const cnt = bar.getAttribute('data-count');
const sz = bar.getAttribute('data-size');
bar.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'FLAME_NODE', [['file', file], ['allocs', cnt], ['bytes', sz + 'B']]);
});
bar.addEventListener('mouseleave', hideChartTooltip);
bar.addEventListener('click', function() {
drillKpi('flame', 'FLAME_NODE', file + ' — ' + cnt + ' allocs, ' + sz + 'B');
});
});
})();
(function renderFfiTopo() {
const svg = document.getElementById('ffiTopoSvg');
if (!svg) return;
const topo = DATA.ffi_call_topology || {};
const nodes = topo.nodes || [];
const edges = topo.edges || [];
if (nodes.length === 0) {
svg.innerHTML = '<text x="50%" y="50%" text-anchor="middle" fill="#948e9c" font-family="JetBrains Mono" font-size="11">No FFI topology nodes.</text>';
return;
}
const W = svg.clientWidth || 600;
const H = 400;
const cx = W / 2;
const cy = H / 2;
const radius = Math.min(W, H) * 0.35;
const positions = nodes.map((node, i) => {
const angle = (i / nodes.length) * Math.PI * 2 - Math.PI / 2;
return { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius, node: node };
});
let html = '';
edges.forEach(edge => {
const src = positions[edge.source] || positions[0];
const tgt = positions[edge.target] || positions[1];
if (!src || !tgt) return;
const edgeStroke = edge.is_active ? 'var(--accent-amber)' : '#494551';
html += `<line x1="${src.x}" y1="${src.y}" x2="${tgt.x}" y2="${tgt.y}" style="stroke: ${edgeStroke}" stroke-width="1.5" stroke-dasharray="${edge.is_active ? '0' : '4 3'}" opacity="0.6" class="ffi-edge" data-label="${edge.label || ''}" data-active="${edge.is_active ? 1 : 0}" style="cursor:pointer"/>`;
const mx = (src.x + tgt.x) / 2;
const my = (src.y + tgt.y) / 2;
if (edge.label) {
html += `<text x="${mx}" y="${my - 4}" fill="#948e9c" font-family="JetBrains Mono" font-size="7" text-anchor="middle">${edge.label.slice(0, 18)}</text>`;
}
});
positions.forEach((pos, i) => {
const node = pos.node;
const fillVar = node.status === 'active' ? 'var(--accent-amber)' : node.status === 'hot' ? '#ef4444' : 'var(--primary)';
html += `<circle cx="${pos.x}" cy="${pos.y}" r="6" style="fill: ${fillVar}" opacity="0.85" class="ffi-node chart-interactive" data-name="${node.name}" data-type="${node.node_type}" data-status="${node.status || '?'}"/>`;
const label = (node.name || '').slice(0, 16);
html += `<text x="${pos.x}" y="${pos.y + 18}" fill="#cbc4d2" font-family="JetBrains Mono" font-size="8" text-anchor="middle">${label}</text>`;
});
svg.innerHTML = html;
svg.querySelectorAll('.ffi-node').forEach(n => {
const nm = n.getAttribute('data-name');
const tp = n.getAttribute('data-type');
const st = n.getAttribute('data-status');
n.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'FFI_NODE', [['name', nm], ['type', tp], ['status', st]]);
});
n.addEventListener('mouseleave', hideChartTooltip);
n.addEventListener('click', function() {
drillKpi('ffi_node', 'FFI_NODE', nm + ' [' + tp + '] — ' + st);
});
});
svg.querySelectorAll('.ffi-edge').forEach(e => {
const lbl = e.getAttribute('data-label');
const act = e.getAttribute('data-active') === '1';
e.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'FFI_EDGE', [['label', lbl || '—'], ['active', act ? 'yes' : 'no']]);
});
e.addEventListener('mouseleave', hideChartTooltip);
});
})();
function renderVariableGraph() {
const container = document.getElementById('variableGraphContainer');
if (!container) return;
const rels = DATA.relationships || [];
if (!rels.length) {
container.innerHTML = '<div class="text-outline text-xs px-2 py-4">No relationship data.</div>';
return;
}
const nodeMeta = {};
rels.forEach(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
if (!nodeMeta[s]) nodeMeta[s] = { name: s, type: r.type_name || '?', ptr: r.source_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
if (!nodeMeta[t]) nodeMeta[t] = { name: t, type: r.type_name || '?', ptr: r.target_ptr || '', inDeg: 0, outDeg: 0, cycle: false };
nodeMeta[s].outDeg++;
nodeMeta[t].inDeg++;
if (r.is_part_of_cycle) { nodeMeta[s].cycle = true; nodeMeta[t].cycle = true; }
});
const nodeNames = Object.keys(nodeMeta)
.sort((a, b) => (nodeMeta[b].inDeg + nodeMeta[b].outDeg) - (nodeMeta[a].inDeg + nodeMeta[a].outDeg))
.slice(0, 36);
const visSet = {};
nodeNames.forEach(n => { visSet[n] = true; });
const W = container.clientWidth || 640;
const H = 380;
const cx = W / 2, cy = H / 2;
const radius = Math.min(W, H) * 0.38;
const pos = {};
nodeNames.forEach((n, i) => {
const angle = (i / nodeNames.length) * Math.PI * 2 - Math.PI / 2;
pos[n] = { x: cx + Math.cos(angle) * radius, y: cy + Math.sin(angle) * radius };
});
const edges = rels.filter(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
return visSet[s] && visSet[t] && s !== t;
}).slice(0, 140);
let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '" id="varGraphSvg">';
html += '<defs><marker id="vgArrow" viewBox="0 -5 10 10" refX="10" refY="0" markerWidth="5" markerHeight="5" orient="auto"><path d="M0,-5L10,0,L0,5" fill="#948e9c"/></marker></defs>';
edges.forEach(e => {
const s = e.source_var_name || e.source_ptr || '?';
const t = e.target_var_name || e.target_ptr || '?';
const sp = pos[s], tp = pos[t];
if (!sp || !tp) return;
const mx = (sp.x + tp.x) / 2, my = (sp.y + tp.y) / 2;
const dx = tp.x - sp.x, dy = tp.y - sp.y;
const ctrlX = mx - dy * 0.12, ctrlY = my + dx * 0.12;
const color = e.color || '#494551';
const sw = Math.max(0.8, (e.strength || 0.5) * 2.2).toFixed(2);
html += '<path class="vg-edge" data-source="' + s + '" data-target="' + t + '" d="M' + sp.x + ',' + sp.y + ' Q' + ctrlX + ',' + ctrlY + ' ' + tp.x + ',' + tp.y + '" stroke="' + color + '" stroke-width="' + sw + '" fill="none" opacity="0.45" marker-end="url(#vgArrow)" style="cursor:pointer"/>';
});
nodeNames.forEach(n => {
const p = pos[n];
const m = nodeMeta[n];
const deg = m.inDeg + m.outDeg;
const r = Math.max(4, Math.min(11, 4 + deg * 0.45));
const ring = m.cycle ? '#ef4444' : 'var(--accent-amber)';
html += '<g class="vg-node" data-name="' + n + '" data-type="' + m.type + '" data-ptr="' + m.ptr + '" data-in="' + m.inDeg + '" data-out="' + m.outDeg + '" data-cycle="' + (m.cycle ? 1 : 0) + '" style="cursor:pointer">';
html += '<circle cx="' + p.x + '" cy="' + p.y + '" r="' + r + '" fill="var(--primary)" stroke="' + ring + '" stroke-width="1.5"/>';
const label = n.length > 14 ? n.slice(0, 14) + '…' : n;
html += '<text x="' + p.x + '" y="' + (p.y - r - 4) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle">' + label + '</text>';
html += '</g>';
});
html += '</svg>';
container.innerHTML = html;
const svgEl = container.querySelector('#varGraphSvg');
function highlightNode(name) {
svgEl.querySelectorAll('.vg-edge').forEach(e => {
const active = e.getAttribute('data-source') === name || e.getAttribute('data-target') === name;
e.setAttribute('opacity', active ? '0.95' : '0.10');
});
svgEl.querySelectorAll('.vg-node').forEach(g => {
g.style.opacity = (g.getAttribute('data-name') === name) ? '1' : '0.30';
});
}
function clearHighlight() {
svgEl.querySelectorAll('.vg-edge').forEach(e => { e.setAttribute('opacity', '0.45'); });
svgEl.querySelectorAll('.vg-node').forEach(g => { g.style.opacity = '1'; });
}
svgEl.querySelectorAll('.vg-node').forEach(g => {
const name = g.getAttribute('data-name');
const type = g.getAttribute('data-type');
const ptr = g.getAttribute('data-ptr');
const inDeg = g.getAttribute('data-in');
const outDeg = g.getAttribute('data-out');
const isCycle = g.getAttribute('data-cycle') === '1';
g.addEventListener('mouseenter', function(ev) {
showChartTooltip(ev, 'VARIABLE_NODE', [
['name', name], ['type', type], ['ptr', ptr || '—'],
['in_degree', inDeg], ['out_degree', outDeg],
['total_rels', (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0)],
['cycle', isCycle ? 'YES' : 'no']
]);
highlightNode(name);
});
g.addEventListener('mousemove', moveChartTooltip);
g.addEventListener('mouseleave', function() { hideChartTooltip(); clearHighlight(); });
g.addEventListener('click', function() {
const detail = document.getElementById('detailNodeName');
const status = document.getElementById('detailNodeStatus');
const typeEl = document.getElementById('detailNodeType');
const cycleEl = document.getElementById('detailNodeCycle');
const execEl = document.getElementById('detailNodeExecTime');
const upEl = document.getElementById('detailNodeUpstream');
const traceEl = document.getElementById('detailNodeTrace');
const uuidEl = document.getElementById('detailNodeUuid');
if (detail) detail.textContent = name;
if (status) status.textContent = 'in:' + inDeg + ' · out:' + outDeg;
if (typeEl) typeEl.textContent = 'TYPE: ' + (type || '—');
if (cycleEl) {
cycleEl.textContent = isCycle ? '⚠ PART OF CYCLE (leak risk)' : '✓ not in cycle';
cycleEl.className = 'font-data-mono text-[10px] mt-1 ' + (isCycle ? 'text-error' : 'text-success');
}
if (execEl) execEl.textContent = ptr ? ptr : '—';
if (upEl) upEl.textContent = (parseInt(inDeg) || 0) + (parseInt(outDeg) || 0) + ' total';
if (uuidEl) uuidEl.textContent = 'PTR: ' + (ptr || '—');
if (traceEl) {
const incidents = rels.filter(r => (r.source_var_name || r.source_ptr) === name || (r.target_var_name || r.target_ptr) === name).slice(0, 8);
if (incidents.length) {
traceEl.innerHTML = incidents.map(r => {
const s = r.source_var_name || r.source_ptr || '?';
const t = r.target_var_name || r.target_ptr || '?';
const cyc = r.is_part_of_cycle ? ' ⚠' : '';
return '<div class="text-on-surface-variant exec-trace-item" data-source="' + s + '" data-target="' + t + '" style="cursor:pointer">' + s + ' → ' + t + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2) + cyc + '</div>';
}).join('');
traceEl.querySelectorAll('.exec-trace-item').forEach((item, i) => {
const r = incidents[i] || {};
const sName = item.getAttribute('data-source');
const tName = item.getAttribute('data-target');
item.classList.add('chart-interactive');
item.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'EXEC_TRACE', [
['from', sName],
['to', tName],
['type', r.relationship_type || 'link'],
['strength', (r.strength || 0).toFixed(2)],
['type_name', r.type_name || '—'],
['cycle', r.is_part_of_cycle ? 'YES ⚠' : 'no'],
['container_src', r.is_container_source ? 'yes' : 'no'],
['container_tgt', r.is_container_target ? 'yes' : 'no']
]);
});
item.addEventListener('mouseleave', hideChartTooltip);
item.addEventListener('click', function() {
drillKpi('exec_trace', 'RELATIONSHIP', sName + ' → ' + tName + ' · ' + (r.relationship_type || 'link') + ' · ' + (r.strength || 0).toFixed(2));
});
});
} else {
traceEl.innerHTML = '<div class="text-outline">No incident relationships.</div>';
}
}
drillKpi('var_node', 'VARIABLE_NODE', name + ' · ' + type + ' · in ' + inDeg + ' / out ' + outDeg + (isCycle ? ' · CYCLE' : ''));
});
});
svgEl.querySelectorAll('.vg-edge').forEach(e => {
const s = e.getAttribute('data-source');
const t = e.getAttribute('data-target');
const rel = rels.find(r => (r.source_var_name || r.source_ptr) === s && (r.target_var_name || r.target_ptr) === t) || {};
e.addEventListener('mouseenter', function(ev) {
showChartTooltip(ev, 'RELATIONSHIP', [
['from', s], ['to', t],
['type', rel.relationship_type || 'link'],
['strength', (rel.strength != null ? rel.strength.toFixed(2) : '?')],
['type_name', rel.type_name || '—'],
['cycle', rel.is_part_of_cycle ? 'yes' : 'no']
]);
});
e.addEventListener('mousemove', moveChartTooltip);
e.addEventListener('mouseleave', hideChartTooltip);
e.addEventListener('click', function() {
drillKpi('var_edge', 'RELATIONSHIP', s + ' → ' + t + ' · ' + (rel.relationship_type || 'link'));
});
});
}
registerThemeAware('variableGraph', renderVariableGraph);
renderVariableGraph();
(function renderTaskTopology() {
const canvas = document.getElementById('taskTopologyCanvas');
if (!canvas) return;
const nodes = DATA.task_topology_nodes || [];
const edges = DATA.task_topology_edges || [];
if (nodes.length === 0) {
canvas.innerHTML = '<div class="text-outline text-xs px-4 py-8">No topology data.</div>';
return;
}
const W = canvas.clientWidth || 600;
const H = 410;
let html = '<svg width="100%" height="' + H + '" viewBox="0 0 ' + W + ' ' + H + '">';
const nodeMap = {};
nodes.forEach(n => { nodeMap[n.task_id] = n; });
edges.forEach(edge => {
const src = nodeMap[edge.source];
const tgt = nodeMap[edge.target];
if (!src || !tgt) return;
const x1 = (src.x_pct / 100) * W;
const y1 = (src.y_pct / 100) * H;
const x2 = (tgt.x_pct / 100) * W;
const y2 = (tgt.y_pct / 100) * H;
const edgeStroke = edge.is_active ? 'var(--accent-amber)' : '#494551';
html += '<line x1="' + x1 + '" y1="' + y1 + '" x2="' + x2 + '" y2="' + y2 + '" style="stroke: ' + edgeStroke + '" stroke-width="1.5" opacity="0.5" class="topo-edge" data-active="' + (edge.is_active ? 1 : 0) + '" style="cursor:pointer"/>';
});
nodes.forEach(node => {
const x = (node.x_pct / 100) * W;
const y = (node.y_pct / 100) * H;
const fillVar = node.status === 'RUNNING' ? '#10B981' : node.status === 'POLLING' ? 'var(--accent-amber)' : node.status === 'COMPLETED' ? 'var(--primary)' : '#948e9c';
html += '<circle cx="' + x + '" cy="' + y + '" r="8" style="fill: ' + fillVar + '" opacity="0.85" class="topo-node chart-interactive" data-id="' + node.task_id + '" data-name="' + node.name + '" data-status="' + node.status + '" data-duration="' + (node.duration_ms || 0) + '"/>';
html += '<text x="' + x + '" y="' + (y + 18) + '" fill="#cbc4d2" font-family="JetBrains Mono" font-size="9" text-anchor="middle" paint-order="stroke" stroke="#141218" stroke-width="3">' + (node.name || '').slice(0, 18) + '</text>';
});
html += '</svg>';
canvas.innerHTML = html;
canvas.querySelectorAll('.topo-node').forEach(n => {
const nm = n.getAttribute('data-name');
const id = n.getAttribute('data-id');
const st = n.getAttribute('data-status');
const dur = n.getAttribute('data-duration');
n.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_NODE', [['name', nm], ['task_id', id], ['status', st], ['duration', dur + 'ms']]);
});
n.addEventListener('mouseleave', hideChartTooltip);
n.addEventListener('click', function() {
drillKpi('topo_node', 'TASK_NODE', nm + ' [' + id + '] — ' + st + ' (' + dur + 'ms)');
});
});
canvas.querySelectorAll('.topo-edge').forEach(e => {
const act = e.getAttribute('data-active') === '1';
e.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_EDGE', [['active', act ? 'yes' : 'no']]);
});
e.addEventListener('mouseleave', hideChartTooltip);
});
})();
(function renderFfiThreadTimeline() {
const container = document.getElementById('ffiThreadTimeline');
if (!container) return;
const rows = DATA.thread_timeline || [];
if (rows.length === 0) {
container.innerHTML = '<div class="text-outline text-xs">No timeline data.</div>';
return;
}
let html = '';
rows.forEach((row, ri) => {
html += '<div class="flex items-center gap-3">';
html += '<span class="font-data-mono text-xs text-accent-amber w-32 truncate">' + (row.thread_name || '?') + '</span>';
html += '<div class="flex-1 h-5 bg-surface-container-low rounded overflow-hidden relative">';
(row.segments || []).forEach((seg, si) => {
const w = Math.max(1, seg.width_pct);
html += '<div class="absolute h-full opacity-70 chart-interactive" style="left:' + seg.start_pct + '%;width:' + w + '%;background:' + (seg.color || 'var(--primary)') + '" title="' + (row.thread_name || '') + ' seg #' + si + ' · ' + seg.width_pct + '%"></div>';
});
html += '</div></div>';
});
container.innerHTML = html;
container.querySelectorAll('.chart-interactive').forEach((seg, i) => {
seg.addEventListener('click', function() {
drillKpi('timeline_seg', 'TIMELINE_SEG', this.getAttribute('title'));
});
});
})();
(function addGridInteractivity() {
const heapGrid = document.getElementById('heapGrid');
if (heapGrid) {
const allocs = DATA.allocations || [];
const sizes = allocs.map(a => a.size || 0).filter(s => s > 0);
heapGrid.querySelectorAll('div').forEach((cell, i) => {
const idx = i % Math.max(1, sizes.length);
const sz = sizes[idx] || 0;
cell.setAttribute('title', 'cell #' + i + ' · size ' + sz + 'B');
cell.style.cursor = 'pointer';
cell.addEventListener('click', function() {
drillKpi('heap_cell', 'HEAP_CELL', 'cell #' + i + ' · size ' + sz + 'B');
});
});
}
const affGrid = document.getElementById('affinityGrid');
if (affGrid) {
affGrid.querySelectorAll('div').forEach((cell, i) => {
const label = cell.textContent || '?';
const state = label === 'P' ? 'PROCESSING' : label === 'I' ? 'IO_WAIT' : 'IDLE';
cell.setAttribute('title', 'core #' + i + ' · ' + state);
cell.style.cursor = 'pointer';
cell.addEventListener('click', function() {
drillKpi('affinity', 'CORE_' + i, 'core #' + i + ' · ' + state);
});
});
}
const wakerGrid = document.getElementById('wakerHeatgrid');
if (wakerGrid) {
const bins = DATA.waker_efficiency_grid || [];
wakerGrid.querySelectorAll('div').forEach((cell, i) => {
const val = bins[i] !== undefined ? bins[i].toFixed(3) : '0';
cell.setAttribute('title', 'waker bin #' + i + ' · efficiency ' + val);
cell.style.cursor = 'pointer';
cell.addEventListener('click', function() {
drillKpi('waker', 'WAKER_BIN_' + i, 'efficiency ' + val);
});
});
}
const hist = document.getElementById('neighborHistogram');
if (hist) {
const bins = DATA.neighbor_density_histogram || [];
hist.querySelectorAll('div').forEach((bar, i) => {
const b = bins[i] || {};
const count = b.count || 0;
bar.setAttribute('title', 'bin #' + i + ' · ' + count + ' neighbors · ' + (b.range_label || ''));
bar.style.cursor = 'pointer';
bar.addEventListener('click', function() {
drillKpi('neighbor', 'DENSITY_BIN_' + i, count + ' neighbors · ' + (b.range_label || ''));
});
});
}
document.querySelectorAll('#mode-thread .flex.items-end.gap-1 .flex-1').forEach((bar, i) => {
const h = bar.style.height || '0%';
bar.setAttribute('title', 'lag sample #' + i + ' · ' + h);
bar.style.cursor = 'pointer';
bar.classList.add('chart-interactive');
bar.addEventListener('click', function() {
drillKpi('lag_bar', 'LAG_SAMPLE_' + i, 'scheduler lag ' + h);
});
});
document.querySelectorAll('#mode-thread .grid.grid-cols-8.gap-1 .aspect-square').forEach((cell, i) => {
const val = cell.textContent || '?';
cell.setAttribute('title', 'affinity cell #' + i + ' · ' + val);
cell.style.cursor = 'pointer';
cell.classList.add('chart-interactive');
cell.addEventListener('click', function() {
drillKpi('aff_cell', 'AFFINITY_' + i, 'cell #' + i + ' · ' + val);
});
});
document.querySelectorAll('#mode-unsafe .grid.grid-cols-2 .text-center').forEach((card, i) => {
const labels = ['NODES', 'EDGES', 'CYCLES', 'RC_CLONES', 'ARC_CLONES'];
const val = card.querySelector('.font-headline-md');
card.style.cursor = 'pointer';
card.classList.add('chart-interactive');
card.setAttribute('title', (labels[i] || 'STAT') + ': ' + (val ? val.textContent : '?'));
card.addEventListener('click', function() {
drillKpi('own_stat', labels[i] || 'STAT', (labels[i] || 'STAT') + ': ' + (val ? val.textContent : '?'));
});
});
document.querySelectorAll('#passportCards .passport-card').forEach((card) => {
const idx = parseInt(card.getAttribute('data-idx') || '0', 10);
const p = (DATA.passport_details || [])[idx] || {};
card.style.cursor = 'pointer';
card.classList.add('chart-interactive');
const lifeEv = (p.lifecycle_events || []).map(e => e.event_type).join(', ') || '—';
const ffiEv = (p.cross_boundary_events || []).map(e => e.event_type).join(', ') || '—';
const factors = (p.risk_factors || []).join('; ') || '—';
card.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'PASSPORT ' + (p.passport_id || '?'), [
['var', p.var_name || '—'],
['type', p.type_name || '—'],
['size', (p.size_bytes || 0) + 'B'],
['status', p.status || '—'],
['risk', p.risk_level || '—' + ' (' + (p.risk_confidence || 0) + ')'],
['ffi_tracked', p.ffi_tracked ? 'yes' : 'no'],
['is_active', p.is_active ? 'yes' : 'no'],
['is_leaked', p.is_leaked ? 'yes' : 'no'],
['lifecycle', lifeEv],
['ffi_crossings', ffiEv],
['ptr', p.allocation_ptr || '—'],
['src', p.source_location || '—'],
['risk_factors', factors]
]);
});
card.addEventListener('mouseleave', hideChartTooltip);
card.addEventListener('click', function() {
drillKpi('passport_card', 'PASSPORT',
(p.passport_id || '?') + ' · ' + (p.var_name || p.type_name || '?') +
' · ' + (p.risk_level || '?') + ' risk · ' + (p.size_bytes || 0) + 'B');
});
});
const resBars = document.getElementById('ffiResourceBars');
if (resBars) {
const bars = DATA.resource_bars || [];
resBars.querySelectorAll('div').forEach(el => {
if (!el.querySelector || !el.querySelector('.bg-surface-container-highest')) return;
});
resBars.querySelectorAll('[style*="width:"]').forEach((bar, i) => {
const wrapper = bar.closest('div').parentElement;
const labelEl = wrapper ? wrapper.querySelector('span') : null;
const pctEl = wrapper ? wrapper.querySelectorAll('span')[1] : null;
const label = labelEl ? labelEl.textContent : 'resource ' + i;
const pct = pctEl ? pctEl.textContent : '?';
bar.style.cursor = 'pointer';
bar.classList.add('chart-interactive');
bar.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'RESOURCE', [['label', label], ['usage', pct]]);
});
bar.addEventListener('mouseleave', hideChartTooltip);
bar.addEventListener('click', function() {
drillKpi('resource', 'RESOURCE', label + ' · ' + pct);
});
});
}
document.querySelectorAll('.unsafe-op-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const r = (DATA.unsafe_reports || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
const lifeEv = (r.lifecycle_events || []).map(e => e.event_type + ' @ ' + e.context).join('; ') || '—';
const ffiEv = (r.cross_boundary_events || []).map(e => e.event_type + ' ' + e.from_context + '→' + e.to_context).join('; ') || '—';
const factors = (r.risk_factors || []).join('; ') || '—';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'UNSAFE ' + (r.passport_id || '?'), [
['var', r.var_name || '—'],
['type', r.type_name || '—'],
['size', (r.size_bytes || 0) + 'B'],
['risk_level', r.risk_level || '—'],
['status', r.status || '—'],
['is_leaked', r.is_leaked ? 'yes' : 'no'],
['ptr', r.allocation_ptr || '—'],
['risk_factors', factors],
['lifecycle', lifeEv],
['ffi_crossings', ffiEv]
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('unsafe_op', 'UNSAFE',
(r.var_name || r.type_name || '?') + ' · ' + (r.risk_level || '?') +
' · ' + (r.risk_factors || []).length + ' factors');
});
});
document.querySelectorAll('.ffi-crossing-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const edge = ((DATA.ffi_call_topology || {}).edges || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'FFI_CROSSING', [
['from', edge.from_name || edge.from || '?'],
['to', edge.to_name || edge.to || '?'],
['label', edge.label || '—'],
['call_count', edge.call_count != null ? edge.call_count : '?'],
['direction', (edge.from_name || '') + ' → ' + (edge.to_name || '')]
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('ffi_crossing', 'FFI_EDGE',
(edge.from_name || '?') + ' → ' + (edge.to_name || '?') + ' · ' + (edge.label || ''));
});
});
document.querySelectorAll('.own-stat-card').forEach((card) => {
const stat = card.getAttribute('data-stat') || '?';
const val = card.querySelector('.font-headline-md');
const v = val ? val.textContent.trim() : '?';
const desc = {
nodes: 'Total nodes in the ownership graph (every tracked allocation)',
edges: 'Total ownership edges (clones, borrows, moves)',
cycles: 'Retain cycles detected — potential memory leaks',
rc_clones: 'Rc<T> clone operations tracked',
arc_clones: 'Arc<T> clone operations tracked'
}[stat] || 'ownership graph metric';
card.classList.add('chart-interactive');
card.style.cursor = 'pointer';
card.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'OWNERSHIP_' + stat.toUpperCase(), [
['metric', stat], ['value', v], ['description', desc]
]);
});
card.addEventListener('mouseleave', hideChartTooltip);
card.addEventListener('click', function() {
drillKpi('own_stat', 'OWNERSHIP', stat + ': ' + v + ' — ' + desc);
});
});
document.querySelectorAll('.alloc-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const a = (DATA.allocations || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'ALLOCATION ' + (a.address || '?'), [
['address', a.address || '—'],
['type', a.type_name || '—'],
['var', a.var_name || '—'],
['size', a.size != null ? a.size : '?'],
['thread_id', a.thread_id != null ? a.thread_id : '?'],
['source', (a.source_file || '—') + ':' + (a.source_line != null ? a.source_line : '?')],
['module', a.module_path || '—'],
['allocation_type', a.allocation_type || '—'],
['is_leaked', a.is_leaked ? 'YES ⚠' : 'no'],
['is_clone', a.is_clone ? 'yes' : 'no'],
['clone_count', a.clone_count != null ? a.clone_count : '?'],
['is_smart_pointer', a.is_smart_pointer ? 'yes' : 'no'],
['smart_pointer_type', a.smart_pointer_type || '—'],
['immutable_borrows', a.immutable_borrows != null ? a.immutable_borrows : '?'],
['mutable_borrows', a.mutable_borrows != null ? a.mutable_borrows : '?'],
['lifetime_ms', a.lifetime_ms != null ? a.lifetime_ms : '?']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('alloc_row', 'ALLOCATION',
(a.address || '?') + ' · ' + (a.type_name || '?') +
' · ' + (a.size != null ? a.size : '?') + 'B' +
' · ' + (a.allocation_type || '?') +
(a.is_leaked ? ' · LEAKED ⚠' : ''));
});
});
document.querySelectorAll('.thread-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const t = (DATA.threads || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'THREAD ' + (t.thread_id || '?'), [
['id', t.thread_id || '—'],
['summary', t.thread_summary || '—'],
['allocations', t.allocation_count != null ? t.allocation_count : '?'],
['current_mem', t.current_memory || '—'],
['peak_mem', t.peak_memory || '—'],
['total_allocated', t.total_allocated || '—'],
['current_bytes', t.current_memory_bytes != null ? t.current_memory_bytes : '?'],
['peak_bytes', t.peak_memory_bytes != null ? t.peak_memory_bytes : '?'],
['total_bytes', t.total_allocated_bytes != null ? t.total_allocated_bytes : '?'],
['is_active', t.is_active ? 'yes' : 'no'],
['status', t.status || '—']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('thread_row', 'THREAD',
(t.thread_id || '?') + ' · ' + (t.thread_summary || '?') +
' · ' + (t.allocation_count != null ? t.allocation_count : '?') + ' allocs' +
' · ' + (t.status || '?'));
});
});
document.querySelectorAll('.sched-lag-bar').forEach((bar) => {
const idx = parseInt(bar.getAttribute('data-idx') || '0', 10);
const val = bar.getAttribute('data-val') || '?';
bar.classList.add('chart-interactive');
bar.style.cursor = 'pointer';
bar.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'SCHEDULER_LAG', [
['sample', '#' + idx],
['lag_pct', val + '%'],
['mean_ms', (DATA.scheduler_lag_ms != null ? DATA.scheduler_lag_ms : '?') + 'ms'],
['migration', (DATA.migration_rate_pct != null ? DATA.migration_rate_pct : '?') + '%']
]);
});
bar.addEventListener('mouseleave', hideChartTooltip);
bar.addEventListener('click', function() {
drillKpi('sched_lag', 'SCHED_LAG', 'sample #' + idx + ' · ' + val + '% lag');
});
});
document.querySelectorAll('.affinity-cell').forEach((cell) => {
const idx = parseInt(cell.getAttribute('data-idx') || '0', 10);
const state = cell.getAttribute('data-state') || '?';
const stateLabel = state === 'P' ? 'PROCESSING' : state === 'I' ? 'IO_WAIT' : state === 'W' ? 'WAITING' : 'IDLE';
cell.classList.add('chart-interactive');
cell.style.cursor = 'pointer';
cell.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'AFFINITY_CELL', [
['cell', '#' + idx],
['core', 'core ' + (idx % 8)],
['row', 'row ' + Math.floor(idx / 8)],
['state', state],
['meaning', stateLabel]
]);
});
cell.addEventListener('mouseleave', hideChartTooltip);
cell.addEventListener('click', function() {
drillKpi('affinity_cell', 'AFFINITY', 'cell #' + idx + ' · ' + stateLabel);
});
});
document.querySelectorAll('.thread-policy-item').forEach((li) => {
const idx = parseInt(li.getAttribute('data-idx') || '0', 10);
const p = (DATA.thread_policies || [])[idx] || {};
li.classList.add('chart-interactive');
li.style.cursor = 'pointer';
li.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'THREAD_POLICY', [
['name', p.name || '—'],
['enabled', p.enabled ? 'YES' : 'no'],
['status', p.enabled ? 'Active — policy enforced' : 'Disabled — policy not enforced']
]);
});
li.addEventListener('mouseleave', hideChartTooltip);
li.addEventListener('click', function() {
drillKpi('thread_policy', 'POLICY', (p.name || '?') + ' · ' + (p.enabled ? 'ON' : 'OFF'));
});
});
document.querySelectorAll('.async-task-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const t = (DATA.async_tasks || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'ASYNC_TASK ' + (t.task_name || '?'), [
['id', t.task_id != null ? t.task_id : '?'],
['name', t.task_name || '—'],
['type', t.task_type || '—'],
['duration', (t.duration_ms != null ? t.duration_ms : '?') + 'ms'],
['allocations', t.total_allocations != null ? t.total_allocations : '?'],
['current_mem', (t.current_memory != null ? t.current_memory : '?') + 'B'],
['peak_mem', (t.peak_memory != null ? t.peak_memory : '?') + 'B'],
['total_bytes', (t.total_bytes != null ? t.total_bytes : '?') + 'B'],
['efficiency', t.efficiency_score != null ? t.efficiency_score : '?'],
['is_completed', t.is_completed ? 'yes' : 'no'],
['has_potential_leak', t.has_potential_leak ? 'YES ⚠' : 'no'],
['status', t.status || '—']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('async_task', 'ASYNC_TASK',
(t.task_name || '?') + ' · ' + (t.task_type || '?') +
' · ' + (t.duration_ms != null ? t.duration_ms : '?') + 'ms' +
' · ' + (t.status || '?') +
(t.has_potential_leak ? ' · LEAK ⚠' : ''));
});
});
document.querySelectorAll('.async-stat').forEach((card) => {
const stat = card.getAttribute('data-stat') || '?';
const val = card.querySelector('.text-lg');
const v = val ? val.textContent.trim() : '?';
const desc = {
total_allocations: 'Total allocations across all async tasks',
total_memory: 'Total memory currently held by async tasks',
peak_memory: 'Peak memory usage during async execution',
active: 'Tasks currently being polled (not yet completed)',
completed: 'Tasks that finished successfully',
leaked: 'Tasks that leaked memory (potential runtime bug)'
}[stat] || 'async runtime metric';
card.classList.add('chart-interactive');
card.style.cursor = 'pointer';
card.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'ASYNC_' + stat.toUpperCase(), [
['metric', stat], ['value', v], ['description', desc]
]);
});
card.addEventListener('mouseleave', hideChartTooltip);
card.addEventListener('click', function() {
drillKpi('async_stat', 'ASYNC', stat + ': ' + v + ' — ' + desc);
});
});
(function bindAutoDiagnosis() {
const lists = [
{ sel: '#diagnosisContent ul:nth-of-type(1) li', key: 'top_allocation_sites', label: 'TOP_SITE', fields: ['name', 'allocation_count', 'total_bytes'] },
{ sel: '#diagnosisContent ul:nth-of-type(2) li', key: 'top_leaked_allocations', label: 'TOP_LEAK', fields: ['type_name', 'size', 'address', 'timestamp_alloc'] },
{ sel: '#diagnosisContent ul:nth-of-type(3) li', key: 'top_temporary_churn', label: 'TOP_CHURN', fields: ['name', 'allocation_count'] }
];
lists.forEach(L => {
const items = (DATA[L.key] || []);
document.querySelectorAll(L.sel).forEach((li, i) => {
const d = items[i] || {};
if (!d || (d.name === undefined && d.type_name === undefined)) return;
li.classList.add('chart-interactive');
li.style.cursor = 'pointer';
li.addEventListener('mousemove', function(ev) {
const rows = L.fields.map(f => [f, d[f] != null ? d[f] : '—']);
showChartTooltip(ev, L.label, rows);
});
li.addEventListener('mouseleave', hideChartTooltip);
li.addEventListener('click', function() {
const summary = L.fields.map(f => f + '=' + (d[f] != null ? d[f] : '?')).join(' · ');
drillKpi(L.key, L.label, summary);
});
});
});
})();
document.querySelectorAll('.symbol-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const s = (DATA.symbol_table || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'SYMBOL ' + (s.hex_addr || '?'), [
['hex_addr', s.hex_addr || '—'],
['symbol_name', s.symbol_name || '—'],
['status', s.status || '—'],
['call_count', s.call_count != null ? s.call_count : '?'],
['time_avg_us', s.time_avg_us != null ? s.time_avg_us + 'µs' : '?'],
['is_hot', s.is_hot ? 'YES 🔥' : 'no']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('symbol', 'SYMBOL',
(s.symbol_name || s.hex_addr || '?') + ' · ' + (s.status || '?') +
' · ' + (s.call_count != null ? s.call_count : '?') + ' calls' +
(s.is_hot ? ' · HOT 🔥' : ''));
});
});
document.querySelectorAll('#mode-variable table tbody tr').forEach((row, i) => {
const cells = row.querySelectorAll('td');
if (cells.length < 2) return;
const txt = Array.from(cells).map(c => c.textContent.trim()).join(' · ');
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
const rels = DATA.relationships || [];
const r = rels[i] || {};
showChartTooltip(ev, 'RELATIONSHIP_ROW', [
['from', r.source_var_name || '?'], ['to', r.target_var_name || '?'],
['type', r.relationship_type || '?'], ['strength', r.strength != null ? r.strength.toFixed(2) : '?'],
['type_name', r.type_name || '—'], ['cycle', r.is_part_of_cycle ? 'yes' : 'no']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('rel_row', 'RELATIONSHIP', txt);
});
});
document.querySelectorAll('.topo-node-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const n = (DATA.task_topology_nodes || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_NODE', [
['task_id', n.task_id != null ? n.task_id : '?'],
['name', n.name || '—'],
['parent_id', n.parent_id != null ? n.parent_id : 'root'],
['status', n.status || '—'],
['duration', (n.duration_ms != null ? n.duration_ms : '?') + 'ms'],
['position', (n.x_pct != null ? n.x_pct : '?') + '%, ' + (n.y_pct != null ? n.y_pct : '?') + '%']
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('topo_node_row', 'TASK_NODE',
(n.name || '?') + ' [' + (n.task_id || '?') + '] · ' + (n.status || '?') +
' · ' + (n.duration_ms != null ? n.duration_ms : '?') + 'ms');
});
});
document.querySelectorAll('.topo-edge-row').forEach((row) => {
const idx = parseInt(row.getAttribute('data-idx') || '0', 10);
const e = (DATA.task_topology_edges || [])[idx] || {};
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'TASK_EDGE', [
['source', e.source || '?'],
['target', e.target || '?'],
['active', e.is_active ? 'yes' : 'no'],
['direction', (e.source || '?') + ' → ' + (e.target || '?')]
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('topo_edge_row', 'TASK_EDGE',
(e.source || '?') + ' → ' + (e.target || '?') + ' · ' + (e.is_active ? 'active' : 'idle'));
});
});
document.querySelectorAll('.dep-graph-node').forEach((node) => {
const idx = parseInt(node.getAttribute('data-idx') || '0', 10);
const d = (DATA.dependency_graph_nodes || [])[idx] || {};
node.classList.add('chart-interactive');
node.style.cursor = 'pointer';
node.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'DEP_NODE', [
['name', d.name || '—'],
['id', d.id || '—'],
['position', d.position || '—'],
['status', d.status || '—'],
['opacity', d.opacity != null ? d.opacity : '?']
]);
});
node.addEventListener('mouseleave', hideChartTooltip);
node.addEventListener('click', function() {
drillKpi('dep_node', 'DEP_NODE',
(d.name || '?') + ' · ' + (d.position || '?') + (d.status ? ' · ' + d.status : ''));
});
});
(function bindStreamingTopology() {
const panel = document.querySelector('#mode-taskgraph .space-y-2.font-data-mono');
if (!panel) return;
const stats = DATA.streaming_topology_stats || {};
const rows = panel.querySelectorAll('.flex.justify-between');
const labels = ['GRAPH_NODES', 'GRAPH_EDGES', 'SAMPLING_RATE', 'WAKER_LOCKS'];
const values = [
(DATA.task_topology_nodes || []).length,
(DATA.task_topology_edges || []).length,
(stats.sampling_rate_ms != null ? stats.sampling_rate_ms : '?') + 'ms',
stats.waker_locks_status || '—'
];
const descs = [
'Total nodes in the async task topology graph',
'Total edges (parent→child relationships) in the topology',
'Streaming sample interval used when capturing topology snapshots',
'Current waker lock state — NONE means no tasks are holding wakers'
];
rows.forEach((row, i) => {
if (i >= labels.length) return;
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'STREAMING_TOPO', [
['metric', labels[i]],
['value', values[i]],
['description', descs[i]]
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('streaming_topo', 'STREAMING_TOPO', labels[i] + ': ' + values[i] + ' — ' + descs[i]);
});
});
})();
(function bindStackIntegrity() {
const si = DATA.stack_integrity || {};
const metrics = [
{ key: 'pointers_checked_pct', label: 'POINTERS_CHECKED', desc: 'Percentage of FFI pointers validated against their expected layout' },
{ key: 'memory_violations', label: 'MEMORY_VIOLATIONS', desc: 'Detected writes to deallocated or out-of-bounds memory regions' },
{ key: 'unwinding_strategy', label: 'UNWINDING_STRATEGY', desc: 'Panic strategy used when a violation is detected' }
];
const cards = document.querySelectorAll('#mode-ffi .flex.flex-col.justify-between');
let target = null;
cards.forEach(c => {
const h = c.querySelector('h2');
if (h && h.textContent.indexOf('STACK_INTEGRITY') !== -1) target = c;
});
if (!target) return;
const rows = target.querySelectorAll('.flex.justify-between');
rows.forEach((row, i) => {
if (i >= metrics.length) return;
const m = metrics[i];
const val = si[m.key];
row.classList.add('chart-interactive');
row.style.cursor = 'pointer';
row.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'STACK_INTEGRITY', [
['metric', m.label],
['value', val != null ? val : '?'],
['description', m.desc]
]);
});
row.addEventListener('mouseleave', hideChartTooltip);
row.addEventListener('click', function() {
drillKpi('stack_integrity', 'STACK_INTEGRITY', m.label + ': ' + (val != null ? val : '?') + ' — ' + m.desc);
});
});
})();
(function bindCircularRefs() {
const cr = DATA.circular_references || {};
const metrics = [
{ key: 'count', label: 'CYCLES', desc: 'Number of retain cycles detected in the ownership graph' },
{ key: 'pointers_in_cycles', label: 'POINTERS_IN_CYCLES', desc: 'Smart pointers participating in cycles vs total tracked' },
{ key: 'total_leaked_memory', label: 'EST_LEAKED', desc: 'Estimated bytes leaked due to circular references' }
];
const diag = document.getElementById('diagnosisContent');
if (!diag) return;
const lists = diag.querySelectorAll('ul.space-y-1');
const circList = lists[lists.length - 1]; if (!circList) return;
const items = circList.querySelectorAll('li');
items.forEach((li, i) => {
if (i >= metrics.length) return;
const m = metrics[i];
let val = cr[m.key];
if (m.key === 'pointers_in_cycles') {
val = (cr.pointers_in_cycles != null ? cr.pointers_in_cycles : '?') + ' / ' + (cr.total_smart_pointers != null ? cr.total_smart_pointers : '?');
}
li.classList.add('chart-interactive');
li.style.cursor = 'pointer';
li.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'CIRCULAR_REF', [
['metric', m.label],
['value', val != null ? val : '?'],
['has_cycles', cr.has_cycles ? 'YES' : 'no'],
['description', m.desc]
]);
});
li.addEventListener('mouseleave', hideChartTooltip);
li.addEventListener('click', function() {
drillKpi('circular_ref', 'CIRCULAR_REF', m.label + ': ' + (val != null ? val : '?') + ' — ' + m.desc);
});
});
})();
document.querySelectorAll('#mode-unsafe .text-on-surface-variant.flex.gap-2').forEach((li, i) => {
const txt = li.textContent.trim();
li.classList.add('chart-interactive');
li.style.cursor = 'pointer';
li.addEventListener('mousemove', function(ev) {
showChartTooltip(ev, 'OWNERSHIP_ISSUE', [['#', i], ['issue', txt]]);
});
li.addEventListener('mouseleave', hideChartTooltip);
li.addEventListener('click', function() {
drillKpi('own_issue', 'OWNERSHIP_ISSUE', 'issue #' + i + ' · ' + txt);
});
});
})();
</script>
<div id="kpiToast" class="kpi-toast"></div>
</body>
</html>