JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.29k stars 1.18k forks source link

TextField cursor animation uses a lot of CPU #4225

Closed zoff99 closed 9 months ago

zoff99 commented 9 months ago

Describe the bug i have a text input in my app, when i click on it to focus and enter text, cpu usage goes to 100% as soon as i click somewhere else to shift focus, cpu goes back to normal again

Affected platforms

Versions

To Reproduce Steps and/or the code snippet to reproduce the behavior:

  1. Go to '...'
  2. Click on '...'
  3. Scroll down to '...'
  4. See error

Expected behavior set focus on the textfield and not use 100% cpu

zoff99 commented 9 months ago

to reproduce:

git clone https://github.com/zoff99/trifa_material
cd trifa_material
git checkout cpu_burn
./gradlew run

then click inside the green textfield to focus it, and you see the blinking cursor. what cpu usage with htop

m-sasha commented 9 months ago

Could you provide a smaller reproducer?

zoff99 commented 9 months ago

whats the problem with this one?

m-sasha commented 9 months ago

A small/minimal reproducer demonstrates the bug.

A project

  1. Requires us to check out the code, potentially running malicious code in the gradle build file, or the code itself.
  2. If the code isn't minimal - requires to look through it to find the issue.
  3. Often the issue turns out to be user error. Reducing the reproducer to a minimal one lowers the chance of that.
  4. Having the code in the ticket makes it easier to discuss and for 3rd parties to look at. Your project could be gone a year from now, and someone looking at this ticket then wouldn't be able to understand the issue.
zoff99 commented 9 months ago

i will remove as much as i can. but as you say its probably a combination of things.

zoff99 commented 9 months ago

i removed as much as possible from that branch. the issue is still there. and now its only Main.kt left with a few lines of code.

m-sasha commented 9 months ago

Can you post the code here then?

zoff99 commented 9 months ago

https://github.com/zoff99/trifa_material/blob/cpu_burn/src/main/kotlin/Main.kt

zoff99 commented 9 months ago

you will still need some project and gradle files to run it.

zoff99 commented 9 months ago
@file:OptIn(ExperimentalComposeUiApi::class, ExperimentalFoundationApi::class)

import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material.MaterialTheme
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.zoffcc.applications.trifa_material.trifa_material.BuildConfig
import kotlinx.coroutines.DelicateCoroutinesApi

@OptIn(DelicateCoroutinesApi::class, ExperimentalFoundationApi::class)
@Composable
@Preview
fun App()
{
    MaterialTheme {
        Column(modifier = Modifier.fillMaxSize()) {
            Row(modifier = Modifier.fillMaxWidth().height(100.dp)
                .background(Color.Green)) {
                SendMessage() { text -> //
                }
            }
        }
    }
}

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SendMessage(sendMessage: (String) -> Unit)
{
    var inputText by remember { mutableStateOf("") }
    // var show_emoji_popup by remember { mutableStateOf(false) }
    TextField(
        modifier = Modifier.fillMaxWidth(),
        value = inputText,
        onValueChange = {
        }
    )
}

fun main() = application(exitProcessOnExit = true) {
    MainAppStart()
}

@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun MainAppStart()
{
    val appIcon = painterResource("icon-linux.png")
    // ----------- main app screen -----------
    // ----------- main app screen -----------
    // ----------- main app screen -----------
    Window(
        onCloseRequest = { }, title = "TRIfA - " + BuildConfig.APP_VERSION,
        icon = appIcon,
        focusable = true,
    ) {
        App()
    }
}
m-sasha commented 9 months ago

On my computer (M1 Ultra) this app goes from ~2% to ~20% CPU when focusing the text field.

fun main() = singleWindowApplication {
    TextField(
        modifier = Modifier.fillMaxWidth(),
        value = "",
        onValueChange = { }
    )
}

This doesn't seem reasonable, indeed.

m-sasha commented 9 months ago

The proximal reason for this is that the cursor blinking is implemented as a regular animation, which causes frameClock.hasAwaiters to be true all the time, in turn causing render to run all the time.

The above shouldn't be spiking the CPU so high, however, and according to @igordmn, on Windows, it doesn't. So it seems we have a problem in Skiko on Linux and/or macOS. For example, this skiko code also causes an unreasonably high CPU utilization:

JFrame("SkikoIdlePerfTest").apply {
    size = Dimension(800, 600)
    val props = SkiaLayerProperties(
        renderApi = GraphicsApi.METAL,
        isVsyncEnabled = true,
    )

    add(SkiaLayer(properties = props).apply {
        skikoView = object : SkikoView {
            override fun onRender(canvas: Canvas, width: Int, height: Int, nanoTime: Long) {
                needRedraw()
            }
        }
    })
    isVisible = true
}
zoff99 commented 9 months ago

any progress on the fix? is there an issue or PR to watch?

m-sasha commented 9 months ago

There will be a fix, but not in 1.6.0. I'll post an update here.

zoff99 commented 9 months ago

There will be a fix, but not in 1.6.0. I'll post an update here.

after it is merged, how can i get it in my app?

mgroth0 commented 9 months ago

A while ago I noticed a similar issue but I cannot remember if I ever reported it.


val MODIFIER = Modifier.pointerHoverIcon(PointerIcon(Cursor(Cursor.WAIT_CURSOR)))
fun main() {
    application {
        Window(visible = true, onCloseRequest = {
            exitApplication()
        }) {
            Column(
                modifier = MODIFIER
            ) {
                Text("Pointer Cached")
            }
        }
    }
}

The issue is that the spinning cursor stutters and sometimes stops for up to a second before spinning again.

I am on MacOS with the latest compose version

m-sasha commented 9 months ago

after it is merged, how can i get it in my app?

We have regular dev releases.

m-sasha commented 9 months ago

Fixed in https://github.com/JetBrains/compose-multiplatform-core/pull/1113

okushnikov commented 2 months ago

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