A live JavaFX scene graph can only be accessed and modified on the
JavaFX application thread. This restriction is usually not enforced,
but there are some places where checks are built into the framework
(for example, adding and removing nodes from the scene graph).

Performing such checks for FX properties would be nice, but could
impose a performance penalty that we probably wouldn't be comfortable
accepting. Here's an idea of how we can enforce the FX thread
invariant in many more places on a best effort basis, without imposing
a significant performance penalty.

The idea is to build a heuristic into `StyleableProperty`
implementations that might be able to catch at least some illegal
writes (and potentially reads).

Here's how the implementation would look like for `StyleableObjectProperty`:

    public abstract class StyleableObjectProperty<T>
            extends ObjectPropertyBase<T>
            implements StyleableProperty<T> {
        ...
        @Override
        public void applyStyle(StyleOrigin origin, T v) {
            try {
                suppressThreadCheck = true;
                set(v);
                this.origin = origin;
            } finally {
                suppressThreadCheck = false;
            }
        }
        ...
        @Override
        public void set(T v) {
            super.set(v);
            checkAccess();
            origin = StyleOrigin.USER;
        }
        ...
        private StyleOrigin origin = null;
        private boolean suppressThreadCheck;
        private boolean disableThreadCheck;

        private void checkAccess() {
            if (!suppressThreadCheck &&
                    !disableThreadCheck &&
                    getBean() instanceof Node node) {
                Scene scene = node.getScene();
                if (scene != null) {
                    Window window = scene.getWindow();
                    if (window != null &&
                            WindowHelper.getPeer(window) != null) {
                        Toolkit.getToolkit().checkFxUserThread();
                        disableThreadCheck = true;
                    }
                }
            }
        }
    }

Note that the two added boolean fields will not increase the memory
footprint of the property instance on most VM configurations, as there
are some unused bytes available in the current memory layout.

Here's how the heuristic works:

1. The calling thread is never checked when calling the `applyStyle()`
method. The rationale is that `applyStyle()` is probably never called
by user code, but it is often called by the CSS engine. No need to
check the calling thread.

2. If the property is hosted on a `Node`, invoking write methods
(set/bind/bindBidirectional) will call `checkAccess()`, which checks
whether the node is connected to a live scene graph showing in a
window. The presence of a window peer is a prerequisite for the FX
thread check, as it is legal to configure JavaFX nodes on any thread
as long as the nodes are not connected to a live scene graph.
Note that calling `Node::getScene`, `Scene::getWindow` or
`WindowHelper::getPeer` of a live scene graph on a non-FX thread is
illegal. If connected to a live scene graph, `checkAccess()` is not
legally callable from any thread other than the FX thread. However, in
many cases, it will still be possible to determine the presence of a
window peer.

3. If `checkAccess()` is legally called on the FX thread, the first
such call will disable the FX thread check for all subsequent calls.
Otherwise an exception is thrown.

It might be interesting to notice that no attempt is made to
synchronize access to `disableThreadCheck`, even though the field is
potentially read by multiple threads. This means that while a legal
call to `set()` on the FX thread will set the `disableThreadCheck`
flag, a call to the same method on a non-FX thread might not be able
to see the new value of the field. But this is not a problem, as the
call will fail with an exception anyway.

Now, since this scheme is biased toward very little runtime overhead,
it will not be able to catch all illegal writes. There are two basic
assumptions here:

1. The first mutation of a property after the node has been configured
and added to the scene graph is often the offending call of an invalid
cross-thread write. This is the only point in time when we perform the
expensive thread check.

2. Nodes are usually configured before being added to a live scene
graph. If an application first adds nodes to a live scene graph and
then configures their properties, the thread checks for all affected
properties will be disabled instantly by the manual configuration. We
won't be able to catch illegal writes of the affected properties after
that.

Here are some things to think about:

1. Instead of disabling the thread check after the first legal write,
we could also opt to disable it after the _second_ legal write. This
would allow us to catch cases where a property is legally configured
after the node is added to a live scene graph, but then written on a
non-FX thread.

2. We could also call `checkAccess()` from the property's getter if we
are comfortable to read the two boolean fields every time the value of
the property is retrieved. However, this might not catch many illegal
calls to the getter, as it is likely that the thread check would often
be disabled by the JavaFX framework itself (as calling getters happens
quite often).

Reply via email to