perf(vue): coalesce canvas surface frames
This commit is contained in:
parent
743f2b5a16
commit
20342d68a2
|
|
@ -6,6 +6,46 @@ type RenderLoopOptions = {
|
|||
layer?: CanvasRenderLayer
|
||||
}
|
||||
|
||||
type EditorRenderScheduler = {
|
||||
schedule: (callback: () => void) => void
|
||||
cancel: (callback: () => void) => void
|
||||
}
|
||||
|
||||
const renderSchedulers = new WeakMap<Editor, EditorRenderScheduler>()
|
||||
|
||||
function getRenderScheduler(editor: Editor): EditorRenderScheduler {
|
||||
const existing = renderSchedulers.get(editor)
|
||||
if (existing) return existing
|
||||
|
||||
let frameId: number | null = null
|
||||
const callbacks = new Set<() => void>()
|
||||
|
||||
function flush() {
|
||||
frameId = null
|
||||
const pending = [...callbacks]
|
||||
callbacks.clear()
|
||||
for (const callback of pending) callback()
|
||||
}
|
||||
|
||||
const scheduler = {
|
||||
schedule(callback: () => void) {
|
||||
callbacks.add(callback)
|
||||
if (frameId !== null) return
|
||||
frameId = requestAnimationFrame(flush)
|
||||
},
|
||||
cancel(callback: () => void) {
|
||||
callbacks.delete(callback)
|
||||
if (callbacks.size === 0 && frameId !== null) {
|
||||
cancelAnimationFrame(frameId)
|
||||
frameId = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderSchedulers.set(editor, scheduler)
|
||||
return scheduler
|
||||
}
|
||||
|
||||
function shouldScheduleForRepaint(layer: CanvasRenderLayer | undefined) {
|
||||
return layer !== 'scene'
|
||||
}
|
||||
|
|
@ -19,27 +59,32 @@ export function createCanvasRenderLoop(
|
|||
renderNow: () => void,
|
||||
options: RenderLoopOptions = {}
|
||||
) {
|
||||
const scheduler = getRenderScheduler(editor)
|
||||
let dirty = true
|
||||
let frameId: number | null = null
|
||||
let frameScheduled = false
|
||||
let lastRenderVersion = -1
|
||||
let lastSelectedIds: Set<string> | null = null
|
||||
|
||||
function renderFrame() {
|
||||
frameScheduled = false
|
||||
if (editor.state.loading) {
|
||||
scheduleRender()
|
||||
return
|
||||
}
|
||||
|
||||
const versionChanged = editor.state.renderVersion !== lastRenderVersion
|
||||
const selectionChanged = editor.state.selectedIds !== lastSelectedIds
|
||||
if (dirty || versionChanged || selectionChanged) {
|
||||
dirty = false
|
||||
renderNow()
|
||||
}
|
||||
}
|
||||
|
||||
const scheduleRender = () => {
|
||||
dirty = true
|
||||
if (frameId !== null) return
|
||||
frameId = requestAnimationFrame(() => {
|
||||
frameId = null
|
||||
if (editor.state.loading) {
|
||||
scheduleRender()
|
||||
return
|
||||
}
|
||||
|
||||
const versionChanged = editor.state.renderVersion !== lastRenderVersion
|
||||
const selectionChanged = editor.state.selectedIds !== lastSelectedIds
|
||||
if (dirty || versionChanged || selectionChanged) {
|
||||
dirty = false
|
||||
renderNow()
|
||||
}
|
||||
})
|
||||
if (frameScheduled) return
|
||||
frameScheduled = true
|
||||
scheduler.schedule(renderFrame)
|
||||
}
|
||||
|
||||
const unsubscribe = [
|
||||
|
|
@ -62,9 +107,9 @@ export function createCanvasRenderLoop(
|
|||
|
||||
function pause() {
|
||||
for (const off of unsubscribe) off()
|
||||
if (frameId !== null) {
|
||||
cancelAnimationFrame(frameId)
|
||||
frameId = null
|
||||
if (frameScheduled) {
|
||||
scheduler.cancel(renderFrame)
|
||||
frameScheduled = false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,37 @@ describe('canvas render loop', () => {
|
|||
}
|
||||
})
|
||||
|
||||
test('coalesces multiple canvas surfaces into one animation frame', () => {
|
||||
const scheduler = createFrameScheduler()
|
||||
try {
|
||||
const { editor, emit } = createEditor()
|
||||
let sceneRenders = 0
|
||||
let overlayRenders = 0
|
||||
createCanvasRenderLoop(
|
||||
editor,
|
||||
() => {
|
||||
sceneRenders++
|
||||
},
|
||||
{ layer: 'scene' }
|
||||
)
|
||||
createCanvasRenderLoop(
|
||||
editor,
|
||||
() => {
|
||||
overlayRenders++
|
||||
},
|
||||
{ layer: 'overlays' }
|
||||
)
|
||||
|
||||
emit('viewport:changed')
|
||||
expect(scheduler.pendingCount).toBe(1)
|
||||
scheduler.flush()
|
||||
expect(sceneRenders).toBe(1)
|
||||
expect(overlayRenders).toBe(1)
|
||||
} finally {
|
||||
scheduler.restore()
|
||||
}
|
||||
})
|
||||
|
||||
test('cancels pending renders when paused', () => {
|
||||
const scheduler = createFrameScheduler()
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue