nuxt-devtools-observatory 0.1.10 → 0.1.12
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/client/dist/assets/index-BugqFK5S.css +1 -0
- package/client/dist/assets/index-DILjvs09.js +17 -0
- package/client/dist/index.html +2 -2
- package/client/src/stores/observatory.ts +67 -13
- package/client/src/views/ComposableTracker.vue +386 -116
- package/client/src/views/ProvideInjectGraph.vue +232 -61
- package/client/src/views/RenderHeatmap.vue +239 -69
- package/client/src/views/TransitionTimeline.vue +2 -2
- package/dist/module.json +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +20 -0
- package/dist/runtime/composables/composable-registry.js +178 -53
- package/dist/runtime/composables/provide-inject-registry.d.ts +13 -1
- package/dist/runtime/composables/provide-inject-registry.js +34 -6
- package/dist/runtime/composables/render-registry.d.ts +19 -6
- package/dist/runtime/composables/render-registry.js +70 -35
- package/dist/runtime/composables/transition-registry.js +13 -7
- package/dist/runtime/plugin.js +43 -12
- package/package.json +7 -3
- package/client/dist/assets/index-BBp7Dvrp.css +0 -1
- package/client/dist/assets/index-mEAMrJUv.js +0 -17
|
@@ -9,12 +9,15 @@ interface ComponentNode {
|
|
|
9
9
|
element?: string
|
|
10
10
|
depth: number
|
|
11
11
|
path: string[]
|
|
12
|
-
|
|
12
|
+
rerenders: number
|
|
13
|
+
mountCount: number
|
|
13
14
|
avgMs: number
|
|
14
15
|
triggers: string[]
|
|
15
16
|
children: ComponentNode[]
|
|
16
17
|
parentId?: string
|
|
17
18
|
parentLabel?: string
|
|
19
|
+
isPersistent: boolean
|
|
20
|
+
isHydrationMount: boolean
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const TreeNode = defineComponent({
|
|
@@ -23,45 +26,19 @@ const TreeNode = defineComponent({
|
|
|
23
26
|
node: Object as () => ComponentNode,
|
|
24
27
|
mode: String,
|
|
25
28
|
threshold: Number,
|
|
26
|
-
hotOnly: Boolean,
|
|
27
29
|
selected: String,
|
|
28
|
-
search: String,
|
|
29
30
|
expandedIds: Object as () => Set<string>,
|
|
30
31
|
},
|
|
31
32
|
emits: ['select', 'toggle'],
|
|
32
33
|
setup(props, { emit }): () => VNode | null {
|
|
33
34
|
function nodeValue(node: ComponentNode) {
|
|
34
|
-
return props.mode === 'count' ? node.
|
|
35
|
+
return props.mode === 'count' ? node.rerenders + node.mountCount : node.avgMs
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
function isHot(node: ComponentNode) {
|
|
38
39
|
return nodeValue(node) >= props.threshold!
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function matchesSearch(node: ComponentNode, search: string): boolean {
|
|
42
|
-
if (!search) {
|
|
43
|
-
return true
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const query = search.toLowerCase()
|
|
47
|
-
|
|
48
|
-
return (
|
|
49
|
-
node.label.toLowerCase().includes(query) ||
|
|
50
|
-
node.file.toLowerCase().includes(query) ||
|
|
51
|
-
node.path.some((segment) => segment.toLowerCase().includes(query)) ||
|
|
52
|
-
node.triggers.some((trigger) => trigger.toLowerCase().includes(query))
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function shouldShow(node: ComponentNode, search: string): boolean {
|
|
57
|
-
const selfMatches = matchesSearch(node, search)
|
|
58
|
-
const childMatches = node.children.some((child) => shouldShow(child, search))
|
|
59
|
-
const searchMatches = !search || selfMatches || childMatches
|
|
60
|
-
const hotMatches = !props.hotOnly || isHot(node) || node.children.some((child) => shouldShow(child, ''))
|
|
61
|
-
|
|
62
|
-
return searchMatches && hotMatches
|
|
63
|
-
}
|
|
64
|
-
|
|
65
42
|
function rowClass(node: ComponentNode) {
|
|
66
43
|
return {
|
|
67
44
|
selected: props.selected === node.id,
|
|
@@ -71,16 +48,9 @@ const TreeNode = defineComponent({
|
|
|
71
48
|
|
|
72
49
|
return () => {
|
|
73
50
|
const node = props.node!
|
|
74
|
-
const search = props.search?.trim() ?? ''
|
|
75
|
-
|
|
76
|
-
if (!shouldShow(node, search)) {
|
|
77
|
-
return null
|
|
78
|
-
}
|
|
79
|
-
|
|
80
51
|
const expanded = props.expandedIds?.has(node.id) ?? false
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const metric = props.mode === 'count' ? `${node.renders}` : `${node.avgMs.toFixed(1)}ms`
|
|
52
|
+
const canExpand = node.children.length > 0
|
|
53
|
+
const metric = props.mode === 'count' ? `${node.rerenders + node.mountCount}` : `${node.avgMs.toFixed(1)}ms`
|
|
84
54
|
const metricLabel = props.mode === 'count' ? 'renders' : 'avg'
|
|
85
55
|
const badges = []
|
|
86
56
|
const normalizedElement = node.element?.toLowerCase()
|
|
@@ -90,7 +60,11 @@ const TreeNode = defineComponent({
|
|
|
90
60
|
}
|
|
91
61
|
|
|
92
62
|
if (node.file !== 'unknown') {
|
|
93
|
-
const fileBadge =
|
|
63
|
+
const fileBadge =
|
|
64
|
+
node.file
|
|
65
|
+
.split('/')
|
|
66
|
+
.pop()
|
|
67
|
+
?.replace(/\.vue$/i, '') ?? node.file
|
|
94
68
|
|
|
95
69
|
if (fileBadge !== node.label && !badges.includes(fileBadge)) {
|
|
96
70
|
badges.push(fileBadge)
|
|
@@ -109,10 +83,11 @@ const TreeNode = defineComponent({
|
|
|
109
83
|
},
|
|
110
84
|
},
|
|
111
85
|
[
|
|
86
|
+
h('span', { class: 'tree-rail', 'aria-hidden': 'true' }),
|
|
112
87
|
h(
|
|
113
88
|
'button',
|
|
114
89
|
{
|
|
115
|
-
class: 'tree-toggle',
|
|
90
|
+
class: ['tree-toggle', { empty: !canExpand }],
|
|
116
91
|
disabled: !canExpand,
|
|
117
92
|
onClick: (event: MouseEvent) => {
|
|
118
93
|
event.stopPropagation()
|
|
@@ -122,9 +97,8 @@ const TreeNode = defineComponent({
|
|
|
122
97
|
}
|
|
123
98
|
},
|
|
124
99
|
},
|
|
125
|
-
canExpand ? (expanded ? '
|
|
100
|
+
canExpand ? (expanded ? '⌄' : '›') : ''
|
|
126
101
|
),
|
|
127
|
-
h('span', { class: 'tree-rail', 'aria-hidden': 'true' }),
|
|
128
102
|
h('div', { class: 'tree-copy' }, [
|
|
129
103
|
h('span', { class: 'tree-name mono', title: node.label }, node.label),
|
|
130
104
|
badges.length
|
|
@@ -135,21 +109,38 @@ const TreeNode = defineComponent({
|
|
|
135
109
|
)
|
|
136
110
|
: null,
|
|
137
111
|
]),
|
|
138
|
-
h('div', { class: 'tree-metrics mono' },
|
|
112
|
+
h('div', { class: 'tree-metrics mono' }, [
|
|
113
|
+
node.isPersistent
|
|
114
|
+
? h(
|
|
115
|
+
'span',
|
|
116
|
+
{ class: 'tree-persistent-pill', title: 'Layout / persistent component — survives navigation' },
|
|
117
|
+
'persistent'
|
|
118
|
+
)
|
|
119
|
+
: null,
|
|
120
|
+
node.isHydrationMount
|
|
121
|
+
? h(
|
|
122
|
+
'span',
|
|
123
|
+
{
|
|
124
|
+
class: 'tree-hydration-pill',
|
|
125
|
+
title: 'First mount was SSR hydration — not a user-triggered render',
|
|
126
|
+
},
|
|
127
|
+
'hydrated'
|
|
128
|
+
)
|
|
129
|
+
: null,
|
|
130
|
+
h('span', { class: 'tree-metric-pill' }, `${metric} ${metricLabel}`),
|
|
131
|
+
]),
|
|
139
132
|
]
|
|
140
133
|
),
|
|
141
|
-
expanded &&
|
|
134
|
+
expanded && canExpand
|
|
142
135
|
? h(
|
|
143
136
|
'div',
|
|
144
137
|
{ class: 'tree-children' },
|
|
145
|
-
|
|
138
|
+
node.children.map((child) =>
|
|
146
139
|
h(TreeNode, {
|
|
147
140
|
node: child,
|
|
148
141
|
mode: props.mode,
|
|
149
142
|
threshold: props.threshold,
|
|
150
|
-
hotOnly: props.hotOnly,
|
|
151
143
|
selected: props.selected,
|
|
152
|
-
search: props.search,
|
|
153
144
|
expandedIds: props.expandedIds,
|
|
154
145
|
onSelect: (value: ComponentNode) => emit('select', value),
|
|
155
146
|
onToggle: (value: string) => emit('toggle', value),
|
|
@@ -165,7 +156,19 @@ const TreeNode = defineComponent({
|
|
|
165
156
|
const { renders, connected } = useObservatoryData()
|
|
166
157
|
|
|
167
158
|
const activeMode = ref<'count' | 'time'>('count')
|
|
168
|
-
|
|
159
|
+
// Separate thresholds per mode so switching modes doesn't produce nonsense results.
|
|
160
|
+
// Count: flag components that rendered 3+ times (1 hydration mount is normal).
|
|
161
|
+
// Time: flag components averaging 16ms+ (one animation frame budget).
|
|
162
|
+
const countThreshold = ref(3)
|
|
163
|
+
const timeThreshold = ref(16)
|
|
164
|
+
// Writable computed so the threshold slider can use v-model directly.
|
|
165
|
+
const activeThreshold = computed({
|
|
166
|
+
get: () => (activeMode.value === 'count' ? countThreshold.value : timeThreshold.value),
|
|
167
|
+
set: (val: number) => {
|
|
168
|
+
if (activeMode.value === 'count') countThreshold.value = val
|
|
169
|
+
else timeThreshold.value = val
|
|
170
|
+
},
|
|
171
|
+
})
|
|
169
172
|
const activeHotOnly = ref(false)
|
|
170
173
|
const frozen = ref(false)
|
|
171
174
|
const search = ref('')
|
|
@@ -184,7 +187,10 @@ function displayLabel(entry: RenderEntry) {
|
|
|
184
187
|
return entry.element
|
|
185
188
|
}
|
|
186
189
|
|
|
187
|
-
const basename = entry.file
|
|
190
|
+
const basename = entry.file
|
|
191
|
+
.split('/')
|
|
192
|
+
.pop()
|
|
193
|
+
?.replace(/\.vue$/i, '')
|
|
188
194
|
|
|
189
195
|
if (basename && basename !== 'unknown') {
|
|
190
196
|
return basename
|
|
@@ -212,11 +218,14 @@ function buildNodes(entries: RenderEntry[]) {
|
|
|
212
218
|
element: entry.element,
|
|
213
219
|
depth: 0,
|
|
214
220
|
path: [],
|
|
215
|
-
|
|
221
|
+
rerenders: entry.rerenders ?? 0,
|
|
222
|
+
mountCount: entry.mountCount ?? 1,
|
|
216
223
|
avgMs: entry.avgMs,
|
|
217
224
|
triggers: entry.triggers.map(formatTrigger),
|
|
218
225
|
children: [],
|
|
219
226
|
parentId: entry.parentUid !== undefined ? String(entry.parentUid) : undefined,
|
|
227
|
+
isPersistent: Boolean(entry.isPersistent),
|
|
228
|
+
isHydrationMount: Boolean(entry.isHydrationMount),
|
|
220
229
|
})
|
|
221
230
|
}
|
|
222
231
|
|
|
@@ -291,12 +300,38 @@ function pathToNode(node: ComponentNode, targetId: string, trail: string[] = [])
|
|
|
291
300
|
return null
|
|
292
301
|
}
|
|
293
302
|
|
|
303
|
+
function findFirstHotNode(node: ComponentNode): ComponentNode | null {
|
|
304
|
+
if (isHot(node)) {
|
|
305
|
+
return node
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const child of node.children) {
|
|
309
|
+
const match = findFirstHotNode(child)
|
|
310
|
+
|
|
311
|
+
if (match) {
|
|
312
|
+
return match
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return null
|
|
317
|
+
}
|
|
318
|
+
|
|
294
319
|
function defaultExpandedIds(root: ComponentNode | null) {
|
|
295
320
|
if (!root) {
|
|
296
321
|
return new Set<string>()
|
|
297
322
|
}
|
|
298
323
|
|
|
299
|
-
|
|
324
|
+
// Expand all nodes that have children — gives a fully-open tree on first load.
|
|
325
|
+
// The user can collapse individual branches as needed.
|
|
326
|
+
const expanded = new Set<string>()
|
|
327
|
+
function expandAll(node: ComponentNode) {
|
|
328
|
+
if (node.children.length > 0) {
|
|
329
|
+
expanded.add(node.id)
|
|
330
|
+
node.children.forEach(expandAll)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
expandAll(root)
|
|
334
|
+
return expanded
|
|
300
335
|
}
|
|
301
336
|
|
|
302
337
|
function searchExpandedIds(root: ComponentNode | null, term: string) {
|
|
@@ -323,7 +358,7 @@ function searchExpandedIds(root: ComponentNode | null, term: string) {
|
|
|
323
358
|
}
|
|
324
359
|
|
|
325
360
|
function nodeValue(node: ComponentNode) {
|
|
326
|
-
return activeMode.value === 'count' ? node.
|
|
361
|
+
return activeMode.value === 'count' ? node.rerenders + node.mountCount : node.avgMs
|
|
327
362
|
}
|
|
328
363
|
|
|
329
364
|
function isHot(node: ComponentNode) {
|
|
@@ -364,6 +399,24 @@ function isVisibleRoot(node: ComponentNode, searchTerm: string): boolean {
|
|
|
364
399
|
return matchesCurrentSearch && matchesCurrentHeat
|
|
365
400
|
}
|
|
366
401
|
|
|
402
|
+
function pruneVisibleTree(node: ComponentNode, searchTerm: string): ComponentNode | null {
|
|
403
|
+
const visibleChildren = node.children
|
|
404
|
+
.map((child) => pruneVisibleTree(child, searchTerm))
|
|
405
|
+
.filter((child): child is ComponentNode => child !== null)
|
|
406
|
+
|
|
407
|
+
const matchesCurrentSearch = !searchTerm || matchesSearch(node, searchTerm) || visibleChildren.length > 0
|
|
408
|
+
const matchesCurrentHeat = !activeHotOnly.value || isHot(node) || visibleChildren.length > 0
|
|
409
|
+
|
|
410
|
+
if (!matchesCurrentSearch || !matchesCurrentHeat) {
|
|
411
|
+
return null
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
...node,
|
|
416
|
+
children: visibleChildren,
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
367
420
|
const displayEntries = computed(() => (frozen.value ? frozenSnapshot.value : renders.value))
|
|
368
421
|
const rootNodes = computed(() => buildNodes(displayEntries.value))
|
|
369
422
|
const rootMap = computed(() => new Map(rootNodes.value.map((node) => [node.id, node])))
|
|
@@ -383,12 +436,18 @@ const activeRoot = computed(() => {
|
|
|
383
436
|
return filteredRoots.value[0] ?? rootNodes.value[0] ?? null
|
|
384
437
|
})
|
|
385
438
|
|
|
439
|
+
const visibleActiveRoot = computed(() => {
|
|
440
|
+
const term = search.value.trim()
|
|
441
|
+
|
|
442
|
+
return activeRoot.value ? pruneVisibleTree(activeRoot.value, term) : null
|
|
443
|
+
})
|
|
444
|
+
|
|
386
445
|
const visibleTreeRoots = computed(() => {
|
|
387
|
-
if (!
|
|
446
|
+
if (!visibleActiveRoot.value) {
|
|
388
447
|
return []
|
|
389
448
|
}
|
|
390
449
|
|
|
391
|
-
return [
|
|
450
|
+
return [visibleActiveRoot.value]
|
|
392
451
|
})
|
|
393
452
|
|
|
394
453
|
const appEntries = computed(() =>
|
|
@@ -401,7 +460,7 @@ const appEntries = computed(() =>
|
|
|
401
460
|
)
|
|
402
461
|
|
|
403
462
|
const activeSelected = computed(() => allComponents.value.find((node) => node.id === activeSelectedId.value) ?? null)
|
|
404
|
-
const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.
|
|
463
|
+
const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.rerenders + node.mountCount, 0))
|
|
405
464
|
const hotCount = computed(() => allComponents.value.filter((node) => isHot(node)).length)
|
|
406
465
|
const avgTime = computed(() => {
|
|
407
466
|
const components = allComponents.value.filter((node) => node.avgMs > 0)
|
|
@@ -476,6 +535,34 @@ watch(search, (term) => {
|
|
|
476
535
|
expandedIds.value = defaultExpandedIds(activeRoot.value)
|
|
477
536
|
})
|
|
478
537
|
|
|
538
|
+
watch([activeHotOnly, activeThreshold, activeMode, filteredRoots], () => {
|
|
539
|
+
if (!activeHotOnly.value) {
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const topLevelRoot = filteredRoots.value[0] ?? null
|
|
544
|
+
|
|
545
|
+
if (!topLevelRoot) {
|
|
546
|
+
activeSelectedId.value = null
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const firstHot = findFirstHotNode(topLevelRoot)
|
|
551
|
+
|
|
552
|
+
if (!firstHot) {
|
|
553
|
+
activeSelectedId.value = null
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
activeRootId.value = topLevelRoot.id
|
|
558
|
+
|
|
559
|
+
if (activeSelectedId.value !== firstHot.id) {
|
|
560
|
+
activeSelectedId.value = firstHot.id
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
expandedIds.value = new Set(pathToNode(topLevelRoot, firstHot.id) ?? [topLevelRoot.id])
|
|
564
|
+
})
|
|
565
|
+
|
|
479
566
|
function selectNode(node: ComponentNode) {
|
|
480
567
|
activeSelectedId.value = node.id
|
|
481
568
|
|
|
@@ -522,7 +609,12 @@ function toggleFreeze() {
|
|
|
522
609
|
}
|
|
523
610
|
|
|
524
611
|
function basename(file: string) {
|
|
525
|
-
return
|
|
612
|
+
return (
|
|
613
|
+
file
|
|
614
|
+
.split('/')
|
|
615
|
+
.pop()
|
|
616
|
+
?.replace(/\.vue$/i, '') ?? file
|
|
617
|
+
)
|
|
526
618
|
}
|
|
527
619
|
|
|
528
620
|
function pathLabel(node: ComponentNode) {
|
|
@@ -539,8 +631,15 @@ function pathLabel(node: ComponentNode) {
|
|
|
539
631
|
</div>
|
|
540
632
|
<div class="threshold-group">
|
|
541
633
|
<span class="muted text-sm">threshold</span>
|
|
542
|
-
<input
|
|
543
|
-
|
|
634
|
+
<input
|
|
635
|
+
v-model.number="activeThreshold"
|
|
636
|
+
type="range"
|
|
637
|
+
:min="activeMode === 'count' ? 2 : 4"
|
|
638
|
+
:max="activeMode === 'count' ? 20 : 100"
|
|
639
|
+
:step="activeMode === 'count' ? 1 : 4"
|
|
640
|
+
style="width: 90px"
|
|
641
|
+
/>
|
|
642
|
+
<span class="mono text-sm">{{ activeThreshold }}{{ activeMode === 'count' ? '+ renders' : 'ms+' }}</span>
|
|
544
643
|
</div>
|
|
545
644
|
<button :class="{ active: activeHotOnly }" @click="activeHotOnly = !activeHotOnly">hot only</button>
|
|
546
645
|
<button :class="{ active: frozen }" style="margin-left: auto" @click="toggleFreeze">
|
|
@@ -599,9 +698,7 @@ function pathLabel(node: ComponentNode) {
|
|
|
599
698
|
:node="root"
|
|
600
699
|
:mode="activeMode"
|
|
601
700
|
:threshold="activeThreshold"
|
|
602
|
-
:hot-only="activeHotOnly"
|
|
603
701
|
:selected="activeSelected?.id"
|
|
604
|
-
:search="search"
|
|
605
702
|
:expanded-ids="expandedIds"
|
|
606
703
|
@select="selectNode"
|
|
607
704
|
@toggle="toggleNode"
|
|
@@ -621,9 +718,23 @@ function pathLabel(node: ComponentNode) {
|
|
|
621
718
|
</div>
|
|
622
719
|
|
|
623
720
|
<div class="detail-pill-row">
|
|
624
|
-
<span class="detail-pill mono">
|
|
721
|
+
<span class="detail-pill mono">
|
|
722
|
+
{{ activeSelected.rerenders + activeSelected.mountCount }} render{{
|
|
723
|
+
activeSelected.rerenders + activeSelected.mountCount !== 1 ? 's' : ''
|
|
724
|
+
}}
|
|
725
|
+
</span>
|
|
726
|
+
<span class="detail-pill mono muted">
|
|
727
|
+
{{ activeSelected.mountCount }} mount{{ activeSelected.mountCount !== 1 ? 's' : '' }}
|
|
728
|
+
</span>
|
|
729
|
+
<span v-if="activeSelected.rerenders" class="detail-pill mono">
|
|
730
|
+
{{ activeSelected.rerenders }} re-render{{ activeSelected.rerenders !== 1 ? 's' : '' }}
|
|
731
|
+
</span>
|
|
732
|
+
<span v-if="activeSelected.isPersistent" class="detail-pill mono persistent">persistent</span>
|
|
733
|
+
<span v-if="activeSelected.isHydrationMount" class="detail-pill mono hydrated">hydrated</span>
|
|
625
734
|
<span class="detail-pill mono">{{ activeSelected.avgMs.toFixed(1) }}ms avg</span>
|
|
626
|
-
<span class="detail-pill mono" :class="{ hot: isHot(activeSelected) }">
|
|
735
|
+
<span class="detail-pill mono" :class="{ hot: isHot(activeSelected) }">
|
|
736
|
+
{{ isHot(activeSelected) ? 'hot' : 'cool' }}
|
|
737
|
+
</span>
|
|
627
738
|
</div>
|
|
628
739
|
|
|
629
740
|
<div class="section-label">identity</div>
|
|
@@ -644,12 +755,24 @@ function pathLabel(node: ComponentNode) {
|
|
|
644
755
|
|
|
645
756
|
<div class="section-label">rendering</div>
|
|
646
757
|
<div class="meta-grid">
|
|
647
|
-
<span class="muted text-sm">
|
|
648
|
-
<span class="mono text-sm">{{
|
|
758
|
+
<span class="muted text-sm">total renders</span>
|
|
759
|
+
<span class="mono text-sm">{{ activeSelected.rerenders + activeSelected.mountCount }}</span>
|
|
760
|
+
<span class="muted text-sm">re-renders</span>
|
|
761
|
+
<span class="mono text-sm">{{ activeSelected.rerenders }}</span>
|
|
762
|
+
<span class="muted text-sm">mounts</span>
|
|
763
|
+
<span class="mono text-sm">{{ activeSelected.mountCount }}</span>
|
|
764
|
+
<span class="muted text-sm">persistent</span>
|
|
765
|
+
<span class="mono text-sm" :style="{ color: activeSelected.isPersistent ? 'var(--amber)' : 'inherit' }">
|
|
766
|
+
{{ activeSelected.isPersistent ? 'yes — survives navigation' : 'no' }}
|
|
767
|
+
</span>
|
|
768
|
+
<span class="muted text-sm">hydration mount</span>
|
|
769
|
+
<span class="mono text-sm">{{ activeSelected.isHydrationMount ? 'yes — SSR hydrated' : 'no' }}</span>
|
|
770
|
+
<span class="muted text-sm">avg render time</span>
|
|
771
|
+
<span class="mono text-sm">{{ activeSelected.avgMs.toFixed(1) }}ms</span>
|
|
649
772
|
<span class="muted text-sm">threshold</span>
|
|
650
|
-
<span class="mono text-sm">{{ activeThreshold }}</span>
|
|
651
|
-
<span class="muted text-sm">
|
|
652
|
-
<span class="mono text-sm">{{ activeMode === 'count' ? 'render count' : 'render time' }}</span>
|
|
773
|
+
<span class="mono text-sm">{{ activeThreshold }}{{ activeMode === 'count' ? '+ renders' : 'ms+' }}</span>
|
|
774
|
+
<span class="muted text-sm">mode</span>
|
|
775
|
+
<span class="mono text-sm">{{ activeMode === 'count' ? 're-render count' : 'render time' }}</span>
|
|
653
776
|
</div>
|
|
654
777
|
|
|
655
778
|
<div class="section-label">triggers</div>
|
|
@@ -698,6 +821,12 @@ function pathLabel(node: ComponentNode) {
|
|
|
698
821
|
flex-shrink: 0;
|
|
699
822
|
}
|
|
700
823
|
|
|
824
|
+
.stat-sub {
|
|
825
|
+
margin-top: 4px;
|
|
826
|
+
font-size: 11px;
|
|
827
|
+
color: var(--text3);
|
|
828
|
+
}
|
|
829
|
+
|
|
701
830
|
.inspector {
|
|
702
831
|
display: grid;
|
|
703
832
|
grid-template-columns: minmax(220px, 280px) minmax(0, 1fr) minmax(260px, 320px);
|
|
@@ -811,7 +940,7 @@ function pathLabel(node: ComponentNode) {
|
|
|
811
940
|
|
|
812
941
|
:deep(.tree-row) {
|
|
813
942
|
display: grid;
|
|
814
|
-
grid-template-columns: 18px
|
|
943
|
+
grid-template-columns: 8px 18px minmax(0, 1fr) auto;
|
|
815
944
|
align-items: center;
|
|
816
945
|
gap: 6px;
|
|
817
946
|
min-width: 0;
|
|
@@ -844,17 +973,20 @@ function pathLabel(node: ComponentNode) {
|
|
|
844
973
|
background: transparent;
|
|
845
974
|
color: var(--text3);
|
|
846
975
|
padding: 0;
|
|
847
|
-
font-size:
|
|
976
|
+
font-size: 14px;
|
|
848
977
|
display: inline-flex;
|
|
849
978
|
align-items: center;
|
|
850
979
|
justify-content: center;
|
|
851
980
|
}
|
|
852
981
|
|
|
853
982
|
:deep(.tree-toggle:disabled) {
|
|
854
|
-
opacity: 0.4;
|
|
855
983
|
cursor: default;
|
|
856
984
|
}
|
|
857
985
|
|
|
986
|
+
:deep(.tree-toggle.empty) {
|
|
987
|
+
opacity: 0;
|
|
988
|
+
}
|
|
989
|
+
|
|
858
990
|
:deep(.tree-rail) {
|
|
859
991
|
display: block;
|
|
860
992
|
width: 2px;
|
|
@@ -904,6 +1036,7 @@ function pathLabel(node: ComponentNode) {
|
|
|
904
1036
|
min-width: 92px;
|
|
905
1037
|
justify-content: flex-end;
|
|
906
1038
|
flex-shrink: 0;
|
|
1039
|
+
gap: 6px;
|
|
907
1040
|
}
|
|
908
1041
|
|
|
909
1042
|
:deep(.tree-metric-pill) {
|
|
@@ -919,6 +1052,28 @@ function pathLabel(node: ComponentNode) {
|
|
|
919
1052
|
color: var(--text3);
|
|
920
1053
|
}
|
|
921
1054
|
|
|
1055
|
+
:deep(.tree-persistent-pill) {
|
|
1056
|
+
display: inline-flex;
|
|
1057
|
+
align-items: center;
|
|
1058
|
+
padding: 2px 8px;
|
|
1059
|
+
border: 1px solid color-mix(in srgb, var(--amber) 55%, var(--border));
|
|
1060
|
+
border-radius: 999px;
|
|
1061
|
+
background: color-mix(in srgb, var(--amber) 10%, var(--bg2));
|
|
1062
|
+
font-size: 10px;
|
|
1063
|
+
color: color-mix(in srgb, var(--amber) 80%, var(--text));
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
:deep(.tree-hydration-pill) {
|
|
1067
|
+
display: inline-flex;
|
|
1068
|
+
align-items: center;
|
|
1069
|
+
padding: 2px 8px;
|
|
1070
|
+
border: 1px solid color-mix(in srgb, var(--teal) 55%, var(--border));
|
|
1071
|
+
border-radius: 999px;
|
|
1072
|
+
background: color-mix(in srgb, var(--teal) 10%, var(--bg2));
|
|
1073
|
+
font-size: 10px;
|
|
1074
|
+
color: color-mix(in srgb, var(--teal) 80%, var(--text));
|
|
1075
|
+
}
|
|
1076
|
+
|
|
922
1077
|
:deep(.tree-children) {
|
|
923
1078
|
margin-left: 7px;
|
|
924
1079
|
padding-left: 11px;
|
|
@@ -965,6 +1120,21 @@ function pathLabel(node: ComponentNode) {
|
|
|
965
1120
|
color: var(--red);
|
|
966
1121
|
}
|
|
967
1122
|
|
|
1123
|
+
.detail-pill.persistent {
|
|
1124
|
+
border-color: color-mix(in srgb, var(--amber) 55%, var(--border));
|
|
1125
|
+
color: color-mix(in srgb, var(--amber) 80%, var(--text));
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
.detail-pill.hydrated {
|
|
1129
|
+
border-color: color-mix(in srgb, var(--teal) 55%, var(--border));
|
|
1130
|
+
color: color-mix(in srgb, var(--teal) 80%, var(--text));
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
.detail-pill.muted {
|
|
1134
|
+
color: var(--text3);
|
|
1135
|
+
border-color: var(--border);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
968
1138
|
.section-label {
|
|
969
1139
|
font-size: 10px;
|
|
970
1140
|
font-weight: 500;
|
|
@@ -56,8 +56,8 @@ const timelineGeometry = computed(() => {
|
|
|
56
56
|
return []
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const minT =
|
|
60
|
-
const maxT =
|
|
59
|
+
const minT = all.reduce((min, e) => Math.min(min, e.startTime), all[0].startTime)
|
|
60
|
+
const maxT = all.reduce((max, e) => Math.max(max, e.endTime ?? e.startTime + 400), 0)
|
|
61
61
|
const span = Math.max(maxT - minT, 1)
|
|
62
62
|
|
|
63
63
|
return all.map((e) => ({
|
package/dist/module.json
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export interface RefChangeEvent {
|
|
2
|
+
t: number;
|
|
3
|
+
key: string;
|
|
4
|
+
value: unknown;
|
|
5
|
+
}
|
|
1
6
|
export interface ComposableEntry {
|
|
2
7
|
id: string;
|
|
3
8
|
name: string;
|
|
@@ -10,6 +15,13 @@ export interface ComposableEntry {
|
|
|
10
15
|
type: 'ref' | 'computed' | 'reactive';
|
|
11
16
|
value: unknown;
|
|
12
17
|
}>;
|
|
18
|
+
/** Capped at MAX_HISTORY_PER_ENTRY events, newest last */
|
|
19
|
+
history: RefChangeEvent[];
|
|
20
|
+
/**
|
|
21
|
+
* Keys whose underlying ref/reactive object is shared across multiple
|
|
22
|
+
* instances of this composable — indicates module-level (global) state.
|
|
23
|
+
*/
|
|
24
|
+
sharedKeys: string[];
|
|
13
25
|
watcherCount: number;
|
|
14
26
|
intervalCount: number;
|
|
15
27
|
lifecycle: {
|
|
@@ -20,6 +32,8 @@ export interface ComposableEntry {
|
|
|
20
32
|
};
|
|
21
33
|
file: string;
|
|
22
34
|
line: number;
|
|
35
|
+
/** Route path the composable was registered on, e.g. "/products". */
|
|
36
|
+
route: string;
|
|
23
37
|
}
|
|
24
38
|
/**
|
|
25
39
|
* Registers a new composable entry, updates an existing one, or retrieves all entries.
|
|
@@ -31,6 +45,12 @@ export interface ComposableEntry {
|
|
|
31
45
|
*/
|
|
32
46
|
export declare function setupComposableRegistry(): {
|
|
33
47
|
register: (entry: ComposableEntry) => void;
|
|
48
|
+
registerLiveRefs: (id: string, refs: Record<string, import("vue").Ref<unknown>>) => void;
|
|
49
|
+
registerRawRefs: (id: string, refs: Record<string, unknown>) => void;
|
|
50
|
+
onComposableChange: (cb: () => void) => void;
|
|
51
|
+
clear: () => void;
|
|
52
|
+
setRoute: (path: string) => void;
|
|
53
|
+
getRoute: () => string;
|
|
34
54
|
update: (id: string, patch: Partial<ComposableEntry>) => void;
|
|
35
55
|
getAll: () => ComposableEntry[];
|
|
36
56
|
};
|