@zenfg/inspector 0.1.0-beta.3 → 0.1.0

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 +61 -89
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +23 -4
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +137 -29
  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 +19 -0
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +36 -2
  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 +15 -2
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +145 -23
  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 +166 -57
  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 +6 -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 +1 -0
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +69 -32
  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 +15 -4
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +14 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +126 -51
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +8 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +242 -48
  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 +34 -4
  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 +10 -1
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +147 -50
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -44
  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 +62 -1
  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 +147 -195
  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 +130 -31
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +53 -2
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +140 -23
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +166 -85
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +6 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +68 -30
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +14 -4
  153. package/src/panelGraphVisuals.ts +122 -52
  154. package/src/panelInspectorView.ts +227 -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 +37 -5
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +7 -1
  164. package/src/panelVisualTheme.ts +38 -49
  165. package/src/panelWorkbenchHelpers.ts +60 -2
  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 +148 -195
  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) => {
180
+ for (const root of snapshot.roots) {
75
181
  const selection: Selection = { kind: 'root', key: root.key };
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
- );
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);
@@ -39,14 +40,14 @@ function usesCenterPorts(node: GraphSceneNode): boolean {
39
40
  || (node.kind === 'pass' && node.passKind === 'external-submission');
40
41
  }
41
42
 
42
- export function createElkLayoutGraph(scene: GraphScene): ElkNode {
43
+ export function createElkLayoutGraph(scene: GraphScene, theme: GraphVisualTheme = GRAPH_VISUAL_THEME): ElkNode {
43
44
  const childrenByParent = new Map<GraphSceneElementId | undefined, GraphSceneNode[]>();
44
45
  const portsByNode = new Map<GraphSceneElementId, ElkPort[]>();
45
46
  const nodesById = new Map(scene.nodes.map((node) => [node.id, node]));
46
47
  const boundaryPort = (id: string, source: boolean) => {
47
48
  const node = nodesById.get(id)!;
48
49
  if (!usesCenterPorts(node)) return {};
49
- const dimensions = nodeDimensions(node);
50
+ const dimensions = nodeDimensions(node, theme);
50
51
  return { x: source ? dimensions.width : 0, y: dimensions.height / 2 };
51
52
  };
52
53
  const elkEdges = scene.edges.map((edge): ElkExtendedEdge => {
@@ -79,7 +80,7 @@ export function createElkLayoutGraph(scene: GraphScene): ElkNode {
79
80
  }
80
81
  const createNode = (node: GraphSceneNode): ElkNode => {
81
82
  const children = childrenByParent.get(node.id)?.map(createNode);
82
- const dimensions = nodeDimensions(node);
83
+ const dimensions = nodeDimensions(node, theme);
83
84
  return {
84
85
  id: node.id,
85
86
  width: dimensions.width,
@@ -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
  }
@@ -9,6 +9,7 @@ import type {
9
9
  import { formatGpuDuration, labelNode, labelResource } from './panelDomHelpers.ts';
10
10
  import { declarationEntrances } from './debugCaptureModel.ts';
11
11
  import type { GraphFlowRelation, Selection } from './panelTypes.ts';
12
+ import { formatMeasuredGpuWork } from './panelWorkbenchHelpers.ts';
12
13
 
13
14
  export type GraphSceneElementId = string;
14
15
 
@@ -98,6 +99,7 @@ export type GraphScene = {
98
99
 
99
100
  export type CreateGraphSceneOptions = {
100
101
  readonly groupsEnabled: boolean;
102
+ readonly showResourceDeclarations?: boolean;
101
103
  readonly expandedGroupPaths: ReadonlySet<string>;
102
104
  };
103
105
 
@@ -113,7 +115,7 @@ export function createGraphScene(
113
115
  snapshot: FrameGraphDebugViewModel,
114
116
  options: CreateGraphSceneOptions,
115
117
  ): GraphScene {
116
- return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths);
118
+ return createFrameFlowScene(snapshot, options.groupsEnabled, options.expandedGroupPaths, options.showResourceDeclarations ?? true);
117
119
  }
118
120
 
119
121
  export function graphGroupElementId(pathKey: string): GraphSceneElementId {
@@ -133,7 +135,7 @@ export function selectionKey(selection: Selection): string {
133
135
  case 'root':
134
136
  return `root:${selection.key}`;
135
137
  case 'culled':
136
- return `culled:${selection.index}`;
138
+ return `culled:${selection.id}`;
137
139
  case 'segment':
138
140
  return `segment:${selection.index}`;
139
141
  }
@@ -143,6 +145,7 @@ function createFrameFlowScene(
143
145
  snapshot: FrameGraphDebugViewModel,
144
146
  groupsEnabled: boolean,
145
147
  expandedGroupPaths: ReadonlySet<string>,
148
+ showResourceDeclarations: boolean,
146
149
  ): GraphScene {
147
150
  const interaction = createMutableInteractionIndex();
148
151
  const segmentByNodeId = executionSegmentByNodeId(snapshot);
@@ -157,7 +160,7 @@ function createFrameFlowScene(
157
160
  ...snapshot.accessEdges.filter((access) => retainedIds.has(access.nodeId)).map((access) => access.resource.id),
158
161
  ...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
159
162
  ]);
160
- const resources = snapshot.resources.filter((resource) => usedResourceIds.has(resource.id));
163
+ const resources = showResourceDeclarations ? snapshot.resources.filter((resource) => usedResourceIds.has(resource.id)) : [];
161
164
  const populatedGroupIds = new Set<string>();
162
165
  for (const item of [...snapshot.nodes, ...resources]) {
163
166
  for (const id of groupsById.get(item.debugGroupId ?? '')?.ancestorIds ?? []) populatedGroupIds.add(id);
@@ -202,8 +205,8 @@ function createFrameFlowScene(
202
205
  groupId: group.id,
203
206
  groupPathKey: group.pathKey,
204
207
  label: createGroupLabel(group, collapsed),
205
- overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label}`,
206
- title: createGroupTitle(group),
208
+ overviewLabel: `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
209
+ title: createGroupTitle(group, snapshot),
207
210
  parentId: group.parentId !== undefined && includedGroupIds.has(group.parentId)
208
211
  ? graphGroupElementId(groupsById.get(group.parentId)!.pathKey)
209
212
  : undefined,
@@ -269,7 +272,7 @@ function createFrameFlowScene(
269
272
  }
270
273
  if (useGroups) {
271
274
  for (const group of snapshot.debugGroups) {
272
- if (includedGroupIds.has(group.id)) continue;
275
+ if (includedGroupIds.has(group.id) || !populatedGroupIds.has(group.id)) continue;
273
276
  const collapsedAncestorId = group.ancestorIds.find((id) => !isExpanded(groupsById.get(id)!));
274
277
  if (collapsedAncestorId !== undefined) {
275
278
  const representative = graphGroupElementId(groupsById.get(collapsedAncestorId)!.pathKey);
@@ -289,7 +292,7 @@ function createFrameFlowScene(
289
292
  const node: ResourceSceneNode = {
290
293
  id: resourceElementId(resource.id), kind: 'resource', resourceId: resource.id, resourceKind: resource.kind,
291
294
  label: origin + ' · ' + (resource.kind === 'buffer' ? 'Buffer' : 'Texture') + '\n' + formatGraphResourceLabel(labelResource(resource)),
292
- overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
295
+ overviewLabel: origin + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1),
293
296
  title: createResourceTitle(resource, snapshot, snapshot.accessesByResourceId.get(resource.id) ?? [])
294
297
  + '\nDeclaration entrance, not a complete initial-content provenance graph.',
295
298
  parentId: useGroups && resource.debugGroupId ? graphGroupElementId(groupsById.get(resource.debugGroupId)!.pathKey) : undefined,
@@ -311,7 +314,7 @@ function createFrameFlowScene(
311
314
  addRelation(representativeByNodeId.get(dependency.fromNodeId)!, representativeByNodeId.get(dependency.toNodeId)!, dependency.resource.id,
312
315
  { role: dependency.kind, nodeIds: [dependency.fromNodeId, dependency.toNodeId], dependency });
313
316
  }
314
- for (const access of declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges)) {
317
+ for (const access of showResourceDeclarations ? declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges) : []) {
315
318
  addRelation(representativeByResourceId.get(access.resource.id)!, representativeByNodeId.get(access.nodeId)!, access.resource.id,
316
319
  { role: 'declaration', nodeIds: [access.nodeId] });
317
320
  }
@@ -343,11 +346,13 @@ function createFrameFlowScene(
343
346
  : rangeLabel;
344
347
  const reasonLabel = root.reason.split('-').map((word) => word[0]!.toUpperCase() + word.slice(1)).join(' ');
345
348
  const disambiguate = rangesByRootFamily.get(JSON.stringify([resource.id, root.reason]))!.size > 1;
349
+ const initialLabel = !showResourceDeclarations && root.resolution?.usesInitialContents
350
+ ? '\n' + (root.resolution.producerNodeIds.length ? 'With initial contents' : 'Initial contents only') : '';
346
351
  rootNodes.push({
347
352
  id, kind: 'root', rootKey: root.key, resourceId: resource.id, resourceKind: resource.kind,
348
353
  label: reasonLabel + '\n' + formatGraphResourceLabel(labelResource(resource))
349
- + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary) : ''),
350
- overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource))),
354
+ + (disambiguate ? '\n' + formatGraphResourceLabel(rangeSummary, 1) : '') + initialLabel,
355
+ overviewLabel: reasonLabel + '\n' + formatGraphResourceLabel(shortGraphLabel(labelResource(resource)), 1) + initialLabel,
351
356
  title: labelResource(resource) + '\n' + root.reason + '\n' + rangeLabel
352
357
  + (root.resolution ? '\nProducers: ' + (root.resolution.producerNodeIds.join(', ') || 'none') + '\nInitial contents: ' + root.resolution.usesInitialContents : '\nOutput sources unavailable in Legacy capture.'),
353
358
  });
@@ -356,7 +361,7 @@ function createFrameFlowScene(
356
361
  for (const nodeId of root.resolution?.producerNodeIds ?? []) {
357
362
  addRelation(representativeByNodeId.get(nodeId)!, id, resource.id, { role: 'output-producer', nodeIds: [nodeId], rootKey: root.key });
358
363
  }
359
- if (root.resolution?.usesInitialContents) addRelation(representativeByResourceId.get(resource.id)!, id, resource.id,
364
+ if (showResourceDeclarations && root.resolution?.usesInitialContents) addRelation(representativeByResourceId.get(resource.id)!, id, resource.id,
360
365
  { role: 'output-initial', nodeIds: [], rootKey: root.key });
361
366
  }
362
367
  const edges: GraphSceneEdge[] = [...aggregates.entries()].map(([key, entry]) => {
@@ -479,20 +484,47 @@ function formatGraphNodeLabel(label: string): string {
479
484
  return formatGraphLabel(label, 20, 3);
480
485
  }
481
486
 
482
- function formatGraphResourceLabel(label: string): string {
483
- const singleLine = label.replace(/\s+/g, ' ').trim();
487
+ function formatGraphResourceLabel(label: string, maxLines = 2): string {
488
+ let remaining = [...label.replace(/\s+/g, ' ').trim()];
484
489
  // Reserve conservative monospace cells for CJK/emoji fallback glyphs as well.
485
- const glyphs = [...singleLine];
486
490
  const cells = (glyph: string) => /\p{Mark}/u.test(glyph) ? 0 : glyph.codePointAt(0)! > 255 ? 2 : 1;
487
- if (glyphs.reduce((sum, glyph) => sum + cells(glyph), 0) <= 18) return singleLine;
488
- let result = '';
489
- let width = 0;
490
- for (const glyph of glyphs) {
491
- if (width + cells(glyph) > 17) break;
492
- result += glyph;
493
- width += cells(glyph);
491
+ const take = (limit: number) => {
492
+ let width = 0;
493
+ let count = 0;
494
+ for (const glyph of remaining) {
495
+ if (width + cells(glyph) > limit) break;
496
+ width += cells(glyph);
497
+ count++;
498
+ }
499
+ return count;
500
+ };
501
+ const lines: string[] = [];
502
+ while (remaining.length) {
503
+ let count = take(18);
504
+ if (count === remaining.length) {
505
+ lines.push(remaining.join(''));
506
+ break;
507
+ }
508
+ if (lines.length === maxLines - 1) {
509
+ lines.push(remaining.slice(0, take(17)).join('').trimEnd() + '…');
510
+ break;
511
+ }
512
+ // Prefer a readable namespace/path boundary over splitting a word.
513
+ let boundary = 0;
514
+ for (const separators of [/[./]/u, /[_\-\s]/u]) {
515
+ for (let index = count - 1; index > 0; index--) {
516
+ if (separators.test(remaining[index]!)) {
517
+ boundary = index + 1;
518
+ break;
519
+ }
520
+ }
521
+ if (boundary) break;
522
+ }
523
+ if (boundary) count = boundary;
524
+ lines.push(remaining.slice(0, count).join('').trimEnd());
525
+ remaining = remaining.slice(count);
494
526
  }
495
- return result + '';
527
+ return lines.join('\n');
496
528
  }
497
529
 
498
530
  function formatGraphLabel(label: string, maxLineLength: number, maxLines: number): string {
@@ -574,10 +606,11 @@ function shortGraphLabel(label: string): string {
574
606
 
575
607
  function createGroupLabel(group: FrameGraphDebugGroup, collapsed: boolean): string {
576
608
  const summary = group.summary;
577
- const gpu = summary.timingEligibleNodeCount === 0
578
- ? 'no timed passes'
579
- : ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms ${summary.timedNodeCount}/${summary.timingEligibleNodeCount}`;
580
- return `${collapsed ? '▸' : '▾'} ${group.label} · ${summary.retainedNodeCount} retained · ${summary.culledNodeCount} culled · ${gpu}`;
609
+ const parts = [
610
+ `${collapsed ? '▸' : '▾'} ${group.label.replace(/\s+/g, ' ').trim()}`,
611
+ `${summary.retainedNodeCount} nodes` + (summary.culledNodeCount ? ` · ${summary.culledNodeCount} culled` : ''),
612
+ ];
613
+ return parts.join(collapsed ? '\n' : ' · ');
581
614
  }
582
615
 
583
616
  function createNodeTitle(
@@ -585,18 +618,22 @@ function createNodeTitle(
585
618
  segment: FrameGraphDebugViewModel['executionSegments'][number] | undefined,
586
619
  snapshot: FrameGraphDebugViewModel,
587
620
  ): string {
621
+ const gpu = node.kind === 'external-submission' ? 'Opaque'
622
+ : node.kind !== 'render' && node.kind !== 'compute' ? 'Not applicable'
623
+ : node.gpuDurationMicros === undefined ? 'Not collected' : `${formatGpuDuration(node)} ms`;
588
624
  return [
589
625
  labelNode(node),
590
626
  `kind: ${node.kind}`,
591
627
  `group: ${debugGroupPathForId(node.debugGroupId, snapshot)}`,
592
628
  `segment: ${segment ? `${segment.index}:${segment.kind}` : '-'}`,
593
- `gpu: ${node.kind === 'external-submission' ? 'opaque' : `${formatGpuDuration(node)} ms`}`,
629
+ `CPU duration: ${node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3) + ' ms'}`,
630
+ `GPU duration: ${gpu}`,
594
631
  `reads: ${node.reads.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
595
632
  `writes: ${node.writes.map((access) => labelResource(access.resource)).join(', ') || '-'}`,
596
633
  ].join('\n');
597
634
  }
598
635
 
599
- function createGroupTitle(group: FrameGraphDebugGroup): string {
636
+ function createGroupTitle(group: FrameGraphDebugGroup, snapshot: FrameGraphDebugViewModel): string {
600
637
  const summary = group.summary;
601
638
  return [
602
639
  group.path.join(' / '),
@@ -605,10 +642,11 @@ function createGroupTitle(group: FrameGraphDebugGroup): string {
605
642
  `inputs: ${summary.inputResources.map(labelResource).join(', ') || '-'}`,
606
643
  `outputs: ${summary.outputResources.map(labelResource).join(', ') || '-'}`,
607
644
  `transient registered/accessed: ${summary.registeredTransientResourceCount}/${summary.accessedTransientResourceCount}`,
608
- `physical allocations: ${summary.physicalAllocationCount}`,
645
+ `physical allocations: ${snapshot.protocol.memory.allocationReport.status === 'available' ? summary.physicalAllocationCount : 'Not collected'}`,
609
646
  `segments: ${summary.executionSegmentCount}`,
610
647
  `opaque: ${summary.externalSubmissionCount}`,
611
- GPU work: ${(summary.gpuWorkDurationMicros / 1000).toFixed(3)} ms (${summary.timedNodeCount}/${summary.timingEligibleNodeCount})`,
648
+ `CPU pass sum: ${summary.cpuTimedNodeCount ? (summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'} (${summary.cpuTimedNodeCount}/${summary.retainedNodeCount})`,
649
+ `GPU pass sum: ${formatMeasuredGpuWork(summary.gpuWorkDurationMicros, summary.timedNodeCount, summary.timingEligibleNodeCount)}`,
612
650
  ].join('\n');
613
651
  }
614
652