@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.4

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 (172) hide show
  1. package/GUIDE.md +182 -0
  2. package/README.md +60 -26
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +24 -8
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +151 -65
  7. package/dist/FrameGraphInspector.js.map +1 -1
  8. package/dist/capture.d.ts +5 -0
  9. package/dist/capture.d.ts.map +1 -0
  10. package/dist/capture.js +2 -0
  11. package/dist/capture.js.map +1 -0
  12. package/dist/debugCaptureModel.d.ts +28 -4
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +72 -16
  15. package/dist/debugCaptureModel.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/panelAliasAnalysis.d.ts +1 -0
  20. package/dist/panelAliasAnalysis.d.ts.map +1 -1
  21. package/dist/panelAliasAnalysis.js +23 -54
  22. package/dist/panelAliasAnalysis.js.map +1 -1
  23. package/dist/panelCytoscapeGraphRenderer.d.ts +18 -3
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +183 -41
  26. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  27. package/dist/panelDetailLayout.d.ts +27 -0
  28. package/dist/panelDetailLayout.d.ts.map +1 -0
  29. package/dist/panelDetailLayout.js +174 -0
  30. package/dist/panelDetailLayout.js.map +1 -0
  31. package/dist/panelDetailStyles.d.ts +2 -0
  32. package/dist/panelDetailStyles.d.ts.map +1 -0
  33. package/dist/panelDetailStyles.js +57 -0
  34. package/dist/panelDetailStyles.js.map +1 -0
  35. package/dist/panelDiagnosticsView.d.ts +10 -1
  36. package/dist/panelDiagnosticsView.d.ts.map +1 -1
  37. package/dist/panelDiagnosticsView.js +167 -58
  38. package/dist/panelDiagnosticsView.js.map +1 -1
  39. package/dist/panelDomHelpers.d.ts.map +1 -1
  40. package/dist/panelDomHelpers.js +6 -5
  41. package/dist/panelDomHelpers.js.map +1 -1
  42. package/dist/panelGraphLayout.d.ts +3 -2
  43. package/dist/panelGraphLayout.d.ts.map +1 -1
  44. package/dist/panelGraphLayout.js +20 -5
  45. package/dist/panelGraphLayout.js.map +1 -1
  46. package/dist/panelGraphRenderer.d.ts +10 -0
  47. package/dist/panelGraphRenderer.d.ts.map +1 -1
  48. package/dist/panelGraphRenderer.js.map +1 -1
  49. package/dist/panelGraphScene.d.ts +15 -26
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +191 -208
  52. package/dist/panelGraphScene.js.map +1 -1
  53. package/dist/panelGraphSearch.d.ts +16 -0
  54. package/dist/panelGraphSearch.d.ts.map +1 -0
  55. package/dist/panelGraphSearch.js +93 -0
  56. package/dist/panelGraphSearch.js.map +1 -0
  57. package/dist/panelGraphView.d.ts +82 -0
  58. package/dist/panelGraphView.d.ts.map +1 -1
  59. package/dist/panelGraphView.js +42 -9
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +18 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +156 -116
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +13 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +326 -46
  68. package/dist/panelInspectorView.js.map +1 -1
  69. package/dist/panelListStyles.d.ts +3 -0
  70. package/dist/panelListStyles.d.ts.map +1 -0
  71. package/dist/panelListStyles.js +155 -0
  72. package/dist/panelListStyles.js.map +1 -0
  73. package/dist/panelMemoryStyles.d.ts +3 -0
  74. package/dist/panelMemoryStyles.d.ts.map +1 -0
  75. package/dist/panelMemoryStyles.js +42 -0
  76. package/dist/panelMemoryStyles.js.map +1 -0
  77. package/dist/panelMemoryView.d.ts +13 -1
  78. package/dist/panelMemoryView.d.ts.map +1 -1
  79. package/dist/panelMemoryView.js +161 -41
  80. package/dist/panelMemoryView.js.map +1 -1
  81. package/dist/panelPassesView.d.ts +15 -0
  82. package/dist/panelPassesView.d.ts.map +1 -1
  83. package/dist/panelPassesView.js +202 -72
  84. package/dist/panelPassesView.js.map +1 -1
  85. package/dist/panelRawView.d.ts +15 -0
  86. package/dist/panelRawView.d.ts.map +1 -0
  87. package/dist/panelRawView.js +105 -0
  88. package/dist/panelRawView.js.map +1 -0
  89. package/dist/panelResourcesView.d.ts +8 -0
  90. package/dist/panelResourcesView.d.ts.map +1 -1
  91. package/dist/panelResourcesView.js +62 -31
  92. package/dist/panelResourcesView.js.map +1 -1
  93. package/dist/panelSelection.d.ts +9 -1
  94. package/dist/panelSelection.d.ts.map +1 -1
  95. package/dist/panelSelection.js +36 -6
  96. package/dist/panelSelection.js.map +1 -1
  97. package/dist/panelTheme.d.ts +13 -0
  98. package/dist/panelTheme.d.ts.map +1 -0
  99. package/dist/panelTheme.js +104 -0
  100. package/dist/panelTheme.js.map +1 -0
  101. package/dist/panelTypes.d.ts +18 -4
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +176 -71
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -36
  107. package/dist/panelVisualTheme.js.map +1 -1
  108. package/dist/panelWorkbenchHelpers.d.ts +12 -1
  109. package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
  110. package/dist/panelWorkbenchHelpers.js +63 -2
  111. package/dist/panelWorkbenchHelpers.js.map +1 -1
  112. package/dist/panelWorkbenchStyles.d.ts +2 -0
  113. package/dist/panelWorkbenchStyles.d.ts.map +1 -0
  114. package/dist/panelWorkbenchStyles.js +101 -0
  115. package/dist/panelWorkbenchStyles.js.map +1 -0
  116. package/dist/panelWorkbenchView.d.ts +16 -0
  117. package/dist/panelWorkbenchView.d.ts.map +1 -1
  118. package/dist/panelWorkbenchView.js +228 -50
  119. package/dist/panelWorkbenchView.js.map +1 -1
  120. package/dist/query.js.map +1 -1
  121. package/dist/styles.d.ts.map +1 -1
  122. package/dist/styles.js +161 -205
  123. package/dist/styles.js.map +1 -1
  124. package/dist/theme.d.ts +22 -0
  125. package/dist/theme.d.ts.map +1 -0
  126. package/dist/theme.js +10 -0
  127. package/dist/theme.js.map +1 -0
  128. package/dist/themeDefinitions.d.ts +251 -0
  129. package/dist/themeDefinitions.d.ts.map +1 -0
  130. package/dist/themeDefinitions.js +96 -0
  131. package/dist/themeDefinitions.js.map +1 -0
  132. package/dist/themePalette.d.ts +66 -0
  133. package/dist/themePalette.d.ts.map +1 -0
  134. package/dist/themePalette.js +37 -0
  135. package/dist/themePalette.js.map +1 -0
  136. package/dist/themes.css +165 -0
  137. package/package.json +44 -10
  138. package/src/FrameGraphInspector.ts +145 -68
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +97 -21
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +177 -40
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +167 -86
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +20 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +204 -292
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +41 -9
  153. package/src/panelGraphVisuals.ts +152 -118
  154. package/src/panelInspectorView.ts +315 -48
  155. package/src/panelListStyles.ts +154 -0
  156. package/src/panelMemoryStyles.ts +41 -0
  157. package/src/panelMemoryView.ts +178 -53
  158. package/src/panelPassesView.ts +190 -137
  159. package/src/panelRawView.ts +99 -0
  160. package/src/panelResourcesView.ts +69 -56
  161. package/src/panelSelection.ts +39 -7
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +16 -4
  164. package/src/panelVisualTheme.ts +38 -38
  165. package/src/panelWorkbenchHelpers.ts +62 -4
  166. package/src/panelWorkbenchStyles.ts +100 -0
  167. package/src/panelWorkbenchView.ts +201 -52
  168. package/src/query.ts +11 -11
  169. package/src/styles.ts +162 -205
  170. package/src/theme.ts +24 -0
  171. package/src/themeDefinitions.ts +103 -0
  172. package/src/themePalette.ts +44 -0
