@zenfg/inspector 0.1.0-beta.3 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/GUIDE.md +182 -0
  2. package/README.md +61 -89
  3. package/THEMING.md +124 -0
  4. package/dist/FrameGraphInspector.d.ts +23 -4
  5. package/dist/FrameGraphInspector.d.ts.map +1 -1
  6. package/dist/FrameGraphInspector.js +137 -29
  7. package/dist/FrameGraphInspector.js.map +1 -1
  8. package/dist/capture.d.ts +5 -0
  9. package/dist/capture.d.ts.map +1 -0
  10. package/dist/capture.js +2 -0
  11. package/dist/capture.js.map +1 -0
  12. package/dist/debugCaptureModel.d.ts +19 -0
  13. package/dist/debugCaptureModel.d.ts.map +1 -1
  14. package/dist/debugCaptureModel.js +36 -2
  15. package/dist/debugCaptureModel.js.map +1 -1
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/panelAliasAnalysis.d.ts +1 -0
  20. package/dist/panelAliasAnalysis.d.ts.map +1 -1
  21. package/dist/panelAliasAnalysis.js +23 -54
  22. package/dist/panelAliasAnalysis.js.map +1 -1
  23. package/dist/panelCytoscapeGraphRenderer.d.ts +15 -2
  24. package/dist/panelCytoscapeGraphRenderer.d.ts.map +1 -1
  25. package/dist/panelCytoscapeGraphRenderer.js +145 -23
  26. package/dist/panelCytoscapeGraphRenderer.js.map +1 -1
  27. package/dist/panelDetailLayout.d.ts +27 -0
  28. package/dist/panelDetailLayout.d.ts.map +1 -0
  29. package/dist/panelDetailLayout.js +174 -0
  30. package/dist/panelDetailLayout.js.map +1 -0
  31. package/dist/panelDetailStyles.d.ts +2 -0
  32. package/dist/panelDetailStyles.d.ts.map +1 -0
  33. package/dist/panelDetailStyles.js +57 -0
  34. package/dist/panelDetailStyles.js.map +1 -0
  35. package/dist/panelDiagnosticsView.d.ts +10 -1
  36. package/dist/panelDiagnosticsView.d.ts.map +1 -1
  37. package/dist/panelDiagnosticsView.js +166 -57
  38. package/dist/panelDiagnosticsView.js.map +1 -1
  39. package/dist/panelDomHelpers.d.ts.map +1 -1
  40. package/dist/panelDomHelpers.js +6 -5
  41. package/dist/panelDomHelpers.js.map +1 -1
  42. package/dist/panelGraphLayout.d.ts +3 -2
  43. package/dist/panelGraphLayout.d.ts.map +1 -1
  44. package/dist/panelGraphLayout.js +6 -5
  45. package/dist/panelGraphLayout.js.map +1 -1
  46. package/dist/panelGraphRenderer.d.ts +10 -0
  47. package/dist/panelGraphRenderer.d.ts.map +1 -1
  48. package/dist/panelGraphRenderer.js.map +1 -1
  49. package/dist/panelGraphScene.d.ts +1 -0
  50. package/dist/panelGraphScene.d.ts.map +1 -1
  51. package/dist/panelGraphScene.js +69 -32
  52. package/dist/panelGraphScene.js.map +1 -1
  53. package/dist/panelGraphSearch.d.ts +16 -0
  54. package/dist/panelGraphSearch.d.ts.map +1 -0
  55. package/dist/panelGraphSearch.js +93 -0
  56. package/dist/panelGraphSearch.js.map +1 -0
  57. package/dist/panelGraphView.d.ts +82 -0
  58. package/dist/panelGraphView.d.ts.map +1 -1
  59. package/dist/panelGraphView.js +15 -4
  60. package/dist/panelGraphView.js.map +1 -1
  61. package/dist/panelGraphVisuals.d.ts +14 -6
  62. package/dist/panelGraphVisuals.d.ts.map +1 -1
  63. package/dist/panelGraphVisuals.js +126 -51
  64. package/dist/panelGraphVisuals.js.map +1 -1
  65. package/dist/panelInspectorView.d.ts +8 -0
  66. package/dist/panelInspectorView.d.ts.map +1 -1
  67. package/dist/panelInspectorView.js +242 -48
  68. package/dist/panelInspectorView.js.map +1 -1
  69. package/dist/panelListStyles.d.ts +3 -0
  70. package/dist/panelListStyles.d.ts.map +1 -0
  71. package/dist/panelListStyles.js +155 -0
  72. package/dist/panelListStyles.js.map +1 -0
  73. package/dist/panelMemoryStyles.d.ts +3 -0
  74. package/dist/panelMemoryStyles.d.ts.map +1 -0
  75. package/dist/panelMemoryStyles.js +42 -0
  76. package/dist/panelMemoryStyles.js.map +1 -0
  77. package/dist/panelMemoryView.d.ts +13 -1
  78. package/dist/panelMemoryView.d.ts.map +1 -1
  79. package/dist/panelMemoryView.js +161 -41
  80. package/dist/panelMemoryView.js.map +1 -1
  81. package/dist/panelPassesView.d.ts +15 -0
  82. package/dist/panelPassesView.d.ts.map +1 -1
  83. package/dist/panelPassesView.js +202 -72
  84. package/dist/panelPassesView.js.map +1 -1
  85. package/dist/panelRawView.d.ts +15 -0
  86. package/dist/panelRawView.d.ts.map +1 -0
  87. package/dist/panelRawView.js +105 -0
  88. package/dist/panelRawView.js.map +1 -0
  89. package/dist/panelResourcesView.d.ts +8 -0
  90. package/dist/panelResourcesView.d.ts.map +1 -1
  91. package/dist/panelResourcesView.js +62 -31
  92. package/dist/panelResourcesView.js.map +1 -1
  93. package/dist/panelSelection.d.ts +9 -1
  94. package/dist/panelSelection.d.ts.map +1 -1
  95. package/dist/panelSelection.js +34 -4
  96. package/dist/panelSelection.js.map +1 -1
  97. package/dist/panelTheme.d.ts +13 -0
  98. package/dist/panelTheme.d.ts.map +1 -0
  99. package/dist/panelTheme.js +104 -0
  100. package/dist/panelTheme.js.map +1 -0
  101. package/dist/panelTypes.d.ts +10 -1
  102. package/dist/panelTypes.d.ts.map +1 -1
  103. package/dist/panelTypes.js.map +1 -1
  104. package/dist/panelVisualTheme.d.ts +147 -50
  105. package/dist/panelVisualTheme.d.ts.map +1 -1
  106. package/dist/panelVisualTheme.js +34 -44
  107. package/dist/panelVisualTheme.js.map +1 -1
  108. package/dist/panelWorkbenchHelpers.d.ts +12 -1
  109. package/dist/panelWorkbenchHelpers.d.ts.map +1 -1
  110. package/dist/panelWorkbenchHelpers.js +62 -1
  111. package/dist/panelWorkbenchHelpers.js.map +1 -1
  112. package/dist/panelWorkbenchStyles.d.ts +2 -0
  113. package/dist/panelWorkbenchStyles.d.ts.map +1 -0
  114. package/dist/panelWorkbenchStyles.js +101 -0
  115. package/dist/panelWorkbenchStyles.js.map +1 -0
  116. package/dist/panelWorkbenchView.d.ts +16 -0
  117. package/dist/panelWorkbenchView.d.ts.map +1 -1
  118. package/dist/panelWorkbenchView.js +228 -50
  119. package/dist/panelWorkbenchView.js.map +1 -1
  120. package/dist/query.js.map +1 -1
  121. package/dist/styles.d.ts.map +1 -1
  122. package/dist/styles.js +147 -195
  123. package/dist/styles.js.map +1 -1
  124. package/dist/theme.d.ts +22 -0
  125. package/dist/theme.d.ts.map +1 -0
  126. package/dist/theme.js +10 -0
  127. package/dist/theme.js.map +1 -0
  128. package/dist/themeDefinitions.d.ts +251 -0
  129. package/dist/themeDefinitions.d.ts.map +1 -0
  130. package/dist/themeDefinitions.js +96 -0
  131. package/dist/themeDefinitions.js.map +1 -0
  132. package/dist/themePalette.d.ts +66 -0
  133. package/dist/themePalette.d.ts.map +1 -0
  134. package/dist/themePalette.js +37 -0
  135. package/dist/themePalette.js.map +1 -0
  136. package/dist/themes.css +165 -0
  137. package/package.json +44 -10
  138. package/src/FrameGraphInspector.ts +130 -31
  139. package/src/capture.ts +4 -0
  140. package/src/debugCaptureModel.ts +53 -2
  141. package/src/index.ts +6 -2
  142. package/src/panelAliasAnalysis.ts +24 -70
  143. package/src/panelCytoscapeGraphRenderer.ts +140 -23
  144. package/src/panelDetailLayout.ts +155 -0
  145. package/src/panelDetailStyles.ts +56 -0
  146. package/src/panelDiagnosticsView.ts +166 -85
  147. package/src/panelDomHelpers.ts +4 -5
  148. package/src/panelGraphLayout.ts +6 -5
  149. package/src/panelGraphRenderer.ts +7 -0
  150. package/src/panelGraphScene.ts +68 -30
  151. package/src/panelGraphSearch.ts +96 -0
  152. package/src/panelGraphView.ts +14 -4
  153. package/src/panelGraphVisuals.ts +122 -52
  154. package/src/panelInspectorView.ts +227 -48
  155. package/src/panelListStyles.ts +154 -0
  156. package/src/panelMemoryStyles.ts +41 -0
  157. package/src/panelMemoryView.ts +178 -53
  158. package/src/panelPassesView.ts +190 -137
  159. package/src/panelRawView.ts +99 -0
  160. package/src/panelResourcesView.ts +69 -56
  161. package/src/panelSelection.ts +37 -5
  162. package/src/panelTheme.ts +85 -0
  163. package/src/panelTypes.ts +7 -1
  164. package/src/panelVisualTheme.ts +38 -49
  165. package/src/panelWorkbenchHelpers.ts +60 -2
  166. package/src/panelWorkbenchStyles.ts +100 -0
  167. package/src/panelWorkbenchView.ts +201 -52
  168. package/src/query.ts +11 -11
  169. package/src/styles.ts +148 -195
  170. package/src/theme.ts +24 -0
  171. package/src/themeDefinitions.ts +103 -0
  172. package/src/themePalette.ts +44 -0
