Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8349091: Charts: exception initializing in a background thread #1697

Open
wants to merge 25 commits into
base: master
Choose a base branch
from

Conversation

andy-goryachev-oracle
Copy link
Contributor

@andy-goryachev-oracle andy-goryachev-oracle commented Feb 6, 2025

Root Cause:
(Multiple) properties are getting bound to the global Platform.accessibilityActive property. Binding (and I say, accessing) of properties is not thread-safe.

I also changed the design a bit. Originally, every symbol in a chart had its focusTraversableProperty bound to Platform.accessibilityActive in order to enable the accessibility navigation across the chart data points. This is rather inefficient, as the property has to manage (thousands?) of listeners.

Instead, a single boolean property is added to each chart, with a listener added to it which iterates over data symbols to toggle the focusTraversableProperty directly.

The exact moment when the new property gets bound is also important, and has to happen in the FX application thread.

With this change, it is safe to create and populate charts with data in a background thread.


NOTES

  1. It looks like the Platform.accessibilityActive property never transitions back to false after it transitioned to true. Some say it is because there is no mechanism in the platform to get notified which cannot possibly be true.
  2. The javadoc for Platform.accessibilityActiveProperty() method stipulates that "This method may be called from any thread" which is patently not true as the current implementation is simply not thread-safe.

Note to the Reviewers

To avoid merge conflicts, the preferred order of integrations:

#1697
#1713
#1717

/reviewers 2


Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8349091: Charts: exception initializing in a background thread (Bug - P4)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jfx.git pull/1697/head:pull/1697
$ git checkout pull/1697

Update a local copy of the PR:
$ git checkout pull/1697
$ git pull https://git.openjdk.org/jfx.git pull/1697/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 1697

View PR using the GUI difftool:
$ git pr show -t 1697

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jfx/pull/1697.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Feb 6, 2025

👋 Welcome back angorya! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Feb 6, 2025

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@andy-goryachev-oracle andy-goryachev-oracle marked this pull request as ready for review February 6, 2025 19:49
@openjdk openjdk bot added the rfr Ready for review label Feb 6, 2025
@openjdk
Copy link

openjdk bot commented Feb 6, 2025

@andy-goryachev-oracle
The total number of required reviews for this PR (including the jcheck configuration and the last /reviewers command) is now set to 2 (with at least 1 Reviewer, 1 Author).

@mlbridge
Copy link

mlbridge bot commented Feb 6, 2025

Webrevs

@kevinrushforth kevinrushforth self-requested a review February 6, 2025 20:13
@kevinrushforth
Copy link
Member

Reviewers: @kevinrushforth @azuev-java

@azuev-java
Copy link
Member

Ok, changes look good and they seems to work on Mac OS. I am curious why do we make labels in charts focus traversable when a11y is switched on? May be something to do with accessibility on Windows? I haven't tested this patch on Windows yet. On Mac OS user can move accessibility cursor to non-focusable elements such as text labels and images so we should not have a problem navigating to the chart labels. Needs to be investigated more.

@andy-goryachev-oracle
Copy link
Contributor Author

I am curious why do we make labels in charts focus traversable when a11y is switched on?

