Add profiler

This commit is contained in:
2026-07-22 01:03:27 +10:00
parent db864af147
commit c27e5906ca
12 changed files with 634 additions and 4 deletions

View File

@@ -13,9 +13,12 @@ import {
type NavigationFailure,
type RouteLocationNormalizedLoaded,
type RouteLocationRaw,
type RouteLocationResolved,
} from 'vue-router'
import type {
NativeDirection,
NativeDiagnosticEvent,
NativeDiagnosticEventType,
NativeEvictionReason,
NativeGestureKind,
NativeNavigationOptions,
@@ -36,6 +39,12 @@ function now() {
return typeof performance === 'undefined' ? Date.now() : performance.now()
}
function diagnosticRoute(route: RouteLocationNormalizedLoaded | RouteLocationResolved) {
return route.name != null
? String(route.name)
: route.matched.at(-1)?.path ?? route.path
}
function entryFor(route: RouteLocationNormalizedLoaded, status: NativeViewEntry['status'], synthetic = false): NativeViewEntry {
return {
key: `${route.fullPath}::${++entrySequence}`,
@@ -101,6 +110,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
private totalEvictions = 0
private lastEviction?: { key: string; route: string; reason: NativeEvictionReason }
private memoryPressureCleanup?: () => void
private readonly diagnosticListeners = new Set<(event: NativeDiagnosticEvent) => void>()
constructor(options: NativeRouterOptions) {
this.router = options.router
@@ -171,7 +181,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
async preload(to: RouteLocationRaw) {
const resolved = this.router.resolve(to)
return await loadRouteLocation(resolved)
return await this.loadResolvedRoute(resolved)
}
async push(to: RouteLocationRaw, options: NativeNavigationOptions = {}) {
@@ -237,14 +247,14 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
const parent = from.route.meta.native?.parent
if (!parent) return null
const parentLocation = typeof parent === 'function' ? parent(from.route) : parent
const route = await this.preload(parentLocation)
const route = await this.loadResolvedRoute(this.router.resolve(parentLocation), attempt)
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
target = entryFor(route, 'preview', true)
synthetic = true
needsMount = true
this.mutableEntries.value = [...this.mutableEntries.value, target]
} else if (!target.mounted) {
const route = await this.preload(target.route.fullPath)
const route = await this.loadResolvedRoute(this.router.resolve(target.route.fullPath), attempt)
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
target.route = route
target.mounted = true
@@ -257,7 +267,7 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
if (!to) return null
const resolved = this.router.resolve(to)
if (resolved.fullPath === from.route.fullPath) return null
const route = await loadRouteLocation(resolved)
const route = await this.loadResolvedRoute(resolved, attempt)
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
const replace = options.replace ?? (route.meta.native?.siblingHistory === 'replace')
target = replace ? this.findHistoryEntry(route.fullPath) : undefined
@@ -307,9 +317,32 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
}
if (attempt !== this.beginAttemptSequence || this.mutableTransaction.value) return null
this.mutableTransaction.value = transaction
this.emitDiagnostic('transaction-start', {
attempt,
transactionId: transaction.id,
route: diagnosticRoute(target.route),
details: {
kind: transaction.kind,
direction: transaction.direction,
presentation: transaction.presentation,
cold: needsMount,
},
})
if (synthetic) target.synthetic = true
if (needsMount) {
const prepareStarted = now()
this.emitDiagnostic('view-prepare-start', {
attempt,
transactionId: transaction.id,
route: diagnosticRoute(target.route),
})
await this.prepareMountedView()
this.emitDiagnostic('view-prepare-end', {
attempt,
transactionId: transaction.id,
route: diagnosticRoute(target.route),
duration: now() - prepareStarted,
})
if (!this.isCurrent(transaction.id)) return null
}
return transaction.id
@@ -335,6 +368,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
}
this.mutableTransaction.value = { ...current, phase: 'committing' }
this.emitDiagnostic('commit-start', {
transactionId: current.id,
route: this.entryByKey(current.toKey) ? diagnosticRoute(this.entryByKey(current.toKey)!.route) : undefined,
})
void this.platform?.haptic?.('commit')
const navigation = this.commitRoute(current)
this.pendingNavigations.set(current.id, navigation)
@@ -390,6 +427,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
this.touchEntries()
}
onDiagnostic(listener: (event: NativeDiagnosticEvent) => void) {
this.diagnosticListeners.add(listener)
return () => this.diagnosticListeners.delete(listener)
}
registerPresentation(definition: NativePresentationDefinition) {
this.presentations.set(definition.name, definition)
}
@@ -485,6 +527,12 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
private finalizeCancelled(transaction: NativeTransaction, evictTarget = false) {
if (!this.isCurrent(transaction.id)) return
const target = this.entryByKey(transaction.toKey)
this.emitDiagnostic('transaction-end', {
transactionId: transaction.id,
route: target ? diagnosticRoute(target.route) : undefined,
details: { outcome: evictTarget ? 'rejected' : 'cancelled' },
})
this.clearTransaction()
if (evictTarget) this.discardTarget(transaction.toKey, 'navigation-rejected')
else this.removePreview(transaction.toKey)
@@ -497,6 +545,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
const target = this.entryByKey(transaction.toKey)
if (target && this.router.currentRoute.value.fullPath !== target.route.fullPath) {
// A redirect is already authoritative; discard only the stale preview.
this.emitDiagnostic('transaction-end', {
transactionId: transaction.id,
route: diagnosticRoute(target.route),
details: { outcome: 'redirected' },
})
this.clearTransaction()
this.discardTarget(transaction.toKey, 'navigation-rejected')
this.markStatuses()
@@ -510,6 +563,11 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
target.lastUsed = now()
this.mutableActiveKey.value = target.key
}
this.emitDiagnostic('transaction-end', {
transactionId: transaction.id,
route: target ? diagnosticRoute(target.route) : undefined,
details: { outcome: 'committed' },
})
this.clearTransaction()
this.markStatuses()
this.enforceCache()
@@ -643,6 +701,10 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
entry.evictionReason = reason
this.totalEvictions += 1
this.lastEviction = { key: entry.key, route: entry.route.fullPath, reason }
this.emitDiagnostic('view-evicted', {
route: diagnosticRoute(entry.route),
details: { reason },
})
}
private shouldRetainInactive(entry: NativeViewEntry) {
@@ -679,6 +741,30 @@ class NativeRouterRuntimeImpl implements NativeRouterRuntime {
await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
}
private async loadResolvedRoute(route: RouteLocationResolved, attempt?: number) {
const started = now()
const label = diagnosticRoute(route)
this.emitDiagnostic('route-load-start', { attempt, route: label })
try {
return await loadRouteLocation(route)
} finally {
this.emitDiagnostic('route-load-end', {
attempt,
route: label,
duration: now() - started,
})
}
}
private emitDiagnostic(
type: NativeDiagnosticEventType,
event: Omit<NativeDiagnosticEvent, 'type' | 'timestamp'> = {},
) {
if (!this.diagnosticListeners.size) return
const diagnostic = { type, timestamp: now(), ...event }
for (const listener of this.diagnosticListeners) listener(diagnostic)
}
private animateProgress(target: number, initialVelocity: number) {
const transaction = this.mutableTransaction.value
if (!transaction) return Promise.resolve()