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

TextField cursor animation uses a lot of CPU

Open zoff99 opened this issue 1 year ago • 14 comments

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

  • Desktop

Versions

  • Kotlin version*: 1.9.22
  • Compose Multiplatform version*: 1.5.11, 1.5.12
  • OS version(s)* (required for Desktop and iOS issues): ubuntu 22.04
  • OS architecture (x86 or arm64): x86 64bit
  • JDK (for desktop issues): 17, 21

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 avatar Feb 03 '24 11:02 zoff99

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

zoff99 avatar Feb 03 '24 11:02 zoff99

Could you provide a smaller reproducer?

m-sasha avatar Feb 03 '24 11:02 m-sasha

whats the problem with this one?

zoff99 avatar Feb 03 '24 11:02 zoff99

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.

m-sasha avatar Feb 03 '24 11:02 m-sasha

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

zoff99 avatar Feb 03 '24 11:02 zoff99

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.

zoff99 avatar Feb 03 '24 12:02 zoff99

Can you post the code here then?

m-sasha avatar Feb 03 '24 12:02 m-sasha

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

zoff99 avatar Feb 03 '24 12:02 zoff99

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

zoff99 avatar Feb 03 '24 12:02 zoff99

@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()
    }
}

zoff99 avatar Feb 03 '24 12:02 zoff99

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 avatar Feb 03 '24 23:02 m-sasha

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
}

m-sasha avatar Feb 05 '24 19:02 m-sasha

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

zoff99 avatar Feb 14 '24 08:02 zoff99

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

m-sasha avatar Feb 14 '24 10:02 m-sasha

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?

zoff99 avatar Feb 21 '24 08:02 zoff99

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

mgroth0 avatar Feb 21 '24 09:02 mgroth0

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

We have regular dev releases.

m-sasha avatar Feb 21 '24 09:02 m-sasha

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

m-sasha avatar Feb 21 '24 10:02 m-sasha