@@ -12,8 +12,10 @@ import { MemoryView } from './panelMemoryView.ts';
12
12
  import { PassesView } from './panelPassesView.ts';
13
13
  import { ResourcesView } from './panelResourcesView.ts';
14
14
  import { renderGraphView, resizeGraph } from './panelGraphView.ts';
15
+ import { DetailLayout } from './panelDetailLayout.ts';
16
+ import { GraphSearch } from './panelGraphSearch.ts';
15
17
  import type { GraphViewState, Selection, WorkbenchTab } from './panelTypes.ts';
16
- import { formatEstimatedBytes, type WorkbenchCallbacks } from './panelWorkbenchHelpers.ts';
18
+ import { enableTabKeyboard, formatEstimatedBytes, formatEstimateCoverage, formatTimingCoverage, type WorkbenchCallbacks } from './panelWorkbenchHelpers.ts';
17
19
 
18
20
  export type FrameGraphDebugWorkbenchActions = {
19
21
  onCapture(): void;
@@ -47,6 +49,8 @@ export class FrameGraphDebugWorkbench {
47
49
  private readonly tabList = document.createElement('div');
48
50
  private readonly commandActions = document.createElement('div');
49
51
  private readonly commandStatus = document.createElement('span');
52
+ private readonly feedback = document.createElement('details');
53
+ private readonly feedbackTitle = document.createElement('summary');
50
54
  private readonly workspace = document.createElement('div');
51
55
  private readonly main = document.createElement('main');
52
56
  private readonly emptyHost = document.createElement('div');
@@ -57,6 +61,10 @@ export class FrameGraphDebugWorkbench {
57
61
  private readonly memory: MemoryView;
58
62
  private readonly diagnostics: DiagnosticsView;
59
63
  private readonly inspector: InspectorView;
64
+ private readonly detailLayout: DetailLayout;
65
+ private readonly graphSearch: GraphSearch;
66
+ private readonly dirtyViews = new Set<WorkbenchTab>();
67
+ private readonly captureDetails = document.createElement('details');
60
68
  private readonly inspectorOpenButton = document.createElement('button');
61
69
  private readonly captureButton = document.createElement('button');
62
70
  private readonly importButton = document.createElement('button');
@@ -72,6 +80,7 @@ export class FrameGraphDebugWorkbench {
72
80
  private snapshot: FrameGraphDebugViewModel | undefined;
73
81
  private selected: Selection | undefined;
74
82
  private hovered: Selection | undefined;
83
+ private destroyed = false;
75
84
 
76
85
  constructor(
77
86
  private readonly graphView: GraphViewState,
@@ -94,7 +103,7 @@ export class FrameGraphDebugWorkbench {
94
103
  this.commandStatus.className = 'zenfg-inspector-command-status';
95
104
  this.commandStatus.setAttribute('role', 'status');
96
105
  this.commandStatus.setAttribute('aria-live', 'polite');
97
- this.workspace.className = 'zenfg-inspector-workspace inspector-open';
106
+ this.workspace.className = 'zenfg-inspector-workspace';
98
107
  this.main.className = 'zenfg-inspector-main';
99
108
  this.emptyHost.className = 'zenfg-inspector-workbench-empty';
100
109
  this.emptyHost.setAttribute('role', 'status');
@@ -107,12 +116,25 @@ export class FrameGraphDebugWorkbench {
107
116
  this.graphRoot.className = 'zenfg-inspector-view zenfg-inspector-graph-view';
108
117
  this.graphRoot.id = `${options.idPrefix}-view-graph`;
109
118
  this.graphRoot.setAttribute('role', 'tabpanel');
110
- this.graphRoot.append(this.graphView.toolbar, this.graphView.host);
119
+ const graphViewport = document.createElement('div');
120
+ graphViewport.className = 'zenfg-inspector-graph-viewport';
121
+ graphViewport.append(this.graphView.host, this.graphView.toolbar);
122
+ if (this.graphView.legend) {
123
+ const legend = document.createElement('details');
124
+ legend.className = 'zenfg-inspector-legend-details';
125
+ const title = document.createElement('summary');
126
+ title.textContent = 'Legend';
127
+ legend.append(title, this.graphView.legend);
128
+ graphViewport.append(legend);
129
+ }
130
+ this.graphRoot.append(graphViewport);
111
131
  this.passes = new PassesView(callbacks, options.idPrefix);
112
132
  this.resources = new ResourcesView(callbacks, options.idPrefix);
113
133
  this.memory = new MemoryView(callbacks, options.idPrefix);
114
134
  this.diagnostics = new DiagnosticsView(callbacks, options.idPrefix);
115
135
  this.inspector = new InspectorView(callbacks, (open) => this.handleInspectorOpenChange(open), options.idPrefix);
136
+ this.graphSearch = new GraphSearch((selection) => callbacks.onReveal?.(selection, 'graph'), `${options.idPrefix}-graph-search`);
137
+ this.graphView.toolbar.prepend(this.graphSearch.root);
116
138
 
117
139
  this.views.set('overview', this.overviewRoot);
118
140
  this.views.set('graph', this.graphRoot);
@@ -138,6 +160,7 @@ export class FrameGraphDebugWorkbench {
138
160
  this.tabButtons.set(tab, button);
139
161
  this.tabList.appendChild(button);
140
162
  }
163
+ enableTabKeyboard(this.tabList);
141
164
 
142
165
  this.inspectorOpenButton.type = 'button';
143
166
  this.inspectorOpenButton.hidden = true;
@@ -191,7 +214,6 @@ export class FrameGraphDebugWorkbench {
191
214
  this.exportMenu.append(this.downloadButton, this.copyButton);
192
215
 
193
216
  this.commandActions.append(
194
- this.commandStatus,
195
217
  this.inspectorOpenButton,
196
218
  this.captureButton,
197
219
  this.importButton,
@@ -206,14 +228,15 @@ export class FrameGraphDebugWorkbench {
206
228
  const targetNode = target as Node;
207
229
  if (!this.exportButton.contains(targetNode) && !this.exportMenu.contains(targetNode)) this.setExportMenuOpen(false);
208
230
  });
209
- this.root.addEventListener('keydown', (event) => {
210
- if (event.key !== 'Escape' || this.exportMenu.hidden) return;
211
- this.setExportMenuOpen(false);
212
- this.exportButton.focus();
213
- });
231
+ this.root.addEventListener('keydown', (event) => this.handleMenuKey(event));
214
232
  this.main.append(this.emptyHost, ...this.views.values());
215
233
  this.workspace.append(this.main, this.inspector.root);
216
- this.root.append(this.commandBar, this.workspace);
234
+ this.feedback.className = 'zenfg-inspector-feedback';
235
+ this.feedback.hidden = true;
236
+ this.feedback.append(this.feedbackTitle, this.commandStatus);
237
+ this.root.append(this.commandBar, this.feedback, this.workspace);
238
+ this.detailLayout = new DetailLayout(this.workspace, this.main, this.inspector.root, this.commandBar, () => this.inspector.setOpen(false));
239
+ this.workspace.addEventListener('detail-layout-change', () => this.resizeGraph());
217
240
  this.showEmptyState('empty', 'Drop a ZenFG Snapshot here or choose Import.', 'Files are processed locally in your browser.');
218
241
  this.setSnapshotActionState({
219
242
  providerAvailable: false,
@@ -230,18 +253,13 @@ export class FrameGraphDebugWorkbench {
230
253
  this.snapshot = snapshot;
231
254
  this.selected = selected;
232
255
  this.hovered = undefined;
233
- this.renderSummary(snapshot);
234
- this.passes.setSnapshot(snapshot);
235
- this.resources.setSnapshot(snapshot);
236
- this.memory.setSnapshot(snapshot);
237
- this.diagnostics.setSnapshot(snapshot);
256
+ for (const tab of this.views.keys()) this.dirtyViews.add(tab);
257
+ this.renderDiagnosticBadge(snapshot);
258
+ this.graphSearch.setSnapshot(snapshot);
238
259
  this.inspector.setSnapshot(snapshot);
239
- this.passes.setSelection(selected);
240
- this.resources.setSelection(selected);
241
- this.memory.setSelection(selected);
242
- this.diagnostics.setSelection(selected);
243
260
  this.inspector.setSelection(selected, false);
244
261
  this.emptyHost.hidden = true;
262
+ this.ensureActiveView();
245
263
  this.updateActiveTab();
246
264
  this.updateWorkspaceState();
247
265
  if (this.activeTab === 'graph') this.renderGraph();
@@ -303,14 +321,14 @@ export class FrameGraphDebugWorkbench {
303
321
  this.commandStatus.dataset.tone = state.message ? state.messageTone ?? 'error' : 'neutral';
304
322
  if (state.message) this.commandStatus.title = state.message;
305
323
  else this.commandStatus.removeAttribute('title');
306
- }
324
+ this.feedback.hidden = !state.message;
325
+ this.feedback.dataset.tone = state.messageTone ?? 'neutral';
326
+ this.feedbackTitle.textContent = state.message ? state.messageTone === 'error' ? 'Operation failed · show details' : 'Operation feedback · show details' : '';
327
+ }
307
328
 
308
329
  setSelection(selected: Selection | undefined): void {
309
330
  this.selected = selected;
310
- this.passes.setSelection(selected);
311
- this.resources.setSelection(selected);
312
- this.memory.setSelection(selected);
313
- this.diagnostics.setSelection(selected);
331
+ this.updateActiveSelection();
314
332
  this.inspector.setSelection(selected);
315
333
  if (this.activeTab === 'graph') this.renderGraph();
316
334
  }
@@ -321,11 +339,19 @@ export class FrameGraphDebugWorkbench {
321
339
  }
322
340
 
323
341
  setActiveTab(tab: WorkbenchTab): void {
342
+ if (this.destroyed) return;
324
343
  if (this.activeTab === tab) return;
344
+ if (this.activeTab === 'graph') {
345
+ this.graphView.revealOnNextRender = undefined;
346
+ this.graphView.renderer?.cancelReveal?.();
347
+ }
325
348
  this.activeTab = tab;
326
349
  this.updateActiveTab();
350
+ this.ensureActiveView();
351
+ this.updateWorkspaceState();
327
352
  if (tab === 'graph') {
328
353
  window.requestAnimationFrame(() => {
354
+ if (this.destroyed || this.activeTab !== 'graph') return;
329
355
  resizeGraph(this.graphView);
330
356
  this.renderGraph();
331
357
  });
@@ -334,13 +360,13 @@ export class FrameGraphDebugWorkbench {
334
360
 
335
361
  refreshGraphStructure(): void {
336
362
  if (!this.snapshot) return;
337
- this.passes.setSnapshot(this.snapshot);
338
363
  if (this.activeTab === 'graph') this.renderGraph();
339
364
  }
340
365
 
341
366
  resizeGraph(fit = false): void {
342
367
  if (fit) this.graphView.fitOnNextRender = true;
343
368
  window.requestAnimationFrame(() => {
369
+ if (this.destroyed) return;
344
370
  resizeGraph(this.graphView);
345
371
  if (this.activeTab === 'graph') this.renderGraph();
346
372
  });
@@ -352,6 +378,7 @@ export class FrameGraphDebugWorkbench {
352
378
  button.disabled = !this.hasSnapshot;
353
379
  button.classList.toggle('active', active);
354
380
  button.setAttribute('aria-selected', active ? 'true' : 'false');
381
+ button.tabIndex = active ? 0 : -1;
355
382
  const view = this.views.get(tab)!;
356
383
  view.hidden = !this.hasSnapshot || !active;
357
384
  view.setAttribute('aria-labelledby', button.id);
@@ -375,9 +402,7 @@ export class FrameGraphDebugWorkbench {
375
402
  const frameGraphSegments = snapshot.executionSegments.filter((segment) => segment.kind === 'frame-graph').length;
376
403
  const opaqueIntervals = snapshot.executionSegments.length - frameGraphSegments;
377
404
  const metrics = snapshot.metrics;
378
- const coverage = metrics.timingEligibleNodeCount === 0
379
- ? 'No eligible passes'
380
- : `${metrics.timedNodeCount}/${metrics.timingEligibleNodeCount} timed`;
405
+ const coverage = formatTimingCoverage(metrics.timedNodeCount, metrics.timingEligibleNodeCount);
381
406
  const slowest = metrics.slowestNode
382
407
  ? `${labelNode(metrics.slowestNode)} · ${(metrics.slowestNode.gpuDurationMicros! / 1000).toFixed(3)} ms`
383
408
  : 'Unknown';
@@ -394,41 +419,55 @@ export class FrameGraphDebugWorkbench {
394
419
  const poolRetained = snapshot.resourcePool.status === 'available'
395
420
  ? `${snapshot.resourcePool.estimatedRetainedBytes === undefined ? 'Unknown' : formatBytes(snapshot.resourcePool.estimatedRetainedBytes)} · ${snapshot.resourcePool.retainedCount} allocations`
396
421
  : 'Unavailable';
422
+ const counts = this.diagnosticCounts(snapshot);
397
423
  this.summary.replaceChildren(
398
- this.createSummaryGroup('Capture', [
399
- ['Source', snapshot.source.label],
400
- ['Schema', `${protocol.format} v${protocol.version.major}.${protocol.version.minor}`],
401
- ['Producer', producer],
402
- ['Runtime', runtimeLabel],
403
- ['Frame', String(snapshot.frameIndex)],
404
- ...(snapshot.source.migratedFromLegacy ? [['Migration', 'Legacy V0 → V1'] as const] : []),
405
- ['Timing', snapshot.profiling.status === 'available' ? 'Available' : snapshot.profiling.reason],
406
- ]),
407
- this.createSummaryGroup('GPU', [
408
- ['GPU Span', snapshot.profiling.status === 'available' ? `${formatGpuFrameDuration(snapshot)} ms` : 'Unknown'],
409
- ['Coverage', coverage],
410
- ['Slowest', slowest],
411
- ]),
424
+ this.createSummaryGroup('Diagnostics', [
425
+ ['Errors', String(counts.error)], ['Warnings', String(counts.warning)], ['Information', String(counts.info)],
426
+ ], () => this.setActiveTab('diagnostics')),
427
+ this.createSummaryGroup('Execution timing', [
428
+ ['CPU execute', snapshot.cpuProfiling.status === 'available' ? (snapshot.cpuProfiling.executionDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'],
429
+ ['CPU pass sum', snapshot.metrics.cpuTimedNodeCount ? (snapshot.metrics.cpuWorkDurationMicros / 1000).toFixed(3) + ' ms' : 'Not collected'],
430
+ ['CPU coverage', formatTimingCoverage(snapshot.metrics.cpuTimedNodeCount, snapshot.nodes.length)],
431
+ ['GPU span', snapshot.profiling.status === 'available' ? `${formatGpuFrameDuration(snapshot)} ms` : 'Not collected'],
432
+ ['GPU coverage', coverage],
433
+ ['Slowest GPU pass', slowest],
434
+ ], () => metrics.slowestNode ? this.callbacks.onReveal?.({ kind: 'node', id: metrics.slowestNode.id }, 'passes') : this.setActiveTab('passes')),
412
435
  this.createSummaryGroup('Work', [
413
436
  ['Nodes', `${snapshot.nodes.length} retained · ${snapshot.culledNodes.length} culled`],
414
437
  ['Segments', `${frameGraphSegments} FG · ${opaqueIntervals} opaque`],
415
438
  ['Groups', snapshot.availability.groups ? String(snapshot.debugGroups.length) : 'Unknown'],
416
439
  ['Recording order', snapshot.availability.recordingOrder ? 'Available' : 'Unknown'],
417
- ]),
440
+ ], () => this.setActiveTab('passes')),
418
441
  this.createSummaryGroup('Resources', [
419
- ['Logical / physical', `${snapshot.resources.length} / ${snapshot.physicalAllocations.length}`],
442
+ ['Logical / physical', `${snapshot.resources.length} / ${protocol.memory.allocationReport.status === 'available' ? snapshot.physicalAllocations.length : 'Unavailable'}`],
420
443
  ['Texture views', snapshot.availability.textureViews ? String(snapshot.textureViewById.size) : 'Unknown'],
421
444
  ['Access regions', snapshot.availability.accessRegions ? 'Available' : 'Unknown'],
422
- ['Transient estimate', formatEstimatedBytes(metrics.transientEstimatedByteSize)],
423
- ]),
445
+ ['Transient estimate', formatEstimateCoverage(metrics.transientEstimatedByteSize, metrics.estimatedCoverage.transient)],
446
+ ], () => this.setActiveTab('resources')),
447
+ this.createSummaryGroup('Memory estimates', [
448
+ ['Physical allocations', protocol.memory.allocationReport.status === 'available' ? formatEstimateCoverage(metrics.physicalEstimatedBytes, metrics.estimatedCoverage.physical) : 'Unavailable'],
449
+ ['Alias reuse', protocol.memory.allocationReport.status === 'available' ? formatEstimatedBytes(metrics.aliasReuseBytes) : 'Unavailable'],
450
+ ], () => this.setActiveTab('memory')),
424
451
  this.createSummaryGroup('Pool', [
425
452
  ['Retained', poolRetained],
426
453
  ['Reuse', formatPoolHitRate(snapshot)],
427
- ]),
454
+ ], () => this.setActiveTab('memory')),
428
455
  );
456
+ const title = document.createElement('summary');
457
+ title.textContent = 'Capture information';
458
+ this.captureDetails.className = 'zenfg-inspector-capture-details';
459
+ this.captureDetails.replaceChildren(title, this.createSummaryGroup('Capture', [
460
+ ['Source', snapshot.source.label], ['Frame', String(snapshot.frameIndex)],
461
+ ['Captured at', protocol.capture.capturedAt ?? 'Unknown'],
462
+ ['Schema', `${protocol.format} v${protocol.version.major}.${protocol.version.minor}`],
463
+ ['Producer', producer], ['Runtime', runtimeLabel],
464
+ ...(protocol.capture.migration ? [['Migration', `${protocol.capture.migration.sourceFormat} → canonical v${protocol.version.major}.${protocol.version.minor}`] as const] : []),
465
+ ['Timing', snapshot.profiling.status === 'available' ? coverage : snapshot.profiling.reason],
466
+ ]));
467
+ this.summary.append(this.captureDetails);
429
468
  }
430
469
 
431
- private createSummaryGroup(title: string, rows: readonly (readonly [string, string])[]): HTMLElement {
470
+ private createSummaryGroup(title: string, rows: readonly (readonly [string, string])[], onNavigate?: () => void): HTMLElement {
432
471
  const group = document.createElement('section');
433
472
  const heading = document.createElement('h2');
434
473
  heading.textContent = title;
@@ -443,12 +482,21 @@ export class FrameGraphDebugWorkbench {
443
482
  row.append(term, description);
444
483
  group.appendChild(row);
445
484
  }
485
+ if (onNavigate) {
486
+ const action = document.createElement('button');
487
+ action.type = 'button';
488
+ action.className = 'zenfg-inspector-relation-button';
489
+ action.textContent = `Explore ${title.toLowerCase()}`;
490
+ action.addEventListener('click', onNavigate);
491
+ group.append(action);
492
+ }
446
493
  return group;
447
494
  }
448
495
 
449
496
  private setExportMenuOpen(open: boolean): void {
450
497
  this.exportMenu.hidden = !open;
451
498
  this.exportButton.setAttribute('aria-expanded', open ? 'true' : 'false');
499
+ if (open) this.downloadButton.focus();
452
500
  }
453
501
 
454
502
  private handleInspectorOpenChange(_open: boolean): void {
@@ -457,10 +505,111 @@ export class FrameGraphDebugWorkbench {
457
505
  }
458
506
 
459
507
  private updateWorkspaceState(): void {
460
- const inspectorOpen = this.hasSnapshot && this.inspector.isOpen;
508
+ const inspectorOpen = this.hasSnapshot && this.inspector.isOpen && this.activeTab !== 'overview';
461
509
  this.workspace.classList.toggle('has-capture', this.hasSnapshot);
462
- this.workspace.classList.toggle('inspector-open', inspectorOpen);
463
- this.inspector.root.classList.toggle('unavailable', !this.hasSnapshot);
464
- this.inspectorOpenButton.hidden = !this.hasSnapshot || inspectorOpen;
510
+ this.inspector.root.classList.toggle('unavailable', !inspectorOpen);
511
+ this.inspectorOpenButton.hidden = !this.hasSnapshot || !this.selected || inspectorOpen || this.activeTab === 'overview';
512
+ this.detailLayout.update(inspectorOpen);
513
+ }
514
+
515
+ reveal(selection: Selection, tab: WorkbenchTab): void {
516
+ if (this.feedbackTitle.textContent === 'Graph location unavailable') {
517
+ this.feedback.hidden = true;
518
+ this.commandStatus.replaceChildren();
519
+ this.commandStatus.hidden = true;
520
+ }
521
+ this.setActiveTab(tab);
522
+ if (tab === 'passes') this.passes.reveal(selection);
523
+ else if (tab === 'resources') this.resources.reveal(selection);
524
+ else if (tab === 'memory') this.memory.reveal(selection);
525
+ else if (tab === 'diagnostics') this.diagnostics.reveal(selection);
526
+ if (this.detailLayout.isDrawer) this.inspector.setOpen(false);
527
+ if (tab === 'graph') this.renderGraph();
528
+ }
529
+
530
+ navigate(tab: WorkbenchTab, filter?: 'culled'): void {
531
+ this.setActiveTab(tab);
532
+ if (tab === 'passes' && filter === 'culled') this.passes.showCulled();
533
+ if (this.detailLayout.isDrawer) this.inspector.setOpen(false);
534
+ }
535
+
536
+ showNavigationIssue(message: string, selection: Selection): void {
537
+ if (this.detailLayout.isDrawer) this.inspector.setOpen(false);
538
+ this.feedback.hidden = false;
539
+ this.feedback.open = true;
540
+ this.feedback.dataset.tone = 'neutral';
541
+ this.feedbackTitle.textContent = 'Graph location unavailable';
542
+ const action = document.createElement('button');
543
+ action.type = 'button';
544
+ action.className = 'zenfg-inspector-relation-button';
545
+ const tab = selection.kind === 'resource' ? 'resources' : selection.kind === 'allocation' ? 'memory'
546
+ : selection.kind === 'root' || selection.kind === 'segment' ? 'diagnostics' : 'passes';
547
+ action.textContent = `Show in ${tab}`;
548
+ action.addEventListener('click', () => this.callbacks.onReveal?.(selection, tab));
549
+ this.commandStatus.hidden = false;
550
+ this.commandStatus.replaceChildren(document.createTextNode(`${message} `), action);
551
+ action.focus();
552
+ }
553
+
554
+ destroy(): void { this.destroyed = true; this.detailLayout.destroy(); }
555
+
556
+ private ensureActiveView(): void {
557
+ const snapshot = this.snapshot;
558
+ if (!snapshot) return;
559
+ if (this.dirtyViews.has(this.activeTab)) {
560
+ switch (this.activeTab) {
561
+ case 'overview': this.renderSummary(snapshot); break;
562
+ case 'passes': this.passes.setSnapshot(snapshot); break;
563
+ case 'resources': this.resources.setSnapshot(snapshot); break;
564
+ case 'memory': this.memory.setSnapshot(snapshot); break;
565
+ case 'diagnostics': this.diagnostics.setSnapshot(snapshot); break;
566
+ }
567
+ this.dirtyViews.delete(this.activeTab);
568
+ }
569
+ this.updateActiveSelection();
570
+ }
571
+
572
+ private updateActiveSelection(): void {
573
+ switch (this.activeTab) {
574
+ case 'passes': this.passes.setSelection(this.selected); break;
575
+ case 'resources': this.resources.setSelection(this.selected); break;
576
+ case 'memory': this.memory.setSelection(this.selected); break;
577
+ case 'diagnostics': this.diagnostics.setSelection(this.selected); break;
578
+ }
579
+ }
580
+
581
+ private diagnosticCounts(snapshot: FrameGraphDebugViewModel): Record<'error' | 'warning' | 'info', number> {
582
+ const counts = { error: 0, warning: 0, info: 0 };
583
+ for (const entry of snapshot.protocol.diagnostics) counts[entry.severity]++;
584
+ return counts;
585
+ }
586
+
587
+ private renderDiagnosticBadge(snapshot: FrameGraphDebugViewModel): void {
588
+ const counts = this.diagnosticCounts(snapshot);
589
+ const tab = this.tabButtons.get('diagnostics')!;
590
+ const label = document.createElement('span');
591
+ label.textContent = 'Diagnostics';
592
+ const badge = document.createElement('span');
593
+ badge.className = 'zenfg-inspector-diagnostic-badge';
594
+ badge.textContent = `${counts.error} / ${counts.warning}`;
595
+ badge.title = `${counts.error} errors, ${counts.warning} warnings`;
596
+ badge.setAttribute('aria-hidden', 'true');
597
+ tab.replaceChildren(label, ...(counts.error + counts.warning ? [badge] : []));
598
+ tab.title = badge.title;
599
+ }
600
+
601
+ private handleMenuKey(event: KeyboardEvent): void {
602
+ if (this.exportMenu.hidden || event.defaultPrevented) return;
603
+ if (event.key === 'Escape') {
604
+ event.preventDefault(); event.stopPropagation();
605
+ this.setExportMenuOpen(false); this.exportButton.focus();
606
+ } else if (['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) {
607
+ event.preventDefault(); event.stopPropagation();
608
+ const items = [this.downloadButton, this.copyButton].filter((button) => !button.disabled);
609
+ const index = items.indexOf(document.activeElement as HTMLButtonElement);
610
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1
611
+ : (index + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
612
+ items[next]?.focus();
613
+ }
465
614
  }
466
615
  }
package/src/query.ts CHANGED
@@ -9,14 +9,14 @@ export const ZENFG_INSPECTOR_QUERY_PARAM = 'fg';
9
9
  * @returns `false` when no URL is available or the supplied URL is invalid.
10
10
  */
11
11
  export function isZenFGInspectorRequested(href = globalThis.location?.href): boolean {
12
- if (!href) {
13
- return false;
14
- }
15
-
16
- try {
17
- return new URL(href).searchParams.has(ZENFG_INSPECTOR_QUERY_PARAM);
18
- }
19
- catch {
20
- return false;
21
- }
22
- }
12
+ if (!href) {
13
+ return false;
14
+ }
15
+
16
+ try {
17
+ return new URL(href).searchParams.has(ZENFG_INSPECTOR_QUERY_PARAM);
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }