@zenfg/inspector 0.1.0-beta.1

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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/dist/FrameGraphInspector.d.ts +153 -0
  4. package/dist/FrameGraphInspector.d.ts.map +1 -0
  5. package/dist/FrameGraphInspector.js +580 -0
  6. package/dist/FrameGraphInspector.js.map +1 -0
  7. package/dist/debugCaptureModel.d.ts +166 -0
  8. package/dist/debugCaptureModel.d.ts.map +1 -0
  9. package/dist/debugCaptureModel.js +364 -0
  10. package/dist/debugCaptureModel.js.map +1 -0
  11. package/dist/index.d.ts +17 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +15 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/panelAliasAnalysis.d.ts +20 -0
  16. package/dist/panelAliasAnalysis.d.ts.map +1 -0
  17. package/dist/panelAliasAnalysis.js +113 -0
  18. package/dist/panelAliasAnalysis.js.map +1 -0
  19. package/dist/panelCytoscapeGraphRenderer.d.ts +71 -0
  20. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -0
  21. package/dist/panelCytoscapeGraphRenderer.js +559 -0
  22. package/dist/panelCytoscapeGraphRenderer.js.map +1 -0
  23. package/dist/panelDiagnosticsView.d.ts +22 -0
  24. package/dist/panelDiagnosticsView.d.ts.map +1 -0
  25. package/dist/panelDiagnosticsView.js +123 -0
  26. package/dist/panelDiagnosticsView.js.map +1 -0
  27. package/dist/panelDomHelpers.d.ts +26 -0
  28. package/dist/panelDomHelpers.d.ts.map +1 -0
  29. package/dist/panelDomHelpers.js +129 -0
  30. package/dist/panelDomHelpers.js.map +1 -0
  31. package/dist/panelGraphLayout.d.ts +28 -0
  32. package/dist/panelGraphLayout.d.ts.map +1 -0
  33. package/dist/panelGraphLayout.js +223 -0
  34. package/dist/panelGraphLayout.js.map +1 -0
  35. package/dist/panelGraphRenderer.d.ts +20 -0
  36. package/dist/panelGraphRenderer.d.ts.map +1 -0
  37. package/dist/panelGraphRenderer.js +2 -0
  38. package/dist/panelGraphRenderer.js.map +1 -0
  39. package/dist/panelGraphScene.d.ts +98 -0
  40. package/dist/panelGraphScene.d.ts.map +1 -0
  41. package/dist/panelGraphScene.js +559 -0
  42. package/dist/panelGraphScene.js.map +1 -0
  43. package/dist/panelGraphView.d.ts +9 -0
  44. package/dist/panelGraphView.d.ts.map +1 -0
  45. package/dist/panelGraphView.js +88 -0
  46. package/dist/panelGraphView.js.map +1 -0
  47. package/dist/panelGraphVisuals.d.ts +38 -0
  48. package/dist/panelGraphVisuals.d.ts.map +1 -0
  49. package/dist/panelGraphVisuals.js +330 -0
  50. package/dist/panelGraphVisuals.js.map +1 -0
  51. package/dist/panelIcons.d.ts +4 -0
  52. package/dist/panelIcons.d.ts.map +1 -0
  53. package/dist/panelIcons.js +43 -0
  54. package/dist/panelIcons.js.map +1 -0
  55. package/dist/panelInspectorView.d.ts +33 -0
  56. package/dist/panelInspectorView.d.ts.map +1 -0
  57. package/dist/panelInspectorView.js +357 -0
  58. package/dist/panelInspectorView.js.map +1 -0
  59. package/dist/panelMemoryView.d.ts +21 -0
  60. package/dist/panelMemoryView.d.ts.map +1 -0
  61. package/dist/panelMemoryView.js +134 -0
  62. package/dist/panelMemoryView.js.map +1 -0
  63. package/dist/panelPassesView.d.ts +31 -0
  64. package/dist/panelPassesView.d.ts.map +1 -0
  65. package/dist/panelPassesView.js +183 -0
  66. package/dist/panelPassesView.js.map +1 -0
  67. package/dist/panelResourcesView.d.ts +19 -0
  68. package/dist/panelResourcesView.d.ts.map +1 -0
  69. package/dist/panelResourcesView.js +89 -0
  70. package/dist/panelResourcesView.js.map +1 -0
  71. package/dist/panelSelection.d.ts +5 -0
  72. package/dist/panelSelection.d.ts.map +1 -0
  73. package/dist/panelSelection.js +88 -0
  74. package/dist/panelSelection.js.map +1 -0
  75. package/dist/panelTypes.d.ts +40 -0
  76. package/dist/panelTypes.d.ts.map +1 -0
  77. package/dist/panelTypes.js +2 -0
  78. package/dist/panelTypes.js.map +1 -0
  79. package/dist/panelVisualTheme.d.ts +76 -0
  80. package/dist/panelVisualTheme.d.ts.map +1 -0
  81. package/dist/panelVisualTheme.js +39 -0
  82. package/dist/panelVisualTheme.js.map +1 -0
  83. package/dist/panelWorkbenchHelpers.d.ts +34 -0
  84. package/dist/panelWorkbenchHelpers.d.ts.map +1 -0
  85. package/dist/panelWorkbenchHelpers.js +162 -0
  86. package/dist/panelWorkbenchHelpers.js.map +1 -0
  87. package/dist/panelWorkbenchView.d.ts +77 -0
  88. package/dist/panelWorkbenchView.d.ts.map +1 -0
  89. package/dist/panelWorkbenchView.js +409 -0
  90. package/dist/panelWorkbenchView.js.map +1 -0
  91. package/dist/query.d.ts +11 -0
  92. package/dist/query.d.ts.map +1 -0
  93. package/dist/query.js +21 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/styles.d.ts +8 -0
  96. package/dist/styles.d.ts.map +1 -0
  97. package/dist/styles.js +1073 -0
  98. package/dist/styles.js.map +1 -0
  99. package/package.json +29 -0
  100. package/src/FrameGraphInspector.ts +632 -0
  101. package/src/debugCaptureModel.ts +583 -0
  102. package/src/index.ts +21 -0
  103. package/src/panelAliasAnalysis.ts +164 -0
  104. package/src/panelCytoscapeGraphRenderer.ts +617 -0
  105. package/src/panelDiagnosticsView.ts +160 -0
  106. package/src/panelDomHelpers.ts +151 -0
  107. package/src/panelGraphLayout.ts +281 -0
  108. package/src/panelGraphRenderer.ts +21 -0
  109. package/src/panelGraphScene.ts +787 -0
  110. package/src/panelGraphView.ts +108 -0
  111. package/src/panelGraphVisuals.ts +364 -0
  112. package/src/panelIcons.ts +56 -0
  113. package/src/panelInspectorView.ts +377 -0
  114. package/src/panelMemoryView.ts +154 -0
  115. package/src/panelPassesView.ts +222 -0
  116. package/src/panelResourcesView.ts +115 -0
  117. package/src/panelSelection.ts +99 -0
  118. package/src/panelTypes.ts +28 -0
  119. package/src/panelVisualTheme.ts +39 -0
  120. package/src/panelWorkbenchHelpers.ts +216 -0
  121. package/src/panelWorkbenchView.ts +465 -0
  122. package/src/query.ts +22 -0
  123. package/src/styles.ts +1076 -0