@@ -1,33 +1,51 @@
1
1
  import type { FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
- import { createCell, labelNode, labelResource } from './panelDomHelpers.ts';
3
- import type { Selection } from './panelTypes.ts';
2
+ import { labelNode, labelResource } from './panelDomHelpers.ts';
3
+ import { resolveNodeSelection } from './panelSelection.ts';
4
+ import type { Selection, WorkbenchTab } from './panelTypes.ts';
4
5
  import {
5
- createEmptyTableRow,
6
- createKindCell,
7
- createRelationButton,
8
- createSelectionCell,
9
- createTableScroller,
10
- registerSelectable,
11
- type WorkbenchCallbacks,
12
- updateSelectedRows,
6
+ createFilterSelect, createRelationButton, createSearchInput, createViewToolbar,
7
+ registerSelectable, selectionKey, type WorkbenchCallbacks, updateSelectedRows,
13
8
  } from './panelWorkbenchHelpers.ts';
14
9
 
10
+ const severityOrder = { error: 0, warning: 1, info: 2 } as const;
11
+
15
12
  export class DiagnosticsView {
16
13
  readonly root = document.createElement('section');
17
14
  private readonly scroller = document.createElement('div');
18
15
  private readonly content = document.createElement('div');
19
16
  private readonly rows = new Map<string, HTMLElement[]>();
17
+ private readonly openSections = new Set<string>();
18
+ private readonly count = document.createElement('span');
19
+ private readonly searchInput: HTMLInputElement;
20
+ private readonly severitySelect: HTMLSelectElement;
20
21
  private snapshot: FrameGraphDebugViewModel | undefined;
21
22
  private selected: Selection | undefined;
23
+ private severity = 'all';
24
+ private search = '';
22
25
 
23
26
  constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
24
27
  this.root.className = 'zenfg-inspector-view zenfg-inspector-diagnostics-view';
25
28
  this.root.id = `${idPrefix}-view-diagnostics`;
26
29
  this.root.setAttribute('role', 'tabpanel');
30
+ const toolbar = createViewToolbar('Diagnostic filters');
31
+ this.searchInput = createSearchInput('Search diagnostic code or message', '', (value) => {
32
+ this.search = value.trim().toLocaleLowerCase();
33
+ this.render();
34
+ });
35
+ this.severitySelect = createFilterSelect('Diagnostic severity', this.severity, [
36
+ ['all', 'All severities'], ['error', 'Error'], ['warning', 'Warning'], ['info', 'Info'],
37
+ ], (value) => { this.severity = value; this.render(); });
38
+ const clear = document.createElement('button');
39
+ clear.type = 'button';
40
+ clear.textContent = 'Clear filters';
41
+ clear.addEventListener('click', () => { this.clearFilters(); this.render(); });
42
+ this.count.className = 'zenfg-inspector-result-count';
43
+ this.count.setAttribute('role', 'status');
44
+ toolbar.append(this.searchInput, this.severitySelect, clear, this.count);
27
45
  this.scroller.className = 'zenfg-inspector-diagnostics-scroller';
28
- this.content.className = 'zenfg-inspector-diagnostics-grid';
46
+ this.content.className = 'zenfg-inspector-diagnostics-sections';
29
47
  this.scroller.appendChild(this.content);
30
- this.root.appendChild(this.scroller);
48
+ this.root.append(toolbar, this.scroller);
31
49
  }
32
50
 
33
51
  setSnapshot(snapshot: FrameGraphDebugViewModel): void {
@@ -40,23 +58,107 @@ export class DiagnosticsView {
40
58
  updateSelectedRows(this.rows, selected);
41
59
  }
42
60
 
61
+ reveal(selection: Selection): void {
62
+ this.clearFilters();
63
+ if (selection.kind === 'root') this.openSections.add('roots');
64
+ if (selection.kind === 'culled') this.openSections.add('culled');
65
+ if (selection.kind === 'segment') {
66
+ this.openSections.add('segments');
67
+ this.openSections.add(`segment:${selection.index}`);
68
+ }
69
+ this.render();
70
+ this.rows.get(selectionKey(selection))?.[0]?.scrollIntoView?.({ block: 'nearest' });
71
+ }
72
+
73
+ private clearFilters(): void {
74
+ this.search = '';
75
+ this.severity = 'all';
76
+ this.searchInput.value = '';
77
+ this.severitySelect.value = 'all';
78
+ }
79
+
43
80
  private render(): void {
44
81
  const snapshot = this.snapshot;
45
82
  if (!snapshot) return;
46
83
  this.rows.clear();
47
84
  this.content.replaceChildren(
48
- this.createRoots(snapshot),
49
- this.createCulled(snapshot),
50
- this.createSegments(snapshot),
51
- this.createTiming(snapshot),
85
+ this.createMessages(snapshot), this.createRoots(snapshot), this.createCulled(snapshot), this.createSegments(snapshot),
52
86
  );
53
87
  updateSelectedRows(this.rows, this.selected);
54
88
  }
55
89
 
56
- private createSection(title: string, description?: string): HTMLElement {
90
+ private createMessages(snapshot: FrameGraphDebugViewModel): HTMLElement {
57
91
  const section = document.createElement('section');
58
- section.className = 'zenfg-inspector-diagnostic-card';
92
+ section.className = 'zenfg-inspector-diagnostic-messages';
59
93
  const heading = document.createElement('h2');
94
+ heading.textContent = 'Diagnostics';
95
+ const summary = document.createElement('p');
96
+ const all = snapshot.protocol.diagnostics;
97
+ const errors = all.filter((entry) => entry.severity === 'error').length;
98
+ const warnings = all.filter((entry) => entry.severity === 'warning').length;
99
+ const infos = all.length - errors - warnings;
100
+ summary.textContent = `${errors} errors · ${warnings} warnings · ${infos} info`;
101
+ section.append(heading, summary);
102
+ // Sort a copy. Stable sort preserves capture order within each severity, including repeated codes.
103
+ const messages = all.filter((entry) => (this.severity === 'all' || entry.severity === this.severity)
104
+ && (!this.search || `${entry.code} ${entry.message}`.toLocaleLowerCase().includes(this.search)))
105
+ .sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
106
+ this.count.textContent = `${messages.length} / ${all.length} diagnostics`;
107
+ for (const diagnostic of messages) {
108
+ const article = document.createElement('article');
109
+ article.className = 'zenfg-inspector-diagnostic-message';
110
+ article.dataset.severity = diagnostic.severity;
111
+ const title = document.createElement('h3');
112
+ const severity = document.createElement('span');
113
+ severity.className = 'zenfg-inspector-diagnostic-severity';
114
+ severity.textContent = diagnostic.severity;
115
+ const code = document.createElement('code');
116
+ code.textContent = diagnostic.code;
117
+ title.append(severity, code);
118
+ const message = document.createElement('p');
119
+ message.textContent = diagnostic.message;
120
+ article.append(title, message);
121
+ if (diagnostic.nodeId !== undefined) {
122
+ const selection = resolveNodeSelection(snapshot, diagnostic.nodeId);
123
+ const node = snapshot.canonicalNodeById.get(diagnostic.nodeId);
124
+ if (selection && node) article.appendChild(this.createObjectLink(labelNode(node), selection, 'passes'));
125
+ }
126
+ if (diagnostic.resourceId !== undefined) {
127
+ const resource = snapshot.resourceById.get(diagnostic.resourceId);
128
+ if (resource) article.appendChild(this.createObjectLink(labelResource(resource), { kind: 'resource', id: resource.id }, 'resources'));
129
+ }
130
+ section.appendChild(article);
131
+ }
132
+ if (messages.length === 0) {
133
+ const empty = document.createElement('p');
134
+ empty.className = 'zenfg-inspector-muted';
135
+ empty.textContent = all.length === 0 ? 'No diagnostic messages in this snapshot.' : 'No diagnostics match the current filters.';
136
+ section.appendChild(empty);
137
+ }
138
+ return section;
139
+ }
140
+
141
+ private createObjectLink(label: string, selection: Selection, tab: WorkbenchTab): HTMLElement {
142
+ const links = document.createElement('span');
143
+ links.className = 'zenfg-inspector-diagnostic-links';
144
+ const select = createRelationButton(label, selection, this.callbacks.onSelect);
145
+ registerSelectable(this.rows, select, selection, this.callbacks);
146
+ const reveal = document.createElement('button');
147
+ reveal.type = 'button';
148
+ reveal.className = 'zenfg-inspector-relation-button zenfg-inspector-diagnostic-reveal';
149
+ reveal.textContent = `Show in ${tab === 'passes' ? 'Passes' : 'Resources'}`;
150
+ reveal.setAttribute('aria-label', `${reveal.textContent}: ${label}`);
151
+ reveal.addEventListener('click', () => this.callbacks.onReveal?.(selection, tab));
152
+ links.append(select, reveal);
153
+ return links;
154
+ }
155
+
156
+ private createSection(key: string, title: string, description?: string): HTMLDetailsElement {
157
+ const section = document.createElement('details');
158
+ section.className = 'zenfg-inspector-diagnostic-section';
159
+ section.dataset.section = key;
160
+ section.open = this.openSections.has(key);
161
+ const heading = document.createElement('summary');
60
162
  heading.textContent = title;
61
163
  section.appendChild(heading);
62
164
  if (description) {
@@ -64,97 +166,76 @@ export class DiagnosticsView {
64
166
  text.textContent = description;
65
167
  section.appendChild(text);
66
168
  }
169
+ section.addEventListener('toggle', () => {
170
+ if (section.open) this.openSections.add(key);
171
+ else this.openSections.delete(key);
172
+ });
67
173
  return section;
68
174
  }
69
175
 
70
176
  private createRoots(snapshot: FrameGraphDebugViewModel): HTMLElement {
71
- const section = this.createSection('Retention roots', 'Roots explain why graph work survived dead-node elimination.');
177
+ const section = this.createSection('roots', `Retention roots (${snapshot.roots.length})`, 'Roots explain why graph work survived dead-node elimination.');
72
178
  const list = document.createElement('div');
73
179
  list.className = 'zenfg-inspector-diagnostic-list';
74
- snapshot.roots.forEach((root, index) => {
75
- const selection: Selection = { kind: 'root', index };
76
- const button = createRelationButton(
77
- `${root.reason} · ${root.resource ? labelResource(root.resource) : root.nodeId === undefined ? '-' : `node-${root.nodeId}`}`,
78
- selection,
79
- this.callbacks.onSelect,
80
- );
180
+ for (const root of snapshot.roots) {
181
+ const selection: Selection = { kind: 'root', key: root.key };
182
+ const node = root.nodeId === undefined ? undefined : snapshot.canonicalNodeById.get(root.nodeId);
183
+ const label = root.resource ? labelResource(root.resource) : node ? labelNode(node) : root.nodeId ?? 'Unknown';
184
+ const button = createRelationButton(`${root.reason} · ${label}`, selection, this.callbacks.onSelect);
81
185
  registerSelectable(this.rows, button, selection, this.callbacks);
82
186
  list.appendChild(button);
83
- });
187
+ }
84
188
  if (snapshot.roots.length === 0) list.textContent = 'No retention roots.';
85
189
  section.appendChild(list);
86
190
  return section;
87
191
  }
88
192
 
89
193
  private createCulled(snapshot: FrameGraphDebugViewModel): HTMLElement {
90
- const section = this.createSection('Culled nodes', 'Recorded nodes that were not reachable from a retention root.');
91
- const table = createTableScroller([{ label: 'Node' }, { label: 'Kind', column: 'kind' }, { label: 'Reason' }]);
92
- for (const [index, entry] of snapshot.culledNodes.entries()) {
93
- const selection: Selection = { kind: 'culled', index };
94
- const row = document.createElement('tr');
95
- registerSelectable(this.rows, row, selection, this.callbacks);
96
- row.append(
97
- createSelectionCell(labelNode(entry.node), selection, this.callbacks),
98
- createKindCell(entry.node.kind),
99
- createCell(entry.reason),
100
- );
101
- table.body.appendChild(row);
194
+ const section = this.createSection('culled', `Culled passes (${snapshot.culledNodes.length})`, 'Recorded passes removed during compilation. Inspect their reasons and accesses in Passes.');
195
+ const reasons = new Map<string, number>();
196
+ for (const entry of snapshot.culledNodes) reasons.set(entry.reason, (reasons.get(entry.reason) ?? 0) + 1);
197
+ const list = document.createElement('ul');
198
+ for (const [reason, count] of reasons) {
199
+ const item = document.createElement('li');
200
+ item.textContent = `${reason}: ${count}`;
201
+ list.appendChild(item);
202
+ }
203
+ section.appendChild(list);
204
+ if (snapshot.culledNodes.length > 0) {
205
+ const button = document.createElement('button');
206
+ button.type = 'button';
207
+ button.className = 'zenfg-inspector-relation-button';
208
+ button.textContent = 'Show culled passes';
209
+ button.addEventListener('click', () => this.callbacks.onNavigate?.('passes', 'culled'));
210
+ section.appendChild(button);
102
211
  }
103
- if (snapshot.culledNodes.length === 0) table.body.appendChild(createEmptyTableRow(3, 'No culled nodes.'));
104
- section.appendChild(table.scroller);
105
212
  return section;
106
213
  }
107
214
 
108
215
  private createSegments(snapshot: FrameGraphDebugViewModel): HTMLElement {
109
- const section = this.createSection(
110
- 'Execution segments',
111
- 'An opaque interval is a graph boundary around external work; it is not a count or timing of third-party GPU submissions.',
112
- );
113
- const table = createTableScroller([{ label: '#', column: 'numeric' }, { label: 'Kind', column: 'kind' }, { label: 'Nodes' }]);
216
+ const section = this.createSection('segments', `Execution segments (${snapshot.executionSegments.length})`,
217
+ 'An opaque interval marks external work. Its GPU submissions and duration are not measured here.');
114
218
  for (const segment of snapshot.executionSegments) {
115
219
  const selection: Selection = { kind: 'segment', index: segment.index };
116
- const row = document.createElement('tr');
117
- registerSelectable(this.rows, row, selection, this.callbacks);
118
- const labels = segment.nodeIds.map((id) => snapshot.nodeById.get(id))
119
- .filter((node) => node !== undefined)
120
- .map((node) => labelNode(node));
121
- row.append(
122
- createSelectionCell(String(segment.index), selection, this.callbacks, 'numeric'),
123
- createKindCell(segment.kind, segment.kind === 'frame-graph' ? 'FrameGraph command segment' : 'Opaque interval'),
124
- createCell(labels.join(', ') || '-'),
125
- );
126
- table.body.appendChild(row);
127
- }
128
- if (snapshot.executionSegments.length === 0) table.body.appendChild(createEmptyTableRow(3, 'No execution segments.'));
129
- section.appendChild(table.scroller);
130
- return section;
131
- }
132
-
133
- private createTiming(snapshot: FrameGraphDebugViewModel): HTMLElement {
134
- const metrics = snapshot.metrics;
135
- const coverage = metrics.timingEligibleNodeCount === 0
136
- ? 'No eligible render or compute passes'
137
- : `${metrics.timedNodeCount}/${metrics.timingEligibleNodeCount} eligible passes timed (${((metrics.timedNodeCount / metrics.timingEligibleNodeCount) * 100).toFixed(0)}%)`;
138
- const section = this.createSection('GPU timing coverage', coverage);
139
- if (snapshot.profiling.status === 'unavailable') {
140
- const status = document.createElement('p');
141
- status.textContent = `Timing unavailable: ${snapshot.profiling.reason}.`;
142
- section.appendChild(status);
220
+ const members = this.createSection(`segment:${segment.index}`, `${segment.index} · ${segment.kind === 'frame-graph' ? 'FrameGraph command segment' : 'Opaque interval'} · ${segment.nodeIds.length} passes`);
221
+ const button = createRelationButton('Inspect segment', selection, this.callbacks.onSelect);
222
+ registerSelectable(this.rows, button, selection, this.callbacks);
223
+ members.appendChild(button);
224
+ const list = document.createElement('div');
225
+ list.className = 'zenfg-inspector-diagnostic-list';
226
+ for (const id of segment.nodeIds) {
227
+ const node = snapshot.canonicalNodeById.get(id);
228
+ const nodeSelection = resolveNodeSelection(snapshot, id);
229
+ if (node && nodeSelection) list.appendChild(this.createObjectLink(labelNode(node), nodeSelection, 'passes'));
230
+ }
231
+ members.appendChild(list);
232
+ section.appendChild(members);
143
233
  }
144
- const table = createTableScroller([{ label: 'Pass' }, { label: 'Status', column: 'kind' }, { label: 'GPU (ms)', column: 'numeric' }]);
145
- for (const node of snapshot.nodes.filter((candidate) => candidate.kind === 'render' || candidate.kind === 'compute')) {
146
- const selection: Selection = { kind: 'node', id: node.id };
147
- const row = document.createElement('tr');
148
- registerSelectable(this.rows, row, selection, this.callbacks);
149
- row.append(
150
- createSelectionCell(labelNode(node), selection, this.callbacks),
151
- createKindCell(node.gpuDurationMicros === undefined ? 'not-timed' : 'timed', node.gpuDurationMicros === undefined ? 'Not timed' : 'Timed'),
152
- createCell(node.gpuDurationMicros === undefined ? '-' : (node.gpuDurationMicros / 1000).toFixed(3), { column: 'numeric' }),
153
- );
154
- table.body.appendChild(row);
234
+ if (snapshot.executionSegments.length === 0) {
235
+ const empty = document.createElement('p');
236
+ empty.textContent = 'No execution segments.';
237
+ section.appendChild(empty);
155
238
  }
156
- if (table.body.childElementCount === 0) table.body.appendChild(createEmptyTableRow(3, 'No timing-eligible passes.'));
157
- section.appendChild(table.scroller);
158
239
  return section;
159
240
  }
160
241
  }
@@ -126,9 +126,8 @@ export function formatGpuFrameDuration(snapshot: FrameGraphDebugViewModel): stri
126
126
 
127
127
  export function formatPoolHitRate(snapshot: FrameGraphDebugViewModel): string {
128
128
  const pool = snapshot.resourcePool;
129
- if (!pool || pool.status !== 'available' || pool.acquireCount === 0) {
130
- return '-';
131
- }
129
+ if (!pool || pool.status !== 'available') return 'Unavailable';
130
+ if (pool.acquireCount === 0) return 'Not applicable · no acquisitions';
132
131
  return `${((pool.reuseCount / pool.acquireCount) * 100).toFixed(0)}%`;
133
132
  }
134
133
 
@@ -143,9 +142,9 @@ export function formatBytes(bytes: number): string {
143
142
  }
144
143
 
145
144
  export function labelNode(node: Pick<FrameGraphDebugNode, 'id' | 'label'>): string {
146
- return node.label ?? `node-${node.id}`;
145
+ return node.label ?? node.id;
147
146
  }
148
147
 
149
148
  export function labelResource(resource: { readonly id: string; readonly kind: string; readonly label?: string }): string {
150
- return resource.label ?? `${resource.kind}-${resource.id}`;
149
+ return resource.label ?? resource.id;
151
150
  }
@@ -1,3 +1,4 @@
1
+ import { GRAPH_VISUAL_THEME, type GraphVisualTheme } from './panelVisualTheme.ts';
1
2
  import type cytoscape from 'cytoscape';
2
3
  import type { ELK, ElkExtendedEdge, ElkNode, ElkPort } from 'elkjs/lib/elk-api';
3
4
 
@@ -22,8 +23,8 @@ export type CytoscapeEdgeRouteData = {
22
23
  readonly segmentDistances: string;
23
24
  };
24
25
 
25
- export async function layoutGraphScene(elk: ELK, scene: GraphScene): Promise<GraphLayoutResult> {
26
- const graph = createElkLayoutGraph(scene);
26
+ export async function layoutGraphScene(elk: ELK, scene: GraphScene, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): Promise<GraphLayoutResult> {
27
+ const graph = createElkLayoutGraph(scene, theme);
27
28
  const layout = await elk.layout(graph);
28
29
  const positions = new Map<GraphSceneElementId, GraphPosition>();
29
30
  collectLayoutPositions(layout, 0, 0, positions);
@@ -34,20 +35,34 @@ export async function layoutGraphScene(elk: ELK, scene: GraphScene): Promise<Gra
34
35
  return { positions, routes };
35
36
  }
36
37
 
37
- export function createElkLayoutGraph(scene: GraphScene): ElkNode {
38
+ function usesCenterPorts(node: GraphSceneNode): boolean {
39
+ return node.kind === 'resource' || node.kind === 'root'
40
+ || (node.kind === 'pass' && node.passKind === 'external-submission');
41
+ }
42
+
43
+ export function createElkLayoutGraph(scene: GraphScene, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): ElkNode {
38
44
  const childrenByParent = new Map<GraphSceneElementId | undefined, GraphSceneNode[]>();
39
45
  const portsByNode = new Map<GraphSceneElementId, ElkPort[]>();
46
+ const nodesById = new Map(scene.nodes.map((node) => [node.id, node]));
47
+ const boundaryPort = (id: string, source: boolean) => {
48
+ const node = nodesById.get(id)!;
49
+ if (!usesCenterPorts(node)) return {};
50
+ const dimensions = nodeDimensions(node, theme);
51
+ return { x: source ? dimensions.width : 0, y: dimensions.height / 2 };
52
+ };
40
53
  const elkEdges = scene.edges.map((edge): ElkExtendedEdge => {
41
54
  const sourcePortId = `${edge.id}:source`;
42
55
  const targetPortId = `${edge.id}:target`;
43
56
  appendPort(portsByNode, edge.from, {
44
57
  id: sourcePortId,
58
+ ...boundaryPort(edge.from, true),
45
59
  width: 1,
46
60
  height: 1,
47
61
  layoutOptions: { 'elk.port.side': 'EAST' },
48
62
  });
49
63
  appendPort(portsByNode, edge.to, {
50
64
  id: targetPortId,
65
+ ...boundaryPort(edge.to, false),
51
66
  width: 1,
52
67
  height: 1,
53
68
  layoutOptions: { 'elk.port.side': 'WEST' },
@@ -65,7 +80,7 @@ export function createElkLayoutGraph(scene: GraphScene): ElkNode {
65
80
  }
66
81
  const createNode = (node: GraphSceneNode): ElkNode => {
67
82
  const children = childrenByParent.get(node.id)?.map(createNode);
68
- const dimensions = nodeDimensions(node);
83
+ const dimensions = nodeDimensions(node, theme);
69
84
  return {
70
85
  id: node.id,
71
86
  width: dimensions.width,
@@ -86,7 +101,7 @@ export function createElkLayoutGraph(scene: GraphScene): ElkNode {
86
101
  'elk.spacing.edgeEdge': '10',
87
102
  'elk.layered.spacing.edgeEdgeBetweenLayers': '10',
88
103
  } : {
89
- 'elk.portConstraints': 'FIXED_SIDE',
104
+ 'elk.portConstraints': usesCenterPorts(node) ? 'FIXED_POS' : 'FIXED_SIDE',
90
105
  },
91
106
  };
92
107
  };
@@ -1,12 +1,17 @@
1
+ import type { GraphVisualTheme } from './panelVisualTheme.ts';
1
2
  import type { GraphScene, GraphSceneElementId } from './panelGraphScene.ts';
2
3
  import type { Selection } from './panelTypes.ts';
3
4
 
4
5
  export type GraphRenderRequest = {
6
+ readonly theme?: GraphVisualTheme;
7
+ readonly onVisible?: () => void;
5
8
  readonly scene: GraphScene;
6
9
  readonly selected: Selection | undefined;
7
10
  readonly hovered: Selection | undefined;
8
11
  readonly fit: boolean;
9
12
  readonly anchorElementId?: GraphSceneElementId;
13
+ readonly reveal?: { readonly selection: Selection; readonly revision: number };
14
+ readonly captureRevision?: number;
10
15
  readonly onSelect: (selection: Selection) => void;
11
16
  readonly onHover: (selection: Selection | undefined) => void;
12
17
  readonly onToggleGroup: (pathKey: string) => void;
@@ -14,8 +19,10 @@ export type GraphRenderRequest = {
14
19
 
15
20
  export interface GraphRenderer {
16
21
  render(request: GraphRenderRequest): void;
22
+ setTheme?(theme: GraphVisualTheme): void;
17
23
  resize(): void;
18
24
  fit(): void;
19
25
  relayout(): void;
26
+ cancelReveal?(): void;
20
27
  destroy(): void;
21
28
  }