@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.
- package/GUIDE.md +182 -0
- package/README.md +61 -89
- package/THEMING.md +124 -0
- package/dist/FrameGraphInspector.d.ts +23 -4
- package/dist/FrameGraphInspector.d.ts.map +1 -1
- package/dist/FrameGraphInspector.js +137 -29
- package/dist/FrameGraphInspector.js.map +1 -1
- package/dist/capture.d.ts +5 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +2 -0
- package/dist/capture.js.map +1 -0
- package/dist/debugCaptureModel.d.ts +19 -0
- package/dist/debugCaptureModel.d.ts.map +1 -1
- package/dist/debugCaptureModel.js +36 -2
- package/dist/debugCaptureModel.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/panelAliasAnalysis.d.ts +1 -0
- package/dist/panelAliasAnalysis.d.ts.map +1 -1
- package/dist/panelAliasAnalysis.js +23 -54
- package/dist/panelAliasAnalysis.js.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.d.ts +15 -2
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.js +145 -23
- package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
- package/dist/panelDetailLayout.d.ts +27 -0
- package/dist/panelDetailLayout.d.ts.map +1 -0
- package/dist/panelDetailLayout.js +174 -0
- package/dist/panelDetailLayout.js.map +1 -0
- package/dist/panelDetailStyles.d.ts +2 -0
- package/dist/panelDetailStyles.d.ts.map +1 -0
- package/dist/panelDetailStyles.js +57 -0
- package/dist/panelDetailStyles.js.map +1 -0
- package/dist/panelDiagnosticsView.d.ts +10 -1
- package/dist/panelDiagnosticsView.d.ts.map +1 -1
- package/dist/panelDiagnosticsView.js +166 -57
- package/dist/panelDiagnosticsView.js.map +1 -1
- package/dist/panelDomHelpers.d.ts.map +1 -1
- package/dist/panelDomHelpers.js +6 -5
- package/dist/panelDomHelpers.js.map +1 -1
- package/dist/panelGraphLayout.d.ts +3 -2
- package/dist/panelGraphLayout.d.ts.map +1 -1
- package/dist/panelGraphLayout.js +6 -5
- package/dist/panelGraphLayout.js.map +1 -1
- package/dist/panelGraphRenderer.d.ts +10 -0
- package/dist/panelGraphRenderer.d.ts.map +1 -1
- package/dist/panelGraphRenderer.js.map +1 -1
- package/dist/panelGraphScene.d.ts +1 -0
- package/dist/panelGraphScene.d.ts.map +1 -1
- package/dist/panelGraphScene.js +69 -32
- package/dist/panelGraphScene.js.map +1 -1
- package/dist/panelGraphSearch.d.ts +16 -0
- package/dist/panelGraphSearch.d.ts.map +1 -0
- package/dist/panelGraphSearch.js +93 -0
- package/dist/panelGraphSearch.js.map +1 -0
- package/dist/panelGraphView.d.ts +82 -0
- package/dist/panelGraphView.d.ts.map +1 -1
- package/dist/panelGraphView.js +15 -4
- package/dist/panelGraphView.js.map +1 -1
- package/dist/panelGraphVisuals.d.ts +14 -6
- package/dist/panelGraphVisuals.d.ts.map +1 -1
- package/dist/panelGraphVisuals.js +126 -51
- package/dist/panelGraphVisuals.js.map +1 -1
- package/dist/panelInspectorView.d.ts +8 -0
- package/dist/panelInspectorView.d.ts.map +1 -1
- package/dist/panelInspectorView.js +242 -48
- package/dist/panelInspectorView.js.map +1 -1
- package/dist/panelListStyles.d.ts +3 -0
- package/dist/panelListStyles.d.ts.map +1 -0
- package/dist/panelListStyles.js +155 -0
- package/dist/panelListStyles.js.map +1 -0
- package/dist/panelMemoryStyles.d.ts +3 -0
- package/dist/panelMemoryStyles.d.ts.map +1 -0
- package/dist/panelMemoryStyles.js +42 -0
- package/dist/panelMemoryStyles.js.map +1 -0
- package/dist/panelMemoryView.d.ts +13 -1
- package/dist/panelMemoryView.d.ts.map +1 -1
- package/dist/panelMemoryView.js +161 -41
- package/dist/panelMemoryView.js.map +1 -1
- package/dist/panelPassesView.d.ts +15 -0
- package/dist/panelPassesView.d.ts.map +1 -1
- package/dist/panelPassesView.js +202 -72
- package/dist/panelPassesView.js.map +1 -1
- package/dist/panelRawView.d.ts +15 -0
- package/dist/panelRawView.d.ts.map +1 -0
- package/dist/panelRawView.js +105 -0
- package/dist/panelRawView.js.map +1 -0
- package/dist/panelResourcesView.d.ts +8 -0
- package/dist/panelResourcesView.d.ts.map +1 -1
- package/dist/panelResourcesView.js +62 -31
- package/dist/panelResourcesView.js.map +1 -1
- package/dist/panelSelection.d.ts +9 -1
- package/dist/panelSelection.d.ts.map +1 -1
- package/dist/panelSelection.js +34 -4
- package/dist/panelSelection.js.map +1 -1
- package/dist/panelTheme.d.ts +13 -0
- package/dist/panelTheme.d.ts.map +1 -0
- package/dist/panelTheme.js +104 -0
- package/dist/panelTheme.js.map +1 -0
- package/dist/panelTypes.d.ts +10 -1
- package/dist/panelTypes.d.ts.map +1 -1
- package/dist/panelTypes.js.map +1 -1
- package/dist/panelVisualTheme.d.ts +147 -50
- package/dist/panelVisualTheme.d.ts.map +1 -1
- package/dist/panelVisualTheme.js +34 -44
- package/dist/panelVisualTheme.js.map +1 -1
- package/dist/panelWorkbenchHelpers.d.ts +12 -1
- package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
- package/dist/panelWorkbenchHelpers.js +62 -1
- package/dist/panelWorkbenchHelpers.js.map +1 -1
- package/dist/panelWorkbenchStyles.d.ts +2 -0
- package/dist/panelWorkbenchStyles.d.ts.map +1 -0
- package/dist/panelWorkbenchStyles.js +101 -0
- package/dist/panelWorkbenchStyles.js.map +1 -0
- package/dist/panelWorkbenchView.d.ts +16 -0
- package/dist/panelWorkbenchView.d.ts.map +1 -1
- package/dist/panelWorkbenchView.js +228 -50
- package/dist/panelWorkbenchView.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +147 -195
- package/dist/styles.js.map +1 -1
- package/dist/theme.d.ts +22 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +10 -0
- package/dist/theme.js.map +1 -0
- package/dist/themeDefinitions.d.ts +251 -0
- package/dist/themeDefinitions.d.ts.map +1 -0
- package/dist/themeDefinitions.js +96 -0
- package/dist/themeDefinitions.js.map +1 -0
- package/dist/themePalette.d.ts +66 -0
- package/dist/themePalette.d.ts.map +1 -0
- package/dist/themePalette.js +37 -0
- package/dist/themePalette.js.map +1 -0
- package/dist/themes.css +165 -0
- package/package.json +44 -10
- package/src/FrameGraphInspector.ts +130 -31
- package/src/capture.ts +4 -0
- package/src/debugCaptureModel.ts +53 -2
- package/src/index.ts +6 -2
- package/src/panelAliasAnalysis.ts +24 -70
- package/src/panelCytoscapeGraphRenderer.ts +140 -23
- package/src/panelDetailLayout.ts +155 -0
- package/src/panelDetailStyles.ts +56 -0
- package/src/panelDiagnosticsView.ts +166 -85
- package/src/panelDomHelpers.ts +4 -5
- package/src/panelGraphLayout.ts +6 -5
- package/src/panelGraphRenderer.ts +7 -0
- package/src/panelGraphScene.ts +68 -30
- package/src/panelGraphSearch.ts +96 -0
- package/src/panelGraphView.ts +14 -4
- package/src/panelGraphVisuals.ts +122 -52
- package/src/panelInspectorView.ts +227 -48
- package/src/panelListStyles.ts +154 -0
- package/src/panelMemoryStyles.ts +41 -0
- package/src/panelMemoryView.ts +178 -53
- package/src/panelPassesView.ts +190 -137
- package/src/panelRawView.ts +99 -0
- package/src/panelResourcesView.ts +69 -56
- package/src/panelSelection.ts +37 -5
- package/src/panelTheme.ts +85 -0
- package/src/panelTypes.ts +7 -1
- package/src/panelVisualTheme.ts +38 -49
- package/src/panelWorkbenchHelpers.ts +60 -2
- package/src/panelWorkbenchStyles.ts +100 -0
- package/src/panelWorkbenchView.ts +201 -52
- package/src/query.ts +11 -11
- package/src/styles.ts +148 -195
- package/src/theme.ts +24 -0
- package/src/themeDefinitions.ts +103 -0
- package/src/themePalette.ts +44 -0
package/src/panelPassesView.ts
CHANGED
|
@@ -1,55 +1,54 @@
|
|
|
1
|
-
import type { FrameGraphDebugNode, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
|
|
2
|
-
import { createCell,
|
|
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
|
-
|
|
7
|
-
|
|
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: '
|
|
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: '
|
|
41
|
-
{ label: '
|
|
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
|
-
|
|
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', '
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
137
|
-
this.groupsButton.setAttribute('aria-selected', listActive
|
|
138
|
-
this.listButton.
|
|
139
|
-
this.groupsButton.classList.toggle('active', !listActive);
|
|
140
|
-
this.listPane.hidden = !listActive;
|
|
141
|
-
this.
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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(
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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.
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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)) ||
|
|
217
|
-
case 'kind': return a.kind.localeCompare(b.kind) ||
|
|
218
|
-
case '
|
|
219
|
-
case '
|
|
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
|
+
}
|