compose-multiplatform icon indicating copy to clipboard operation
compose-multiplatform copied to clipboard

Research performance issues on Desktop

Open mazunin-v-jb opened this issue 1 year ago • 12 comments

According to this article and performed tests with different versions of Compose, performance regression has been noticed after Compose 1.0 and Compose 1.2 (tested on Windows). This is a potential point for further optimization.

mazunin-v-jb avatar Aug 21 '23 18:08 mazunin-v-jb

Windows 11, Corretto 18, 10000 squares

Compose, Kotlin FPS
1.5.0, 1.9.0 69
1.5.0, 1.7.10 69
1.4.3, 1.7.10 68
1.3.1, 1.7.10 78
1.2.1, 1.7.10 78
1.1.1, 1.6.10 122
1.0.0, 1.5.31 122
0.4.0, 1.5.10 149

A reduced benchmark:

import androidx.compose.ui.awt.ComposeWindow     // for >= 1.0.0
// import androidx.compose.desktop.ComposeWindow // for 0.4.0
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import java.awt.Dimension
import javax.swing.SwingUtilities
import kotlin.math.roundToInt

fun main() = SwingUtilities.invokeLater {
    System.setProperty("skiko.renderApi", "OPENGL")
    System.setProperty("skiko.vsync.enabled", "false")

    ComposeWindow().apply {
        size = Dimension(1280, 720)
        setContent {
            App()
        }
        isVisible = true
    }
}

val particlesCount = 10_000
val fpsCounter = FPSCounter()
var time by mutableStateOf(0L)

@Composable
fun App() = Canvas(Modifier.fillMaxSize()) {
    drawIntoCanvas {
        time++

        fpsCounter.tick()
        drawRect(Color.Red)
        repeat(particlesCount) {
            val x = Math.random().toFloat()
            val y = Math.random().toFloat()
            drawRect(
                color = Color.White,
                Offset(size.width * x, size.height * y),
                Size(10f, 10f)
            )
        }
    }
}

// a reduced copy from skiko
class FPSCounter(
    private val periodSeconds: Double = 2.0,
) {
    private val times = mutableListOf<Long>()
    private var lastLogTime = System.nanoTime()
    private var lastTime = System.nanoTime()

    fun tick() {
        val time = System.nanoTime()
        val frameTime = time - lastTime
        lastTime = time

        times.add(frameTime)

        if ((time - lastLogTime) > periodSeconds.secondsToNanos() && times.isNotEmpty()) {
            val average = (nanosPerSecond / times.average()).roundToInt()
            times.clear()
            lastLogTime = time
            println("FPS $average")
        }
    }

    private val nanosPerSecond = 1_000_000_000.0
    private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
}

igordmn avatar Aug 21 '23 20:08 igordmn

The regression is in Skiko or in Skia, not in Compose itself. A pure Skiko benchmark shows similar regressions:

// for 0.4.0
//import org.jetbrains.skija.Canvas
//import org.jetbrains.skija.Color
//import org.jetbrains.skija.Paint
//import org.jetbrains.skija.Rect

import org.jetbrains.skia.Canvas
import org.jetbrains.skia.Color
import org.jetbrains.skia.Paint
import org.jetbrains.skia.Rect
import org.jetbrains.skiko.SkikoView
import org.jetbrains.skiko.SkiaLayer
import java.awt.Dimension
import javax.swing.JFrame
import javax.swing.SwingUtilities
import kotlin.math.roundToInt

val redPaint = Paint().apply {
    color = Color.makeRGB(255, 0, 0)
}

val whitePaint = Paint().apply {
    color = Color.makeRGB(255, 255, 255)
}

fun main() = SwingUtilities.invokeLater {
    System.setProperty("skiko.renderApi", "OPENGL")
    System.setProperty("skiko.vsync.enabled", "false")

    JFrame().apply {
        size = Dimension(1280, 720)
        add(SkiaLayer().apply {
            size = Dimension(1280, 720)
// for 0.4.0
//            renderer = object : org.jetbrains.skiko.SkiaRenderer {
            skikoView = object : SkikoView {
                override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
                    fpsCounter.tick()
                    canvas.drawRect(Rect(0f, 0f, width.toFloat(), height.toFloat()), redPaint)
                    repeat(particlesCount) {
                        val x = Math.random().toFloat()
                        val y = Math.random().toFloat()
                        canvas.drawRect(
                            Rect.makeXYWH(width * x, height * y, 10f, 10f),
                            whitePaint
                        )
                    }
                    needRedraw()
                }
            }
        })
        isVisible = true
    }
}

val particlesCount = 10_000
val fpsCounter = FPSCounter()

// a reduced copy from skiko
class FPSCounter(
    private val periodSeconds: Double = 2.0,
) {
    private val times = mutableListOf<Long>()
    private var lastLogTime = System.nanoTime()
    private var lastTime = System.nanoTime()

    fun tick() {
        val time = System.nanoTime()
        val frameTime = time - lastTime
        lastTime = time

        times.add(frameTime)

        if ((time - lastLogTime) > periodSeconds.secondsToNanos() && times.isNotEmpty()) {
            val average = (nanosPerSecond / times.average()).roundToInt()
            times.clear()
            lastLogTime = time
            println("FPS $average")
        }
    }

    private val nanosPerSecond = 1_000_000_000.0
    private fun Double.secondsToNanos(): Long = (this * nanosPerSecond).toLong()
}

igordmn avatar Aug 21 '23 20:08 igordmn

Does this mean that it could have some effects on other targets as well? iOS? If so, fixing this problem is going to boost performance for all targets. Also, I think that the title and labels should be changed.

MohamedRejeb avatar Aug 24 '23 09:08 MohamedRejeb

If it is not a Java/JNI overhead issue, then, yes, it should also affect iOS/web platforms.

igordmn avatar Aug 24 '23 10:08 igordmn

https://github.com/JetBrains/skiko/compare/master...es/regression-rollback Regression was introduced by commit from which this branch starts, this commit shows FPS similar to Compose 1.1.1.

Regression is not present on macOS targets. We will try to update to latest Skia and see if they fixed the issue.

elijah-semyonov avatar Sep 01 '23 13:09 elijah-semyonov

Hey, Any news about this issue?

MohamedRejeb avatar Sep 25 '23 08:09 MohamedRejeb

We plan to update Skia, and if this won't resolve the issue, we'll look further. It either a Skia regression, or a build configuration change.

igordmn avatar Sep 25 '23 08:09 igordmn

Awesome, thanks for the great work! I'm working on a drawing app and users are encountering some latency issues, I hope that this will fix it.

MohamedRejeb avatar Sep 28 '23 10:09 MohamedRejeb

Upgrading Skia from m110 to m116 doesn't help.

I checked the new Skiko with this PR - FPS is the same

igordmn avatar Oct 05 '23 18:10 igordmn

Hello, is there any news regarding this problem?

RyuuyaS avatar Oct 20 '23 16:10 RyuuyaS

Any news about this problem? I'm very worried because my customers of my desktop project have low-end specs and the FPS is about 10~15 that doesn't support OPENGL I spend the last 2 years working on this project since the compose multiplatform was in 1.0 alpha

RafaelAthosPrime avatar Jan 29 '24 21:01 RafaelAthosPrime

Any news about this problem?

MohamedRejeb avatar Feb 08 '24 16:02 MohamedRejeb

Any news? Same problem with the version 1.6.0

RafaelAthosPrime avatar Mar 01 '24 03:03 RafaelAthosPrime

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

okushnikov avatar Jul 14 '24 15:07 okushnikov