Files
tolaria/src/utils/noteOpenPerformance.ts

113 lines
4.0 KiB
TypeScript
Raw Normal View History

type NoteOpenStage =
| 'beforeNavigateStart'
| 'beforeNavigateEnd'
| 'cacheReady'
2026-04-29 10:08:33 +02:00
| 'freshnessCheckStart'
| 'freshnessCheckEnd'
| 'contentLoadStart'
| 'contentLoadEnd'
| 'editorSwapped'
interface NoteOpenTrace {
startedAt: number
source: string
marks: Partial<Record<NoteOpenStage, number>>
}
const inFlightNoteOpens = new Map<string, NoteOpenTrace>()
function isVitestRuntime(): boolean {
return '__vitest_worker__' in globalThis
}
function canMeasurePerformance(): boolean {
return import.meta.env.DEV && typeof performance !== 'undefined' && !isVitestRuntime()
}
function formatDuration(durationMs: number | null): string {
return durationMs === null ? 'n/a' : `${durationMs.toFixed(1)}ms`
}
function measureDuration(
trace: NoteOpenTrace,
start: keyof NoteOpenTrace['marks'] | 'startedAt',
end: keyof NoteOpenTrace['marks'],
): number | null {
const startTime = start === 'startedAt' ? trace.startedAt : Reflect.get(trace.marks, start) as number | undefined
const endTime = Reflect.get(trace.marks, end) as number | undefined
if (startTime === undefined || endTime === undefined) return null
return endTime - startTime
}
function logPerf(message: string): void {
if (!canMeasurePerformance()) return
console.debug(`[perf] ${message}`)
}
export function beginNoteOpenTrace(path: string, source: string): void {
if (!canMeasurePerformance()) return
const startedAt = performance.now()
for (const [otherPath, trace] of inFlightNoteOpens) {
if (otherPath === path) continue
logPerf(`noteOpen cancel path=${otherPath} source=${trace.source} total=${formatDuration(startedAt - trace.startedAt)} reason=superseded`)
inFlightNoteOpens.delete(otherPath)
}
inFlightNoteOpens.set(path, { startedAt, source, marks: {} })
}
export function markNoteOpenTrace(path: string, stage: NoteOpenStage): void {
if (!canMeasurePerformance()) return
const trace = inFlightNoteOpens.get(path)
if (!trace) return
Reflect.set(trace.marks, stage, performance.now())
}
export function finishNoteOpenTrace(path: string): void {
if (!canMeasurePerformance()) return
const trace = inFlightNoteOpens.get(path)
if (!trace) return
Reflect.set(trace.marks, 'editorSwapped', performance.now())
const editorSwappedAt = Reflect.get(trace.marks, 'editorSwapped') as number
const total = editorSwappedAt - trace.startedAt
const beforeNavigate = measureDuration(trace, 'beforeNavigateStart', 'beforeNavigateEnd')
2026-04-29 10:08:33 +02:00
const freshnessCheck = measureDuration(trace, 'freshnessCheckStart', 'freshnessCheckEnd')
const contentLoad = measureDuration(trace, 'contentLoadStart', 'contentLoadEnd')
const editorSwap = measureDuration(trace, 'contentLoadEnd', 'editorSwapped')
2026-04-29 10:08:33 +02:00
?? measureDuration(trace, 'freshnessCheckEnd', 'editorSwapped')
?? measureDuration(trace, 'beforeNavigateEnd', 'editorSwapped')
?? measureDuration(trace, 'startedAt', 'editorSwapped')
logPerf(
`noteOpen path=${path} source=${trace.source} total=${formatDuration(total)} `
+ `beforeNavigate=${formatDuration(beforeNavigate)} `
2026-04-29 10:08:33 +02:00
+ `freshnessCheck=${formatDuration(freshnessCheck)} `
+ `contentLoad=${formatDuration(contentLoad)} `
+ `editorSwap=${formatDuration(editorSwap)} `
+ `cache=${Reflect.get(trace.marks, 'cacheReady') !== undefined ? 'hit' : 'miss'}`,
)
inFlightNoteOpens.delete(path)
}
export function failNoteOpenTrace(path: string, reason: string): void {
if (!canMeasurePerformance()) return
const trace = inFlightNoteOpens.get(path)
if (!trace) return
const total = performance.now() - trace.startedAt
logPerf(`noteOpen cancel path=${path} source=${trace.source} total=${formatDuration(total)} reason=${reason}`)
inFlightNoteOpens.delete(path)
}
export function logKeyboardNavigationTrace(
direction: 'up' | 'down',
itemCount: number,
durationMs: number,
): void {
if (!canMeasurePerformance()) return
if (itemCount < 500 && durationMs < 4) return
logPerf(`noteListKeyboard direction=${direction} items=${itemCount} move=${formatDuration(durationMs)}`)
}