2026-01-12 15:32:33 -03:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Ladybird GC Heap Explorer</title>
|
|
|
|
|
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
|
|
|
<style>
|
|
|
|
|
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
|
|
|
|
|
|
|
|
|
:root {
|
|
|
|
|
--bg-primary: #0a0e14;
|
|
|
|
|
--bg-secondary: #0f1419;
|
|
|
|
|
--bg-tertiary: #151b23;
|
|
|
|
|
--bg-elevated: #1a222d;
|
|
|
|
|
--border-color: #2d3a4a;
|
|
|
|
|
--border-highlight: #3d4f66;
|
|
|
|
|
--text-primary: #e6e8eb;
|
|
|
|
|
--text-secondary: #8a919a;
|
|
|
|
|
--text-muted: #5c6370;
|
|
|
|
|
--accent-cyan: #00d4ff;
|
|
|
|
|
--accent-cyan-dim: #00a3c7;
|
|
|
|
|
--accent-orange: #ff9f43;
|
|
|
|
|
--accent-orange-dim: #cc7a2e;
|
|
|
|
|
--accent-purple: #b48ead;
|
|
|
|
|
--accent-green: #98c379;
|
|
|
|
|
--accent-red: #e06c75;
|
|
|
|
|
--accent-yellow: #e5c07b;
|
|
|
|
|
--root-color: #ff6b9d;
|
|
|
|
|
--node-default: #4a5568;
|
|
|
|
|
--graph-bg: #080c10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
* {
|
|
|
|
|
margin: 0;
|
|
|
|
|
padding: 0;
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
body {
|
|
|
|
|
font-family: 'Space Grotesk', -apple-system, sans-serif;
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mono {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Drop Zone */
|
|
|
|
|
#drop-zone {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
background: var(--bg-primary);
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
transition: opacity 0.3s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#drop-zone.hidden {
|
|
|
|
|
opacity: 0;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#drop-zone.drag-over {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#drop-zone.drag-over .drop-border {
|
|
|
|
|
border-color: var(--accent-cyan);
|
|
|
|
|
box-shadow: 0 0 60px rgba(0, 212, 255, 0.2);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drop-content {
|
|
|
|
|
text-align: center;
|
|
|
|
|
max-width: 600px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drop-border {
|
|
|
|
|
width: 400px;
|
|
|
|
|
height: 300px;
|
|
|
|
|
border: 2px dashed var(--border-color);
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drop-icon {
|
|
|
|
|
font-size: 64px;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drop-title {
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.drop-subtitle {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-input-wrapper {
|
|
|
|
|
margin-top: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-input-btn {
|
|
|
|
|
background: var(--accent-cyan);
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 12px 32px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-input-btn:hover {
|
|
|
|
|
background: var(--accent-cyan-dim);
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#file-input {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 32px;
|
|
|
|
|
left: 32px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-icon {
|
|
|
|
|
width: 40px;
|
|
|
|
|
height: 40px;
|
|
|
|
|
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-text {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
letter-spacing: -0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.brand-sub {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Main Layout */
|
|
|
|
|
#main-app {
|
|
|
|
|
display: none;
|
|
|
|
|
height: 100vh;
|
|
|
|
|
grid-template-columns: 320px 1fr 380px;
|
|
|
|
|
grid-template-rows: auto 1fr;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#main-app.visible {
|
|
|
|
|
display: grid;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Header */
|
|
|
|
|
.header {
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
padding: 12px 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-left {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-brand {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-brand-icon {
|
|
|
|
|
width: 28px;
|
|
|
|
|
height: 28px;
|
|
|
|
|
background: linear-gradient(135deg, var(--accent-cyan), var(--accent-purple));
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.file-name {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.header-stats {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 24px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-label {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.5px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stat-value.cyan { color: var(--accent-cyan); }
|
|
|
|
|
.stat-value.orange { color: var(--accent-orange); }
|
|
|
|
|
.stat-value.purple { color: var(--accent-purple); }
|
|
|
|
|
.stat-value.green { color: var(--accent-green); }
|
|
|
|
|
|
|
|
|
|
.reload-btn {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.reload-btn:hover {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
border-color: var(--border-highlight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Left Panel - Classes */
|
|
|
|
|
.left-panel {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-right: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-header {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.panel-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-box {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input {
|
|
|
|
|
width: 100%;
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
padding: 10px 14px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--accent-cyan);
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.search-input::placeholder {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-list {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-item:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-item.selected {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
border: 1px solid var(--border-highlight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-item.root-class {
|
|
|
|
|
border-left: 3px solid var(--root-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-name {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
flex: 1;
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-count {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
background: rgba(0, 212, 255, 0.1);
|
|
|
|
|
padding: 3px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-bar {
|
|
|
|
|
height: 3px;
|
|
|
|
|
background: var(--accent-cyan);
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
margin-top: 6px;
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.class-item:hover .class-bar {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Center - Graph */
|
|
|
|
|
.graph-container {
|
|
|
|
|
background: var(--graph-bg);
|
|
|
|
|
position: relative;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#graph-svg {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-controls {
|
|
|
|
|
position: absolute;
|
|
|
|
|
bottom: 20px;
|
|
|
|
|
left: 20px;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-btn {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
width: 36px;
|
|
|
|
|
height: 36px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-btn:hover {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
border-color: var(--border-highlight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-info {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 70px;
|
|
|
|
|
left: 20px;
|
|
|
|
|
background: rgba(15, 20, 25, 0.9);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.graph-legend {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 20px;
|
|
|
|
|
right: 20px;
|
|
|
|
|
background: rgba(15, 20, 25, 0.9);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend-item:last-child {
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend-dot {
|
|
|
|
|
width: 10px;
|
|
|
|
|
height: 10px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.legend-dot.root { background: var(--root-color); }
|
|
|
|
|
.legend-dot.selected { background: var(--accent-orange); }
|
|
|
|
|
.legend-dot.connected { background: var(--accent-cyan); }
|
|
|
|
|
.legend-dot.default { background: var(--node-default); }
|
|
|
|
|
|
|
|
|
|
/* Right Panel - Details */
|
|
|
|
|
.right-panel {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
border-left: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-header {
|
|
|
|
|
padding: 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-title {
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 1px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.selected-node-info {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-address {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
color: var(--accent-orange);
|
|
|
|
|
margin-bottom: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-class {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node-root-badge {
|
|
|
|
|
display: inline-block;
|
|
|
|
|
background: var(--root-color);
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.detail-section {
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: background 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-header:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-title {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-count {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-toggle {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
transition: transform 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-content {
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 0 8px 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.section-content.collapsed {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.15s ease;
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-item:hover {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-item.root {
|
|
|
|
|
border-left: 3px solid var(--root-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-address {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
width: 100px;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.edge-class {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Path to Root */
|
|
|
|
|
.path-section {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-header {
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-title {
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-content {
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
padding: 10px 0;
|
|
|
|
|
border-left: 2px solid var(--border-color);
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node::before {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
left: -5px;
|
|
|
|
|
top: 14px;
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
background: var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node.root::before {
|
|
|
|
|
background: var(--root-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node.target::before {
|
|
|
|
|
background: var(--accent-orange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node:first-child {
|
|
|
|
|
border-left-color: var(--root-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node:last-child {
|
|
|
|
|
border-left-color: var(--accent-orange);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node-info {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node-class {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-node-addr {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.path-root-info {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
color: var(--root-color);
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.no-selection {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 40px 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Scrollbar styling */
|
|
|
|
|
::-webkit-scrollbar {
|
|
|
|
|
width: 8px;
|
|
|
|
|
height: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-track {
|
|
|
|
|
background: var(--bg-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
background: var(--border-color);
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: var(--border-highlight);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Loading indicator */
|
|
|
|
|
.loading {
|
|
|
|
|
position: fixed;
|
|
|
|
|
inset: 0;
|
|
|
|
|
background: rgba(10, 14, 20, 0.9);
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
z-index: 2000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading.hidden {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.spinner {
|
|
|
|
|
width: 48px;
|
|
|
|
|
height: 48px;
|
|
|
|
|
border: 3px solid var(--border-color);
|
|
|
|
|
border-top-color: var(--accent-cyan);
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
animation: spin 1s linear infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.loading-text {
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Graph nodes and links */
|
|
|
|
|
.node circle {
|
|
|
|
|
stroke-width: 2px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: r 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node:hover circle {
|
|
|
|
|
r: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.node text {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 7px;
|
|
|
|
|
fill: var(--text-secondary);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.link {
|
|
|
|
|
stroke: var(--border-color);
|
|
|
|
|
stroke-opacity: 0.3;
|
|
|
|
|
fill: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.link.highlighted {
|
|
|
|
|
stroke: var(--accent-cyan);
|
|
|
|
|
stroke-opacity: 0.8;
|
|
|
|
|
stroke-width: 2px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.link.path-highlight {
|
|
|
|
|
stroke: var(--accent-orange);
|
|
|
|
|
stroke-opacity: 1;
|
|
|
|
|
stroke-width: 3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Tabs */
|
|
|
|
|
.tabs {
|
|
|
|
|
display: flex;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 12px;
|
|
|
|
|
text-align: center;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab:hover {
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.active {
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
border-bottom-color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-content {
|
|
|
|
|
display: none;
|
|
|
|
|
flex: 1;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-content.active {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Treemap styles */
|
|
|
|
|
.treemap-container {
|
|
|
|
|
flex: 1;
|
|
|
|
|
padding: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#treemap-svg {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.treemap-cell {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.treemap-cell:hover {
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.treemap-label {
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
fill: var(--text-primary);
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Filter buttons */
|
|
|
|
|
.filter-row {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
border-bottom: 1px solid var(--border-color);
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn:hover {
|
|
|
|
|
background: var(--bg-elevated);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.filter-btn.active {
|
|
|
|
|
background: var(--accent-cyan);
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
border-color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Root type badges */
|
|
|
|
|
.root-type {
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
background: rgba(255, 107, 157, 0.2);
|
|
|
|
|
color: var(--root-color);
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
/* Stack trace */
|
|
|
|
|
.stack-frame-item {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: baseline;
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
margin-bottom: 1px;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-item.highlighted {
|
|
|
|
|
background: rgba(0, 212, 255, 0.12);
|
|
|
|
|
color: var(--accent-cyan-dim);
|
|
|
|
|
border-left: 3px solid var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-index {
|
|
|
|
|
min-width: 24px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 06:49:41 -03:00
|
|
|
.stack-frame-size {
|
|
|
|
|
min-width: 32px;
|
|
|
|
|
text-align: right;
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
.stack-frame-symbol {
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-item.highlighted .stack-frame-symbol {
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 07:30:23 -03:00
|
|
|
.stack-frame-item.inline {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-item.inline.highlighted {
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-item.inline .stack-frame-size {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.stack-frame-item.inline .stack-frame-symbol {
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
.root-category-header {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
transition: background 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.root-category-header:hover {
|
|
|
|
|
background: var(--bg-elevated) !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.root-category-header .class-name {
|
|
|
|
|
flex: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.root-category-content {
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Edge direction toggle */
|
|
|
|
|
.graph-mode-toggle {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 20px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
display: flex;
|
|
|
|
|
background: rgba(15, 20, 25, 0.95);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn {
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn:first-child {
|
|
|
|
|
border-right: 1px solid var(--border-color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn:hover {
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn.active {
|
|
|
|
|
color: var(--accent-cyan);
|
|
|
|
|
background: rgba(0, 212, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.mode-btn .arrow {
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Address search */
|
|
|
|
|
.address-search {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.address-input {
|
|
|
|
|
background: var(--bg-tertiary);
|
|
|
|
|
border: 1px solid var(--border-color);
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
padding: 8px 12px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-family: 'JetBrains Mono', monospace;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
width: 180px;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.address-input:focus {
|
|
|
|
|
outline: none;
|
|
|
|
|
border-color: var(--accent-cyan);
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 212, 255, 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.address-input::placeholder {
|
|
|
|
|
color: var(--text-muted);
|
|
|
|
|
font-family: 'Space Grotesk', sans-serif;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.address-search-btn {
|
|
|
|
|
background: var(--accent-cyan);
|
|
|
|
|
color: var(--bg-primary);
|
|
|
|
|
border: none;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.address-search-btn:hover {
|
|
|
|
|
background: var(--accent-cyan-dim);
|
|
|
|
|
}
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<!-- Loading indicator -->
|
|
|
|
|
<div id="loading" class="loading hidden">
|
|
|
|
|
<div class="spinner"></div>
|
|
|
|
|
<div class="loading-text">Processing heap dump...</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Drop Zone -->
|
|
|
|
|
<div id="drop-zone">
|
|
|
|
|
<div class="brand">
|
|
|
|
|
<div class="brand-icon">GC</div>
|
|
|
|
|
<div>
|
|
|
|
|
<div class="brand-text">Ladybird GC Heap Explorer</div>
|
|
|
|
|
<div class="brand-sub">Visualize and analyze garbage collector snapshots</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="drop-content">
|
|
|
|
|
<div class="drop-border">
|
|
|
|
|
<div class="drop-icon">📊</div>
|
|
|
|
|
<div class="drop-title">Drop GC Dump Here</div>
|
|
|
|
|
<div class="drop-subtitle">JSON file from LibGC heap dump</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="file-input-wrapper">
|
|
|
|
|
<button class="file-input-btn" onclick="document.getElementById('file-input').click()">
|
|
|
|
|
Choose File
|
|
|
|
|
</button>
|
2026-02-01 16:52:16 -03:00
|
|
|
<input type="file" id="file-input" accept=".json,.js">
|
2026-01-12 15:32:33 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Main Application -->
|
|
|
|
|
<div id="main-app">
|
|
|
|
|
<!-- Header -->
|
|
|
|
|
<header class="header">
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
<div class="header-brand">
|
|
|
|
|
<div class="header-brand-icon">GC</div>
|
|
|
|
|
<span>Heap Explorer</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="file-name" id="file-name">-</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="header-stats">
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-value cyan" id="stat-nodes">0</span>
|
|
|
|
|
<span class="stat-label">Nodes</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-value orange" id="stat-roots">0</span>
|
|
|
|
|
<span class="stat-label">Roots</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-value purple" id="stat-edges">0</span>
|
|
|
|
|
<span class="stat-label">Edges</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="stat-item">
|
|
|
|
|
<span class="stat-value green" id="stat-classes">0</span>
|
|
|
|
|
<span class="stat-label">Classes</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="address-search">
|
|
|
|
|
<input type="text" id="address-input" class="address-input" placeholder="Find by address (0x...)">
|
|
|
|
|
<button class="address-search-btn" onclick="searchByAddress()">Go</button>
|
|
|
|
|
</div>
|
|
|
|
|
<button class="reload-btn" onclick="location.reload()">Load New File</button>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Left Panel -->
|
|
|
|
|
<aside class="left-panel">
|
|
|
|
|
<div class="tabs">
|
|
|
|
|
<div class="tab active" data-tab="classes">Classes</div>
|
|
|
|
|
<div class="tab" data-tab="roots">Roots</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-content active" id="tab-classes">
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<input type="text" class="search-input" id="class-search" placeholder="Filter classes...">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="filter-row">
|
|
|
|
|
<button class="filter-btn active" data-sort="count">By Count</button>
|
|
|
|
|
<button class="filter-btn" data-sort="name">By Name</button>
|
|
|
|
|
<button class="filter-btn" data-sort="edges">By Edges</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="class-list" id="class-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="tab-content" id="tab-roots">
|
|
|
|
|
<div class="search-box">
|
|
|
|
|
<input type="text" class="search-input" id="root-search" placeholder="Filter roots...">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="class-list" id="root-list"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Graph Container -->
|
|
|
|
|
<main class="graph-container">
|
|
|
|
|
<svg id="graph-svg"></svg>
|
|
|
|
|
<div class="graph-mode-toggle">
|
|
|
|
|
<button class="mode-btn active" id="mode-outgoing" onclick="setGraphMode('outgoing')">
|
|
|
|
|
<span class="arrow">→</span> Outgoing
|
|
|
|
|
</button>
|
|
|
|
|
<button class="mode-btn" id="mode-incoming" onclick="setGraphMode('incoming')">
|
|
|
|
|
<span class="arrow">←</span> Incoming
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="graph-controls">
|
|
|
|
|
<button class="graph-btn" onclick="zoomIn()" title="Zoom In">+</button>
|
|
|
|
|
<button class="graph-btn" onclick="zoomOut()" title="Zoom Out">-</button>
|
|
|
|
|
<button class="graph-btn" onclick="resetZoom()" title="Reset View">↺</button>
|
|
|
|
|
<button class="graph-btn" onclick="toggleSimulation()" title="Pause/Resume" id="pause-btn">⏸</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="graph-info" id="graph-info">
|
|
|
|
|
Click a class to visualize its instances
|
|
|
|
|
</div>
|
|
|
|
|
<div class="graph-legend">
|
|
|
|
|
<div class="legend-item"><div class="legend-dot root"></div> Root node</div>
|
|
|
|
|
<div class="legend-item"><div class="legend-dot selected"></div> Selected</div>
|
|
|
|
|
<div class="legend-item"><div class="legend-dot connected"></div> Connected</div>
|
|
|
|
|
<div class="legend-item"><div class="legend-dot default"></div> Other</div>
|
|
|
|
|
</div>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
<!-- Right Panel -->
|
|
|
|
|
<aside class="right-panel">
|
|
|
|
|
<div class="detail-header">
|
|
|
|
|
<div class="detail-title">Node Details</div>
|
|
|
|
|
<div id="selected-node-display">
|
|
|
|
|
<div class="no-selection">Select a node to view details</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
<div class="detail-section" id="stack-trace-section" style="display: none;">
|
|
|
|
|
<div class="section-header" onclick="toggleSection('stack-trace')">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
Stack Trace <span class="section-count" id="stack-trace-count">0</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="section-toggle">▼</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-content collapsed" id="stack-trace-content" style="max-height: 300px;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
<div class="detail-section" id="outgoing-section">
|
|
|
|
|
<div class="section-header" onclick="toggleSection('outgoing')">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
Outgoing Edges <span class="section-count" id="outgoing-count">0</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="section-toggle">▼</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-content" id="outgoing-content"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="detail-section" id="incoming-section">
|
|
|
|
|
<div class="section-header" onclick="toggleSection('incoming')">
|
|
|
|
|
<div class="section-title">
|
|
|
|
|
Incoming Edges <span class="section-count" id="incoming-count">0</span>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="section-toggle">▼</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="section-content" id="incoming-content"></div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="path-section">
|
|
|
|
|
<div class="path-header">
|
|
|
|
|
<span class="path-title">Path to Root</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="path-content" id="path-content">
|
|
|
|
|
<div class="no-selection">Select a node to see its retention path</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
// Global state
|
|
|
|
|
let heapData = null;
|
2026-02-28 04:42:54 -03:00
|
|
|
let stackFrames = null;
|
|
|
|
|
let stackFrameElements = null;
|
|
|
|
|
let highlightedFrameIndex = undefined;
|
2026-01-12 15:32:33 -03:00
|
|
|
let classStats = null;
|
|
|
|
|
let reverseEdges = null;
|
|
|
|
|
let rootNodes = null;
|
|
|
|
|
let selectedNode = null;
|
|
|
|
|
let selectedClass = null;
|
|
|
|
|
let simulation = null;
|
|
|
|
|
let svg, g, zoom;
|
|
|
|
|
let isPaused = false;
|
|
|
|
|
let graphMode = 'outgoing'; // 'outgoing' or 'incoming'
|
|
|
|
|
|
|
|
|
|
// DOM elements
|
|
|
|
|
const dropZone = document.getElementById('drop-zone');
|
|
|
|
|
const mainApp = document.getElementById('main-app');
|
|
|
|
|
const fileInput = document.getElementById('file-input');
|
|
|
|
|
const loading = document.getElementById('loading');
|
2026-02-28 09:21:48 -03:00
|
|
|
const classSearchInput = document.getElementById('class-search');
|
|
|
|
|
const rootSearchInput = document.getElementById('root-search');
|
|
|
|
|
let currentFilterButton = document.querySelector('.filter-btn.active');
|
2026-01-12 15:32:33 -03:00
|
|
|
|
|
|
|
|
// File handling
|
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.add('drag-over');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
|
|
|
dropZone.classList.remove('drag-over');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
dropZone.classList.remove('drag-over');
|
|
|
|
|
const file = e.dataTransfer.files[0];
|
|
|
|
|
if (file) loadFile(file);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
|
|
|
const file = e.target.files[0];
|
|
|
|
|
if (file) loadFile(file);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function loadFile(file) {
|
|
|
|
|
loading.classList.remove('hidden');
|
|
|
|
|
document.getElementById('file-name').textContent = file.name;
|
|
|
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = (e) => {
|
|
|
|
|
try {
|
2026-02-01 16:52:16 -03:00
|
|
|
let text = e.target.result;
|
|
|
|
|
// Strip "var GC_GRAPH_DUMP = " prefix and trailing ";\n" if present (.js format)
|
|
|
|
|
const prefix = 'var GC_GRAPH_DUMP = ';
|
|
|
|
|
if (text.startsWith(prefix))
|
|
|
|
|
text = text.slice(prefix.length).replace(/;\s*$/, '');
|
|
|
|
|
const data = JSON.parse(text);
|
2026-01-12 15:32:33 -03:00
|
|
|
processData(data);
|
|
|
|
|
dropZone.classList.add('hidden');
|
|
|
|
|
mainApp.classList.add('visible');
|
|
|
|
|
loading.classList.add('hidden');
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert('Error parsing JSON: ' + err.message);
|
|
|
|
|
loading.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function processData(data) {
|
2026-02-28 04:42:54 -03:00
|
|
|
// Extract stack_frames before treating data as a node map
|
|
|
|
|
stackFrames = data.stack_frames || null;
|
|
|
|
|
delete data.stack_frames;
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
heapData = data;
|
|
|
|
|
|
|
|
|
|
// Build reverse edges map
|
|
|
|
|
reverseEdges = {};
|
|
|
|
|
for (const addr in data) {
|
|
|
|
|
reverseEdges[addr] = [];
|
|
|
|
|
}
|
|
|
|
|
for (const addr in data) {
|
|
|
|
|
const node = data[addr];
|
|
|
|
|
for (const edge of (node.edges || [])) {
|
|
|
|
|
if (reverseEdges[edge]) {
|
|
|
|
|
reverseEdges[edge].push(addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Collect root nodes
|
|
|
|
|
rootNodes = {};
|
|
|
|
|
for (const addr in data) {
|
|
|
|
|
if (data[addr].root) {
|
|
|
|
|
rootNodes[addr] = data[addr].root;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute class statistics
|
|
|
|
|
classStats = {};
|
|
|
|
|
let totalEdges = 0;
|
|
|
|
|
for (const addr in data) {
|
|
|
|
|
const node = data[addr];
|
|
|
|
|
const className = node.class_name;
|
|
|
|
|
if (!classStats[className]) {
|
|
|
|
|
classStats[className] = { count: 0, edges: 0, hasRoot: false, instances: [] };
|
|
|
|
|
}
|
|
|
|
|
classStats[className].count++;
|
|
|
|
|
classStats[className].edges += (node.edges || []).length;
|
|
|
|
|
classStats[className].instances.push(addr);
|
|
|
|
|
if (node.root) classStats[className].hasRoot = true;
|
|
|
|
|
totalEdges += (node.edges || []).length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update stats
|
|
|
|
|
document.getElementById('stat-nodes').textContent = Object.keys(data).length.toLocaleString();
|
|
|
|
|
document.getElementById('stat-roots').textContent = Object.keys(rootNodes).length.toLocaleString();
|
|
|
|
|
document.getElementById('stat-edges').textContent = totalEdges.toLocaleString();
|
|
|
|
|
document.getElementById('stat-classes').textContent = Object.keys(classStats).length.toLocaleString();
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
// Render UI
|
|
|
|
|
renderStackTrace();
|
2026-01-12 15:32:33 -03:00
|
|
|
renderClassList();
|
|
|
|
|
renderRootList();
|
|
|
|
|
setupTabs();
|
|
|
|
|
initGraph();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 09:21:48 -03:00
|
|
|
function renderClassList() {
|
|
|
|
|
const filter = classSearchInput.value;
|
|
|
|
|
const sortBy = currentFilterButton.dataset.sort;
|
2026-01-12 15:32:33 -03:00
|
|
|
const list = document.getElementById('class-list');
|
|
|
|
|
const maxCount = Math.max(...Object.values(classStats).map(s => s.count));
|
|
|
|
|
|
|
|
|
|
let entries = Object.entries(classStats);
|
|
|
|
|
|
|
|
|
|
// Filter
|
|
|
|
|
if (filter) {
|
|
|
|
|
entries = entries.filter(([name]) =>
|
|
|
|
|
name.toLowerCase().includes(filter.toLowerCase())
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Sort
|
|
|
|
|
if (sortBy === 'count') {
|
|
|
|
|
entries.sort((a, b) => b[1].count - a[1].count);
|
|
|
|
|
} else if (sortBy === 'name') {
|
|
|
|
|
entries.sort((a, b) => a[0].localeCompare(b[0]));
|
|
|
|
|
} else if (sortBy === 'edges') {
|
|
|
|
|
entries.sort((a, b) => b[1].edges - a[1].edges);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list.innerHTML = entries.map(([name, stats]) => `
|
|
|
|
|
<div class="class-item ${stats.hasRoot ? 'root-class' : ''} ${selectedClass === name ? 'selected' : ''}"
|
|
|
|
|
onclick="selectClass('${name}')">
|
|
|
|
|
<div style="flex: 1; min-width: 0;">
|
|
|
|
|
<div class="class-name">${escapeHtml(name)}</div>
|
|
|
|
|
<div class="class-bar" style="width: ${(stats.count / maxCount) * 100}%"></div>
|
|
|
|
|
</div>
|
|
|
|
|
<span class="class-count">${stats.count.toLocaleString()}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 09:21:48 -03:00
|
|
|
function renderRootList() {
|
|
|
|
|
const filter = rootSearchInput.value;
|
2026-01-12 15:32:33 -03:00
|
|
|
const list = document.getElementById('root-list');
|
|
|
|
|
|
|
|
|
|
// Group roots by type
|
|
|
|
|
const rootsByType = {};
|
|
|
|
|
for (const addr in rootNodes) {
|
|
|
|
|
const rootType = rootNodes[addr];
|
|
|
|
|
const shortType = rootType.split(' ')[0];
|
|
|
|
|
if (!rootsByType[shortType]) {
|
|
|
|
|
rootsByType[shortType] = [];
|
|
|
|
|
}
|
|
|
|
|
rootsByType[shortType].push({ addr, fullType: rootType, node: heapData[addr] });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let html = '';
|
|
|
|
|
let categoryIndex = 0;
|
|
|
|
|
for (const [type, roots] of Object.entries(rootsByType).sort((a, b) => b[1].length - a[1].length)) {
|
|
|
|
|
if (filter && !type.toLowerCase().includes(filter.toLowerCase())) continue;
|
|
|
|
|
|
|
|
|
|
const categoryId = `root-category-${categoryIndex++}`;
|
|
|
|
|
html += `<div class="root-category-header" onclick="toggleRootCategory('${categoryId}')" style="background: var(--bg-tertiary); margin-bottom: 2px; cursor: pointer;">
|
|
|
|
|
<span class="section-toggle" id="${categoryId}-toggle" style="margin-right: 8px; display: inline-block; transition: transform 0.2s;">▶</span>
|
|
|
|
|
<span class="class-name" style="color: var(--root-color)">${escapeHtml(type)}</span>
|
|
|
|
|
<span class="class-count">${roots.length}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
|
|
|
|
|
html += `<div id="${categoryId}" class="root-category-content" style="display: none;">`;
|
|
|
|
|
for (const root of roots) {
|
|
|
|
|
html += `<div class="edge-item root" onclick="selectNode('${root.addr}')">
|
|
|
|
|
<span class="edge-address">${formatAddress(root.addr)}</span>
|
|
|
|
|
<span class="edge-class">${escapeHtml(root.node.class_name)}</span>
|
|
|
|
|
</div>`;
|
|
|
|
|
}
|
|
|
|
|
html += `</div>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list.innerHTML = html || '<div class="no-selection">No roots found</div>';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleRootCategory(categoryId) {
|
|
|
|
|
const content = document.getElementById(categoryId);
|
|
|
|
|
const toggle = document.getElementById(categoryId + '-toggle');
|
|
|
|
|
if (content.style.display === 'none') {
|
|
|
|
|
content.style.display = 'block';
|
|
|
|
|
toggle.style.transform = 'rotate(90deg)';
|
|
|
|
|
} else {
|
|
|
|
|
content.style.display = 'none';
|
|
|
|
|
toggle.style.transform = 'rotate(0deg)';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setupTabs() {
|
|
|
|
|
document.querySelectorAll('.tab').forEach(tab => {
|
|
|
|
|
tab.addEventListener('click', () => {
|
|
|
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
|
|
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
|
|
|
tab.classList.add('active');
|
|
|
|
|
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-28 09:21:48 -03:00
|
|
|
classSearchInput.addEventListener('input', renderClassList);
|
|
|
|
|
rootSearchInput.addEventListener('input', renderRootList);
|
2026-01-12 15:32:33 -03:00
|
|
|
|
|
|
|
|
document.querySelectorAll('.filter-btn').forEach(btn => {
|
|
|
|
|
btn.addEventListener('click', () => {
|
2026-02-28 09:21:48 -03:00
|
|
|
currentFilterButton.classList.remove('active');
|
2026-01-12 15:32:33 -03:00
|
|
|
btn.classList.add('active');
|
2026-02-28 09:21:48 -03:00
|
|
|
currentFilterButton = btn;
|
|
|
|
|
renderClassList();
|
2026-01-12 15:32:33 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectClass(className) {
|
|
|
|
|
selectedClass = className;
|
2026-02-28 09:21:48 -03:00
|
|
|
renderClassList();
|
2026-01-12 15:32:33 -03:00
|
|
|
visualizeClass(className);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initGraph() {
|
|
|
|
|
svg = d3.select('#graph-svg');
|
|
|
|
|
const width = svg.node().parentElement.clientWidth;
|
|
|
|
|
const height = svg.node().parentElement.clientHeight;
|
|
|
|
|
|
|
|
|
|
svg.selectAll('*').remove();
|
|
|
|
|
|
|
|
|
|
// Add arrow marker
|
|
|
|
|
svg.append('defs').append('marker')
|
|
|
|
|
.attr('id', 'arrowhead')
|
|
|
|
|
.attr('viewBox', '-0 -5 10 10')
|
|
|
|
|
.attr('refX', 20)
|
|
|
|
|
.attr('refY', 0)
|
|
|
|
|
.attr('orient', 'auto')
|
|
|
|
|
.attr('markerWidth', 6)
|
|
|
|
|
.attr('markerHeight', 6)
|
|
|
|
|
.append('path')
|
|
|
|
|
.attr('d', 'M 0,-5 L 10,0 L 0,5')
|
|
|
|
|
.attr('fill', 'var(--border-color)');
|
|
|
|
|
|
|
|
|
|
g = svg.append('g');
|
|
|
|
|
|
|
|
|
|
zoom = d3.zoom()
|
|
|
|
|
.scaleExtent([0.1, 10])
|
|
|
|
|
.on('zoom', (event) => {
|
|
|
|
|
g.attr('transform', event.transform);
|
|
|
|
|
// Show labels when zoomed in past 1.5x
|
|
|
|
|
const scale = event.transform.k;
|
|
|
|
|
const labelOpacity = scale > 0.8 ? Math.min((scale - 0.8) / 0.4, 1) : 0;
|
|
|
|
|
g.selectAll('.node-label').style('opacity', labelOpacity);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
svg.call(zoom);
|
|
|
|
|
|
|
|
|
|
document.getElementById('graph-info').textContent =
|
|
|
|
|
`${Object.keys(heapData).length.toLocaleString()} nodes loaded. Select a class to visualize.`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function visualizeClass(className) {
|
|
|
|
|
const stats = classStats[className];
|
|
|
|
|
if (!stats) return;
|
|
|
|
|
|
|
|
|
|
const width = svg.node().parentElement.clientWidth;
|
|
|
|
|
const height = svg.node().parentElement.clientHeight;
|
|
|
|
|
|
|
|
|
|
// Limit nodes for performance
|
|
|
|
|
const maxNodes = 500;
|
|
|
|
|
const instances = stats.instances.slice(0, maxNodes);
|
|
|
|
|
|
|
|
|
|
// Collect connected nodes based on mode
|
|
|
|
|
const nodeSet = new Set(instances);
|
|
|
|
|
const links = [];
|
|
|
|
|
|
|
|
|
|
for (const addr of instances) {
|
|
|
|
|
const node = heapData[addr];
|
|
|
|
|
|
|
|
|
|
if (graphMode === 'outgoing') {
|
|
|
|
|
// Show what this class references (outgoing edges)
|
|
|
|
|
for (const edge of (node.edges || [])) {
|
|
|
|
|
if (heapData[edge]) {
|
|
|
|
|
nodeSet.add(edge);
|
|
|
|
|
links.push({ source: addr, target: edge });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Show what references this class (incoming edges)
|
|
|
|
|
for (const incoming of (reverseEdges[addr] || [])) {
|
|
|
|
|
nodeSet.add(incoming);
|
|
|
|
|
links.push({ source: incoming, target: addr });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Limit total nodes
|
|
|
|
|
const nodes = Array.from(nodeSet).slice(0, maxNodes * 2).map(addr => ({
|
|
|
|
|
id: addr,
|
|
|
|
|
class_name: heapData[addr].class_name,
|
|
|
|
|
isRoot: !!heapData[addr].root,
|
|
|
|
|
isTarget: instances.includes(addr)
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const nodeIds = new Set(nodes.map(n => n.id));
|
|
|
|
|
const filteredLinks = links.filter(l => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
|
|
|
|
|
|
|
|
// Clear and redraw
|
|
|
|
|
g.selectAll('*').remove();
|
|
|
|
|
|
|
|
|
|
// Stop existing simulation
|
|
|
|
|
if (simulation) simulation.stop();
|
|
|
|
|
|
|
|
|
|
simulation = d3.forceSimulation(nodes)
|
|
|
|
|
.force('link', d3.forceLink(filteredLinks).id(d => d.id).distance(80))
|
|
|
|
|
.force('charge', d3.forceManyBody().strength(-150))
|
|
|
|
|
.force('center', d3.forceCenter(width / 2, height / 2))
|
|
|
|
|
.force('collision', d3.forceCollide().radius(25));
|
|
|
|
|
|
|
|
|
|
const link = g.append('g')
|
|
|
|
|
.selectAll('line')
|
|
|
|
|
.data(filteredLinks)
|
|
|
|
|
.join('line')
|
|
|
|
|
.attr('class', 'link')
|
|
|
|
|
.attr('marker-end', 'url(#arrowhead)');
|
|
|
|
|
|
|
|
|
|
const node = g.append('g')
|
|
|
|
|
.selectAll('g')
|
|
|
|
|
.data(nodes)
|
|
|
|
|
.join('g')
|
|
|
|
|
.attr('class', 'node')
|
|
|
|
|
.call(d3.drag()
|
|
|
|
|
.on('start', dragstarted)
|
|
|
|
|
.on('drag', dragged)
|
|
|
|
|
.on('end', dragended))
|
|
|
|
|
.on('click', (event, d) => {
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
selectNode(d.id);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
node.append('circle')
|
|
|
|
|
.attr('r', d => d.isTarget ? 8 : 6)
|
|
|
|
|
.attr('fill', d => {
|
|
|
|
|
if (d.isRoot) return 'var(--root-color)';
|
|
|
|
|
if (d.isTarget) return 'var(--accent-cyan)';
|
|
|
|
|
return 'var(--node-default)';
|
|
|
|
|
})
|
|
|
|
|
.attr('stroke', d => {
|
|
|
|
|
if (d.isRoot) return '#ff8ab5';
|
|
|
|
|
if (d.isTarget) return '#33ddff';
|
|
|
|
|
return '#6b7a8a';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
node.append('title')
|
|
|
|
|
.text(d => `${d.class_name}\n${d.id}`);
|
|
|
|
|
|
|
|
|
|
node.append('text')
|
|
|
|
|
.attr('class', 'node-label')
|
|
|
|
|
.attr('dy', -12)
|
|
|
|
|
.attr('text-anchor', 'middle')
|
|
|
|
|
.text(d => d.class_name)
|
|
|
|
|
.style('opacity', 0);
|
|
|
|
|
|
|
|
|
|
simulation.on('tick', () => {
|
|
|
|
|
link
|
|
|
|
|
.attr('x1', d => d.source.x)
|
|
|
|
|
.attr('y1', d => d.source.y)
|
|
|
|
|
.attr('x2', d => d.target.x)
|
|
|
|
|
.attr('y2', d => d.target.y);
|
|
|
|
|
|
|
|
|
|
node.attr('transform', d => `translate(${d.x},${d.y})`);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const modeLabel = graphMode === 'outgoing' ? 'outgoing refs from' : 'incoming refs to';
|
|
|
|
|
document.getElementById('graph-info').textContent =
|
|
|
|
|
`${nodes.length} nodes, ${filteredLinks.length} ${modeLabel} "${className}"`;
|
|
|
|
|
|
|
|
|
|
function dragstarted(event) {
|
|
|
|
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
|
|
|
event.subject.fx = event.subject.x;
|
|
|
|
|
event.subject.fy = event.subject.y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dragged(event) {
|
|
|
|
|
event.subject.fx = event.x;
|
|
|
|
|
event.subject.fy = event.y;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dragended(event) {
|
|
|
|
|
if (!event.active) simulation.alphaTarget(0);
|
|
|
|
|
event.subject.fx = null;
|
|
|
|
|
event.subject.fy = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectNode(addr) {
|
|
|
|
|
selectedNode = addr;
|
|
|
|
|
const node = heapData[addr];
|
|
|
|
|
if (!node) return;
|
|
|
|
|
|
|
|
|
|
// Update selected node display
|
|
|
|
|
const display = document.getElementById('selected-node-display');
|
|
|
|
|
display.innerHTML = `
|
|
|
|
|
<div class="selected-node-info">
|
|
|
|
|
<div class="node-address">${toHex(addr)}</div>
|
|
|
|
|
<div class="node-class">${escapeHtml(node.class_name)}</div>
|
2026-02-28 04:42:54 -03:00
|
|
|
${node.root ? `<div class="node-root-badge">ROOT: ${escapeHtml(rootLabel(node))}</div>` : ''}
|
2026-01-12 15:32:33 -03:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Update outgoing edges
|
|
|
|
|
const outgoing = node.edges || [];
|
|
|
|
|
document.getElementById('outgoing-count').textContent = outgoing.length;
|
|
|
|
|
document.getElementById('outgoing-content').innerHTML = outgoing.map(edge => `
|
|
|
|
|
<div class="edge-item ${heapData[edge]?.root ? 'root' : ''}" onclick="selectNode('${edge}')">
|
|
|
|
|
<span class="edge-address">${formatAddress(edge)}</span>
|
|
|
|
|
<span class="edge-class">${escapeHtml(heapData[edge]?.class_name || 'Unknown')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('') || '<div class="no-selection">No outgoing edges</div>';
|
|
|
|
|
|
|
|
|
|
// Update incoming edges
|
|
|
|
|
const incoming = reverseEdges[addr] || [];
|
|
|
|
|
document.getElementById('incoming-count').textContent = incoming.length;
|
|
|
|
|
document.getElementById('incoming-content').innerHTML = incoming.map(edge => `
|
|
|
|
|
<div class="edge-item ${heapData[edge]?.root ? 'root' : ''}" onclick="selectNode('${edge}')">
|
|
|
|
|
<span class="edge-address">${formatAddress(edge)}</span>
|
|
|
|
|
<span class="edge-class">${escapeHtml(heapData[edge]?.class_name || 'Unknown')}</span>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('') || '<div class="no-selection">No incoming edges</div>';
|
|
|
|
|
|
|
|
|
|
// Highlight in graph
|
|
|
|
|
highlightNode(addr);
|
|
|
|
|
|
|
|
|
|
// Automatically find path to root
|
|
|
|
|
findPathToRoot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function highlightNode(addr) {
|
|
|
|
|
g.selectAll('.node circle')
|
|
|
|
|
.attr('stroke-width', d => d.id === addr ? 4 : 2)
|
|
|
|
|
.attr('stroke', d => {
|
|
|
|
|
if (d.id === addr) return 'var(--accent-orange)';
|
|
|
|
|
if (d.isRoot) return '#ff8ab5';
|
|
|
|
|
if (d.isTarget) return '#33ddff';
|
|
|
|
|
return '#6b7a8a';
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Highlight connected links
|
|
|
|
|
g.selectAll('.link')
|
|
|
|
|
.classed('highlighted', d => d.source.id === addr || d.target.id === addr);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
function renderStackTrace() {
|
|
|
|
|
const section = document.getElementById('stack-trace-section');
|
|
|
|
|
const content = document.getElementById('stack-trace-content');
|
|
|
|
|
const count = document.getElementById('stack-trace-count');
|
|
|
|
|
|
|
|
|
|
if (!stackFrames || stackFrames.length === 0) {
|
|
|
|
|
section.style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
section.style.display = '';
|
|
|
|
|
count.textContent = stackFrames.length;
|
|
|
|
|
|
|
|
|
|
stackFrameElements = [];
|
|
|
|
|
for (let i = 0; i < stackFrames.length; i++) {
|
2026-02-28 06:49:41 -03:00
|
|
|
const frame = stackFrames[i];
|
2026-02-28 07:30:23 -03:00
|
|
|
const isInline = frame.size === 0;
|
2026-02-28 04:42:54 -03:00
|
|
|
const el = document.createElement('div');
|
2026-02-28 07:30:23 -03:00
|
|
|
el.className = 'stack-frame-item' + (isInline ? ' inline' : '');
|
2026-02-28 04:42:54 -03:00
|
|
|
el.innerHTML = `<span class="stack-frame-index">${i}</span>`
|
2026-02-28 07:30:23 -03:00
|
|
|
+ `<span class="stack-frame-size">${isInline ? '-' : formatSize(frame.size)}</span>`
|
2026-02-28 06:49:41 -03:00
|
|
|
+ `<span class="stack-frame-symbol">${escapeHtml(insertWordBreaks(frame.label) || '???')}</span>`;
|
2026-02-28 04:42:54 -03:00
|
|
|
content.appendChild(el);
|
|
|
|
|
stackFrameElements.push(el);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateStackTrace(frameIndex) {
|
|
|
|
|
if (!stackFrameElements) return;
|
|
|
|
|
if (frameIndex === highlightedFrameIndex) return;
|
|
|
|
|
if (frameIndex >= stackFrameElements.length) return;
|
|
|
|
|
|
|
|
|
|
if (highlightedFrameIndex !== undefined) {
|
|
|
|
|
stackFrameElements[highlightedFrameIndex].classList.remove('highlighted');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
highlightedFrameIndex = frameIndex;
|
|
|
|
|
|
|
|
|
|
if (frameIndex !== undefined) {
|
|
|
|
|
stackFrameElements[frameIndex].classList.add('highlighted');
|
|
|
|
|
stackFrameElements[frameIndex].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
function findPathToRoot() {
|
|
|
|
|
if (!selectedNode) return;
|
|
|
|
|
|
|
|
|
|
const path = bfsToRoot(selectedNode);
|
|
|
|
|
const pathContent = document.getElementById('path-content');
|
|
|
|
|
|
|
|
|
|
if (!path) {
|
|
|
|
|
pathContent.innerHTML = '<div class="no-selection">No path to root found (node may be unreachable)</div>';
|
2026-02-28 04:42:54 -03:00
|
|
|
updateStackTrace(undefined);
|
2026-01-12 15:32:33 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
// Use the nearest root's stack frame index for the stack trace highlight
|
|
|
|
|
const rootNode = heapData[path[0]];
|
|
|
|
|
updateStackTrace(rootNode?.stack_frame_index);
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
pathContent.innerHTML = path.map((addr, i) => {
|
|
|
|
|
const node = heapData[addr];
|
|
|
|
|
const isRoot = i === 0;
|
|
|
|
|
const isTarget = i === path.length - 1;
|
|
|
|
|
return `
|
|
|
|
|
<div class="path-node ${isRoot ? 'root' : ''} ${isTarget ? 'target' : ''}"
|
|
|
|
|
onclick="selectNode('${addr}')" style="cursor: pointer;">
|
|
|
|
|
<div class="path-node-info">
|
|
|
|
|
<div class="path-node-class">${escapeHtml(node.class_name)}</div>
|
|
|
|
|
<div class="path-node-addr">${formatAddress(addr)}</div>
|
2026-02-28 04:42:54 -03:00
|
|
|
${node.root ? `<div class="path-root-info">${escapeHtml(rootLabel(node))}</div>` : ''}
|
2026-01-12 15:32:33 -03:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
// Highlight path in graph
|
|
|
|
|
highlightPath(path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function bfsToRoot(startAddr) {
|
|
|
|
|
const visited = new Set();
|
|
|
|
|
const queue = [[startAddr]];
|
|
|
|
|
|
|
|
|
|
while (queue.length > 0) {
|
|
|
|
|
const path = queue.shift();
|
|
|
|
|
const current = path[path.length - 1];
|
|
|
|
|
|
|
|
|
|
if (visited.has(current)) continue;
|
|
|
|
|
visited.add(current);
|
|
|
|
|
|
|
|
|
|
// Check if we've reached a root
|
|
|
|
|
if (heapData[current]?.root) {
|
|
|
|
|
return path.reverse(); // Return path from root to target
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add all nodes that point to this one
|
|
|
|
|
for (const incoming of (reverseEdges[current] || [])) {
|
|
|
|
|
if (!visited.has(incoming)) {
|
|
|
|
|
queue.push([...path, incoming]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null; // No path found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function highlightPath(path) {
|
|
|
|
|
const pathSet = new Set(path);
|
|
|
|
|
const pathEdges = new Set();
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < path.length - 1; i++) {
|
|
|
|
|
pathEdges.add(`${path[i]}-${path[i + 1]}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g.selectAll('.node circle')
|
|
|
|
|
.attr('fill', d => {
|
|
|
|
|
if (pathSet.has(d.id)) {
|
|
|
|
|
if (heapData[d.id]?.root) return 'var(--root-color)';
|
|
|
|
|
if (d.id === selectedNode) return 'var(--accent-orange)';
|
|
|
|
|
return 'var(--accent-cyan)';
|
|
|
|
|
}
|
|
|
|
|
return d.isRoot ? 'var(--root-color)' : 'var(--node-default)';
|
|
|
|
|
})
|
|
|
|
|
.attr('stroke-width', d => pathSet.has(d.id) ? 4 : 2);
|
|
|
|
|
|
|
|
|
|
g.selectAll('.link')
|
|
|
|
|
.classed('path-highlight', d =>
|
|
|
|
|
pathEdges.has(`${d.source.id}-${d.target.id}`) ||
|
|
|
|
|
pathEdges.has(`${d.target.id}-${d.source.id}`)
|
|
|
|
|
)
|
|
|
|
|
.classed('highlighted', false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSection(section) {
|
|
|
|
|
const content = document.getElementById(section + '-content');
|
|
|
|
|
content.classList.toggle('collapsed');
|
2026-02-28 04:42:54 -03:00
|
|
|
|
|
|
|
|
// When expanding the stack trace, scroll the highlighted frame into view
|
|
|
|
|
if (section === 'stack-trace') {
|
|
|
|
|
const highlighted = content.querySelector('.highlighted');
|
|
|
|
|
if (highlighted) {
|
|
|
|
|
highlighted.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-12 15:32:33 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Graph controls
|
|
|
|
|
function zoomIn() {
|
|
|
|
|
svg.transition().call(zoom.scaleBy, 1.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function zoomOut() {
|
|
|
|
|
svg.transition().call(zoom.scaleBy, 0.67);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetZoom() {
|
|
|
|
|
const width = svg.node().parentElement.clientWidth;
|
|
|
|
|
const height = svg.node().parentElement.clientHeight;
|
|
|
|
|
svg.transition().call(zoom.transform, d3.zoomIdentity.translate(width/2, height/2).scale(1));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleSimulation() {
|
|
|
|
|
if (simulation) {
|
|
|
|
|
if (isPaused) {
|
|
|
|
|
simulation.alpha(0.3).restart();
|
|
|
|
|
document.getElementById('pause-btn').innerHTML = '⏸';
|
|
|
|
|
} else {
|
|
|
|
|
simulation.stop();
|
|
|
|
|
document.getElementById('pause-btn').innerHTML = '▶';
|
|
|
|
|
}
|
|
|
|
|
isPaused = !isPaused;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setGraphMode(mode) {
|
|
|
|
|
graphMode = mode;
|
|
|
|
|
document.getElementById('mode-outgoing').classList.toggle('active', mode === 'outgoing');
|
|
|
|
|
document.getElementById('mode-incoming').classList.toggle('active', mode === 'incoming');
|
|
|
|
|
|
|
|
|
|
// Re-visualize current class if one is selected
|
|
|
|
|
if (selectedClass) {
|
|
|
|
|
visualizeClass(selectedClass);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Utility functions
|
2026-02-28 06:10:22 -03:00
|
|
|
const wordBreakRegEx = /(\W)(\w)/g;
|
|
|
|
|
function insertWordBreaks(text) {
|
|
|
|
|
return text.replace(wordBreakRegEx, '$1\u200B$2');
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 04:42:54 -03:00
|
|
|
function rootLabel(node) {
|
2026-02-28 06:49:41 -03:00
|
|
|
const frame = stackFrames?.[node.stack_frame_index];
|
|
|
|
|
if (frame)
|
|
|
|
|
return `${node.root} ${frame.label}`;
|
2026-02-28 04:42:54 -03:00
|
|
|
return insertWordBreaks(node.root);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 06:49:41 -03:00
|
|
|
function formatSize(bytes) {
|
|
|
|
|
if (bytes >= 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + 'M';
|
|
|
|
|
if (bytes >= 1024) return (bytes / 1024).toFixed(1) + 'K';
|
|
|
|
|
return bytes + 'B';
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
function toHex(addr) {
|
|
|
|
|
return '0x' + BigInt(addr).toString(16).toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatAddress(addr) {
|
|
|
|
|
const hex = BigInt(addr).toString(16).toUpperCase();
|
|
|
|
|
return hex.length > 10 ? '...' + hex.slice(-8) : '0x' + hex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function escapeHtml(text) {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.textContent = text;
|
|
|
|
|
return div.innerHTML;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function searchByAddress() {
|
|
|
|
|
const input = document.getElementById('address-input').value.trim();
|
|
|
|
|
if (!input || !heapData) return;
|
|
|
|
|
|
|
|
|
|
let decimalAddr;
|
|
|
|
|
try {
|
|
|
|
|
// Handle hex (0x...) or decimal input
|
|
|
|
|
if (input.toLowerCase().startsWith('0x')) {
|
|
|
|
|
decimalAddr = BigInt(input).toString();
|
|
|
|
|
} else if (/^[0-9a-fA-F]+$/.test(input) && input.length > 10) {
|
|
|
|
|
// Looks like hex without prefix
|
|
|
|
|
decimalAddr = BigInt('0x' + input).toString();
|
|
|
|
|
} else {
|
|
|
|
|
decimalAddr = input;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
alert('Invalid address format');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (heapData[decimalAddr]) {
|
|
|
|
|
selectNode(decimalAddr);
|
|
|
|
|
// Also select the class to show it in the graph
|
|
|
|
|
const className = heapData[decimalAddr].class_name;
|
|
|
|
|
selectedClass = className;
|
2026-02-28 09:21:48 -03:00
|
|
|
renderClassList();
|
2026-01-12 15:32:33 -03:00
|
|
|
visualizeClass(className);
|
|
|
|
|
} else {
|
|
|
|
|
alert('Address not found in heap dump');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Allow Enter key to trigger search
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
|
document.getElementById('address-input')?.addEventListener('keypress', (e) => {
|
|
|
|
|
if (e.key === 'Enter') searchByAddress();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-01 16:52:16 -03:00
|
|
|
// Auto-load heap data via <script> tag to avoid CORS issues with file:// URLs.
|
|
|
|
|
// Usage: ?script=file:///path/to/gc-graph.js (the .js file sets var GC_GRAPH_DUMP = {...})
|
|
|
|
|
(function() {
|
|
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
const scriptUrl = params.get('script');
|
|
|
|
|
if (scriptUrl) {
|
|
|
|
|
loading.classList.remove('hidden');
|
|
|
|
|
const script = document.createElement('script');
|
|
|
|
|
script.src = scriptUrl;
|
|
|
|
|
script.onload = function() {
|
|
|
|
|
if (typeof GC_GRAPH_DUMP !== 'undefined') {
|
|
|
|
|
const name = scriptUrl.split('/').pop().replace('.js', '.json') || 'heap.json';
|
|
|
|
|
document.getElementById('file-name').textContent = name;
|
|
|
|
|
processData(GC_GRAPH_DUMP);
|
|
|
|
|
dropZone.classList.add('hidden');
|
|
|
|
|
mainApp.classList.add('visible');
|
|
|
|
|
} else {
|
|
|
|
|
alert('Script loaded but GC_GRAPH_DUMP was not defined');
|
|
|
|
|
}
|
|
|
|
|
loading.classList.add('hidden');
|
|
|
|
|
};
|
|
|
|
|
script.onerror = function() {
|
|
|
|
|
alert('Failed to load script: ' + scriptUrl);
|
|
|
|
|
loading.classList.add('hidden');
|
|
|
|
|
};
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
|
2026-01-12 15:32:33 -03:00
|
|
|
// Handle window resize
|
|
|
|
|
window.addEventListener('resize', () => {
|
|
|
|
|
if (selectedClass) {
|
|
|
|
|
visualizeClass(selectedClass);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|