From faaa74191199028350665e01fbdda32f81b5e6ff Mon Sep 17 00:00:00 2001 From: Danila Poyarkov Date: Sun, 17 May 2026 03:09:16 +0300 Subject: [PATCH] feat(profiler): expose scene picture and flush timings --- packages/core/src/canvas/renderer/pipeline.ts | 23 ++++++++++++++++--- packages/core/src/profiler/capture-session.ts | 5 ++++ packages/core/src/profiler/frame/capture.ts | 5 ++++ packages/core/src/profiler/frame/stats.ts | 3 +++ packages/core/src/profiler/hud-renderer.ts | 9 +++++++- packages/core/src/profiler/render-profiler.ts | 12 ++++++++++ 6 files changed, 53 insertions(+), 4 deletions(-) diff --git a/packages/core/src/canvas/renderer/pipeline.ts b/packages/core/src/canvas/renderer/pipeline.ts index 2bfe69376..47f456850 100644 --- a/packages/core/src/canvas/renderer/pipeline.ts +++ b/packages/core/src/canvas/renderer/pipeline.ts @@ -116,6 +116,14 @@ function canUseScenePicture( ) } +const now = typeof performance !== 'undefined' ? () => performance.now() : () => 0 + +function measure(fn: () => T): { value: T; duration: number } { + const start = now() + const value = fn() + return { value, duration: now() - start } +} + export function render( r: SkiaRenderer, graph: SceneGraph, @@ -126,6 +134,9 @@ export function render( ): void { const p = r.profiler p.beginFrame() + p.setScenePictureDrawTime(0) + p.setScenePictureRecordTime(0) + p.setFlushTime(0) graph.clearAbsPosCache() @@ -167,7 +178,11 @@ export function render( if (canUsePicture) { p.setScenePictureMode('hit') p.beginPhase('render:drawPicture') - if (r.scenePicture) canvas.drawPicture(r.scenePicture) + if (r.scenePicture) { + const picture = r.scenePicture + const { duration } = measure(() => canvas.drawPicture(picture)) + p.setScenePictureDrawTime(duration) + } p.endPhase('render:drawPicture') } else if (hasVolatileOverlays) { p.setScenePictureMode('volatile', cacheMissReason) @@ -181,7 +196,8 @@ export function render( r._nodeCount = 0 r._culledCount = 0 p.beginPhase('render:recordPicture') - recordScenePicture(r, canvas, graph, sceneVersion) + const { duration } = measure(() => recordScenePicture(r, canvas, graph, sceneVersion)) + p.setScenePictureRecordTime(duration) p.endPhase('render:recordPicture') } p.endPhase('render:scene') @@ -231,7 +247,8 @@ export function render( } p.beginPhase('render:flush') - r.surface.flush() + const { duration: flushDuration } = measure(() => r.surface.flush()) + p.setFlushTime(flushDuration) p.endPhase('render:flush') p.setNodeCounts(r._nodeCount, r._culledCount) diff --git a/packages/core/src/profiler/capture-session.ts b/packages/core/src/profiler/capture-session.ts index 3ea0d2ae0..6921cba8b 100644 --- a/packages/core/src/profiler/capture-session.ts +++ b/packages/core/src/profiler/capture-session.ts @@ -29,6 +29,11 @@ export function createFrameCapture( culledNodes: stats.culledNodes, drawCalls: stats.drawCalls, scenePictureCacheHit: stats.scenePictureCacheHit, + scenePictureMode: stats.scenePictureMode, + scenePictureMissReason: stats.scenePictureMissReason, + scenePictureDrawTimeMs: stats.scenePictureDrawTime, + scenePictureRecordTimeMs: stats.scenePictureRecordTime, + flushTimeMs: stats.flushTime, rootProfiles: session.stack.getRootProfiles() } } diff --git a/packages/core/src/profiler/frame/capture.ts b/packages/core/src/profiler/frame/capture.ts index 5f668b064..daaf286ab 100644 --- a/packages/core/src/profiler/frame/capture.ts +++ b/packages/core/src/profiler/frame/capture.ts @@ -22,6 +22,11 @@ export interface FrameCapture { culledNodes: number drawCalls: number scenePictureCacheHit: boolean + scenePictureMode: 'hit' | 'record' | 'volatile' | 'none' + scenePictureMissReason: string + scenePictureDrawTimeMs: number + scenePictureRecordTimeMs: number + flushTimeMs: number rootProfiles: NodeProfile[] } diff --git a/packages/core/src/profiler/frame/stats.ts b/packages/core/src/profiler/frame/stats.ts index 015d15343..7b898461f 100644 --- a/packages/core/src/profiler/frame/stats.ts +++ b/packages/core/src/profiler/frame/stats.ts @@ -27,6 +27,9 @@ export class FrameStats { scenePictureCacheHit = false scenePictureMode: 'hit' | 'record' | 'volatile' | 'none' = 'none' scenePictureMissReason = '' + scenePictureDrawTime = 0 + scenePictureRecordTime = 0 + flushTime = 0 private frameTimeBuffer = new Float64Array(BUFFER_SIZE) private cpuTimeBuffer = new Float64Array(BUFFER_SIZE) diff --git a/packages/core/src/profiler/hud-renderer.ts b/packages/core/src/profiler/hud-renderer.ts index 92726063a..3fcbb0510 100644 --- a/packages/core/src/profiler/hud-renderer.ts +++ b/packages/core/src/profiler/hud-renderer.ts @@ -106,7 +106,7 @@ export class HudRenderer { ] const visiblePhases = phaseNames.filter((n) => (phases.get(n) ?? 0) > 0.01) - const statsRows = 3 + const statsRows = 4 const phaseRows = visiblePhases.length > 0 ? 1 + visiblePhases.length : 0 const statsHeight = (statsRows + phaseRows) * LINE_HEIGHT const graphSection = hasGraph ? GRAPH_HEIGHT + PADDING + LINE_HEIGHT : 0 @@ -151,6 +151,13 @@ export class HudRenderer { this.hudFont ) canvas.drawText(`Cache: ${cacheStatus}`, col2, y, this.textPaint, this.hudFont) + y += LINE_HEIGHT + + const pictureLabel = stats.scenePictureMode === 'record' ? 'record' : 'picture' + const pictureTime = + stats.scenePictureMode === 'record' ? stats.scenePictureRecordTime : stats.scenePictureDrawTime + canvas.drawText(`${pictureLabel}: ${pictureTime.toFixed(1)}ms`, col1, y, this.textPaint, this.hudFont) + canvas.drawText(`flush: ${stats.flushTime.toFixed(1)}ms`, col2, y, this.textPaint, this.hudFont) if (visiblePhases.length > 0) { y += LINE_HEIGHT diff --git a/packages/core/src/profiler/render-profiler.ts b/packages/core/src/profiler/render-profiler.ts index 99c0161b0..cdf711536 100644 --- a/packages/core/src/profiler/render-profiler.ts +++ b/packages/core/src/profiler/render-profiler.ts @@ -89,6 +89,18 @@ export class RenderProfiler { this.stats.scenePictureMissReason = reason } + setScenePictureDrawTime(ms: number): void { + this.stats.scenePictureDrawTime = ms + } + + setScenePictureRecordTime(ms: number): void { + this.stats.scenePictureRecordTime = ms + } + + setFlushTime(ms: number): void { + this.stats.flushTime = ms + } + beginCapture(): void { this.capturing = true this.captureSession = createCaptureSession(now())