ADDITIONAL SYSTEM INFORMATION :
Windows 10
JRE 21
A DESCRIPTION OF THE PROBLEM :
Creating a tooltip with constructor new can lead to null pointer exceptions:
- call `new Tooltip()`
- tooltip does some initial CSS layouting
- for this, some internal node tries to get it's stylable parent
- Tooltip overwrites this with returning the global static variable `Tooltip.BEHAVIOUR.hoveredNode`, if it's not null
If, at the same time, the user hovers or un-hovers a node, bookkeeping of the parent hierarchy in the background thread can get corrupted, leading to a NPE.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
- run the example
- place your mouse at the border of the bottom of the label, so that you rapidly un-hover and hover the label
- observe a NPE
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
no NPE
ACTUAL -
an NPE
---------- BEGIN SOURCE ----------
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example.app
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.control.Tooltip
import javafx.scene.layout.Border
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.stage.Stage
import javafx.util.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.javafx.JavaFx
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import kotlin.coroutines.EmptyCoroutineContext
class App: Application() {
companion object {
@JvmStatic
fun main(args: Array<String>) {
launch(App::class.java, *args)
}
}
override fun start(stage: Stage) {
val root = VBox()
val scene = Scene(root)
stage.scene = scene
val text = Label()
text.text = "some text"
text.border = Border.stroke(Color.BLACK)
val tooltip1 = Tooltip()
tooltip1.showDelay = Duration(0.0)
tooltip1.hideDelay = Duration(0.0)
tooltip1.text = "tooltip1"
text.tooltip = tooltip1
root.children += text
stage.show()
val scope = CoroutineScope(EmptyCoroutineContext)
// continuously resize text to force re-layout
scope.launch(Dispatchers.JavaFx) {
val h1 = 400.0
val h2 = 410.0
while (true) {
yield()
text.minHeight = h1
text.maxHeight = h1
stage.height = h1
yield()
text.minHeight = h2
text.maxHeight = h2
stage.height = h2
}
}
scope.launch {
while (true) {
val tooltip2 = Tooltip()
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
create all tooltip instances on the JavaFX thread to ensure no concurrent update of the global static variable
FREQUENCY : occasionally
Windows 10
JRE 21
A DESCRIPTION OF THE PROBLEM :
Creating a tooltip with constructor new can lead to null pointer exceptions:
- call `new Tooltip()`
- tooltip does some initial CSS layouting
- for this, some internal node tries to get it's stylable parent
- Tooltip overwrites this with returning the global static variable `Tooltip.BEHAVIOUR.hoveredNode`, if it's not null
If, at the same time, the user hovers or un-hovers a node, bookkeeping of the parent hierarchy in the background thread can get corrupted, leading to a NPE.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
- run the example
- place your mouse at the border of the bottom of the label, so that you rapidly un-hover and hover the label
- observe a NPE
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
no NPE
ACTUAL -
an NPE
---------- BEGIN SOURCE ----------
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example.app
import javafx.application.Application
import javafx.scene.Scene
import javafx.scene.control.Label
import javafx.scene.control.Tooltip
import javafx.scene.layout.Border
import javafx.scene.layout.VBox
import javafx.scene.paint.Color
import javafx.stage.Stage
import javafx.util.Duration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.javafx.JavaFx
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import kotlin.coroutines.EmptyCoroutineContext
class App: Application() {
companion object {
@JvmStatic
fun main(args: Array<String>) {
launch(App::class.java, *args)
}
}
override fun start(stage: Stage) {
val root = VBox()
val scene = Scene(root)
stage.scene = scene
val text = Label()
text.text = "some text"
text.border = Border.stroke(Color.BLACK)
val tooltip1 = Tooltip()
tooltip1.showDelay = Duration(0.0)
tooltip1.hideDelay = Duration(0.0)
tooltip1.text = "tooltip1"
text.tooltip = tooltip1
root.children += text
stage.show()
val scope = CoroutineScope(EmptyCoroutineContext)
// continuously resize text to force re-layout
scope.launch(Dispatchers.JavaFx) {
val h1 = 400.0
val h2 = 410.0
while (true) {
yield()
text.minHeight = h1
text.maxHeight = h1
stage.height = h1
yield()
text.minHeight = h2
text.maxHeight = h2
stage.height = h2
}
}
scope.launch {
while (true) {
val tooltip2 = Tooltip()
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
create all tooltip instances on the JavaFX thread to ensure no concurrent update of the global static variable
FREQUENCY : occasionally
- blocks
-
JDK-8348987 ☂ Thread safety in Node initialization
- In Progress
- relates to
-
JDK-8347392 Thread-unsafe implementation of c.s.j.scene.control.skin.Utils
- In Progress
-
JDK-8348423 [TestBug] stress test Nodes initialization from a background thread
- In Progress