-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Benchmarks. Mimic the behavior of Skiko rendering #5215
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,12 +2,16 @@ import androidx.compose.runtime.Composable | |
import androidx.compose.ui.InternalComposeUiApi | ||
import androidx.compose.ui.graphics.asComposeCanvas | ||
import androidx.compose.ui.scene.CanvasLayersComposeScene | ||
import androidx.compose.ui.scene.ComposeScene | ||
import androidx.compose.ui.unit.IntSize | ||
import org.jetbrains.skia.Surface | ||
import kotlin.time.Duration | ||
import kotlin.time.Duration.Companion.nanoseconds | ||
import kotlin.time.ExperimentalTime | ||
import kotlinx.coroutines.* | ||
import org.jetbrains.skia.Color | ||
import org.jetbrains.skia.PictureRecorder | ||
import org.jetbrains.skia.Rect | ||
import kotlin.time.TimeSource.Monotonic.markNow | ||
import kotlin.time.measureTime | ||
|
||
|
@@ -46,16 +50,15 @@ suspend fun measureComposable( | |
graphicsContext: GraphicsContext?, | ||
content: @Composable () -> Unit | ||
): BenchmarkResult { | ||
val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) | ||
val scene = CanvasLayersComposeScene(size = IntSize(width, height)) | ||
try { | ||
val nanosPerFrame = (1.0 / targetFps.toDouble() * nanosPerSecond).toLong() | ||
scene.setContent(content) | ||
val surface = graphicsContext?.surface(width, height) ?: Surface.makeNull(width, height) | ||
val canvas = surface.canvas.asComposeCanvas() | ||
|
||
// warmup | ||
repeat(warmupCount) { | ||
scene.render(canvas, it * nanosPerFrame) | ||
scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) | ||
surface.flushAndSubmit(false) | ||
graphicsContext?.awaitGPUCompletion() | ||
} | ||
|
@@ -67,7 +70,7 @@ suspend fun measureComposable( | |
if (Args.isModeEnabled(Mode.CPU)) { | ||
renderTime = measureTime { | ||
repeat(frameCount) { | ||
scene.render(canvas, it * nanosPerFrame) | ||
scene.mimicSkikoRender(surface, it * nanosPerFrame, width, height) | ||
surface.flushAndSubmit(false) | ||
gpuTime += measureTime { | ||
graphicsContext?.awaitGPUCompletion() | ||
|
@@ -93,7 +96,7 @@ suspend fun measureComposable( | |
repeat(frameCount) { | ||
val frameStart = start + nextVSync | ||
|
||
scene.render(canvas, nextVSync.inWholeNanoseconds) | ||
scene.mimicSkikoRender(surface, it * nextVSync.inWholeNanoseconds, width, height) | ||
surface.flushAndSubmit(false) | ||
|
||
val cpuTime = frameStart.elapsedNow() | ||
|
@@ -124,6 +127,33 @@ suspend fun measureComposable( | |
) | ||
} finally { | ||
scene.close() | ||
surface.close() | ||
runGC() // cleanup for next benchmarks | ||
} | ||
} | ||
|
||
private val pictureRecorder = PictureRecorder() | ||
|
||
/** | ||
* Mimic Skiko render logic from https://github.com/JetBrains/skiko/blob/eb1f04ec99d50ff0bdb2f592fdf49711a9251aa7/skiko/src/awtMain/kotlin/org/jetbrains/skiko/SkiaLayer.awt.kt#L531 | ||
* | ||
* This is very simplified logic, and it still can differ from the real cases. | ||
* | ||
* Though one main function - rendering into picture - was affecting performance. | ||
* | ||
* Benchmarks showed an improvement by 10%, but there was a regression by 10%. | ||
* | ||
* Beware that this logic can be changed in some new version of Skiko. | ||
* | ||
* If Skiko stops using `picture`, we need to remove it here too. | ||
*/ | ||
@OptIn(InternalComposeUiApi::class) | ||
fun ComposeScene.mimicSkikoRender(surface: Surface, time: Long, width: Int, height: Int) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would rename it to reflect what it actually does , f.i There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function should match Skiko’s rendering. While it currently uses a picture, that’s an implementation detail. If Skiko stops using a picture, we should change the implementation here, without renaming. It also clears the Canvas, which may impact benchmarking accuracy. Ideally we should reuse some function from Skiko, but this a much more complex task. |
||
val pictureCanvas = pictureRecorder.beginRecording(Rect(0f, 0f, width.toFloat(), height.toFloat())) | ||
render(pictureCanvas.asComposeCanvas(), time) | ||
val picture = pictureRecorder.finishRecordingAsPicture() | ||
|
||
surface.canvas.clear(Color.TRANSPARENT) | ||
surface.canvas.drawPicture(picture) | ||
picture.close() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
imo: requires BIG TODO to keep it in sync with skiko implementation, because I'm not sure that it won't be changed there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a comment here
Without reusing the whole renderer from Skiko (which realistically we won't do without a good reason), we should keep it sync manually.
When we merge it, I will add a comment to Skiko: