@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
@@ -0,0 +1,41 @@
1
+ /** Appended after the base styles so the axis and every resource share one grid. */
2
+ export const PANEL_MEMORY_CSS = `
3
+ .zenfg-inspector-memory-view {
4
+ grid-template-rows: auto auto auto minmax(0, 1fr);
5
+ container-type: inline-size;
6
+ container-name: zenfg-memory;
7
+ }
8
+ .zenfg-inspector-memory-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
9
+ .zenfg-inspector-memory-summary > div { border: 0; background: transparent; }
10
+ .zenfg-inspector-memory-summary span { color: var(--fgd-muted); font: var(--fgd-font-size-small)/1.4 var(--fgd-font-ui); }
11
+ .zenfg-inspector-memory-summary strong { font-size: var(--fgd-font-size); white-space: normal; overflow-wrap: anywhere; }
12
+ .zenfg-inspector-memory-note { margin: 0; padding: 6px var(--fgd-space-3); font: var(--fgd-font-size-small)/1.45 var(--fgd-font-ui); }
13
+ .zenfg-inspector-memory-timeline { align-content: start; grid-template-columns: minmax(0, 1fr); min-width: 720px; font-size: var(--fgd-font-size-small); }
14
+ .zenfg-inspector-memory-axis,
15
+ .zenfg-inspector-memory-resource {
16
+ grid-template-columns: minmax(165px, 0.9fr) 86px minmax(300px, 2.4fr) 82px;
17
+ column-gap: 10px;
18
+ padding-left: var(--fgd-space-2);
19
+ padding-right: var(--fgd-space-2);
20
+ }
21
+ .zenfg-inspector-memory-axis { min-height: var(--fgd-row-height); z-index: 4; font-size: var(--fgd-font-size-small); }
22
+ .zenfg-inspector-memory-axis > :first-child,
23
+ .zenfg-inspector-memory-name { position: sticky; left: 0; z-index: 2; background: var(--fgd-canvas); }
24
+ .zenfg-inspector-memory-axis-track,
25
+ .zenfg-inspector-memory-track { grid-column: 3; }
26
+ .zenfg-inspector-memory-axis-track > span { transform: translateX(-50%); border-left: 0; font-size: var(--fgd-font-size-small); }
27
+ .zenfg-inspector-memory-gridline { position: absolute; height: 100%; top: 0; border-left: 1px dotted var(--fgd-border-strong); }
28
+ .zenfg-inspector-memory-resource { min-height: var(--fgd-row-height); }
29
+ .zenfg-inspector-memory-name { display: flex; align-items: center; min-height: var(--fgd-row-height); gap: 6px; min-width: 0; }
30
+ .zenfg-inspector-memory-name > .zenfg-inspector-relation-button { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-family: var(--fgd-font-ui); }
31
+ .zenfg-inspector-memory-reveal { flex: 0 0 auto; padding: 2px var(--fgd-space-1); border: 1px solid var(--fgd-border); border-radius: var(--fgd-radius-sm); color: var(--fgd-text-secondary); background: var(--fgd-panel); font: var(--fgd-font-size-small)/1.4 var(--fgd-font-ui); cursor: pointer; }
32
+ .zenfg-inspector-memory-reveal:focus-visible { outline: 2px solid var(--fgd-accent); outline-offset: 1px; }
33
+ .zenfg-inspector-memory-resource.selected .zenfg-inspector-memory-name { background: var(--fgd-panel); box-shadow: inset 2px 0 var(--fgd-accent); }
34
+ .zenfg-inspector-memory-allocation { min-width: 0; align-items: flex-start; font-family: var(--fgd-font-ui); }
35
+ .zenfg-inspector-memory-allocation > button { min-width: 0; overflow: hidden; }
36
+ .zenfg-inspector-memory-allocation > span { min-width: 0; overflow-wrap: anywhere; font-size: var(--fgd-font-size-small); }
37
+ .zenfg-inspector-memory-bar { min-width: 0; }
38
+ @container zenfg-memory (max-width: 660px) {
39
+ .zenfg-inspector-memory-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
40
+ }
41
+ `;
@@ -1,38 +1,73 @@
1
1
  import type { FrameGraphDebugResource, FrameGraphDebugViewModel } from './debugCaptureModel.ts';
