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
15.56k stars 1.13k forks source link

Desktop `MenuBar` `Item` Shortcuts Cause Performance Hit #3801

Open ScottHamper opened 9 months ago

ScottHamper commented 9 months ago

Describe the problem Pressing the shortcut key(s) for a MenuBar Item results in a relatively significant performance hit. The hit can be observed in at least a couple of different ways:

Affected platforms Select one of the platforms below:

Versions

Sample code

fun main() = application {
    var count by remember { mutableStateOf(0) }

    Window(
        onCloseRequest = ::exitApplication,
        state = rememberWindowState(size = DpSize(width = 400.dp, height = 400.dp)),
        title = "MenuBar Item Shortcut Perf Issue",
        resizable = false,
        onKeyEvent = { event ->
            if (event.type == KeyEventType.KeyDown && event.key == Key.F2) {
                count++
                true
            } else {
                false
            }
        }
    ) {
        MenuBar {
            Menu("Counter") {
                Item(text = "Increment", shortcut = KeyShortcut(Key.F1)) { count++ }
            }
        }

        Box(
            contentAlignment = Alignment.Center,
            modifier = Modifier.fillMaxSize(),
        ) {
            Text("$count")
        }

        LaunchedEffect(Unit) {
            var lastTickNanos = withFrameNanos { it }

            while (true) {
                withFrameNanos { nowNanos ->
                    val elapsedNanos = nowNanos - lastTickNanos
                    lastTickNanos = nowNanos

                    // At 60 FPS, `elapsedNanos` should be approximately 16_666_666 nanoseconds
                    if (elapsedNanos >= 30_000_000) {
                        println("Long delay of %,d nanos".format(elapsedNanos))
                    }
                }
            }
        }
    }
}

Reproduction steps Press F1 and observe the console for print messages. On my computer, pressing F1 causes a delay of ~70,000,000 nanoseconds between withFrameNanos calls, which is over four animation frames at 60 FPS.

Alternatively, hold down F1 and observe the Text component displaying the count variable. It will re-compose very sluggishly, with the delay between re-compositions getting progressively worse the longer the key is held down. This will also be reflected via print messages in the console, as the time delta between withFrameNanos gets larger and larger.

The above two options can be compared against pressing/holding F2, which executes the same action as F1 but is implemented as an onKeyEvent handler on the Window composable instead of via a MenuBar Item shortcut. Pressing and/or holding F2 never results in a print message to the console caused by a long delay between withFrameNanos, and the Text composable consistently re-composes immediately every time the count variable is incremented. Similarly, manually clicking the "Increment" menu item instead of using its F1 shortcut will not result in a delay.

Video Here's a GIF of tapping F1 five times, followed by holding it down for a while: menu-item-perf-bug

Profiling data I'm a noob when it comes to profiling, but here's a VisualVM timeline of thread state. The AWT_EventQueue-0 thread starts sleeping and DefaultDispatcher-worker-3 parks when holding down F1. This does not occur when holding down F2. visualvm-threads-monitor I have not observed any CPU or memory usage spiking while holding F1.

Additional information I don't know if it's intended behavior or not, but I was surprised to discover that holding down a shortcut key would continually trigger the item's onClick action. That seems like strange default behavior to me, especially when thinking about, for example, a "File" -> "Save" item, where holding Ctrl+S continually triggers a save. Would there be any interest in changing this default behavior, or providing a way to configure shortcuts so that they only trigger once? If so, I'd be happy to create a separate enhancement issue for that - I would definitely use the functionality.

elijah-semyonov commented 9 months ago

Thank you for such a detailed report. We will investigate that.

ScottHamper commented 9 months ago

The issue also occurs when using a keyboard mnemonic to activate the menu item. For example:

Menu("Counter", mnemonic = 'C') {
    Item(text = "Increment", mnemonic = 'I', shortcut = KeyShortcut(Key.F1)) { count++ }
}

Pressing Alt -> C -> I results in the same performance hit as tapping F1. However, pressing Alt -> down arrow -> Enter does not.