@zenfg/inspector 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GUIDE.md +182 -0
- package/README.md +60 -26
- package/THEMING.md +124 -0
- package/dist/FrameGraphInspector.d.ts +24 -8
- package/dist/FrameGraphInspector.d.ts.map +1 -1
- package/dist/FrameGraphInspector.js +151 -65
- 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 +28 -4
- package/dist/debugCaptureModel.d.ts.map +1 -1
- package/dist/debugCaptureModel.js +72 -16
- 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 +18 -3
- package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
- package/dist/panelCytoscapeGraphRenderer.js +183 -41
- 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 +167 -58
- 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 +20 -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 +15 -26
- package/dist/panelGraphScene.d.ts.map +1 -1
- package/dist/panelGraphScene.js +191 -208
- 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 +42 -9
- package/dist/panelGraphView.js.map +1 -1
- package/dist/panelGraphVisuals.d.ts +18 -6
- package/dist/panelGraphVisuals.d.ts.map +1 -1
- package/dist/panelGraphVisuals.js +156 -116
- package/dist/panelGraphVisuals.js.map +1 -1
- package/dist/panelInspectorView.d.ts +13 -0
- package/dist/panelInspectorView.d.ts.map +1 -1
- package/dist/panelInspectorView.js +326 -46
- 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 +36 -6
- 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 +18 -4
- package/dist/panelTypes.d.ts.map +1 -1
- package/dist/panelTypes.js.map +1 -1
- package/dist/panelVisualTheme.d.ts +176 -71
- package/dist/panelVisualTheme.d.ts.map +1 -1
- package/dist/panelVisualTheme.js +34 -36
- 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 +63 -2
- 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 +161 -205
- 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 +145 -68
- package/src/capture.ts +4 -0
- package/src/debugCaptureModel.ts +97 -21
- package/src/index.ts +6 -2
- package/src/panelAliasAnalysis.ts +24 -70
- package/src/panelCytoscapeGraphRenderer.ts +177 -40
- package/src/panelDetailLayout.ts +155 -0
- package/src/panelDetailStyles.ts +56 -0
- package/src/panelDiagnosticsView.ts +167 -86
- package/src/panelDomHelpers.ts +4 -5
- package/src/panelGraphLayout.ts +20 -5
- package/src/panelGraphRenderer.ts +7 -0
- package/src/panelGraphScene.ts +204 -292
- package/src/panelGraphSearch.ts +96 -0
- package/src/panelGraphView.ts +41 -9
- package/src/panelGraphVisuals.ts +152 -118
- package/src/panelInspectorView.ts +315 -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 +39 -7
- package/src/panelTheme.ts +85 -0
- package/src/panelTypes.ts +16 -4
- package/src/panelVisualTheme.ts +38 -38
- package/src/panelWorkbenchHelpers.ts +62 -4
- package/src/panelWorkbenchStyles.ts +100 -0
- package/src/panelWorkbenchView.ts +201 -52
- package/src/query.ts +11 -11
- package/src/styles.ts +162 -205
- package/src/theme.ts +24 -0
- package/src/themeDefinitions.ts +103 -0
- package/src/themePalette.ts +44 -0
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
FrameGraphDebugAccess,
|
|
3
|
+
FrameGraphDebugRoot,
|
|
3
4
|
FrameGraphDebugResourceRef,
|
|
4
5
|
FrameGraphDebugViewModel,
|
|
5
6
|
} from './debugCaptureModel.ts';
|
|
6
7
|
import { labelNode, labelResource } from './panelDomHelpers.ts';
|
|
7
8
|
import { createPanelIcon } from './panelIcons.ts';
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
9
|
+
import { RawDetailView } from './panelRawView.ts';
|
|
10
|
+
import { resolveNodeSelection, resolveSelectedCanonicalDetail } from './panelSelection.ts';
|
|
11
|
+
import type { InspectorTab, Selection, WorkbenchTab } from './panelTypes.ts';
|
|
10
12
|
import {
|
|
11
13
|
createRelationButton,
|
|
12
14
|
formatEstimatedBytes,
|
|
13
15
|
formatResourceDescriptor,
|
|
16
|
+
formatTimingCoverage,
|
|
17
|
+
enableTabKeyboard,
|
|
18
|
+
writeClipboardText,
|
|
19
|
+
selectionKey,
|
|
14
20
|
groupPath,
|
|
15
21
|
resourceLabel,
|
|
16
22
|
type WorkbenchCallbacks,
|
|
17
23
|
} from './panelWorkbenchHelpers.ts';
|
|
18
24
|
|
|
25
|
+
type DetailRelation = readonly [label: string, selection: Selection, description?: string];
|
|
26
|
+
|
|
27
|
+
function formatAccessFacts(access: FrameGraphDebugAccess): string {
|
|
28
|
+
return [
|
|
29
|
+
access.mode, access.access,
|
|
30
|
+
...(access.mode === 'write' ? [access.contents, access.producesValue ? 'produces a value' : 'does not produce a value'] : []),
|
|
31
|
+
...(access.bufferRange ? [`bytes ${access.bufferRange.offset}–${access.bufferRange.size === undefined ? 'end' : access.bufferRange.offset + access.bufferRange.size}`] : []),
|
|
32
|
+
...(access.textureRegion ? [formatTextureRegion(access.textureRegion)] : []),
|
|
33
|
+
...(access.textureViewId ? [`view ${access.textureViewId}`] : []),
|
|
34
|
+
].join(' · ');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function formatTextureRegion(region: NonNullable<FrameGraphDebugAccess['textureRegion']>): string {
|
|
38
|
+
const end = (start: number | undefined, count: number | undefined) => start === undefined ? 'unknown' : count === undefined ? `${start}–end` : `${start}–${start + count - 1}`;
|
|
39
|
+
return `mip ${end(region.baseMipLevel, region.mipLevelCount)} · ${region.baseDepthSlice === undefined
|
|
40
|
+
? `layers ${end(region.baseArrayLayer, region.arrayLayerCount)}`
|
|
41
|
+
: `depth ${end(region.baseDepthSlice, region.depthSliceCount)}`} · aspect ${region.aspect}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function formatRootRange(root: FrameGraphDebugRoot): string {
|
|
45
|
+
if (!root.range) return 'Range unavailable';
|
|
46
|
+
if (root.range.kind === 'buffer') return `bytes ${root.range.offset}–${root.range.offset + root.range.size}`;
|
|
47
|
+
return root.range.regions.map(formatTextureRegion).join('; ');
|
|
48
|
+
}
|
|
49
|
+
|
|
19
50
|
export class InspectorView {
|
|
20
51
|
readonly root = document.createElement('aside');
|
|
21
52
|
private readonly title = document.createElement('strong');
|
|
@@ -25,7 +56,9 @@ export class InspectorView {
|
|
|
25
56
|
private snapshot: FrameGraphDebugViewModel | undefined;
|
|
26
57
|
private selected: Selection | undefined;
|
|
27
58
|
private activeTab: InspectorTab = 'summary';
|
|
28
|
-
private open =
|
|
59
|
+
private open = false;
|
|
60
|
+
private hoveredLink: HTMLButtonElement | undefined;
|
|
61
|
+
private rawCache: { readonly snapshot: FrameGraphDebugViewModel; readonly key: string; readonly view: RawDetailView } | undefined;
|
|
29
62
|
|
|
30
63
|
constructor(
|
|
31
64
|
private readonly callbacks: WorkbenchCallbacks,
|
|
@@ -39,7 +72,7 @@ export class InspectorView {
|
|
|
39
72
|
const close = document.createElement('button');
|
|
40
73
|
close.type = 'button';
|
|
41
74
|
close.className = 'zenfg-inspector-inspector-close';
|
|
42
|
-
close.appendChild(createPanelIcon('close'));
|
|
75
|
+
close.appendChild(createPanelIcon('close'));
|
|
43
76
|
close.title = 'Close inspector';
|
|
44
77
|
close.setAttribute('aria-label', 'Close inspector');
|
|
45
78
|
close.addEventListener('click', () => this.setOpen(false));
|
|
@@ -62,6 +95,7 @@ export class InspectorView {
|
|
|
62
95
|
this.tabs.set(tab, button);
|
|
63
96
|
this.tabList.appendChild(button);
|
|
64
97
|
}
|
|
98
|
+
enableTabKeyboard(this.tabList);
|
|
65
99
|
this.content.id = `${idPrefix}-inspector-panel`;
|
|
66
100
|
this.content.className = 'zenfg-inspector-inspector-content';
|
|
67
101
|
this.content.setAttribute('role', 'tabpanel');
|
|
@@ -71,20 +105,30 @@ export class InspectorView {
|
|
|
71
105
|
}
|
|
72
106
|
|
|
73
107
|
setSnapshot(snapshot: FrameGraphDebugViewModel): void {
|
|
108
|
+
if (this.snapshot !== snapshot) this.rawCache = undefined;
|
|
74
109
|
this.snapshot = snapshot;
|
|
75
110
|
this.render();
|
|
76
111
|
}
|
|
77
112
|
|
|
78
113
|
setSelection(selected: Selection | undefined, reveal = true): void {
|
|
114
|
+
if (!selected || !this.selected || selectionKey(selected) !== selectionKey(this.selected)) this.rawCache = undefined;
|
|
115
|
+
if (selected?.kind === 'resource'
|
|
116
|
+
&& (this.selected?.kind !== 'resource' || this.selected.id !== selected.id)) this.activeTab = 'summary';
|
|
79
117
|
this.selected = selected;
|
|
80
|
-
if (selected
|
|
118
|
+
if (!selected) this.setOpen(false);
|
|
119
|
+
else if (reveal && !this.open) {
|
|
120
|
+
this.setOpen(true);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
81
123
|
this.render();
|
|
82
124
|
}
|
|
83
125
|
|
|
84
126
|
setOpen(open: boolean): void {
|
|
85
127
|
if (this.open === open) return;
|
|
128
|
+
if (!open) this.clearLinkHover();
|
|
86
129
|
this.open = open;
|
|
87
130
|
this.updateOpenState();
|
|
131
|
+
if (open) this.render();
|
|
88
132
|
this.onOpenChange(open);
|
|
89
133
|
}
|
|
90
134
|
|
|
@@ -98,10 +142,13 @@ export class InspectorView {
|
|
|
98
142
|
}
|
|
99
143
|
|
|
100
144
|
private render(): void {
|
|
145
|
+
const restoreFocus = this.content.contains(document.activeElement);
|
|
146
|
+
this.clearLinkHover();
|
|
101
147
|
for (const [tab, button] of this.tabs) {
|
|
102
148
|
const active = tab === this.activeTab;
|
|
103
149
|
button.classList.toggle('active', active);
|
|
104
150
|
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
151
|
+
button.tabIndex = active ? 0 : -1;
|
|
105
152
|
if (active) this.content.setAttribute('aria-labelledby', button.id);
|
|
106
153
|
}
|
|
107
154
|
const snapshot = this.snapshot;
|
|
@@ -116,32 +163,52 @@ export class InspectorView {
|
|
|
116
163
|
return;
|
|
117
164
|
}
|
|
118
165
|
this.title.textContent = this.selectionTitle(snapshot, selected);
|
|
166
|
+
if (!this.open) return;
|
|
119
167
|
if (this.activeTab === 'raw') {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
168
|
+
const key = selectionKey(selected);
|
|
169
|
+
if (this.rawCache?.snapshot !== snapshot || this.rawCache.key !== key) {
|
|
170
|
+
const detail = resolveSelectedCanonicalDetail(snapshot, selected);
|
|
171
|
+
this.rawCache = detail ? { snapshot, key, view: new RawDetailView(detail, Boolean(snapshot.protocol.capture.migration)) } : undefined;
|
|
172
|
+
}
|
|
173
|
+
if (this.rawCache) this.content.appendChild(this.rawCache.view.root);
|
|
174
|
+
else this.content.textContent = 'Canonical object unavailable in this capture.';
|
|
124
175
|
} else if (this.activeTab === 'relations') {
|
|
125
176
|
this.content.appendChild(this.createRelations(snapshot, selected));
|
|
126
177
|
} else {
|
|
127
|
-
this.content.
|
|
178
|
+
this.content.append(this.createLocationActions(snapshot, selected), this.createSummary(snapshot, selected));
|
|
179
|
+
const identity = this.selectionId(snapshot, selected);
|
|
180
|
+
if (identity) this.content.appendChild(this.summary([['ID', this.copyId(identity)]]));
|
|
181
|
+
const diagnostics = this.createDiagnostics(snapshot, selected);
|
|
182
|
+
if (diagnostics) this.content.appendChild(diagnostics);
|
|
183
|
+
if (selected.kind === 'node' || selected.kind === 'culled') {
|
|
184
|
+
const node = snapshot.nodeById.get(selected.id) ?? snapshot.culledById.get(selected.id)?.node;
|
|
185
|
+
if (node) {
|
|
186
|
+
const accesses = document.createElement('details');
|
|
187
|
+
const heading = document.createElement('summary');
|
|
188
|
+
heading.textContent = `Access facts · ${node.reads.length + node.writes.length}`;
|
|
189
|
+
accesses.append(heading, this.accessRelations('Reads', node.reads), this.accessRelations('Writes', node.writes));
|
|
190
|
+
this.content.appendChild(accesses);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
128
193
|
}
|
|
194
|
+
// Selecting a relation replaces its button. Keep keyboard focus in the pane.
|
|
195
|
+
if (restoreFocus) this.tabs.get(this.activeTab)?.focus();
|
|
129
196
|
}
|
|
130
197
|
|
|
131
198
|
private selectionTitle(snapshot: FrameGraphDebugViewModel, selection: Selection): string {
|
|
132
199
|
switch (selection.kind) {
|
|
133
200
|
case 'node': {
|
|
134
201
|
const node = snapshot.nodeById.get(selection.id);
|
|
135
|
-
return node ? labelNode(node) :
|
|
202
|
+
return node ? labelNode(node) : selection.id;
|
|
136
203
|
}
|
|
137
204
|
case 'group': return snapshot.groupByPathKey.get(selection.pathKey)?.label ?? 'Group';
|
|
138
205
|
case 'resource': {
|
|
139
206
|
const resource = snapshot.resourceById.get(selection.id);
|
|
140
|
-
return resource ? labelResource(resource) :
|
|
207
|
+
return resource ? labelResource(resource) : selection.id;
|
|
141
208
|
}
|
|
142
|
-
case 'allocation': return `Allocation
|
|
143
|
-
case 'root': return
|
|
144
|
-
case 'culled': return
|
|
209
|
+
case 'allocation': return /^allocation[:#-]/i.test(selection.id) ? selection.id : `Allocation ${selection.id}`;
|
|
210
|
+
case 'root': return 'Output root';
|
|
211
|
+
case 'culled': return `${snapshot.culledById.get(selection.id)?.node.label ?? selection.id} (culled)`;
|
|
145
212
|
case 'segment': return `Segment #${selection.index}`;
|
|
146
213
|
}
|
|
147
214
|
}
|
|
@@ -152,12 +219,21 @@ export class InspectorView {
|
|
|
152
219
|
const node = snapshot.nodeById.get(selection.id);
|
|
153
220
|
if (!node) return this.summary([]);
|
|
154
221
|
const segment = snapshot.segmentByNodeId.get(node.id);
|
|
222
|
+
const roots = snapshot.roots.filter((root) => root.nodeId === node.id || root.resolution?.producerNodeIds.includes(node.id));
|
|
155
223
|
return this.summary([
|
|
224
|
+
['Compile state', 'Retained · compiler included this pass; execution success is not implied'],
|
|
225
|
+
['Retention', roots.length ? roots.map((root) => root.reason).join(', ') : 'No direct output root recorded; inspect dependencies in Relations'],
|
|
156
226
|
['Kind', node.kind],
|
|
157
|
-
['
|
|
227
|
+
['Execution slot', String(node.order)],
|
|
158
228
|
['Group', groupPath(snapshot, node.debugGroupId)],
|
|
159
229
|
['Segment', segment ? `#${segment.index} ${segment.kind}` : '-'],
|
|
160
|
-
['
|
|
230
|
+
['CPU duration', node.cpuDurationMicros === undefined ? 'Not collected' : (node.cpuDurationMicros / 1000).toFixed(3) + ' ms'],
|
|
231
|
+
['CPU scope', 'Synchronous elapsed time; zero may reflect clock precision.' + (node.kind === 'external-submission' ? ' Includes synchronous external submission.' : '')],
|
|
232
|
+
['GPU duration', node.kind === 'external-submission' ? 'Opaque · external work is not measured'
|
|
233
|
+
: node.kind !== 'render' && node.kind !== 'compute' ? 'Not applicable · this pass kind is not timing eligible'
|
|
234
|
+
: node.gpuDurationMicros === undefined ? `Not collected${snapshot.profiling.status === 'unavailable' ? ` · ${snapshot.profiling.reason}` : ''}`
|
|
235
|
+
: `${(node.gpuDurationMicros / 1000).toFixed(3)} ms`],
|
|
236
|
+
['GPU scope', 'GPU timestamp interval for render/compute passes; excludes external work. Zero may reflect timestamp precision.'],
|
|
161
237
|
['Accesses', `${node.reads.length} reads · ${node.writes.length} writes`],
|
|
162
238
|
['Side effect', node.sideEffect ? 'yes' : 'no'],
|
|
163
239
|
]);
|
|
@@ -169,9 +245,12 @@ export class InspectorView {
|
|
|
169
245
|
['Path', group.path.join(' / ')],
|
|
170
246
|
['Retained', String(group.summary.retainedNodeCount)],
|
|
171
247
|
['Culled', String(group.summary.culledNodeCount)],
|
|
172
|
-
['
|
|
173
|
-
['
|
|
174
|
-
['
|
|
248
|
+
['CPU coverage', formatTimingCoverage(group.summary.cpuTimedNodeCount, group.summary.retainedNodeCount)],
|
|
249
|
+
['CPU pass sum', group.summary.cpuTimedNodeCount ? (group.summary.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'],
|
|
250
|
+
['GPU coverage', formatTimingCoverage(group.summary.timedNodeCount, group.summary.timingEligibleNodeCount)],
|
|
251
|
+
['GPU pass sum', group.summary.timingEligibleNodeCount === 0 ? 'Not applicable' : group.summary.timedNodeCount === 0 ? 'Not collected' : (group.summary.gpuWorkDurationMicros / 1000).toFixed(3) + ' ms'],
|
|
252
|
+
['Opaque passes', `${group.summary.externalSubmissionCount} · excluded from GPU pass sum`],
|
|
253
|
+
['Allocations', snapshot.protocol.memory.allocationReport.status === 'available' ? String(group.summary.physicalAllocationCount) : 'Unknown · allocation report unavailable'],
|
|
175
254
|
['Segments', String(group.summary.executionSegmentCount)],
|
|
176
255
|
]);
|
|
177
256
|
}
|
|
@@ -182,9 +261,14 @@ export class InspectorView {
|
|
|
182
261
|
['Kind / origin', `${resource.kind} · ${resource.origin}`],
|
|
183
262
|
['Group', groupPath(snapshot, resource.debugGroupId)],
|
|
184
263
|
['Descriptor', formatResourceDescriptor(resource)],
|
|
185
|
-
['
|
|
186
|
-
['Lifetime', resource.lifetime ?
|
|
187
|
-
['Allocation', resource.physicalResourceId
|
|
264
|
+
['Resource estimate', `${formatEstimatedBytes(resource.estimatedByteSize)} · logical descriptor estimate`],
|
|
265
|
+
['Lifetime', resource.lifetime ? `execution slots ${resource.lifetime.firstUse}–${resource.lifetime.lastUse} (inclusive)` : 'Unavailable'],
|
|
266
|
+
['Allocation', resource.physicalResourceId !== undefined
|
|
267
|
+
? this.createDetailLink(resource.physicalResourceId, { kind: 'allocation', id: resource.physicalResourceId })
|
|
268
|
+
: resource.origin !== 'transient' ? 'Not applicable · externally owned resource'
|
|
269
|
+
: snapshot.protocol.memory.allocationReport.status === 'unavailable' ? 'Unavailable · allocation report not captured'
|
|
270
|
+
: 'Unallocated'],
|
|
271
|
+
['Initial contents', resource.initialContents ?? 'Unknown'],
|
|
188
272
|
['Usage', resource.usageFlags.join(', ') || '-'],
|
|
189
273
|
]);
|
|
190
274
|
}
|
|
@@ -194,26 +278,33 @@ export class InspectorView {
|
|
|
194
278
|
return this.summary([
|
|
195
279
|
['Kind', allocation.kind],
|
|
196
280
|
['Compatibility class', String(allocation.compatibilityClassId)],
|
|
197
|
-
['
|
|
281
|
+
['Physical estimate', `${formatEstimatedBytes(allocation.estimatedByteSize)} · allocation capacity estimate`],
|
|
198
282
|
['Logical resources', String(allocation.resourceIds.length)],
|
|
199
283
|
['Alias', allocation.resourceIds.length > 1 ? `yes · ×${allocation.resourceIds.length}` : 'single'],
|
|
200
284
|
]);
|
|
201
285
|
}
|
|
202
286
|
case 'root': {
|
|
203
|
-
const root = snapshot.roots
|
|
287
|
+
const root = snapshot.roots.find((root) => root.key === selection.key);
|
|
204
288
|
return this.summary(root ? [
|
|
205
289
|
['Reason', root.reason],
|
|
206
|
-
['Node', root.nodeId === undefined ? '
|
|
207
|
-
['Resource', root.resource ?
|
|
290
|
+
['Node', root.nodeId === undefined ? 'Not applicable' : this.nodeLink(snapshot, root.nodeId)],
|
|
291
|
+
['Resource', root.resource ? this.viewResourceButton(snapshot, root.resource.id) : '-'],
|
|
292
|
+
...(root.reason === 'side-effect' ? [] : [
|
|
293
|
+
['Range', root.range ? formatRootRange(root) : 'Unavailable in Legacy capture'],
|
|
294
|
+
['Initial contents', root.resolution ? String(root.resolution.usesInitialContents) : 'Unavailable in Legacy capture'],
|
|
295
|
+
['Producers', root.resolution ? root.resolution.producerNodeIds.join(', ') || 'None' : 'Unavailable in Legacy capture'],
|
|
296
|
+
] as [string, string][]),
|
|
208
297
|
] : []);
|
|
209
298
|
}
|
|
210
299
|
case 'culled': {
|
|
211
|
-
const culled = snapshot.
|
|
300
|
+
const culled = snapshot.culledById.get(selection.id);
|
|
212
301
|
return this.summary(culled ? [
|
|
302
|
+
['Compile state', 'Culled · excluded by the compiler'],
|
|
213
303
|
['Node', labelNode(culled.node)],
|
|
214
304
|
['Kind', culled.node.kind],
|
|
215
305
|
['Group', groupPath(snapshot, culled.node.debugGroupId)],
|
|
216
306
|
['Reason', culled.reason],
|
|
307
|
+
['Execution / segment / CPU / GPU', 'Not applicable · culled passes do not execute'],
|
|
217
308
|
['Accesses', `${culled.node.reads.length} reads · ${culled.node.writes.length} writes`],
|
|
218
309
|
] : []);
|
|
219
310
|
}
|
|
@@ -233,6 +324,7 @@ export class InspectorView {
|
|
|
233
324
|
private createRelations(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
|
|
234
325
|
const host = document.createElement('div');
|
|
235
326
|
host.className = 'zenfg-inspector-inspector-relations';
|
|
327
|
+
host.appendChild(this.createLocationActions(snapshot, selection));
|
|
236
328
|
switch (selection.kind) {
|
|
237
329
|
case 'node': {
|
|
238
330
|
const node = snapshot.nodeById.get(selection.id);
|
|
@@ -245,6 +337,18 @@ export class InspectorView {
|
|
|
245
337
|
if (segment) host.appendChild(this.relationGroup('Segment', [
|
|
246
338
|
[`#${segment.index} ${segment.kind}`, { kind: 'segment', index: segment.index }],
|
|
247
339
|
]));
|
|
340
|
+
for (const kind of ['value', 'ordering'] as const) {
|
|
341
|
+
const relations: DetailRelation[] = [];
|
|
342
|
+
for (const edge of snapshot.edges) {
|
|
343
|
+
if (edge.kind !== kind || (edge.fromNodeId !== node.id && edge.toNodeId !== node.id)) continue;
|
|
344
|
+
const upstream = edge.toNodeId === node.id;
|
|
345
|
+
const otherId = upstream ? edge.fromNodeId : edge.toNodeId;
|
|
346
|
+
const other = resolveNodeSelection(snapshot, otherId);
|
|
347
|
+
if (other) relations.push([this.accessNodeLabel(snapshot, otherId), other,
|
|
348
|
+
`${upstream ? 'Upstream' : 'Downstream'} · ${resourceLabel(snapshot, edge.resource.id)} · ${kind} dependency`]);
|
|
349
|
+
}
|
|
350
|
+
if (relations.length) host.appendChild(this.relationGroup(kind === 'value' ? 'Value dependencies' : 'Ordering dependencies', relations));
|
|
351
|
+
}
|
|
248
352
|
}
|
|
249
353
|
break;
|
|
250
354
|
}
|
|
@@ -254,6 +358,10 @@ export class InspectorView {
|
|
|
254
358
|
host.append(
|
|
255
359
|
this.resourceRelations('Inputs', group.summary.inputResources),
|
|
256
360
|
this.resourceRelations('Outputs', group.summary.outputResources),
|
|
361
|
+
this.relationGroup('Output roots', group.summary.outputRoots.map((root) => [
|
|
362
|
+
`${root.resource ? labelResource(root.resource) : '-'} · ${root.reason} · ${formatRootRange(root)}`,
|
|
363
|
+
{ kind: 'root', key: root.key },
|
|
364
|
+
])),
|
|
257
365
|
);
|
|
258
366
|
}
|
|
259
367
|
break;
|
|
@@ -261,17 +369,22 @@ export class InspectorView {
|
|
|
261
369
|
case 'resource': {
|
|
262
370
|
const resource = snapshot.resourceById.get(selection.id);
|
|
263
371
|
const accesses = snapshot.accessesByResourceId.get(selection.id) ?? [];
|
|
264
|
-
const passRelations:
|
|
372
|
+
const passRelations: DetailRelation[] = [];
|
|
265
373
|
for (const access of accesses) {
|
|
266
374
|
const nodeSelection = this.accessNodeSelection(snapshot, access.nodeId);
|
|
267
375
|
if (nodeSelection) passRelations.push([
|
|
268
|
-
|
|
376
|
+
this.accessNodeLabel(snapshot, access.nodeId),
|
|
269
377
|
nodeSelection,
|
|
378
|
+
formatAccessFacts(access),
|
|
270
379
|
]);
|
|
271
380
|
}
|
|
272
381
|
host.appendChild(this.relationGroup('Pass accesses', passRelations));
|
|
382
|
+
const roots = snapshot.roots.filter((root) => root.resource?.id === selection.id);
|
|
383
|
+
if (roots.length) host.appendChild(this.relationGroup('Output roots', roots.map((root) => [
|
|
384
|
+
`${root.reason} · ${formatRootRange(root)}`, { kind: 'root', key: root.key },
|
|
385
|
+
])));
|
|
273
386
|
if (resource?.physicalResourceId !== undefined) host.appendChild(this.relationGroup('Allocation', [
|
|
274
|
-
[
|
|
387
|
+
[resource.physicalResourceId, { kind: 'allocation', id: resource.physicalResourceId }],
|
|
275
388
|
]));
|
|
276
389
|
break;
|
|
277
390
|
}
|
|
@@ -283,17 +396,19 @@ export class InspectorView {
|
|
|
283
396
|
break;
|
|
284
397
|
}
|
|
285
398
|
case 'root': {
|
|
286
|
-
const root = snapshot.roots
|
|
399
|
+
const root = snapshot.roots.find((root) => root.key === selection.key);
|
|
287
400
|
const relations: Array<readonly [string, Selection]> = [];
|
|
288
401
|
if (root?.nodeId !== undefined && snapshot.nodeById.has(root.nodeId)) relations.push([
|
|
289
402
|
this.accessNodeLabel(snapshot, root.nodeId), { kind: 'node', id: root.nodeId },
|
|
290
403
|
]);
|
|
291
|
-
if (root?.resource)
|
|
292
|
-
|
|
404
|
+
if (root?.resource) host.appendChild(this.relationGroup('Resource', [[labelResource(root.resource), { kind: 'resource', id: root.resource.id }]]));
|
|
405
|
+
if (root?.resource && root.resolution?.usesInitialContents) relations.push([`Initial contents · ${labelResource(root.resource)}`, { kind: 'resource', id: root.resource.id }]);
|
|
406
|
+
for (const id of root?.resolution?.producerNodeIds ?? []) relations.push([this.accessNodeLabel(snapshot, id), { kind: 'node', id }]);
|
|
407
|
+
host.appendChild(this.relationGroup('Output sources', relations));
|
|
293
408
|
break;
|
|
294
409
|
}
|
|
295
410
|
case 'culled': {
|
|
296
|
-
const culled = snapshot.
|
|
411
|
+
const culled = snapshot.culledById.get(selection.id);
|
|
297
412
|
if (culled) host.append(
|
|
298
413
|
this.accessRelations('Reads', culled.node.reads),
|
|
299
414
|
this.accessRelations('Writes', culled.node.writes),
|
|
@@ -309,7 +424,7 @@ export class InspectorView {
|
|
|
309
424
|
break;
|
|
310
425
|
}
|
|
311
426
|
}
|
|
312
|
-
if (host.childElementCount ===
|
|
427
|
+
if (host.childElementCount === 1) {
|
|
313
428
|
const empty = document.createElement('p');
|
|
314
429
|
empty.className = 'zenfg-inspector-muted';
|
|
315
430
|
empty.textContent = 'No related objects.';
|
|
@@ -320,8 +435,9 @@ export class InspectorView {
|
|
|
320
435
|
|
|
321
436
|
private accessRelations(title: string, accesses: readonly FrameGraphDebugAccess[]): HTMLElement {
|
|
322
437
|
return this.relationGroup(title, accesses.map((access) => [
|
|
323
|
-
|
|
438
|
+
labelResource(access.resource),
|
|
324
439
|
{ kind: 'resource', id: access.resource.id },
|
|
440
|
+
formatAccessFacts(access),
|
|
325
441
|
]));
|
|
326
442
|
}
|
|
327
443
|
|
|
@@ -331,13 +447,27 @@ export class InspectorView {
|
|
|
331
447
|
]));
|
|
332
448
|
}
|
|
333
449
|
|
|
334
|
-
private relationGroup(title: string, relations: readonly
|
|
450
|
+
private relationGroup(title: string, relations: readonly DetailRelation[]): HTMLElement {
|
|
335
451
|
const section = document.createElement('section');
|
|
336
452
|
const heading = document.createElement('h3');
|
|
337
453
|
heading.textContent = `${title} ${relations.length}`;
|
|
338
454
|
section.appendChild(heading);
|
|
339
|
-
for (const [label, selection] of relations) {
|
|
340
|
-
|
|
455
|
+
for (const [label, selection, description] of relations) {
|
|
456
|
+
const link = this.createDetailLink(label, selection);
|
|
457
|
+
const entry = document.createElement('div');
|
|
458
|
+
entry.className = 'zenfg-inspector-relation-entry';
|
|
459
|
+
const links = document.createElement('div');
|
|
460
|
+
links.className = 'zenfg-inspector-relation-links';
|
|
461
|
+
links.appendChild(link);
|
|
462
|
+
if (this.callbacks.onReveal) links.appendChild(this.revealButton(selection, this.primaryPage(selection)));
|
|
463
|
+
entry.appendChild(links);
|
|
464
|
+
if (description !== undefined) {
|
|
465
|
+
const metadata = document.createElement('span');
|
|
466
|
+
metadata.className = 'zenfg-inspector-muted';
|
|
467
|
+
metadata.textContent = description;
|
|
468
|
+
entry.appendChild(metadata);
|
|
469
|
+
}
|
|
470
|
+
section.appendChild(entry);
|
|
341
471
|
}
|
|
342
472
|
if (relations.length === 0) {
|
|
343
473
|
const empty = document.createElement('span');
|
|
@@ -348,15 +478,47 @@ export class InspectorView {
|
|
|
348
478
|
return section;
|
|
349
479
|
}
|
|
350
480
|
|
|
351
|
-
private
|
|
481
|
+
private selectRelated(selection: Selection): void {
|
|
482
|
+
this.clearLinkHover();
|
|
483
|
+
this.callbacks.onSelect(selection);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private viewResourceButton(snapshot: FrameGraphDebugViewModel, resourceId: string): HTMLButtonElement {
|
|
487
|
+
return this.createDetailLink(`View resource · ${resourceLabel(snapshot, resourceId)}`,
|
|
488
|
+
{ kind: 'resource', id: resourceId });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private createDetailLink(label: string, selection: Selection): HTMLButtonElement {
|
|
492
|
+
const link = createRelationButton(label, selection, (target) => this.selectRelated(target));
|
|
493
|
+
link.addEventListener('mouseenter', () => {
|
|
494
|
+
this.hoveredLink = link;
|
|
495
|
+
this.callbacks.onHover(selection);
|
|
496
|
+
});
|
|
497
|
+
link.addEventListener('mouseleave', () => {
|
|
498
|
+
if (this.hoveredLink === link) this.clearLinkHover();
|
|
499
|
+
});
|
|
500
|
+
return link;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private clearLinkHover(): void {
|
|
504
|
+
if (!this.hoveredLink) return;
|
|
505
|
+
this.hoveredLink = undefined;
|
|
506
|
+
this.callbacks.onHover(undefined);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private summary(rows: readonly (readonly [string, string | HTMLElement])[]): HTMLElement {
|
|
352
510
|
const summary = document.createElement('dl');
|
|
353
511
|
summary.className = 'zenfg-inspector-inspector-summary';
|
|
354
512
|
for (const [label, value] of rows) {
|
|
355
513
|
const term = document.createElement('dt');
|
|
356
514
|
term.textContent = label;
|
|
357
515
|
const description = document.createElement('dd');
|
|
358
|
-
|
|
359
|
-
|
|
516
|
+
if (typeof value === 'string') {
|
|
517
|
+
description.textContent = value;
|
|
518
|
+
description.title = value;
|
|
519
|
+
} else {
|
|
520
|
+
description.appendChild(value);
|
|
521
|
+
}
|
|
360
522
|
summary.append(term, description);
|
|
361
523
|
}
|
|
362
524
|
return summary;
|
|
@@ -365,13 +527,118 @@ export class InspectorView {
|
|
|
365
527
|
private accessNodeLabel(snapshot: FrameGraphDebugViewModel, nodeId: string): string {
|
|
366
528
|
const node = snapshot.nodeById.get(nodeId);
|
|
367
529
|
if (node) return labelNode(node);
|
|
368
|
-
const culled = snapshot.
|
|
369
|
-
return culled ? `${labelNode(culled.node)} (culled)` :
|
|
530
|
+
const culled = snapshot.culledById.get(nodeId);
|
|
531
|
+
return culled ? `${labelNode(culled.node)} (culled)` : nodeId;
|
|
370
532
|
}
|
|
371
533
|
|
|
372
534
|
private accessNodeSelection(snapshot: FrameGraphDebugViewModel, nodeId: string): Selection | undefined {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
535
|
+
return resolveNodeSelection(snapshot, nodeId);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private nodeLink(snapshot: FrameGraphDebugViewModel, id: string): string | HTMLButtonElement {
|
|
539
|
+
const selected = resolveNodeSelection(snapshot, id);
|
|
540
|
+
return selected ? this.createDetailLink(this.accessNodeLabel(snapshot, id), selected) : id;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private primaryPage(selection: Selection): WorkbenchTab {
|
|
544
|
+
switch (selection.kind) {
|
|
545
|
+
case 'node': case 'culled': case 'group': return 'passes';
|
|
546
|
+
case 'resource': return 'resources';
|
|
547
|
+
case 'allocation': return 'memory';
|
|
548
|
+
case 'root': case 'segment': return 'diagnostics';
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private revealButton(selection: Selection, page: WorkbenchTab): HTMLButtonElement {
|
|
553
|
+
const button = document.createElement('button');
|
|
554
|
+
button.type = 'button';
|
|
555
|
+
button.className = 'zenfg-inspector-inline-action';
|
|
556
|
+
button.textContent = `Locate in ${page[0].toUpperCase()}${page.slice(1)}`;
|
|
557
|
+
button.addEventListener('click', () => {
|
|
558
|
+
this.clearLinkHover();
|
|
559
|
+
this.callbacks.onReveal?.(selection, page);
|
|
560
|
+
});
|
|
561
|
+
return button;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private createLocationActions(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement {
|
|
565
|
+
const actions = document.createElement('div');
|
|
566
|
+
actions.className = 'zenfg-inspector-detail-actions';
|
|
567
|
+
if (!this.callbacks.onReveal) return actions;
|
|
568
|
+
actions.appendChild(this.revealButton(selection, this.primaryPage(selection)));
|
|
569
|
+
if (selection.kind === 'node' || selection.kind === 'group' || selection.kind === 'resource' || selection.kind === 'root') {
|
|
570
|
+
actions.appendChild(this.revealButton(selection, 'graph'));
|
|
571
|
+
}
|
|
572
|
+
if (selection.kind === 'resource' && snapshot.resourceById.get(selection.id)?.origin === 'transient') actions.appendChild(this.revealButton(selection, 'memory'));
|
|
573
|
+
if (selection.kind === 'culled') {
|
|
574
|
+
const note = document.createElement('span');
|
|
575
|
+
note.className = 'zenfg-inspector-muted';
|
|
576
|
+
note.textContent = 'Culled passes are absent from Graph. Locate them in Passes.';
|
|
577
|
+
actions.appendChild(note);
|
|
578
|
+
}
|
|
579
|
+
return actions;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
private selectionId(snapshot: FrameGraphDebugViewModel, selection: Selection): string | undefined {
|
|
583
|
+
if ('id' in selection) return selection.id;
|
|
584
|
+
if (selection.kind === 'group') return snapshot.groupByPathKey.get(selection.pathKey)?.id;
|
|
585
|
+
if (selection.kind === 'segment') return snapshot.segmentByIndex.get(selection.index)?.id;
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private copyId(id: string): HTMLElement {
|
|
590
|
+
const host = document.createElement('span');
|
|
591
|
+
host.className = 'zenfg-inspector-copy-id';
|
|
592
|
+
const code = document.createElement('code');
|
|
593
|
+
code.textContent = id;
|
|
594
|
+
const button = document.createElement('button');
|
|
595
|
+
button.type = 'button';
|
|
596
|
+
button.textContent = 'Copy ID';
|
|
597
|
+
button.setAttribute('aria-label', 'Copy full object ID');
|
|
598
|
+
button.addEventListener('click', () => {
|
|
599
|
+
void writeClipboardText(id).then(() => { button.textContent = 'Copied'; }, () => { button.textContent = 'Copy failed'; });
|
|
600
|
+
});
|
|
601
|
+
host.append(code, button);
|
|
602
|
+
return host;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private createDiagnostics(snapshot: FrameGraphDebugViewModel, selection: Selection): HTMLElement | undefined {
|
|
606
|
+
let diagnostics: FrameGraphDebugViewModel['protocol']['diagnostics'] = [];
|
|
607
|
+
if (selection.kind === 'node' || selection.kind === 'culled') diagnostics = snapshot.diagnosticsByNodeId.get(selection.id) ?? [];
|
|
608
|
+
else if (selection.kind === 'resource') diagnostics = snapshot.diagnosticsByResourceId.get(selection.id) ?? [];
|
|
609
|
+
else if (selection.kind === 'allocation') {
|
|
610
|
+
const resourceIds = new Set(snapshot.allocationById.get(selection.id)?.resourceIds);
|
|
611
|
+
diagnostics = snapshot.protocol.diagnostics.filter((diagnostic) => diagnostic.resourceId !== undefined && resourceIds.has(diagnostic.resourceId));
|
|
612
|
+
} else if (selection.kind === 'group') {
|
|
613
|
+
const groupId = snapshot.groupByPathKey.get(selection.pathKey)?.id;
|
|
614
|
+
const groups = new Set(snapshot.debugGroups.filter((group) => groupId !== undefined && group.ancestorIds.includes(groupId)).map((group) => group.id));
|
|
615
|
+
diagnostics = snapshot.protocol.diagnostics.filter((diagnostic) => {
|
|
616
|
+
const nodeGroup = diagnostic.nodeId ? snapshot.canonicalNodeById.get(diagnostic.nodeId)?.groupId : undefined;
|
|
617
|
+
const resourceGroup = diagnostic.resourceId ? snapshot.resourceById.get(diagnostic.resourceId)?.debugGroupId : undefined;
|
|
618
|
+
return (nodeGroup !== undefined && groups.has(nodeGroup)) || (resourceGroup !== undefined && groups.has(resourceGroup));
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
if (!diagnostics.length) return undefined;
|
|
622
|
+
const section = document.createElement('section');
|
|
623
|
+
section.className = 'zenfg-inspector-detail-diagnostics';
|
|
624
|
+
const heading = document.createElement('h3');
|
|
625
|
+
heading.textContent = `Related diagnostics ${diagnostics.length}`;
|
|
626
|
+
section.appendChild(heading);
|
|
627
|
+
for (const diagnostic of diagnostics) {
|
|
628
|
+
const entry = document.createElement('article');
|
|
629
|
+
entry.dataset.severity = diagnostic.severity;
|
|
630
|
+
const label = document.createElement('strong');
|
|
631
|
+
label.textContent = `${diagnostic.severity} · ${diagnostic.code}`;
|
|
632
|
+
const message = document.createElement('p');
|
|
633
|
+
message.textContent = diagnostic.message;
|
|
634
|
+
entry.append(label, message);
|
|
635
|
+
if (diagnostic.nodeId) {
|
|
636
|
+
const node = resolveNodeSelection(snapshot, diagnostic.nodeId);
|
|
637
|
+
if (node) entry.appendChild(this.createDetailLink(this.accessNodeLabel(snapshot, diagnostic.nodeId), node));
|
|
638
|
+
}
|
|
639
|
+
if (diagnostic.resourceId) entry.appendChild(this.viewResourceButton(snapshot, diagnostic.resourceId));
|
|
640
|
+
section.appendChild(entry);
|
|
641
|
+
}
|
|
642
|
+
return section;
|
|
376
643
|
}
|
|
377
644
|
}
|