I think the reason is to announce the axis values for each data point (that's what the VoiceOver announces on macOS for me) - you can readily try it in the MonkeyTester.

@andy-goryachev-oracle
Copy link
Contributor Author

andy-goryachev-oracle commented Feb 12, 2025

Sorry, forgot to enable the pie chart test.
On a positive note, I've tested the last commit in the cumulative branch
https://github.com/andy-goryachev-oracle/jfx/tree/ag.thread.safe.init
which includes all the fixes, the heap size lowered from 1000m to 128m, and the test duration increased 3x to 15 seconds.

@andy-goryachev-oracle
Copy link
Contributor Author

Hi @kevinrushforth and @azuev-java , could you please re-approve this PR to unblock two more?

Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is heading in the right direction, but has some bugs that will need to be fixed.

@@ -541,7 +543,7 @@ private Node createSymbol(Series<X,Y> series, int seriesIndex, final Data<X,Y> i
symbol = new StackPane();
symbol.setAccessibleRole(AccessibleRole.TEXT);
symbol.setAccessibleRoleDescription("Point");
symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty());
symbol.setFocusTraversable(isAccessibilityActive());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you are mixing the setting up of the listeners with setting the focus traversable of this symbol to the current state of accessibilityActive, which will be Platform.accessibilityActiveProperty for the FX app thread) and "false" (for background thread. As a result, this method, which is called once per symbol, will set up a new listener each time it is called.

You might consider refactoring this a bit to only ever set up the listeners when the object is constructed. Either that or ensure that you guard against repeatedly recreating the listener.

* @return
*/
// package protected: custom charts must handle accessbility on their own
boolean isAccessibilityActive() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be final, although as I mentioned in an earlier comment, it could probably use to be refactored.

Comment on lines 550 to 551
@Override
public void changed(ObservableValue<? extends Scene> src, Scene prev, Scene scene) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is insufficient. It will miss the case where both the node and the scene are created in the background thread, and the scene is later added to a window on the FX app thread. I tested this case and verified my assertion. This also means that in some cases, if this listener is called from a background thread, you will create a new scene listener from the current scene listener.

Consider instead using TreeShowingProperty, which is used by ProgressIndicatorSkin to set up its animation.TreeShowingProperty sets up a listener chain that fires when the the "tree showing" status of a node changed (tree showing means the node and all ancestors are visible and is attached to a scene that is attached to a window that is showing.

Alternatively, since you might not care whether the node is visible, you can just set up a binding using ObservableValue::flatMap. The code snippet of the docs:

ObservableValue<Boolean> isShowing = sceneProperty()
    .flatMap(Scene::windowProperty)
    .flatMap(Window::showingProperty)
    .orElse(false);

And then you can then add a listener (or a value subscription might be better) on isShowing -- when it becomes true, you can setup the binding or listener to Platform.accessibilityActiveProperty.

With either of the above two solutions, the showing state will only become true on the FX app thread.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for noticing! the old code was indeed flawed.

@kevinrushforth kevinrushforth self-requested a review February 26, 2025 14:23
Copy link
Member

@kevinrushforth kevinrushforth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What you have now works in all cases I've tried. I left a couple suggestions and will reapprove if you decide to make changes.

@@ -104,6 +102,9 @@ public abstract class Chart extends Region {
/** Animator for animating stuff on the chart */
private final ChartLayoutAnimator animator = new ChartLayoutAnimator(chartContent);

// SimpleBooleanProperty or ObjectBinding
private volatile Object accessibilityActive;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use ObservableValue<?> instead of Object as the type. Alternatively, use two fields, a SimpleBooleanProperty for use by the FX app thread and an ObjectBinding for use by a background thread. They wouldn't need to be volatile in that case. What you have is OK, but using two properties might simplify the logic a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a design decision - I won't want to waste an extra pointer.
The cpu cycles are much cheaper nowadays than bytes.
Extra bytes cost much more in cpu cycles (gc) and electricity.

ObservableValue<Window> winProp = sceneProperty().flatMap(Scene::windowProperty);
accessibilityActive = winProp; // keep the reference so it won't get gc

// lambda cannot be used in place of a ChangeListener in removeListener()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a Subscription then? It seems tailor-made for what you are trying to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to use Subscription in this case.
This does not work:

                ObservableValue<Window> winProp = sceneProperty().flatMap(Scene::windowProperty);
                accessibilityActive = winProp; // keep the reference so it won't get gc
                Subscription sub = winProp.subscribe((win) -> {
                    if (win != null) {
                        if (accessibilityActive == winProp) {
                            accessibilityActive = null;
                        }
                        if (isAccessibilityActive()) {
                            handleAccessibilityActive(true);
                        }
                        //winProp.removeListener(this);
                        sub.unsubscribe(); <-- COMPILE ERROR
                    }
                });

@kevinrushforth kevinrushforth self-requested a review March 3, 2025 14:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
rfr Ready for review
Development

Successfully merging this pull request may close these issues.

3 participants