nuxt-devtools-observatory 0.1.31 → 0.1.32
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 +74 -46
- package/client/dist/assets/index-5Wl1XYRH.js +17 -0
- package/client/dist/assets/index-DT_QUiIh.css +1 -0
- package/client/dist/index.html +2 -2
- package/client/src/components/Flamegraph.vue +3 -4
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- package/client/src/composables/composable-search.ts +124 -0
- package/client/src/composables/trace-render-aggregation.ts +254 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +1 -5
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +65 -30
- package/client/src/views/RenderHeatmap.vue +63 -1
- package/client/src/views/TraceViewer.vue +618 -5
- package/dist/module.json +1 -1
- package/dist/module.mjs +1 -1
- package/dist/runtime/composables/composable-registry.d.ts +19 -0
- package/dist/runtime/composables/composable-registry.js +63 -5
- package/dist/runtime/composables/render-registry.js +19 -11
- package/dist/runtime/nitro/fetch-capture.d.ts +1 -2
- package/dist/runtime/nitro/fetch-capture.js +85 -7
- package/dist/runtime/nitro/ssr-trace-store.d.ts +85 -0
- package/dist/runtime/nitro/ssr-trace-store.js +84 -0
- package/dist/runtime/plugin.js +44 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +5 -1
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.css +0 -1
|
@@ -6,14 +6,33 @@ import WaterfallView from '@observatory-client/components/WaterfallView.vue'
|
|
|
6
6
|
import SpanInspector from '@observatory-client/components/SpanInspector.vue'
|
|
7
7
|
import TraceFilter from '@observatory-client/components/TraceFilter.vue'
|
|
8
8
|
import { useTraceFilter } from '@observatory-client/composables/useTraceFilter'
|
|
9
|
+
import { exportJson, importJson } from '@observatory-client/composables/useExportImport'
|
|
10
|
+
import {
|
|
11
|
+
buildRenderSummaryForTrace,
|
|
12
|
+
buildCrossTraceRenderSummary,
|
|
13
|
+
type TraceRenderStatsRow,
|
|
14
|
+
} from '@observatory-client/composables/trace-render-aggregation'
|
|
15
|
+
import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
|
|
9
16
|
import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
|
|
10
17
|
|
|
11
18
|
const { traces, connected } = useObservatoryData()
|
|
12
19
|
|
|
20
|
+
const importedTraces = ref<TraceEntry[]>([])
|
|
21
|
+
const isImportMode = computed(() => importedTraces.value.length > 0)
|
|
22
|
+
|
|
13
23
|
const selectedTraceId = ref<string | null>(null)
|
|
14
24
|
const selectedSpan = ref<TraceSpan | undefined>(undefined)
|
|
15
25
|
const viewMode = ref<'overview' | 'flamegraph' | 'waterfall'>('overview')
|
|
16
26
|
const showFilters = ref(false)
|
|
27
|
+
const renderSummaryOpen = ref(true)
|
|
28
|
+
const crossTraceSummaryOpen = ref(true)
|
|
29
|
+
const crossTraceSortBy = ref<'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName'>('avgRerendersPerTrace')
|
|
30
|
+
const crossTraceSortDir = ref<'asc' | 'desc'>('desc')
|
|
31
|
+
const crossTraceOnlyRegressions = ref(false)
|
|
32
|
+
const crossTraceOnlyComparable = ref(false)
|
|
33
|
+
const crossTraceSearch = ref('')
|
|
34
|
+
const highlightedUid = ref<string | number | undefined>(undefined)
|
|
35
|
+
const highlightedComponentKey = ref<string | undefined>(undefined)
|
|
17
36
|
|
|
18
37
|
const {
|
|
19
38
|
searchQuery,
|
|
@@ -27,7 +46,8 @@ const {
|
|
|
27
46
|
} = useTraceFilter()
|
|
28
47
|
|
|
29
48
|
const sortedTraces = computed(() => {
|
|
30
|
-
|
|
49
|
+
const source = isImportMode.value ? importedTraces.value : traces.value
|
|
50
|
+
return [...source].sort((a, b) => b.startTime - a.startTime)
|
|
31
51
|
})
|
|
32
52
|
|
|
33
53
|
const filteredTraces = computed(() => {
|
|
@@ -35,11 +55,13 @@ const filteredTraces = computed(() => {
|
|
|
35
55
|
})
|
|
36
56
|
|
|
37
57
|
const traceCountLabel = computed(() => {
|
|
58
|
+
const suffix = isImportMode.value ? ' (imported)' : ''
|
|
59
|
+
|
|
38
60
|
if (!hasActiveFilters.value) {
|
|
39
|
-
return `${sortedTraces.value.length} traces`
|
|
61
|
+
return `${sortedTraces.value.length} traces${suffix}`
|
|
40
62
|
}
|
|
41
63
|
|
|
42
|
-
return `Showing ${filteredTraces.value.length} of ${sortedTraces.value.length} traces`
|
|
64
|
+
return `Showing ${filteredTraces.value.length} of ${sortedTraces.value.length} traces${suffix}`
|
|
43
65
|
})
|
|
44
66
|
|
|
45
67
|
const selectedTrace = computed(() => {
|
|
@@ -50,6 +72,59 @@ const selectedTrace = computed(() => {
|
|
|
50
72
|
return filteredTraces.value.find((t) => t.id === selectedTraceId.value)
|
|
51
73
|
})
|
|
52
74
|
|
|
75
|
+
const renderSummary = computed<TraceRenderStatsRow[]>(() => buildRenderSummaryForTrace(selectedTrace.value))
|
|
76
|
+
|
|
77
|
+
const crossTraceRenderSummary = computed(() => {
|
|
78
|
+
return buildCrossTraceRenderSummary(filteredTraces.value, selectedTrace.value?.id)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const crossTraceRows = computed(() => {
|
|
82
|
+
const q = crossTraceSearch.value.trim().toLowerCase()
|
|
83
|
+
|
|
84
|
+
let rows = crossTraceRenderSummary.value.filter((row) => {
|
|
85
|
+
if (crossTraceOnlyRegressions.value && (row.deltaVsBaseline ?? 0) <= 0) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (crossTraceOnlyComparable.value && row.selectedRerenders === undefined) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (q) {
|
|
94
|
+
const nameMatch = row.componentName.toLowerCase().includes(q)
|
|
95
|
+
const fileMatch = row.file.toLowerCase().includes(q)
|
|
96
|
+
|
|
97
|
+
if (!nameMatch && !fileMatch) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
rows = [...rows].sort((a, b) => {
|
|
106
|
+
const key = crossTraceSortBy.value
|
|
107
|
+
const dir = crossTraceSortDir.value === 'asc' ? 1 : -1
|
|
108
|
+
|
|
109
|
+
if (key === 'componentName') {
|
|
110
|
+
return a.componentName.localeCompare(b.componentName) * dir
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (key === 'deltaVsBaseline') {
|
|
114
|
+
const av = a.deltaVsBaseline ?? Number.NEGATIVE_INFINITY
|
|
115
|
+
const bv = b.deltaVsBaseline ?? Number.NEGATIVE_INFINITY
|
|
116
|
+
|
|
117
|
+
return (av - bv) * dir
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return ((a[key] as number) - (b[key] as number)) * dir
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return rows
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const crossTraceRegressionsCount = computed(() => crossTraceRenderSummary.value.filter((row) => (row.deltaVsBaseline ?? 0) > 0).length)
|
|
127
|
+
|
|
53
128
|
function elapsedFromSpans(trace: TraceEntry): number | undefined {
|
|
54
129
|
if (!trace.spans.length) {
|
|
55
130
|
return undefined
|
|
@@ -129,16 +204,152 @@ function getSpanDisplayName(span: TraceSpan): string {
|
|
|
129
204
|
function selectTrace(trace: TraceEntry) {
|
|
130
205
|
selectedTraceId.value = trace.id
|
|
131
206
|
selectedSpan.value = undefined
|
|
207
|
+
highlightedUid.value = undefined
|
|
208
|
+
highlightedComponentKey.value = undefined
|
|
132
209
|
}
|
|
133
210
|
|
|
134
211
|
function handleClearFilters() {
|
|
135
212
|
clearFilters()
|
|
136
213
|
selectedSpan.value = undefined
|
|
214
|
+
highlightedUid.value = undefined
|
|
215
|
+
highlightedComponentKey.value = undefined
|
|
137
216
|
}
|
|
138
217
|
|
|
139
218
|
function handleSpanTypesUpdate(value: Set<string>) {
|
|
140
219
|
selectedSpanTypes.value = value
|
|
141
220
|
}
|
|
221
|
+
|
|
222
|
+
function handleExport() {
|
|
223
|
+
exportJson(`observatory-traces-${Date.now()}.json`, {
|
|
224
|
+
type: 'observatory-traces',
|
|
225
|
+
version: '1',
|
|
226
|
+
exportedAt: Date.now(),
|
|
227
|
+
count: traces.value.length,
|
|
228
|
+
data: traces.value,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function handleImport() {
|
|
233
|
+
let parsed: unknown
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
parsed = await importJson()
|
|
237
|
+
} catch (err) {
|
|
238
|
+
if (err instanceof Error && err.message !== 'cancelled') {
|
|
239
|
+
alert(`Import failed: ${err.message}`)
|
|
240
|
+
}
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const file = parsed as ObservatoryExportFile<TraceEntry>
|
|
245
|
+
|
|
246
|
+
if (
|
|
247
|
+
file?.type !== 'observatory-traces' ||
|
|
248
|
+
file?.version !== '1' ||
|
|
249
|
+
!Array.isArray(file?.data) ||
|
|
250
|
+
(file.data.length > 0 && (!file.data[0]?.id || !file.data[0]?.name || !Array.isArray(file.data[0]?.spans)))
|
|
251
|
+
) {
|
|
252
|
+
alert('Invalid observatory traces file.')
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
importedTraces.value = file.data
|
|
257
|
+
selectedTraceId.value = null
|
|
258
|
+
selectedSpan.value = undefined
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function handleBackToLive() {
|
|
262
|
+
importedTraces.value = []
|
|
263
|
+
selectedTraceId.value = null
|
|
264
|
+
selectedSpan.value = undefined
|
|
265
|
+
highlightedUid.value = undefined
|
|
266
|
+
highlightedComponentKey.value = undefined
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function handleRenderSummaryRowClick(uid: string | number) {
|
|
270
|
+
// Toggle highlight: clicking the already-highlighted row deselects it.
|
|
271
|
+
highlightedUid.value = highlightedUid.value === uid ? undefined : uid
|
|
272
|
+
highlightedComponentKey.value = undefined
|
|
273
|
+
selectedSpan.value = undefined
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function formatDelta(value?: number): string {
|
|
277
|
+
if (value === undefined) {
|
|
278
|
+
return 'n/a'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const rounded = Math.round(value * 10) / 10
|
|
282
|
+
|
|
283
|
+
if (rounded > 0) {
|
|
284
|
+
return `+${rounded}`
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return `${rounded}`
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function deltaToneClass(value?: number): string {
|
|
291
|
+
if (value === undefined) {
|
|
292
|
+
return 'trace-viewer__delta--na'
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (value > 0) {
|
|
296
|
+
return 'trace-viewer__delta--regression'
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (value < 0) {
|
|
300
|
+
return 'trace-viewer__delta--improvement'
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return 'trace-viewer__delta--neutral'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function cycleCrossTraceSort(next: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName') {
|
|
307
|
+
if (crossTraceSortBy.value === next) {
|
|
308
|
+
crossTraceSortDir.value = crossTraceSortDir.value === 'desc' ? 'asc' : 'desc'
|
|
309
|
+
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
crossTraceSortBy.value = next
|
|
314
|
+
crossTraceSortDir.value = next === 'componentName' ? 'asc' : 'desc'
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function sortIndicator(key: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName') {
|
|
318
|
+
if (crossTraceSortBy.value !== key) {
|
|
319
|
+
return ''
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return crossTraceSortDir.value === 'desc' ? ' ↓' : ' ↑'
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function clearCrossTraceHighlight() {
|
|
326
|
+
highlightedComponentKey.value = undefined
|
|
327
|
+
highlightedUid.value = undefined
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function handleCrossTraceRowClick(componentKey: string) {
|
|
331
|
+
const row = crossTraceRows.value.find((item) => item.componentKey === componentKey)
|
|
332
|
+
|
|
333
|
+
if (!row || row.selectedUid === undefined) {
|
|
334
|
+
highlightedUid.value = undefined
|
|
335
|
+
highlightedComponentKey.value = undefined
|
|
336
|
+
selectedSpan.value = undefined
|
|
337
|
+
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (highlightedComponentKey.value === componentKey) {
|
|
342
|
+
highlightedComponentKey.value = undefined
|
|
343
|
+
highlightedUid.value = undefined
|
|
344
|
+
selectedSpan.value = undefined
|
|
345
|
+
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
highlightedComponentKey.value = componentKey
|
|
350
|
+
highlightedUid.value = row.selectedUid
|
|
351
|
+
selectedSpan.value = undefined
|
|
352
|
+
}
|
|
142
353
|
</script>
|
|
143
354
|
|
|
144
355
|
<template>
|
|
@@ -148,6 +359,18 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
148
359
|
<div class="trace-viewer__title">Trace Viewer</div>
|
|
149
360
|
<div class="trace-viewer__header-actions">
|
|
150
361
|
<div class="trace-viewer__count muted text-sm">{{ traceCountLabel }}</div>
|
|
362
|
+
<button v-if="isImportMode" class="trace-viewer__import-mode-btn" title="Return to live data" @click="handleBackToLive">
|
|
363
|
+
← live
|
|
364
|
+
</button>
|
|
365
|
+
<button
|
|
366
|
+
class="trace-viewer__action-btn"
|
|
367
|
+
title="Export traces as JSON"
|
|
368
|
+
:disabled="traces.length === 0 && !isImportMode"
|
|
369
|
+
@click="handleExport"
|
|
370
|
+
>
|
|
371
|
+
↓ export
|
|
372
|
+
</button>
|
|
373
|
+
<button class="trace-viewer__action-btn" title="Import traces from JSON file" @click="handleImport">↑ import</button>
|
|
151
374
|
<button
|
|
152
375
|
:class="{ 'trace-viewer__filter-btn--active': showFilters }"
|
|
153
376
|
class="trace-viewer__filter-btn"
|
|
@@ -278,7 +501,12 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
278
501
|
<div
|
|
279
502
|
v-for="span in selectedTrace.spans"
|
|
280
503
|
:key="span.id"
|
|
281
|
-
:class="{
|
|
504
|
+
:class="{
|
|
505
|
+
'trace-viewer__span-item--selected': selectedSpan?.id === span.id,
|
|
506
|
+
'trace-viewer__span-item--highlighted':
|
|
507
|
+
highlightedUid !== undefined &&
|
|
508
|
+
(span.metadata as Record<string, unknown> | undefined)?.uid === highlightedUid,
|
|
509
|
+
}"
|
|
282
510
|
class="trace-viewer__span-item"
|
|
283
511
|
@click="selectedSpan = span"
|
|
284
512
|
>
|
|
@@ -289,6 +517,165 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
289
517
|
</div>
|
|
290
518
|
</div>
|
|
291
519
|
</div>
|
|
520
|
+
|
|
521
|
+
<!-- Render Summary panel — shown when the trace has render spans -->
|
|
522
|
+
<template v-if="renderSummary.length > 0">
|
|
523
|
+
<div class="trace-viewer__render-summary-header" @click="renderSummaryOpen = !renderSummaryOpen">
|
|
524
|
+
<span class="trace-viewer__render-summary-toggle">{{ renderSummaryOpen ? '▾' : '▸' }}</span>
|
|
525
|
+
<span class="trace-viewer__render-summary-title">Render Summary</span>
|
|
526
|
+
<span class="trace-viewer__render-summary-count muted">{{ renderSummary.length }} components</span>
|
|
527
|
+
<span
|
|
528
|
+
v-if="highlightedUid !== undefined"
|
|
529
|
+
class="trace-viewer__render-summary-clear"
|
|
530
|
+
@click.stop="highlightedUid = undefined"
|
|
531
|
+
>
|
|
532
|
+
✕ clear
|
|
533
|
+
</span>
|
|
534
|
+
</div>
|
|
535
|
+
<div v-if="renderSummaryOpen" class="trace-viewer__render-summary-table-wrap">
|
|
536
|
+
<table class="data-table trace-viewer__render-summary-table">
|
|
537
|
+
<thead>
|
|
538
|
+
<tr>
|
|
539
|
+
<th>Component</th>
|
|
540
|
+
<th title="Times the component was mounted">Mounts</th>
|
|
541
|
+
<th title="Reactive re-renders after initial mount">Re-renders</th>
|
|
542
|
+
<th title="Average render duration">Avg</th>
|
|
543
|
+
<th title="Sum of all render durations">Total</th>
|
|
544
|
+
</tr>
|
|
545
|
+
</thead>
|
|
546
|
+
<tbody>
|
|
547
|
+
<tr
|
|
548
|
+
v-for="row in renderSummary"
|
|
549
|
+
:key="row.uid"
|
|
550
|
+
:class="{ 'trace-viewer__render-summary-row--active': highlightedUid === row.uid }"
|
|
551
|
+
class="trace-viewer__render-summary-row"
|
|
552
|
+
:title="row.file"
|
|
553
|
+
@click="handleRenderSummaryRowClick(row.uid)"
|
|
554
|
+
>
|
|
555
|
+
<td class="mono">{{ row.componentName }}</td>
|
|
556
|
+
<td class="mono">{{ row.mountCount }}</td>
|
|
557
|
+
<td class="mono" :class="{ 'trace-viewer__render-hot': row.rerenderCount > 3 }">
|
|
558
|
+
{{ row.rerenderCount }}
|
|
559
|
+
</td>
|
|
560
|
+
<td class="mono">{{ formatDuration(row.avgMs) }}</td>
|
|
561
|
+
<td class="mono">{{ formatDuration(row.totalMs) }}</td>
|
|
562
|
+
</tr>
|
|
563
|
+
</tbody>
|
|
564
|
+
</table>
|
|
565
|
+
</div>
|
|
566
|
+
</template>
|
|
567
|
+
|
|
568
|
+
<!-- Cross-trace comparison panel -->
|
|
569
|
+
<template v-if="crossTraceRenderSummary.length > 0">
|
|
570
|
+
<div class="trace-viewer__render-summary-header" @click="crossTraceSummaryOpen = !crossTraceSummaryOpen">
|
|
571
|
+
<span class="trace-viewer__render-summary-toggle">{{ crossTraceSummaryOpen ? '▾' : '▸' }}</span>
|
|
572
|
+
<span class="trace-viewer__render-summary-title">Cross-Trace Render Comparison</span>
|
|
573
|
+
<span class="trace-viewer__render-summary-count muted">
|
|
574
|
+
{{ crossTraceRenderSummary.length }} components · {{ filteredTraces.length }} traces
|
|
575
|
+
</span>
|
|
576
|
+
<span class="trace-viewer__render-summary-count muted">
|
|
577
|
+
{{ crossTraceRegressionsCount }} regressions
|
|
578
|
+
</span>
|
|
579
|
+
<span class="trace-viewer__mobile-hint muted">mobile mode: condensed columns</span>
|
|
580
|
+
<span
|
|
581
|
+
v-if="highlightedComponentKey !== undefined"
|
|
582
|
+
class="trace-viewer__render-summary-clear"
|
|
583
|
+
@click.stop="clearCrossTraceHighlight"
|
|
584
|
+
>
|
|
585
|
+
✕ clear
|
|
586
|
+
</span>
|
|
587
|
+
</div>
|
|
588
|
+
<div v-if="crossTraceSummaryOpen" class="trace-viewer__render-summary-table-wrap">
|
|
589
|
+
<div class="trace-viewer__comparison-toolbar">
|
|
590
|
+
<button
|
|
591
|
+
:class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyRegressions }"
|
|
592
|
+
class="trace-viewer__comparison-chip"
|
|
593
|
+
@click="crossTraceOnlyRegressions = !crossTraceOnlyRegressions"
|
|
594
|
+
>
|
|
595
|
+
regressions only
|
|
596
|
+
</button>
|
|
597
|
+
<button
|
|
598
|
+
:class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyComparable }"
|
|
599
|
+
class="trace-viewer__comparison-chip"
|
|
600
|
+
@click="crossTraceOnlyComparable = !crossTraceOnlyComparable"
|
|
601
|
+
>
|
|
602
|
+
selected trace only
|
|
603
|
+
</button>
|
|
604
|
+
<span class="trace-viewer__comparison-spacer"></span>
|
|
605
|
+
<input
|
|
606
|
+
v-model="crossTraceSearch"
|
|
607
|
+
class="trace-viewer__comparison-search mono"
|
|
608
|
+
type="search"
|
|
609
|
+
placeholder="filter component..."
|
|
610
|
+
/>
|
|
611
|
+
</div>
|
|
612
|
+
<table class="data-table trace-viewer__render-summary-table">
|
|
613
|
+
<thead>
|
|
614
|
+
<tr>
|
|
615
|
+
<th class="trace-viewer__sortable" @click="cycleCrossTraceSort('componentName')">
|
|
616
|
+
Component{{ sortIndicator('componentName') }}
|
|
617
|
+
</th>
|
|
618
|
+
<th class="trace-viewer__col-desktop" title="How many traces include this component">
|
|
619
|
+
Traces
|
|
620
|
+
</th>
|
|
621
|
+
<th
|
|
622
|
+
class="trace-viewer__sortable"
|
|
623
|
+
title="Average re-renders per trace"
|
|
624
|
+
@click="cycleCrossTraceSort('avgRerendersPerTrace')"
|
|
625
|
+
>
|
|
626
|
+
Avg Re-renders{{ sortIndicator('avgRerendersPerTrace') }}
|
|
627
|
+
</th>
|
|
628
|
+
<th class="trace-viewer__col-desktop" title="Re-renders in selected trace">Selected</th>
|
|
629
|
+
<th
|
|
630
|
+
class="trace-viewer__sortable"
|
|
631
|
+
title="Selected trace vs other traces baseline"
|
|
632
|
+
@click="cycleCrossTraceSort('deltaVsBaseline')"
|
|
633
|
+
>
|
|
634
|
+
Delta{{ sortIndicator('deltaVsBaseline') }}
|
|
635
|
+
</th>
|
|
636
|
+
<th class="trace-viewer__col-desktop" title="Average render duration across all renders">
|
|
637
|
+
Avg ms
|
|
638
|
+
</th>
|
|
639
|
+
<th
|
|
640
|
+
class="trace-viewer__sortable"
|
|
641
|
+
title="Sum of all render duration across filtered traces"
|
|
642
|
+
@click="cycleCrossTraceSort('totalMs')"
|
|
643
|
+
>
|
|
644
|
+
Total ms{{ sortIndicator('totalMs') }}
|
|
645
|
+
</th>
|
|
646
|
+
</tr>
|
|
647
|
+
</thead>
|
|
648
|
+
<tbody>
|
|
649
|
+
<tr
|
|
650
|
+
v-for="row in crossTraceRows"
|
|
651
|
+
:key="row.componentKey"
|
|
652
|
+
:class="{
|
|
653
|
+
'trace-viewer__render-summary-row--active':
|
|
654
|
+
highlightedComponentKey === row.componentKey,
|
|
655
|
+
}"
|
|
656
|
+
class="trace-viewer__render-summary-row"
|
|
657
|
+
:title="row.file"
|
|
658
|
+
@click="handleCrossTraceRowClick(row.componentKey)"
|
|
659
|
+
>
|
|
660
|
+
<td class="mono">{{ row.componentName }}</td>
|
|
661
|
+
<td class="mono trace-viewer__col-desktop">{{ row.tracesSeen }}</td>
|
|
662
|
+
<td class="mono">{{ (Math.round(row.avgRerendersPerTrace * 10) / 10).toFixed(1) }}</td>
|
|
663
|
+
<td class="mono trace-viewer__col-desktop">{{ row.selectedRerenders ?? 'n/a' }}</td>
|
|
664
|
+
<td class="mono">
|
|
665
|
+
<span class="trace-viewer__delta" :class="deltaToneClass(row.deltaVsBaseline)">
|
|
666
|
+
{{ formatDelta(row.deltaVsBaseline) }}
|
|
667
|
+
</span>
|
|
668
|
+
</td>
|
|
669
|
+
<td class="mono trace-viewer__col-desktop">{{ formatDuration(row.avgMsPerRender) }}</td>
|
|
670
|
+
<td class="mono">{{ formatDuration(row.totalMs) }}</td>
|
|
671
|
+
</tr>
|
|
672
|
+
<tr v-if="!crossTraceRows.length">
|
|
673
|
+
<td colspan="7" class="tracker-empty-cell">No components match the comparison filters.</td>
|
|
674
|
+
</tr>
|
|
675
|
+
</tbody>
|
|
676
|
+
</table>
|
|
677
|
+
</div>
|
|
678
|
+
</template>
|
|
292
679
|
</div>
|
|
293
680
|
|
|
294
681
|
<!-- Flamegraph -->
|
|
@@ -365,6 +752,44 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
365
752
|
color: var(--accent);
|
|
366
753
|
}
|
|
367
754
|
|
|
755
|
+
.trace-viewer__action-btn {
|
|
756
|
+
padding: 3px 8px;
|
|
757
|
+
background: none;
|
|
758
|
+
border: 1px solid var(--border);
|
|
759
|
+
color: var(--text-secondary);
|
|
760
|
+
cursor: pointer;
|
|
761
|
+
font-size: 11px;
|
|
762
|
+
border-radius: 3px;
|
|
763
|
+
transition: all 0.12s;
|
|
764
|
+
font-family: var(--mono);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.trace-viewer__action-btn:hover:not(:disabled) {
|
|
768
|
+
background: var(--bg-secondary);
|
|
769
|
+
color: var(--text);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.trace-viewer__action-btn:disabled {
|
|
773
|
+
opacity: 0.4;
|
|
774
|
+
cursor: not-allowed;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
.trace-viewer__import-mode-btn {
|
|
778
|
+
padding: 3px 8px;
|
|
779
|
+
background: var(--accent-bg);
|
|
780
|
+
border: 1px solid var(--accent);
|
|
781
|
+
color: var(--accent);
|
|
782
|
+
cursor: pointer;
|
|
783
|
+
font-size: 11px;
|
|
784
|
+
border-radius: 3px;
|
|
785
|
+
transition: all 0.12s;
|
|
786
|
+
font-family: var(--mono);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
.trace-viewer__import-mode-btn:hover {
|
|
790
|
+
opacity: 0.8;
|
|
791
|
+
}
|
|
792
|
+
|
|
368
793
|
.trace-viewer__container {
|
|
369
794
|
display: flex;
|
|
370
795
|
flex: 1;
|
|
@@ -572,7 +997,176 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
572
997
|
overflow: auto;
|
|
573
998
|
}
|
|
574
999
|
|
|
575
|
-
|
|
1000
|
+
.trace-viewer__span-item--highlighted {
|
|
1001
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 8%, transparent);
|
|
1002
|
+
border-left: 2px solid var(--purple, #a855f7);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.trace-viewer__render-summary-header {
|
|
1006
|
+
display: flex;
|
|
1007
|
+
align-items: center;
|
|
1008
|
+
gap: 8px;
|
|
1009
|
+
padding: 8px 16px;
|
|
1010
|
+
border-top: 1px solid var(--border);
|
|
1011
|
+
border-bottom: 1px solid var(--border);
|
|
1012
|
+
background: var(--bg3, var(--bg2, var(--bg)));
|
|
1013
|
+
cursor: pointer;
|
|
1014
|
+
user-select: none;
|
|
1015
|
+
flex-shrink: 0;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.trace-viewer__render-summary-header:hover {
|
|
1019
|
+
background: var(--bg2, var(--bg));
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.trace-viewer__render-summary-toggle {
|
|
1023
|
+
font-size: 10px;
|
|
1024
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1025
|
+
width: 12px;
|
|
1026
|
+
flex-shrink: 0;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.trace-viewer__render-summary-title {
|
|
1030
|
+
font-size: 11px;
|
|
1031
|
+
font-weight: 600;
|
|
1032
|
+
color: var(--text2, var(--text));
|
|
1033
|
+
text-transform: uppercase;
|
|
1034
|
+
letter-spacing: 0.5px;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
.trace-viewer__render-summary-count {
|
|
1038
|
+
font-size: 11px;
|
|
1039
|
+
font-family: var(--mono);
|
|
1040
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.trace-viewer__render-summary-clear {
|
|
1044
|
+
margin-left: auto;
|
|
1045
|
+
font-size: 11px;
|
|
1046
|
+
color: var(--accent);
|
|
1047
|
+
cursor: pointer;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
.trace-viewer__render-summary-clear:hover {
|
|
1051
|
+
text-decoration: underline;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
.trace-viewer__mobile-hint {
|
|
1055
|
+
display: none;
|
|
1056
|
+
margin-left: auto;
|
|
1057
|
+
font-size: 11px;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.trace-viewer__render-summary-table-wrap {
|
|
1061
|
+
flex-shrink: 0;
|
|
1062
|
+
max-height: 200px;
|
|
1063
|
+
overflow: auto;
|
|
1064
|
+
border-bottom: 1px solid var(--border);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.trace-viewer__comparison-toolbar {
|
|
1068
|
+
display: flex;
|
|
1069
|
+
align-items: center;
|
|
1070
|
+
gap: 8px;
|
|
1071
|
+
padding: 8px 12px;
|
|
1072
|
+
border-bottom: 1px solid var(--border);
|
|
1073
|
+
background: var(--bg2, var(--bg));
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
.trace-viewer__comparison-chip {
|
|
1077
|
+
padding: 2px 8px;
|
|
1078
|
+
border: 1px solid var(--border);
|
|
1079
|
+
border-radius: 999px;
|
|
1080
|
+
background: transparent;
|
|
1081
|
+
color: var(--text2, var(--text));
|
|
1082
|
+
font-size: 11px;
|
|
1083
|
+
font-family: var(--mono);
|
|
1084
|
+
cursor: pointer;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
.trace-viewer__comparison-chip--active {
|
|
1088
|
+
border-color: color-mix(in srgb, var(--purple, #a855f7) 55%, var(--border));
|
|
1089
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 15%, transparent);
|
|
1090
|
+
color: var(--purple, #a855f7);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.trace-viewer__comparison-spacer {
|
|
1094
|
+
flex: 1;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.trace-viewer__comparison-search {
|
|
1098
|
+
min-width: 180px;
|
|
1099
|
+
max-width: 240px;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.trace-viewer__render-summary-table {
|
|
1103
|
+
width: 100%;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.trace-viewer__sortable {
|
|
1107
|
+
cursor: pointer;
|
|
1108
|
+
user-select: none;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
.trace-viewer__sortable:hover {
|
|
1112
|
+
color: var(--text);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
.trace-viewer__render-summary-row {
|
|
1116
|
+
cursor: pointer;
|
|
1117
|
+
transition: background 0.15s;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
.trace-viewer__render-summary-row:hover {
|
|
1121
|
+
background: var(--bg2, var(--bg));
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
.trace-viewer__render-summary-row--active {
|
|
1125
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 8%, transparent);
|
|
1126
|
+
border-left: 2px solid var(--purple, #a855f7);
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
.trace-viewer__render-hot {
|
|
1130
|
+
color: var(--orange, #f97316);
|
|
1131
|
+
font-weight: 600;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
.trace-viewer__delta {
|
|
1135
|
+
display: inline-flex;
|
|
1136
|
+
align-items: center;
|
|
1137
|
+
justify-content: center;
|
|
1138
|
+
min-width: 46px;
|
|
1139
|
+
padding: 2px 6px;
|
|
1140
|
+
border-radius: 999px;
|
|
1141
|
+
border: 1px solid transparent;
|
|
1142
|
+
font-weight: 600;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.trace-viewer__delta--regression {
|
|
1146
|
+
color: var(--orange, #f97316);
|
|
1147
|
+
border-color: color-mix(in srgb, var(--orange, #f97316) 40%, var(--border));
|
|
1148
|
+
background: color-mix(in srgb, var(--orange, #f97316) 14%, transparent);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
.trace-viewer__delta--improvement {
|
|
1152
|
+
color: var(--green, #22c55e);
|
|
1153
|
+
border-color: color-mix(in srgb, var(--green, #22c55e) 45%, var(--border));
|
|
1154
|
+
background: color-mix(in srgb, var(--green, #22c55e) 14%, transparent);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.trace-viewer__delta--neutral {
|
|
1158
|
+
color: var(--text2, var(--text));
|
|
1159
|
+
border-color: var(--border);
|
|
1160
|
+
background: var(--bg2, var(--bg));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
.trace-viewer__delta--na {
|
|
1164
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1165
|
+
border-color: var(--border);
|
|
1166
|
+
background: transparent;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
@media (width <= 1024px) {
|
|
576
1170
|
.trace-viewer__container {
|
|
577
1171
|
flex-direction: column;
|
|
578
1172
|
gap: 0;
|
|
@@ -595,5 +1189,24 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
595
1189
|
border-left: none;
|
|
596
1190
|
border-top: 1px solid var(--border);
|
|
597
1191
|
}
|
|
1192
|
+
|
|
1193
|
+
.trace-viewer__comparison-toolbar {
|
|
1194
|
+
flex-wrap: wrap;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.trace-viewer__comparison-search {
|
|
1198
|
+
min-width: 100%;
|
|
1199
|
+
max-width: 100%;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
@media (width <= 768px) {
|
|
1204
|
+
.trace-viewer__col-desktop {
|
|
1205
|
+
display: none;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.trace-viewer__mobile-hint {
|
|
1209
|
+
display: inline-flex;
|
|
1210
|
+
}
|
|
598
1211
|
}
|
|
599
1212
|
</style>
|