nuxt-devtools-observatory 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -0
- package/client/.env.example +1 -0
- package/client/dist/assets/index-BqKYgjVB.js +20 -0
- package/client/dist/assets/index-bs1JBJ2u.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +4 -0
- package/client/src/components/Flamegraph.vue +4 -4
- package/client/src/components/SpanInspector.vue +1 -1
- package/client/src/composables/composable-search.ts +3 -0
- package/client/src/composables/trace-render-aggregation.ts +11 -2
- package/client/src/composables/useVirtualizationConfig.ts +40 -0
- package/client/src/composables/useVirtualizationFlags.ts +129 -0
- package/client/src/views/ComposableTracker.vue +212 -71
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/ProvideInjectGraph.vue +41 -18
- package/client/src/views/RenderHeatmap.vue +329 -75
- package/client/src/views/TraceViewer.vue +190 -20
- package/client/src/views/TransitionTimeline.vue +112 -19
- package/dist/module.d.mts +5 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -22
- package/dist/runtime/composables/render-registry.js +6 -4
- package/dist/runtime/instrumentation/asyncData.d.ts +1 -1
- package/dist/runtime/instrumentation/fetch.d.ts +7 -1
- package/dist/runtime/instrumentation/fetch.js +22 -1
- package/dist/runtime/plugin.js +4 -1
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +86 -0
- package/package.json +14 -3
- package/client/dist/assets/index-5Wl1XYRH.js +0 -17
- package/client/dist/assets/index-DT_QUiIh.css +0 -1
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, defineComponent, h, ref, watch, type VNode } from 'vue'
|
|
3
|
+
import { useVirtualizer } from '@tanstack/vue-virtual'
|
|
4
|
+
import { useVirtualizationConfig } from '@observatory-client/composables/useVirtualizationConfig'
|
|
5
|
+
import { useVirtualizationFlags } from '@observatory-client/composables/useVirtualizationFlags'
|
|
3
6
|
import { useResizablePane } from '@observatory-client/composables/useResizablePane'
|
|
4
7
|
import { useObservatoryData, openInEditor as openInEditorFromStore } from '@observatory-client/stores/observatory'
|
|
5
8
|
import { exportJson, importJson } from '@observatory-client/composables/useExportImport'
|
|
@@ -26,6 +29,37 @@ interface ComponentNode {
|
|
|
26
29
|
route: string
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
interface VisibleTreeRow {
|
|
33
|
+
node: ComponentNode
|
|
34
|
+
primaryBadge: string | null
|
|
35
|
+
metricValue: string
|
|
36
|
+
metricLabel: string
|
|
37
|
+
hot: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function nodeBadges(node: ComponentNode): string[] {
|
|
41
|
+
const badges: string[] = []
|
|
42
|
+
const normalizedElement = node.element?.toLowerCase()
|
|
43
|
+
|
|
44
|
+
if (node.element && node.element !== node.label && !['div', 'span', 'p'].includes(normalizedElement ?? '')) {
|
|
45
|
+
badges.push(node.element)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (node.file !== 'unknown') {
|
|
49
|
+
const fileBadge =
|
|
50
|
+
node.file
|
|
51
|
+
.split('/')
|
|
52
|
+
.pop()
|
|
53
|
+
?.replace(/\.vue$/i, '') ?? node.file
|
|
54
|
+
|
|
55
|
+
if (fileBadge !== node.label && !badges.includes(fileBadge)) {
|
|
56
|
+
badges.push(fileBadge)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return badges
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
const TreeNode = defineComponent({
|
|
30
64
|
name: 'TreeNode',
|
|
31
65
|
props: {
|
|
@@ -58,24 +92,7 @@ const TreeNode = defineComponent({
|
|
|
58
92
|
const canExpand = node.children.length > 0
|
|
59
93
|
const metric = props.mode === 'count' ? `${node.rerenders + node.mountCount}` : `${node.avgMs.toFixed(1)}ms`
|
|
60
94
|
const metricLabel = props.mode === 'count' ? 'renders' : 'avg'
|
|
61
|
-
const badges =
|
|
62
|
-
const normalizedElement = node.element?.toLowerCase()
|
|
63
|
-
|
|
64
|
-
if (node.element && node.element !== node.label && !['div', 'span', 'p'].includes(normalizedElement ?? '')) {
|
|
65
|
-
badges.push(node.element)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (node.file !== 'unknown') {
|
|
69
|
-
const fileBadge =
|
|
70
|
-
node.file
|
|
71
|
-
.split('/')
|
|
72
|
-
.pop()
|
|
73
|
-
?.replace(/\.vue$/i, '') ?? node.file
|
|
74
|
-
|
|
75
|
-
if (fileBadge !== node.label && !badges.includes(fileBadge)) {
|
|
76
|
-
badges.push(fileBadge)
|
|
77
|
-
}
|
|
78
|
-
}
|
|
95
|
+
const badges = nodeBadges(node)
|
|
79
96
|
|
|
80
97
|
return h('div', { class: 'tree-node' }, [
|
|
81
98
|
h(
|
|
@@ -206,6 +223,10 @@ const activeRootId = ref<string | null>(null)
|
|
|
206
223
|
const expandedIds = ref<Set<string>>(new Set())
|
|
207
224
|
const frozenSnapshot = ref<RenderEntry[]>([])
|
|
208
225
|
const expansionReady = ref(false)
|
|
226
|
+
const treeFrameRef = ref<HTMLElement | null>(null)
|
|
227
|
+
|
|
228
|
+
const { effective: virtualizationFlags } = useVirtualizationFlags()
|
|
229
|
+
const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 34, overscan: 6 })
|
|
209
230
|
|
|
210
231
|
function displayLabel(entry: RenderEntry) {
|
|
211
232
|
if (entry.name && entry.name !== 'unknown' && !/^Component#\d+$/.test(entry.name)) {
|
|
@@ -303,33 +324,55 @@ function flatten(nodes: ComponentNode[]) {
|
|
|
303
324
|
return flat
|
|
304
325
|
}
|
|
305
326
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
}
|
|
327
|
+
function buildSubtreeSizeMap(roots: ComponentNode[]) {
|
|
328
|
+
const sizes = new Map<string, number>()
|
|
309
329
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
node.children.forEach((child) => collectIds(child, target))
|
|
330
|
+
for (const root of roots) {
|
|
331
|
+
const stack: Array<{ node: ComponentNode; visited: boolean }> = [{ node: root, visited: false }]
|
|
313
332
|
|
|
314
|
-
|
|
315
|
-
|
|
333
|
+
while (stack.length) {
|
|
334
|
+
const current = stack.pop()!
|
|
335
|
+
|
|
336
|
+
if (!current.visited) {
|
|
337
|
+
stack.push({ node: current.node, visited: true })
|
|
338
|
+
|
|
339
|
+
for (let i = current.node.children.length - 1; i >= 0; i--) {
|
|
340
|
+
stack.push({ node: current.node.children[i], visited: false })
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
continue
|
|
344
|
+
}
|
|
316
345
|
|
|
317
|
-
|
|
318
|
-
const nextTrail = [...trail, node.id]
|
|
346
|
+
let size = 1
|
|
319
347
|
|
|
320
|
-
|
|
321
|
-
|
|
348
|
+
for (const child of current.node.children) {
|
|
349
|
+
size += sizes.get(child.id) ?? 1
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
sizes.set(current.node.id, size)
|
|
353
|
+
}
|
|
322
354
|
}
|
|
323
355
|
|
|
324
|
-
|
|
325
|
-
|
|
356
|
+
return sizes
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function buildRootLookup(roots: ComponentNode[]) {
|
|
360
|
+
const lookup = new Map<string, string>()
|
|
361
|
+
|
|
362
|
+
for (const root of roots) {
|
|
363
|
+
const stack: ComponentNode[] = [root]
|
|
364
|
+
|
|
365
|
+
while (stack.length) {
|
|
366
|
+
const node = stack.pop()!
|
|
367
|
+
lookup.set(node.id, root.id)
|
|
326
368
|
|
|
327
|
-
|
|
328
|
-
|
|
369
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
370
|
+
stack.push(node.children[i])
|
|
371
|
+
}
|
|
329
372
|
}
|
|
330
373
|
}
|
|
331
374
|
|
|
332
|
-
return
|
|
375
|
+
return lookup
|
|
333
376
|
}
|
|
334
377
|
|
|
335
378
|
function findFirstHotNode(node: ComponentNode): ComponentNode | null {
|
|
@@ -476,6 +519,37 @@ const displayEntries = computed(() => (frozen.value ? frozenSnapshot.value : ren
|
|
|
476
519
|
const rootNodes = computed(() => buildNodes(displayEntries.value))
|
|
477
520
|
const rootMap = computed(() => new Map(rootNodes.value.map((node) => [node.id, node])))
|
|
478
521
|
const allComponents = computed(() => flatten(rootNodes.value))
|
|
522
|
+
const allComponentsById = computed(() => new Map(allComponents.value.map((node) => [node.id, node])))
|
|
523
|
+
const allComponentIds = computed(() => new Set(allComponents.value.map((node) => node.id)))
|
|
524
|
+
const subtreeSizeById = computed(() => buildSubtreeSizeMap(rootNodes.value))
|
|
525
|
+
const rootIdByNodeId = computed(() => buildRootLookup(rootNodes.value))
|
|
526
|
+
|
|
527
|
+
function pathToNodeWithinRoot(targetId: string, rootId?: string | null): string[] {
|
|
528
|
+
const path: string[] = []
|
|
529
|
+
let current = allComponentsById.value.get(targetId)
|
|
530
|
+
|
|
531
|
+
while (current) {
|
|
532
|
+
path.push(current.id)
|
|
533
|
+
|
|
534
|
+
if (rootId && current.id === rootId) {
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!current.parentId) {
|
|
539
|
+
break
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
current = allComponentsById.value.get(current.parentId)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
path.reverse()
|
|
546
|
+
|
|
547
|
+
if (rootId && path[0] !== rootId) {
|
|
548
|
+
return []
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return path
|
|
552
|
+
}
|
|
479
553
|
|
|
480
554
|
const filteredRoots = computed(() => {
|
|
481
555
|
const term = search.value.trim()
|
|
@@ -483,9 +557,11 @@ const filteredRoots = computed(() => {
|
|
|
483
557
|
return rootNodes.value.filter((root) => isVisibleRoot(root, term))
|
|
484
558
|
})
|
|
485
559
|
|
|
560
|
+
const filteredRootById = computed(() => new Map(filteredRoots.value.map((node) => [node.id, node])))
|
|
561
|
+
|
|
486
562
|
const activeRoot = computed(() => {
|
|
487
563
|
if (activeRootId.value) {
|
|
488
|
-
return
|
|
564
|
+
return filteredRootById.value.get(activeRootId.value) ?? rootMap.value.get(activeRootId.value) ?? null
|
|
489
565
|
}
|
|
490
566
|
|
|
491
567
|
return filteredRoots.value[0] ?? rootNodes.value[0] ?? null
|
|
@@ -505,11 +581,99 @@ const visibleTreeRoots = computed(() => {
|
|
|
505
581
|
return [visibleActiveRoot.value]
|
|
506
582
|
})
|
|
507
583
|
|
|
584
|
+
const virtualizedTreeEnabled = computed(() => virtualizationFlags.value.heatmap)
|
|
585
|
+
|
|
586
|
+
function flattenVisibleTree(root: ComponentNode | null, expanded: Set<string>) {
|
|
587
|
+
if (!root) {
|
|
588
|
+
return [] as ComponentNode[]
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const rows: ComponentNode[] = []
|
|
592
|
+
|
|
593
|
+
function walk(node: ComponentNode) {
|
|
594
|
+
rows.push(node)
|
|
595
|
+
|
|
596
|
+
if (!expanded.has(node.id)) {
|
|
597
|
+
return
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
node.children.forEach(walk)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
walk(root)
|
|
604
|
+
|
|
605
|
+
return rows
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const expandedVisibleNodes = computed(() => flattenVisibleTree(visibleActiveRoot.value, expandedIds.value))
|
|
609
|
+
|
|
610
|
+
const treeVirtualizerOptions = computed(() => ({
|
|
611
|
+
count: expandedVisibleNodes.value.length,
|
|
612
|
+
getScrollElement: () => treeFrameRef.value,
|
|
613
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
614
|
+
overscan: virtualizationPreset.value.overscan,
|
|
615
|
+
}))
|
|
616
|
+
|
|
617
|
+
const treeVirtualizer = useVirtualizer(treeVirtualizerOptions)
|
|
618
|
+
|
|
619
|
+
const treeVirtualItems = computed(() => {
|
|
620
|
+
if (!virtualizedTreeEnabled.value) {
|
|
621
|
+
return []
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return treeVirtualizer.value.getVirtualItems()
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
const topTreePadding = computed(() => {
|
|
628
|
+
if (!virtualizedTreeEnabled.value || treeVirtualItems.value.length === 0) {
|
|
629
|
+
return 0
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return treeVirtualItems.value[0].start
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
const bottomTreePadding = computed(() => {
|
|
636
|
+
if (!virtualizedTreeEnabled.value || treeVirtualItems.value.length === 0) {
|
|
637
|
+
return 0
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const total = treeVirtualizer.value.getTotalSize()
|
|
641
|
+
const last = treeVirtualItems.value[treeVirtualItems.value.length - 1]
|
|
642
|
+
|
|
643
|
+
return Math.max(0, total - last.end)
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
const visibleTreeRows = computed(() => {
|
|
647
|
+
if (!virtualizedTreeEnabled.value) {
|
|
648
|
+
return expandedVisibleNodes.value
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return treeVirtualItems.value
|
|
652
|
+
.map((item) => expandedVisibleNodes.value[item.index])
|
|
653
|
+
.filter((node): node is ComponentNode => Boolean(node))
|
|
654
|
+
})
|
|
655
|
+
|
|
656
|
+
const visibleTreeRowItems = computed<VisibleTreeRow[]>(() => {
|
|
657
|
+
const metricLabel = activeMode.value === 'count' ? 'renders' : 'avg'
|
|
658
|
+
|
|
659
|
+
return visibleTreeRows.value.map((node) => {
|
|
660
|
+
const badges = nodeBadges(node)
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
node,
|
|
664
|
+
primaryBadge: badges[0] ?? null,
|
|
665
|
+
metricValue: activeMode.value === 'count' ? `${node.rerenders + node.mountCount}` : `${node.avgMs.toFixed(1)}ms`,
|
|
666
|
+
metricLabel,
|
|
667
|
+
hot: isHot(node),
|
|
668
|
+
}
|
|
669
|
+
})
|
|
670
|
+
})
|
|
671
|
+
|
|
508
672
|
const appEntries = computed(() =>
|
|
509
673
|
filteredRoots.value.map((root, index) => ({
|
|
510
674
|
id: root.id,
|
|
511
675
|
label: `App ${index + 1}`,
|
|
512
|
-
meta: `${
|
|
676
|
+
meta: `${subtreeSizeById.value.get(root.id) ?? 1} nodes`,
|
|
513
677
|
root,
|
|
514
678
|
}))
|
|
515
679
|
)
|
|
@@ -529,7 +693,31 @@ const knownRoutes = computed(() => {
|
|
|
529
693
|
return [...routes].sort()
|
|
530
694
|
})
|
|
531
695
|
|
|
532
|
-
const activeSelected = computed(() =>
|
|
696
|
+
const activeSelected = computed(() => {
|
|
697
|
+
if (!activeSelectedId.value) {
|
|
698
|
+
return null
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return allComponentsById.value.get(activeSelectedId.value) ?? null
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
const activeSelectedTimelineRecent = computed(() => {
|
|
705
|
+
const timeline = activeSelected.value?.timeline ?? []
|
|
706
|
+
|
|
707
|
+
if (!timeline.length) {
|
|
708
|
+
return [] as Array<{ key: string; event: RenderEvent }>
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const recent: Array<{ key: string; event: RenderEvent }> = []
|
|
712
|
+
const end = Math.max(timeline.length - 30, 0)
|
|
713
|
+
|
|
714
|
+
for (let i = timeline.length - 1; i >= end; i--) {
|
|
715
|
+
const event = timeline[i]
|
|
716
|
+
recent.push({ key: `${event.kind}-${event.t}-${i}`, event })
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
return recent
|
|
720
|
+
})
|
|
533
721
|
const totalRenders = computed(() => allComponents.value.reduce((sum, node) => sum + node.rerenders + node.mountCount, 0))
|
|
534
722
|
const hotCount = computed(() => allComponents.value.filter((node) => isHot(node)).length)
|
|
535
723
|
const avgTime = computed(() => {
|
|
@@ -560,12 +748,11 @@ watch(
|
|
|
560
748
|
activeRootId.value = roots[0].id
|
|
561
749
|
}
|
|
562
750
|
|
|
563
|
-
if (activeSelectedId.value && !
|
|
751
|
+
if (activeSelectedId.value && !allComponentIds.value.has(activeSelectedId.value)) {
|
|
564
752
|
activeSelectedId.value = null
|
|
565
753
|
}
|
|
566
754
|
|
|
567
|
-
const
|
|
568
|
-
const preserved = new Set([...expandedIds.value].filter((id) => validIds.has(id)))
|
|
755
|
+
const preserved = new Set([...expandedIds.value].filter((id) => allComponentIds.value.has(id)))
|
|
569
756
|
|
|
570
757
|
if (!expansionReady.value) {
|
|
571
758
|
expandedIds.value = defaultExpandedIds(activeRoot.value)
|
|
@@ -575,7 +762,7 @@ watch(
|
|
|
575
762
|
}
|
|
576
763
|
|
|
577
764
|
if (!search.value.trim() && activeSelectedId.value && activeRoot.value) {
|
|
578
|
-
const selectedPath =
|
|
765
|
+
const selectedPath = pathToNodeWithinRoot(activeSelectedId.value, activeRoot.value.id)
|
|
579
766
|
selectedPath.forEach((id) => preserved.add(id))
|
|
580
767
|
}
|
|
581
768
|
|
|
@@ -598,8 +785,8 @@ watch(search, (term) => {
|
|
|
598
785
|
}
|
|
599
786
|
|
|
600
787
|
if (activeSelectedId.value) {
|
|
601
|
-
const selectedPath =
|
|
602
|
-
expandedIds.value = selectedPath ? new Set(selectedPath) : defaultExpandedIds(activeRoot.value)
|
|
788
|
+
const selectedPath = pathToNodeWithinRoot(activeSelectedId.value, activeRoot.value.id)
|
|
789
|
+
expandedIds.value = selectedPath.length ? new Set(selectedPath) : defaultExpandedIds(activeRoot.value)
|
|
603
790
|
|
|
604
791
|
return
|
|
605
792
|
}
|
|
@@ -634,17 +821,18 @@ watch([activeHotOnly, activeThreshold, activeMode, filteredRoots], () => {
|
|
|
634
821
|
activeSelectedId.value = firstHot.id
|
|
635
822
|
}
|
|
636
823
|
|
|
637
|
-
expandedIds.value = new Set(
|
|
824
|
+
expandedIds.value = new Set(pathToNodeWithinRoot(firstHot.id, topLevelRoot.id))
|
|
638
825
|
})
|
|
639
826
|
|
|
640
827
|
function selectNode(node: ComponentNode) {
|
|
641
828
|
activeSelectedId.value = node.id
|
|
642
829
|
|
|
643
|
-
const
|
|
830
|
+
const rootId = rootIdByNodeId.value.get(node.id)
|
|
831
|
+
const topLevelRoot = rootId ? rootMap.value.get(rootId) : null
|
|
644
832
|
|
|
645
833
|
if (topLevelRoot) {
|
|
646
834
|
activeRootId.value = topLevelRoot.id
|
|
647
|
-
expandedIds.value = new Set(
|
|
835
|
+
expandedIds.value = new Set(pathToNodeWithinRoot(node.id, topLevelRoot.id))
|
|
648
836
|
}
|
|
649
837
|
}
|
|
650
838
|
|
|
@@ -829,21 +1017,89 @@ function formatTimestamp(t: number): string {
|
|
|
829
1017
|
/>
|
|
830
1018
|
</div>
|
|
831
1019
|
|
|
832
|
-
<div class="render-heatmap__tree-frame">
|
|
1020
|
+
<div ref="treeFrameRef" class="render-heatmap__tree-frame">
|
|
833
1021
|
<div class="render-heatmap__tree-canvas tree-canvas">
|
|
834
|
-
<
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1022
|
+
<template v-if="virtualizedTreeEnabled">
|
|
1023
|
+
<div
|
|
1024
|
+
v-if="topTreePadding > 0"
|
|
1025
|
+
class="render-heatmap__tree-spacer"
|
|
1026
|
+
:style="{ height: `${topTreePadding}px` }"
|
|
1027
|
+
aria-hidden="true"
|
|
1028
|
+
/>
|
|
1029
|
+
<div
|
|
1030
|
+
v-for="row in visibleTreeRowItems"
|
|
1031
|
+
:key="row.node.id"
|
|
1032
|
+
class="tree-row"
|
|
1033
|
+
:class="{ selected: activeSelected?.id === row.node.id, hot: row.hot }"
|
|
1034
|
+
:style="{ '--tree-depth': String(row.node.depth) }"
|
|
1035
|
+
@click="selectNode(row.node)"
|
|
1036
|
+
>
|
|
1037
|
+
<span class="tree-rail" aria-hidden="true" />
|
|
1038
|
+
<button
|
|
1039
|
+
class="tree-toggle"
|
|
1040
|
+
:class="{ empty: !row.node.children.length }"
|
|
1041
|
+
:disabled="!row.node.children.length"
|
|
1042
|
+
@click.stop="row.node.children.length ? toggleNode(row.node.id) : undefined"
|
|
1043
|
+
>
|
|
1044
|
+
{{ row.node.children.length ? (expandedIds.has(row.node.id) ? '⌄' : '›') : '' }}
|
|
1045
|
+
</button>
|
|
1046
|
+
<div class="tree-copy">
|
|
1047
|
+
<span class="tree-name mono" :title="row.node.label">{{ row.node.label }}</span>
|
|
1048
|
+
<div v-if="row.primaryBadge" class="tree-badges">
|
|
1049
|
+
<span class="tree-badge mono" :title="row.primaryBadge">{{ row.primaryBadge }}</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
</div>
|
|
1052
|
+
<div class="tree-metrics mono">
|
|
1053
|
+
<span
|
|
1054
|
+
v-if="row.node.isPersistent"
|
|
1055
|
+
class="tree-persistent-pill"
|
|
1056
|
+
title="Layout / persistent component — survives navigation"
|
|
1057
|
+
>
|
|
1058
|
+
persistent
|
|
1059
|
+
</span>
|
|
1060
|
+
<span
|
|
1061
|
+
v-if="row.node.isHydrationMount"
|
|
1062
|
+
class="tree-hydration-pill"
|
|
1063
|
+
title="First mount was SSR hydration — not a user-triggered render"
|
|
1064
|
+
>
|
|
1065
|
+
hydrated
|
|
1066
|
+
</span>
|
|
1067
|
+
<span class="tree-metric-pill">
|
|
1068
|
+
{{ row.metricValue }}
|
|
1069
|
+
{{ row.metricLabel }}
|
|
1070
|
+
</span>
|
|
1071
|
+
<button
|
|
1072
|
+
v-if="row.node.file && row.node.file !== 'unknown'"
|
|
1073
|
+
class="tree-jump-btn"
|
|
1074
|
+
:title="`Open ${row.node.file} in editor`"
|
|
1075
|
+
@click.stop="openInEditor(row.node.file)"
|
|
1076
|
+
>
|
|
1077
|
+
↗
|
|
1078
|
+
</button>
|
|
1079
|
+
</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
<div
|
|
1082
|
+
v-if="bottomTreePadding > 0"
|
|
1083
|
+
class="render-heatmap__tree-spacer"
|
|
1084
|
+
:style="{ height: `${bottomTreePadding}px` }"
|
|
1085
|
+
aria-hidden="true"
|
|
1086
|
+
/>
|
|
1087
|
+
</template>
|
|
1088
|
+
<template v-else>
|
|
1089
|
+
<TreeNode
|
|
1090
|
+
v-for="root in visibleTreeRoots"
|
|
1091
|
+
:key="root.id"
|
|
1092
|
+
:node="root"
|
|
1093
|
+
:mode="activeMode"
|
|
1094
|
+
:threshold="activeThreshold"
|
|
1095
|
+
:selected="activeSelected?.id"
|
|
1096
|
+
:expanded-ids="expandedIds"
|
|
1097
|
+
@select="selectNode"
|
|
1098
|
+
@toggle="toggleNode"
|
|
1099
|
+
/>
|
|
1100
|
+
</template>
|
|
845
1101
|
</div>
|
|
846
|
-
<div v-if="!
|
|
1102
|
+
<div v-if="!expandedVisibleNodes.length" class="render-heatmap__detail-empty">
|
|
847
1103
|
{{ connected ? 'No render activity recorded yet.' : 'Waiting for connection to the Nuxt app…' }}
|
|
848
1104
|
</div>
|
|
849
1105
|
</div>
|
|
@@ -951,16 +1207,14 @@ function formatTimestamp(t: number): string {
|
|
|
951
1207
|
</div>
|
|
952
1208
|
<div v-if="!activeSelected.timeline.length" class="muted text-sm">no timeline events yet</div>
|
|
953
1209
|
<div v-else class="render-heatmap__timeline-list">
|
|
954
|
-
<div
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
class="render-heatmap__timeline-row
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
<span class="render-heatmap__timeline-
|
|
962
|
-
<span v-if="event.triggerKey" class="render-heatmap__timeline-trigger mono muted">{{ event.triggerKey }}</span>
|
|
963
|
-
<span class="render-heatmap__timeline-route mono muted">{{ event.route }}</span>
|
|
1210
|
+
<div v-for="row in activeSelectedTimelineRecent" :key="row.key" class="render-heatmap__timeline-row">
|
|
1211
|
+
<span class="render-heatmap__timeline-kind mono" :class="row.event.kind">{{ row.event.kind }}</span>
|
|
1212
|
+
<span class="render-heatmap__timeline-time mono muted">{{ formatTimestamp(row.event.t) }}</span>
|
|
1213
|
+
<span class="render-heatmap__timeline-dur mono">{{ formatMs(row.event.durationMs) }}</span>
|
|
1214
|
+
<span v-if="row.event.triggerKey" class="render-heatmap__timeline-trigger mono muted">
|
|
1215
|
+
{{ row.event.triggerKey }}
|
|
1216
|
+
</span>
|
|
1217
|
+
<span class="render-heatmap__timeline-route mono muted">{{ row.event.route }}</span>
|
|
964
1218
|
</div>
|
|
965
1219
|
<div v-if="activeSelected.timeline.length > 30" class="render-heatmap__compact-muted muted text-sm">
|
|
966
1220
|
… {{ activeSelected.timeline.length - 30 }} earlier events
|
|
@@ -1027,11 +1281,6 @@ function formatTimestamp(t: number): string {
|
|
|
1027
1281
|
min-height: 0;
|
|
1028
1282
|
}
|
|
1029
1283
|
|
|
1030
|
-
.render-heatmap__roots-panel,
|
|
1031
|
-
.render-heatmap__detail-panel {
|
|
1032
|
-
flex-shrink: 0;
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
1284
|
.render-heatmap__roots-panel {
|
|
1036
1285
|
width: 240px;
|
|
1037
1286
|
margin-right: 12px;
|
|
@@ -1048,6 +1297,7 @@ function formatTimestamp(t: number): string {
|
|
|
1048
1297
|
|
|
1049
1298
|
.render-heatmap__roots-panel,
|
|
1050
1299
|
.render-heatmap__detail-panel {
|
|
1300
|
+
flex-shrink: 0;
|
|
1051
1301
|
display: flex;
|
|
1052
1302
|
flex-direction: column;
|
|
1053
1303
|
overflow: auto;
|
|
@@ -1134,6 +1384,10 @@ function formatTimestamp(t: number): string {
|
|
|
1134
1384
|
width: max-content;
|
|
1135
1385
|
}
|
|
1136
1386
|
|
|
1387
|
+
.render-heatmap__tree-spacer {
|
|
1388
|
+
width: 100%;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1137
1391
|
:deep(.tree-node) {
|
|
1138
1392
|
margin-bottom: 4px;
|
|
1139
1393
|
}
|