feat(stage-*): performance overlay & markdown stress test (#838)
--------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Neko <neko@ayaka.moe>
This commit is contained in:
co-authored by
autofix-ci[bot]
Neko
parent
1f58faafa2
commit
9005b9f430
@@ -31,6 +31,7 @@
|
||||
"@proj-airi/model-driver-mediapipe": "workspace:^",
|
||||
"@proj-airi/server-sdk": "workspace:^",
|
||||
"@proj-airi/stage-layouts": "workspace:^",
|
||||
"@proj-airi/stage-shared": "workspace:^",
|
||||
"@proj-airi/stage-ui": "workspace:^",
|
||||
"@proj-airi/stage-ui-three": "workspace:^",
|
||||
"@proj-airi/stage-ui-three-performance-runtime": "workspace:^",
|
||||
|
||||
@@ -16,6 +16,8 @@ import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
import { toast, Toaster } from 'vue-sonner'
|
||||
|
||||
import PerformanceOverlay from './components/Devtools/PerformanceOverlay.vue'
|
||||
|
||||
import { usePWAStore } from './stores/pwa'
|
||||
|
||||
import 'vue-sonner/style.css'
|
||||
@@ -125,6 +127,8 @@ function handleSetupSkipped() {
|
||||
@configured="handleSetupConfigured"
|
||||
@skipped="handleSetupSkipped"
|
||||
/>
|
||||
|
||||
<PerformanceOverlay />
|
||||
</template>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script setup lang="ts">
|
||||
import type { LagMetric } from '../../stores/devtools-lag'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { useDevtoolsLagStore } from '../../stores/devtools-lag'
|
||||
|
||||
const store = useDevtoolsLagStore()
|
||||
const { enabled, buffers, recording } = storeToRefs(store)
|
||||
|
||||
const hovered = ref(false)
|
||||
|
||||
const metrics: Array<{ key: LagMetric, label: string, enabled: () => boolean }> = [
|
||||
{ key: 'fps', label: 'FPS', enabled: () => enabled.value.fps },
|
||||
{ key: 'frameDuration', label: 'Frame (ms)', enabled: () => enabled.value.frameDuration },
|
||||
{ key: 'longtask', label: 'Long task (ms)', enabled: () => enabled.value.longtask },
|
||||
{ key: 'memory', label: 'Memory (MB)', enabled: () => enabled.value.memory },
|
||||
]
|
||||
|
||||
const visibleMetrics = computed(() => metrics.filter(metric => metric.enabled()))
|
||||
const hasAnyEnabled = computed(() => visibleMetrics.value.length > 0)
|
||||
const metricStatsMap = computed<Record<LagMetric, ReturnType<typeof store.calcStats>>>(() => {
|
||||
const result = {} as Record<LagMetric, ReturnType<typeof store.calcStats>>
|
||||
for (const metric of metrics) {
|
||||
const values = buffers.value[metric.key].map(sample => sample.value)
|
||||
result[metric.key] = store.calcStats(values)
|
||||
}
|
||||
return result
|
||||
})
|
||||
const metricsWithStats = computed(() => visibleMetrics.value.map(metric => ({
|
||||
...metric,
|
||||
stats: metricStatsMap.value[metric.key],
|
||||
})))
|
||||
|
||||
function formatValue(metric: string, value: number) {
|
||||
if (!Number.isFinite(value))
|
||||
return '--'
|
||||
|
||||
if (metric === 'memory')
|
||||
return `${(value / 1048576).toFixed(1)}`
|
||||
|
||||
if (metric === 'fps')
|
||||
return value.toFixed(0)
|
||||
|
||||
return value.toFixed(1)
|
||||
}
|
||||
|
||||
function barSeries(metric: LagMetric) {
|
||||
const values = buffers.value[metric].map(sample => sample.value)
|
||||
const histogram = store.buildHistogram(values, 20)
|
||||
const max = Math.max(1, ...histogram.map(bin => bin.count))
|
||||
return histogram.map(bin => ({
|
||||
width: `${100 / (histogram.length || 1)}%`,
|
||||
height: `${(bin.count / max) * 100}%`,
|
||||
}))
|
||||
}
|
||||
|
||||
function toggleRecording() {
|
||||
if (recording.value) {
|
||||
const snapshot = store.stopRecording()
|
||||
if (snapshot)
|
||||
store.exportCsv(snapshot)
|
||||
return
|
||||
}
|
||||
|
||||
store.startRecording()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="hasAnyEnabled"
|
||||
:style="{ opacity: hovered ? 1 : 0.3 }"
|
||||
class="fixed bottom-3 left-3 z-50"
|
||||
p-3
|
||||
flex="~ col gap-2"
|
||||
rounded="xl"
|
||||
bg="neutral-900/80"
|
||||
text="white sm"
|
||||
shadow="xl"
|
||||
transition="opacity 200ms ease"
|
||||
@mouseenter="hovered = true"
|
||||
@mouseleave="hovered = false"
|
||||
>
|
||||
<div flex="~ row items-center gap-2" justify-between>
|
||||
<div text="xs neutral-200" uppercase tracking="wide">
|
||||
Performance Overlay
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded bg-white/10 px-2 py-1 text-xs transition-colors hover:bg-white/20"
|
||||
@click="toggleRecording"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full"
|
||||
:class="recording ? 'bg-red-400' : 'bg-neutral-400'"
|
||||
/>
|
||||
<span>{{ recording ? 'Stop' : 'Record' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="metric in metricsWithStats" :key="metric.key" flex="~ col gap-1">
|
||||
<div flex="~ row items-center justify-between">
|
||||
<span text="xs neutral-100">{{ metric.label }}</span>
|
||||
<span text="xs neutral-300">
|
||||
<template v-if="metric.stats.latest">
|
||||
avg {{ formatValue(metric.key, metric.stats.avg) }}
|
||||
/
|
||||
p95 {{ formatValue(metric.key, metric.stats.p95) }}
|
||||
</template>
|
||||
<template v-else>
|
||||
--
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<div class="h-10 flex items-end gap-0.5 overflow-hidden rounded bg-white/5 px-1 py-1">
|
||||
<div
|
||||
v-for="(bar, index) in barSeries(metric.key)"
|
||||
:key="index"
|
||||
:style="{ width: bar.width, height: bar.height }"
|
||||
class="bg-white/50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { PerfTracer } from '@proj-airi/stage-shared'
|
||||
|
||||
interface LagEnabled {
|
||||
fps: boolean
|
||||
frameDuration: boolean
|
||||
longtask: boolean
|
||||
memory: boolean
|
||||
}
|
||||
|
||||
export function createLagSampler(tracer: PerfTracer) {
|
||||
let rafId: number | undefined
|
||||
let lastTs: number | undefined
|
||||
let longTaskObserver: PerformanceObserver | undefined
|
||||
let memoryTimer: ReturnType<typeof setInterval> | undefined
|
||||
|
||||
function stopRaf() {
|
||||
if (rafId !== undefined) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = undefined
|
||||
}
|
||||
lastTs = undefined
|
||||
}
|
||||
|
||||
function startRaf() {
|
||||
stopRaf()
|
||||
|
||||
const loop = (ts: number) => {
|
||||
if (lastTs !== undefined) {
|
||||
const delta = ts - lastTs
|
||||
const fps = delta > 0 ? 1000 / delta : 0
|
||||
|
||||
tracer.emit({
|
||||
tracerId: 'lag',
|
||||
name: 'fps',
|
||||
ts,
|
||||
duration: fps,
|
||||
})
|
||||
|
||||
tracer.emit({
|
||||
tracerId: 'lag',
|
||||
name: 'frameDuration',
|
||||
ts,
|
||||
duration: delta,
|
||||
})
|
||||
}
|
||||
|
||||
lastTs = ts
|
||||
rafId = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(loop)
|
||||
}
|
||||
|
||||
function stopLongTaskObserver() {
|
||||
longTaskObserver?.disconnect()
|
||||
longTaskObserver = undefined
|
||||
}
|
||||
|
||||
function startLongTaskObserver() {
|
||||
stopLongTaskObserver()
|
||||
if (!('PerformanceObserver' in window))
|
||||
return
|
||||
|
||||
try {
|
||||
longTaskObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
tracer.emit({
|
||||
tracerId: 'lag',
|
||||
name: 'longtask',
|
||||
ts: entry.startTime,
|
||||
duration: entry.duration,
|
||||
})
|
||||
}
|
||||
})
|
||||
longTaskObserver.observe({ type: 'longtask', buffered: true })
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('[LagSampler] Failed to start longtask observer', error)
|
||||
}
|
||||
}
|
||||
|
||||
function stopMemoryTimer() {
|
||||
if (memoryTimer) {
|
||||
clearInterval(memoryTimer)
|
||||
memoryTimer = undefined
|
||||
}
|
||||
}
|
||||
|
||||
function startMemoryTimer() {
|
||||
stopMemoryTimer()
|
||||
const perfWithMemory = performance as Performance & { memory?: { usedJSHeapSize: number } }
|
||||
if (!perfWithMemory.memory)
|
||||
return
|
||||
|
||||
memoryTimer = setInterval(() => {
|
||||
tracer.emit({
|
||||
tracerId: 'lag',
|
||||
name: 'memory',
|
||||
ts: performance.now(),
|
||||
duration: perfWithMemory.memory?.usedJSHeapSize ?? 0,
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function start(enabled: LagEnabled) {
|
||||
stop()
|
||||
|
||||
if (enabled.fps || enabled.frameDuration)
|
||||
startRaf()
|
||||
|
||||
if (enabled.longtask)
|
||||
startLongTaskObserver()
|
||||
|
||||
if (enabled.memory)
|
||||
startMemoryTimer()
|
||||
}
|
||||
|
||||
function stop() {
|
||||
stopRaf()
|
||||
stopLongTaskObserver()
|
||||
stopMemoryTimer()
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
stop,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonBar, CheckBar } from '@proj-airi/stage-ui/components'
|
||||
import { useMagicKeys, whenever } from '@vueuse/core'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useDevtoolsLagStore } from '../../stores/devtools-lag'
|
||||
|
||||
const lagStore = useDevtoolsLagStore()
|
||||
const { enabled, lastRecording, recording } = storeToRefs(lagStore)
|
||||
|
||||
const recordingLabel = computed(() => recording.value ? 'Stop recording (max 60s)' : 'Start recording')
|
||||
const hasRecording = computed(() => !!lastRecording.value)
|
||||
const allEnabled = computed(() => enabled.value.fps && enabled.value.frameDuration && enabled.value.longtask && enabled.value.memory)
|
||||
|
||||
const magicKeys = useMagicKeys()
|
||||
whenever(magicKeys['ctrl+alt+l'], () => toggleAll(true))
|
||||
whenever(magicKeys['ctrl+alt+k'], () => toggleAll(false))
|
||||
|
||||
function toggleAll(on: boolean) {
|
||||
lagStore.toggleAll(on)
|
||||
}
|
||||
|
||||
function exportCsv() {
|
||||
lagStore.exportCsv()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div flex="~ col gap-4" pb-6>
|
||||
<div flex="~ col gap-2">
|
||||
<div flex="~ row items-center gap-2">
|
||||
<CheckBar
|
||||
:model-value="allEnabled"
|
||||
icon-on="i-solar:sledgehammer-bold-duotone"
|
||||
icon-off="i-solar:sledgehammer-bold-duotone"
|
||||
text="Enable all metrics"
|
||||
description="Toggle all lag metrics (FPS, frame time, long task, memory)"
|
||||
@update:model-value="value => toggleAll(Boolean(value))"
|
||||
/>
|
||||
<ButtonBar
|
||||
:icon="recording ? 'i-solar:stop-circle-bold-duotone' : 'i-solar:recive-bold-duotone'"
|
||||
text="Recording"
|
||||
@click="recording ? lagStore.stopRecording() : lagStore.startRecording()"
|
||||
>
|
||||
{{ recordingLabel }}
|
||||
</ButtonBar>
|
||||
<ButtonBar
|
||||
icon="i-solar:export-bold-duotone"
|
||||
text="Export CSV"
|
||||
:disabled="!hasRecording"
|
||||
@click="exportCsv"
|
||||
>
|
||||
Export last recording
|
||||
</ButtonBar>
|
||||
</div>
|
||||
|
||||
<div flex="~ col gap-2">
|
||||
<CheckBar
|
||||
v-model="enabled.fps"
|
||||
icon-on="i-solar:activity-bold-duotone"
|
||||
icon-off="i-solar:activity-bold-duotone"
|
||||
text="FPS"
|
||||
description="Collect FPS histogram"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="enabled.frameDuration"
|
||||
icon-on="i-solar:chart-bold-duotone"
|
||||
icon-off="i-solar:chart-bold-duotone"
|
||||
text="Frame time (ms)"
|
||||
description="Collect frame duration histogram"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="enabled.longtask"
|
||||
icon-on="i-solar:timer-bold-duotone"
|
||||
icon-off="i-solar:timer-bold-duotone"
|
||||
text="Long tasks"
|
||||
description="PerformanceObserver('longtask')"
|
||||
/>
|
||||
<CheckBar
|
||||
v-model="enabled.memory"
|
||||
icon-on="i-solar:database-bold-duotone"
|
||||
icon-off="i-solar:database-bold-duotone"
|
||||
text="Memory"
|
||||
description="Sample performance.memory every second"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hasRecording" flex="~ col gap-2" rounded="lg" border="1 dashed neutral-700" p-3>
|
||||
<div text="sm neutral-200">
|
||||
Last recording
|
||||
</div>
|
||||
<div text="xs neutral-400">
|
||||
Started at {{ lastRecording?.startedAt.toFixed(0) }} ms, duration
|
||||
{{ (lastRecording!.stoppedAt - lastRecording!.startedAt).toFixed(0) }} ms
|
||||
</div>
|
||||
<div text="xs neutral-400">
|
||||
Samples:
|
||||
FPS {{ lastRecording?.samples.fps.length }},
|
||||
Frames {{ lastRecording?.samples.frameDuration.length }},
|
||||
Long tasks {{ lastRecording?.samples.longtask.length }},
|
||||
Memory {{ lastRecording?.samples.memory.length }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div text="xs neutral-500">
|
||||
Overlay is visible when any metric is enabled. Recording caps at 60s.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
layout: settings
|
||||
</route>
|
||||
@@ -14,6 +14,18 @@ const menu = computed(() => [
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/audio-record',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.system.sections.section.developer.sections.section.performance-visualizer.title'),
|
||||
description: t('settings.pages.system.sections.section.developer.sections.section.performance-visualizer.description'),
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/performance-visualizer',
|
||||
},
|
||||
{
|
||||
title: t('settings.pages.system.sections.section.developer.sections.section.markdown-stress.title'),
|
||||
description: t('settings.pages.system.sections.section.developer.sections.section.markdown-stress.description'),
|
||||
icon: 'i-solar:sledgehammer-bold-duotone',
|
||||
to: '/devtools/markdown-stress',
|
||||
},
|
||||
{
|
||||
title: 'Background Theme color blending',
|
||||
description: 'Test blending & theme',
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
import type { TraceEvent } from '@proj-airi/stage-shared'
|
||||
|
||||
import { defaultPerfTracer, exportCsv as exportCsvFile } from '@proj-airi/stage-shared'
|
||||
import { defineStore } from 'pinia'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
|
||||
import { createLagSampler } from '../composables/perf/register-lag-sampler'
|
||||
|
||||
export type LagMetric = 'fps' | 'frameDuration' | 'longtask' | 'memory'
|
||||
|
||||
interface Sample {
|
||||
ts: number
|
||||
value: number
|
||||
meta?: Record<string, any>
|
||||
}
|
||||
|
||||
interface RecordingSnapshot {
|
||||
startedAt: number
|
||||
stoppedAt: number
|
||||
samples: Record<LagMetric, Sample[]>
|
||||
}
|
||||
|
||||
interface HistogramBin {
|
||||
start: number
|
||||
end: number
|
||||
count: number
|
||||
}
|
||||
|
||||
function createEmptySamples(): Record<LagMetric, Sample[]> {
|
||||
return {
|
||||
fps: [],
|
||||
frameDuration: [],
|
||||
longtask: [],
|
||||
memory: [],
|
||||
}
|
||||
}
|
||||
|
||||
function pruneSamples(buffer: Sample[], cutoffTs: number) {
|
||||
while (buffer.length && buffer[0].ts < cutoffTs)
|
||||
buffer.shift()
|
||||
}
|
||||
|
||||
function calcStats(values: number[]) {
|
||||
if (!values.length)
|
||||
return { avg: 0, p95: 0, latest: 0 }
|
||||
|
||||
const total = values.reduce((acc, n) => acc + n, 0)
|
||||
const avg = total / values.length
|
||||
const sorted = [...values].sort((a, b) => a - b)
|
||||
const idx = Math.max(0, Math.floor(0.95 * (sorted.length - 1)))
|
||||
const p95 = sorted[idx]
|
||||
const latest = values[values.length - 1]
|
||||
|
||||
return { avg, p95, latest }
|
||||
}
|
||||
|
||||
function buildHistogram(values: number[], bins = 20): HistogramBin[] {
|
||||
if (!values.length)
|
||||
return []
|
||||
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
if (min === max) {
|
||||
return [{
|
||||
start: min,
|
||||
end: max || min + 1,
|
||||
count: values.length,
|
||||
}]
|
||||
}
|
||||
|
||||
const width = (max - min) / bins
|
||||
const buckets = Array.from({ length: bins }, (_, idx) => ({
|
||||
start: min + (idx * width),
|
||||
end: min + ((idx + 1) * width),
|
||||
count: 0,
|
||||
}))
|
||||
|
||||
for (const value of values) {
|
||||
let binIndex = Math.floor((value - min) / width)
|
||||
if (binIndex >= bins)
|
||||
binIndex = bins - 1
|
||||
|
||||
buckets[binIndex].count += 1
|
||||
}
|
||||
|
||||
return buckets
|
||||
}
|
||||
|
||||
export const useDevtoolsLagStore = defineStore('devtoolsLag', () => {
|
||||
const enabled = reactive({
|
||||
fps: false,
|
||||
frameDuration: false,
|
||||
longtask: false,
|
||||
memory: false,
|
||||
})
|
||||
|
||||
const windowMs = 10000
|
||||
const buffers = reactive(createEmptySamples())
|
||||
|
||||
const recording = ref(false)
|
||||
const recordingStartedAt = ref<number | null>(null)
|
||||
const recordingSamples = reactive(createEmptySamples())
|
||||
const lastRecording = ref<RecordingSnapshot>()
|
||||
let recordingTimeout: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const sampler = createLagSampler(defaultPerfTracer)
|
||||
let unsubscribeTracer: (() => void) | undefined
|
||||
let releaseTracer: (() => void) | undefined
|
||||
|
||||
function resetRecordingSamples() {
|
||||
for (const metric of Object.keys(recordingSamples) as LagMetric[])
|
||||
recordingSamples[metric] = []
|
||||
}
|
||||
|
||||
function applySample(metric: LagMetric, value: number, meta?: Record<string, any>) {
|
||||
const ts = performance.now()
|
||||
const cutoff = ts - windowMs
|
||||
|
||||
const buffer = buffers[metric]
|
||||
buffer.push({ ts, value, meta })
|
||||
pruneSamples(buffer, cutoff)
|
||||
|
||||
if (recording.value) {
|
||||
const sampleBuffer = recordingSamples[metric]
|
||||
sampleBuffer.push({ ts, value, meta })
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
if (recording.value)
|
||||
return
|
||||
|
||||
recording.value = true
|
||||
recordingStartedAt.value = performance.now()
|
||||
resetRecordingSamples()
|
||||
|
||||
recordingTimeout = setTimeout(() => stopRecording(), 60000)
|
||||
}
|
||||
|
||||
function stopRecording(): RecordingSnapshot | undefined {
|
||||
if (!recording.value)
|
||||
return
|
||||
|
||||
if (recordingTimeout) {
|
||||
clearTimeout(recordingTimeout)
|
||||
recordingTimeout = undefined
|
||||
}
|
||||
|
||||
const stoppedAt = performance.now()
|
||||
const snapshot: RecordingSnapshot = {
|
||||
startedAt: recordingStartedAt.value || stoppedAt,
|
||||
stoppedAt,
|
||||
samples: {
|
||||
fps: [...recordingSamples.fps],
|
||||
frameDuration: [...recordingSamples.frameDuration],
|
||||
longtask: [...recordingSamples.longtask],
|
||||
memory: [...recordingSamples.memory],
|
||||
},
|
||||
}
|
||||
lastRecording.value = snapshot
|
||||
|
||||
resetRecordingSamples()
|
||||
recordingStartedAt.value = null
|
||||
recording.value = false
|
||||
return snapshot
|
||||
}
|
||||
|
||||
function stopAll() {
|
||||
sampler.stop()
|
||||
if (unsubscribeTracer) {
|
||||
unsubscribeTracer()
|
||||
unsubscribeTracer = undefined
|
||||
}
|
||||
releaseTracer?.()
|
||||
releaseTracer = undefined
|
||||
}
|
||||
|
||||
function ensureSampler() {
|
||||
const anyEnabled = enabled.fps || enabled.frameDuration || enabled.longtask || enabled.memory
|
||||
if (!anyEnabled) {
|
||||
stopAll()
|
||||
return
|
||||
}
|
||||
|
||||
// Start tracer listener if needed
|
||||
if (!unsubscribeTracer) {
|
||||
unsubscribeTracer = defaultPerfTracer.subscribe((event: TraceEvent) => {
|
||||
if (event.tracerId !== 'lag')
|
||||
return
|
||||
|
||||
const metric = event.name as LagMetric
|
||||
if (!['fps', 'frameDuration', 'longtask', 'memory'].includes(metric))
|
||||
return
|
||||
|
||||
// Only accept samples for enabled metrics
|
||||
const isMetricEnabled = enabled[metric]
|
||||
|
||||
if (!isMetricEnabled)
|
||||
return
|
||||
|
||||
const value = typeof event.duration === 'number' ? event.duration : 0
|
||||
applySample(metric, value, event.meta)
|
||||
})
|
||||
}
|
||||
|
||||
if (!releaseTracer)
|
||||
releaseTracer = defaultPerfTracer.acquire('lag-overlay')
|
||||
sampler.start({
|
||||
fps: enabled.fps,
|
||||
frameDuration: enabled.frameDuration,
|
||||
longtask: enabled.longtask,
|
||||
memory: enabled.memory,
|
||||
})
|
||||
}
|
||||
|
||||
function toggleAll(on: boolean) {
|
||||
enabled.fps = on
|
||||
enabled.frameDuration = on
|
||||
enabled.longtask = on
|
||||
enabled.memory = on
|
||||
ensureSampler()
|
||||
}
|
||||
|
||||
function exportCsv(snapshot?: RecordingSnapshot) {
|
||||
const target = snapshot ?? lastRecording.value
|
||||
if (!target)
|
||||
return
|
||||
|
||||
const rows: Array<Array<string | number>> = [['metric', 'ts', 'value', 'meta']]
|
||||
for (const metric of Object.keys(target.samples) as LagMetric[]) {
|
||||
for (const sample of target.samples[metric]) {
|
||||
rows.push([
|
||||
metric,
|
||||
sample.ts.toFixed(3),
|
||||
sample.value,
|
||||
JSON.stringify(sample.meta ?? {}),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
exportCsvFile(rows, 'lag-recording')
|
||||
}
|
||||
|
||||
// React to enablement changes
|
||||
watch(
|
||||
() => ({ ...enabled }),
|
||||
() => {
|
||||
ensureSampler()
|
||||
},
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
// Cleanup on tab close
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
stopRecording()
|
||||
stopAll()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
buffers,
|
||||
recording,
|
||||
lastRecording,
|
||||
startRecording,
|
||||
stopRecording,
|
||||
exportCsv,
|
||||
toggleAll,
|
||||
calcStats,
|
||||
buildHistogram,
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user