const FRAME_BUDGET_MS = 1000 / 60; export interface CartesiumPerformanceReport { scenario: string; durationMs: number; drawFrames: number; tilePublications: number; retentionPasses: number; estimatedMissedFrames: number; worstFrameIntervalMs: number; blank: { episodes: number; totalMs: number; worstMs: number; worstRatio: number; recoveredBy: { tiles: number; scene: number; mixed: number; viewport: number; unresolved: number }; workerOverlap: { scene: { completions: number; busyMs: number }; tile: { completions: number; busyMs: number }; }; timeline: Array<{ startedMs: number; durationMs: number; peakRatio: number; recoveredBy: 'tiles' | 'scene' | 'mixed' | 'viewport' | 'unresolved'; demand?: { level: number; visibleHoleCount: number; queuedCoverageCount: number; queuedVisibleCount: number; queuedReserveCount: number; queuedPrefetchCount: number; crossLevelCacheHits: number; }; }>; }; presentation: MetricSummary & { computed: MetricSummary; cacheReuse: MetricSummary; intervals: { receiveToCache: MetricSummary; cacheToPublish: MetricSummary; availableToPublish: MetricSummary; publishToRetain: MetricSummary; retainToPaint: MetricSummary; }; }; presentedFill: MetricSummary & { computeToPaint: MetricSummary }; mainThread: { draw: MetricSummary; retain: MetricSummary; sceneRetain: MetricSummary; tileRetain: MetricSummary; expressionResize: MetricSummary; }; animation: { ticks: number; valueUpdates: number; publishedUpdates: number; singleVariableUpdates: number; multiVariableUpdates: number; singleVariablePublications: number; multiVariablePublications: number; singleVariablePublicationsWhileMultipleActive: number; peakActiveVariables: number; interval: MetricSummary; tickCost: MetricSummary; }; scene: { builds: number; coalescedRequests: number; reusedLayers: number; peakPackedGeometryBytes: number; slowestBuilds: Array<{ buildMs: number; viewport: { width: number; height: number; xMin: number; xMax: number; yMin: number; yMax: number; }; reusedLayers: number; cachedLayers: number; coalescedRequests: number; }>; presentation: MetricSummary; paintInterval: MetricSummary; intervals: { clientQueue: MetricSummary; workerQueue: MetricSummary; build: MetricSummary; packing: MetricSummary; transfer: MetricSummary; roundTrip: MetricSummary; }; }; polarScene: { builds: number; presentation: MetricSummary; quality: { refined: { build: MetricSummary; presentation: MetricSummary }; preview: { build: MetricSummary; presentation: MetricSummary }; }; geometry: { peakBoundaryVertices: number; peakFillVertices: number; peakCoordinateBytes: number; vertexBudgetReachedBuilds: number; maxDepthReachedBuilds: number; peakSampleEvaluations: number; peakRefinedIntervals: number; peakVisibleRefinedIntervals: number; peakOffscreenRefinedIntervals: number; peakDeferredOffscreenIntervals: number; peakCandidateIntervals: number; }; intervals: { clientQueue: MetricSummary; workerQueue: MetricSummary; build: MetricSummary; packing: MetricSummary; transfer: MetricSummary; receiveToPublish: MetricSummary; publishToRetainStart: MetricSummary; retention: MetricSummary; retainToPaintStart: MetricSummary; draw: MetricSummary; }; }; demand: { count: number; peakVisibleHoles: number; peakQueuedCoverage: number; peakQueuedVisible: number; peakQueuedReserve: number; peakQueuedPrefetch: number; peakQueuedTotal: number; activeRetained: { offscreen: number; staleModel: number }; }; resources: { peakPackedGeometryBytes: number; peakTileCacheBytes: number; peakSceneCachedLayers: number; }; workerUtilization: { scene: number; tile: number }; } interface MetricSummary { count: number; medianMs: number | null; worstMs: number | null; } interface ObservedMeasure { name: string; startTime: number; duration: number; detail: Record; observedAt: number; } interface BlankEpisode { startedAt: number; endedAt?: number; peakRatio: number; startTileRatio: number; startSceneRatio: number; lastTileRatio: number; lastSceneRatio: number; } interface PerformanceSession { scenario: string; startedAt: number; observer: PerformanceObserver; measures: ObservedMeasure[]; frameIntervals: number[]; lastFrameAt?: number; frameRequest: number; blankEpisode?: BlankEpisode; blankEpisodes: BlankEpisode[]; } let activeSession: PerformanceSession | undefined; export function startPerformanceSession(scenario: string) { if (activeSession) stopPerformanceSession(); const startedAt = performance.now(); let session!: PerformanceSession; session = { scenario, startedAt, observer: new PerformanceObserver((list) => observeMeasures(session, list.getEntries(), performance.now()) ), measures: [] as ObservedMeasure[], frameIntervals: [] as number[], frameRequest: 0, blankEpisodes: [] as BlankEpisode[] }; const sampleFrame = (now: number) => { if (session.lastFrameAt !== undefined) session.frameIntervals.push(now - session.lastFrameAt); session.lastFrameAt = now; session.frameRequest = requestAnimationFrame(sampleFrame); }; session.observer.observe({ type: 'measure' }); session.frameRequest = requestAnimationFrame(sampleFrame); activeSession = session; return scenario; } export function stopPerformanceSession(): CartesiumPerformanceReport | undefined { const session = activeSession; if (!session) return undefined; activeSession = undefined; const stoppedAt = performance.now(); cancelAnimationFrame(session.frameRequest); observeMeasures(session, session.observer.takeRecords(), stoppedAt); session.observer.disconnect(); if (session.blankEpisode) { session.blankEpisodes.push(session.blankEpisode); session.blankEpisode = undefined; } const durationMs = Math.max(0, stoppedAt - session.startedAt); const named = (name: string) => session.measures.filter((entry) => entry.name === name); const presentationEntries = named('cartesium:tile-presentation'); const sceneEntries = named('cartesium:scene'); const slowestSceneBuilds = [...sceneEntries] .sort( (left, right) => Number(right.detail.buildMs ?? 0) - Number(left.detail.buildMs ?? 0) ) .slice(0, 3) .map((entry) => { const viewport = typeof entry.detail.viewport === 'object' && entry.detail.viewport !== null ? (entry.detail.viewport as Record) : {}; return { buildMs: Number(entry.detail.buildMs ?? 0), viewport: { width: Number(viewport.width ?? 0), height: Number(viewport.height ?? 0), xMin: Number(viewport.x_min ?? 0), xMax: Number(viewport.x_max ?? 0), yMin: Number(viewport.y_min ?? 0), yMax: Number(viewport.y_max ?? 0) }, reusedLayers: Number(entry.detail.reusedLayerCount ?? 0), cachedLayers: Number(entry.detail.cachedLayerEntryCount ?? 0), coalescedRequests: Number(entry.detail.coalescedBuildCount ?? 0) }; }); const polarPresentationEntries = named('cartesium:polar-presentation'); const scenePresentationEntries = named('cartesium:scene-presentation'); const scenePaintTimes = scenePresentationEntries .map((entry) => Number(entry.detail.paintedAt)) .filter(Number.isFinite) .sort((left, right) => left - right); const polarSceneEntries = sceneEntries.filter( (entry) => Number(entry.detail.polarExpressionCount ?? 0) > 0 ); const refinedPolarScenes = polarSceneEntries.filter( (entry) => Number(entry.detail.gridCellSize ?? 1) <= 1 ); const previewPolarScenes = polarSceneEntries.filter( (entry) => Number(entry.detail.gridCellSize ?? 1) > 1 ); const refinedPolarPresentations = polarPresentationEntries.filter( (entry) => Number(entry.detail.gridCellSize ?? 1) <= 1 ); const previewPolarPresentations = polarPresentationEntries.filter( (entry) => Number(entry.detail.gridCellSize ?? 1) > 1 ); const presentationTimes = presentationEntries.map((entry) => Number(entry.detail.availableToPaintMs ?? entry.duration) ); const presentationBySource = (cached: boolean) => presentationEntries .filter((entry) => String(entry.detail.source ?? '').includes('cache') === cached) .map((entry) => Number(entry.detail.availableToPaintMs ?? entry.duration)); const presentedFillEntries = named('cartesium:tile-presented-fill'); const presentedFillTimes = presentedFillEntries.map((entry) => Number(entry.detail.presentedFillMs ?? entry.duration) ); const workerBusyMs = (name: string) => named(name).reduce((total, entry) => total + Number(entry.detail.buildMs ?? 0), 0); const demandEntries = named('cartesium:tile-demand'); const animationEntries = named('cartesium:animation-tick'); const sceneRetainEntries = named('cartesium:scene-retain'); const tileRetainEntries = named('cartesium:tile-retain'); const blankDurations = session.blankEpisodes.map( (episode) => (episode.endedAt ?? stoppedAt) - episode.startedAt ); const recoveredBy = { tiles: 0, scene: 0, mixed: 0, viewport: 0, unresolved: 0 }; for (const episode of session.blankEpisodes) recoveredBy[classifyBlankRecovery(episode)] += 1; const blankWorkerOverlap = (name: string) => { const entries = named(name); const buildInterval = (entry: ObservedMeasure) => { const queueMs = Number(entry.detail.queueMs ?? 0); const buildMs = Number(entry.detail.buildMs ?? entry.duration); const start = entry.startTime + queueMs; return { start, end: start + buildMs }; }; return { completions: entries.filter((entry) => { const build = buildInterval(entry); return session.blankEpisodes.some( (episode) => build.end >= episode.startedAt && build.end <= (episode.endedAt ?? stoppedAt) ); }).length, busyMs: session.blankEpisodes.reduce( (total, episode) => total + entries.reduce( (sum, entry) => { const build = buildInterval(entry); return sum + intervalOverlap( build.start, build.end, episode.startedAt, episode.endedAt ?? stoppedAt ); }, 0 ), 0 ) }; }; const peakDemandDetail = (key: string) => Math.max(0, ...demandEntries.map((entry) => Number(entry.detail[key] ?? 0))); const blankTimeline = session.blankEpisodes.map((episode) => { const demand = demandEntries.findLast((entry) => entry.startTime <= episode.startedAt); const episodeReport: CartesiumPerformanceReport['blank']['timeline'][number] = { startedMs: episode.startedAt - session.startedAt, durationMs: (episode.endedAt ?? stoppedAt) - episode.startedAt, peakRatio: episode.peakRatio, recoveredBy: classifyBlankRecovery(episode) }; if (demand) { episodeReport.demand = { level: Number(demand.detail.level ?? 0), visibleHoleCount: Number(demand.detail.visibleHoleCount ?? 0), queuedCoverageCount: Number(demand.detail.queuedCoverageCount ?? 0), queuedVisibleCount: Number(demand.detail.queuedVisibleCount ?? 0), queuedReserveCount: Number(demand.detail.queuedReserveCount ?? 0), queuedPrefetchCount: Number(demand.detail.queuedPrefetchCount ?? 0), crossLevelCacheHits: Number(demand.detail.crossLevelCacheHits ?? 0) }; } return episodeReport; }); const report: CartesiumPerformanceReport = { scenario: session.scenario, durationMs, drawFrames: named('cartesium:canvas-draw').length, tilePublications: named('cartesium:tile-publication').length, retentionPasses: named('cartesium:tile-retain').length, estimatedMissedFrames: session.frameIntervals.reduce( (total, interval) => total + Math.max(0, Math.round(interval / FRAME_BUDGET_MS) - 1), 0 ), worstFrameIntervalMs: Math.max(0, ...session.frameIntervals), blank: { episodes: session.blankEpisodes.length, totalMs: sum(blankDurations), worstMs: Math.max(0, ...blankDurations), worstRatio: Math.max(0, ...session.blankEpisodes.map(({ peakRatio }) => peakRatio)), recoveredBy, workerOverlap: { scene: blankWorkerOverlap('cartesium:scene'), tile: blankWorkerOverlap('cartesium:spatial-tile') }, timeline: blankTimeline }, presentation: { ...summarize(presentationTimes), computed: summarize(presentationBySource(false)), cacheReuse: summarize(presentationBySource(true)), intervals: { receiveToCache: summarizeDetail(presentationEntries, 'receiveToCacheMs'), cacheToPublish: summarizeDetail(presentationEntries, 'cacheToPublishMs'), availableToPublish: summarizeDetail(presentationEntries, 'availableToPublishMs'), publishToRetain: summarizeDetail(presentationEntries, 'publishToRetainMs'), retainToPaint: summarizeDetail(presentationEntries, 'retainToPaintMs') } }, presentedFill: { ...summarize(presentedFillTimes), computeToPaint: summarizeDetail(presentedFillEntries, 'computeToPaintMs') }, mainThread: { draw: summarize(named('cartesium:canvas-draw').map((entry) => entry.duration)), retain: summarize( [...sceneRetainEntries, ...tileRetainEntries].map((entry) => entry.duration) ), sceneRetain: summarize(sceneRetainEntries.map((entry) => entry.duration)), tileRetain: summarize(tileRetainEntries.map((entry) => entry.duration)), expressionResize: summarize( named('cartesium:expression-resize').map((entry) => entry.duration) ) }, animation: { ticks: animationEntries.length, valueUpdates: animationEntries.filter( (entry) => Number(entry.detail.changedVariableCount ?? 0) > 0 ).length, publishedUpdates: animationEntries.filter( (entry) => Number(entry.detail.publishedVariableCount ?? 0) > 0 ).length, singleVariableUpdates: animationEntries.filter( (entry) => Number(entry.detail.changedVariableCount ?? 0) === 1 ).length, multiVariableUpdates: animationEntries.filter( (entry) => Number(entry.detail.changedVariableCount ?? 0) > 1 ).length, singleVariablePublications: animationEntries.filter( (entry) => Number(entry.detail.publishedVariableCount ?? 0) === 1 ).length, multiVariablePublications: animationEntries.filter( (entry) => Number(entry.detail.publishedVariableCount ?? 0) > 1 ).length, singleVariablePublicationsWhileMultipleActive: animationEntries.filter( (entry) => Number(entry.detail.activeVariableCount ?? 0) > 1 && Number(entry.detail.publishedVariableCount ?? 0) === 1 ).length, peakActiveVariables: peakDetail(animationEntries, 'activeVariableCount'), interval: summarizeDetail(animationEntries, 'intervalMs'), tickCost: summarize(animationEntries.map((entry) => entry.duration)) }, scene: { builds: sceneEntries.length, coalescedRequests: sceneEntries.reduce( (total, entry) => total + Number(entry.detail.coalescedBuildCount ?? 0), 0 ), reusedLayers: sceneEntries.reduce( (total, entry) => total + Number(entry.detail.reusedLayerCount ?? 0), 0 ), peakPackedGeometryBytes: peakDetail(sceneEntries, 'packedGeometryBytes'), slowestBuilds: slowestSceneBuilds, presentation: summarizeDetail(scenePresentationEntries, 'requestToPaintMs'), paintInterval: summarize( scenePaintTimes.slice(1).map((paintedAt, index) => paintedAt - scenePaintTimes[index]) ), intervals: { clientQueue: summarizeDetail(sceneEntries, 'clientQueueMs'), workerQueue: summarizeDetail(sceneEntries, 'queueMs'), build: summarizeDetail(sceneEntries, 'buildMs'), packing: summarizeDetail(sceneEntries, 'packingMs'), transfer: summarizeDetail(sceneEntries, 'transferMs'), roundTrip: summarizeDetail(sceneEntries, 'roundTripMs') } }, polarScene: { builds: polarSceneEntries.length, presentation: summarizeDetail(polarPresentationEntries, 'requestToPaintMs'), quality: { refined: { build: summarizeDetail(refinedPolarScenes, 'buildMs'), presentation: summarizeDetail(refinedPolarPresentations, 'requestToPaintMs') }, preview: { build: summarizeDetail(previewPolarScenes, 'buildMs'), presentation: summarizeDetail(previewPolarPresentations, 'requestToPaintMs') } }, geometry: { peakBoundaryVertices: peakDetail(polarSceneEntries, 'polarBoundaryVertexCount'), peakFillVertices: peakDetail(polarSceneEntries, 'polarFillVertexCount'), peakCoordinateBytes: peakDetail(polarSceneEntries, 'polarCoordinateBytes'), vertexBudgetReachedBuilds: polarSceneEntries.filter( (entry) => entry.detail.polarVertexBudgetReached === true ).length, maxDepthReachedBuilds: polarSceneEntries.filter( (entry) => entry.detail.polarMaxDepthReached === true ).length, peakSampleEvaluations: peakDetail(polarSceneEntries, 'polarSampleEvaluations'), peakRefinedIntervals: peakDetail(polarSceneEntries, 'polarRefinedIntervals'), peakVisibleRefinedIntervals: peakDetail( polarSceneEntries, 'polarVisibleRefinedIntervals' ), peakOffscreenRefinedIntervals: peakDetail( polarSceneEntries, 'polarOffscreenRefinedIntervals' ), peakDeferredOffscreenIntervals: peakDetail( polarSceneEntries, 'polarDeferredOffscreenIntervals' ), peakCandidateIntervals: peakDetail( polarSceneEntries, 'polarPeakCandidateIntervals' ) }, intervals: { clientQueue: summarizeDetail(polarSceneEntries, 'clientQueueMs'), workerQueue: summarizeDetail(polarSceneEntries, 'queueMs'), build: summarizeDetail(polarSceneEntries, 'buildMs'), packing: summarizeDetail(polarSceneEntries, 'packingMs'), transfer: summarizeDetail(polarSceneEntries, 'transferMs'), receiveToPublish: summarizeDetail(polarPresentationEntries, 'receiveToPublishMs'), publishToRetainStart: summarizeDetail( polarPresentationEntries, 'publishToRetainStartMs' ), retention: summarizeDetail(polarPresentationEntries, 'retentionMs'), retainToPaintStart: summarizeDetail( polarPresentationEntries, 'retainToPaintStartMs' ), draw: summarizeDetail(polarPresentationEntries, 'drawMs') } }, demand: { count: demandEntries.length, peakVisibleHoles: peakDemandDetail('visibleHoleCount'), peakQueuedCoverage: peakDemandDetail('queuedCoverageCount'), peakQueuedVisible: peakDemandDetail('queuedVisibleCount'), peakQueuedReserve: peakDemandDetail('queuedReserveCount'), peakQueuedPrefetch: peakDemandDetail('queuedPrefetchCount'), peakQueuedTotal: Math.max( 0, ...demandEntries.map( (entry) => Number(entry.detail.queuedCoverageCount ?? 0) + Number(entry.detail.queuedVisibleCount ?? 0) + Number(entry.detail.queuedReserveCount ?? 0) + Number(entry.detail.queuedPrefetchCount ?? 0) ) ), activeRetained: { offscreen: demandEntries.filter( (entry) => entry.detail.activeDisposition === 'offscreen-retained' ).length, staleModel: demandEntries.filter( (entry) => entry.detail.activeDisposition === 'stale-model-retained' ).length } }, resources: { peakPackedGeometryBytes: peakDetail(sceneEntries, 'packedGeometryBytes'), peakTileCacheBytes: peakDemandDetail('cachedBytes'), peakSceneCachedLayers: peakDetail(sceneEntries, 'cachedLayerEntryCount') }, workerUtilization: { scene: durationMs === 0 ? 0 : workerBusyMs('cartesium:scene') / durationMs, tile: durationMs === 0 ? 0 : workerBusyMs('cartesium:spatial-tile') / durationMs } }; console.table(report); return report; } export function installPerformanceHarness(loadPreset?: (presetId: string) => boolean) { const harness: NonNullable = { start: startPerformanceSession, stop: stopPerformanceSession }; if (loadPreset) { harness.loadPreset = loadPreset; } window.cartesiumPerf = harness; return () => { if (activeSession) stopPerformanceSession(); delete window.cartesiumPerf; }; } function summarize(values: number[]) { const sorted = [...values].sort((left, right) => left - right); return { count: sorted.length, medianMs: sorted.length === 0 ? null : sorted[Math.floor(sorted.length / 2)], worstMs: sorted.length === 0 ? null : sorted[sorted.length - 1] }; } function sum(values: number[]) { return values.reduce((total, value) => total + value, 0); } function summarizeDetail(entries: ObservedMeasure[], key: string) { return summarize( entries .map((entry) => entry.detail[key]) .filter((value): value is number => typeof value === 'number') ); } function peakDetail(entries: ObservedMeasure[], key: string) { return Math.max(0, ...entries.map((entry) => Number(entry.detail[key] ?? 0))); } function observeMeasures( session: PerformanceSession, entries: PerformanceEntry[], observedAt: number ) { for (const entry of entries as PerformanceMeasure[]) { const detail = (entry.detail ?? {}) as Record; session.measures.push({ name: entry.name, startTime: entry.startTime, duration: entry.duration, detail, observedAt }); if (entry.name !== 'cartesium:canvas-draw') continue; const blankRatio = Number(detail.spatialBlankRatio ?? 0); const fallbackRatio = Number(detail.spatialFallbackRatio ?? 0); const tileRatio = Math.max(0, 1 - fallbackRatio); const sceneRatio = Math.max(0, fallbackRatio - blankRatio); const drawnAt = entry.startTime + entry.duration; if (blankRatio > 0 && !session.blankEpisode) { session.blankEpisode = { startedAt: drawnAt, peakRatio: blankRatio, startTileRatio: tileRatio, startSceneRatio: sceneRatio, lastTileRatio: tileRatio, lastSceneRatio: sceneRatio }; } else if (session.blankEpisode) { session.blankEpisode.peakRatio = Math.max(session.blankEpisode.peakRatio, blankRatio); session.blankEpisode.lastTileRatio = tileRatio; session.blankEpisode.lastSceneRatio = sceneRatio; if (blankRatio === 0) closeBlankEpisode(session, drawnAt); } } } function closeBlankEpisode(session: PerformanceSession, endedAt: number) { const episode = session.blankEpisode; if (!episode) return; episode.endedAt = endedAt; session.blankEpisodes.push(episode); session.blankEpisode = undefined; } function classifyBlankRecovery(episode: BlankEpisode): keyof CartesiumPerformanceReport['blank']['recoveredBy'] { if (episode.endedAt === undefined) return 'unresolved'; const epsilon = 1e-6; const tileGain = episode.lastTileRatio - episode.startTileRatio > epsilon; const sceneGain = episode.lastSceneRatio - episode.startSceneRatio > epsilon; if (tileGain && sceneGain) return 'mixed'; if (tileGain) return 'tiles'; if (sceneGain) return 'scene'; return 'viewport'; } function intervalOverlap(leftStart: number, leftEnd: number, rightStart: number, rightEnd: number) { return Math.max(0, Math.min(leftEnd, rightEnd) - Math.max(leftStart, rightStart)); }