nuxt-devtools-observatory 0.1.32 → 0.1.34
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 +37 -1
- package/client/.env.example +2 -0
- package/client/dist/assets/index-BO7neKEi.css +1 -0
- package/client/dist/assets/index-fFBuk6M6.js +20 -0
- package/client/dist/index.html +2 -2
- package/client/src/App.vue +8 -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/stores/observatory.ts +20 -0
- package/client/src/views/ComposableTracker.vue +212 -71
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/PiniaStoreTracker.vue +343 -0
- package/client/src/views/ProvideInjectGraph.vue +66 -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 +15 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +28 -24
- package/dist/runtime/composables/pinia-store-registry.d.ts +44 -0
- package/dist/runtime/composables/pinia-store-registry.js +447 -0
- package/dist/runtime/composables/provide-inject-registry.js +13 -8
- 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 +39 -2
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +100 -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, ref } 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 { useObservatoryData } from '@observatory-client/stores/observatory'
|
|
4
7
|
import Flamegraph from '@observatory-client/components/Flamegraph.vue'
|
|
5
8
|
import WaterfallView from '@observatory-client/components/WaterfallView.vue'
|
|
@@ -10,11 +13,18 @@ import { exportJson, importJson } from '@observatory-client/composables/useExpor
|
|
|
10
13
|
import {
|
|
11
14
|
buildRenderSummaryForTrace,
|
|
12
15
|
buildCrossTraceRenderSummary,
|
|
16
|
+
type CrossTraceRenderSummaryRow,
|
|
13
17
|
type TraceRenderStatsRow,
|
|
14
18
|
} from '@observatory-client/composables/trace-render-aggregation'
|
|
15
19
|
import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
|
|
16
20
|
import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
|
|
17
21
|
|
|
22
|
+
type TraceSpanRow = {
|
|
23
|
+
span: TraceSpan
|
|
24
|
+
displayName: string
|
|
25
|
+
uid: string | number | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
const { traces, connected } = useObservatoryData()
|
|
19
29
|
|
|
20
30
|
const importedTraces = ref<TraceEntry[]>([])
|
|
@@ -33,6 +43,11 @@ const crossTraceOnlyComparable = ref(false)
|
|
|
33
43
|
const crossTraceSearch = ref('')
|
|
34
44
|
const highlightedUid = ref<string | number | undefined>(undefined)
|
|
35
45
|
const highlightedComponentKey = ref<string | undefined>(undefined)
|
|
46
|
+
const traceListScrollRef = ref<HTMLElement | null>(null)
|
|
47
|
+
const crossTraceScrollRef = ref<HTMLElement | null>(null)
|
|
48
|
+
|
|
49
|
+
const { effective: virtualizationFlags } = useVirtualizationFlags()
|
|
50
|
+
const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 34, overscan: 6 })
|
|
36
51
|
|
|
37
52
|
const {
|
|
38
53
|
searchQuery,
|
|
@@ -54,6 +69,10 @@ const filteredTraces = computed(() => {
|
|
|
54
69
|
return applyFilters(sortedTraces.value)
|
|
55
70
|
})
|
|
56
71
|
|
|
72
|
+
const filteredTraceById = computed(() => {
|
|
73
|
+
return new Map(filteredTraces.value.map((trace) => [trace.id, trace] as const))
|
|
74
|
+
})
|
|
75
|
+
|
|
57
76
|
const traceCountLabel = computed(() => {
|
|
58
77
|
const suffix = isImportMode.value ? ' (imported)' : ''
|
|
59
78
|
|
|
@@ -69,7 +88,7 @@ const selectedTrace = computed(() => {
|
|
|
69
88
|
return filteredTraces.value[0]
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
return
|
|
91
|
+
return filteredTraceById.value.get(selectedTraceId.value)
|
|
73
92
|
})
|
|
74
93
|
|
|
75
94
|
const renderSummary = computed<TraceRenderStatsRow[]>(() => buildRenderSummaryForTrace(selectedTrace.value))
|
|
@@ -125,6 +144,100 @@ const crossTraceRows = computed(() => {
|
|
|
125
144
|
|
|
126
145
|
const crossTraceRegressionsCount = computed(() => crossTraceRenderSummary.value.filter((row) => (row.deltaVsBaseline ?? 0) > 0).length)
|
|
127
146
|
|
|
147
|
+
const virtualizedTraceRowsEnabled = computed(() => virtualizationFlags.value.traces)
|
|
148
|
+
|
|
149
|
+
const traceListVirtualizerOptions = computed(() => ({
|
|
150
|
+
count: filteredTraces.value.length,
|
|
151
|
+
getScrollElement: () => traceListScrollRef.value,
|
|
152
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
153
|
+
overscan: virtualizationPreset.value.overscan,
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
const traceListVirtualizer = useVirtualizer(traceListVirtualizerOptions)
|
|
157
|
+
|
|
158
|
+
const traceListVirtualItems = computed(() => {
|
|
159
|
+
if (!virtualizedTraceRowsEnabled.value) {
|
|
160
|
+
return []
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return traceListVirtualizer.value.getVirtualItems()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const traceListTopPadding = computed(() => {
|
|
167
|
+
if (!virtualizedTraceRowsEnabled.value || traceListVirtualItems.value.length === 0) {
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return traceListVirtualItems.value[0].start
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const traceListBottomPadding = computed(() => {
|
|
175
|
+
if (!virtualizedTraceRowsEnabled.value || traceListVirtualItems.value.length === 0) {
|
|
176
|
+
return 0
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const total = traceListVirtualizer.value.getTotalSize()
|
|
180
|
+
const last = traceListVirtualItems.value[traceListVirtualItems.value.length - 1]
|
|
181
|
+
|
|
182
|
+
return Math.max(0, total - last.end)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const visibleTraceRows = computed(() => {
|
|
186
|
+
if (!virtualizedTraceRowsEnabled.value) {
|
|
187
|
+
return filteredTraces.value
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return traceListVirtualItems.value
|
|
191
|
+
.map((item) => filteredTraces.value[item.index])
|
|
192
|
+
.filter((trace): trace is TraceEntry => Boolean(trace))
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const crossTraceVirtualizerOptions = computed(() => ({
|
|
196
|
+
count: crossTraceRows.value.length,
|
|
197
|
+
getScrollElement: () => crossTraceScrollRef.value,
|
|
198
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
199
|
+
overscan: virtualizationPreset.value.overscan,
|
|
200
|
+
}))
|
|
201
|
+
|
|
202
|
+
const crossTraceVirtualizer = useVirtualizer(crossTraceVirtualizerOptions)
|
|
203
|
+
|
|
204
|
+
const crossTraceVirtualItems = computed(() => {
|
|
205
|
+
if (!virtualizedTraceRowsEnabled.value) {
|
|
206
|
+
return []
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return crossTraceVirtualizer.value.getVirtualItems()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const crossTraceTopPadding = computed(() => {
|
|
213
|
+
if (!virtualizedTraceRowsEnabled.value || crossTraceVirtualItems.value.length === 0) {
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return crossTraceVirtualItems.value[0].start
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const crossTraceBottomPadding = computed(() => {
|
|
221
|
+
if (!virtualizedTraceRowsEnabled.value || crossTraceVirtualItems.value.length === 0) {
|
|
222
|
+
return 0
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const total = crossTraceVirtualizer.value.getTotalSize()
|
|
226
|
+
const last = crossTraceVirtualItems.value[crossTraceVirtualItems.value.length - 1]
|
|
227
|
+
|
|
228
|
+
return Math.max(0, total - last.end)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const visibleCrossTraceRows = computed(() => {
|
|
232
|
+
if (!virtualizedTraceRowsEnabled.value) {
|
|
233
|
+
return crossTraceRows.value
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return crossTraceVirtualItems.value
|
|
237
|
+
.map((item) => crossTraceRows.value[item.index])
|
|
238
|
+
.filter((row): row is CrossTraceRenderSummaryRow => Boolean(row))
|
|
239
|
+
})
|
|
240
|
+
|
|
128
241
|
function elapsedFromSpans(trace: TraceEntry): number | undefined {
|
|
129
242
|
if (!trace.spans.length) {
|
|
130
243
|
return undefined
|
|
@@ -201,6 +314,26 @@ function getSpanDisplayName(span: TraceSpan): string {
|
|
|
201
314
|
return span.name
|
|
202
315
|
}
|
|
203
316
|
|
|
317
|
+
function getSpanUid(span: TraceSpan): string | number | undefined {
|
|
318
|
+
const metadata = span.metadata as Record<string, unknown> | undefined
|
|
319
|
+
|
|
320
|
+
return metadata?.uid as string | number | undefined
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const selectedTraceSpanRows = computed<TraceSpanRow[]>(() => {
|
|
324
|
+
const trace = selectedTrace.value
|
|
325
|
+
|
|
326
|
+
if (!trace) {
|
|
327
|
+
return []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return trace.spans.map((span) => ({
|
|
331
|
+
span,
|
|
332
|
+
displayName: getSpanDisplayName(span),
|
|
333
|
+
uid: getSpanUid(span),
|
|
334
|
+
}))
|
|
335
|
+
})
|
|
336
|
+
|
|
204
337
|
function selectTrace(trace: TraceEntry) {
|
|
205
338
|
selectedTraceId.value = trace.id
|
|
206
339
|
selectedSpan.value = undefined
|
|
@@ -322,13 +455,17 @@ function sortIndicator(key: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalM
|
|
|
322
455
|
return crossTraceSortDir.value === 'desc' ? ' ↓' : ' ↑'
|
|
323
456
|
}
|
|
324
457
|
|
|
458
|
+
const crossTraceRowByKey = computed(() => {
|
|
459
|
+
return new Map(crossTraceRows.value.map((row) => [row.componentKey, row] as const))
|
|
460
|
+
})
|
|
461
|
+
|
|
325
462
|
function clearCrossTraceHighlight() {
|
|
326
463
|
highlightedComponentKey.value = undefined
|
|
327
464
|
highlightedUid.value = undefined
|
|
328
465
|
}
|
|
329
466
|
|
|
330
467
|
function handleCrossTraceRowClick(componentKey: string) {
|
|
331
|
-
const row =
|
|
468
|
+
const row = crossTraceRowByKey.value.get(componentKey)
|
|
332
469
|
|
|
333
470
|
if (!row || row.selectedUid === undefined) {
|
|
334
471
|
highlightedUid.value = undefined
|
|
@@ -407,7 +544,7 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
407
544
|
<div class="trace-viewer__list-header">
|
|
408
545
|
<span class="trace-viewer__list-title">Traces</span>
|
|
409
546
|
</div>
|
|
410
|
-
<div class="trace-viewer__table-wrap tracker-table-wrap">
|
|
547
|
+
<div ref="traceListScrollRef" class="trace-viewer__table-wrap tracker-table-wrap">
|
|
411
548
|
<table class="data-table">
|
|
412
549
|
<thead>
|
|
413
550
|
<tr>
|
|
@@ -418,7 +555,14 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
418
555
|
</thead>
|
|
419
556
|
<tbody>
|
|
420
557
|
<tr
|
|
421
|
-
v-
|
|
558
|
+
v-if="virtualizedTraceRowsEnabled && traceListTopPadding > 0"
|
|
559
|
+
class="trace-viewer__virtual-spacer-row"
|
|
560
|
+
aria-hidden="true"
|
|
561
|
+
>
|
|
562
|
+
<td colspan="3" :style="{ height: `${traceListTopPadding}px` }"></td>
|
|
563
|
+
</tr>
|
|
564
|
+
<tr
|
|
565
|
+
v-for="trace in visibleTraceRows"
|
|
422
566
|
:key="trace.id"
|
|
423
567
|
:class="{ 'trace-viewer__trace-row--selected': selectedTrace?.id === trace.id }"
|
|
424
568
|
class="trace-viewer__trace-row"
|
|
@@ -428,6 +572,13 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
428
572
|
<td class="mono">{{ formatDuration(trace.durationMs, trace) }}</td>
|
|
429
573
|
<td class="mono">{{ trace.spans.length }}</td>
|
|
430
574
|
</tr>
|
|
575
|
+
<tr
|
|
576
|
+
v-if="virtualizedTraceRowsEnabled && traceListBottomPadding > 0"
|
|
577
|
+
class="trace-viewer__virtual-spacer-row"
|
|
578
|
+
aria-hidden="true"
|
|
579
|
+
>
|
|
580
|
+
<td colspan="3" :style="{ height: `${traceListBottomPadding}px` }"></td>
|
|
581
|
+
</tr>
|
|
431
582
|
<tr v-if="!filteredTraces.length">
|
|
432
583
|
<td colspan="3" class="tracker-empty-cell">
|
|
433
584
|
{{
|
|
@@ -499,21 +650,20 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
499
650
|
|
|
500
651
|
<div class="trace-viewer__span-list">
|
|
501
652
|
<div
|
|
502
|
-
v-for="
|
|
503
|
-
:key="span.id"
|
|
653
|
+
v-for="spanRow in selectedTraceSpanRows"
|
|
654
|
+
:key="spanRow.span.id"
|
|
504
655
|
:class="{
|
|
505
|
-
'trace-viewer__span-item--selected': selectedSpan?.id === span.id,
|
|
656
|
+
'trace-viewer__span-item--selected': selectedSpan?.id === spanRow.span.id,
|
|
506
657
|
'trace-viewer__span-item--highlighted':
|
|
507
|
-
highlightedUid !== undefined &&
|
|
508
|
-
(span.metadata as Record<string, unknown> | undefined)?.uid === highlightedUid,
|
|
658
|
+
highlightedUid !== undefined && spanRow.uid === highlightedUid,
|
|
509
659
|
}"
|
|
510
660
|
class="trace-viewer__span-item"
|
|
511
|
-
@click="selectedSpan = span"
|
|
661
|
+
@click="selectedSpan = spanRow.span"
|
|
512
662
|
>
|
|
513
|
-
<div class="trace-viewer__span-name">{{
|
|
663
|
+
<div class="trace-viewer__span-name">{{ spanRow.displayName }}</div>
|
|
514
664
|
<div class="trace-viewer__span-meta">
|
|
515
|
-
<span class="trace-viewer__span-type">{{ span.type }}</span>
|
|
516
|
-
<span class="trace-viewer__span-duration">{{ formatDuration(span.durationMs) }}</span>
|
|
665
|
+
<span class="trace-viewer__span-type">{{ spanRow.span.type }}</span>
|
|
666
|
+
<span class="trace-viewer__span-duration">{{ formatDuration(spanRow.span.durationMs) }}</span>
|
|
517
667
|
</div>
|
|
518
668
|
</div>
|
|
519
669
|
</div>
|
|
@@ -585,7 +735,7 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
585
735
|
✕ clear
|
|
586
736
|
</span>
|
|
587
737
|
</div>
|
|
588
|
-
<div v-if="crossTraceSummaryOpen" class="trace-viewer__render-summary-table-wrap">
|
|
738
|
+
<div v-if="crossTraceSummaryOpen" ref="crossTraceScrollRef" class="trace-viewer__render-summary-table-wrap">
|
|
589
739
|
<div class="trace-viewer__comparison-toolbar">
|
|
590
740
|
<button
|
|
591
741
|
:class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyRegressions }"
|
|
@@ -647,7 +797,14 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
647
797
|
</thead>
|
|
648
798
|
<tbody>
|
|
649
799
|
<tr
|
|
650
|
-
v-
|
|
800
|
+
v-if="virtualizedTraceRowsEnabled && crossTraceTopPadding > 0"
|
|
801
|
+
class="trace-viewer__virtual-spacer-row"
|
|
802
|
+
aria-hidden="true"
|
|
803
|
+
>
|
|
804
|
+
<td colspan="7" :style="{ height: `${crossTraceTopPadding}px` }"></td>
|
|
805
|
+
</tr>
|
|
806
|
+
<tr
|
|
807
|
+
v-for="row in visibleCrossTraceRows"
|
|
651
808
|
:key="row.componentKey"
|
|
652
809
|
:class="{
|
|
653
810
|
'trace-viewer__render-summary-row--active':
|
|
@@ -669,6 +826,13 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
669
826
|
<td class="mono trace-viewer__col-desktop">{{ formatDuration(row.avgMsPerRender) }}</td>
|
|
670
827
|
<td class="mono">{{ formatDuration(row.totalMs) }}</td>
|
|
671
828
|
</tr>
|
|
829
|
+
<tr
|
|
830
|
+
v-if="virtualizedTraceRowsEnabled && crossTraceBottomPadding > 0"
|
|
831
|
+
class="trace-viewer__virtual-spacer-row"
|
|
832
|
+
aria-hidden="true"
|
|
833
|
+
>
|
|
834
|
+
<td colspan="7" :style="{ height: `${crossTraceBottomPadding}px` }"></td>
|
|
835
|
+
</tr>
|
|
672
836
|
<tr v-if="!crossTraceRows.length">
|
|
673
837
|
<td colspan="7" class="tracker-empty-cell">No components match the comparison filters.</td>
|
|
674
838
|
</tr>
|
|
@@ -764,16 +928,16 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
764
928
|
font-family: var(--mono);
|
|
765
929
|
}
|
|
766
930
|
|
|
767
|
-
.trace-viewer__action-btn:hover:not(:disabled) {
|
|
768
|
-
background: var(--bg-secondary);
|
|
769
|
-
color: var(--text);
|
|
770
|
-
}
|
|
771
|
-
|
|
772
931
|
.trace-viewer__action-btn:disabled {
|
|
773
932
|
opacity: 0.4;
|
|
774
933
|
cursor: not-allowed;
|
|
775
934
|
}
|
|
776
935
|
|
|
936
|
+
.trace-viewer__action-btn:hover:not(:disabled) {
|
|
937
|
+
background: var(--bg-secondary);
|
|
938
|
+
color: var(--text);
|
|
939
|
+
}
|
|
940
|
+
|
|
777
941
|
.trace-viewer__import-mode-btn {
|
|
778
942
|
padding: 3px 8px;
|
|
779
943
|
background: var(--accent-bg);
|
|
@@ -838,6 +1002,12 @@ function handleCrossTraceRowClick(componentKey: string) {
|
|
|
838
1002
|
border-left: 2px solid var(--accent);
|
|
839
1003
|
}
|
|
840
1004
|
|
|
1005
|
+
.trace-viewer__virtual-spacer-row td {
|
|
1006
|
+
padding: 0;
|
|
1007
|
+
border-bottom: 0;
|
|
1008
|
+
background: transparent;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
841
1011
|
.trace-viewer__detail {
|
|
842
1012
|
flex: 1;
|
|
843
1013
|
display: flex;
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed } 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 } from '@observatory-client/stores/observatory'
|
|
5
8
|
import type { TransitionEntry } from '@observatory/types/snapshot'
|
|
@@ -10,10 +13,25 @@ const { paneWidth: detailWidth, onHandleMouseDown } = useResizablePane(260, 'obs
|
|
|
10
13
|
type FilterMode = 'all' | 'cancelled' | 'active' | 'completed'
|
|
11
14
|
const filter = ref<FilterMode>('all')
|
|
12
15
|
const search = ref('')
|
|
13
|
-
const
|
|
16
|
+
const selectedId = ref<string | null>(null)
|
|
17
|
+
const tableScrollRef = ref<HTMLElement | null>(null)
|
|
18
|
+
|
|
19
|
+
const { effective: virtualizationFlags } = useVirtualizationFlags()
|
|
20
|
+
const { preset: virtualizationPreset } = useVirtualizationConfig({ rowHeight: 42, overscan: 6 })
|
|
21
|
+
|
|
22
|
+
const entriesSorted = computed(() => [...entries.value].sort((a, b) => a.startTime - b.startTime))
|
|
23
|
+
const entriesById = computed(() => new Map(entries.value.map((entry) => [entry.id, entry] as const)))
|
|
24
|
+
|
|
25
|
+
const selected = computed(() => {
|
|
26
|
+
if (!selectedId.value) {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return entriesById.value.get(selectedId.value) ?? null
|
|
31
|
+
})
|
|
14
32
|
|
|
15
33
|
const filtered = computed(() => {
|
|
16
|
-
let list =
|
|
34
|
+
let list = entriesSorted.value
|
|
17
35
|
|
|
18
36
|
if (search.value) {
|
|
19
37
|
const q = search.value.toLowerCase()
|
|
@@ -28,7 +46,7 @@ const filtered = computed(() => {
|
|
|
28
46
|
list = list.filter((e) => e.phase === 'entered' || e.phase === 'left')
|
|
29
47
|
}
|
|
30
48
|
|
|
31
|
-
return list
|
|
49
|
+
return list
|
|
32
50
|
})
|
|
33
51
|
|
|
34
52
|
const stats = computed(() => ({
|
|
@@ -64,6 +82,60 @@ const timelineGeometry = computed(() => {
|
|
|
64
82
|
}))
|
|
65
83
|
})
|
|
66
84
|
|
|
85
|
+
const virtualizedRowsEnabled = computed(() => virtualizationFlags.value.transitions)
|
|
86
|
+
|
|
87
|
+
const tableVirtualizerOptions = computed(() => ({
|
|
88
|
+
count: filtered.value.length,
|
|
89
|
+
getScrollElement: () => tableScrollRef.value,
|
|
90
|
+
estimateSize: () => virtualizationPreset.value.rowHeight,
|
|
91
|
+
overscan: virtualizationPreset.value.overscan,
|
|
92
|
+
}))
|
|
93
|
+
|
|
94
|
+
const tableVirtualizer = useVirtualizer(tableVirtualizerOptions)
|
|
95
|
+
|
|
96
|
+
const tableVirtualItems = computed(() => {
|
|
97
|
+
if (!virtualizedRowsEnabled.value) {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return tableVirtualizer.value.getVirtualItems()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const topTablePadding = computed(() => {
|
|
105
|
+
if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
|
|
106
|
+
return 0
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return tableVirtualItems.value[0].start
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const bottomTablePadding = computed(() => {
|
|
113
|
+
if (!virtualizedRowsEnabled.value || tableVirtualItems.value.length === 0) {
|
|
114
|
+
return 0
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const total = tableVirtualizer.value.getTotalSize()
|
|
118
|
+
const last = tableVirtualItems.value[tableVirtualItems.value.length - 1]
|
|
119
|
+
|
|
120
|
+
return Math.max(0, total - last.end)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
const visibleRows = computed(() => {
|
|
124
|
+
if (!virtualizedRowsEnabled.value) {
|
|
125
|
+
return filtered.value.map((entry, index) => ({
|
|
126
|
+
entry,
|
|
127
|
+
geometry: timelineGeometry.value[index],
|
|
128
|
+
}))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return tableVirtualItems.value
|
|
132
|
+
.map((item) => ({
|
|
133
|
+
entry: filtered.value[item.index],
|
|
134
|
+
geometry: timelineGeometry.value[item.index],
|
|
135
|
+
}))
|
|
136
|
+
.filter((row): row is { entry: TransitionEntry; geometry: { left: number; width: number } } => Boolean(row.entry && row.geometry))
|
|
137
|
+
})
|
|
138
|
+
|
|
67
139
|
function phaseColor(phase: TransitionEntry['phase']): string {
|
|
68
140
|
if (phase === 'entering' || phase === 'leaving') {
|
|
69
141
|
return '#7f77dd'
|
|
@@ -166,7 +238,7 @@ function directionColor(e: TransitionEntry): string {
|
|
|
166
238
|
<!-- Main content -->
|
|
167
239
|
<div class="transition-timeline__content tracker-split">
|
|
168
240
|
<!-- Timeline table -->
|
|
169
|
-
<div class="transition-timeline__table tracker-table-wrap">
|
|
241
|
+
<div ref="tableScrollRef" class="transition-timeline__table tracker-table-wrap">
|
|
170
242
|
<table class="data-table">
|
|
171
243
|
<thead>
|
|
172
244
|
<tr>
|
|
@@ -180,41 +252,56 @@ function directionColor(e: TransitionEntry): string {
|
|
|
180
252
|
</thead>
|
|
181
253
|
<tbody>
|
|
182
254
|
<tr
|
|
183
|
-
v-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
255
|
+
v-if="virtualizedRowsEnabled && topTablePadding > 0"
|
|
256
|
+
class="transition-timeline__virtual-spacer-row"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<td colspan="6" :style="{ height: `${topTablePadding}px` }"></td>
|
|
260
|
+
</tr>
|
|
261
|
+
<tr
|
|
262
|
+
v-for="row in visibleRows"
|
|
263
|
+
:key="row.entry.id"
|
|
264
|
+
:class="{ selected: selected?.id === row.entry.id }"
|
|
265
|
+
@click="selectedId = selected?.id === row.entry.id ? null : row.entry.id"
|
|
187
266
|
>
|
|
188
267
|
<td>
|
|
189
|
-
<span class="transition-timeline__name mono">{{ entry.transitionName }}</span>
|
|
268
|
+
<span class="transition-timeline__name mono">{{ row.entry.transitionName }}</span>
|
|
190
269
|
</td>
|
|
191
270
|
<td>
|
|
192
|
-
<span class="transition-timeline__direction mono" :style="{ color: directionColor(entry) }">
|
|
193
|
-
{{ directionLabel(entry) }}
|
|
271
|
+
<span class="transition-timeline__direction mono" :style="{ color: directionColor(row.entry) }">
|
|
272
|
+
{{ directionLabel(row.entry) }}
|
|
194
273
|
</span>
|
|
195
274
|
</td>
|
|
196
275
|
<td>
|
|
197
|
-
<span class="badge" :class="phaseBadgeClass(entry.phase)">{{ entry.phase }}</span>
|
|
276
|
+
<span class="badge" :class="phaseBadgeClass(row.entry.phase)">{{ row.entry.phase }}</span>
|
|
198
277
|
</td>
|
|
199
278
|
<td class="transition-timeline__duration mono">
|
|
200
|
-
{{ entry.durationMs !== undefined ? entry.durationMs + 'ms' : '—' }}
|
|
279
|
+
{{ row.entry.durationMs !== undefined ? row.entry.durationMs + 'ms' : '—' }}
|
|
201
280
|
</td>
|
|
202
|
-
<td class="transition-timeline__component muted">{{ entry.parentComponent }}</td>
|
|
281
|
+
<td class="transition-timeline__component muted">{{ row.entry.parentComponent }}</td>
|
|
203
282
|
<td class="transition-timeline__bar-cell">
|
|
204
283
|
<div class="transition-timeline__bar-track">
|
|
205
284
|
<div
|
|
206
285
|
class="transition-timeline__bar-fill"
|
|
207
286
|
:style="{
|
|
208
|
-
left:
|
|
209
|
-
width: Math.max(
|
|
210
|
-
background: phaseColor(entry.phase),
|
|
211
|
-
opacity: entry.phase === 'entering' || entry.phase === 'leaving' ? '0.55' : '1',
|
|
287
|
+
left: row.geometry.left + '%',
|
|
288
|
+
width: Math.max(row.geometry.width, 1) + '%',
|
|
289
|
+
background: phaseColor(row.entry.phase),
|
|
290
|
+
opacity: row.entry.phase === 'entering' || row.entry.phase === 'leaving' ? '0.55' : '1',
|
|
212
291
|
}"
|
|
213
292
|
/>
|
|
214
293
|
</div>
|
|
215
294
|
</td>
|
|
216
295
|
</tr>
|
|
217
296
|
|
|
297
|
+
<tr
|
|
298
|
+
v-if="virtualizedRowsEnabled && bottomTablePadding > 0"
|
|
299
|
+
class="transition-timeline__virtual-spacer-row"
|
|
300
|
+
aria-hidden="true"
|
|
301
|
+
>
|
|
302
|
+
<td colspan="6" :style="{ height: `${bottomTablePadding}px` }"></td>
|
|
303
|
+
</tr>
|
|
304
|
+
|
|
218
305
|
<tr v-if="!filtered.length">
|
|
219
306
|
<td colspan="6" class="tracker-empty-cell">
|
|
220
307
|
{{
|
|
@@ -236,7 +323,7 @@ function directionColor(e: TransitionEntry): string {
|
|
|
236
323
|
<aside v-if="selected" class="transition-timeline__detail" :style="{ width: detailWidth + 'px' }">
|
|
237
324
|
<div class="transition-timeline__detail-header">
|
|
238
325
|
<span class="transition-timeline__detail-title">{{ selected.transitionName }}</span>
|
|
239
|
-
<button class="transition-timeline__close-btn" @click="
|
|
326
|
+
<button class="transition-timeline__close-btn" @click="selectedId = null">✕</button>
|
|
240
327
|
</div>
|
|
241
328
|
|
|
242
329
|
<div class="transition-timeline__detail-section">
|
|
@@ -392,6 +479,12 @@ function directionColor(e: TransitionEntry): string {
|
|
|
392
479
|
border-radius: 0;
|
|
393
480
|
}
|
|
394
481
|
|
|
482
|
+
.transition-timeline__virtual-spacer-row td {
|
|
483
|
+
padding: 0;
|
|
484
|
+
border-bottom: 0;
|
|
485
|
+
background: transparent;
|
|
486
|
+
}
|
|
487
|
+
|
|
395
488
|
/* ── Timeline bar ────────────────────────────────────────────────────────── */
|
|
396
489
|
.transition-timeline__bar-cell {
|
|
397
490
|
width: 200px;
|
package/dist/module.d.mts
CHANGED
|
@@ -19,6 +19,11 @@ interface ModuleOptions {
|
|
|
19
19
|
* @default 10000
|
|
20
20
|
*/
|
|
21
21
|
maxPayloadBytes?: number;
|
|
22
|
+
/**
|
|
23
|
+
* Number of fetch rows to load per infinite-scroll step in the Fetch Dashboard.
|
|
24
|
+
* @default 20
|
|
25
|
+
*/
|
|
26
|
+
fetchPageSize?: number;
|
|
22
27
|
/**
|
|
23
28
|
* Maximum number of transition entries to keep in memory
|
|
24
29
|
* @default 500
|
|
@@ -34,6 +39,11 @@ interface ModuleOptions {
|
|
|
34
39
|
* @default 300
|
|
35
40
|
*/
|
|
36
41
|
maxComposableEntries?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Maximum number of Pinia timeline events to keep per store
|
|
44
|
+
* @default 100
|
|
45
|
+
*/
|
|
46
|
+
maxPiniaTimeline?: number;
|
|
37
47
|
/**
|
|
38
48
|
* Maximum number of render timeline events per entry
|
|
39
49
|
* @default 100
|
|
@@ -61,6 +71,11 @@ interface ModuleOptions {
|
|
|
61
71
|
* @default true
|
|
62
72
|
*/
|
|
63
73
|
composableTracker?: boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Enable the Pinia state tracker tab
|
|
76
|
+
* @default true
|
|
77
|
+
*/
|
|
78
|
+
piniaTracker?: boolean;
|
|
64
79
|
/**
|
|
65
80
|
* Enable the render heatmap tab
|
|
66
81
|
* @default true
|