2
- import { analyzeSnapshotAliases } from './panelAliasAnalysis.ts';
2
+ import { analyzeSnapshotAliases, type AliasAnalysisAllocation } from './panelAliasAnalysis.ts';
3
3
  import { createMutedText, labelResource } from './panelDomHelpers.ts';
4
4
  import type { Selection } from './panelTypes.ts';
5
5
  import {
6
- createRelationButton,
7
- formatEstimatedBytes,
8
- registerSelectable,
9
- type WorkbenchCallbacks,
10
- updateSelectedRows,
6
+ createFilterSelect, createRelationButton, createSearchInput, createViewToolbar,
7
+ formatEstimatedBytes, formatEstimateCoverage, groupPath, registerSelectable,
8
+ selectionKey, type WorkbenchCallbacks, updateSelectedRows,
11
9
  } from './panelWorkbenchHelpers.ts';
12
10
 
11
+ type MemoryFilter = 'all' | 'aliased' | 'single' | 'unallocated';
12
+ type MemorySort = 'allocation' | 'size';
13
+
13
14
  export class MemoryView {
14
15
  readonly root = document.createElement('section');
15
16
  private readonly metrics = document.createElement('div');
16
17
  private readonly scroller = document.createElement('div');
17
18
  private readonly timeline = document.createElement('div');
19
+ private readonly count = document.createElement('span');
20
+ private readonly search: HTMLInputElement;
21
+ private readonly filterSelect: HTMLSelectElement;
18
22
  private readonly rows = new Map<string, HTMLElement[]>();
19
23
  private snapshot: FrameGraphDebugViewModel | undefined;
20
24
  private selected: Selection | undefined;
25
+ private query = '';
26
+ private filter: MemoryFilter = 'all';
27
+ private sort: MemorySort = 'allocation';
21
28
 
22
29
  constructor(private readonly callbacks: WorkbenchCallbacks, idPrefix: string) {
23
30
  this.root.className = 'zenfg-inspector-view zenfg-inspector-memory-view';
24
31
  this.root.id = `${idPrefix}-view-memory`;
25
32
  this.root.setAttribute('role', 'tabpanel');
26
33
  this.metrics.className = 'zenfg-inspector-memory-summary';
34
+ const toolbar = createViewToolbar('Memory filters');
35
+ this.search = createSearchInput('Search memory resources or allocations', '', (value) => {
36
+ this.query = value;
37
+ this.renderTimeline();
38
+ });
39
+ this.filterSelect = createFilterSelect('Memory allocation status', this.filter, [
40
+ ['all', 'All'], ['aliased', 'Aliased'], ['single', 'Single'], ['unallocated', 'Unallocated'],
41
+ ], (value) => {
42
+ this.filter = value as MemoryFilter;
43
+ this.renderTimeline();
44
+ });
45
+ const sort = createFilterSelect('Memory sort', this.sort, [
46
+ ['allocation', 'Allocation order'], ['size', 'Size: largest first'],
47
+ ], (value) => {
48
+ this.sort = value as MemorySort;
49
+ this.renderTimeline();
50
+ });
51
+ const clear = document.createElement('button');
52
+ clear.type = 'button';
53
+ clear.textContent = 'Clear filters';
54
+ clear.addEventListener('click', () => this.clearFilters());
55
+ this.count.className = 'zenfg-inspector-result-count';
56
+ this.count.setAttribute('role', 'status');
57
+ toolbar.append(this.search, this.filterSelect, sort, clear, this.count);
58
+ const note = createMutedText('Whole-snapshot estimates, not total GPU memory or a measured peak. Bars include the first and last execution slots; missing lifetimes have no bar.');
59
+ note.classList.add('zenfg-inspector-memory-note');
27
60
  this.scroller.className = 'zenfg-inspector-memory-scroller';
61
+ this.scroller.setAttribute('aria-label', 'Resource lifetimes by execution slot');
28
62
  this.timeline.className = 'zenfg-inspector-memory-timeline';
29
63
  this.scroller.appendChild(this.timeline);
30
- this.root.append(this.metrics, this.scroller);
64
+ this.root.append(this.metrics, toolbar, note, this.scroller);
31
65
  }
32
66
 
33
67
  setSnapshot(snapshot: FrameGraphDebugViewModel): void {
34
68
  this.snapshot = snapshot;
35
- this.render();
69
+ this.renderMetrics();
70
+ this.renderTimeline();
36
71
  }
37
72
 
38
73
  setSelection(selected: Selection | undefined): void {
@@ -40,58 +75,116 @@ export class MemoryView {
40
75
  updateSelectedRows(this.rows, selected);
41
76
  }
42
77
 
43
- private render(): void {
78
+ /** Explicit navigation can remove filters; ordinary selection never does. */
79
+ reveal(selection: Selection): void {
80
+ this.clearFilters();
81
+ this.setSelection(selection);
82
+ const row = this.rows.get(selectionKey(selection))?.[0];
83
+ row?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' });
84
+ row?.querySelector<HTMLButtonElement>('.zenfg-inspector-relation-button')?.focus({ preventScroll: true });
85
+ }
86
+
87
+ private clearFilters(): void {
88
+ this.query = '';
89
+ this.filter = 'all';
90
+ this.search.value = '';
91
+ this.filterSelect.value = 'all';
92
+ this.renderTimeline();
93
+ }
94
+
95
+ private renderMetrics(): void {
44
96
  const snapshot = this.snapshot;
45
97
  if (!snapshot) return;
46
98
  const metrics = snapshot.metrics;
99
+ const coverage = metrics.estimatedCoverage;
100
+ const allocationAvailable = snapshot.protocol.memory.allocationReport.status === 'available';
101
+ const pool = snapshot.resourcePool;
47
102
  this.metrics.replaceChildren(
48
- this.createMetric('Logical capacity', formatEstimatedBytes(metrics.logicalCapacityBytes)),
49
- this.createMetric('Physical estimate', formatEstimatedBytes(metrics.physicalEstimatedBytes)),
50
- this.createMetric('Alias reuse', formatEstimatedBytes(metrics.aliasReuseBytes)),
51
- this.createMetric('Allocations', `${snapshot.physicalAllocations.length} · ${metrics.aliasedAllocationCount} aliased`),
103
+ this.createMetric('Resource estimate', formatEstimateCoverage(metrics.transientEstimatedByteSize, coverage.transient),
104
+ 'The declared estimated sizes of all transient logical resources.'),
105
+ this.createMetric('Logical capacity', allocationAvailable ? formatEstimateCoverage(metrics.logicalCapacityBytes, coverage.logical) : 'Not collected',
106
+ 'Assigned allocation capacity counted once per logical transient resource.'),
107
+ this.createMetric('Physical estimate', allocationAvailable ? formatEstimateCoverage(metrics.physicalEstimatedBytes, coverage.physical) : 'Not collected',
108
+ 'Estimated allocation sizes, counting each physical allocation once.'),
109
+ this.createMetric('Alias reuse', allocationAvailable ? formatEstimatedBytes(metrics.aliasReuseBytes) : 'Not collected',
110
+ 'Logical allocation capacity minus physical allocation estimate; not a measured saving.'),
111
+ this.createMetric('Pool retained', pool.status === 'available' ? formatEstimatedBytes(pool.estimatedRetainedBytes) : 'Not collected',
112
+ 'Estimated bytes retained by the resource pool, including allocations outside this frame.'),
113
+ this.createMetric('Allocations', allocationAvailable ? `${snapshot.physicalAllocations.length} · ${metrics.aliasedAllocationCount} aliased` : 'Not collected',
114
+ 'Physical allocation count for the entire snapshot.'),
52
115
  );
116
+ }
117
+
118
+ private renderTimeline(): void {
119
+ const snapshot = this.snapshot;
120
+ if (!snapshot) return;
53
121
  this.rows.clear();
54
122
  this.timeline.replaceChildren();
55
123
  const analysis = analyzeSnapshotAliases(snapshot);
56
- this.timeline.appendChild(this.createAxis(analysis.minUse, analysis.maxUse));
57
-
58
- for (const group of analysis.allocations) {
59
- const selection: Selection = { kind: 'allocation', id: group.allocation.id };
60
- const header = document.createElement('div');
61
- header.className = 'zenfg-inspector-memory-allocation';
62
- header.dataset.kind = group.allocation.kind;
63
- registerSelectable(this.rows, header, selection, this.callbacks);
64
- const name = createRelationButton(`Allocation #${group.allocation.id}`, selection, this.callbacks.onSelect);
65
- const meta = document.createElement('span');
66
- meta.textContent = `${group.allocation.kind} · class ${group.allocation.compatibilityClassId} · ${formatEstimatedBytes(group.allocation.estimatedByteSize)} · ${group.resources.length > 1 ? `alias ×${group.resources.length}` : 'single'}`;
67
- header.append(name, meta);
68
- this.timeline.appendChild(header);
124
+ const ticks = analysis.hasLifetimes ? this.executionTicks(analysis.minUse, analysis.maxUse) : [];
125
+ this.timeline.appendChild(this.createAxis(analysis.minUse, analysis.maxUse, ticks));
126
+ const query = this.query.trim().toLocaleLowerCase();
127
+ const matches = (resource: FrameGraphDebugResource): boolean => !query || [
128
+ resource.id, resource.label, groupPath(snapshot, resource.debugGroupId),
129
+ ].some((value) => value?.toLocaleLowerCase().includes(query));
130
+ const groups = analysis.allocations.flatMap((group) => {
131
+ if (this.filter === 'unallocated' || (this.filter === 'aliased' && !group.aliases) || (this.filter === 'single' && group.aliases)) return [];
132
+ const allocationMatch = !query || [group.allocation.id, group.allocation.compatibilityClassId]
133
+ .some((value) => value.toLocaleLowerCase().includes(query));
134
+ const resources = allocationMatch ? group.resources : group.resources.filter(matches);
135
+ return resources.length > 0 || allocationMatch ? [{ ...group, resources }] : [];
136
+ });
137
+ if (this.sort === 'size') groups.sort((a, b) => compareSize(a.allocation.estimatedByteSize, b.allocation.estimatedByteSize));
138
+ let shown = 0;
139
+ for (const group of groups) {
140
+ this.timeline.appendChild(this.createAllocationHeader(group));
69
141
  for (const resource of group.resources) {
70
- this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse));
142
+ this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse, ticks));
143
+ shown++;
71
144
  }
72
145
  }
73
146
 
74
- const unallocated = snapshot.resources.filter((resource) => (
75
- resource.origin === 'transient' && resource.physicalResourceId === undefined
76
- ));
147
+ const allUnallocated = snapshot.resources.filter((resource) => resource.origin === 'transient'
148
+ && (resource.physicalResourceId === undefined || !snapshot.allocationById.has(resource.physicalResourceId)));
149
+ const unallocated = this.filter === 'all' || this.filter === 'unallocated' ? allUnallocated.filter(matches) : [];
150
+ if (this.sort === 'size') unallocated.sort((a, b) => compareSize(a.estimatedByteSize, b.estimatedByteSize));
77
151
  if (unallocated.length > 0) {
78
152
  const header = document.createElement('div');
79
153
  header.className = 'zenfg-inspector-memory-allocation muted';
80
154
  header.dataset.kind = 'unallocated';
81
- header.textContent = 'Unallocated transient resources';
155
+ header.textContent = snapshot.protocol.memory.allocationReport.status === 'available'
156
+ ? 'Unallocated transient resources' : 'Transient resources — allocation report unavailable';
82
157
  this.timeline.appendChild(header);
83
158
  for (const resource of unallocated) {
84
- this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse));
159
+ this.timeline.appendChild(this.createResourceRow(resource, analysis.minUse, analysis.maxUse, ticks));
160
+ shown++;
85
161
  }
