Use ScenePulseListener to avoid expensive recalculations?
Martin Sladecek
martin.sladecek at oracle.com
Thu Nov 7 04:20:39 PST 2013
On 11/07/2013 11:58 AM, John Hendrikx wrote:
> On 7/11/2013 09:20, Martin Sladecek wrote:
>> On 11/06/2013 07:31 PM, John Hendrikx wrote:
>>> On 5/11/2013 20:10, Jonathan Giles wrote:
>>>> You're right in that controls don't tend to use ScenePulseListener.
>>>> This
>>>> may be due to an oversight on our part, or just that our requirements
>>>> differ (I don't honestly know).
>>>>
>>>> You're also right that it is important to batch up property changes
>>>> and
>>>> do them once per pulse, rather than as they come in. If this is not
>>>> done
>>>> you do indeed get a severe performance hit (I remember back in the day
>>>> before I optimised many of the controls in this way, the performance
>>>> impact of not doing this was staggering).
>>>>
>>>> The way I do it is simple: in places where I receive events from
>>>> properties / bindings / listeners, where I know that there is
>>>> potentially a lot of changes coming through, I create a boolean (e.g.
>>>> 'refreshView' or somesuch). I then set this boolean to true, and in
>>>> the
>>>> appropriate place in the code (most commonly layoutChildren(), but
>>>> sometimes in the computePref*() methods), I start the method with code
>>>> like this:
>>>>
>>>> if (refreshView) {
>>>> doExpensiveOperationThatShouldHappenOnlyOncePerPulse();
>>>> refreshView = false;
>>>> }
>>>>
>>>> The reason why I sometimes use layoutChildren() and sometimes the
>>>> computePref*() methods comes down to the (new) rule in JavaFX 8.0 that
>>>> you can not change the children list of a node within the
>>>> layoutChildren() method. Because of this, if I need to modify the
>>>> children list, I put the above code in the computePref*() method.
>>>
>>> I'm not sure in which circumstances layoutChildren() will be called
>>> or if I can mark my control as needing layout, since the properties
>>> I want to trigger on are not directly attached to anything that
>>> JavaFX will be monitoring. The ScenePulseListener therefore seems
>>> like a good choice -- the reason I'm asking about is that I cannot
>>> find any examples and the documentation for it is also pretty lean.
>> If you call requestLayout(), the layoutChildren() will be called on
>> the next pulse. The ScenePulseListener is not a public API and the
>> layout is the only way how Controls can react on pulse. Can you be
>> more specific on what you need to do on pulse? If it's not related to
>> the layout, maybe it would be reasonable to introduce some API that
>> would allow you to execute code on each pulse.
>>
> Hm, I found it googling, and since it showed up here:
>
> http://docs.oracle.com/javafx/2/api/javafx/scene/Scene.ScenePulseListener.html
>
>
> I figured it was public, but I just noticed the class is defined
> package private.
>
> Anyway, what I need to do:
>
> 1) I have two listeners that keep an eye on two properties,
> expandTopLevel and nodes:
>
> expandTopLevel.addListener(new InvalidationListener() {
> @Override
> public void invalidated(Observable observable) {
> buildTree();
> }
> });
>
> mediaNodes.addListener(new ListChangeListener<MediaNode>() {
> @Override
> public void onChanged(ListChangeListener.Change<? extends
> MediaNode> change) {
> buildTree();
> }
> });
>
> 2) buildTree() will call TreeView#setRoot() directly/indirectly in
> both cases, either with the full list of nodes or a subtree, depending
> on expandTopLevel state. The other possible subtree choices are moved
> into a tab-like control, which will have either 1 tab (if not
> expandTopLevel) or multiple tabs (one for each top level node).
>
> Now, TreeView itself might be smart enough now to actually draw the
> sets of Nodes twice when both properties change, however buildTree
> will still get called twice, which itself is doing quite some work
> (although not enough to move it off the event thread), but also can
> cause things like selection to change.
>
> None of this will affect the size of these controls, which I assumed
> would mean layoutChildren() does not get called (definitely not on the
> Pane containing both the tab-like control and the TreeView control).
> Also, if I donot call buildTree, but "wait", I'd at the very least
> would need to change something to trigger a layout -- requestLayout()
> would work me there.
The layout is not only about different preferred size. In your custom
Control, stable preferred size just means, the parent of this control
doesn't need to relayout. But internally, I would recommend you to
modify the properties of children in layoutChildren() method. This
means, instead of buildTree, call requestLayout() and call buildTree()
inside of layoutChildren(). Since, as you say, no changes to preferred
size will happen, you don't need to do any special computation in
compute* methods.
-Martin
>
> --John
>
>> -Martin
>>
>>>
>>> I'll experiment with a ScenePulseListener and see how it pans out,
>>> it looks like it would be an elegant solution.
>>>
>>> Thanks Jonathan!
>>>
>>>
>>>>
>>>> I hope that helps.
>>>>
>>>> -- Jonathan
>>>>
>>>> On 6/11/2013 3:58 a.m., John Hendrikx wrote:
>>>>> Hi List,
>>>>>
>>>>> I'm considering using a ScenePulseListener to avoid expensive
>>>>> recalculations when multiple bindings trigger an action.
>>>>>
>>>>> My problem is this:
>>>>>
>>>>> I like to build Views (Controls) that are dumb and expose properties
>>>>> that can be manipulated by whatever is using the View. The View
>>>>> listens to its own Properties and adjusts its state accordingly.
>>>>> Some
>>>>> of these adjustments are related (like width + height) and can be
>>>>> expensive when calculated immediately. So I would like to mark the
>>>>> View as "invalid" and recalculate its state (if invalid) on the next
>>>>> Pulse.
>>>>>
>>>>> My current use case I'm looking at is a View that wraps (amongst
>>>>> others) a TreeView. The View exposes an ObservableList and a
>>>>> BooleanProperty that decides whether the first level of the Tree
>>>>> should be displayed as Tabs or as Nodes (which has an impact on what
>>>>> Nodes actually are added to the TreeView, and which are added as
>>>>> Tabs). User code will thus often set a new list of nodes + change
>>>>> the
>>>>> boolean to show tabs or nodes. The View currently naively has
>>>>> InvalidationListeners on both of these properties which cause
>>>>> TreeNodes to be created after the first change... then discarded and
>>>>> recreated after the second change by the user code, ie:
>>>>>
>>>>> view.nodesProperty().setAll(nodes); // Recreates all
>>>>> Tabs/TreeNodes with the current value of expand top level, as we
>>>>> donot
>>>>> know another change might be incoming...
>>>>> view.expandTopLevelProperty().set(false); // Recreates all
>>>>> Tabs/TreeNodes again if expand top level was changed...
>>>>>
>>>>> This specific problem might be done in a better way, but the point
>>>>> remains, how do I avoid expensive calculations when multiple
>>>>> properties get changed one after the other by the user code? I'm
>>>>> assuming that JavaFX controls already avoid these kinds of things,
>>>>> and
>>>>> I'd like to know whether using a ScenePulseListener is the way to go,
>>>>> or that it can/should be done in a different way -- examining the
>>>>> code
>>>>> for TreeView (and its superclasses), I couldn't find uses of
>>>>> ScenePulseListener...
>>>>>
>>>>> --John
>>>>>
>>>>>
>>>>>
>>>>>
>>>
>>
>
More information about the openjfx-dev
mailing list