@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.3
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/README.md +63 -1
- package/dist/FrameGraphInspector.d.ts +2 -5
- package/dist/FrameGraphInspector.d.ts.map +1 -1
- package/dist/FrameGraphInspector.js +16 -38
- package/dist/FrameGraphInspector.js.map +1 -1
- package/dist/debugCaptureModel.d.ts +9 -4
- package/dist/debugCaptureModel.d.ts.map +1 -1
- package/dist/debugCaptureModel.js +36 -14
- package/dist/debugCaptureModel.js.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.d.ts +3 -1
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.js +39 -19
- package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
- package/dist/panelDiagnosticsView.js +2 -2
- package/dist/panelDiagnosticsView.js.map +1 -1
- package/dist/panelGraphLayout.d.ts.map +1 -1
- package/dist/panelGraphLayout.js +15 -1
- package/dist/panelGraphLayout.js.map +1 -1
- package/dist/panelGraphScene.d.ts +14 -26
- package/dist/panelGraphScene.d.ts.map +1 -1
- package/dist/panelGraphScene.js +141 -195
- package/dist/panelGraphScene.js.map +1 -1
- package/dist/panelGraphView.d.ts.map +1 -1
- package/dist/panelGraphView.js +31 -9
- package/dist/panelGraphView.js.map +1 -1
- package/dist/panelGraphVisuals.d.ts +7 -3
- package/dist/panelGraphVisuals.d.ts.map +1 -1
- package/dist/panelGraphVisuals.js +45 -80
- package/dist/panelGraphVisuals.js.map +1 -1
- package/dist/panelInspectorView.d.ts +5 -0
- package/dist/panelInspectorView.d.ts.map +1 -1
- package/dist/panelInspectorView.js +99 -13
- package/dist/panelInspectorView.js.map +1 -1
- package/dist/panelSelection.js +2 -2
- package/dist/panelSelection.js.map +1 -1
- package/dist/panelTypes.d.ts +8 -3
- package/dist/panelTypes.d.ts.map +1 -1
- package/dist/panelTypes.js.map +1 -1
- package/dist/panelVisualTheme.d.ts +21 -13
- package/dist/panelVisualTheme.d.ts.map +1 -1
- package/dist/panelVisualTheme.js +15 -7
- package/dist/panelVisualTheme.js.map +1 -1
- package/dist/panelWorkbenchHelpers.js +1 -1
- package/dist/panelWorkbenchHelpers.js.map +1 -1
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +16 -12
- package/dist/styles.js.map +1 -1
- package/package.json +2 -2
- package/src/FrameGraphInspector.ts +17 -39
- package/src/debugCaptureModel.ts +44 -19
- package/src/panelCytoscapeGraphRenderer.ts +38 -18
- package/src/panelDiagnosticsView.ts +3 -3
- package/src/panelGraphLayout.ts +15 -1
- package/src/panelGraphScene.ts +153 -279
- package/src/panelGraphView.ts +31 -9
- package/src/panelGraphVisuals.ts +46 -82
- package/src/panelInspectorView.ts +104 -16
- package/src/panelSelection.ts +2 -2
- package/src/panelTypes.ts +9 -3
- package/src/panelVisualTheme.ts +18 -7
- package/src/panelWorkbenchHelpers.ts +2 -2
- package/src/styles.ts +16 -12
package/src/panelGraphView.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function renderGraphView(
|
|
|
22
22
|
onToggleGroup: (pathKey: string) => void,
|
|
23
23
|
): void {
|
|
24
24
|
const scene = resolveGraphScene(graphView, snapshot);
|
|
25
|
-
renderGraphLegend(graphView.legend,
|
|
25
|
+
renderGraphLegend(graphView.legend, snapshot);
|
|
26
26
|
const elementCount = scene.nodes.length + scene.edges.length;
|
|
27
27
|
const layoutElementBudget = graphView.layoutElementBudget ?? Number.MAX_SAFE_INTEGER;
|
|
28
28
|
if (elementCount > layoutElementBudget) {
|
|
@@ -56,14 +56,12 @@ export function resolveGraphScene(
|
|
|
56
56
|
snapshot: FrameGraphDebugViewModel,
|
|
57
57
|
): GraphScene {
|
|
58
58
|
const optionsKey = JSON.stringify([
|
|
59
|
-
graphView.graphMode,
|
|
60
59
|
graphView.groupsEnabled,
|
|
61
60
|
[...graphView.expandedGroupPaths].sort(),
|
|
62
61
|
]);
|
|
63
62
|
const cached = graphSceneCache.get(graphView);
|
|
64
63
|
if (cached?.snapshot === snapshot && cached.optionsKey === optionsKey) return cached.scene;
|
|
65
64
|
const scene = createGraphScene(snapshot, {
|
|
66
|
-
mode: graphView.graphMode,
|
|
67
65
|
groupsEnabled: graphView.groupsEnabled,
|
|
68
66
|
expandedGroupPaths: graphView.expandedGroupPaths,
|
|
69
67
|
});
|
|
@@ -85,13 +83,26 @@ export function destroyGraph(graphView: GraphViewState): void {
|
|
|
85
83
|
graphSceneCache.delete(graphView);
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
function renderGraphLegend(host: HTMLElement | undefined,
|
|
86
|
+
function renderGraphLegend(host: HTMLElement | undefined, snapshot: FrameGraphDebugViewModel): void {
|
|
89
87
|
if (!host) return;
|
|
90
|
-
const entries = createGraphLegend(
|
|
91
|
-
const key =
|
|
88
|
+
const entries = createGraphLegend(snapshot);
|
|
89
|
+
const key = JSON.stringify(entries);
|
|
92
90
|
if (host.dataset.legendKey === key) return;
|
|
93
91
|
host.dataset.legendKey = key;
|
|
94
|
-
host.replaceChildren(
|
|
92
|
+
host.replaceChildren();
|
|
93
|
+
let group: HTMLElement | undefined;
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (group?.getAttribute('aria-label') !== entry.group) {
|
|
96
|
+
group = document.createElement('span');
|
|
97
|
+
group.className = 'zenfg-inspector-legend-group';
|
|
98
|
+
group.setAttribute('role', 'group');
|
|
99
|
+
group.setAttribute('aria-label', entry.group);
|
|
100
|
+
const heading = document.createElement('span');
|
|
101
|
+
heading.className = 'zenfg-inspector-legend-heading';
|
|
102
|
+
heading.textContent = entry.group;
|
|
103
|
+
group.appendChild(heading);
|
|
104
|
+
host.appendChild(group);
|
|
105
|
+
}
|
|
95
106
|
const item = document.createElement('span');
|
|
96
107
|
item.className = 'zenfg-inspector-legend-item';
|
|
97
108
|
const swatch = document.createElement('span');
|
|
@@ -100,9 +111,20 @@ function renderGraphLegend(host: HTMLElement | undefined, scene: GraphScene): vo
|
|
|
100
111
|
if (entry.lineStyle) swatch.dataset.lineStyle = entry.lineStyle;
|
|
101
112
|
if (entry.hollowArrow) swatch.dataset.hollowArrow = 'true';
|
|
102
113
|
swatch.style.setProperty('--zenfg-inspector-legend-color', entry.color);
|
|
114
|
+
if (entry.shape === 'cut-rectangle' || entry.shape === 'tag') {
|
|
115
|
+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
116
|
+
svg.setAttribute('viewBox', '0 0 20 14');
|
|
117
|
+
svg.setAttribute('aria-hidden', 'true');
|
|
118
|
+
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
|
|
119
|
+
polygon.setAttribute('points', entry.shape === 'tag'
|
|
120
|
+
? '1,1 12,1 19,7 12,13 1,13'
|
|
121
|
+
: '4,1 16,1 19,4 19,10 16,13 4,13 1,10 1,4');
|
|
122
|
+
svg.appendChild(polygon);
|
|
123
|
+
swatch.appendChild(svg);
|
|
124
|
+
}
|
|
103
125
|
const label = document.createElement('span');
|
|
104
126
|
label.textContent = entry.label;
|
|
105
127
|
item.append(swatch, label);
|
|
106
|
-
|
|
107
|
-
}
|
|
128
|
+
group!.appendChild(item);
|
|
129
|
+
}
|
|
108
130
|
}
|
package/src/panelGraphVisuals.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type cytoscape from 'cytoscape';
|
|
|
2
2
|
|
|
3
3
|
import type { GraphScene, GraphSceneEdge, GraphSceneNode } from './panelGraphScene.ts';
|
|
4
4
|
import { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
|
|
5
|
+
import { declarationEntrances, type FrameGraphDebugViewModel } from './debugCaptureModel.ts';
|
|
5
6
|
|
|
6
7
|
export { GRAPH_VISUAL_THEME } from './panelVisualTheme.ts';
|
|
7
8
|
|
|
@@ -16,16 +17,19 @@ export const GRAPH_GEOMETRY = {
|
|
|
16
17
|
groupPadding: 32,
|
|
17
18
|
elkGroupPadding: { top: 46, right: 24, bottom: 24, left: 24 },
|
|
18
19
|
edgeCornerRadius: 6,
|
|
20
|
+
outputWidth: 256,
|
|
21
|
+
outputLabelWidth: 144,
|
|
19
22
|
} as const;
|
|
20
23
|
|
|
21
24
|
const ENDPOINT_EDGE_DISTANCES = 'endpoints' as unknown as cytoscape.Css.Edge['edge-distances'];
|
|
22
25
|
const EDGE_SEGMENT_RADII = `${GRAPH_GEOMETRY.edgeCornerRadius}px` as unknown as cytoscape.Css.Edge['segment-radii'];
|
|
23
26
|
|
|
24
27
|
export type GraphLegendEntry = {
|
|
28
|
+
readonly group: 'Execution' | 'Resources' | 'Relationships';
|
|
25
29
|
readonly key: string;
|
|
26
30
|
readonly label: string;
|
|
27
31
|
readonly color: string;
|
|
28
|
-
readonly shape: 'box' | 'ellipse' | 'group' | 'line';
|
|
32
|
+
readonly shape: 'box' | 'cut-rectangle' | 'tag' | 'ellipse' | 'group' | 'line';
|
|
29
33
|
readonly lineStyle?: 'solid' | 'dotted' | 'dashed';
|
|
30
34
|
readonly hollowArrow?: boolean;
|
|
31
35
|
};
|
|
@@ -36,50 +40,37 @@ const NODE_LEGEND_ENTRIES = {
|
|
|
36
40
|
copy: nodeLegend('copy', 'Copy', GRAPH_VISUAL_THEME.copy.stroke),
|
|
37
41
|
'clear-buffer': nodeLegend('clear', 'Clear', GRAPH_VISUAL_THEME.clear.stroke),
|
|
38
42
|
command: nodeLegend('command', 'Command', GRAPH_VISUAL_THEME.command.stroke),
|
|
39
|
-
'external-submission': nodeLegend('external', 'External', GRAPH_VISUAL_THEME.external.stroke),
|
|
43
|
+
'external-submission': { ...nodeLegend('external', 'External', GRAPH_VISUAL_THEME.external.stroke), shape: 'cut-rectangle' as const },
|
|
40
44
|
} as const;
|
|
41
45
|
|
|
42
|
-
export function createGraphLegend(
|
|
46
|
+
export function createGraphLegend(snapshot: FrameGraphDebugViewModel): readonly GraphLegendEntry[] {
|
|
43
47
|
const entries: GraphLegendEntry[] = [];
|
|
44
|
-
const passKinds = new Set(
|
|
45
|
-
? [node.passKind]
|
|
46
|
-
: []));
|
|
48
|
+
const passKinds = new Set(snapshot.nodes.map((node) => node.kind));
|
|
47
49
|
for (const kind of ['render', 'compute', 'copy', 'clear-buffer', 'command', 'external-submission'] as const) {
|
|
48
50
|
if (passKinds.has(kind)) entries.push(NODE_LEGEND_ENTRIES[kind]);
|
|
49
51
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
const retained = new Set(snapshot.nodes.map((node) => node.id));
|
|
53
|
+
const used = new Set([
|
|
54
|
+
...snapshot.accessEdges.filter((access) => retained.has(access.nodeId)).map((access) => access.resource.id),
|
|
55
|
+
...snapshot.roots.flatMap((root) => root.resource ? [root.resource.id] : []),
|
|
56
|
+
]);
|
|
57
|
+
const resources = snapshot.resources.filter((resource) => used.has(resource.id));
|
|
58
|
+
if (resources.length) entries.push({ group: 'Resources', key: 'declaration', label: 'Declaration', color: GRAPH_VISUAL_THEME.declaration.stroke, shape: 'ellipse' });
|
|
59
|
+
if (snapshot.roots.some((root) => root.resource)) entries.push({ group: 'Resources', key: 'output', label: 'Output', color: GRAPH_VISUAL_THEME.output.stroke, shape: 'tag' });
|
|
60
|
+
if ([...snapshot.nodes, ...resources].some((item) => item.debugGroupId !== undefined)) {
|
|
61
|
+
entries.push({ group: 'Relationships', key: 'group', label: 'Group', color: GRAPH_VISUAL_THEME.group.stroke, shape: 'group' });
|
|
52
62
|
}
|
|
53
|
-
if (
|
|
54
|
-
|
|
63
|
+
if (snapshot.edges.some((edge) => edge.kind === 'value')
|
|
64
|
+
|| declarationEntrances(snapshot.nodes, snapshot.accessEdges, snapshot.edges).length
|
|
65
|
+
|| snapshot.roots.some((root) => root.resource && root.resolution && (root.resolution.usesInitialContents || root.resolution.producerNodeIds.length))) {
|
|
66
|
+
entries.push(edgeLegend('flow', 'Resource Flow', GRAPH_VISUAL_THEME.dependency.value, 'solid'));
|
|
55
67
|
}
|
|
56
|
-
if (
|
|
57
|
-
entries.push({ key: 'buffer', label: 'Buffer', color: GRAPH_VISUAL_THEME.buffer.stroke, shape: 'ellipse' });
|
|
58
|
-
}
|
|
59
|
-
if (scene.nodes.some((node) => node.kind === 'culled-pass')) {
|
|
60
|
-
entries.push({
|
|
61
|
-
key: 'culled',
|
|
62
|
-
label: 'Culled',
|
|
63
|
-
color: GRAPH_VISUAL_THEME.culled.stroke,
|
|
64
|
-
shape: 'box',
|
|
65
|
-
lineStyle: 'dashed',
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
if (scene.edges.some((edge) => edge.kind === 'dependency' && edge.dependencyKind === 'value')) {
|
|
69
|
-
entries.push(edgeLegend('value', 'Value', GRAPH_VISUAL_THEME.dependency.value, 'solid'));
|
|
70
|
-
}
|
|
71
|
-
if (scene.edges.some((edge) => edge.kind === 'dependency' && edge.dependencyKind === 'ordering')) {
|
|
68
|
+
if (snapshot.edges.some((edge) => edge.kind === 'ordering')) {
|
|
72
69
|
entries.push({
|
|
73
70
|
...edgeLegend('ordering', 'Order', GRAPH_VISUAL_THEME.dependency.ordering, 'dotted'),
|
|
74
71
|
hollowArrow: true,
|
|
75
72
|
});
|
|
76
73
|
}
|
|
77
|
-
if (scene.edges.some((edge) => edge.kind === 'access' && edge.accessMode === 'read')) {
|
|
78
|
-
entries.push(edgeLegend('read', 'Read', GRAPH_VISUAL_THEME.access.read, 'solid'));
|
|
79
|
-
}
|
|
80
|
-
if (scene.edges.some((edge) => edge.kind === 'access' && edge.accessMode === 'write')) {
|
|
81
|
-
entries.push(edgeLegend('write', 'Write', GRAPH_VISUAL_THEME.access.write, 'solid'));
|
|
82
|
-
}
|
|
83
74
|
return entries;
|
|
84
75
|
}
|
|
85
76
|
|
|
@@ -102,6 +93,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
102
93
|
'background-color': theme.surfaceRaised,
|
|
103
94
|
'border-color': theme.group.stroke,
|
|
104
95
|
'border-width': 1.25,
|
|
96
|
+
'border-style': 'solid',
|
|
105
97
|
'text-valign': 'center',
|
|
106
98
|
'text-halign': 'center',
|
|
107
99
|
'overlay-opacity': 0,
|
|
@@ -118,34 +110,27 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
118
110
|
style: {
|
|
119
111
|
'background-color': theme.external.fill,
|
|
120
112
|
'border-color': theme.external.stroke,
|
|
121
|
-
'
|
|
122
|
-
'
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
selector: 'node[kind = "culled-pass"]',
|
|
127
|
-
style: {
|
|
128
|
-
'background-color': theme.culled.fill,
|
|
129
|
-
'border-color': theme.culled.stroke,
|
|
130
|
-
'color': theme.muted,
|
|
131
|
-
'border-style': 'dashed',
|
|
132
|
-
'opacity': 0.72,
|
|
113
|
+
'shape': 'cut-rectangle',
|
|
114
|
+
'corner-radius': '10px',
|
|
133
115
|
},
|
|
134
116
|
},
|
|
135
117
|
{
|
|
136
|
-
selector: 'node[kind = "resource"]
|
|
118
|
+
selector: 'node[kind = "resource"]',
|
|
137
119
|
style: {
|
|
138
120
|
'shape': 'ellipse',
|
|
139
|
-
'background-color': theme.
|
|
140
|
-
'border-color': theme.
|
|
121
|
+
'background-color': theme.declaration.fill,
|
|
122
|
+
'border-color': theme.declaration.stroke,
|
|
141
123
|
},
|
|
142
124
|
},
|
|
143
125
|
{
|
|
144
|
-
selector: 'node[kind = "
|
|
126
|
+
selector: 'node[kind = "root"]',
|
|
145
127
|
style: {
|
|
146
|
-
'shape': '
|
|
147
|
-
'background-color': theme.
|
|
148
|
-
'border-color': theme.
|
|
128
|
+
'shape': 'tag',
|
|
129
|
+
'background-color': theme.output.fill,
|
|
130
|
+
'border-color': theme.output.stroke,
|
|
131
|
+
'text-max-width': `${GRAPH_GEOMETRY.outputLabelWidth}px`,
|
|
132
|
+
// Native tag shoulders sit at 5/8 of its width. Center text in the rectangular body.
|
|
133
|
+
'text-margin-x': -GRAPH_GEOMETRY.outputWidth * 3 / 16,
|
|
149
134
|
},
|
|
150
135
|
},
|
|
151
136
|
{
|
|
@@ -183,7 +168,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
183
168
|
'arrow-scale': 0.8,
|
|
184
169
|
'curve-style': 'straight',
|
|
185
170
|
'label': 'data(displayLabel)',
|
|
186
|
-
'font-family': 'ui-monospace, SFMono-Regular, Consolas, Liberation Mono, monospace',
|
|
171
|
+
'font-family': 'ui-monospace, SFMono-Regular, Consolas, Liberation Mono, monospace',
|
|
187
172
|
'font-size': 10,
|
|
188
173
|
'min-zoomed-font-size': GRAPH_GEOMETRY.minimumZoomedFontSize,
|
|
189
174
|
'color': theme.text,
|
|
@@ -196,7 +181,7 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
196
181
|
},
|
|
197
182
|
},
|
|
198
183
|
{
|
|
199
|
-
selector: 'edge[
|
|
184
|
+
selector: 'edge[kind = "ordering"]',
|
|
200
185
|
style: {
|
|
201
186
|
'line-color': theme.dependency.ordering,
|
|
202
187
|
'target-arrow-color': theme.dependency.ordering,
|
|
@@ -205,24 +190,6 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
205
190
|
'line-opacity': 0.72,
|
|
206
191
|
},
|
|
207
192
|
},
|
|
208
|
-
{
|
|
209
|
-
selector: 'edge[accessMode = "read"]',
|
|
210
|
-
style: {
|
|
211
|
-
'line-color': theme.access.read,
|
|
212
|
-
'target-arrow-color': theme.access.read,
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
selector: 'edge[accessMode = "write"]',
|
|
217
|
-
style: {
|
|
218
|
-
'line-color': theme.access.write,
|
|
219
|
-
'target-arrow-color': theme.access.write,
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
selector: 'edge[dashed = 1]',
|
|
224
|
-
style: { 'line-style': 'dashed', 'line-opacity': 0.46, 'opacity': 0.58 },
|
|
225
|
-
},
|
|
226
193
|
{
|
|
227
194
|
selector: 'edge.elk-route',
|
|
228
195
|
style: {
|
|
@@ -263,9 +230,6 @@ export function createGraphStyles(): cytoscape.StylesheetJson {
|
|
|
263
230
|
style: {
|
|
264
231
|
'border-color': theme.selected,
|
|
265
232
|
'border-width': 3,
|
|
266
|
-
'underlay-color': theme.selected,
|
|
267
|
-
'underlay-padding': 5,
|
|
268
|
-
'underlay-opacity': 0.18,
|
|
269
233
|
'opacity': 1,
|
|
270
234
|
'z-index': 20,
|
|
271
235
|
},
|
|
@@ -287,10 +251,10 @@ export function nodeDimensions(node: GraphSceneNode): { readonly width: number;
|
|
|
287
251
|
switch (node.kind) {
|
|
288
252
|
case 'pass':
|
|
289
253
|
return { width: 184, height: node.label.includes('\n') ? 62 : 48 };
|
|
290
|
-
case '
|
|
291
|
-
return { width:
|
|
254
|
+
case 'root':
|
|
255
|
+
return { width: GRAPH_GEOMETRY.outputWidth, height: Math.max(58, node.label.split('\n').length * 17 + 24) };
|
|
292
256
|
case 'resource':
|
|
293
|
-
return { width:
|
|
257
|
+
return { width: 184, height: Math.max(58, node.label.split('\n').length * 17 + 24) };
|
|
294
258
|
case 'group':
|
|
295
259
|
return node.collapsed ? { width: 224, height: 68 } : { width: 120, height: 80 };
|
|
296
260
|
}
|
|
@@ -301,13 +265,13 @@ export function graphLayoutGeometryKey(scene: GraphScene): string {
|
|
|
301
265
|
topology: scene.topologyKey,
|
|
302
266
|
dimensions: scene.nodes.map((node) => {
|
|
303
267
|
const dimensions = nodeDimensions(node);
|
|
304
|
-
return [node.id, dimensions.width, dimensions.height];
|
|
268
|
+
return [node.id, dimensions.width, dimensions.height, node.kind === 'pass' && node.passKind === 'external-submission'];
|
|
305
269
|
}),
|
|
306
270
|
});
|
|
307
271
|
}
|
|
308
272
|
|
|
309
|
-
export function graphEdgeDisplayLabel(
|
|
310
|
-
return
|
|
273
|
+
export function graphEdgeDisplayLabel(_edge: GraphSceneEdge): string {
|
|
274
|
+
return '';
|
|
311
275
|
}
|
|
312
276
|
|
|
313
277
|
export function expandedGroupLabelMaxWidth(outerWidth: number): number {
|
|
@@ -319,7 +283,7 @@ export function isOverviewGraphScale(zoom: number): boolean {
|
|
|
319
283
|
}
|
|
320
284
|
|
|
321
285
|
function nodeLegend(key: string, label: string, color: string): GraphLegendEntry {
|
|
322
|
-
return { key, label, color, shape: 'box' };
|
|
286
|
+
return { group: 'Execution', key, label, color, shape: 'box' };
|
|
323
287
|
}
|
|
324
288
|
|
|
325
289
|
function edgeLegend(
|
|
@@ -328,7 +292,7 @@ function edgeLegend(
|
|
|
328
292
|
color: string,
|
|
329
293
|
lineStyle: NonNullable<GraphLegendEntry['lineStyle']>,
|
|
330
294
|
): GraphLegendEntry {
|
|
331
|
-
return { key, label, color, shape: 'line', lineStyle };
|
|
295
|
+
return { group: 'Relationships', key, label, color, shape: 'line', lineStyle };
|
|
332
296
|
}
|
|
333
297
|
|
|
334
298
|
function passStyle(
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
FrameGraphDebugAccess,
|
|
3
|
+
FrameGraphDebugRoot,
|
|
3
4
|
FrameGraphDebugResourceRef,
|
|
4
5
|
FrameGraphDebugViewModel,
|
|
5
6
|
} from './debugCaptureModel.ts';
|
|
@@ -16,6 +17,28 @@ import {
|
|
|
16
17
|
type WorkbenchCallbacks,
|
|
17
18
|
} from './panelWorkbenchHelpers.ts';
|
|
18
19
|
|
|
20
|
+
type DetailRelation = readonly [label: string, selection: Selection, description?: string];
|
|
21
|
+
|
|
22
|
+
function formatAccessFacts(access: FrameGraphDebugAccess): string {
|
|
23
|
+
return [
|
|
24
|
+
access.mode, access.access,
|
|
25
|
+
...(access.mode === 'write' ? [access.contents, `producesValue: ${access.producesValue}`] : []),
|
|
26
|
+
...(access.bufferRange ? [`bytes ${access.bufferRange.offset}–${access.bufferRange.size === undefined ? 'end' : access.bufferRange.offset + access.bufferRange.size}`] : []),
|
|
27
|
+
...(access.textureRegion ? [JSON.stringify(access.textureRegion)] : []),
|
|
28
|
+
...(access.textureViewId ? [`view ${access.textureViewId}`] : []),
|
|
29
|
+
].join(' · ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatRootRange(root: FrameGraphDebugRoot): string {
|
|
33
|
+
if (!root.range) return 'Range unavailable';
|
|
34
|
+
if (root.range.kind === 'buffer') return `bytes ${root.range.offset}–${root.range.offset + root.range.size}`;
|
|
35
|
+
return root.range.regions.map((region) =>
|
|
36
|
+
`mip ${region.baseMipLevel}+${region.mipLevelCount} · ${region.baseDepthSlice === undefined
|
|
37
|
+
? `layers ${region.baseArrayLayer}+${region.arrayLayerCount}`
|
|
38
|
+
: `depth ${region.baseDepthSlice}+${region.depthSliceCount}`} · ${region.aspect}`,
|
|
39
|
+
).join('; ');
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
export class InspectorView {
|
|
20
43
|
readonly root = document.createElement('aside');
|
|
21
44
|
private readonly title = document.createElement('strong');
|
|
@@ -26,6 +49,7 @@ export class InspectorView {
|
|
|
26
49
|
private selected: Selection | undefined;
|
|
27
50
|
private activeTab: InspectorTab = 'summary';
|
|
28
51
|
private open = true;
|
|
52
|
+
private hoveredLink: HTMLButtonElement | undefined;
|
|
29
53
|
|
|
30
54
|
constructor(
|
|
31
55
|
private readonly callbacks: WorkbenchCallbacks,
|
|
@@ -39,7 +63,7 @@ export class InspectorView {
|
|
|
39
63
|
const close = document.createElement('button');
|
|
40
64
|
close.type = 'button';
|
|
41
65
|
close.className = 'zenfg-inspector-inspector-close';
|
|
42
|
-
close.appendChild(createPanelIcon('close'));
|
|
66
|
+
close.appendChild(createPanelIcon('close'));
|
|
43
67
|
close.title = 'Close inspector';
|
|
44
68
|
close.setAttribute('aria-label', 'Close inspector');
|
|
45
69
|
close.addEventListener('click', () => this.setOpen(false));
|
|
@@ -76,6 +100,8 @@ export class InspectorView {
|
|
|
76
100
|
}
|
|
77
101
|
|
|
78
102
|
setSelection(selected: Selection | undefined, reveal = true): void {
|
|
103
|
+
if (selected?.kind === 'resource'
|
|
104
|
+
&& (this.selected?.kind !== 'resource' || this.selected.id !== selected.id)) this.activeTab = 'summary';
|
|
79
105
|
this.selected = selected;
|
|
80
106
|
if (selected && reveal) this.setOpen(true);
|
|
81
107
|
this.render();
|
|
@@ -83,6 +109,7 @@ export class InspectorView {
|
|
|
83
109
|
|
|
84
110
|
setOpen(open: boolean): void {
|
|
85
111
|
if (this.open === open) return;
|
|
112
|
+
if (!open) this.clearLinkHover();
|
|
86
113
|
this.open = open;
|
|
87
114
|
this.updateOpenState();
|
|
88
115
|
this.onOpenChange(open);
|
|
@@ -98,6 +125,7 @@ export class InspectorView {
|
|
|
98
125
|
}
|
|
99
126
|
|
|
100
127
|
private render(): void {
|
|
128
|
+
this.clearLinkHover();
|
|
101
129
|
for (const [tab, button] of this.tabs) {
|
|
102
130
|
const active = tab === this.activeTab;
|
|
103
131
|
button.classList.toggle('active', active);
|
|
@@ -140,7 +168,7 @@ export class InspectorView {
|
|
|
140
168
|
return resource ? labelResource(resource) : `Resource #${selection.id}`;
|
|
141
169
|
}
|
|
142
170
|
case 'allocation': return `Allocation #${selection.id}`;
|
|
143
|
-
case 'root': return
|
|
171
|
+
case 'root': return 'Output root';
|
|
144
172
|
case 'culled': return `Culled node #${selection.index}`;
|
|
145
173
|
case 'segment': return `Segment #${selection.index}`;
|
|
146
174
|
}
|
|
@@ -200,11 +228,16 @@ export class InspectorView {
|
|
|
200
228
|
]);
|
|
201
229
|
}
|
|
202
230
|
case 'root': {
|
|
203
|
-
const root = snapshot.roots
|
|
231
|
+
const root = snapshot.roots.find((root) => root.key === selection.key);
|
|
204
232
|
return this.summary(root ? [
|
|
205
233
|
['Reason', root.reason],
|
|
206
234
|
['Node', root.nodeId === undefined ? '-' : `#${root.nodeId}`],
|
|
207
|
-
['Resource', root.resource ?
|
|
235
|
+
['Resource', root.resource ? this.viewResourceButton(snapshot, root.resource.id) : '-'],
|
|
236
|
+
...(root.reason === 'side-effect' ? [] : [
|
|
237
|
+
['Range', root.range ? JSON.stringify(root.range) : 'Unavailable in Legacy capture'],
|
|
238
|
+
['Initial contents', root.resolution ? String(root.resolution.usesInitialContents) : 'Unavailable in Legacy capture'],
|
|
239
|
+
['Producers', root.resolution ? root.resolution.producerNodeIds.join(', ') || 'None' : 'Unavailable in Legacy capture'],
|
|
240
|
+
] as [string, string][]),
|
|
208
241
|
] : []);
|
|
209
242
|
}
|
|
210
243
|
case 'culled': {
|
|
@@ -254,6 +287,10 @@ export class InspectorView {
|
|
|
254
287
|
host.append(
|
|
255
288
|
this.resourceRelations('Inputs', group.summary.inputResources),
|
|
256
289
|
this.resourceRelations('Outputs', group.summary.outputResources),
|
|
290
|
+
this.relationGroup('Output roots', group.summary.outputRoots.map((root) => [
|
|
291
|
+
`${root.resource ? labelResource(root.resource) : '-'} · ${root.reason} · ${root.range ? JSON.stringify(root.range) : 'Range unavailable'}`,
|
|
292
|
+
{ kind: 'root', key: root.key },
|
|
293
|
+
])),
|
|
257
294
|
);
|
|
258
295
|
}
|
|
259
296
|
break;
|
|
@@ -261,15 +298,20 @@ export class InspectorView {
|
|
|
261
298
|
case 'resource': {
|
|
262
299
|
const resource = snapshot.resourceById.get(selection.id);
|
|
263
300
|
const accesses = snapshot.accessesByResourceId.get(selection.id) ?? [];
|
|
264
|
-
const passRelations:
|
|
301
|
+
const passRelations: DetailRelation[] = [];
|
|
265
302
|
for (const access of accesses) {
|
|
266
303
|
const nodeSelection = this.accessNodeSelection(snapshot, access.nodeId);
|
|
267
304
|
if (nodeSelection) passRelations.push([
|
|
268
|
-
|
|
305
|
+
this.accessNodeLabel(snapshot, access.nodeId),
|
|
269
306
|
nodeSelection,
|
|
307
|
+
formatAccessFacts(access),
|
|
270
308
|
]);
|
|
271
309
|
}
|
|
272
310
|
host.appendChild(this.relationGroup('Pass accesses', passRelations));
|
|
311
|
+
const roots = snapshot.roots.filter((root) => root.resource?.id === selection.id);
|
|
312
|
+
if (roots.length) host.appendChild(this.relationGroup('Output roots', roots.map((root) => [
|
|
313
|
+
`${root.reason} · ${formatRootRange(root)}`, { kind: 'root', key: root.key },
|
|
314
|
+
])));
|
|
273
315
|
if (resource?.physicalResourceId !== undefined) host.appendChild(this.relationGroup('Allocation', [
|
|
274
316
|
[`#${resource.physicalResourceId}`, { kind: 'allocation', id: resource.physicalResourceId }],
|
|
275
317
|
]));
|
|
@@ -283,13 +325,15 @@ export class InspectorView {
|
|
|
283
325
|
break;
|
|
284
326
|
}
|
|
285
327
|
case 'root': {
|
|
286
|
-
const root = snapshot.roots
|
|
328
|
+
const root = snapshot.roots.find((root) => root.key === selection.key);
|
|
287
329
|
const relations: Array<readonly [string, Selection]> = [];
|
|
288
330
|
if (root?.nodeId !== undefined && snapshot.nodeById.has(root.nodeId)) relations.push([
|
|
289
331
|
this.accessNodeLabel(snapshot, root.nodeId), { kind: 'node', id: root.nodeId },
|
|
290
332
|
]);
|
|
291
|
-
if (root?.resource)
|
|
292
|
-
|
|
333
|
+
if (root?.resource) host.appendChild(this.relationGroup('Resource', [[labelResource(root.resource), { kind: 'resource', id: root.resource.id }]]));
|
|
334
|
+
if (root?.resource && root.resolution?.usesInitialContents) relations.push([`Initial contents · ${labelResource(root.resource)}`, { kind: 'resource', id: root.resource.id }]);
|
|
335
|
+
for (const id of root?.resolution?.producerNodeIds ?? []) relations.push([this.accessNodeLabel(snapshot, id), { kind: 'node', id }]);
|
|
336
|
+
host.appendChild(this.relationGroup('Output sources', relations));
|
|
293
337
|
break;
|
|
294
338
|
}
|
|
295
339
|
case 'culled': {
|
|
@@ -320,8 +364,9 @@ export class InspectorView {
|
|
|
320
364
|
|
|
321
365
|
private accessRelations(title: string, accesses: readonly FrameGraphDebugAccess[]): HTMLElement {
|
|
322
366
|
return this.relationGroup(title, accesses.map((access) => [
|
|
323
|
-
|
|
367
|
+
labelResource(access.resource),
|
|
324
368
|
{ kind: 'resource', id: access.resource.id },
|
|
369
|
+
formatAccessFacts(access),
|
|
325
370
|
]));
|
|
326
371
|
}
|
|
327
372
|
|
|
@@ -331,13 +376,24 @@ export class InspectorView {
|
|
|
331
376
|
]));
|
|
332
377
|
}
|
|
333
378
|
|
|
334
|
-
private relationGroup(title: string, relations: readonly
|
|
379
|
+
private relationGroup(title: string, relations: readonly DetailRelation[]): HTMLElement {
|
|
335
380
|
const section = document.createElement('section');
|
|
336
381
|
const heading = document.createElement('h3');
|
|
337
382
|
heading.textContent = `${title} ${relations.length}`;
|
|
338
383
|
section.appendChild(heading);
|
|
339
|
-
for (const [label, selection] of relations) {
|
|
340
|
-
|
|
384
|
+
for (const [label, selection, description] of relations) {
|
|
385
|
+
const link = this.createDetailLink(label, selection);
|
|
386
|
+
if (description === undefined) {
|
|
387
|
+
section.appendChild(link);
|
|
388
|
+
} else {
|
|
389
|
+
const entry = document.createElement('div');
|
|
390
|
+
entry.className = 'zenfg-inspector-relation-entry';
|
|
391
|
+
const metadata = document.createElement('span');
|
|
392
|
+
metadata.className = 'zenfg-inspector-muted';
|
|
393
|
+
metadata.textContent = description;
|
|
394
|
+
entry.append(link, metadata);
|
|
395
|
+
section.appendChild(entry);
|
|
396
|
+
}
|
|
341
397
|
}
|
|
342
398
|
if (relations.length === 0) {
|
|
343
399
|
const empty = document.createElement('span');
|
|
@@ -348,15 +404,47 @@ export class InspectorView {
|
|
|
348
404
|
return section;
|
|
349
405
|
}
|
|
350
406
|
|
|
351
|
-
private
|
|
407
|
+
private selectRelated(selection: Selection): void {
|
|
408
|
+
this.clearLinkHover();
|
|
409
|
+
this.callbacks.onSelect(selection);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private viewResourceButton(snapshot: FrameGraphDebugViewModel, resourceId: string): HTMLButtonElement {
|
|
413
|
+
return this.createDetailLink(`View resource · ${resourceLabel(snapshot, resourceId)}`,
|
|
414
|
+
{ kind: 'resource', id: resourceId });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private createDetailLink(label: string, selection: Selection): HTMLButtonElement {
|
|
418
|
+
const link = createRelationButton(label, selection, (target) => this.selectRelated(target));
|
|
419
|
+
link.addEventListener('mouseenter', () => {
|
|
420
|
+
this.hoveredLink = link;
|
|
421
|
+
this.callbacks.onHover(selection);
|
|
422
|
+
});
|
|
423
|
+
link.addEventListener('mouseleave', () => {
|
|
424
|
+
if (this.hoveredLink === link) this.clearLinkHover();
|
|
425
|
+
});
|
|
426
|
+
return link;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private clearLinkHover(): void {
|
|
430
|
+
if (!this.hoveredLink) return;
|
|
431
|
+
this.hoveredLink = undefined;
|
|
432
|
+
this.callbacks.onHover(undefined);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private summary(rows: readonly (readonly [string, string | HTMLElement])[]): HTMLElement {
|
|
352
436
|
const summary = document.createElement('dl');
|
|
353
437
|
summary.className = 'zenfg-inspector-inspector-summary';
|
|
354
438
|
for (const [label, value] of rows) {
|
|
355
439
|
const term = document.createElement('dt');
|
|
356
440
|
term.textContent = label;
|
|
357
441
|
const description = document.createElement('dd');
|
|
358
|
-
|
|
359
|
-
|
|
442
|
+
if (typeof value === 'string') {
|
|
443
|
+
description.textContent = value;
|
|
444
|
+
description.title = value;
|
|
445
|
+
} else {
|
|
446
|
+
description.appendChild(value);
|
|
447
|
+
}
|
|
360
448
|
summary.append(term, description);
|
|
361
449
|
}
|
|
362
450
|
return summary;
|
package/src/panelSelection.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function resolveSelectedDetail(
|
|
|
26
26
|
case 'group':
|
|
27
27
|
return snapshot.groupByPathKey.get(selected.pathKey);
|
|
28
28
|
case 'root':
|
|
29
|
-
return snapshot.roots
|
|
29
|
+
return snapshot.roots.find((root) => root.key === selected.key);
|
|
30
30
|
case 'culled': {
|
|
31
31
|
const culled = snapshot.culledNodes[selected.index];
|
|
32
32
|
return culled ? {
|
|
@@ -54,7 +54,7 @@ export function selectionExists(snapshot: FrameGraphDebugViewModel, selected: Se
|
|
|
54
54
|
case 'resource':
|
|
55
55
|
return snapshot.resourceById.has(selected.id);
|
|
56
56
|
case 'root':
|
|
57
|
-
return
|
|
57
|
+
return snapshot.roots.filter((root) => root.key === selected.key).length === 1;
|
|
58
58
|
case 'culled':
|
|
59
59
|
return isValidIndex(selected.index, snapshot.culledNodes.length);
|
|
60
60
|
case 'allocation':
|
package/src/panelTypes.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { GraphRenderer } from './panelGraphRenderer.ts';
|
|
2
|
+
import type { FrameGraphDebugEdge } from './debugCaptureModel.ts';
|
|
3
|
+
|
|
4
|
+
export type GraphFlowRelation = {
|
|
5
|
+
readonly role: 'declaration' | 'value' | 'ordering' | 'output-producer' | 'output-initial';
|
|
6
|
+
readonly nodeIds: readonly string[];
|
|
7
|
+
readonly rootKey?: string;
|
|
8
|
+
readonly dependency?: FrameGraphDebugEdge;
|
|
9
|
+
};
|
|
2
10
|
|
|
3
11
|
export type Selection =
|
|
4
12
|
| { kind: 'node'; id: string }
|
|
5
13
|
| { kind: 'group'; pathKey: string }
|
|
6
14
|
| { kind: 'resource'; id: string }
|
|
7
|
-
| { kind: 'root';
|
|
15
|
+
| { kind: 'root'; key: string }
|
|
8
16
|
| { kind: 'culled'; index: number }
|
|
9
17
|
| { kind: 'allocation'; id: string }
|
|
10
18
|
| { kind: 'segment'; index: number };
|
|
@@ -12,14 +20,12 @@ export type Selection =
|
|
|
12
20
|
export type WorkbenchTab = 'overview' | 'graph' | 'passes' | 'resources' | 'memory' | 'diagnostics';
|
|
13
21
|
export type PassesSubview = 'list' | 'groups';
|
|
14
22
|
export type InspectorTab = 'summary' | 'relations' | 'raw';
|
|
15
|
-
export type GraphViewMode = 'passes' | 'resources';
|
|
16
23
|
|
|
17
24
|
export type GraphViewState = {
|
|
18
25
|
readonly host: HTMLElement;
|
|
19
26
|
readonly toolbar: HTMLElement;
|
|
20
27
|
readonly legend?: HTMLElement;
|
|
21
28
|
readonly layoutElementBudget?: number;
|
|
22
|
-
graphMode: GraphViewMode;
|
|
23
29
|
groupsEnabled: boolean;
|
|
24
30
|
readonly expandedGroupPaths: Set<string>;
|
|
25
31
|
renderer?: GraphRenderer;
|