86
162
  }
87
- if (analysis.allocations.length === 0 && unallocated.length === 0) {
88
- this.timeline.appendChild(createMutedText('No transient resource lifetimes.'));
163
+ const total = analysis.allocations.reduce((sum, group) => sum + group.resources.length, 0) + allUnallocated.length;
164
+ this.count.textContent = `${shown} / ${total} resources`;
165
+ if (groups.length === 0 && unallocated.length === 0) {
166
+ this.timeline.appendChild(createMutedText(total > 0 ? 'No resources match these filters.' : 'No transient resource lifetimes.'));
89
167
  }
90
168
  updateSelectedRows(this.rows, this.selected);
91
169
  }
92
170
 
93
- private createMetric(label: string, value: string): HTMLElement {
171
+ private createAllocationHeader(group: AliasAnalysisAllocation): HTMLElement {
172
+ const selection: Selection = { kind: 'allocation', id: group.allocation.id };
173
+ const header = document.createElement('div');
174
+ header.className = 'zenfg-inspector-memory-allocation';
175
+ header.dataset.kind = group.allocation.kind;
176
+ registerSelectable(this.rows, header, selection, this.callbacks);
177
+ const name = createRelationButton(`Allocation ${group.allocation.id.replace(/^allocation:/, '')}`, selection, this.callbacks.onSelect);
178
+ name.title = group.allocation.id;
179
+ const meta = document.createElement('span');
180
+ meta.textContent = `${group.allocation.kind} · class ${group.allocation.compatibilityClassId} · ${formatEstimatedBytes(group.allocation.estimatedByteSize)} · ${group.aliases ? `aliased ×${group.allocation.resourceIds.length}` : 'single'}`;
181
+ header.append(name, meta);
182
+ return header;
183
+ }
184
+
185
+ private createMetric(label: string, value: string, explanation: string): HTMLElement {
94
186
  const item = document.createElement('div');
187
+ item.title = explanation;
95
188
  const term = document.createElement('span');
96
189
  term.textContent = label;
97
190
  const amount = document.createElement('strong');
@@ -100,48 +193,74 @@ export class MemoryView {
100
193
  return item;
101
194
  }
102
195
 
103
- private createAxis(minUse: number, maxUse: number): HTMLElement {
196
+ private executionTicks(minUse: number, maxUse: number): number[] {
197
+ const count = Math.min(6, maxUse - minUse + 1);
198
+ return Array.from({ length: count }, (_, index) => count === 1 ? minUse : minUse + Math.round(index * (maxUse - minUse) / (count - 1)));
199
+ }
200
+
201
+ private createAxis(minUse: number, maxUse: number, ticks: readonly number[]): HTMLElement {
104
202
  const axis = document.createElement('div');
105
203
  axis.className = 'zenfg-inspector-memory-axis';
106
204
  const label = document.createElement('span');
107
- label.textContent = 'Execution order';
205
+ label.textContent = 'Resource';
206
+ const lifetime = document.createElement('span');
207
+ lifetime.textContent = 'First–last slot';
108
208
  const track = document.createElement('div');
109
209
  track.className = 'zenfg-inspector-memory-axis-track';
110
- const range = Math.max(1, maxUse - minUse);
111
- const step = Math.max(1, Math.ceil(range / 5));
112
- for (let value = minUse; value <= maxUse; value += step) {
210
+ track.setAttribute('aria-label', 'Execution slots');
211
+ if (ticks.length === 0) track.textContent = 'No execution lifetimes';
212
+ const slots = maxUse - minUse + 1;
213
+ for (const value of ticks) {
113
214
  const tick = document.createElement('span');
114
- tick.style.left = `${((value - minUse) / range) * 100}%`;
215
+ tick.style.left = `${((value - minUse + 0.5) / slots) * 100}%`;
115
216
  tick.textContent = String(value);
116
217
  track.appendChild(tick);
117
218
  }
118
- if (maxUse > minUse && (maxUse - minUse) % step !== 0) {
119
- const tick = document.createElement('span');
120
- tick.style.left = '100%';
121
- tick.textContent = String(maxUse);
122
- track.appendChild(tick);
123
- }
124
- axis.append(label, track);
219
+ const size = document.createElement('span');
220
+ size.textContent = 'Estimate';
221
+ axis.append(label, lifetime, track, size);
125
222
  return axis;
126
223
  }
127
224
 
128
- private createResourceRow(resource: FrameGraphDebugResource, minUse: number, maxUse: number): HTMLElement {
225
+ private createResourceRow(resource: FrameGraphDebugResource, minUse: number, maxUse: number, ticks: readonly number[]): HTMLElement {
129
226
  const selection: Selection = { kind: 'resource', id: resource.id };
130
227
  const row = document.createElement('div');
131
228
  row.className = 'zenfg-inspector-memory-resource';
132
- row.dataset.kind = resource.kind;
229
+ row.dataset.kind = resource.kind;
133
230
  registerSelectable(this.rows, row, selection, this.callbacks);
134
- const name = createRelationButton(labelResource(resource), selection, this.callbacks.onSelect);
231
+ const name = document.createElement('div');
232
+ name.className = 'zenfg-inspector-memory-name';
233
+ const button = createRelationButton(labelResource(resource), selection, this.callbacks.onSelect);
234
+ button.title = resource.id;
235
+ name.appendChild(button);
236
+ if (this.callbacks.onReveal) {
237
+ const reveal = document.createElement('button');
238
+ reveal.type = 'button';
239
+ reveal.className = 'zenfg-inspector-memory-reveal';
240
+ reveal.textContent = 'Locate';
241
+ reveal.title = 'Locate in Resources';
242
+ reveal.setAttribute('aria-label', `Locate ${labelResource(resource)} in Resources`);
243
+ reveal.addEventListener('click', () => this.callbacks.onReveal?.(selection, 'resources'));
244
+ name.appendChild(reveal);
245
+ }
135
246
  const range = document.createElement('span');
136
- range.textContent = resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : 'no lifetime';
247
+ range.textContent = resource.lifetime ? `${resource.lifetime.firstUse}–${resource.lifetime.lastUse}` : 'No lifetime';
137
248
  const track = document.createElement('div');
138
249
  track.className = 'zenfg-inspector-memory-track';
250
+ const slots = maxUse - minUse + 1;
251
+ for (const value of ticks) {
252
+ const grid = document.createElement('span');
253
+ grid.className = 'zenfg-inspector-memory-gridline';
254
+ grid.style.left = `${((value - minUse + 0.5) / slots) * 100}%`;
255
+ grid.setAttribute('aria-hidden', 'true');
256
+ track.appendChild(grid);
257
+ }
139
258
  const bar = document.createElement('span');
140
259
  bar.className = `zenfg-inspector-memory-bar ${resource.kind}`;
141
260
  if (resource.lifetime) {
142
- const slots = Math.max(1, maxUse - minUse + 1);
143
261
  bar.style.left = `${((resource.lifetime.firstUse - minUse) / slots) * 100}%`;
144
262
  bar.style.width = `${((resource.lifetime.lastUse - resource.lifetime.firstUse + 1) / slots) * 100}%`;
263
+ bar.title = `Execution slots ${resource.lifetime.firstUse}–${resource.lifetime.lastUse} (inclusive)`;
145
264
  } else {
146
265
  bar.classList.add('empty');
147
266
  }
@@ -152,3 +271,9 @@ export class MemoryView {
152
271
  return row;
153
272
  }
154
273
  }
274
+
275
+ function compareSize(a: number | undefined, b: number | undefined): number {
276
+ if (a === undefined) return b === undefined ? 0 : 1;
277
+ if (b === undefined) return -1;
278
+ return b - a;
279
+ }