Something went wrong. Try again.
[PRE-ALPHA] an open source web-based 2D/3D graphing calculator cartesium.org
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718const 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<string, unknown>; 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<string, unknown>) : {};
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<Window['cartesiumPerf']> = { 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<string, unknown>; 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));}