-
Notifications
You must be signed in to change notification settings - Fork 505
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
8349091: Charts: exception initializing in a background thread #1697
Conversation
👋 Welcome back angorya! A progress list of the required criteria for merging this PR into |
@andy-goryachev-oracle This change now passes all automated pre-integration checks. ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details. After integration, the commit message for the final commit will be:
You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed. At the time when this comment was updated there had been no new commits pushed to the ➡️ To integrate this PR with the above commit message to the |
@andy-goryachev-oracle |
Reviewers: @kevinrushforth @azuev-java |
tests/system/src/test/java/test/robot/javafx/scene/NodeInitializationStressTest.java
Show resolved
Hide resolved
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. |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I still think the code would be cleaner with two properties, and the extra memory is not significant, what you have will work, so I won't object.
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() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hjohn could you help here please? How could we use Subscription in a situation when it has to be unsubscribed from within the lambda?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't been tracking these fixes for allowing initialization in background threads, but it seems to me that basically anything should be allowed as long as you're not part of a visible scene graph -- and I think there's also no expectation that all functionality of a control "works" as long as it is not yet part of such a graph (ie. the listeners are only needed once it is part of a scene graph).
If you make listeners conditional on being part of a scene graph, then I think you can handle these with a single code path. Such a condition would be:
ObservableValue<Boolean> partOfSceneGraph = node.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
.orElse(false);
// The node here is "this", the chart, because as soon as it is part of the scene
// graph, you want the listeners to function.
You can then safely install any listeners directly, regardless if they're on a background thread or not:
someProperty().when(partOfSceneGraph).subscribe( ... );
Or:
someProperty().when(partOfSceneGraph).addListener( ... );
Or with any mappings added, you can even pick where you put the when
:
someProperty().flatMap(xyz).when(partOfSceneGraph).addListener( ... );
someProperty().when(partOfSceneGraph).flatMap(xyz).addListener( ... );
On a background thread, partOfSceneGraph
will be false
, and no listener gets installed (yet). As soon as it becomes true
all listeners with that same condition become active. It becomes true
automatically when the node has a scene belonging to a window that is visible. Vice versa, if it ever becomes false
again, (which it may when the Node is removed or replaced) all listeners using this condition are removed again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
This is not what I asked though: my question was about using Subscription
in one-off case, and more specifically, about whether it is even possible to unsubscribe from within the lambda.
What you are proposing is a slightly more involved change: for example, registering and unregistering listeners each time might introduce other issues (especially since we have no way to prioritize listeners/handlers). It also complicates the management of skin (as skins can be added/removed), something I would rather avoid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for this current code:
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
}
});
What you want there is not possible in this way as you can't use this
in a lambda, nor refer to it via a local. You can achieve it only if you store the subscription in a field, not in a local. Assigning the Lambda to a ChangeListener local, and using a ChangeListener may be simpler (unless you go for the when
solution that may eliminate the need for 2 different code paths).
Note that there is no need to store winProp
; flatMap
does not use weak listeners (when in active use), and so as long as sceneProperty()
exists, the derived property via flatMap
will also exist, and also its listener. The way the fluent mapping system works is that listeners are only present when something is actually listening:
ObservableValue<Window> winProp = sceneProperty().flatMap(Scene::windowProperty);
// ^^ nobody is listening, so no listeners installed and no hard references; it could GC but
// it is currently in a local, so it won't
ObservableValue<Window> winProp = sceneProperty().flatMap(Scene::windowProperty);
winProp.subscribe(v -> { .. });
// ^^ somebody is listening; all listeners get installed automatically, and now there are
// hard references. No risk of GC.
You can then write it as a single statement which won't be GC'd as long as the subscription is active:
subscriptionFIeld = sceneProperty()
.flatMap(Scene::windowProperty);
.subscribe(v -> {
// use subscription field here if you want to unregister the lambda
// Note: when you do, there will again be no listeners and thus no hard references and
// thus the intermediate property created by flatMap can now be GC'd :)
});
symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty()); | ||
symbol.setFocusTraversable(isAccessibilityActive()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, if I understand correctly, we can't directly do this bind
:
symbol.focusTraversableProperty().bind(Platform.accessibilityActiveProperty())
Because on a background thread there may not yet be an initialized Platform
? Or perhaps, Platform
would need to initialize and we don't want to do that yet on a background thread?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason we should not
be binding in this PR is because in part it's bad design(tm): we are creating a zillion of listeners (one per plot data point); a better solution would be to create one listener and use (already existing series) to toggle the data points.
The other reason is https://bugs.openjdk.org/browse/JDK-8351067 - the Platform::accessibilityActive
property getter is not thread safe, but even if was, registering a listener with it may cause the change to come in before the node becomes a part of the scene graph, leading to unsynchronized multi-threaded access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was this solution considered:
ObservableValue<Boolean> partOfSceneGraph = this.sceneProperty()
.flatMap(Scene::windowProperty)
.flatMap(Window::showingProperty)
.orElse(false);
symbol.focusTraversableProperty()
.bind(Platform.accessibilityActiveProperty().when(partOfSceneGraph));
What the above code does is create a binding on the when
statement (a listener is added on the when
result). However, the when
property will not add a listener on Platform.accessibilityActiveProperty()
until partOfSceneGraph
is true
. As soon as it does though, a listener is installed on Platform.accessibilityActiveProperty()
and its latest value is set to focusTraversableProperty
via the bind.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The above does require though that Platform.accessibilityActiveProperty()
is properly synchronized. I think it may be a good idea to fix that first. Perhaps all platform provided properties (if there are more) should ensure they're only initialized once.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated, but I would rather disallow background access to any platform properties.
per the earlier email, this might (read: will) create concurrent access when the node is not yet attached to the scene graph:
background thread fx app thread
1. start creating the node
2. add listener to platform prop
3. keep initializing the node <-- platform property change notification
4. some more code
step 3 is when concurrent access occurs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but that's because there is no synchronization. Here is a version that does do synchronization; I see no more exceptions (and it runs just as fast really):
public class PropTest {
public static void main(String[] args) {
Thread mainThread = Thread.currentThread();
final var prop = new SimpleBooleanProperty(false) {
@Override
public synchronized void addListener(ChangeListener<? super Boolean> listener) {
super.addListener(listener);
}
@Override
public synchronized void set(boolean newValue) {
super.set(newValue);
}
};
// add listeners in a background thread
var thr = new Thread(() -> {
for (int i = 0; i < 1000000; i++) {
ChangeListener<Boolean> listener = (obs, o, n) -> {
if (!mainThread.equals(Thread.currentThread())) {
System.err.println("***** ERROR: listener called on wrong thread: " + Thread.currentThread());
}
};
prop.addListener(listener);
}
});
thr.start();
// Fire events in main thread
for (int jj = 0; jj < 1000000; jj++) {
prop.set(!prop.get());
}
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course for a generic solution, we could provide a wrapper for properties, or custom synchronized properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, using a custom property, or a wrapper, would solve this particular synchronization problem, although that wouldn't solve all of the problems.
We would then be left with the problem that if the accessibility change listener did fire -- which necessarily happens on the FX app thread -- while the object was being set up on a background thread, the listener might try to modify properties that are being touched on the background thread. This is just another case of a more general problem we already have with animation, and which Andy fixed by not starting animation in a few places unless and until we are on the FX app thread.
So I think the solution Andy has chosen of deferring adding the listener is better than trying to fix the accessibility property, followed by fixing the listeners that use it, to be thread-safe.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, @kevinrushforth .
Getting back to the rule of allowing Node construction from a background thread but not "use", should we consider enforcing the thread access rules for global and static objects (like Platform properties)?
In other words, constructing objects that might modify internal properties is ok, but trying to access shared objects is not?
(maybe it's time to move the discussion out of this PR into the mailing list perhaps?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enforcing the threading restriction when using properties is a larger conversation. If we want to have it, I agree it should be on the mailing list.
boolean aa = Platform.accessibilityActiveProperty().get(); | ||
SimpleBooleanProperty active = new SimpleBooleanProperty(aa); | ||
accessibilityActive = active; | ||
active.bind(Platform.accessibilityActiveProperty()); | ||
active.addListener((src, prev, on) -> { | ||
handleAccessibilityActive(on); | ||
}); | ||
return active.get(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused as to what is happening here. A boolean property active
is created, which is bound to Platform.accessibilityActiveProperty
(this creates a listener on accessibilityActiveProperty
). We add then another listener on active
.
What confuses me is what this is supposed to achieve, and also why we're initialising the property, but then bind it anyway... active = new SimpleBooleanProperty(aa)
and later active.bind
means that the variable aa
is completely unnecessary as the bind
will do this get
for you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
aa is unnecessary, you are right, bind() will set the value.
the property is created as a way to signal any subsequent calls that no more work is needed.
Modified to use Subscription (thanks, @hjohn !) Check out the code sample which exercises the scenario. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll do some more testing, but this looks OK to me.
.flatMap(Scene::windowProperty) | ||
.subscribe((w) -> { | ||
if (w != null) { | ||
// also unsubscribes when appears in a window |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion: Add a comment that this can only happen on the FX app thread, since you are relying on that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added comment
@@ -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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I still think the code would be cleaner with two properties, and the extra memory is not significant, what you have will work, so I won't object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All my testing looks good.
I finished testing of the last iteration and i do not see any regressions in the accessibility on Mac or Windows - seems to be working fine on both platforms. |
@hjohn thank you for help with |
No, but admittedly I did not look beyond the subscription issue. I think Kevin and Alexander have got you covered on this one :) |
/integrate |
Going to push as commit fc770fb. |
@andy-goryachev-oracle Pushed as commit fc770fb. 💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored. |
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 toPlatform.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
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.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
Issue
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