perf(canvas): keep scene picture cache warm after edits

This commit is contained in:
Danila Poyarkov 2026-05-17 02:27:58 +03:00
parent 0b3b620b54
commit 617f3864ac
4 changed files with 51 additions and 25 deletions

View file

@ -80,8 +80,6 @@ export class SkiaRenderer {
scenePictureVersion = -1
scenePicturePositionPreviewVersion = -1
scenePicturePageId: string | null = null
lastObservedSceneVersion = -1
lastSceneVersionChangeAt = 0
nodePictureCache = new Map<string, SkPicture | null>()
readonly labelCache = new LabelCache()
readonly profiler: RenderProfiler

View file

@ -24,9 +24,6 @@ export function renderSceneToCanvas(
export type RenderLayer = 'full' | 'scene' | 'overlays'
const LIVE_SCENE_CHANGE_MS = 120
const now = typeof performance !== 'undefined' ? () => performance.now() : () => Date.now()
export function renderFromEditorState(
r: SkiaRenderer,
state: EditorState,
@ -87,17 +84,36 @@ function hasVolatileOverlay(overlays: RenderOverlays): boolean {
)
}
function hasLiveSceneChange(r: SkiaRenderer, sceneVersion: number, layer: RenderLayer): boolean {
if (layer === 'overlays' || sceneVersion < 0 || sceneVersion === r.lastObservedSceneVersion) {
return false
}
function scenePictureMissReason(
r: SkiaRenderer,
graph: SceneGraph,
overlays: RenderOverlays,
sceneVersion: number,
hasPositionPreview: boolean
): string {
if (hasPositionPreview) return 'position-preview'
if (hasVolatileOverlay(overlays)) return 'volatile-overlay'
if (!r.scenePicture) return 'missing-picture'
if (graph.positionPreviewVersion !== r.scenePicturePositionPreviewVersion)
return 'position-preview-version'
if (sceneVersion !== r.scenePictureVersion) return 'scene-version'
if (r.pageId !== r.scenePicturePageId) return 'page'
return 'unknown'
}
const timestamp = now()
const live =
r.lastSceneVersionChangeAt > 0 && timestamp - r.lastSceneVersionChangeAt < LIVE_SCENE_CHANGE_MS
r.lastObservedSceneVersion = sceneVersion
r.lastSceneVersionChangeAt = timestamp
return live
function canUseScenePicture(
r: SkiaRenderer,
graph: SceneGraph,
sceneVersion: number,
hasVolatileOverlays: boolean
): boolean {
return (
!hasVolatileOverlays &&
!!r.scenePicture &&
graph.positionPreviewVersion === r.scenePicturePositionPreviewVersion &&
sceneVersion === r.scenePictureVersion &&
r.pageId === r.scenePicturePageId
)
}
export function render(
@ -128,17 +144,16 @@ export function render(
}
const hasPositionPreview = graph.positionPreviewVersion !== r.scenePicturePositionPreviewVersion
const hasVolatileOverlays =
hasPositionPreview || hasVolatileOverlay(overlays) || hasLiveSceneChange(r, sceneVersion, layer)
const hasVolatileOverlays = hasPositionPreview || hasVolatileOverlay(overlays)
const canUsePicture =
!hasVolatileOverlays &&
r.scenePicture &&
graph.positionPreviewVersion === r.scenePicturePositionPreviewVersion &&
sceneVersion === r.scenePictureVersion &&
r.pageId === r.scenePicturePageId
p.setCacheHit(!!canUsePicture)
const canUsePicture = canUseScenePicture(r, graph, sceneVersion, hasVolatileOverlays)
const cacheMissReason = scenePictureMissReason(
r,
graph,
overlays,
sceneVersion,
hasPositionPreview
)
if (layer !== 'overlays') {
canvas.save()
@ -148,16 +163,19 @@ export function render(
p.beginPhase('render:scene')
if (canUsePicture) {
p.setScenePictureMode('hit')
p.beginPhase('render:drawPicture')
if (r.scenePicture) canvas.drawPicture(r.scenePicture)
p.endPhase('render:drawPicture')
} else if (hasVolatileOverlays) {
p.setScenePictureMode('volatile', cacheMissReason)
r._nodeCount = 0
r._culledCount = 0
p.beginPhase('render:volatile')
renderPageChildren(r, canvas, graph, overlays)
p.endPhase('render:volatile')
} else {
p.setScenePictureMode('record', cacheMissReason)
r._nodeCount = 0
r._culledCount = 0
p.beginPhase('render:recordPicture')

View file

@ -25,6 +25,8 @@ export class FrameStats {
culledNodes = 0
drawCalls = 0
scenePictureCacheHit = false
scenePictureMode: 'hit' | 'record' | 'volatile' | 'none' = 'none'
scenePictureMissReason = ''
private frameTimeBuffer = new Float64Array(BUFFER_SIZE)
private cpuTimeBuffer = new Float64Array(BUFFER_SIZE)

View file

@ -79,6 +79,14 @@ export class RenderProfiler {
setCacheHit(hit: boolean): void {
this.stats.scenePictureCacheHit = hit
this.stats.scenePictureMode = hit ? 'hit' : 'none'
if (hit) this.stats.scenePictureMissReason = ''
}
setScenePictureMode(mode: 'hit' | 'record' | 'volatile' | 'none', reason = ''): void {
this.stats.scenePictureCacheHit = mode === 'hit'
this.stats.scenePictureMode = mode
this.stats.scenePictureMissReason = reason
}
beginCapture(): void {