| /** | |
| * DEV-only render-frame timing attribution for DeckGLMap (#4558 / #4537). | |
| * | |
| * Pure, dependency-free so it is unit-testable under `tsx --test` without a | |
| * DOM/WebGL context. `DeckGLMap.updateLayers()` feeds it the measured parts of a | |
| * render frame; `summarizeRenderTiming` splits the cost into the JS-build bucket | |
| * (our `buildLayers()` — layer construction + any Supercluster index rebuild that | |
| * fires inside it) vs the deck.gl commit bucket (tessellation + attribute | |
| * generation inside `setProps`). That distinction is the core #4558 question: is | |
| * a slow frame our synchronous JS or deck.gl's intrinsic tessellation? | |
| */ | |
| /** The 16ms single-frame budget the existing DEV warning checks against. */ | |
| export const FRAME_BUDGET_MS = 16; | |
| export interface RenderTimingParts { | |
| /** Total wall-clock for the render frame (performance.now delta). */ | |
| total: number; | |
| /** Time spent in `buildLayers()` (layer construction + in-build Supercluster rebuilds). */ | |
| jsBuild?: number; | |
| /** Number of deck.gl layers committed this frame. */ | |
| layerCount?: number; | |
| /** Heavy layers whose data changed this frame (e.g. conflict-zones, clusters). */ | |
| changedHeavyLayers?: string[]; | |
| } | |
| export interface RenderTimingSummary { | |
| total: number; | |
| /** Our synchronous JS cost (`buildLayers`). */ | |
| jsBuild: number; | |
| /** Remainder attributed to the deck.gl commit (tessellation + attributes). */ | |
| deckCommit: number; | |
| layerCount: number; | |
| changedHeavyLayers: string[]; | |
| /** True when the frame exceeded the single-frame budget. */ | |
| overBudget: boolean; | |
| } | |
| const clampNonNegative = (n: number | undefined): number => | |
| typeof n === 'number' && Number.isFinite(n) && n > 0 ? n : 0; | |
| /** | |
| * Split a measured render frame into attributable buckets. `deckCommit` is the | |
| * remainder after the JS build, floored at 0 so a noisy measurement (build | |
| * slightly exceeding total) never reports a negative bucket. | |
| */ | |
| export function summarizeRenderTiming(parts: RenderTimingParts): RenderTimingSummary { | |
| const total = clampNonNegative(parts.total); | |
| const jsBuild = Math.min(total, clampNonNegative(parts.jsBuild)); | |
| return { | |
| total, | |
| jsBuild, | |
| deckCommit: Math.max(0, total - jsBuild), | |
| layerCount: clampNonNegative(parts.layerCount), | |
| changedHeavyLayers: parts.changedHeavyLayers ? [...parts.changedHeavyLayers] : [], | |
| overBudget: total > FRAME_BUDGET_MS, | |
| }; | |
| } | |
| /** Compact one-line DEV log string for a slow frame. */ | |
| export function formatRenderTiming(summary: RenderTimingSummary): string { | |
| const heavy = summary.changedHeavyLayers.length | |
| ? ` changed=[${summary.changedHeavyLayers.join(',')}]` | |
| : ''; | |
| return ( | |
| `[DeckGLMap] render ${summary.total.toFixed(1)}ms ` + | |
| `(jsBuild ${summary.jsBuild.toFixed(1)} / ` + | |
| `deck ${summary.deckCommit.toFixed(1)}) layers=${summary.layerCount}${heavy}` | |
| ); | |
| } | |