haoline 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haoline/.streamlit/config.toml +10 -0
- haoline/__init__.py +248 -0
- haoline/analyzer.py +935 -0
- haoline/cli.py +2712 -0
- haoline/compare.py +811 -0
- haoline/compare_visualizations.py +1564 -0
- haoline/edge_analysis.py +525 -0
- haoline/eval/__init__.py +131 -0
- haoline/eval/adapters.py +844 -0
- haoline/eval/cli.py +390 -0
- haoline/eval/comparison.py +542 -0
- haoline/eval/deployment.py +633 -0
- haoline/eval/schemas.py +833 -0
- haoline/examples/__init__.py +15 -0
- haoline/examples/basic_inspection.py +74 -0
- haoline/examples/compare_models.py +117 -0
- haoline/examples/hardware_estimation.py +78 -0
- haoline/format_adapters.py +1001 -0
- haoline/formats/__init__.py +123 -0
- haoline/formats/coreml.py +250 -0
- haoline/formats/gguf.py +483 -0
- haoline/formats/openvino.py +255 -0
- haoline/formats/safetensors.py +273 -0
- haoline/formats/tflite.py +369 -0
- haoline/hardware.py +2307 -0
- haoline/hierarchical_graph.py +462 -0
- haoline/html_export.py +1573 -0
- haoline/layer_summary.py +769 -0
- haoline/llm_summarizer.py +465 -0
- haoline/op_icons.py +618 -0
- haoline/operational_profiling.py +1492 -0
- haoline/patterns.py +1116 -0
- haoline/pdf_generator.py +265 -0
- haoline/privacy.py +250 -0
- haoline/pydantic_models.py +241 -0
- haoline/report.py +1923 -0
- haoline/report_sections.py +539 -0
- haoline/risks.py +521 -0
- haoline/schema.py +523 -0
- haoline/streamlit_app.py +2024 -0
- haoline/tests/__init__.py +4 -0
- haoline/tests/conftest.py +123 -0
- haoline/tests/test_analyzer.py +868 -0
- haoline/tests/test_compare_visualizations.py +293 -0
- haoline/tests/test_edge_analysis.py +243 -0
- haoline/tests/test_eval.py +604 -0
- haoline/tests/test_format_adapters.py +460 -0
- haoline/tests/test_hardware.py +237 -0
- haoline/tests/test_hardware_recommender.py +90 -0
- haoline/tests/test_hierarchical_graph.py +326 -0
- haoline/tests/test_html_export.py +180 -0
- haoline/tests/test_layer_summary.py +428 -0
- haoline/tests/test_llm_patterns.py +540 -0
- haoline/tests/test_llm_summarizer.py +339 -0
- haoline/tests/test_patterns.py +774 -0
- haoline/tests/test_pytorch.py +327 -0
- haoline/tests/test_report.py +383 -0
- haoline/tests/test_risks.py +398 -0
- haoline/tests/test_schema.py +417 -0
- haoline/tests/test_tensorflow.py +380 -0
- haoline/tests/test_visualizations.py +316 -0
- haoline/universal_ir.py +856 -0
- haoline/visualizations.py +1086 -0
- haoline/visualize_yolo.py +44 -0
- haoline/web.py +110 -0
- haoline-0.3.0.dist-info/METADATA +471 -0
- haoline-0.3.0.dist-info/RECORD +70 -0
- haoline-0.3.0.dist-info/WHEEL +4 -0
- haoline-0.3.0.dist-info/entry_points.txt +5 -0
- haoline-0.3.0.dist-info/licenses/LICENSE +22 -0
haoline/html_export.py
ADDED
|
@@ -0,0 +1,1573 @@
|
|
|
1
|
+
# Copyright (c) 2025 HaoLine Contributors
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Interactive HTML Export for graph visualization.
|
|
6
|
+
|
|
7
|
+
Task 5.8: Creates standalone HTML files with embedded visualization
|
|
8
|
+
that can be opened in any browser without a server.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .edge_analysis import EdgeAnalysisResult
|
|
20
|
+
from .hierarchical_graph import HierarchicalGraph
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# HTML template with embedded D3.js visualization - Jony Ive Edition
|
|
24
|
+
HTML_TEMPLATE = """<!DOCTYPE html>
|
|
25
|
+
<html lang="en">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="UTF-8">
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
29
|
+
<title>{title} - Neural Architecture</title>
|
|
30
|
+
<link href="https://fonts.googleapis.com/css2?family=SF+Pro+Display:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
31
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
32
|
+
<style>
|
|
33
|
+
:root {{
|
|
34
|
+
--bg-deep: #000000;
|
|
35
|
+
--bg-primary: #0a0a0a;
|
|
36
|
+
--bg-elevated: #1a1a1a;
|
|
37
|
+
--bg-glass: rgba(255, 255, 255, 0.03);
|
|
38
|
+
--text-primary: rgba(255, 255, 255, 0.92);
|
|
39
|
+
--text-secondary: rgba(255, 255, 255, 0.55);
|
|
40
|
+
--text-tertiary: rgba(255, 255, 255, 0.35);
|
|
41
|
+
--accent: #0A84FF;
|
|
42
|
+
--accent-glow: rgba(10, 132, 255, 0.3);
|
|
43
|
+
--border: rgba(255, 255, 255, 0.08);
|
|
44
|
+
--success: #30D158;
|
|
45
|
+
--warning: #FFD60A;
|
|
46
|
+
--error: #FF453A;
|
|
47
|
+
--purple: #BF5AF2;
|
|
48
|
+
--orange: #FF9F0A;
|
|
49
|
+
--teal: #64D2FF;
|
|
50
|
+
}}
|
|
51
|
+
|
|
52
|
+
* {{
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
margin: 0;
|
|
55
|
+
padding: 0;
|
|
56
|
+
}}
|
|
57
|
+
|
|
58
|
+
body {{
|
|
59
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;
|
|
60
|
+
background: var(--bg-deep);
|
|
61
|
+
color: var(--text-primary);
|
|
62
|
+
overflow: hidden;
|
|
63
|
+
-webkit-font-smoothing: antialiased;
|
|
64
|
+
}}
|
|
65
|
+
|
|
66
|
+
.container {{
|
|
67
|
+
display: flex;
|
|
68
|
+
height: 100vh;
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
.sidebar {{
|
|
72
|
+
width: 280px;
|
|
73
|
+
background: var(--bg-primary);
|
|
74
|
+
border-right: 1px solid var(--border);
|
|
75
|
+
padding: 32px 24px;
|
|
76
|
+
overflow-y: auto;
|
|
77
|
+
backdrop-filter: blur(20px);
|
|
78
|
+
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
79
|
+
position: relative;
|
|
80
|
+
}}
|
|
81
|
+
|
|
82
|
+
.sidebar.collapsed {{
|
|
83
|
+
width: 0;
|
|
84
|
+
padding: 0;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
border-right: none;
|
|
87
|
+
}}
|
|
88
|
+
|
|
89
|
+
.sidebar-toggle {{
|
|
90
|
+
position: fixed;
|
|
91
|
+
top: 12px;
|
|
92
|
+
left: 248px;
|
|
93
|
+
width: 28px;
|
|
94
|
+
height: 28px;
|
|
95
|
+
background: var(--bg-elevated);
|
|
96
|
+
border: 1px solid var(--border);
|
|
97
|
+
border-radius: 6px;
|
|
98
|
+
color: var(--text-secondary);
|
|
99
|
+
cursor: pointer;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
font-size: 14px;
|
|
104
|
+
z-index: 1000;
|
|
105
|
+
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
106
|
+
}}
|
|
107
|
+
|
|
108
|
+
.sidebar-toggle:hover {{
|
|
109
|
+
background: var(--accent);
|
|
110
|
+
color: white;
|
|
111
|
+
border-color: var(--accent);
|
|
112
|
+
}}
|
|
113
|
+
|
|
114
|
+
.sidebar.collapsed + .main .sidebar-toggle,
|
|
115
|
+
.sidebar.collapsed ~ .sidebar-toggle {{
|
|
116
|
+
left: 8px;
|
|
117
|
+
}}
|
|
118
|
+
|
|
119
|
+
.sidebar h1 {{
|
|
120
|
+
font-size: 1.25rem;
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
letter-spacing: -0.02em;
|
|
123
|
+
margin-bottom: 32px;
|
|
124
|
+
background: linear-gradient(135deg, #fff 0%, rgba(255,255,255,0.7) 100%);
|
|
125
|
+
-webkit-background-clip: text;
|
|
126
|
+
-webkit-text-fill-color: transparent;
|
|
127
|
+
}}
|
|
128
|
+
|
|
129
|
+
.sidebar h2 {{
|
|
130
|
+
font-size: 0.6875rem;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
color: var(--text-tertiary);
|
|
133
|
+
margin: 24px 0 12px;
|
|
134
|
+
text-transform: uppercase;
|
|
135
|
+
letter-spacing: 0.08em;
|
|
136
|
+
}}
|
|
137
|
+
|
|
138
|
+
.stats {{
|
|
139
|
+
display: grid;
|
|
140
|
+
grid-template-columns: 1fr 1fr;
|
|
141
|
+
gap: 8px;
|
|
142
|
+
}}
|
|
143
|
+
|
|
144
|
+
.stat-card {{
|
|
145
|
+
background: var(--bg-glass);
|
|
146
|
+
border: 1px solid var(--border);
|
|
147
|
+
border-radius: 12px;
|
|
148
|
+
padding: 16px;
|
|
149
|
+
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
150
|
+
}}
|
|
151
|
+
|
|
152
|
+
.stat-card:hover {{
|
|
153
|
+
background: rgba(255, 255, 255, 0.06);
|
|
154
|
+
transform: translateY(-1px);
|
|
155
|
+
}}
|
|
156
|
+
|
|
157
|
+
.stat-value {{
|
|
158
|
+
font-size: 1.5rem;
|
|
159
|
+
font-weight: 600;
|
|
160
|
+
letter-spacing: -0.03em;
|
|
161
|
+
background: linear-gradient(135deg, var(--accent) 0%, var(--teal) 100%);
|
|
162
|
+
-webkit-background-clip: text;
|
|
163
|
+
-webkit-text-fill-color: transparent;
|
|
164
|
+
}}
|
|
165
|
+
|
|
166
|
+
.stat-label {{
|
|
167
|
+
font-size: 0.6875rem;
|
|
168
|
+
color: var(--text-tertiary);
|
|
169
|
+
margin-top: 4px;
|
|
170
|
+
}}
|
|
171
|
+
|
|
172
|
+
.controls {{
|
|
173
|
+
display: flex;
|
|
174
|
+
flex-wrap: wrap;
|
|
175
|
+
gap: 6px;
|
|
176
|
+
}}
|
|
177
|
+
|
|
178
|
+
.btn {{
|
|
179
|
+
padding: 8px 14px;
|
|
180
|
+
background: var(--bg-glass);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 8px;
|
|
183
|
+
color: var(--text-secondary);
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
font-size: 0.75rem;
|
|
186
|
+
font-weight: 500;
|
|
187
|
+
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
188
|
+
}}
|
|
189
|
+
|
|
190
|
+
.btn:hover {{
|
|
191
|
+
background: var(--accent);
|
|
192
|
+
border-color: var(--accent);
|
|
193
|
+
color: white;
|
|
194
|
+
box-shadow: 0 4px 16px var(--accent-glow);
|
|
195
|
+
}}
|
|
196
|
+
|
|
197
|
+
.legend-item {{
|
|
198
|
+
display: flex;
|
|
199
|
+
align-items: center;
|
|
200
|
+
padding: 6px 8px;
|
|
201
|
+
margin: 2px 0;
|
|
202
|
+
font-size: 0.75rem;
|
|
203
|
+
color: var(--text-secondary);
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
border-radius: 6px;
|
|
206
|
+
transition: all 0.15s ease;
|
|
207
|
+
}}
|
|
208
|
+
|
|
209
|
+
.legend-item:hover {{
|
|
210
|
+
background: rgba(255,255,255,0.05);
|
|
211
|
+
}}
|
|
212
|
+
|
|
213
|
+
.legend-item.active {{
|
|
214
|
+
background: rgba(10, 132, 255, 0.2);
|
|
215
|
+
color: var(--text-primary);
|
|
216
|
+
}}
|
|
217
|
+
|
|
218
|
+
.legend-dot {{
|
|
219
|
+
width: 10px;
|
|
220
|
+
height: 10px;
|
|
221
|
+
border-radius: 50%;
|
|
222
|
+
margin-right: 12px;
|
|
223
|
+
flex-shrink: 0;
|
|
224
|
+
}}
|
|
225
|
+
|
|
226
|
+
.legend-symbol {{
|
|
227
|
+
font-size: 1rem;
|
|
228
|
+
width: 20px;
|
|
229
|
+
text-align: center;
|
|
230
|
+
margin-right: 10px;
|
|
231
|
+
}}
|
|
232
|
+
|
|
233
|
+
.main {{
|
|
234
|
+
flex: 1;
|
|
235
|
+
position: relative;
|
|
236
|
+
background: radial-gradient(ellipse at center, #0a0a0a 0%, #000 100%);
|
|
237
|
+
}}
|
|
238
|
+
|
|
239
|
+
/* Subtle grid pattern */
|
|
240
|
+
.main::before {{
|
|
241
|
+
content: '';
|
|
242
|
+
position: absolute;
|
|
243
|
+
inset: 0;
|
|
244
|
+
background-image:
|
|
245
|
+
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
|
246
|
+
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
|
247
|
+
background-size: 40px 40px;
|
|
248
|
+
pointer-events: none;
|
|
249
|
+
}}
|
|
250
|
+
|
|
251
|
+
svg {{
|
|
252
|
+
width: 100%;
|
|
253
|
+
height: 100%;
|
|
254
|
+
}}
|
|
255
|
+
|
|
256
|
+
.node {{
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
}}
|
|
259
|
+
|
|
260
|
+
.node:hover .node-circle {{
|
|
261
|
+
filter: brightness(1.2) drop-shadow(0 0 12px currentColor);
|
|
262
|
+
}}
|
|
263
|
+
|
|
264
|
+
.node-circle {{
|
|
265
|
+
stroke: rgba(255,255,255,0.1);
|
|
266
|
+
stroke-width: 1;
|
|
267
|
+
filter: drop-shadow(0 2px 8px rgba(0,0,0,0.4));
|
|
268
|
+
}}
|
|
269
|
+
|
|
270
|
+
.node-label {{
|
|
271
|
+
font-size: 9px;
|
|
272
|
+
font-weight: 500;
|
|
273
|
+
fill: white;
|
|
274
|
+
text-anchor: middle;
|
|
275
|
+
dominant-baseline: middle;
|
|
276
|
+
pointer-events: none;
|
|
277
|
+
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
|
|
278
|
+
opacity: 0;
|
|
279
|
+
transition: opacity 0.2s ease;
|
|
280
|
+
}}
|
|
281
|
+
|
|
282
|
+
/* Show labels on hover */
|
|
283
|
+
.node:hover .node-label {{
|
|
284
|
+
opacity: 1;
|
|
285
|
+
}}
|
|
286
|
+
|
|
287
|
+
/* Always show labels for large/important nodes */
|
|
288
|
+
.node.show-label .node-label {{
|
|
289
|
+
opacity: 1;
|
|
290
|
+
}}
|
|
291
|
+
|
|
292
|
+
.node-sublabel {{
|
|
293
|
+
font-size: 7px;
|
|
294
|
+
fill: rgba(255,255,255,0.5);
|
|
295
|
+
text-anchor: middle;
|
|
296
|
+
pointer-events: none;
|
|
297
|
+
opacity: 0;
|
|
298
|
+
transition: opacity 0.2s ease;
|
|
299
|
+
}}
|
|
300
|
+
|
|
301
|
+
.node:hover .node-sublabel {{
|
|
302
|
+
opacity: 1;
|
|
303
|
+
}}
|
|
304
|
+
|
|
305
|
+
.edge {{
|
|
306
|
+
fill: none;
|
|
307
|
+
stroke-linecap: round;
|
|
308
|
+
opacity: 0.6;
|
|
309
|
+
}}
|
|
310
|
+
|
|
311
|
+
.edge:hover {{
|
|
312
|
+
opacity: 1;
|
|
313
|
+
stroke-width: 3;
|
|
314
|
+
}}
|
|
315
|
+
|
|
316
|
+
.tooltip {{
|
|
317
|
+
position: fixed;
|
|
318
|
+
background: rgba(20, 20, 20, 0.95);
|
|
319
|
+
backdrop-filter: blur(20px);
|
|
320
|
+
border: 1px solid var(--border);
|
|
321
|
+
border-radius: 12px;
|
|
322
|
+
padding: 16px;
|
|
323
|
+
font-size: 0.8125rem;
|
|
324
|
+
pointer-events: none;
|
|
325
|
+
opacity: 0;
|
|
326
|
+
transform: translateY(4px);
|
|
327
|
+
transition: all 0.2s cubic-bezier(0.25, 0.1, 0.25, 1);
|
|
328
|
+
max-width: 280px;
|
|
329
|
+
z-index: 1000;
|
|
330
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.6);
|
|
331
|
+
}}
|
|
332
|
+
|
|
333
|
+
.tooltip.visible {{
|
|
334
|
+
opacity: 1;
|
|
335
|
+
transform: translateY(0);
|
|
336
|
+
}}
|
|
337
|
+
|
|
338
|
+
.tooltip-title {{
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
font-size: 0.9375rem;
|
|
341
|
+
margin-bottom: 6px;
|
|
342
|
+
color: var(--accent);
|
|
343
|
+
}}
|
|
344
|
+
|
|
345
|
+
.tooltip-desc {{
|
|
346
|
+
font-size: 0.75rem;
|
|
347
|
+
color: var(--text-secondary);
|
|
348
|
+
line-height: 1.4;
|
|
349
|
+
margin-bottom: 12px;
|
|
350
|
+
padding-bottom: 10px;
|
|
351
|
+
border-bottom: 1px solid var(--border);
|
|
352
|
+
}}
|
|
353
|
+
|
|
354
|
+
.tooltip-row {{
|
|
355
|
+
display: flex;
|
|
356
|
+
justify-content: space-between;
|
|
357
|
+
padding: 3px 0;
|
|
358
|
+
font-size: 0.75rem;
|
|
359
|
+
}}
|
|
360
|
+
|
|
361
|
+
.tooltip-label {{
|
|
362
|
+
color: var(--text-tertiary);
|
|
363
|
+
}}
|
|
364
|
+
|
|
365
|
+
.tooltip-value {{
|
|
366
|
+
color: var(--text-primary);
|
|
367
|
+
font-weight: 500;
|
|
368
|
+
font-family: 'SF Mono', 'Menlo', monospace;
|
|
369
|
+
}}
|
|
370
|
+
|
|
371
|
+
.block-indicator {{
|
|
372
|
+
position: absolute;
|
|
373
|
+
top: -6px;
|
|
374
|
+
right: -6px;
|
|
375
|
+
width: 14px;
|
|
376
|
+
height: 14px;
|
|
377
|
+
background: var(--accent);
|
|
378
|
+
border-radius: 50%;
|
|
379
|
+
font-size: 10px;
|
|
380
|
+
display: flex;
|
|
381
|
+
align-items: center;
|
|
382
|
+
justify-content: center;
|
|
383
|
+
color: white;
|
|
384
|
+
font-weight: 600;
|
|
385
|
+
box-shadow: 0 2px 8px var(--accent-glow);
|
|
386
|
+
}}
|
|
387
|
+
|
|
388
|
+
/* Zoom controls */
|
|
389
|
+
.zoom-controls {{
|
|
390
|
+
position: absolute;
|
|
391
|
+
bottom: 24px;
|
|
392
|
+
right: 24px;
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-direction: column;
|
|
395
|
+
gap: 4px;
|
|
396
|
+
}}
|
|
397
|
+
|
|
398
|
+
.zoom-btn {{
|
|
399
|
+
width: 36px;
|
|
400
|
+
height: 36px;
|
|
401
|
+
background: rgba(20, 20, 20, 0.9);
|
|
402
|
+
backdrop-filter: blur(10px);
|
|
403
|
+
border: 1px solid var(--border);
|
|
404
|
+
border-radius: 8px;
|
|
405
|
+
color: var(--text-secondary);
|
|
406
|
+
cursor: pointer;
|
|
407
|
+
font-size: 1.25rem;
|
|
408
|
+
display: flex;
|
|
409
|
+
align-items: center;
|
|
410
|
+
justify-content: center;
|
|
411
|
+
transition: all 0.2s;
|
|
412
|
+
}}
|
|
413
|
+
|
|
414
|
+
.zoom-btn:hover {{
|
|
415
|
+
background: var(--bg-elevated);
|
|
416
|
+
color: var(--text-primary);
|
|
417
|
+
}}
|
|
418
|
+
|
|
419
|
+
/* Search functionality - Task 5.7.6 */
|
|
420
|
+
.search-input {{
|
|
421
|
+
width: 100%;
|
|
422
|
+
padding: 10px 14px;
|
|
423
|
+
background: var(--bg-glass);
|
|
424
|
+
border: 1px solid var(--border);
|
|
425
|
+
border-radius: 8px;
|
|
426
|
+
color: var(--text-primary);
|
|
427
|
+
font-size: 0.875rem;
|
|
428
|
+
margin-bottom: 8px;
|
|
429
|
+
transition: all 0.2s;
|
|
430
|
+
}}
|
|
431
|
+
|
|
432
|
+
.search-input:focus {{
|
|
433
|
+
outline: none;
|
|
434
|
+
border-color: var(--accent);
|
|
435
|
+
box-shadow: 0 0 0 3px var(--accent-glow);
|
|
436
|
+
}}
|
|
437
|
+
|
|
438
|
+
.search-input::placeholder {{
|
|
439
|
+
color: var(--text-tertiary);
|
|
440
|
+
}}
|
|
441
|
+
|
|
442
|
+
.search-results {{
|
|
443
|
+
max-height: 200px;
|
|
444
|
+
overflow-y: auto;
|
|
445
|
+
margin-bottom: 16px;
|
|
446
|
+
}}
|
|
447
|
+
|
|
448
|
+
.search-result {{
|
|
449
|
+
padding: 8px 12px;
|
|
450
|
+
font-size: 0.75rem;
|
|
451
|
+
cursor: pointer;
|
|
452
|
+
border-radius: 6px;
|
|
453
|
+
display: flex;
|
|
454
|
+
align-items: center;
|
|
455
|
+
gap: 8px;
|
|
456
|
+
transition: background 0.15s;
|
|
457
|
+
}}
|
|
458
|
+
|
|
459
|
+
.search-result:hover {{
|
|
460
|
+
background: rgba(255,255,255,0.05);
|
|
461
|
+
}}
|
|
462
|
+
|
|
463
|
+
.search-result .result-name {{
|
|
464
|
+
flex: 1;
|
|
465
|
+
overflow: hidden;
|
|
466
|
+
text-overflow: ellipsis;
|
|
467
|
+
white-space: nowrap;
|
|
468
|
+
}}
|
|
469
|
+
|
|
470
|
+
.search-result .result-type {{
|
|
471
|
+
font-size: 0.65rem;
|
|
472
|
+
color: var(--text-tertiary);
|
|
473
|
+
padding: 2px 6px;
|
|
474
|
+
background: var(--bg-glass);
|
|
475
|
+
border-radius: 4px;
|
|
476
|
+
}}
|
|
477
|
+
|
|
478
|
+
.search-highlight {{
|
|
479
|
+
filter: drop-shadow(0 0 20px var(--accent)) brightness(1.5);
|
|
480
|
+
z-index: 100;
|
|
481
|
+
}}
|
|
482
|
+
|
|
483
|
+
/* Performance mode indicator */
|
|
484
|
+
.perf-mode {{
|
|
485
|
+
font-size: 0.65rem;
|
|
486
|
+
color: var(--warning);
|
|
487
|
+
padding: 4px 8px;
|
|
488
|
+
background: rgba(255, 214, 10, 0.1);
|
|
489
|
+
border-radius: 4px;
|
|
490
|
+
margin-bottom: 12px;
|
|
491
|
+
text-align: center;
|
|
492
|
+
}}
|
|
493
|
+
</style>
|
|
494
|
+
</head>
|
|
495
|
+
<body>
|
|
496
|
+
<div class="container">
|
|
497
|
+
<button class="sidebar-toggle" id="sidebarToggle" onclick="toggleSidebar()" title="Toggle sidebar (more graph space)">◀</button>
|
|
498
|
+
<aside class="sidebar" id="graphSidebar">
|
|
499
|
+
<h1>{title}</h1>
|
|
500
|
+
|
|
501
|
+
<h2>Overview</h2>
|
|
502
|
+
<div class="stats">
|
|
503
|
+
<div class="stat-card">
|
|
504
|
+
<div class="stat-value" id="node-count">0</div>
|
|
505
|
+
<div class="stat-label">Nodes</div>
|
|
506
|
+
</div>
|
|
507
|
+
<div class="stat-card">
|
|
508
|
+
<div class="stat-value" id="edge-count">0</div>
|
|
509
|
+
<div class="stat-label">Edges</div>
|
|
510
|
+
</div>
|
|
511
|
+
<div class="stat-card">
|
|
512
|
+
<div class="stat-value" id="peak-memory">0</div>
|
|
513
|
+
<div class="stat-label">Peak Activation</div>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="stat-card">
|
|
516
|
+
<div class="stat-value" id="model-size">-</div>
|
|
517
|
+
<div class="stat-label">Model Size</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<h2>Search</h2>
|
|
522
|
+
<input type="text" id="nodeSearch" class="search-input"
|
|
523
|
+
placeholder="Search nodes..." oninput="searchNodes(this.value)">
|
|
524
|
+
<div id="searchResults" class="search-results"></div>
|
|
525
|
+
|
|
526
|
+
<h2>Navigation</h2>
|
|
527
|
+
<div class="controls">
|
|
528
|
+
<button class="btn" onclick="expandAll()">Expand</button>
|
|
529
|
+
<button class="btn" onclick="collapseAll()">Collapse</button>
|
|
530
|
+
<button class="btn" onclick="fitToScreen()">Fit</button>
|
|
531
|
+
<button class="btn" onclick="resetZoom()">Reset</button>
|
|
532
|
+
</div>
|
|
533
|
+
|
|
534
|
+
<h2>Visualization</h2>
|
|
535
|
+
<div class="controls">
|
|
536
|
+
<button class="btn" id="labels-btn" onclick="toggleAllLabels()">Show All Labels</button>
|
|
537
|
+
<button class="btn" id="heatmap-btn" onclick="toggleHeatMap()" style="margin-top:6px;">FLOPs Heat Map</button>
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
<h2>Op Types <span style="font-size:0.6rem;color:var(--text-tertiary)">(click to filter)</span></h2>
|
|
541
|
+
<div class="legend" id="op-legend">
|
|
542
|
+
<div class="legend-item" data-category="conv" onclick="filterByCategory('conv')">
|
|
543
|
+
<div class="legend-symbol" style="color: #4A90D9;">▣</div>
|
|
544
|
+
<span>Convolution</span>
|
|
545
|
+
</div>
|
|
546
|
+
<div class="legend-item" data-category="linear" onclick="filterByCategory('linear')">
|
|
547
|
+
<div class="legend-symbol" style="color: #BF5AF2;">◆</div>
|
|
548
|
+
<span>Linear/MatMul</span>
|
|
549
|
+
</div>
|
|
550
|
+
<div class="legend-item" data-category="attention" onclick="filterByCategory('attention')">
|
|
551
|
+
<div class="legend-symbol" style="color: #FF9F0A;">◎</div>
|
|
552
|
+
<span>Attention</span>
|
|
553
|
+
</div>
|
|
554
|
+
<div class="legend-item" data-category="norm" onclick="filterByCategory('norm')">
|
|
555
|
+
<div class="legend-symbol" style="color: #64D2FF;">≡</div>
|
|
556
|
+
<span>Normalization</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="legend-item" data-category="activation" onclick="filterByCategory('activation')">
|
|
559
|
+
<div class="legend-symbol" style="color: #FFD60A;">⚡</div>
|
|
560
|
+
<span>Activation</span>
|
|
561
|
+
</div>
|
|
562
|
+
<div class="legend-item" data-category="pool" onclick="filterByCategory('pool')">
|
|
563
|
+
<div class="legend-symbol" style="color: #30D158;">▼</div>
|
|
564
|
+
<span>Pooling</span>
|
|
565
|
+
</div>
|
|
566
|
+
<div class="legend-item" data-category="reshape" onclick="filterByCategory('reshape')">
|
|
567
|
+
<div class="legend-symbol" style="color: #5E5CE6;">⤨</div>
|
|
568
|
+
<span>Reshape</span>
|
|
569
|
+
</div>
|
|
570
|
+
<div class="legend-item" data-category="elementwise" onclick="filterByCategory('elementwise')">
|
|
571
|
+
<div class="legend-symbol" style="color: #FF6482;">+</div>
|
|
572
|
+
<span>Math ops</span>
|
|
573
|
+
</div>
|
|
574
|
+
<div class="legend-item" data-category="default" onclick="filterByCategory('default')">
|
|
575
|
+
<div class="legend-symbol" style="color: #636366;">●</div>
|
|
576
|
+
<span>Other</span>
|
|
577
|
+
</div>
|
|
578
|
+
<div class="legend-item" data-category="all" onclick="filterByCategory('all')" style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px;">
|
|
579
|
+
<span style="color:var(--accent)">↺ Show all</span>
|
|
580
|
+
</div>
|
|
581
|
+
</div>
|
|
582
|
+
|
|
583
|
+
<h2>Heat Map Scale</h2>
|
|
584
|
+
<div class="legend" id="heatmap-legend" style="display: none;">
|
|
585
|
+
<div class="legend-item">
|
|
586
|
+
<div class="legend-dot" style="background: #0A84FF; box-shadow: 0 0 8px #0A84FF;"></div>
|
|
587
|
+
<span>Low FLOPs</span>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="legend-item">
|
|
590
|
+
<div class="legend-dot" style="background: #64D2FF; box-shadow: 0 0 8px #64D2FF;"></div>
|
|
591
|
+
<span>Light</span>
|
|
592
|
+
</div>
|
|
593
|
+
<div class="legend-item">
|
|
594
|
+
<div class="legend-dot" style="background: #30D158; box-shadow: 0 0 8px #30D158;"></div>
|
|
595
|
+
<span>Medium</span>
|
|
596
|
+
</div>
|
|
597
|
+
<div class="legend-item">
|
|
598
|
+
<div class="legend-dot" style="background: #FFD60A; box-shadow: 0 0 8px #FFD60A;"></div>
|
|
599
|
+
<span>Heavy</span>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="legend-item">
|
|
602
|
+
<div class="legend-dot" style="background: #FF9F0A; box-shadow: 0 0 8px #FF9F0A;"></div>
|
|
603
|
+
<span>Very heavy</span>
|
|
604
|
+
</div>
|
|
605
|
+
<div class="legend-item">
|
|
606
|
+
<div class="legend-dot" style="background: #FF453A; box-shadow: 0 0 8px #FF453A;"></div>
|
|
607
|
+
<span>Compute hotspot</span>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="legend" id="optype-legend-note">
|
|
611
|
+
<span style="font-size: 0.7rem; color: var(--text-tertiary);">Toggle heat maps to visualize compute or timing</span>
|
|
612
|
+
</div>
|
|
613
|
+
</aside>
|
|
614
|
+
|
|
615
|
+
<main class="main">
|
|
616
|
+
<svg id="graph"></svg>
|
|
617
|
+
<div class="tooltip" id="tooltip"></div>
|
|
618
|
+
<div class="zoom-controls">
|
|
619
|
+
<button class="zoom-btn" onclick="zoomIn()">+</button>
|
|
620
|
+
<button class="zoom-btn" onclick="zoomOut()">-</button>
|
|
621
|
+
</div>
|
|
622
|
+
</main>
|
|
623
|
+
</div>
|
|
624
|
+
|
|
625
|
+
<script>
|
|
626
|
+
// Embedded graph data
|
|
627
|
+
const graphData = {graph_json};
|
|
628
|
+
const edgeData = {edge_json};
|
|
629
|
+
|
|
630
|
+
// Get label for a node
|
|
631
|
+
function getNodeLabel(node) {{
|
|
632
|
+
if (node.node_type === 'op' && node.op_type) {{
|
|
633
|
+
return node.op_type;
|
|
634
|
+
}}
|
|
635
|
+
if (node.display_name) {{
|
|
636
|
+
return node.display_name;
|
|
637
|
+
}}
|
|
638
|
+
return node.name;
|
|
639
|
+
}}
|
|
640
|
+
|
|
641
|
+
// Apple-inspired color palette with symbols
|
|
642
|
+
const opStyles = {{
|
|
643
|
+
conv: {{ color: '#4A90D9', symbol: '▣', name: 'Convolution' }},
|
|
644
|
+
linear: {{ color: '#BF5AF2', symbol: '◆', name: 'Linear' }},
|
|
645
|
+
attention: {{ color: '#FF9F0A', symbol: '◎', name: 'Attention' }},
|
|
646
|
+
norm: {{ color: '#64D2FF', symbol: '≡', name: 'Normalize' }},
|
|
647
|
+
activation: {{ color: '#FFD60A', symbol: '⚡', name: 'Activation' }},
|
|
648
|
+
pool: {{ color: '#30D158', symbol: '▼', name: 'Pooling' }},
|
|
649
|
+
embed: {{ color: '#AF52DE', symbol: '⊞', name: 'Embed' }},
|
|
650
|
+
reshape: {{ color: '#5E5CE6', symbol: '⤨', name: 'Reshape' }},
|
|
651
|
+
elementwise: {{ color: '#FF6482', symbol: '+', name: 'Math' }},
|
|
652
|
+
reduce: {{ color: '#FF453A', symbol: 'Σ', name: 'Reduce' }},
|
|
653
|
+
default: {{ color: '#636366', symbol: '●', name: 'Other' }}
|
|
654
|
+
}};
|
|
655
|
+
|
|
656
|
+
// For backwards compat
|
|
657
|
+
const colors = Object.fromEntries(
|
|
658
|
+
Object.entries(opStyles).map(([k, v]) => [k, v.color])
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// Get op category key
|
|
662
|
+
function getOpCategory(node) {{
|
|
663
|
+
if (node.node_type === 'block') {{
|
|
664
|
+
const blockType = (node.attributes?.block_type || '').toLowerCase();
|
|
665
|
+
if (blockType.includes('attention')) return 'attention';
|
|
666
|
+
if (blockType.includes('mlp') || blockType.includes('ffn')) return 'linear';
|
|
667
|
+
if (blockType.includes('conv')) return 'conv';
|
|
668
|
+
if (blockType.includes('norm')) return 'norm';
|
|
669
|
+
if (blockType.includes('embed')) return 'embed';
|
|
670
|
+
return 'default';
|
|
671
|
+
}}
|
|
672
|
+
|
|
673
|
+
const op = (node.op_type || '').toLowerCase();
|
|
674
|
+
if (op.includes('conv')) return 'conv';
|
|
675
|
+
if (op.includes('matmul') || op.includes('gemm')) return 'linear';
|
|
676
|
+
if (op.includes('norm') || op.includes('layer')) return 'norm';
|
|
677
|
+
if (op.includes('relu') || op.includes('gelu') || op.includes('softmax') || op.includes('sigmoid') || op.includes('silu') || op.includes('tanh')) return 'activation';
|
|
678
|
+
if (op.includes('pool')) return 'pool';
|
|
679
|
+
if (op.includes('reshape') || op.includes('transpose') || op.includes('flatten') || op.includes('squeeze') || op.includes('unsqueeze')) return 'reshape';
|
|
680
|
+
if (op.includes('add') || op.includes('mul') || op.includes('sub') || op.includes('div') || op.includes('concat') || op.includes('split')) return 'elementwise';
|
|
681
|
+
if (op.includes('reduce')) return 'reduce';
|
|
682
|
+
if (op.includes('gather') || op.includes('embed')) return 'embed';
|
|
683
|
+
return 'default';
|
|
684
|
+
}}
|
|
685
|
+
|
|
686
|
+
// Get color for op type
|
|
687
|
+
function getNodeColor(node) {{
|
|
688
|
+
return opStyles[getOpCategory(node)].color;
|
|
689
|
+
}}
|
|
690
|
+
|
|
691
|
+
// Get symbol for op type
|
|
692
|
+
function getNodeSymbol(node) {{
|
|
693
|
+
return opStyles[getOpCategory(node)].symbol;
|
|
694
|
+
}}
|
|
695
|
+
|
|
696
|
+
// Get node size based on type and compute
|
|
697
|
+
// Track max FLOPs for scaling (computed once during render)
|
|
698
|
+
let globalMaxFlops = 1;
|
|
699
|
+
|
|
700
|
+
function getNodeSize(node) {{
|
|
701
|
+
// Scale by FLOPs - expensive ops are visually bigger
|
|
702
|
+
const flops = node.total_flops || 0;
|
|
703
|
+
|
|
704
|
+
if (flops > 0 && globalMaxFlops > 1) {{
|
|
705
|
+
// Log scale: 12px min, 45px max based on FLOPs
|
|
706
|
+
const logFlops = Math.log10(flops + 1);
|
|
707
|
+
const logMax = Math.log10(globalMaxFlops + 1);
|
|
708
|
+
const ratio = logFlops / logMax;
|
|
709
|
+
return 12 + ratio * 33;
|
|
710
|
+
}}
|
|
711
|
+
|
|
712
|
+
// Fallback for nodes without FLOPs data - use hierarchy
|
|
713
|
+
const base = node.node_type === 'model' ? 40 :
|
|
714
|
+
node.node_type === 'layer' ? 30 :
|
|
715
|
+
node.node_type === 'block' ? 25 : 15;
|
|
716
|
+
return base;
|
|
717
|
+
}}
|
|
718
|
+
|
|
719
|
+
// Heat map mode toggle
|
|
720
|
+
let heatMapMode = false;
|
|
721
|
+
|
|
722
|
+
// Get heat map color based on compute intensity
|
|
723
|
+
function getHeatColor(flops, maxFlops) {{
|
|
724
|
+
if (!flops || flops === 0) return null;
|
|
725
|
+
const intensity = Math.log10(flops + 1) / Math.log10(maxFlops + 1);
|
|
726
|
+
// Blue -> Cyan -> Green -> Yellow -> Orange -> Red
|
|
727
|
+
if (intensity < 0.2) return '#0A84FF';
|
|
728
|
+
if (intensity < 0.4) return '#64D2FF';
|
|
729
|
+
if (intensity < 0.6) return '#30D158';
|
|
730
|
+
if (intensity < 0.8) return '#FFD60A';
|
|
731
|
+
if (intensity < 0.9) return '#FF9F0A';
|
|
732
|
+
return '#FF453A';
|
|
733
|
+
}}
|
|
734
|
+
|
|
735
|
+
// Initialize visualization
|
|
736
|
+
const svg = d3.select('#graph');
|
|
737
|
+
const container = svg.append('g');
|
|
738
|
+
|
|
739
|
+
// Zoom behavior
|
|
740
|
+
const zoom = d3.zoom()
|
|
741
|
+
.scaleExtent([0.1, 4])
|
|
742
|
+
.on('zoom', (event) => {{
|
|
743
|
+
container.attr('transform', event.transform);
|
|
744
|
+
}});
|
|
745
|
+
|
|
746
|
+
svg.call(zoom);
|
|
747
|
+
|
|
748
|
+
// Tooltip
|
|
749
|
+
const tooltip = d3.select('#tooltip');
|
|
750
|
+
|
|
751
|
+
// Op type explanations for demystifying the model
|
|
752
|
+
const opDescriptions = {{
|
|
753
|
+
'Conv': 'Convolution: Slides filters across input to detect features like edges, textures, shapes',
|
|
754
|
+
'MatMul': 'Matrix Multiply: Core linear transformation - learns weighted combinations of inputs',
|
|
755
|
+
'Gemm': 'General Matrix Multiply: Linear layer with weights and bias',
|
|
756
|
+
'Relu': 'ReLU Activation: Keeps positive values, zeros negatives - adds non-linearity',
|
|
757
|
+
'Gelu': 'GELU Activation: Smooth activation used in transformers - better gradients',
|
|
758
|
+
'Softmax': 'Softmax: Converts scores to probabilities (sums to 1) - used in attention',
|
|
759
|
+
'Sigmoid': 'Sigmoid: Squashes values to 0-1 range - used for gates/probabilities',
|
|
760
|
+
'LayerNormalization': 'Layer Norm: Normalizes activations for stable training',
|
|
761
|
+
'BatchNormalization': 'Batch Norm: Normalizes across batch - speeds up training',
|
|
762
|
+
'Add': 'Addition: Element-wise sum - often a residual/skip connection',
|
|
763
|
+
'Mul': 'Multiply: Element-wise product - used in attention and gating',
|
|
764
|
+
'Div': 'Division: Often scaling (e.g., by sqrt(d) in attention)',
|
|
765
|
+
'Transpose': 'Transpose: Rearranges tensor dimensions',
|
|
766
|
+
'Reshape': 'Reshape: Changes tensor shape without changing data',
|
|
767
|
+
'Gather': 'Gather: Lookup operation - retrieves embeddings by index',
|
|
768
|
+
'Concat': 'Concatenate: Joins tensors along an axis',
|
|
769
|
+
'Split': 'Split: Divides tensor into parts',
|
|
770
|
+
'MaxPool': 'Max Pooling: Downsamples by taking maximum in each window',
|
|
771
|
+
'GlobalAveragePool': 'Global Avg Pool: Reduces spatial dims to single values',
|
|
772
|
+
'Flatten': 'Flatten: Collapses dimensions into 1D for dense layers',
|
|
773
|
+
'Dropout': 'Dropout: Randomly zeros values during training for regularization',
|
|
774
|
+
'Attention': 'Self-Attention: Computes relationships between all positions',
|
|
775
|
+
'AttentionHead': 'Attention Head: Q/K/V projections + scaled dot-product attention',
|
|
776
|
+
'MLPBlock': 'MLP/FFN: Feed-forward network - expands then contracts dimensions',
|
|
777
|
+
}};
|
|
778
|
+
|
|
779
|
+
function getOpDescription(node) {{
|
|
780
|
+
const opType = node.op_type || node.attributes?.block_type || '';
|
|
781
|
+
return opDescriptions[opType] || `${{opType}}: Neural network operation`;
|
|
782
|
+
}}
|
|
783
|
+
|
|
784
|
+
function showTooltip(event, node) {{
|
|
785
|
+
const opType = node.op_type || node.node_type;
|
|
786
|
+
const description = getOpDescription(node);
|
|
787
|
+
|
|
788
|
+
// Build detailed info
|
|
789
|
+
let details = '';
|
|
790
|
+
|
|
791
|
+
// Input/output info
|
|
792
|
+
if (node.inputs && node.inputs.length > 0) {{
|
|
793
|
+
details += `<div class="tooltip-row"><span class="tooltip-label">Inputs:</span><span class="tooltip-value">${{node.inputs.length}} tensor(s)</span></div>`;
|
|
794
|
+
}}
|
|
795
|
+
if (node.outputs && node.outputs.length > 0) {{
|
|
796
|
+
details += `<div class="tooltip-row"><span class="tooltip-label">Outputs:</span><span class="tooltip-value">${{node.outputs.length}} tensor(s)</span></div>`;
|
|
797
|
+
}}
|
|
798
|
+
|
|
799
|
+
// Child count for blocks
|
|
800
|
+
if (node.children && node.children.length > 0) {{
|
|
801
|
+
details += `<div class="tooltip-row"><span class="tooltip-label">Contains:</span><span class="tooltip-value">${{node.node_count || node.children.length}} ops</span></div>`;
|
|
802
|
+
}}
|
|
803
|
+
|
|
804
|
+
// Compute info
|
|
805
|
+
if (node.total_flops > 0) {{
|
|
806
|
+
const intensity = node.total_flops > 1e9 ? 'high' : node.total_flops > 1e6 ? 'medium' : 'low';
|
|
807
|
+
details += `<div class="tooltip-row"><span class="tooltip-label">Compute:</span><span class="tooltip-value">${{formatNumber(node.total_flops)}} FLOPs (${{intensity}})</span></div>`;
|
|
808
|
+
}}
|
|
809
|
+
|
|
810
|
+
// Memory info
|
|
811
|
+
if (node.total_memory_bytes > 0) {{
|
|
812
|
+
details += `<div class="tooltip-row"><span class="tooltip-label">Memory:</span><span class="tooltip-value">${{formatBytes(node.total_memory_bytes)}}</span></div>`;
|
|
813
|
+
}}
|
|
814
|
+
|
|
815
|
+
const html = `
|
|
816
|
+
<div class="tooltip-title">${{opType}}</div>
|
|
817
|
+
<div class="tooltip-desc">${{description}}</div>
|
|
818
|
+
${{details}}
|
|
819
|
+
`;
|
|
820
|
+
|
|
821
|
+
tooltip.html(html);
|
|
822
|
+
|
|
823
|
+
// Smart positioning using fixed coordinates (relative to viewport)
|
|
824
|
+
const tooltipNode = tooltip.node();
|
|
825
|
+
const tooltipWidth = tooltipNode.offsetWidth || 280;
|
|
826
|
+
const tooltipHeight = tooltipNode.offsetHeight || 150;
|
|
827
|
+
const viewportWidth = window.innerWidth;
|
|
828
|
+
const viewportHeight = window.innerHeight;
|
|
829
|
+
|
|
830
|
+
// Use clientX/clientY for fixed positioning (viewport coords)
|
|
831
|
+
let left = event.clientX + 15;
|
|
832
|
+
let top = event.clientY + 15;
|
|
833
|
+
|
|
834
|
+
// Check right edge - flip to left side if needed
|
|
835
|
+
if (left + tooltipWidth > viewportWidth - 20) {{
|
|
836
|
+
left = event.clientX - tooltipWidth - 15;
|
|
837
|
+
}}
|
|
838
|
+
|
|
839
|
+
// Check bottom edge - flip to top if needed
|
|
840
|
+
if (top + tooltipHeight > viewportHeight - 20) {{
|
|
841
|
+
top = event.clientY - tooltipHeight - 15;
|
|
842
|
+
}}
|
|
843
|
+
|
|
844
|
+
// Ensure not off left or top edge
|
|
845
|
+
left = Math.max(10, left);
|
|
846
|
+
top = Math.max(10, top);
|
|
847
|
+
|
|
848
|
+
tooltip
|
|
849
|
+
.style('left', left + 'px')
|
|
850
|
+
.style('top', top + 'px')
|
|
851
|
+
.classed('visible', true);
|
|
852
|
+
}}
|
|
853
|
+
|
|
854
|
+
function hideTooltip() {{
|
|
855
|
+
tooltip.classed('visible', false);
|
|
856
|
+
}}
|
|
857
|
+
|
|
858
|
+
// Improved grid layout with depth calculation attempt
|
|
859
|
+
// Falls back to clean grid if depth calc fails
|
|
860
|
+
function layoutNodes(nodes) {{
|
|
861
|
+
// Check if sidebar is collapsed for width calculation
|
|
862
|
+
const sidebar = document.getElementById('graphSidebar');
|
|
863
|
+
const sidebarWidth = sidebar && sidebar.classList.contains('collapsed') ? 0 : 280;
|
|
864
|
+
const width = window.innerWidth - sidebarWidth - 40;
|
|
865
|
+
const height = window.innerHeight;
|
|
866
|
+
const padding = 40;
|
|
867
|
+
|
|
868
|
+
// Try to calculate depths based on inputs/outputs
|
|
869
|
+
const outputToNode = {{}};
|
|
870
|
+
const depths = {{}};
|
|
871
|
+
|
|
872
|
+
nodes.forEach(node => {{
|
|
873
|
+
if (node.outputs) {{
|
|
874
|
+
node.outputs.forEach(out => {{ outputToNode[out] = node; }});
|
|
875
|
+
}}
|
|
876
|
+
}});
|
|
877
|
+
|
|
878
|
+
// Simple depth: count parent chain length
|
|
879
|
+
function getDepth(node, visited) {{
|
|
880
|
+
if (!node) return 0;
|
|
881
|
+
if (depths[node.id] !== undefined) return depths[node.id];
|
|
882
|
+
if (visited[node.id]) return 0;
|
|
883
|
+
visited[node.id] = true;
|
|
884
|
+
|
|
885
|
+
let maxParent = -1;
|
|
886
|
+
if (node.inputs && node.inputs.length > 0) {{
|
|
887
|
+
for (const inp of node.inputs) {{
|
|
888
|
+
const parent = outputToNode[inp];
|
|
889
|
+
if (parent) {{
|
|
890
|
+
maxParent = Math.max(maxParent, getDepth(parent, visited));
|
|
891
|
+
}}
|
|
892
|
+
}}
|
|
893
|
+
}}
|
|
894
|
+
depths[node.id] = maxParent + 1;
|
|
895
|
+
return depths[node.id];
|
|
896
|
+
}}
|
|
897
|
+
|
|
898
|
+
// Calculate all depths
|
|
899
|
+
nodes.forEach(n => getDepth(n, {{}}));
|
|
900
|
+
|
|
901
|
+
// Group by depth
|
|
902
|
+
const byDepth = {{}};
|
|
903
|
+
let maxDepth = 0;
|
|
904
|
+
nodes.forEach(node => {{
|
|
905
|
+
const d = depths[node.id] || 0;
|
|
906
|
+
maxDepth = Math.max(maxDepth, d);
|
|
907
|
+
if (!byDepth[d]) byDepth[d] = [];
|
|
908
|
+
byDepth[d].push(node);
|
|
909
|
+
}});
|
|
910
|
+
|
|
911
|
+
// If we got meaningful depths, use horizontal flow layout
|
|
912
|
+
if (maxDepth > 0) {{
|
|
913
|
+
// Prefer horizontal spread - use full width
|
|
914
|
+
const colWidth = (width - padding * 2) / (maxDepth + 1);
|
|
915
|
+
const availableHeight = height - padding * 2;
|
|
916
|
+
|
|
917
|
+
for (let d = 0; d <= maxDepth; d++) {{
|
|
918
|
+
const nodesAtDepth = byDepth[d] || [];
|
|
919
|
+
const x = padding + d * colWidth + colWidth / 2;
|
|
920
|
+
|
|
921
|
+
// Distribute vertically within available height
|
|
922
|
+
const rowH = availableHeight / Math.max(nodesAtDepth.length, 1);
|
|
923
|
+
|
|
924
|
+
nodesAtDepth.forEach((node, i) => {{
|
|
925
|
+
node.x = x;
|
|
926
|
+
node.y = padding + i * rowH + rowH / 2;
|
|
927
|
+
node.r = getNodeSize(node);
|
|
928
|
+
}});
|
|
929
|
+
}}
|
|
930
|
+
}} else {{
|
|
931
|
+
// Fallback: horizontal-biased grid (more columns than rows)
|
|
932
|
+
const cols = Math.ceil(Math.sqrt(nodes.length * 2.5));
|
|
933
|
+
const rows = Math.ceil(nodes.length / cols);
|
|
934
|
+
const cellW = (width - padding * 2) / cols;
|
|
935
|
+
const cellH = (height - padding * 2) / rows;
|
|
936
|
+
|
|
937
|
+
nodes.forEach((node, i) => {{
|
|
938
|
+
node.x = padding + (i % cols) * cellW + cellW / 2;
|
|
939
|
+
node.y = padding + Math.floor(i / cols) * cellH + cellH / 2;
|
|
940
|
+
node.r = getNodeSize(node);
|
|
941
|
+
}});
|
|
942
|
+
}}
|
|
943
|
+
|
|
944
|
+
return nodes;
|
|
945
|
+
}}
|
|
946
|
+
|
|
947
|
+
// Render graph with performance optimizations (Task 5.7.9)
|
|
948
|
+
function render() {{
|
|
949
|
+
container.selectAll('*').remove();
|
|
950
|
+
|
|
951
|
+
// Flatten visible nodes
|
|
952
|
+
let visibleNodes = [];
|
|
953
|
+
function collectVisible(node) {{
|
|
954
|
+
visibleNodes.push(node);
|
|
955
|
+
if (!node.is_collapsed && node.children) {{
|
|
956
|
+
node.children.forEach(collectVisible);
|
|
957
|
+
}}
|
|
958
|
+
}}
|
|
959
|
+
|
|
960
|
+
if (graphData.root) {{
|
|
961
|
+
collectVisible(graphData.root);
|
|
962
|
+
}}
|
|
963
|
+
|
|
964
|
+
// Performance: limit visible nodes in performance mode
|
|
965
|
+
if (performanceMode && visibleNodes.length > 1000) {{
|
|
966
|
+
console.log(`Limiting display to 1000 nodes (had ${{visibleNodes.length}})`);
|
|
967
|
+
// Prioritize by FLOPs/importance
|
|
968
|
+
visibleNodes = visibleNodes
|
|
969
|
+
.sort((a, b) => (b.total_flops || 0) - (a.total_flops || 0))
|
|
970
|
+
.slice(0, 1000);
|
|
971
|
+
}}
|
|
972
|
+
|
|
973
|
+
// Compute max FLOPs for size scaling (expensive ops = bigger nodes)
|
|
974
|
+
globalMaxFlops = Math.max(1, ...visibleNodes.map(n => n.total_flops || 0));
|
|
975
|
+
|
|
976
|
+
// Layout
|
|
977
|
+
layoutNodes(visibleNodes);
|
|
978
|
+
|
|
979
|
+
// Create node lookup for edges
|
|
980
|
+
const nodeById = {{}};
|
|
981
|
+
visibleNodes.forEach(n => {{
|
|
982
|
+
nodeById[n.id] = n;
|
|
983
|
+
nodeById[n.name] = n;
|
|
984
|
+
}});
|
|
985
|
+
|
|
986
|
+
// Draw edges first (so they're behind nodes)
|
|
987
|
+
const edges = [];
|
|
988
|
+
visibleNodes.forEach(node => {{
|
|
989
|
+
if (node.outputs) {{
|
|
990
|
+
node.outputs.forEach(output => {{
|
|
991
|
+
// Find nodes that consume this output
|
|
992
|
+
visibleNodes.forEach(target => {{
|
|
993
|
+
if (target.inputs && target.inputs.includes(output)) {{
|
|
994
|
+
edges.push({{
|
|
995
|
+
source: node,
|
|
996
|
+
target: target,
|
|
997
|
+
output: output
|
|
998
|
+
}});
|
|
999
|
+
}}
|
|
1000
|
+
}});
|
|
1001
|
+
}});
|
|
1002
|
+
}}
|
|
1003
|
+
}});
|
|
1004
|
+
|
|
1005
|
+
// Setup defs for markers and filters
|
|
1006
|
+
const defs = container.append('defs');
|
|
1007
|
+
|
|
1008
|
+
// Arrow marker for edges
|
|
1009
|
+
defs.append('marker')
|
|
1010
|
+
.attr('id', 'arrowhead')
|
|
1011
|
+
.attr('viewBox', '0 -5 10 10')
|
|
1012
|
+
.attr('refX', 8)
|
|
1013
|
+
.attr('refY', 0)
|
|
1014
|
+
.attr('markerWidth', 5)
|
|
1015
|
+
.attr('markerHeight', 5)
|
|
1016
|
+
.attr('orient', 'auto')
|
|
1017
|
+
.append('path')
|
|
1018
|
+
.attr('d', 'M0,-3L8,0L0,3')
|
|
1019
|
+
.attr('fill', 'rgba(255,255,255,0.5)');
|
|
1020
|
+
|
|
1021
|
+
// Glow filter for nodes
|
|
1022
|
+
const filter = defs.append('filter')
|
|
1023
|
+
.attr('id', 'glow');
|
|
1024
|
+
filter.append('feGaussianBlur')
|
|
1025
|
+
.attr('stdDeviation', '2')
|
|
1026
|
+
.attr('result', 'coloredBlur');
|
|
1027
|
+
|
|
1028
|
+
// Draw edges with arrows
|
|
1029
|
+
container.selectAll('.edge')
|
|
1030
|
+
.data(edges)
|
|
1031
|
+
.enter()
|
|
1032
|
+
.append('path')
|
|
1033
|
+
.attr('class', 'edge')
|
|
1034
|
+
.attr('d', d => {{
|
|
1035
|
+
const sr = d.source.r || 20;
|
|
1036
|
+
const tr = d.target.r || 20;
|
|
1037
|
+
const sx = d.source.x + sr;
|
|
1038
|
+
const sy = d.source.y;
|
|
1039
|
+
const tx = d.target.x - tr;
|
|
1040
|
+
const ty = d.target.y;
|
|
1041
|
+
|
|
1042
|
+
// Bezier curve
|
|
1043
|
+
const midX = (sx + tx) / 2;
|
|
1044
|
+
return `M${{sx}},${{sy}} C${{midX}},${{sy}} ${{midX}},${{ty}} ${{tx}},${{ty}}`;
|
|
1045
|
+
}})
|
|
1046
|
+
.attr('stroke', 'rgba(255,255,255,0.3)')
|
|
1047
|
+
.attr('stroke-width', 1.5)
|
|
1048
|
+
.attr('marker-end', 'url(#arrowhead)');
|
|
1049
|
+
|
|
1050
|
+
// Draw nodes as circles
|
|
1051
|
+
const nodeGroups = container.selectAll('.node')
|
|
1052
|
+
.data(visibleNodes)
|
|
1053
|
+
.enter()
|
|
1054
|
+
.append('g')
|
|
1055
|
+
.attr('class', d => {{
|
|
1056
|
+
// Show labels for important nodes: model, layers, blocks
|
|
1057
|
+
const isImportant = d.node_type === 'model' ||
|
|
1058
|
+
d.node_type === 'layer' ||
|
|
1059
|
+
d.node_type === 'block' ||
|
|
1060
|
+
d.r > 35;
|
|
1061
|
+
return 'node' + (isImportant ? ' show-label' : '');
|
|
1062
|
+
}})
|
|
1063
|
+
.attr('transform', d => `translate(${{d.x}}, ${{d.y}})`)
|
|
1064
|
+
.on('mouseover', showTooltip)
|
|
1065
|
+
.on('mouseout', hideTooltip)
|
|
1066
|
+
.on('click', (event, d) => {{
|
|
1067
|
+
if (d.children && d.children.length > 0) {{
|
|
1068
|
+
d.is_collapsed = !d.is_collapsed;
|
|
1069
|
+
render();
|
|
1070
|
+
}}
|
|
1071
|
+
}});
|
|
1072
|
+
|
|
1073
|
+
// Calculate max FLOPs for heat map
|
|
1074
|
+
const maxFlops = getMaxFlops(visibleNodes);
|
|
1075
|
+
|
|
1076
|
+
// Circle nodes - use heat map or category colors
|
|
1077
|
+
nodeGroups.append('circle')
|
|
1078
|
+
.attr('class', 'node-circle')
|
|
1079
|
+
.attr('r', d => d.r)
|
|
1080
|
+
.attr('fill', d => {{
|
|
1081
|
+
if (heatMapMode && d.total_flops > 0) {{
|
|
1082
|
+
return getHeatColor(d.total_flops, maxFlops);
|
|
1083
|
+
}}
|
|
1084
|
+
return getNodeColor(d);
|
|
1085
|
+
}})
|
|
1086
|
+
.attr('opacity', d => d.node_type === 'model' ? 0.9 : 0.85)
|
|
1087
|
+
.style('filter', 'url(#glow)');
|
|
1088
|
+
|
|
1089
|
+
// Inner highlight
|
|
1090
|
+
nodeGroups.append('circle')
|
|
1091
|
+
.attr('r', d => d.r - 2)
|
|
1092
|
+
.attr('fill', 'none')
|
|
1093
|
+
.attr('stroke', 'rgba(255,255,255,0.2)')
|
|
1094
|
+
.attr('stroke-width', 1);
|
|
1095
|
+
|
|
1096
|
+
// Symbol in center
|
|
1097
|
+
nodeGroups.append('text')
|
|
1098
|
+
.attr('class', 'node-symbol')
|
|
1099
|
+
.attr('y', d => d.r > 28 ? -4 : 0)
|
|
1100
|
+
.attr('font-size', d => d.r > 28 ? '14px' : '12px')
|
|
1101
|
+
.attr('fill', 'white')
|
|
1102
|
+
.attr('text-anchor', 'middle')
|
|
1103
|
+
.attr('dominant-baseline', 'middle')
|
|
1104
|
+
.text(d => getNodeSymbol(d));
|
|
1105
|
+
|
|
1106
|
+
// Labels below symbol for larger nodes
|
|
1107
|
+
nodeGroups.filter(d => d.r > 28)
|
|
1108
|
+
.append('text')
|
|
1109
|
+
.attr('class', 'node-label')
|
|
1110
|
+
.attr('y', 10)
|
|
1111
|
+
.text(d => truncate(getNodeLabel(d), 8));
|
|
1112
|
+
|
|
1113
|
+
// Just label for small nodes
|
|
1114
|
+
nodeGroups.filter(d => d.r <= 28)
|
|
1115
|
+
.append('text')
|
|
1116
|
+
.attr('class', 'node-label')
|
|
1117
|
+
.attr('y', d => d.r + 14)
|
|
1118
|
+
.text(d => truncate(getNodeLabel(d), 6));
|
|
1119
|
+
|
|
1120
|
+
// Expand indicator for blocks
|
|
1121
|
+
nodeGroups.filter(d => d.children && d.children.length > 0)
|
|
1122
|
+
.append('circle')
|
|
1123
|
+
.attr('cx', d => d.r * 0.7)
|
|
1124
|
+
.attr('cy', d => -d.r * 0.7)
|
|
1125
|
+
.attr('r', 8)
|
|
1126
|
+
.attr('fill', '#0A84FF')
|
|
1127
|
+
.attr('stroke', '#000')
|
|
1128
|
+
.attr('stroke-width', 1);
|
|
1129
|
+
|
|
1130
|
+
nodeGroups.filter(d => d.children && d.children.length > 0)
|
|
1131
|
+
.append('text')
|
|
1132
|
+
.attr('x', d => d.r * 0.7)
|
|
1133
|
+
.attr('y', d => -d.r * 0.7 + 1)
|
|
1134
|
+
.attr('text-anchor', 'middle')
|
|
1135
|
+
.attr('dominant-baseline', 'middle')
|
|
1136
|
+
.attr('fill', 'white')
|
|
1137
|
+
.attr('font-size', '10px')
|
|
1138
|
+
.attr('font-weight', '600')
|
|
1139
|
+
.text(d => d.is_collapsed ? '+' : '-');
|
|
1140
|
+
|
|
1141
|
+
// Update stats
|
|
1142
|
+
document.getElementById('node-count').textContent = visibleNodes.length;
|
|
1143
|
+
document.getElementById('edge-count').textContent = edges.length || edgeData?.num_edges || 0;
|
|
1144
|
+
document.getElementById('peak-memory').textContent = formatBytes(edgeData?.peak_activation_bytes || 0);
|
|
1145
|
+
document.getElementById('model-size').textContent = formatBytes(graphData.model_size_bytes || 0);
|
|
1146
|
+
}}
|
|
1147
|
+
|
|
1148
|
+
// Sidebar toggle for more graph space
|
|
1149
|
+
function toggleSidebar() {{
|
|
1150
|
+
const sidebar = document.getElementById('graphSidebar');
|
|
1151
|
+
const toggleBtn = document.getElementById('sidebarToggle');
|
|
1152
|
+
const isCollapsed = sidebar.classList.toggle('collapsed');
|
|
1153
|
+
toggleBtn.textContent = isCollapsed ? '▶' : '◀';
|
|
1154
|
+
toggleBtn.style.left = isCollapsed ? '8px' : '248px';
|
|
1155
|
+
// Re-fit graph after sidebar toggle
|
|
1156
|
+
setTimeout(fitToScreen, 350);
|
|
1157
|
+
}}
|
|
1158
|
+
|
|
1159
|
+
function zoomIn() {{
|
|
1160
|
+
svg.transition().duration(300).call(zoom.scaleBy, 1.3);
|
|
1161
|
+
}}
|
|
1162
|
+
|
|
1163
|
+
function zoomOut() {{
|
|
1164
|
+
svg.transition().duration(300).call(zoom.scaleBy, 0.7);
|
|
1165
|
+
}}
|
|
1166
|
+
|
|
1167
|
+
let activeFilter = 'all';
|
|
1168
|
+
let showAllLabels = false;
|
|
1169
|
+
|
|
1170
|
+
function toggleAllLabels() {{
|
|
1171
|
+
showAllLabels = !showAllLabels;
|
|
1172
|
+
const btn = document.getElementById('labels-btn');
|
|
1173
|
+
btn.style.background = showAllLabels ? 'var(--accent)' : '';
|
|
1174
|
+
btn.style.color = showAllLabels ? 'white' : '';
|
|
1175
|
+
btn.textContent = showAllLabels ? 'Hide Labels' : 'Show All Labels';
|
|
1176
|
+
|
|
1177
|
+
// Toggle show-label class on all nodes
|
|
1178
|
+
container.selectAll('.node').classed('show-label', showAllLabels);
|
|
1179
|
+
}}
|
|
1180
|
+
|
|
1181
|
+
function toggleHeatMap() {{
|
|
1182
|
+
heatMapMode = !heatMapMode;
|
|
1183
|
+
|
|
1184
|
+
const btn = document.getElementById('heatmap-btn');
|
|
1185
|
+
btn.style.background = heatMapMode ? 'var(--accent)' : '';
|
|
1186
|
+
btn.style.color = heatMapMode ? 'white' : '';
|
|
1187
|
+
|
|
1188
|
+
// Toggle legend visibility
|
|
1189
|
+
document.getElementById('heatmap-legend').style.display = heatMapMode ? 'block' : 'none';
|
|
1190
|
+
document.getElementById('optype-legend-note').style.display = heatMapMode ? 'none' : 'block';
|
|
1191
|
+
|
|
1192
|
+
render();
|
|
1193
|
+
}}
|
|
1194
|
+
|
|
1195
|
+
function filterByCategory(category) {{
|
|
1196
|
+
activeFilter = category;
|
|
1197
|
+
|
|
1198
|
+
// Update legend active state
|
|
1199
|
+
document.querySelectorAll('#op-legend .legend-item').forEach(item => {{
|
|
1200
|
+
item.classList.remove('active');
|
|
1201
|
+
if (item.dataset.category === category) {{
|
|
1202
|
+
item.classList.add('active');
|
|
1203
|
+
}}
|
|
1204
|
+
}});
|
|
1205
|
+
|
|
1206
|
+
// Update node visibility/opacity
|
|
1207
|
+
container.selectAll('.node').each(function(d) {{
|
|
1208
|
+
const nodeCategory = getOpCategory(d);
|
|
1209
|
+
const visible = category === 'all' || nodeCategory === category;
|
|
1210
|
+
d3.select(this)
|
|
1211
|
+
.transition()
|
|
1212
|
+
.duration(200)
|
|
1213
|
+
.style('opacity', visible ? 1 : 0.15);
|
|
1214
|
+
}});
|
|
1215
|
+
}}
|
|
1216
|
+
|
|
1217
|
+
// Calculate max FLOPs for heat map scaling
|
|
1218
|
+
function getMaxFlops(nodes) {{
|
|
1219
|
+
let max = 0;
|
|
1220
|
+
nodes.forEach(n => {{
|
|
1221
|
+
if (n.total_flops > max) max = n.total_flops;
|
|
1222
|
+
}});
|
|
1223
|
+
return max || 1;
|
|
1224
|
+
}}
|
|
1225
|
+
|
|
1226
|
+
// Helper functions
|
|
1227
|
+
function truncate(str, len) {{
|
|
1228
|
+
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
1229
|
+
}}
|
|
1230
|
+
|
|
1231
|
+
function formatNumber(n) {{
|
|
1232
|
+
if (n >= 1e12) return (n / 1e12).toFixed(1) + 'T';
|
|
1233
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + 'G';
|
|
1234
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
|
|
1235
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
|
|
1236
|
+
return n.toString();
|
|
1237
|
+
}}
|
|
1238
|
+
|
|
1239
|
+
function formatBytes(bytes) {{
|
|
1240
|
+
if (bytes >= 1e9) return (bytes / 1e9).toFixed(2) + ' GB';
|
|
1241
|
+
if (bytes >= 1e6) return (bytes / 1e6).toFixed(1) + ' MB';
|
|
1242
|
+
if (bytes >= 1e3) return (bytes / 1e3).toFixed(1) + ' KB';
|
|
1243
|
+
return bytes + ' B';
|
|
1244
|
+
}}
|
|
1245
|
+
|
|
1246
|
+
// Control functions
|
|
1247
|
+
function expandAll() {{
|
|
1248
|
+
function expand(node) {{
|
|
1249
|
+
node.is_collapsed = false;
|
|
1250
|
+
if (node.children) node.children.forEach(expand);
|
|
1251
|
+
}}
|
|
1252
|
+
if (graphData.root) expand(graphData.root);
|
|
1253
|
+
render();
|
|
1254
|
+
}}
|
|
1255
|
+
|
|
1256
|
+
function collapseAll() {{
|
|
1257
|
+
function collapse(node) {{
|
|
1258
|
+
if (node.children && node.children.length > 0) {{
|
|
1259
|
+
node.is_collapsed = true;
|
|
1260
|
+
}}
|
|
1261
|
+
if (node.children) node.children.forEach(collapse);
|
|
1262
|
+
}}
|
|
1263
|
+
if (graphData.root) collapse(graphData.root);
|
|
1264
|
+
render();
|
|
1265
|
+
}}
|
|
1266
|
+
|
|
1267
|
+
function resetZoom() {{
|
|
1268
|
+
svg.transition().duration(500).call(zoom.transform, d3.zoomIdentity);
|
|
1269
|
+
}}
|
|
1270
|
+
|
|
1271
|
+
function fitToScreen() {{
|
|
1272
|
+
const bounds = container.node().getBBox();
|
|
1273
|
+
const parent = svg.node().parentElement;
|
|
1274
|
+
const fullWidth = parent.clientWidth;
|
|
1275
|
+
const fullHeight = parent.clientHeight;
|
|
1276
|
+
const width = bounds.width;
|
|
1277
|
+
const height = bounds.height;
|
|
1278
|
+
const midX = bounds.x + width / 2;
|
|
1279
|
+
const midY = bounds.y + height / 2;
|
|
1280
|
+
|
|
1281
|
+
const scale = 0.9 / Math.max(width / fullWidth, height / fullHeight);
|
|
1282
|
+
const translate = [fullWidth / 2 - scale * midX, fullHeight / 2 - scale * midY];
|
|
1283
|
+
|
|
1284
|
+
svg.transition().duration(500).call(
|
|
1285
|
+
zoom.transform,
|
|
1286
|
+
d3.zoomIdentity.translate(translate[0], translate[1]).scale(scale)
|
|
1287
|
+
);
|
|
1288
|
+
}}
|
|
1289
|
+
|
|
1290
|
+
// Task 5.7.6: Search functionality
|
|
1291
|
+
let allFlatNodes = [];
|
|
1292
|
+
|
|
1293
|
+
function buildSearchIndex() {{
|
|
1294
|
+
allFlatNodes = [];
|
|
1295
|
+
function collect(node) {{
|
|
1296
|
+
allFlatNodes.push(node);
|
|
1297
|
+
if (node.children) node.children.forEach(collect);
|
|
1298
|
+
}}
|
|
1299
|
+
if (graphData.root) collect(graphData.root);
|
|
1300
|
+
}}
|
|
1301
|
+
|
|
1302
|
+
function searchNodes(query) {{
|
|
1303
|
+
const resultsDiv = document.getElementById('searchResults');
|
|
1304
|
+
resultsDiv.innerHTML = '';
|
|
1305
|
+
|
|
1306
|
+
if (!query || query.length < 2) {{
|
|
1307
|
+
// Clear highlights
|
|
1308
|
+
container.selectAll('.node').classed('search-highlight', false);
|
|
1309
|
+
return;
|
|
1310
|
+
}}
|
|
1311
|
+
|
|
1312
|
+
query = query.toLowerCase();
|
|
1313
|
+
const matches = allFlatNodes.filter(node => {{
|
|
1314
|
+
const name = (node.name || '').toLowerCase();
|
|
1315
|
+
const opType = (node.op_type || '').toLowerCase();
|
|
1316
|
+
const blockType = (node.attributes?.block_type || '').toLowerCase();
|
|
1317
|
+
return name.includes(query) || opType.includes(query) || blockType.includes(query);
|
|
1318
|
+
}}).slice(0, 20); // Limit to 20 results
|
|
1319
|
+
|
|
1320
|
+
if (matches.length === 0) {{
|
|
1321
|
+
resultsDiv.innerHTML = '<div style="font-size: 0.7rem; color: var(--text-tertiary); padding: 8px;">No matches found</div>';
|
|
1322
|
+
return;
|
|
1323
|
+
}}
|
|
1324
|
+
|
|
1325
|
+
matches.forEach(node => {{
|
|
1326
|
+
const div = document.createElement('div');
|
|
1327
|
+
div.className = 'search-result';
|
|
1328
|
+
div.innerHTML = `
|
|
1329
|
+
<span class="result-name">${{node.name}}</span>
|
|
1330
|
+
<span class="result-type">${{node.op_type || node.node_type}}</span>
|
|
1331
|
+
`;
|
|
1332
|
+
div.onclick = () => highlightAndFocusNode(node);
|
|
1333
|
+
resultsDiv.appendChild(div);
|
|
1334
|
+
}});
|
|
1335
|
+
}}
|
|
1336
|
+
|
|
1337
|
+
function highlightAndFocusNode(targetNode) {{
|
|
1338
|
+
// Expand path to node if collapsed
|
|
1339
|
+
function expandToNode(node, target) {{
|
|
1340
|
+
if (node.id === target.id) return true;
|
|
1341
|
+
if (node.children) {{
|
|
1342
|
+
for (const child of node.children) {{
|
|
1343
|
+
if (expandToNode(child, target)) {{
|
|
1344
|
+
node.is_collapsed = false;
|
|
1345
|
+
return true;
|
|
1346
|
+
}}
|
|
1347
|
+
}}
|
|
1348
|
+
}}
|
|
1349
|
+
return false;
|
|
1350
|
+
}}
|
|
1351
|
+
|
|
1352
|
+
if (graphData.root) expandToNode(graphData.root, targetNode);
|
|
1353
|
+
render();
|
|
1354
|
+
|
|
1355
|
+
// Highlight the node
|
|
1356
|
+
setTimeout(() => {{
|
|
1357
|
+
container.selectAll('.node').classed('search-highlight', false);
|
|
1358
|
+
container.selectAll('.node')
|
|
1359
|
+
.filter(d => d.id === targetNode.id)
|
|
1360
|
+
.classed('search-highlight', true);
|
|
1361
|
+
|
|
1362
|
+
// Pan to node
|
|
1363
|
+
const nodeElem = container.selectAll('.node').filter(d => d.id === targetNode.id);
|
|
1364
|
+
if (!nodeElem.empty()) {{
|
|
1365
|
+
const d = nodeElem.datum();
|
|
1366
|
+
const width = window.innerWidth - 320;
|
|
1367
|
+
const height = window.innerHeight;
|
|
1368
|
+
const scale = 1.5;
|
|
1369
|
+
const x = width / 2 - d.x * scale;
|
|
1370
|
+
const y = height / 2 - d.y * scale;
|
|
1371
|
+
|
|
1372
|
+
svg.transition().duration(500).call(
|
|
1373
|
+
zoom.transform,
|
|
1374
|
+
d3.zoomIdentity.translate(x, y).scale(scale)
|
|
1375
|
+
);
|
|
1376
|
+
}}
|
|
1377
|
+
}}, 100);
|
|
1378
|
+
}}
|
|
1379
|
+
|
|
1380
|
+
// Task 5.7.9: Performance mode for large graphs
|
|
1381
|
+
const LARGE_GRAPH_THRESHOLD = 500;
|
|
1382
|
+
const VERY_LARGE_THRESHOLD = 5000;
|
|
1383
|
+
let performanceMode = false;
|
|
1384
|
+
let cullingEnabled = false;
|
|
1385
|
+
|
|
1386
|
+
function checkPerformanceMode() {{
|
|
1387
|
+
const totalNodes = graphData.total_nodes || 0;
|
|
1388
|
+
|
|
1389
|
+
if (totalNodes > VERY_LARGE_THRESHOLD) {{
|
|
1390
|
+
performanceMode = true;
|
|
1391
|
+
cullingEnabled = true;
|
|
1392
|
+
console.log(`Performance mode: ON (${{totalNodes}} nodes). Culling enabled.`);
|
|
1393
|
+
|
|
1394
|
+
// Add indicator
|
|
1395
|
+
const sidebar = document.querySelector('.sidebar');
|
|
1396
|
+
const perfDiv = document.createElement('div');
|
|
1397
|
+
perfDiv.className = 'perf-mode';
|
|
1398
|
+
perfDiv.innerHTML = `Large model (${{formatNumber(totalNodes)}} nodes) - simplified view`;
|
|
1399
|
+
sidebar.insertBefore(perfDiv, sidebar.firstChild.nextSibling);
|
|
1400
|
+
|
|
1401
|
+
// Auto-collapse to depth 1
|
|
1402
|
+
if (graphData.root) {{
|
|
1403
|
+
function collapseDeep(node, depth) {{
|
|
1404
|
+
if (depth > 1 && node.children && node.children.length > 0) {{
|
|
1405
|
+
node.is_collapsed = true;
|
|
1406
|
+
}}
|
|
1407
|
+
if (node.children) node.children.forEach(c => collapseDeep(c, depth + 1));
|
|
1408
|
+
}}
|
|
1409
|
+
collapseDeep(graphData.root, 0);
|
|
1410
|
+
}}
|
|
1411
|
+
}} else if (totalNodes > LARGE_GRAPH_THRESHOLD) {{
|
|
1412
|
+
performanceMode = true;
|
|
1413
|
+
console.log(`Performance mode: ON (${{totalNodes}} nodes). Simplified rendering.`);
|
|
1414
|
+
}}
|
|
1415
|
+
}}
|
|
1416
|
+
|
|
1417
|
+
// Viewport culling for very large graphs
|
|
1418
|
+
function getViewportBounds() {{
|
|
1419
|
+
const transform = d3.zoomTransform(svg.node());
|
|
1420
|
+
const width = window.innerWidth - 320;
|
|
1421
|
+
const height = window.innerHeight;
|
|
1422
|
+
|
|
1423
|
+
return {{
|
|
1424
|
+
x: -transform.x / transform.k - 100,
|
|
1425
|
+
y: -transform.y / transform.k - 100,
|
|
1426
|
+
width: width / transform.k + 200,
|
|
1427
|
+
height: height / transform.k + 200
|
|
1428
|
+
}};
|
|
1429
|
+
}}
|
|
1430
|
+
|
|
1431
|
+
function isInViewport(node, bounds) {{
|
|
1432
|
+
if (!cullingEnabled) return true;
|
|
1433
|
+
if (!node.x || !node.y) return true;
|
|
1434
|
+
const r = node.r || 30;
|
|
1435
|
+
return node.x + r >= bounds.x &&
|
|
1436
|
+
node.x - r <= bounds.x + bounds.width &&
|
|
1437
|
+
node.y + r >= bounds.y &&
|
|
1438
|
+
node.y - r <= bounds.y + bounds.height;
|
|
1439
|
+
}}
|
|
1440
|
+
|
|
1441
|
+
// Initialize
|
|
1442
|
+
buildSearchIndex();
|
|
1443
|
+
checkPerformanceMode();
|
|
1444
|
+
|
|
1445
|
+
// Check if embedded in iframe (Streamlit) - auto-collapse sidebar for more space
|
|
1446
|
+
const isEmbedded = window.self !== window.top;
|
|
1447
|
+
if (isEmbedded) {{
|
|
1448
|
+
// Start with sidebar collapsed in embedded mode
|
|
1449
|
+
setTimeout(() => {{
|
|
1450
|
+
const sidebar = document.getElementById('graphSidebar');
|
|
1451
|
+
const toggleBtn = document.getElementById('sidebarToggle');
|
|
1452
|
+
if (sidebar && toggleBtn) {{
|
|
1453
|
+
sidebar.classList.add('collapsed');
|
|
1454
|
+
toggleBtn.textContent = '▶';
|
|
1455
|
+
toggleBtn.style.left = '8px';
|
|
1456
|
+
}}
|
|
1457
|
+
}}, 50);
|
|
1458
|
+
}}
|
|
1459
|
+
|
|
1460
|
+
// Initial render
|
|
1461
|
+
render();
|
|
1462
|
+
// Auto-fit after a short delay to ensure everything is laid out
|
|
1463
|
+
setTimeout(fitToScreen, isEmbedded ? 200 : 100);
|
|
1464
|
+
</script>
|
|
1465
|
+
</body>
|
|
1466
|
+
</html>
|
|
1467
|
+
"""
|
|
1468
|
+
|
|
1469
|
+
|
|
1470
|
+
def generate_html(
|
|
1471
|
+
graph: HierarchicalGraph,
|
|
1472
|
+
edge_result: EdgeAnalysisResult | None = None,
|
|
1473
|
+
title: str = "Model Architecture",
|
|
1474
|
+
output_path: Path | str | None = None,
|
|
1475
|
+
model_size_bytes: int | None = None,
|
|
1476
|
+
layer_timing: dict[str, float] | None = None,
|
|
1477
|
+
) -> str:
|
|
1478
|
+
"""
|
|
1479
|
+
Generate interactive HTML visualization.
|
|
1480
|
+
|
|
1481
|
+
Task 5.8.1-5.8.5: Create standalone HTML with embedded visualization.
|
|
1482
|
+
|
|
1483
|
+
Args:
|
|
1484
|
+
graph: HierarchicalGraph to visualize.
|
|
1485
|
+
edge_result: Optional edge analysis results.
|
|
1486
|
+
title: Page title.
|
|
1487
|
+
output_path: Optional path to save HTML file.
|
|
1488
|
+
model_size_bytes: Optional model file size in bytes.
|
|
1489
|
+
layer_timing: Optional dict mapping layer name to execution time (ms).
|
|
1490
|
+
Used to color-code nodes by execution time (hotspots).
|
|
1491
|
+
|
|
1492
|
+
Returns:
|
|
1493
|
+
HTML content as string.
|
|
1494
|
+
"""
|
|
1495
|
+
# Convert graph to JSON, adding model_size_bytes and layer_timing
|
|
1496
|
+
graph_dict = graph.to_dict()
|
|
1497
|
+
if model_size_bytes is not None:
|
|
1498
|
+
graph_dict["model_size_bytes"] = model_size_bytes
|
|
1499
|
+
if layer_timing is not None:
|
|
1500
|
+
graph_dict["layer_timing"] = layer_timing
|
|
1501
|
+
graph_json = json.dumps(graph_dict)
|
|
1502
|
+
|
|
1503
|
+
# Convert edge analysis to JSON
|
|
1504
|
+
if edge_result:
|
|
1505
|
+
edge_json = json.dumps(edge_result.to_dict())
|
|
1506
|
+
else:
|
|
1507
|
+
edge_json = "{}"
|
|
1508
|
+
|
|
1509
|
+
# Generate HTML
|
|
1510
|
+
html = HTML_TEMPLATE.format(
|
|
1511
|
+
title=title,
|
|
1512
|
+
graph_json=graph_json,
|
|
1513
|
+
edge_json=edge_json,
|
|
1514
|
+
)
|
|
1515
|
+
|
|
1516
|
+
# Save to file if path provided
|
|
1517
|
+
if output_path:
|
|
1518
|
+
output_path = Path(output_path)
|
|
1519
|
+
output_path.write_text(html, encoding="utf-8")
|
|
1520
|
+
|
|
1521
|
+
return html
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
class HTMLExporter:
|
|
1525
|
+
"""
|
|
1526
|
+
Export ONNX models to interactive HTML visualization.
|
|
1527
|
+
|
|
1528
|
+
Combines HierarchicalGraph and EdgeAnalysis into a single
|
|
1529
|
+
standalone HTML file.
|
|
1530
|
+
"""
|
|
1531
|
+
|
|
1532
|
+
def __init__(self, logger: logging.Logger | None = None):
|
|
1533
|
+
self.logger = logger or logging.getLogger("haoline.html")
|
|
1534
|
+
|
|
1535
|
+
def export(
|
|
1536
|
+
self,
|
|
1537
|
+
graph: HierarchicalGraph,
|
|
1538
|
+
edge_result: EdgeAnalysisResult | None = None,
|
|
1539
|
+
output_path: Path | str = "model_graph.html",
|
|
1540
|
+
title: str | None = None,
|
|
1541
|
+
model_size_bytes: int | None = None,
|
|
1542
|
+
layer_timing: dict[str, float] | None = None,
|
|
1543
|
+
) -> Path:
|
|
1544
|
+
"""
|
|
1545
|
+
Export graph to HTML file.
|
|
1546
|
+
|
|
1547
|
+
Args:
|
|
1548
|
+
graph: HierarchicalGraph to export.
|
|
1549
|
+
edge_result: Optional EdgeAnalysisResult for edge data.
|
|
1550
|
+
output_path: Output file path.
|
|
1551
|
+
title: Optional page title.
|
|
1552
|
+
model_size_bytes: Optional model file size in bytes.
|
|
1553
|
+
layer_timing: Optional dict mapping layer name to execution time (ms).
|
|
1554
|
+
|
|
1555
|
+
Returns:
|
|
1556
|
+
Path to generated HTML file.
|
|
1557
|
+
"""
|
|
1558
|
+
output_path = Path(output_path)
|
|
1559
|
+
|
|
1560
|
+
if title is None:
|
|
1561
|
+
title = graph.root.name if graph.root else "Model"
|
|
1562
|
+
|
|
1563
|
+
generate_html(
|
|
1564
|
+
graph=graph,
|
|
1565
|
+
edge_result=edge_result,
|
|
1566
|
+
title=title,
|
|
1567
|
+
output_path=output_path,
|
|
1568
|
+
model_size_bytes=model_size_bytes,
|
|
1569
|
+
layer_timing=layer_timing,
|
|
1570
|
+
)
|
|
1571
|
+
|
|
1572
|
+
self.logger.info(f"HTML visualization exported to {output_path}")
|
|
1573
|
+
return output_path
|