@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,55 +1,54 @@
1
- import type { FrameGraphDebugNode, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
- import { createCell, formatGpuDuration, labelNode } from './panelDomHelpers.ts';
1
+ import type { FrameGraphDebugGroup, FrameGraphDebugNode, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
+ import { createCell, labelNode } from './panelDomHelpers.ts';
3
3
  import type { PassesSubview, Selection } from './panelTypes.ts';
4
4
  import {
5
- createEmptyTableRow,
6
- createKindCell,
7
- createFilterSelect,
8
- createSearchInput,
9
- createSelectionCell,
10
- createTableScroller,
11
- createViewToolbar,
12
- groupPath,
13
- registerSelectable,
14
- type WorkbenchCallbacks,
15
- updateSelectedRows,
5
+ createEmptyTableRow, createKindCell, createFilterSelect, createSearchInput, createSelectionCell,
6
+ createTableScroller, createViewToolbar, enableTabKeyboard, formatMeasuredGpuWork, formatTimingCoverage,
7
+ groupPath, registerSelectable, selectionKey, type WorkbenchCallbacks, updateSelectedRows,
16
8
  } from './panelWorkbenchHelpers.ts';
17
9
 
18
- type PassSort = 'order' | 'label' | 'kind' | 'gpu';
10
+ type PassSort = 'order' | 'label' | 'kind' | 'gpu' | 'cpu';
11
+ type PassEntry = {
12
+ readonly node: Omit<FrameGraphDebugNode, 'order'>;
13
+ readonly selection: Selection;
14
+ readonly order?: number;
15
+ readonly recordingOrder: number;
16
+ };
19
17
 
20
18
  export class PassesView {
21
19
  readonly root = document.createElement('section');
22
20
  private readonly toolbar = createViewToolbar('Pass filters and sorting');
21
+ private readonly groupToolbar = createViewToolbar('Group search');
23
22
  private readonly tabList = document.createElement('div');
24
23
  private readonly listButton = this.createSubviewButton('list', 'Pass List');
25
24
  private readonly groupsButton = this.createSubviewButton('groups', 'Group Hierarchy');
26
25
  private readonly listPane = document.createElement('div');
27
26
  private readonly groupsPane = document.createElement('div');
28
27
  private readonly listTable = createTableScroller([
29
- { label: 'Order', column: 'numeric' },
30
- { label: 'Segment', column: 'code' },
31
- { label: 'Label' },
32
- { label: 'Group' },
33
- { label: 'Kind', column: 'kind' },
34
- { label: 'GPU (ms)', column: 'numeric' },
35
- { label: 'Reads', column: 'numeric' },
36
- { label: 'Writes', column: 'numeric' },
28
+ { label: 'Name' }, { label: 'Compile state', column: 'kind' }, { label: 'Order', column: 'numeric' },
29
+ { label: 'Kind', column: 'kind' }, { label: 'CPU (ms)', column: 'numeric' }, { label: 'GPU (ms)', column: 'numeric' }, { label: 'R / W', column: 'numeric' },
37
30
  ]);
38
31
  private readonly groupsTable = createTableScroller([
39
- { label: '' },
40
- { label: 'Group' },
41
- { label: 'Retained', column: 'numeric' },
42
- { label: 'Culled', column: 'numeric' },
43
- { label: 'GPU work', column: 'numeric' },
44
- { label: 'Inputs', column: 'numeric' },
45
- { label: 'Outputs', column: 'numeric' },
32
+ { label: 'Group' }, { label: 'Retained', column: 'numeric' }, { label: 'Culled', column: 'numeric' },
33
+ { label: 'CPU sum / coverage', column: 'numeric' }, { label: 'GPU pass sum', column: 'numeric' }, { label: 'Inputs', column: 'numeric' },
34
+ { label: 'Outputs', column: 'numeric' }, { label: 'Locate' },
46
35
  ]);
47
36
  private readonly rows = new Map<string, HTMLElement[]>();
37
+ private readonly collapsedGroups = new Set<string>();
38
+ private readonly count = document.createElement('span');
39
+ private readonly groupCount = document.createElement('span');
40
+ private readonly timing = document.createElement('p');
41
+ private readonly searchInput: HTMLInputElement;
42
+ private readonly groupSearchInput: HTMLInputElement;
43
+ private readonly kindSelect: HTMLSelectElement;
44
+ private readonly statusSelect: HTMLSelectElement;
48
45
  private snapshot: FrameGraphDebugViewModel | undefined;
49
46
  private selected: Selection | undefined;
50
47
  private subview: PassesSubview = 'list';
51
48
  private search = '';
49
+ private groupSearch = '';
52
50
  private kind = 'all';
51
+ private status = 'all';
53
52
  private sort: PassSort = 'order';
54
53
 
55
54
  constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
@@ -60,49 +59,47 @@ export class PassesView {
60
59
  this.tabList.setAttribute('role', 'tablist');
61
60
  this.tabList.setAttribute('aria-label', 'Pass views');
62
61
  this.tabList.append(this.listButton, this.groupsButton);
63
-
64
- const search = createSearchInput('Search pass or group', this.search, (value) => {
65
- this.search = value.trim().toLocaleLowerCase();
66
- this.renderRows();
67
- });
68
- const kind = createFilterSelect('Pass kind', this.kind, [
69
- ['all', 'All kinds'],
70
- ['render', 'Render'],
71
- ['compute', 'Compute'],
72
- ['copy', 'Copy'],
73
- ['clear-buffer', 'Clear Buffer'],
74
- ['command', 'Command'],
75
- ['external-submission', 'External'],
76
- ], (value) => {
77
- this.kind = value;
78
- this.renderRows();
62
+ enableTabKeyboard(this.tabList);
63
+ this.searchInput = createSearchInput('Search pass, ID or group', '', (value) => {
64
+ this.search = value.trim().toLocaleLowerCase(); this.renderRows();
79
65
  });
66
+ this.statusSelect = createFilterSelect('Pass compile state', this.status, [
67
+ ['all', 'All'], ['retained', 'Retained'], ['culled', 'Culled'],
68
+ ], (value) => { this.status = value; this.renderRows(); });
69
+ this.kindSelect = createFilterSelect('Pass kind', this.kind, [
70
+ ['all', 'All kinds'], ['render', 'Render'], ['compute', 'Compute'], ['copy', 'Copy'],
71
+ ['clear-buffer', 'Clear Buffer'], ['command', 'Command'], ['external-submission', 'External'],
72
+ ], (value) => { this.kind = value; this.renderRows(); });
80
73
  const sort = createFilterSelect('Sort passes', this.sort, [
81
- ['order', 'Order'],
82
- ['label', 'Label'],
83
- ['kind', 'Kind'],
84
- ['gpu', 'GPU'],
85
- ], (value) => {
86
- this.sort = value as PassSort;
87
- this.renderRows();
74
+ ['order', 'Execution order'], ['label', 'Name'], ['kind', 'Kind'], ['gpu', 'GPU: slowest first'], ['cpu', 'CPU: slowest first'],
75
+ ], (value) => { this.sort = value as PassSort; this.renderRows(); });
76
+ const clear = document.createElement('button');
77
+ clear.type = 'button'; clear.textContent = 'Clear filters';
78
+ clear.addEventListener('click', () => { this.clearFilters(); this.renderRows(); });
79
+ this.count.className = 'zenfg-inspector-result-count'; this.count.setAttribute('role', 'status');
80
+ this.toolbar.append(this.searchInput, this.statusSelect, this.kindSelect, sort, clear, this.count);
81
+ this.groupSearchInput = createSearchInput('Search group path or ID', '', (value) => {
82
+ this.groupSearch = value.trim().toLocaleLowerCase(); this.renderRows();
88
83
  });
89
- this.toolbar.append(search, kind, sort);
90
-
84
+ const clearGroups = document.createElement('button');
85
+ clearGroups.type = 'button'; clearGroups.textContent = 'Clear filters';
86
+ clearGroups.addEventListener('click', () => {
87
+ this.groupSearch = ''; this.groupSearchInput.value = ''; this.renderRows();
88
+ });
89
+ this.groupCount.className = 'zenfg-inspector-result-count'; this.groupCount.setAttribute('role', 'status');
90
+ this.groupToolbar.append(this.groupSearchInput, clearGroups, this.groupCount);
91
+ this.timing.className = 'zenfg-inspector-list-context';
91
92
  this.listButton.id = `${idPrefix}-list-subtab`;
92
93
  this.listButton.setAttribute('aria-controls', `${idPrefix}-pass-list-panel`);
93
94
  this.groupsButton.id = `${idPrefix}-groups-subtab`;
94
95
  this.groupsButton.setAttribute('aria-controls', `${idPrefix}-group-list-panel`);
95
- this.listPane.id = `${idPrefix}-pass-list-panel`;
96
- this.listPane.className = 'zenfg-inspector-subview';
97
- this.listPane.setAttribute('role', 'tabpanel');
98
- this.listPane.setAttribute('aria-labelledby', this.listButton.id);
96
+ this.listPane.id = `${idPrefix}-pass-list-panel`; this.listPane.className = 'zenfg-inspector-subview';
97
+ this.listPane.setAttribute('role', 'tabpanel'); this.listPane.setAttribute('aria-labelledby', this.listButton.id);
99
98
  this.listPane.appendChild(this.listTable.scroller);
100
- this.groupsPane.id = `${idPrefix}-group-list-panel`;
101
- this.groupsPane.className = 'zenfg-inspector-subview';
102
- this.groupsPane.setAttribute('role', 'tabpanel');
103
- this.groupsPane.setAttribute('aria-labelledby', this.groupsButton.id);
99
+ this.groupsPane.id = `${idPrefix}-group-list-panel`; this.groupsPane.className = 'zenfg-inspector-subview';
100
+ this.groupsPane.setAttribute('role', 'tabpanel'); this.groupsPane.setAttribute('aria-labelledby', this.groupsButton.id);
104
101
  this.groupsPane.appendChild(this.groupsTable.scroller);
105
- this.root.append(this.tabList, this.toolbar, this.listPane, this.groupsPane);
102
+ this.root.append(this.tabList, this.toolbar, this.groupToolbar, this.timing, this.listPane, this.groupsPane);
106
103
  this.updateSubview();
107
104
  }
108
105
 
@@ -110,113 +107,169 @@ export class PassesView {
110
107
  this.snapshot = snapshot;
111
108
  if (snapshot.debugGroups.length === 0 && this.subview === 'groups') this.subview = 'list';
112
109
  this.groupsButton.disabled = snapshot.debugGroups.length === 0;
113
- this.renderRows();
114
- this.updateSubview();
110
+ for (const path of this.collapsedGroups) if (!snapshot.groupByPathKey.has(path)) this.collapsedGroups.delete(path);
111
+ this.renderRows(); this.updateSubview();
115
112
  }
116
113
 
117
114
  setSelection(selected: Selection | undefined): void {
118
- this.selected = selected;
119
- updateSelectedRows(this.rows, selected);
115
+ this.selected = selected; updateSelectedRows(this.rows, selected);
116
+ }
117
+
118
+ showCulled(): void {
119
+ this.clearFilters(); this.status = 'culled'; this.statusSelect.value = 'culled'; this.subview = 'list';
120
+ this.updateSubview(); this.renderRows();
121
+ }
122
+
123
+ reveal(selection: Selection): void {
124
+ if (selection.kind === 'group') {
125
+ this.subview = 'groups'; this.groupSearch = ''; this.groupSearchInput.value = '';
126
+ const group = this.snapshot?.groupByPathKey.get(selection.pathKey);
127
+ for (const id of group?.ancestorIds ?? []) {
128
+ const ancestor = this.snapshot?.groupById.get(id);
129
+ if (ancestor) this.collapsedGroups.delete(ancestor.pathKey);
130
+ }
131
+ } else if (selection.kind === 'node' || selection.kind === 'culled') {
132
+ this.subview = 'list'; this.clearFilters();
133
+ } else return;
134
+ this.updateSubview(); this.renderRows();
135
+ this.rows.get(selectionKey(selection))?.[0]?.scrollIntoView?.({ block: 'nearest' });
136
+ }
137
+
138
+ private clearFilters(): void {
139
+ this.search = ''; this.kind = 'all'; this.status = 'all';
140
+ this.searchInput.value = ''; this.kindSelect.value = 'all'; this.statusSelect.value = 'all';
120
141
  }
121
142
 
122
143
  private createSubviewButton(subview: PassesSubview, label: string): HTMLButtonElement {
123
- const button = document.createElement('button');
124
- button.type = 'button';
125
- button.textContent = label;
144
+ const button = document.createElement('button'); button.type = 'button'; button.textContent = label;
126
145
  button.setAttribute('role', 'tab');
127
- button.addEventListener('click', () => {
128
- this.subview = subview;
129
- this.updateSubview();
130
- });
146
+ button.addEventListener('click', () => { this.subview = subview; this.updateSubview(); });
131
147
  return button;
132
148
  }
133
149
 
134
150
  private updateSubview(): void {
135
151
  const listActive = this.subview === 'list';
136
- this.listButton.setAttribute('aria-selected', listActive ? 'true' : 'false');
137
- this.groupsButton.setAttribute('aria-selected', listActive ? 'false' : 'true');
138
- this.listButton.classList.toggle('active', listActive);
139
- this.groupsButton.classList.toggle('active', !listActive);
140
- this.listPane.hidden = !listActive;
141
- this.groupsPane.hidden = listActive;
152
+ this.listButton.setAttribute('aria-selected', String(listActive));
153
+ this.groupsButton.setAttribute('aria-selected', String(!listActive));
154
+ this.listButton.tabIndex = listActive ? 0 : -1; this.groupsButton.tabIndex = listActive ? -1 : 0;
155
+ this.listButton.classList.toggle('active', listActive); this.groupsButton.classList.toggle('active', !listActive);
156
+ this.listPane.hidden = !listActive; this.groupsPane.hidden = listActive;
157
+ this.toolbar.hidden = !listActive; this.groupToolbar.hidden = listActive; this.timing.hidden = !listActive;
142
158
  }
143
159
 
144
160
  private renderRows(): void {
145
161
  const snapshot = this.snapshot;
146
162
  if (!snapshot) return;
147
- this.rows.clear();
148
- this.listTable.body.replaceChildren();
149
- this.groupsTable.body.replaceChildren();
150
-
151
- const passes = snapshot.nodes.filter((node) => this.matchesNode(snapshot, node));
152
- passes.sort((a, b) => this.compareNodes(a, b));
153
- for (const node of passes) {
154
- const selection: Selection = { kind: 'node', id: node.id };
155
- const row = document.createElement('tr');
156
- registerSelectable(this.rows, row, selection, this.callbacks);
157
- const segment = snapshot.segmentByNodeId.get(node.id);
158
- row.append(
159
- createCell(String(node.order), { column: 'numeric' }),
160
- createCell(segment ? `${segment.index}:${segment.kind === 'frame-graph' ? 'FG' : 'opaque'}` : '-', { column: 'code' }),
161
- createSelectionCell(labelNode(node), selection, this.callbacks),
162
- createCell(groupPath(snapshot, node.debugGroupId), { column: 'code' }),
163
- createKindCell(node.kind),
164
- createCell(node.kind === 'external-submission' ? 'opaque' : formatGpuDuration(node), { column: 'numeric' }),
165
- createCell(String(node.reads.length), { column: 'numeric' }),
166
- createCell(String(node.writes.length), { column: 'numeric' }),
167
- );
163
+ this.rows.clear(); this.listTable.body.replaceChildren(); this.groupsTable.body.replaceChildren();
164
+ const entries = snapshot.protocol.graph.nodes.flatMap((canonical, index): PassEntry[] => {
165
+ const node = snapshot.nodeById.get(canonical.id) ?? snapshot.culledById.get(canonical.id)?.node;
166
+ if (!node) return [];
167
+ return [{ node,
168
+ selection: { kind: canonical.compileState.status === 'retained' ? 'node' : 'culled', id: node.id },
169
+ order: canonical.compileState.status === 'retained' ? canonical.compileState.executionOrder : undefined,
170
+ recordingOrder: canonical.recordingOrder ?? index,
171
+ }];
172
+ });
173
+ const passes = entries.filter((entry) => this.matchesNode(snapshot, entry));
174
+ passes.sort((a, b) => this.compareNodes(a, b)); this.count.textContent = `${passes.length} / ${entries.length} passes`;
175
+ const metrics = snapshot.metrics;
176
+ this.timing.textContent = `Retained means kept by compilation. CPU timing: ${formatTimingCoverage(metrics.cpuTimedNodeCount, snapshot.nodes.length)}. GPU timing: ${formatTimingCoverage(metrics.timedNodeCount, metrics.timingEligibleNodeCount)}${snapshot.profiling.status === 'unavailable' ? ` (${snapshot.profiling.reason})` : ''}.`;
177
+ for (const { node, selection, order } of passes) {
178
+ const row = document.createElement('tr'); registerSelectable(this.rows, row, selection, this.callbacks);
179
+ const labelCell = createSelectionCell(labelNode(node), selection, this.callbacks); labelCell.title = node.id;
180
+ if (node.debugGroupId !== undefined) {
181
+ const group = document.createElement('small'); group.textContent = groupPath(snapshot, node.debugGroupId); labelCell.appendChild(group);
182
+ }
183
+ const stateCell = createKindCell(selection.kind === 'culled' ? 'culled' : 'retained', selection.kind === 'culled' ? 'Culled' : 'Retained');
184
+ stateCell.title = selection.kind === 'culled' ? snapshot.culledById.get(node.id)?.reason ?? '' : 'Kept by compilation; this does not prove GPU execution.';
185
+ const gpu = selection.kind === 'culled' ? 'Not applicable' : node.kind === 'external-submission' ? 'Opaque'
186
+ : node.gpuDurationMicros !== undefined ? (node.gpuDurationMicros / 1000).toFixed(3)
187
+ : node.kind === 'render' || node.kind === 'compute' ? 'Not collected' : 'Not applicable';
188
+ const cpu = selection.kind === 'culled' ? 'Not executed' : node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3);
189
+ const cpuCell = createCell(cpu, { column: 'numeric' });
190
+ cpuCell.title = 'Synchronous elapsed time; zero may reflect clock precision. External nodes include synchronous submission.';
191
+ row.append(labelCell, stateCell, createCell(order === undefined ? 'Not applicable' : String(order), { column: 'numeric' }),
192
+ createKindCell(node.kind), cpuCell, createCell(gpu, { column: 'numeric' }), createCell(`${node.reads.length} / ${node.writes.length}`, { column: 'numeric' }));
168
193
  this.listTable.body.appendChild(row);
169
194
  }
170
- if (passes.length === 0) this.listTable.body.appendChild(createEmptyTableRow(8, 'No passes match the current filters.'));
195
+ if (passes.length === 0) this.listTable.body.appendChild(createEmptyTableRow(7, 'No passes match the current filters.'));
196
+ this.renderGroups(snapshot); updateSelectedRows(this.rows, this.selected);
197
+ }
171
198
 
172
- const query = this.search;
173
- for (const group of snapshot.debugGroups.filter((candidate) => (
174
- query.length === 0 || candidate.path.join(' / ').toLocaleLowerCase().includes(query)
175
- ))) {
199
+ private renderGroups(snapshot: FrameGraphDebugViewModel): void {
200
+ const included = new Set<string>();
201
+ const childrenByParent = new Map<string | undefined, FrameGraphDebugGroup[]>();
202
+ for (const group of snapshot.debugGroups) {
203
+ const siblings = childrenByParent.get(group.parentId) ?? [];
204
+ siblings.push(group);
205
+ childrenByParent.set(group.parentId, siblings);
206
+ }
207
+ // Parent-before-child capture order need not be depth-first; keep each subtree together.
208
+ const orderedGroups: FrameGraphDebugGroup[] = [];
209
+ const pending = [...(childrenByParent.get(undefined) ?? [])].reverse();
210
+ while (pending.length) {
211
+ const group = pending.pop()!;
212
+ orderedGroups.push(group);
213
+ pending.push(...[...(childrenByParent.get(group.id) ?? [])].reverse());
214
+ }
215
+ const parents = new Set(childrenByParent.keys());
216
+ if (this.groupSearch) for (const group of snapshot.debugGroups) {
217
+ if (`${group.path.join(' / ')} ${group.id}`.toLocaleLowerCase().includes(this.groupSearch)) {
218
+ for (const id of group.ancestorIds) included.add(id);
219
+ }
220
+ }
221
+ const groups = orderedGroups.filter((group) => this.groupSearch ? included.has(group.id)
222
+ : !group.ancestorIds.slice(0, -1).some((id) => this.collapsedGroups.has(snapshot.groupById.get(id)!.pathKey)));
223
+ this.groupCount.textContent = `${groups.length} / ${snapshot.debugGroups.length} groups`;
224
+ for (const group of groups) {
176
225
  const selection: Selection = { kind: 'group', pathKey: group.pathKey };
177
- const row = document.createElement('tr');
178
- registerSelectable(this.rows, row, selection, this.callbacks);
179
- const toggleCell = document.createElement('td');
180
- const toggle = document.createElement('button');
181
- toggle.type = 'button';
182
- toggle.className = 'zenfg-inspector-group-toggle';
183
- const expanded = this.callbacks.isGroupExpanded(group.pathKey);
184
- toggle.textContent = expanded ? '▾' : '▸';
185
- toggle.title = 'Toggle group in Graph view';
186
- toggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} group ${group.path.join(' / ')}`);
187
- toggle.addEventListener('click', () => this.callbacks.onGroupToggle(group.pathKey));
188
- toggleCell.appendChild(toggle);
226
+ const row = document.createElement('tr'); registerSelectable(this.rows, row, selection, this.callbacks);
189
227
  const labelCell = createSelectionCell(group.label, selection, this.callbacks);
190
- labelCell.style.paddingLeft = `${8 + group.depth * 18}px`;
228
+ labelCell.style.paddingLeft = `${8 + group.depth * 18}px`; labelCell.title = group.path.join(' / ');
229
+ const toggle = document.createElement('button'); toggle.type = 'button'; toggle.className = 'zenfg-inspector-group-toggle';
230
+ const expanded = this.groupSearch.length > 0 || !this.collapsedGroups.has(group.pathKey);
231
+ toggle.textContent = parents.has(group.id) ? expanded ? '▾' : '▸' : '·';
232
+ toggle.disabled = !parents.has(group.id) || this.groupSearch.length > 0;
233
+ toggle.title = this.groupSearch ? 'Matching groups and ancestors are expanded while searching.' : 'Toggle this group hierarchy';
234
+ toggle.setAttribute('aria-label', `${expanded ? 'Collapse' : 'Expand'} group ${group.path.join(' / ')}`);
235
+ if (parents.has(group.id)) toggle.setAttribute('aria-expanded', String(expanded));
236
+ toggle.addEventListener('click', () => {
237
+ if (expanded) this.collapsedGroups.add(group.pathKey); else this.collapsedGroups.delete(group.pathKey);
238
+ this.renderRows();
239
+ this.rows.get(selectionKey(selection))?.[0]?.querySelector<HTMLButtonElement>('.zenfg-inspector-group-toggle')?.focus({ preventScroll: true });
240
+ });
241
+ labelCell.prepend(toggle);
191
242
  const summary = group.summary;
192
- row.append(
193
- toggleCell,
194
- labelCell,
195
- createCell(String(summary.retainedNodeCount), { column: 'numeric' }),
243
+ const revealCell = document.createElement('td'); const reveal = document.createElement('button');
244
+ reveal.type = 'button'; reveal.className = 'zenfg-inspector-relation-button'; reveal.textContent = 'Show in Graph';
245
+ reveal.addEventListener('click', () => this.callbacks.onReveal?.(selection, 'graph')); revealCell.appendChild(reveal);
246
+ row.append(labelCell, createCell(String(summary.retainedNodeCount), { column: 'numeric' }),
196
247
  createCell(String(summary.culledNodeCount), { column: 'numeric' }),
197
- createCell(summary.timedNodeCount === 0 ? '-' : (summary.gpuWorkDurationMicros / 1000).toFixed(3), { column: 'numeric' }),
248
+ createCell((summary.cpuTimedNodeCount ? (summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected') + ' · ' + formatTimingCoverage(summary.cpuTimedNodeCount, summary.retainedNodeCount), { column: 'numeric' }),
249
+ createCell(formatMeasuredGpuWork(summary.gpuWorkDurationMicros, summary.timedNodeCount, summary.timingEligibleNodeCount), { column: 'numeric' }),
198
250
  createCell(String(summary.inputResources.length), { column: 'numeric' }),
199
- createCell(String(summary.outputResources.length), { column: 'numeric' }),
200
- );
251
+ createCell(String(summary.outputResources.length), { column: 'numeric' }), revealCell);
201
252
  this.groupsTable.body.appendChild(row);
202
253
  }
203
- if (this.groupsTable.body.childElementCount === 0) this.groupsTable.body.appendChild(createEmptyTableRow(7, 'No diagnostic groups.'));
204
- updateSelectedRows(this.rows, this.selected);
254
+ if (groups.length === 0) this.groupsTable.body.appendChild(createEmptyTableRow(8, 'No groups match the current filters.'));
205
255
  }
206
256
 
207
- private matchesNode(snapshot: FrameGraphDebugViewModel, node: FrameGraphDebugNode): boolean {
257
+ private matchesNode(snapshot: FrameGraphDebugViewModel, entry: PassEntry): boolean {
258
+ const { node, selection } = entry;
259
+ if (this.status === 'retained' && selection.kind !== 'node' || this.status === 'culled' && selection.kind !== 'culled') return false;
208
260
  if (this.kind !== 'all' && node.kind !== this.kind) return false;
209
- if (this.search.length === 0) return true;
210
- const haystack = `${labelNode(node)} ${groupPath(snapshot, node.debugGroupId)}`.toLocaleLowerCase();
211
- return haystack.includes(this.search);
261
+ return !this.search || `${labelNode(node)} ${node.id} ${groupPath(snapshot, node.debugGroupId)}`.toLocaleLowerCase().includes(this.search);
212
262
  }
213
263
 
214
- private compareNodes(a: FrameGraphDebugNode, b: FrameGraphDebugNode): number {
264
+ private compareNodes(a: PassEntry, b: PassEntry): number {
265
+ const order = () => a.order === undefined ? b.order === undefined ? a.recordingOrder - b.recordingOrder : 1
266
+ : b.order === undefined ? -1 : a.order - b.order;
215
267
  switch (this.sort) {
216
- case 'label': return labelNode(a).localeCompare(labelNode(b)) || a.order - b.order;
217
- case 'kind': return a.kind.localeCompare(b.kind) || a.order - b.order;
218
- case 'gpu': return (b.gpuDurationMicros ?? -1) - (a.gpuDurationMicros ?? -1) || a.order - b.order;
219
- case 'order': return a.order - b.order;
268
+ case 'label': return labelNode(a.node).localeCompare(labelNode(b.node)) || order();
269
+ case 'kind': return a.node.kind.localeCompare(b.node.kind) || order();
270
+ case 'cpu': return (b.node.cpuDurationMicros ?? -1) - (a.node.cpuDurationMicros ?? -1) || order();
271
+ case 'gpu': return (b.node.gpuDurationMicros ?? -1) - (a.node.gpuDurationMicros ?? -1) || order();
272
+ case 'order': return order();
220
273
  }
221
274
  }
222
275
  }
@@ -0,0 +1,99 @@
1
+ import type { SelectedCanonicalDetail } from './panelSelection.ts';
2
+ import { createSearchInput, writeClipboardText } from './panelWorkbenchHelpers.ts';
3
+
4
+ /** Created only when Raw is visited and retained for the current object/capture. */
5
+ export class RawDetailView {
6
+ readonly root = document.createElement('div');
7
+ private readonly tree = document.createElement('div');
8
+ private readonly status = document.createElement('span');
9
+ private readonly expanded = new Set<string>(['$']);
10
+ private query = '';
11
+ private readonly json: string;
12
+
13
+ constructor(private readonly detail: SelectedCanonicalDetail, migrated: boolean) {
14
+ this.root.className = 'zenfg-inspector-raw-view';
15
+ this.json = JSON.stringify(detail.value, null, 2);
16
+ const path = document.createElement('code');
17
+ path.className = 'zenfg-inspector-raw-path';
18
+ path.textContent = detail.path;
19
+ const toolbar = document.createElement('div');
20
+ toolbar.className = 'zenfg-inspector-raw-toolbar';
21
+ const search = createSearchInput('Search Raw fields or values', '', (value) => {
22
+ this.query = value.trim().toLocaleLowerCase();
23
+ this.renderTree();
24
+ });
25
+ search.addEventListener('keydown', (event) => {
26
+ if (event.key !== 'Escape' || !search.value) return;
27
+ search.value = '';
28
+ this.query = '';
29
+ this.renderTree();
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ });
33
+ const copy = document.createElement('button');
34
+ copy.type = 'button';
35
+ copy.textContent = 'Copy object';
36
+ copy.title = 'Copy the complete canonical object, including fields hidden by search';
37
+ copy.addEventListener('click', () => {
38
+ void writeClipboardText(this.json).then(() => {
39
+ this.status.textContent = 'Copied complete object';
40
+ }, () => {
41
+ this.status.textContent = 'Copy failed. Select the Raw fields to copy manually.';
42
+ });
43
+ });
44
+ this.status.setAttribute('role', 'status');
45
+ this.status.className = 'zenfg-inspector-muted';
46
+ toolbar.append(search, copy);
47
+ this.tree.className = 'zenfg-inspector-raw-detail';
48
+ this.tree.setAttribute('aria-label', 'Canonical object fields');
49
+ this.root.append(path, toolbar, this.status);
50
+ if (migrated) {
51
+ const note = document.createElement('p');
52
+ note.className = 'zenfg-inspector-muted';
53
+ note.textContent = 'Legacy import: showing the migrated canonical Snapshot object.';
54
+ this.root.appendChild(note);
55
+ }
56
+ this.root.appendChild(this.tree);
57
+ this.renderTree();
58
+ }
59
+
60
+ private renderTree(): void {
61
+ this.tree.replaceChildren();
62
+ const node = this.createValue('$', this.detail.value, '$', false);
63
+ if (node) this.tree.appendChild(node);
64
+ else this.tree.textContent = 'No matching fields or values.';
65
+ }
66
+
67
+ private createValue(key: string, value: unknown, path: string, includeChildren: boolean): HTMLElement | undefined {
68
+ const keyMatches = key.toLocaleLowerCase().includes(this.query);
69
+ const matched = !this.query || includeChildren || keyMatches;
70
+ if (typeof value !== 'object' || value === null) {
71
+ const literal = JSON.stringify(value) ?? 'undefined';
72
+ if (!matched && !literal.toLocaleLowerCase().includes(this.query)) return undefined;
73
+ const leaf = document.createElement('div');
74
+ leaf.className = 'zenfg-inspector-raw-leaf';
75
+ leaf.textContent = `${key}: ${literal}`;
76
+ return leaf;
77
+ }
78
+ const entries = Object.entries(value);
79
+ const children = entries.flatMap(([childKey, childValue]) => {
80
+ const child = this.createValue(childKey, childValue, `${path}.${childKey}`, matched);
81
+ return child ? [child] : [];
82
+ });
83
+ if (this.query && !matched && children.length === 0) return undefined;
84
+ const group = document.createElement('details');
85
+ const summary = document.createElement('summary');
86
+ summary.textContent = `${key}: ${Array.isArray(value) ? `[${entries.length} items]` : `{${entries.length} fields}`}`;
87
+ group.open = Boolean(this.query) || this.expanded.has(path);
88
+ group.addEventListener('toggle', () => {
89
+ if (this.query) return;
90
+ if (group.open) this.expanded.add(path);
91
+ else this.expanded.delete(path);
92
+ });
93
+ const body = document.createElement('div');
94
+ body.className = 'zenfg-inspector-raw-children';
95
+ body.append(...children);
96
+ group.append(summary, body);
97
+ return group;
98
+ }
99
+ }