nuxt-devtools-observatory 0.1.31 → 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 +79 -46
- 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 +7 -8
- package/client/src/components/SpanInspector.vue +1 -1
- package/client/src/components/TraceFilter.vue +0 -2
- package/client/src/components/WaterfallView.vue +1 -1
- package/client/src/composables/composable-search.ts +127 -0
- package/client/src/composables/trace-render-aggregation.ts +263 -0
- package/client/src/composables/useExportImport.ts +63 -0
- package/client/src/composables/useTraceFilter.ts +1 -5
- package/client/src/composables/useVirtualizationConfig.ts +40 -0
- package/client/src/composables/useVirtualizationFlags.ts +129 -0
- package/client/src/stores/observatory.ts +9 -1
- package/client/src/views/ComposableTracker.vue +273 -97
- package/client/src/views/FetchDashboard.vue +181 -16
- package/client/src/views/ProvideInjectGraph.vue +41 -18
- package/client/src/views/RenderHeatmap.vue +392 -76
- package/client/src/views/TraceViewer.vue +797 -14
- 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 +12 -23
- 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 +23 -13
- 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/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 +48 -1
- package/dist/runtime/test-bridge.d.ts +18 -0
- package/dist/runtime/test-bridge.js +86 -0
- package/dist/runtime/tracing/trace.d.ts +1 -1
- package/package.json +18 -3
- package/client/.env +0 -17
- package/client/dist/assets/index-BuMXDBO9.js +0 -17
- package/client/dist/assets/index-CwcspZ6w.css +0 -1
|
@@ -1,19 +1,53 @@
|
|
|
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'
|
|
6
9
|
import SpanInspector from '@observatory-client/components/SpanInspector.vue'
|
|
7
10
|
import TraceFilter from '@observatory-client/components/TraceFilter.vue'
|
|
8
11
|
import { useTraceFilter } from '@observatory-client/composables/useTraceFilter'
|
|
12
|
+
import { exportJson, importJson } from '@observatory-client/composables/useExportImport'
|
|
13
|
+
import {
|
|
14
|
+
buildRenderSummaryForTrace,
|
|
15
|
+
buildCrossTraceRenderSummary,
|
|
16
|
+
type CrossTraceRenderSummaryRow,
|
|
17
|
+
type TraceRenderStatsRow,
|
|
18
|
+
} from '@observatory-client/composables/trace-render-aggregation'
|
|
19
|
+
import type { ObservatoryExportFile } from '@observatory-client/composables/useExportImport'
|
|
9
20
|
import type { TraceEntry, TraceSpan } from '@observatory/types/snapshot'
|
|
10
21
|
|
|
22
|
+
type TraceSpanRow = {
|
|
23
|
+
span: TraceSpan
|
|
24
|
+
displayName: string
|
|
25
|
+
uid: string | number | undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
11
28
|
const { traces, connected } = useObservatoryData()
|
|
12
29
|
|
|
30
|
+
const importedTraces = ref<TraceEntry[]>([])
|
|
31
|
+
const isImportMode = computed(() => importedTraces.value.length > 0)
|
|
32
|
+
|
|
13
33
|
const selectedTraceId = ref<string | null>(null)
|
|
14
34
|
const selectedSpan = ref<TraceSpan | undefined>(undefined)
|
|
15
35
|
const viewMode = ref<'overview' | 'flamegraph' | 'waterfall'>('overview')
|
|
16
36
|
const showFilters = ref(false)
|
|
37
|
+
const renderSummaryOpen = ref(true)
|
|
38
|
+
const crossTraceSummaryOpen = ref(true)
|
|
39
|
+
const crossTraceSortBy = ref<'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName'>('avgRerendersPerTrace')
|
|
40
|
+
const crossTraceSortDir = ref<'asc' | 'desc'>('desc')
|
|
41
|
+
const crossTraceOnlyRegressions = ref(false)
|
|
42
|
+
const crossTraceOnlyComparable = ref(false)
|
|
43
|
+
const crossTraceSearch = ref('')
|
|
44
|
+
const highlightedUid = ref<string | number | undefined>(undefined)
|
|
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 })
|
|
17
51
|
|
|
18
52
|
const {
|
|
19
53
|
searchQuery,
|
|
@@ -27,19 +61,26 @@ const {
|
|
|
27
61
|
} = useTraceFilter()
|
|
28
62
|
|
|
29
63
|
const sortedTraces = computed(() => {
|
|
30
|
-
|
|
64
|
+
const source = isImportMode.value ? importedTraces.value : traces.value
|
|
65
|
+
return [...source].sort((a, b) => b.startTime - a.startTime)
|
|
31
66
|
})
|
|
32
67
|
|
|
33
68
|
const filteredTraces = computed(() => {
|
|
34
69
|
return applyFilters(sortedTraces.value)
|
|
35
70
|
})
|
|
36
71
|
|
|
72
|
+
const filteredTraceById = computed(() => {
|
|
73
|
+
return new Map(filteredTraces.value.map((trace) => [trace.id, trace] as const))
|
|
74
|
+
})
|
|
75
|
+
|
|
37
76
|
const traceCountLabel = computed(() => {
|
|
77
|
+
const suffix = isImportMode.value ? ' (imported)' : ''
|
|
78
|
+
|
|
38
79
|
if (!hasActiveFilters.value) {
|
|
39
|
-
return `${sortedTraces.value.length} traces`
|
|
80
|
+
return `${sortedTraces.value.length} traces${suffix}`
|
|
40
81
|
}
|
|
41
82
|
|
|
42
|
-
return `Showing ${filteredTraces.value.length} of ${sortedTraces.value.length} traces`
|
|
83
|
+
return `Showing ${filteredTraces.value.length} of ${sortedTraces.value.length} traces${suffix}`
|
|
43
84
|
})
|
|
44
85
|
|
|
45
86
|
const selectedTrace = computed(() => {
|
|
@@ -47,7 +88,154 @@ const selectedTrace = computed(() => {
|
|
|
47
88
|
return filteredTraces.value[0]
|
|
48
89
|
}
|
|
49
90
|
|
|
50
|
-
return
|
|
91
|
+
return filteredTraceById.value.get(selectedTraceId.value)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const renderSummary = computed<TraceRenderStatsRow[]>(() => buildRenderSummaryForTrace(selectedTrace.value))
|
|
95
|
+
|
|
96
|
+
const crossTraceRenderSummary = computed(() => {
|
|
97
|
+
return buildCrossTraceRenderSummary(filteredTraces.value, selectedTrace.value?.id)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const crossTraceRows = computed(() => {
|
|
101
|
+
const q = crossTraceSearch.value.trim().toLowerCase()
|
|
102
|
+
|
|
103
|
+
let rows = crossTraceRenderSummary.value.filter((row) => {
|
|
104
|
+
if (crossTraceOnlyRegressions.value && (row.deltaVsBaseline ?? 0) <= 0) {
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (crossTraceOnlyComparable.value && row.selectedRerenders === undefined) {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (q) {
|
|
113
|
+
const nameMatch = row.componentName.toLowerCase().includes(q)
|
|
114
|
+
const fileMatch = row.file.toLowerCase().includes(q)
|
|
115
|
+
|
|
116
|
+
if (!nameMatch && !fileMatch) {
|
|
117
|
+
return false
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return true
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
rows = [...rows].sort((a, b) => {
|
|
125
|
+
const key = crossTraceSortBy.value
|
|
126
|
+
const dir = crossTraceSortDir.value === 'asc' ? 1 : -1
|
|
127
|
+
|
|
128
|
+
if (key === 'componentName') {
|
|
129
|
+
return a.componentName.localeCompare(b.componentName) * dir
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (key === 'deltaVsBaseline') {
|
|
133
|
+
const av = a.deltaVsBaseline ?? Number.NEGATIVE_INFINITY
|
|
134
|
+
const bv = b.deltaVsBaseline ?? Number.NEGATIVE_INFINITY
|
|
135
|
+
|
|
136
|
+
return (av - bv) * dir
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return ((a[key] as number) - (b[key] as number)) * dir
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
return rows
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
const crossTraceRegressionsCount = computed(() => crossTraceRenderSummary.value.filter((row) => (row.deltaVsBaseline ?? 0) > 0).length)
|
|
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))
|
|
51
239
|
})
|
|
52
240
|
|
|
53
241
|
function elapsedFromSpans(trace: TraceEntry): number | undefined {
|
|
@@ -126,19 +314,179 @@ function getSpanDisplayName(span: TraceSpan): string {
|
|
|
126
314
|
return span.name
|
|
127
315
|
}
|
|
128
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
|
+
|
|
129
337
|
function selectTrace(trace: TraceEntry) {
|
|
130
338
|
selectedTraceId.value = trace.id
|
|
131
339
|
selectedSpan.value = undefined
|
|
340
|
+
highlightedUid.value = undefined
|
|
341
|
+
highlightedComponentKey.value = undefined
|
|
132
342
|
}
|
|
133
343
|
|
|
134
344
|
function handleClearFilters() {
|
|
135
345
|
clearFilters()
|
|
136
346
|
selectedSpan.value = undefined
|
|
347
|
+
highlightedUid.value = undefined
|
|
348
|
+
highlightedComponentKey.value = undefined
|
|
137
349
|
}
|
|
138
350
|
|
|
139
351
|
function handleSpanTypesUpdate(value: Set<string>) {
|
|
140
352
|
selectedSpanTypes.value = value
|
|
141
353
|
}
|
|
354
|
+
|
|
355
|
+
function handleExport() {
|
|
356
|
+
exportJson(`observatory-traces-${Date.now()}.json`, {
|
|
357
|
+
type: 'observatory-traces',
|
|
358
|
+
version: '1',
|
|
359
|
+
exportedAt: Date.now(),
|
|
360
|
+
count: traces.value.length,
|
|
361
|
+
data: traces.value,
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function handleImport() {
|
|
366
|
+
let parsed: unknown
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
parsed = await importJson()
|
|
370
|
+
} catch (err) {
|
|
371
|
+
if (err instanceof Error && err.message !== 'cancelled') {
|
|
372
|
+
alert(`Import failed: ${err.message}`)
|
|
373
|
+
}
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const file = parsed as ObservatoryExportFile<TraceEntry>
|
|
378
|
+
|
|
379
|
+
if (
|
|
380
|
+
file?.type !== 'observatory-traces' ||
|
|
381
|
+
file?.version !== '1' ||
|
|
382
|
+
!Array.isArray(file?.data) ||
|
|
383
|
+
(file.data.length > 0 && (!file.data[0]?.id || !file.data[0]?.name || !Array.isArray(file.data[0]?.spans)))
|
|
384
|
+
) {
|
|
385
|
+
alert('Invalid observatory traces file.')
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
importedTraces.value = file.data
|
|
390
|
+
selectedTraceId.value = null
|
|
391
|
+
selectedSpan.value = undefined
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handleBackToLive() {
|
|
395
|
+
importedTraces.value = []
|
|
396
|
+
selectedTraceId.value = null
|
|
397
|
+
selectedSpan.value = undefined
|
|
398
|
+
highlightedUid.value = undefined
|
|
399
|
+
highlightedComponentKey.value = undefined
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function handleRenderSummaryRowClick(uid: string | number) {
|
|
403
|
+
// Toggle highlight: clicking the already-highlighted row deselects it.
|
|
404
|
+
highlightedUid.value = highlightedUid.value === uid ? undefined : uid
|
|
405
|
+
highlightedComponentKey.value = undefined
|
|
406
|
+
selectedSpan.value = undefined
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function formatDelta(value?: number): string {
|
|
410
|
+
if (value === undefined) {
|
|
411
|
+
return 'n/a'
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const rounded = Math.round(value * 10) / 10
|
|
415
|
+
|
|
416
|
+
if (rounded > 0) {
|
|
417
|
+
return `+${rounded}`
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return `${rounded}`
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function deltaToneClass(value?: number): string {
|
|
424
|
+
if (value === undefined) {
|
|
425
|
+
return 'trace-viewer__delta--na'
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (value > 0) {
|
|
429
|
+
return 'trace-viewer__delta--regression'
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (value < 0) {
|
|
433
|
+
return 'trace-viewer__delta--improvement'
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return 'trace-viewer__delta--neutral'
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function cycleCrossTraceSort(next: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName') {
|
|
440
|
+
if (crossTraceSortBy.value === next) {
|
|
441
|
+
crossTraceSortDir.value = crossTraceSortDir.value === 'desc' ? 'asc' : 'desc'
|
|
442
|
+
|
|
443
|
+
return
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
crossTraceSortBy.value = next
|
|
447
|
+
crossTraceSortDir.value = next === 'componentName' ? 'asc' : 'desc'
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function sortIndicator(key: 'avgRerendersPerTrace' | 'deltaVsBaseline' | 'totalMs' | 'componentName') {
|
|
451
|
+
if (crossTraceSortBy.value !== key) {
|
|
452
|
+
return ''
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return crossTraceSortDir.value === 'desc' ? ' ↓' : ' ↑'
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const crossTraceRowByKey = computed(() => {
|
|
459
|
+
return new Map(crossTraceRows.value.map((row) => [row.componentKey, row] as const))
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
function clearCrossTraceHighlight() {
|
|
463
|
+
highlightedComponentKey.value = undefined
|
|
464
|
+
highlightedUid.value = undefined
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function handleCrossTraceRowClick(componentKey: string) {
|
|
468
|
+
const row = crossTraceRowByKey.value.get(componentKey)
|
|
469
|
+
|
|
470
|
+
if (!row || row.selectedUid === undefined) {
|
|
471
|
+
highlightedUid.value = undefined
|
|
472
|
+
highlightedComponentKey.value = undefined
|
|
473
|
+
selectedSpan.value = undefined
|
|
474
|
+
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (highlightedComponentKey.value === componentKey) {
|
|
479
|
+
highlightedComponentKey.value = undefined
|
|
480
|
+
highlightedUid.value = undefined
|
|
481
|
+
selectedSpan.value = undefined
|
|
482
|
+
|
|
483
|
+
return
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
highlightedComponentKey.value = componentKey
|
|
487
|
+
highlightedUid.value = row.selectedUid
|
|
488
|
+
selectedSpan.value = undefined
|
|
489
|
+
}
|
|
142
490
|
</script>
|
|
143
491
|
|
|
144
492
|
<template>
|
|
@@ -148,6 +496,18 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
148
496
|
<div class="trace-viewer__title">Trace Viewer</div>
|
|
149
497
|
<div class="trace-viewer__header-actions">
|
|
150
498
|
<div class="trace-viewer__count muted text-sm">{{ traceCountLabel }}</div>
|
|
499
|
+
<button v-if="isImportMode" class="trace-viewer__import-mode-btn" title="Return to live data" @click="handleBackToLive">
|
|
500
|
+
← live
|
|
501
|
+
</button>
|
|
502
|
+
<button
|
|
503
|
+
class="trace-viewer__action-btn"
|
|
504
|
+
title="Export traces as JSON"
|
|
505
|
+
:disabled="traces.length === 0 && !isImportMode"
|
|
506
|
+
@click="handleExport"
|
|
507
|
+
>
|
|
508
|
+
↓ export
|
|
509
|
+
</button>
|
|
510
|
+
<button class="trace-viewer__action-btn" title="Import traces from JSON file" @click="handleImport">↑ import</button>
|
|
151
511
|
<button
|
|
152
512
|
:class="{ 'trace-viewer__filter-btn--active': showFilters }"
|
|
153
513
|
class="trace-viewer__filter-btn"
|
|
@@ -184,7 +544,7 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
184
544
|
<div class="trace-viewer__list-header">
|
|
185
545
|
<span class="trace-viewer__list-title">Traces</span>
|
|
186
546
|
</div>
|
|
187
|
-
<div class="trace-viewer__table-wrap tracker-table-wrap">
|
|
547
|
+
<div ref="traceListScrollRef" class="trace-viewer__table-wrap tracker-table-wrap">
|
|
188
548
|
<table class="data-table">
|
|
189
549
|
<thead>
|
|
190
550
|
<tr>
|
|
@@ -195,7 +555,14 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
195
555
|
</thead>
|
|
196
556
|
<tbody>
|
|
197
557
|
<tr
|
|
198
|
-
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"
|
|
199
566
|
:key="trace.id"
|
|
200
567
|
:class="{ 'trace-viewer__trace-row--selected': selectedTrace?.id === trace.id }"
|
|
201
568
|
class="trace-viewer__trace-row"
|
|
@@ -205,6 +572,13 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
205
572
|
<td class="mono">{{ formatDuration(trace.durationMs, trace) }}</td>
|
|
206
573
|
<td class="mono">{{ trace.spans.length }}</td>
|
|
207
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>
|
|
208
582
|
<tr v-if="!filteredTraces.length">
|
|
209
583
|
<td colspan="3" class="tracker-empty-cell">
|
|
210
584
|
{{
|
|
@@ -276,19 +650,196 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
276
650
|
|
|
277
651
|
<div class="trace-viewer__span-list">
|
|
278
652
|
<div
|
|
279
|
-
v-for="
|
|
280
|
-
:key="span.id"
|
|
281
|
-
:class="{
|
|
653
|
+
v-for="spanRow in selectedTraceSpanRows"
|
|
654
|
+
:key="spanRow.span.id"
|
|
655
|
+
:class="{
|
|
656
|
+
'trace-viewer__span-item--selected': selectedSpan?.id === spanRow.span.id,
|
|
657
|
+
'trace-viewer__span-item--highlighted':
|
|
658
|
+
highlightedUid !== undefined && spanRow.uid === highlightedUid,
|
|
659
|
+
}"
|
|
282
660
|
class="trace-viewer__span-item"
|
|
283
|
-
@click="selectedSpan = span"
|
|
661
|
+
@click="selectedSpan = spanRow.span"
|
|
284
662
|
>
|
|
285
|
-
<div class="trace-viewer__span-name">{{
|
|
663
|
+
<div class="trace-viewer__span-name">{{ spanRow.displayName }}</div>
|
|
286
664
|
<div class="trace-viewer__span-meta">
|
|
287
|
-
<span class="trace-viewer__span-type">{{ span.type }}</span>
|
|
288
|
-
<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>
|
|
289
667
|
</div>
|
|
290
668
|
</div>
|
|
291
669
|
</div>
|
|
670
|
+
|
|
671
|
+
<!-- Render Summary panel — shown when the trace has render spans -->
|
|
672
|
+
<template v-if="renderSummary.length > 0">
|
|
673
|
+
<div class="trace-viewer__render-summary-header" @click="renderSummaryOpen = !renderSummaryOpen">
|
|
674
|
+
<span class="trace-viewer__render-summary-toggle">{{ renderSummaryOpen ? '▾' : '▸' }}</span>
|
|
675
|
+
<span class="trace-viewer__render-summary-title">Render Summary</span>
|
|
676
|
+
<span class="trace-viewer__render-summary-count muted">{{ renderSummary.length }} components</span>
|
|
677
|
+
<span
|
|
678
|
+
v-if="highlightedUid !== undefined"
|
|
679
|
+
class="trace-viewer__render-summary-clear"
|
|
680
|
+
@click.stop="highlightedUid = undefined"
|
|
681
|
+
>
|
|
682
|
+
✕ clear
|
|
683
|
+
</span>
|
|
684
|
+
</div>
|
|
685
|
+
<div v-if="renderSummaryOpen" class="trace-viewer__render-summary-table-wrap">
|
|
686
|
+
<table class="data-table trace-viewer__render-summary-table">
|
|
687
|
+
<thead>
|
|
688
|
+
<tr>
|
|
689
|
+
<th>Component</th>
|
|
690
|
+
<th title="Times the component was mounted">Mounts</th>
|
|
691
|
+
<th title="Reactive re-renders after initial mount">Re-renders</th>
|
|
692
|
+
<th title="Average render duration">Avg</th>
|
|
693
|
+
<th title="Sum of all render durations">Total</th>
|
|
694
|
+
</tr>
|
|
695
|
+
</thead>
|
|
696
|
+
<tbody>
|
|
697
|
+
<tr
|
|
698
|
+
v-for="row in renderSummary"
|
|
699
|
+
:key="row.uid"
|
|
700
|
+
:class="{ 'trace-viewer__render-summary-row--active': highlightedUid === row.uid }"
|
|
701
|
+
class="trace-viewer__render-summary-row"
|
|
702
|
+
:title="row.file"
|
|
703
|
+
@click="handleRenderSummaryRowClick(row.uid)"
|
|
704
|
+
>
|
|
705
|
+
<td class="mono">{{ row.componentName }}</td>
|
|
706
|
+
<td class="mono">{{ row.mountCount }}</td>
|
|
707
|
+
<td class="mono" :class="{ 'trace-viewer__render-hot': row.rerenderCount > 3 }">
|
|
708
|
+
{{ row.rerenderCount }}
|
|
709
|
+
</td>
|
|
710
|
+
<td class="mono">{{ formatDuration(row.avgMs) }}</td>
|
|
711
|
+
<td class="mono">{{ formatDuration(row.totalMs) }}</td>
|
|
712
|
+
</tr>
|
|
713
|
+
</tbody>
|
|
714
|
+
</table>
|
|
715
|
+
</div>
|
|
716
|
+
</template>
|
|
717
|
+
|
|
718
|
+
<!-- Cross-trace comparison panel -->
|
|
719
|
+
<template v-if="crossTraceRenderSummary.length > 0">
|
|
720
|
+
<div class="trace-viewer__render-summary-header" @click="crossTraceSummaryOpen = !crossTraceSummaryOpen">
|
|
721
|
+
<span class="trace-viewer__render-summary-toggle">{{ crossTraceSummaryOpen ? '▾' : '▸' }}</span>
|
|
722
|
+
<span class="trace-viewer__render-summary-title">Cross-Trace Render Comparison</span>
|
|
723
|
+
<span class="trace-viewer__render-summary-count muted">
|
|
724
|
+
{{ crossTraceRenderSummary.length }} components · {{ filteredTraces.length }} traces
|
|
725
|
+
</span>
|
|
726
|
+
<span class="trace-viewer__render-summary-count muted">
|
|
727
|
+
{{ crossTraceRegressionsCount }} regressions
|
|
728
|
+
</span>
|
|
729
|
+
<span class="trace-viewer__mobile-hint muted">mobile mode: condensed columns</span>
|
|
730
|
+
<span
|
|
731
|
+
v-if="highlightedComponentKey !== undefined"
|
|
732
|
+
class="trace-viewer__render-summary-clear"
|
|
733
|
+
@click.stop="clearCrossTraceHighlight"
|
|
734
|
+
>
|
|
735
|
+
✕ clear
|
|
736
|
+
</span>
|
|
737
|
+
</div>
|
|
738
|
+
<div v-if="crossTraceSummaryOpen" ref="crossTraceScrollRef" class="trace-viewer__render-summary-table-wrap">
|
|
739
|
+
<div class="trace-viewer__comparison-toolbar">
|
|
740
|
+
<button
|
|
741
|
+
:class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyRegressions }"
|
|
742
|
+
class="trace-viewer__comparison-chip"
|
|
743
|
+
@click="crossTraceOnlyRegressions = !crossTraceOnlyRegressions"
|
|
744
|
+
>
|
|
745
|
+
regressions only
|
|
746
|
+
</button>
|
|
747
|
+
<button
|
|
748
|
+
:class="{ 'trace-viewer__comparison-chip--active': crossTraceOnlyComparable }"
|
|
749
|
+
class="trace-viewer__comparison-chip"
|
|
750
|
+
@click="crossTraceOnlyComparable = !crossTraceOnlyComparable"
|
|
751
|
+
>
|
|
752
|
+
selected trace only
|
|
753
|
+
</button>
|
|
754
|
+
<span class="trace-viewer__comparison-spacer"></span>
|
|
755
|
+
<input
|
|
756
|
+
v-model="crossTraceSearch"
|
|
757
|
+
class="trace-viewer__comparison-search mono"
|
|
758
|
+
type="search"
|
|
759
|
+
placeholder="filter component..."
|
|
760
|
+
/>
|
|
761
|
+
</div>
|
|
762
|
+
<table class="data-table trace-viewer__render-summary-table">
|
|
763
|
+
<thead>
|
|
764
|
+
<tr>
|
|
765
|
+
<th class="trace-viewer__sortable" @click="cycleCrossTraceSort('componentName')">
|
|
766
|
+
Component{{ sortIndicator('componentName') }}
|
|
767
|
+
</th>
|
|
768
|
+
<th class="trace-viewer__col-desktop" title="How many traces include this component">
|
|
769
|
+
Traces
|
|
770
|
+
</th>
|
|
771
|
+
<th
|
|
772
|
+
class="trace-viewer__sortable"
|
|
773
|
+
title="Average re-renders per trace"
|
|
774
|
+
@click="cycleCrossTraceSort('avgRerendersPerTrace')"
|
|
775
|
+
>
|
|
776
|
+
Avg Re-renders{{ sortIndicator('avgRerendersPerTrace') }}
|
|
777
|
+
</th>
|
|
778
|
+
<th class="trace-viewer__col-desktop" title="Re-renders in selected trace">Selected</th>
|
|
779
|
+
<th
|
|
780
|
+
class="trace-viewer__sortable"
|
|
781
|
+
title="Selected trace vs other traces baseline"
|
|
782
|
+
@click="cycleCrossTraceSort('deltaVsBaseline')"
|
|
783
|
+
>
|
|
784
|
+
Delta{{ sortIndicator('deltaVsBaseline') }}
|
|
785
|
+
</th>
|
|
786
|
+
<th class="trace-viewer__col-desktop" title="Average render duration across all renders">
|
|
787
|
+
Avg ms
|
|
788
|
+
</th>
|
|
789
|
+
<th
|
|
790
|
+
class="trace-viewer__sortable"
|
|
791
|
+
title="Sum of all render duration across filtered traces"
|
|
792
|
+
@click="cycleCrossTraceSort('totalMs')"
|
|
793
|
+
>
|
|
794
|
+
Total ms{{ sortIndicator('totalMs') }}
|
|
795
|
+
</th>
|
|
796
|
+
</tr>
|
|
797
|
+
</thead>
|
|
798
|
+
<tbody>
|
|
799
|
+
<tr
|
|
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"
|
|
808
|
+
:key="row.componentKey"
|
|
809
|
+
:class="{
|
|
810
|
+
'trace-viewer__render-summary-row--active':
|
|
811
|
+
highlightedComponentKey === row.componentKey,
|
|
812
|
+
}"
|
|
813
|
+
class="trace-viewer__render-summary-row"
|
|
814
|
+
:title="row.file"
|
|
815
|
+
@click="handleCrossTraceRowClick(row.componentKey)"
|
|
816
|
+
>
|
|
817
|
+
<td class="mono">{{ row.componentName }}</td>
|
|
818
|
+
<td class="mono trace-viewer__col-desktop">{{ row.tracesSeen }}</td>
|
|
819
|
+
<td class="mono">{{ (Math.round(row.avgRerendersPerTrace * 10) / 10).toFixed(1) }}</td>
|
|
820
|
+
<td class="mono trace-viewer__col-desktop">{{ row.selectedRerenders ?? 'n/a' }}</td>
|
|
821
|
+
<td class="mono">
|
|
822
|
+
<span class="trace-viewer__delta" :class="deltaToneClass(row.deltaVsBaseline)">
|
|
823
|
+
{{ formatDelta(row.deltaVsBaseline) }}
|
|
824
|
+
</span>
|
|
825
|
+
</td>
|
|
826
|
+
<td class="mono trace-viewer__col-desktop">{{ formatDuration(row.avgMsPerRender) }}</td>
|
|
827
|
+
<td class="mono">{{ formatDuration(row.totalMs) }}</td>
|
|
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>
|
|
836
|
+
<tr v-if="!crossTraceRows.length">
|
|
837
|
+
<td colspan="7" class="tracker-empty-cell">No components match the comparison filters.</td>
|
|
838
|
+
</tr>
|
|
839
|
+
</tbody>
|
|
840
|
+
</table>
|
|
841
|
+
</div>
|
|
842
|
+
</template>
|
|
292
843
|
</div>
|
|
293
844
|
|
|
294
845
|
<!-- Flamegraph -->
|
|
@@ -365,6 +916,44 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
365
916
|
color: var(--accent);
|
|
366
917
|
}
|
|
367
918
|
|
|
919
|
+
.trace-viewer__action-btn {
|
|
920
|
+
padding: 3px 8px;
|
|
921
|
+
background: none;
|
|
922
|
+
border: 1px solid var(--border);
|
|
923
|
+
color: var(--text-secondary);
|
|
924
|
+
cursor: pointer;
|
|
925
|
+
font-size: 11px;
|
|
926
|
+
border-radius: 3px;
|
|
927
|
+
transition: all 0.12s;
|
|
928
|
+
font-family: var(--mono);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
.trace-viewer__action-btn:disabled {
|
|
932
|
+
opacity: 0.4;
|
|
933
|
+
cursor: not-allowed;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
.trace-viewer__action-btn:hover:not(:disabled) {
|
|
937
|
+
background: var(--bg-secondary);
|
|
938
|
+
color: var(--text);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.trace-viewer__import-mode-btn {
|
|
942
|
+
padding: 3px 8px;
|
|
943
|
+
background: var(--accent-bg);
|
|
944
|
+
border: 1px solid var(--accent);
|
|
945
|
+
color: var(--accent);
|
|
946
|
+
cursor: pointer;
|
|
947
|
+
font-size: 11px;
|
|
948
|
+
border-radius: 3px;
|
|
949
|
+
transition: all 0.12s;
|
|
950
|
+
font-family: var(--mono);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
.trace-viewer__import-mode-btn:hover {
|
|
954
|
+
opacity: 0.8;
|
|
955
|
+
}
|
|
956
|
+
|
|
368
957
|
.trace-viewer__container {
|
|
369
958
|
display: flex;
|
|
370
959
|
flex: 1;
|
|
@@ -413,6 +1002,12 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
413
1002
|
border-left: 2px solid var(--accent);
|
|
414
1003
|
}
|
|
415
1004
|
|
|
1005
|
+
.trace-viewer__virtual-spacer-row td {
|
|
1006
|
+
padding: 0;
|
|
1007
|
+
border-bottom: 0;
|
|
1008
|
+
background: transparent;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
416
1011
|
.trace-viewer__detail {
|
|
417
1012
|
flex: 1;
|
|
418
1013
|
display: flex;
|
|
@@ -572,7 +1167,176 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
572
1167
|
overflow: auto;
|
|
573
1168
|
}
|
|
574
1169
|
|
|
575
|
-
|
|
1170
|
+
.trace-viewer__span-item--highlighted {
|
|
1171
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 8%, transparent);
|
|
1172
|
+
border-left: 2px solid var(--purple, #a855f7);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
.trace-viewer__render-summary-header {
|
|
1176
|
+
display: flex;
|
|
1177
|
+
align-items: center;
|
|
1178
|
+
gap: 8px;
|
|
1179
|
+
padding: 8px 16px;
|
|
1180
|
+
border-top: 1px solid var(--border);
|
|
1181
|
+
border-bottom: 1px solid var(--border);
|
|
1182
|
+
background: var(--bg3, var(--bg2, var(--bg)));
|
|
1183
|
+
cursor: pointer;
|
|
1184
|
+
user-select: none;
|
|
1185
|
+
flex-shrink: 0;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.trace-viewer__render-summary-header:hover {
|
|
1189
|
+
background: var(--bg2, var(--bg));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.trace-viewer__render-summary-toggle {
|
|
1193
|
+
font-size: 10px;
|
|
1194
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1195
|
+
width: 12px;
|
|
1196
|
+
flex-shrink: 0;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
.trace-viewer__render-summary-title {
|
|
1200
|
+
font-size: 11px;
|
|
1201
|
+
font-weight: 600;
|
|
1202
|
+
color: var(--text2, var(--text));
|
|
1203
|
+
text-transform: uppercase;
|
|
1204
|
+
letter-spacing: 0.5px;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.trace-viewer__render-summary-count {
|
|
1208
|
+
font-size: 11px;
|
|
1209
|
+
font-family: var(--mono);
|
|
1210
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.trace-viewer__render-summary-clear {
|
|
1214
|
+
margin-left: auto;
|
|
1215
|
+
font-size: 11px;
|
|
1216
|
+
color: var(--accent);
|
|
1217
|
+
cursor: pointer;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
.trace-viewer__render-summary-clear:hover {
|
|
1221
|
+
text-decoration: underline;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
.trace-viewer__mobile-hint {
|
|
1225
|
+
display: none;
|
|
1226
|
+
margin-left: auto;
|
|
1227
|
+
font-size: 11px;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
.trace-viewer__render-summary-table-wrap {
|
|
1231
|
+
flex-shrink: 0;
|
|
1232
|
+
max-height: 200px;
|
|
1233
|
+
overflow: auto;
|
|
1234
|
+
border-bottom: 1px solid var(--border);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
.trace-viewer__comparison-toolbar {
|
|
1238
|
+
display: flex;
|
|
1239
|
+
align-items: center;
|
|
1240
|
+
gap: 8px;
|
|
1241
|
+
padding: 8px 12px;
|
|
1242
|
+
border-bottom: 1px solid var(--border);
|
|
1243
|
+
background: var(--bg2, var(--bg));
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.trace-viewer__comparison-chip {
|
|
1247
|
+
padding: 2px 8px;
|
|
1248
|
+
border: 1px solid var(--border);
|
|
1249
|
+
border-radius: 999px;
|
|
1250
|
+
background: transparent;
|
|
1251
|
+
color: var(--text2, var(--text));
|
|
1252
|
+
font-size: 11px;
|
|
1253
|
+
font-family: var(--mono);
|
|
1254
|
+
cursor: pointer;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
.trace-viewer__comparison-chip--active {
|
|
1258
|
+
border-color: color-mix(in srgb, var(--purple, #a855f7) 55%, var(--border));
|
|
1259
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 15%, transparent);
|
|
1260
|
+
color: var(--purple, #a855f7);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.trace-viewer__comparison-spacer {
|
|
1264
|
+
flex: 1;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.trace-viewer__comparison-search {
|
|
1268
|
+
min-width: 180px;
|
|
1269
|
+
max-width: 240px;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
.trace-viewer__render-summary-table {
|
|
1273
|
+
width: 100%;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.trace-viewer__sortable {
|
|
1277
|
+
cursor: pointer;
|
|
1278
|
+
user-select: none;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.trace-viewer__sortable:hover {
|
|
1282
|
+
color: var(--text);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
.trace-viewer__render-summary-row {
|
|
1286
|
+
cursor: pointer;
|
|
1287
|
+
transition: background 0.15s;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.trace-viewer__render-summary-row:hover {
|
|
1291
|
+
background: var(--bg2, var(--bg));
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
.trace-viewer__render-summary-row--active {
|
|
1295
|
+
background: color-mix(in srgb, var(--purple, #a855f7) 8%, transparent);
|
|
1296
|
+
border-left: 2px solid var(--purple, #a855f7);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
.trace-viewer__render-hot {
|
|
1300
|
+
color: var(--orange, #f97316);
|
|
1301
|
+
font-weight: 600;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
.trace-viewer__delta {
|
|
1305
|
+
display: inline-flex;
|
|
1306
|
+
align-items: center;
|
|
1307
|
+
justify-content: center;
|
|
1308
|
+
min-width: 46px;
|
|
1309
|
+
padding: 2px 6px;
|
|
1310
|
+
border-radius: 999px;
|
|
1311
|
+
border: 1px solid transparent;
|
|
1312
|
+
font-weight: 600;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
.trace-viewer__delta--regression {
|
|
1316
|
+
color: var(--orange, #f97316);
|
|
1317
|
+
border-color: color-mix(in srgb, var(--orange, #f97316) 40%, var(--border));
|
|
1318
|
+
background: color-mix(in srgb, var(--orange, #f97316) 14%, transparent);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
.trace-viewer__delta--improvement {
|
|
1322
|
+
color: var(--green, #22c55e);
|
|
1323
|
+
border-color: color-mix(in srgb, var(--green, #22c55e) 45%, var(--border));
|
|
1324
|
+
background: color-mix(in srgb, var(--green, #22c55e) 14%, transparent);
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
.trace-viewer__delta--neutral {
|
|
1328
|
+
color: var(--text2, var(--text));
|
|
1329
|
+
border-color: var(--border);
|
|
1330
|
+
background: var(--bg2, var(--bg));
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.trace-viewer__delta--na {
|
|
1334
|
+
color: var(--text3, var(--text2, var(--text)));
|
|
1335
|
+
border-color: var(--border);
|
|
1336
|
+
background: transparent;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
@media (width <= 1024px) {
|
|
576
1340
|
.trace-viewer__container {
|
|
577
1341
|
flex-direction: column;
|
|
578
1342
|
gap: 0;
|
|
@@ -595,5 +1359,24 @@ function handleSpanTypesUpdate(value: Set<string>) {
|
|
|
595
1359
|
border-left: none;
|
|
596
1360
|
border-top: 1px solid var(--border);
|
|
597
1361
|
}
|
|
1362
|
+
|
|
1363
|
+
.trace-viewer__comparison-toolbar {
|
|
1364
|
+
flex-wrap: wrap;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
.trace-viewer__comparison-search {
|
|
1368
|
+
min-width: 100%;
|
|
1369
|
+
max-width: 100%;
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
@media (width <= 768px) {
|
|
1374
|
+
.trace-viewer__col-desktop {
|
|
1375
|
+
display: none;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.trace-viewer__mobile-hint {
|
|
1379
|
+
display: inline-flex;
|
|
1380
|
+
}
|
|
598
1381
|
}
|
|
599
1382
|
</style>
|