@@ -0,0 +1,377 @@
1
+ import type {
2
+ FrameGraphDebugAccess,
3
+ FrameGraphDebugResourceRef,
4
+ FrameGraphDebugViewModel,
5
+ } from './debugCaptureModel.ts';
6
+ import { labelNode, labelResource } from './panelDomHelpers.ts';
7
+ import { createPanelIcon } from './panelIcons.ts';
8
+ import { resolveSelectedDetail } from './panelSelection.ts';
9
+ import type { InspectorTab, Selection } from './panelTypes.ts';
10
+ import {
11
+ createRelationButton,
12
+ formatEstimatedBytes,
13
+ formatResourceDescriptor,
14
+ groupPath,
15
+ resourceLabel,
16
+ type WorkbenchCallbacks,
17
+ } from './panelWorkbenchHelpers.ts';
18
+
19
+ export class InspectorView {
20
+ readonly root = document.createElement('aside');
21
+ private readonly title = document.createElement('strong');
22
+ private readonly tabList = document.createElement('div');
23
+ private readonly content = document.createElement('div');
24
+ private readonly tabs = new Map<InspectorTab, HTMLButtonElement>();
25
+ private snapshot: FrameGraphDebugViewModel | undefined;
26
+ private selected: Selection | undefined;
27
+ private activeTab: InspectorTab = 'summary';
28
+ private open = true;
29
+
30
+ constructor(
31
+ private readonly callbacks: WorkbenchCallbacks,
32
+ private readonly onOpenChange: (open: boolean) => void,
33
+ idPrefix: string,
34
+ ) {
35
+ this.root.className = 'zenfg-inspector-inspector';
36
+ this.root.setAttribute('aria-label', 'Selection inspector');
37
+ const header = document.createElement('header');
38
+ this.title.textContent = 'Inspector';
39
+ const close = document.createElement('button');
40
+ close.type = 'button';
41
+ close.className = 'zenfg-inspector-inspector-close';
42
+ close.appendChild(createPanelIcon('close'));
43
+ close.title = 'Close inspector';
44
+ close.setAttribute('aria-label', 'Close inspector');
45
+ close.addEventListener('click', () => this.setOpen(false));
46
+ header.append(this.title, close);
47
+
48
+ this.tabList.className = 'zenfg-inspector-inspector-tabs';
49
+ this.tabList.setAttribute('role', 'tablist');
50
+ this.tabList.setAttribute('aria-label', 'Inspector views');
51
+ for (const [tab, label] of [['summary', 'Summary'], ['relations', 'Relations'], ['raw', 'Raw']] as const) {
52
+ const button = document.createElement('button');
53
+ button.type = 'button';
54
+ button.id = `${idPrefix}-inspector-${tab}-tab`;
55
+ button.textContent = label;
56
+ button.setAttribute('role', 'tab');
57
+ button.setAttribute('aria-controls', `${idPrefix}-inspector-panel`);
58
+ button.addEventListener('click', () => {
59
+ this.activeTab = tab;
60
+ this.render();
61
+ });
62
+ this.tabs.set(tab, button);
63
+ this.tabList.appendChild(button);
64
+ }
65
+ this.content.id = `${idPrefix}-inspector-panel`;
66
+ this.content.className = 'zenfg-inspector-inspector-content';
67
+ this.content.setAttribute('role', 'tabpanel');
68
+ this.root.append(header, this.tabList, this.content);
69
+ this.updateOpenState();
70
+ this.render();
71
+ }
72
+
73
+ setSnapshot(snapshot: FrameGraphDebugViewModel): void {
74
+ this.snapshot = snapshot;
75
+ this.render();
76
+ }
77
+
78
+ setSelection(selected: Selection | undefined, reveal = true): void {
79
+ this.selected = selected;
80
+ if (selected && reveal) this.setOpen(true);
81
+ this.render();
82
+ }
83
+
84
+ setOpen(open: boolean): void {
85
+ if (this.open === open) return;
86
+ this.open = open;
87
+ this.updateOpenState();
88
+ this.onOpenChange(open);
89
+ }
90
+
91
+ get isOpen(): boolean {
92
+ return this.open;
93
+ }
94
+
95
+ private updateOpenState(): void {
96
+ this.root.hidden = !this.open;
97
+ this.root.classList.toggle('open', this.open);
98
+ }
99
+
100
+ private render(): void {
101
+ for (const [tab, button] of this.tabs) {
102
+ const active = tab === this.activeTab;
103
+ button.classList.toggle('active', active);
104
+ button.setAttribute('aria-selected', active ? 'true' : 'false');
105
+ if (active) this.content.setAttribute('aria-labelledby', button.id);
106
+ }
107
+ const snapshot = this.snapshot;
108
+ const selected = this.selected;
109
+ this.content.replaceChildren();
110
+ if (!snapshot || !selected) {
111
+ this.title.textContent = 'Inspector';
112
+ const empty = document.createElement('p');
113
+ empty.className = 'zenfg-inspector-muted';
114
+ empty.textContent = 'Select a pass, group, resource, allocation, root, culled node, or segment.';
115
+ this.content.appendChild(empty);
116
+ return;
117
+ }
118
+ this.title.textContent = this.selectionTitle(snapshot, selected);
119
+ if (this.activeTab === 'raw') {
120
+ const raw = document.createElement('pre');
121
+ raw.className = 'zenfg-inspector-raw-detail';
122
+ raw.textContent = JSON.stringify(resolveSelectedDetail(snapshot, selected), null, 2);
123
+ this.content.appendChild(raw);
124
+ } else if (this.activeTab === 'relations') {
125
+ this.content.appendChild(this.createRelations(snapshot, selected));
126
+ } else {
127
+ this.content.appendChild(this.createSummary(snapshot, selected));
128
+ }
129
+ }
130
+
131
+ private selectionTitle(snapshot: FrameGraphDebugViewModel, selection: Selection): string {
132
+ switch (selection.kind) {
133
+ case 'node': {
134
+ const node = snapshot.nodeById.get(selection.id);
135
+ return node ? labelNode(node) : `Pass #${selection.id}`;
136
+ }
137
+ case 'group': return snapshot.groupByPathKey.get(selection.pathKey)?.label ?? 'Group';
138
+ case 'resource': {
139
+ const resource = snapshot.resourceById.get(selection.id);
140
+ return resource ? labelResource(resource) : `Resource #${selection.id}`;
141
+ }
142
+ case 'allocation': return `Allocation #${selection.id}`;
143
+ case 'root': return `Retention root #${selection.index}`;
144
+ case 'culled': return `Culled node #${selection.index}`;
145
+ case 'segment': return `Segment #${selection.index}`;
146
+ }
147
+ }
148
+
149
+ private createSummary(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
150
+ switch (selection.kind) {
151
+ case 'node': {
152
+ const node = snapshot.nodeById.get(selection.id);
153
+ if (!node) return this.summary([]);
154
+ const segment = snapshot.segmentByNodeId.get(node.id);
155
+ return this.summary([
156
+ ['Kind', node.kind],
157
+ ['Order', String(node.order)],
158
+ ['Group', groupPath(snapshot, node.debugGroupId)],
159
+ ['Segment', segment ? `#${segment.index} ${segment.kind}` : '-'],
160
+ ['GPU', node.kind === 'external-submission' ? 'Opaque' : node.gpuDurationMicros === undefined ? 'Not timed' : `${(node.gpuDurationMicros / 1000).toFixed(3)} ms`],
161
+ ['Accesses', `${node.reads.length} reads · ${node.writes.length} writes`],
162
+ ['Side effect', node.sideEffect ? 'yes' : 'no'],
163
+ ]);
164
+ }
165
+ case 'group': {
166
+ const group = snapshot.groupByPathKey.get(selection.pathKey);
167
+ if (!group) return this.summary([]);
168
+ return this.summary([
169
+ ['Path', group.path.join(' / ')],
170
+ ['Retained', String(group.summary.retainedNodeCount)],
171
+ ['Culled', String(group.summary.culledNodeCount)],
172
+ ['GPU coverage', `${group.summary.timedNodeCount}/${group.summary.timingEligibleNodeCount}`],
173
+ ['GPU work', `${(group.summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms`],
174
+ ['Allocations', String(group.summary.physicalAllocationCount)],
175
+ ['Segments', String(group.summary.executionSegmentCount)],
176
+ ]);
177
+ }
178
+ case 'resource': {
179
+ const resource = snapshot.resourceById.get(selection.id);
180
+ if (!resource) return this.summary([]);
181
+ return this.summary([
182
+ ['Kind / origin', `${resource.kind} · ${resource.origin}`],
183
+ ['Group', groupPath(snapshot, resource.debugGroupId)],
184
+ ['Descriptor', formatResourceDescriptor(resource)],
185
+ ['Estimated', formatEstimatedBytes(resource.estimatedByteSize)],
186
+ ['Lifetime', resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : '-'],
187
+ ['Allocation', resource.physicalResourceId === undefined ? '-' : `#${resource.physicalResourceId}`],
188
+ ['Usage', resource.usageFlags.join(', ') || '-'],
189
+ ]);
190
+ }
191
+ case 'allocation': {
192
+ const allocation = snapshot.allocationById.get(selection.id);
193
+ if (!allocation) return this.summary([]);
194
+ return this.summary([
195
+ ['Kind', allocation.kind],
196
+ ['Compatibility class', String(allocation.compatibilityClassId)],
197
+ ['Estimated', formatEstimatedBytes(allocation.estimatedByteSize)],
198
+ ['Logical resources', String(allocation.resourceIds.length)],
199
+ ['Alias', allocation.resourceIds.length > 1 ? `yes · ×${allocation.resourceIds.length}` : 'single'],
200
+ ]);
201
+ }
202
+ case 'root': {
203
+ const root = snapshot.roots[selection.index];
204
+ return this.summary(root ? [
205
+ ['Reason', root.reason],
206
+ ['Node', root.nodeId === undefined ? '-' : `#${root.nodeId}`],
207
+ ['Resource', root.resource ? labelResource(root.resource) : '-'],
208
+ ] : []);
209
+ }
210
+ case 'culled': {
211
+ const culled = snapshot.culledNodes[selection.index];
212
+ return this.summary(culled ? [
213
+ ['Node', labelNode(culled.node)],
214
+ ['Kind', culled.node.kind],
215
+ ['Group', groupPath(snapshot, culled.node.debugGroupId)],
216
+ ['Reason', culled.reason],
217
+ ['Accesses', `${culled.node.reads.length} reads · ${culled.node.writes.length} writes`],
218
+ ] : []);
219
+ }
220
+ case 'segment': {
221
+ const segment = snapshot.segmentByIndex.get(selection.index);
222
+ return this.summary(segment ? [
223
+ ['Kind', segment.kind === 'frame-graph' ? 'FrameGraph command segment' : 'Opaque interval'],
224
+ ['Nodes', String(segment.nodeIds.length)],
225
+ ['Meaning', segment.kind === 'external-submission'
226
+ ? 'Boundary around external work; not an actual third-party submission count.'
227
+ : 'Contiguous FrameGraph-encoded command work.'],
228
+ ] : []);
229
+ }
230
+ }
231
+ }
232
+
233
+ private createRelations(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
234
+ const host = document.createElement('div');
235
+ host.className = 'zenfg-inspector-inspector-relations';
236
+ switch (selection.kind) {
237
+ case 'node': {
238
+ const node = snapshot.nodeById.get(selection.id);
239
+ if (node) {
240
+ host.append(
241
+ this.accessRelations('Reads', node.reads),
242
+ this.accessRelations('Writes', node.writes),
243
+ );
244
+ const segment = snapshot.segmentByNodeId.get(node.id);
245
+ if (segment) host.appendChild(this.relationGroup('Segment', [
246
+ [`#${segment.index} ${segment.kind}`, { kind: 'segment', index: segment.index }],
247
+ ]));
248
+ }
249
+ break;
250
+ }
251
+ case 'group': {
252
+ const group = snapshot.groupByPathKey.get(selection.pathKey);
253
+ if (group) {
254
+ host.append(
255
+ this.resourceRelations('Inputs', group.summary.inputResources),
256
+ this.resourceRelations('Outputs', group.summary.outputResources),
257
+ );
258
+ }
259
+ break;
260
+ }
261
+ case 'resource': {
262
+ const resource = snapshot.resourceById.get(selection.id);
263
+ const accesses = snapshot.accessesByResourceId.get(selection.id) ?? [];
264
+ const passRelations: Array<readonly [string, Selection]> = [];
265
+ for (const access of accesses) {
266
+ const nodeSelection = this.accessNodeSelection(snapshot, access.nodeId);
267
+ if (nodeSelection) passRelations.push([
268
+ `${access.mode} · ${this.accessNodeLabel(snapshot, access.nodeId)} · ${access.access}`,
269
+ nodeSelection,
270
+ ]);
271
+ }
272
+ host.appendChild(this.relationGroup('Pass accesses', passRelations));
273
+ if (resource?.physicalResourceId !== undefined) host.appendChild(this.relationGroup('Allocation', [
274
+ [`#${resource.physicalResourceId}`, { kind: 'allocation', id: resource.physicalResourceId }],
275
+ ]));
276
+ break;
277
+ }
278
+ case 'allocation': {
279
+ const allocation = snapshot.allocationById.get(selection.id);
280
+ if (allocation) host.appendChild(this.relationGroup('Logical resources', allocation.resourceIds.map((id) => [
281
+ resourceLabel(snapshot, id), { kind: 'resource', id },
282
+ ])));
283
+ break;
284
+ }
285
+ case 'root': {
286
+ const root = snapshot.roots[selection.index];
287
+ const relations: Array<readonly [string, Selection]> = [];
288
+ if (root?.nodeId !== undefined && snapshot.nodeById.has(root.nodeId)) relations.push([
289
+ this.accessNodeLabel(snapshot, root.nodeId), { kind: 'node', id: root.nodeId },
290
+ ]);
291
+ if (root?.resource) relations.push([labelResource(root.resource), { kind: 'resource', id: root.resource.id }]);
292
+ host.appendChild(this.relationGroup('Retained object', relations));
293
+ break;
294
+ }
295
+ case 'culled': {
296
+ const culled = snapshot.culledNodes[selection.index];
297
+ if (culled) host.append(
298
+ this.accessRelations('Reads', culled.node.reads),
299
+ this.accessRelations('Writes', culled.node.writes),
300
+ );
301
+ break;
302
+ }
303
+ case 'segment': {
304
+ const segment = snapshot.segmentByIndex.get(selection.index);
305
+ if (segment) host.appendChild(this.relationGroup('Passes', segment.nodeIds.flatMap((id) => {
306
+ const node = snapshot.nodeById.get(id);
307
+ return node ? [[labelNode(node), { kind: 'node', id }] as const] : [];
308
+ })));
309
+ break;
310
+ }
311
+ }
312
+ if (host.childElementCount === 0) {
313
+ const empty = document.createElement('p');
314
+ empty.className = 'zenfg-inspector-muted';
315
+ empty.textContent = 'No related objects.';
316
+ host.appendChild(empty);
317
+ }
318
+ return host;
319
+ }
320
+
321
+ private accessRelations(title: string, accesses: readonly FrameGraphDebugAccess[]): HTMLElement {
322
+ return this.relationGroup(title, accesses.map((access) => [
323
+ `${labelResource(access.resource)} · ${access.access}${access.mode === 'write' ? ` · ${access.contents}` : ''}`,
324
+ { kind: 'resource', id: access.resource.id },
325
+ ]));
326
+ }
327
+
328
+ private resourceRelations(title: string, resources: readonly FrameGraphDebugResourceRef[]): HTMLElement {
329
+ return this.relationGroup(title, resources.map((resource) => [
330
+ labelResource(resource), { kind: 'resource', id: resource.id },
331
+ ]));
332
+ }
333
+
334
+ private relationGroup(title: string, relations: readonly (readonly [string, Selection])[]): HTMLElement {
335
+ const section = document.createElement('section');
336
+ const heading = document.createElement('h3');
337
+ heading.textContent = `${title} ${relations.length}`;
338
+ section.appendChild(heading);
339
+ for (const [label, selection] of relations) {
340
+ section.appendChild(createRelationButton(label, selection, this.callbacks.onSelect));
341
+ }
342
+ if (relations.length === 0) {
343
+ const empty = document.createElement('span');
344
+ empty.className = 'zenfg-inspector-muted';
345
+ empty.textContent = 'None';
346
+ section.appendChild(empty);
347
+ }
348
+ return section;
349
+ }
350
+
351
+ private summary(rows: readonly (readonly [string, string])[]): HTMLElement {
352
+ const summary = document.createElement('dl');
353
+ summary.className = 'zenfg-inspector-inspector-summary';
354
+ for (const [label, value] of rows) {
355
+ const term = document.createElement('dt');
356
+ term.textContent = label;
357
+ const description = document.createElement('dd');
358
+ description.textContent = value;
359
+ description.title = value;
360
+ summary.append(term, description);
361
+ }
362
+ return summary;
363
+ }
364
+
365
+ private accessNodeLabel(snapshot: FrameGraphDebugViewModel, nodeId: string): string {
366
+ const node = snapshot.nodeById.get(nodeId);
367
+ if (node) return labelNode(node);
368
+ const culled = snapshot.culledNodes.find((candidate) => candidate.node.id === nodeId);
369
+ return culled ? `${labelNode(culled.node)} (culled)` : `node-${nodeId}`;
370
+ }
371
+
372
+ private accessNodeSelection(snapshot: FrameGraphDebugViewModel, nodeId: string): Selection | undefined {
373
+ if (snapshot.nodeById.has(nodeId)) return { kind: 'node', id: nodeId };
374
+ const index = snapshot.culledNodes.findIndex((candidate) => candidate.node.id === nodeId);
375
+ return index < 0 ? undefined : { kind: 'culled', index };
376
+ }
377
+ }
@@ -0,0 +1,154 @@
1
+ import type { FrameGraphDebugResource, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import { analyzeSnapshotAliases } from './panelAliasAnalysis.ts';
3
+ import { createMutedText, labelResource } from './panelDomHelpers.ts';
4
+ import type { Selection } from './panelTypes.ts';
5
+ import {
6
+ createRelationButton,
7
+ formatEstimatedBytes,
8
+ registerSelectable,
9
+ type WorkbenchCallbacks,
10
+ updateSelectedRows,
11
+ } from './panelWorkbenchHelpers.ts';
12
+
13
+ export class MemoryView {
14
+ readonly root = document.createElement('section');
15
+ private readonly metrics = document.createElement('div');
16
+ private readonly scroller = document.createElement('div');
17
+ private readonly timeline = document.createElement('div');
18
+ private readonly rows = new Map<string, HTMLElement[]>();
19
+ private snapshot: FrameGraphDebugViewModel | undefined;
20
+ private selected: Selection | undefined;
21
+
22
+ constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
23
+ this.root.className = 'zenfg-inspector-view zenfg-inspector-memory-view';
24
+ this.root.id = `${idPrefix}-view-memory`;
25
+ this.root.setAttribute('role', 'tabpanel');
26
+ this.metrics.className = 'zenfg-inspector-memory-summary';
27
+ this.scroller.className = 'zenfg-inspector-memory-scroller';
28
+ this.timeline.className = 'zenfg-inspector-memory-timeline';
29
+ this.scroller.appendChild(this.timeline);
30
+ this.root.append(this.metrics, this.scroller);
31
+ }
32
+
33
+ setSnapshot(snapshot: FrameGraphDebugViewModel): void {
34
+ this.snapshot = snapshot;
35
+ this.render();
36
+ }
37
+
38
+ setSelection(selected: Selection | undefined): void {
39
+ this.selected = selected;
40
+ updateSelectedRows(this.rows, selected);
41
+ }
42
+
43
+ private render(): void {
44
+ const snapshot = this.snapshot;
45
+ if (!snapshot) return;
46
+ const metrics = snapshot.metrics;
47
+ this.metrics.replaceChildren(
48
+ this.createMetric('Logical capacity', formatEstimatedBytes(metrics.logicalCapacityBytes)),
49
+ this.createMetric('Physical estimate', formatEstimatedBytes(metrics.physicalEstimatedBytes)),
50
+ this.createMetric('Alias reuse', formatEstimatedBytes(metrics.aliasReuseBytes)),
51
+ this.createMetric('Allocations', `${snapshot.physicalAllocations.length} · ${metrics.aliasedAllocationCount} aliased`),
52
+ );
53
+ this.rows.clear();
54
+ this.timeline.replaceChildren();
55
+ const analysis = analyzeSnapshotAliases(snapshot);
56
+ this.timeline.appendChild(this.createAxis(analysis.minUse, analysis.maxUse));
57
+
58
+ for (const group of analysis.allocations) {
59
+ const selection: Selection = { kind: 'allocation', id: group.allocation.id };
60
+ const header = document.createElement('div');
61
+ header.className = 'zenfg-inspector-memory-allocation';
62
+ header.dataset.kind = group.allocation.kind;
63
+ registerSelectable(this.rows, header, selection, this.callbacks);
64
+ const name = createRelationButton(`Allocation #${group.allocation.id}`, selection, this.callbacks.onSelect);
65
+ const meta = document.createElement('span');
66
+ meta.textContent = `${group.allocation.kind} · class ${group.allocation.compatibilityClassId} · ${formatEstimatedBytes(group.allocation.estimatedByteSize)} · ${group.resources.length > 1 ? `alias ×${group.resources.length}` : 'single'}`;
67
+ header.append(name, meta);
68
+ this.timeline.appendChild(header);
69
+ for (const resource of group.resources) {
70
+ this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse));
71
+ }
72
+ }
73
+
74
+ const unallocated = snapshot.resources.filter((resource) => (
75
+ resource.origin === 'transient' && resource.physicalResourceId === undefined
76
+ ));
77
+ if (unallocated.length > 0) {
78
+ const header = document.createElement('div');
79
+ header.className = 'zenfg-inspector-memory-allocation muted';
80
+ header.dataset.kind = 'unallocated';
81
+ header.textContent = 'Unallocated transient resources';
82
+ this.timeline.appendChild(header);
83
+ for (const resource of unallocated) {
84
+ this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse));
85
+ }
86
+ }
87
+ if (analysis.allocations.length === 0 && unallocated.length === 0) {
88
+ this.timeline.appendChild(createMutedText('No transient resource lifetimes.'));
89
+ }
90
+ updateSelectedRows(this.rows, this.selected);
91
+ }
92
+
93
+ private createMetric(label: string, value: string): HTMLElement {
94
+ const item = document.createElement('div');
95
+ const term = document.createElement('span');
96
+ term.textContent = label;
97
+ const amount = document.createElement('strong');
98
+ amount.textContent = value;
99
+ item.append(term, amount);
100
+ return item;
101
+ }
102
+
103
+ private createAxis(minUse: number, maxUse: number): HTMLElement {
104
+ const axis = document.createElement('div');
105
+ axis.className = 'zenfg-inspector-memory-axis';
106
+ const label = document.createElement('span');
107
+ label.textContent = 'Execution order';
108
+ const track = document.createElement('div');
109
+ track.className = 'zenfg-inspector-memory-axis-track';
110
+ const range = Math.max(1, maxUse - minUse);
111
+ const step = Math.max(1, Math.ceil(range / 5));
112
+ for (let value = minUse; value <= maxUse; value += step) {
113
+ const tick = document.createElement('span');
114
+ tick.style.left = `${((value - minUse) / range) * 100}%`;
115
+ tick.textContent = String(value);
116
+ track.appendChild(tick);
117
+ }
118
+ if (maxUse > minUse && (maxUse - minUse) % step !== 0) {
119
+ const tick = document.createElement('span');
120
+ tick.style.left = '100%';
121
+ tick.textContent = String(maxUse);
122
+ track.appendChild(tick);
123
+ }
124
+ axis.append(label, track);
125
+ return axis;
126
+ }
127
+
128
+ private createResourceRow(resource: FrameGraphDebugResource, minUse: number, maxUse: number): HTMLElement {
129
+ const selection: Selection = { kind: 'resource', id: resource.id };
130
+ const row = document.createElement('div');
131
+ row.className = 'zenfg-inspector-memory-resource';
132
+ row.dataset.kind = resource.kind;
133
+ registerSelectable(this.rows, row, selection, this.callbacks);
134
+ const name = createRelationButton(labelResource(resource), selection, this.callbacks.onSelect);
135
+ const range = document.createElement('span');
136
+ range.textContent = resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : 'no lifetime';
137
+ const track = document.createElement('div');
138
+ track.className = 'zenfg-inspector-memory-track';
139
+ const bar = document.createElement('span');
140
+ bar.className = `zenfg-inspector-memory-bar ${resource.kind}`;
141
+ if (resource.lifetime) {
142
+ const slots = Math.max(1, maxUse - minUse + 1);
143
+ bar.style.left = `${((resource.lifetime.firstUse - minUse) / slots) * 100}%`;
144
+ bar.style.width = `${((resource.lifetime.lastUse - resource.lifetime.firstUse + 1) / slots) * 100}%`;
145
+ } else {
146
+ bar.classList.add('empty');
147
+ }
148
+ track.appendChild(bar);
149
+ const size = document.createElement('span');
150
+ size.textContent = formatEstimatedBytes(resource.estimatedByteSize);
151
+ row.append(name, range, track, size);
152
+ return row;
153
+ }
154
+ }